Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/deploy-worker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ jobs:
- name: Build
run: pnpm build

- name: Deploy product router
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
run: |
for attempt in 1 2 3; do
pnpm deploy:product && exit 0
sleep $((attempt * 5))
done
pnpm deploy:product

- name: Apply D1 migrations
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Fix GitHub OAuth login after the canonical host move by honoring the registered `crabfleet.openclaw.ai` callback URL.
- Add the Crabfleet v2 fleet-control spec, redacted fleet registry API, and dashboard summary for visible Codex crabboxes.
- Make `crabfleet.openclaw.ai` the OpenClaw app/API canonical URL, redirect old OpenClaw aliases there, and keep `crabfleet.ai` independent as the public product site.
- Deploy the source-controlled `crabfleet.ai` product router before the app Worker so product traffic cannot drift back to an app redirect.
- Route the built-in interactive provision hook in-process and default new sessions to Cloudflare Sandbox so production creates usable Codex terminals without a crabbox adapter.
- Keep Cloudflare Sandbox model and GitHub credentials in the Worker path, add DO-backed sandbox credential/checkpoint state, and add CLI lifecycle commands for doctor/status/stop/checkpoint/restore.
- Show failed and expired Codex sessions as stable log replays instead of remounting Ghostty terminals.
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,26 @@ merge:
### Deploy

Pushes to `main` run `.github/workflows/deploy-worker.yml`, which checks, tests, builds,
applies remote D1 migrations, and deploys the Worker. Configure the repository secret
`CLOUDFLARE_API_TOKEN` with permissions for Workers deploys and D1 migrations.
deploys the generic product router, applies remote D1 migrations, and deploys the app
Worker. Configure the repository secret `CLOUDFLARE_API_TOKEN` with permissions for
Workers deploys and D1 migrations.
`crabfleet.ai` product routing, `crabfleet.openclaw.ai`, and `crabd.sh` DNS/route
convergence is handled by `scripts/ensure-cloudflare-domains.mjs`; set
`CLOUDFLARE_DNS_API_TOKEN` when CI should manage those records. Without that
DNS-scoped token, CI skips domain convergence. The app Worker still proxies the generic
product site for `crabfleet.ai` as a defensive fallback, never the authenticated app.
The product router source and deploy configuration live in `src/product-router.ts` and
`wrangler.product.jsonc`.

Manual deploy is still available:

```bash
# Build assets
pnpm build

# Deploy the generic product router
pnpm deploy:product

# Apply migrations
wrangler d1 migrations apply DB --remote

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"scripts": {
"build": "node scripts/generate-assets.mjs && tsgo --noEmit",
"build:static": "node scripts/generate-assets.mjs --static",
"deploy": "pnpm build && wrangler d1 migrations apply DB --remote && wrangler deploy",
"deploy": "pnpm build && pnpm deploy:product && wrangler d1 migrations apply DB --remote && wrangler deploy",
"deploy:product": "wrangler deploy --config wrangler.product.jsonc",
"test": "node --test --experimental-strip-types tests/*.test.ts",
"lint": "oxlint --ignore-pattern node_modules --ignore-pattern src/generated.ts .",
"format": "oxfmt --check .",
Expand Down
15 changes: 15 additions & 0 deletions src/canonical-host.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const appCanonicalHost = "crabfleet.openclaw.ai";
export const appCanonicalOrigin = `https://${appCanonicalHost}`;
const productCanonicalHost = "crabfleet.ai";
const productCanonicalOrigin = `https://${productCanonicalHost}`;
const productOriginHost = "crabbox.sh";
const productHosts = new Set([productCanonicalHost, `www.${productCanonicalHost}`]);
export const appRedirectHosts = new Set([
Expand Down Expand Up @@ -62,6 +63,20 @@ export async function productHostResponse(
});
}

export async function routeProductRequest(
request: Request,
fetcher: typeof fetch = fetch,
): Promise<Response> {
const productResponse = await productHostResponse(request, fetcher);
if (productResponse) return productResponse;

const source = new URL(request.url);
const target = new URL(productCanonicalOrigin);
target.pathname = source.pathname;
target.search = source.search;
return Response.redirect(target.toString(), 308);
}

export function canonicalAppRedirect(url: URL): Response | null {
if (!appRedirectHosts.has(url.hostname)) return null;
// Existing CLI, agent, and terminal clients may attach Authorization headers.
Expand Down
7 changes: 7 additions & 0 deletions src/product-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { routeProductRequest } from "./canonical-host";

export default {
async fetch(request: Request): Promise<Response> {
return routeProductRequest(request);
},
};
13 changes: 12 additions & 1 deletion tests/canonical-host.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { canonicalAppRedirect, productHostResponse } from "../src/canonical-host.ts";
import {
canonicalAppRedirect,
productHostResponse,
routeProductRequest,
} from "../src/canonical-host.ts";

test("product hosts never fall through to the app worker", async () => {
let upstreamRequest: Request | undefined;
Expand Down Expand Up @@ -32,6 +36,13 @@ test("product www host redirects to the product apex", async () => {
assert.equal(response?.headers.get("location"), "https://crabfleet.ai/docs?mode=full");
});

test("product aliases redirect to the product apex", async () => {
const response = await routeProductRequest(new Request("https://crabfleet.app/docs?mode=full"));

assert.equal(response.status, 308);
assert.equal(response.headers.get("location"), "https://crabfleet.ai/docs?mode=full");
});

test("legacy app pages redirect to the canonical host", () => {
const response = canonicalAppRedirect(
new URL("https://clawfleet.openclaw.ai/app/sessions/IS-1?view=grid"),
Expand Down
2 changes: 1 addition & 1 deletion wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
],
"workers_dev": true,
// The canonical app/API host and legacy OpenClaw aliases are Worker Custom
// Domains. The public crabfleet.ai product site is managed separately.
// Domains. The public crabfleet.ai product site uses wrangler.product.jsonc.
"routes": [
{ "pattern": "crabfleet.openclaw.ai", "custom_domain": true },
{ "pattern": "clawfleet.openclaw.ai", "custom_domain": true },
Expand Down
11 changes: 11 additions & 0 deletions wrangler.product.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "crabfleet-canonical-router",
"main": "src/product-router.ts",
"compatibility_date": "2026-06-11",
"account_id": "91b59577e757131d68d55a471fe32aca",
"workers_dev": false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Source-control the product alias route

With workers_dev disabled and no routes/custom domains in this product config, the new deploy only uploads the crabfleet-canonical-router script; it does not make the added crabfleet.app alias testable in production. I checked the domain convergence script and it only iterates crabfleet.ai and www.crabfleet.ai, so requests to crabfleet.app will still depend on an out-of-band Cloudflare route and can drift or be absent even after this workflow succeeds.

Useful? React with 👍 / 👎.

"observability": {
"enabled": true,
},
}