From cc43db2a580b6d5b34d05e266f8668dc3c5bea99 Mon Sep 17 00:00:00 2001
From: clibequilibrium <105135724+clibequilibrium@users.noreply.github.com>
Date: Wed, 7 Oct 2026 20:09:37 -0400
Subject: [PATCH 1/8] Console drops the hosting agent
Nobody deploys apps through managed hosting, and Workers for Platforms is
a paid dependency we no longer want to carry, so the console stops
speaking to it: the registry entry and sidebar section, the hosting API
proxy and OpenAPI module, the GitHub App push-to-deploy flow, deploy
tokens (cfbd_) and the Actions OIDC bearer in the guard, and the D1
tables they owned. Existing app/deploy_token/github_* tables are left
in place untouched; nothing reads them any more.
The e2e stack loses its :8800 hosting server; the landing page lists
four live primitives instead of five.
---
.prettierignore | 7 -
e2e/deploy-token.api.spec.ts | 163 ----
e2e/github-connect.api.spec.ts | 213 -----
e2e/helpers.ts | 83 --
e2e/hosting-env.api.spec.ts | 270 ------
e2e/hosting-real.api.spec.ts | 135 ---
e2e/hosting.api.spec.ts | 367 --------
e2e/hosting.ui.spec.ts | 146 ----
e2e/landing.ui.spec.ts | 2 +-
e2e/service-keys.api.spec.ts | 3 +-
eslint.config.js | 1 -
package.json | 4 +-
playwright.config.ts | 7 -
scripts/local-dev.json | 6 +-
scripts/postinstall.json | 4 -
src/app.d.ts | 27 -
src/hooks.server.ts | 105 +--
src/lib/agent-registry.ts | 8 +-
src/lib/agents.ts | 322 --------
src/lib/branch-match.ts | 53 --
src/lib/branch-match.unit.test.ts | 49 --
src/lib/components/console-shell.svelte | 8 +-
src/lib/components/github-mark.svelte | 24 -
src/lib/hosting-workflow.ts | 330 --------
src/lib/hosting-workflow.unit.test.ts | 162 ----
src/lib/openapi/hosting.ts | 470 -----------
src/lib/openapi/index.ts | 11 +-
src/lib/openapi/shared.ts | 2 +-
src/lib/server/db/index.ts | 55 +-
src/lib/server/db/schema.ts | 172 +---
src/lib/server/frameworks.ts | 314 -------
src/lib/server/frameworks.unit.test.ts | 222 -----
src/lib/server/github-connect.ts | 781 ------------------
src/lib/server/github-deploy.ts | 245 ------
src/lib/server/github-oidc.ts | 167 ----
src/lib/server/github-repo.ts | 479 -----------
src/lib/server/github.ts | 365 --------
src/lib/server/hosting.ts | 422 ----------
src/lib/server/registry.ts | 9 -
src/lib/server/service-keys.ts | 6 +-
src/params/hostingapp.ts | 9 -
.../(app)/dashboard/(account)/+page.svelte | 2 +-
.../dashboard/[projectId]/+layout.svelte | 3 +-
.../(app)/dashboard/[projectId]/+page.svelte | 2 +-
.../[projectId]/hosting/+page.server.ts | 70 --
.../[projectId]/hosting/+page.svelte | 771 -----------------
.../[[tab=hostingapp]]/+page.server.ts | 92 ---
.../[appName]/[[tab=hostingapp]]/+page.svelte | 706 ----------------
.../hosting/connect-github-dialog.svelte | 497 -----------
.../[projectId]/hosting/deploys-table.svelte | 125 ---
.../[projectId]/hosting/vars-editor.svelte | 256 ------
.../[projectId]/settings/+page.svelte | 4 +-
src/routes/(marketing)/+page.svelte | 7 +-
src/routes/+layout.svelte | 4 +-
src/routes/api/github/callback/+server.ts | 91 --
src/routes/api/github/webhook/+server.ts | 59 --
.../hosting/apps/[appName]/+server.ts | 64 --
.../apps/[appName]/analytics/+server.ts | 24 -
.../apps/[appName]/build-env/+server.ts | 50 --
.../[appName]/build-secrets/[name]/+server.ts | 54 --
.../apps/[appName]/build-vars/+server.ts | 29 -
.../hosting/apps/[appName]/deploys/+server.ts | 68 --
.../hosting/apps/[appName]/secrets/+server.ts | 44 -
.../apps/[appName]/secrets/[name]/+server.ts | 25 -
.../hosting/apps/[appName]/vars/+server.ts | 47 --
.../[projectId]/hosting/claims/+server.ts | 63 --
.../[projectId]/hosting/deploys/+server.ts | 14 -
.../[projectId]/hosting/github/+server.ts | 26 -
.../hosting/github/connections/+server.ts | 36 -
.../github/connections/[appName]/+server.ts | 40 -
.../hosting/github/install/+server.ts | 50 --
.../hosting/github/repos/+server.ts | 64 --
.../[projectId]/hosting/overview/+server.ts | 12 -
.../[projectId]/hosting/tokens/+server.ts | 26 -
.../hosting/tokens/[tokenId]/+server.ts | 13 -
src/worker-configuration.d.ts | 10 +-
wrangler.e2e.jsonc | 1 -
wrangler.jsonc | 7 +-
78 files changed, 38 insertions(+), 9646 deletions(-)
delete mode 100644 e2e/deploy-token.api.spec.ts
delete mode 100644 e2e/github-connect.api.spec.ts
delete mode 100644 e2e/hosting-env.api.spec.ts
delete mode 100644 e2e/hosting-real.api.spec.ts
delete mode 100644 e2e/hosting.api.spec.ts
delete mode 100644 e2e/hosting.ui.spec.ts
delete mode 100644 src/lib/branch-match.ts
delete mode 100644 src/lib/branch-match.unit.test.ts
delete mode 100644 src/lib/components/github-mark.svelte
delete mode 100644 src/lib/hosting-workflow.ts
delete mode 100644 src/lib/hosting-workflow.unit.test.ts
delete mode 100644 src/lib/openapi/hosting.ts
delete mode 100644 src/lib/server/frameworks.ts
delete mode 100644 src/lib/server/frameworks.unit.test.ts
delete mode 100644 src/lib/server/github-connect.ts
delete mode 100644 src/lib/server/github-deploy.ts
delete mode 100644 src/lib/server/github-oidc.ts
delete mode 100644 src/lib/server/github-repo.ts
delete mode 100644 src/lib/server/github.ts
delete mode 100644 src/lib/server/hosting.ts
delete mode 100644 src/params/hostingapp.ts
delete mode 100644 src/routes/(app)/dashboard/[projectId]/hosting/+page.server.ts
delete mode 100644 src/routes/(app)/dashboard/[projectId]/hosting/+page.svelte
delete mode 100644 src/routes/(app)/dashboard/[projectId]/hosting/apps/[appName]/[[tab=hostingapp]]/+page.server.ts
delete mode 100644 src/routes/(app)/dashboard/[projectId]/hosting/apps/[appName]/[[tab=hostingapp]]/+page.svelte
delete mode 100644 src/routes/(app)/dashboard/[projectId]/hosting/connect-github-dialog.svelte
delete mode 100644 src/routes/(app)/dashboard/[projectId]/hosting/deploys-table.svelte
delete mode 100644 src/routes/(app)/dashboard/[projectId]/hosting/vars-editor.svelte
delete mode 100644 src/routes/api/github/callback/+server.ts
delete mode 100644 src/routes/api/github/webhook/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/apps/[appName]/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/apps/[appName]/analytics/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-env/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-secrets/[name]/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-vars/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/apps/[appName]/deploys/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/apps/[appName]/secrets/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/apps/[appName]/secrets/[name]/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/apps/[appName]/vars/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/claims/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/deploys/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/github/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/github/connections/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/github/connections/[appName]/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/github/install/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/github/repos/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/overview/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/tokens/+server.ts
delete mode 100644 src/routes/api/projects/[projectId]/hosting/tokens/[tokenId]/+server.ts
diff --git a/.prettierignore b/.prettierignore
index 402facbc..eab7b1d8 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -2,29 +2,24 @@
src/lib/components/ui/
agents/auth/drizzle/meta/
agents/db/drizzle/meta/
-agents/hosting/drizzle/meta/
agents/storage/drizzle/meta/
# Regenerated by drizzle-kit; formatting it here is undone on the next generate.
agents/auth/drizzle/migrations.js
agents/db/drizzle/migrations.js
-agents/hosting/drizzle/migrations.js
agents/storage/drizzle/migrations.js
**/worker-configuration.d.ts
AGENTS.md
agents/auth/AGENTS.md
agents/db/AGENTS.md
-agents/hosting/AGENTS.md
agents/storage/AGENTS.md
**/package-lock.json
agents/auth/.prettierrc
agents/db/.prettierrc
-agents/hosting/.prettierrc
agents/storage/.prettierrc
# Publish build output for the published packages.
agents/auth/dist/
agents/db/dist/
-agents/hosting/dist/
agents/storage/dist/
cli/dist/
@@ -36,8 +31,6 @@ agents/auth/LICENSE
agents/auth/NOTICE
agents/db/LICENSE
agents/db/NOTICE
-agents/hosting/LICENSE
-agents/hosting/NOTICE
agents/storage/LICENSE
agents/storage/NOTICE
cli/LICENSE
diff --git a/e2e/deploy-token.api.spec.ts b/e2e/deploy-token.api.spec.ts
deleted file mode 100644
index d01cf3d4..00000000
--- a/e2e/deploy-token.api.spec.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import { request as playwrightRequest, expect, test } from '@playwright/test';
-import {
- hostingAppPath,
- hostingDeployPath,
- hostingOverviewPath,
- hostingTokenPath,
- hostingTokensPath,
- projectBranchesPath,
- registryProjectPath,
- SEED_PROJECT
-} from './helpers';
-
-/**
- * Deploy tokens (Phase B): CI's durable
- * credential. The contract this file pins: minted on roots only, the secret
- * appears exactly once and is stored hashed, the `cfbd_` bearer is accepted
- * SOLELY on the deploy and branch-create endpoints for the token's family -
- * everywhere else it is a plain 401 - and revocation ends it immediately.
- */
-
-const runId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.slice(0, 12);
-const rootId = `e2e-tok-${runId}`;
-const appName = `tok-${runId}`;
-
-const deployBody = {
- multipart: {
- meta: JSON.stringify({}),
- 'asset:/index.html': {
- name: 'index.html',
- mimeType: 'text/html',
- buffer: Buffer.from('
token deploy ')
- }
- }
-};
-
-/** A cookie-less context whose only credential is the deploy token. */
-function tokenContext(baseURL: string | undefined, token: string) {
- return playwrightRequest.newContext({
- baseURL,
- extraHTTPHeaders: { origin: baseURL ?? '', authorization: `Bearer ${token}` },
- storageState: { cookies: [], origins: [] }
- });
-}
-
-test.describe('deploy tokens', () => {
- let secret = '';
- let tokenId = '';
-
- test.beforeAll(async ({ request }) => {
- const created = await request.post('/api/registry/projects', {
- data: { id: rootId, name: `Token e2e ${rootId}` }
- });
- expect(created.status(), await created.text()).toBe(201);
- });
-
- test.afterAll(async ({ request }) => {
- await request.delete(registryProjectPath(rootId));
- });
-
- test('minting returns the secret exactly once', async ({ request }) => {
- const minted = await request.post(hostingTokensPath(rootId), { data: { name: 'ci' } });
- expect(minted.status(), await minted.text()).toBe(201);
- const body = (await minted.json()) as { id: string; token: string };
- expect(body.token).toMatch(/^cfbd_[0-9a-f]{64}$/);
- secret = body.token;
- tokenId = body.id;
-
- // The list carries metadata only - the secret is unrecoverable.
- const listed = await request.get(hostingTokensPath(rootId));
- expect(listed.status(), await listed.text()).toBe(200);
- const { tokens } = (await listed.json()) as { tokens: Record[] };
- expect(tokens.some((token) => token.id === tokenId)).toBe(true);
- for (const token of tokens) {
- expect(token.token).toBeUndefined();
- expect(token.tokenHash).toBeUndefined();
- }
- });
-
- test('branch rows refuse minting - tokens live on the root', async ({ request }) => {
- const branch = await request.post(projectBranchesPath(rootId), { data: { branch: 'ci' } });
- expect(branch.status(), await branch.text()).toBe(201);
- const minted = await request.post(hostingTokensPath(`${rootId}--ci`), {
- data: { name: 'nope' }
- });
- expect(minted.status(), await minted.text()).toBe(400);
- });
-
- test('a deploy token deploys - and does nothing else', async ({ baseURL }) => {
- const bearer = await tokenContext(baseURL, secret);
- try {
- const deploy = await bearer.post(hostingDeployPath(rootId, appName), deployBody);
- expect(deploy.status(), await deploy.text()).toBe(201);
-
- // Not a session: every other surface answers 401.
- const overview = await bearer.get(hostingOverviewPath(rootId));
- expect(overview.status(), 'a deploy token must not read the overview').toBe(401);
- const registry = await bearer.get('/api/registry/projects');
- expect(registry.status(), 'a deploy token must not list projects').toBe(401);
- const tokens = await bearer.get(hostingTokensPath(rootId));
- expect(tokens.status(), 'a deploy token must not read tokens').toBe(401);
-
- // Not transferable: another project's deploys refuse it.
- const foreign = await bearer.post(hostingDeployPath(SEED_PROJECT, appName), deployBody);
- expect(foreign.status(), 'a deploy token is scoped to its family').toBe(401);
-
- // Not destructive: a credential that ships code must never erase an
- // app - deletion is a session-only surface.
- const erase = await bearer.delete(hostingAppPath(rootId, appName));
- expect(erase.status(), 'a deploy token must not delete apps').toBe(401);
- } finally {
- await bearer.dispose();
- }
- });
-
- test('a deploy token creates branch rows for new git branches', async ({ baseURL }) => {
- const bearer = await tokenContext(baseURL, secret);
- try {
- const created = await bearer.post(projectBranchesPath(rootId), {
- data: { branch: 'preview-x' }
- });
- expect(created.status(), await created.text()).toBe(201);
- const again = await bearer.post(projectBranchesPath(rootId), {
- data: { branch: 'preview-x' }
- });
- expect(again.status(), 'the second create is the idempotent 409').toBe(409);
-
- // ... and can then deploy to the branch it minted.
- const deploy = await bearer.post(
- hostingDeployPath(`${rootId}--preview-x`, appName),
- deployBody
- );
- expect(deploy.status(), await deploy.text()).toBe(201);
- expect(((await deploy.json()) as { subdomain: string }).subdomain).toBe(
- `${appName}-preview-x`
- );
- } finally {
- await bearer.dispose();
- }
- });
-
- test('revocation ends the token immediately', async ({ request, baseURL }) => {
- const revoked = await request.delete(hostingTokenPath(rootId, tokenId));
- expect(revoked.status(), await revoked.text()).toBe(200);
-
- const bearer = await tokenContext(baseURL, secret);
- try {
- const deploy = await bearer.post(hostingDeployPath(rootId, appName), deployBody);
- expect(deploy.status(), 'a revoked token must not deploy').toBe(401);
- } finally {
- await bearer.dispose();
- }
- });
-
- test('a fabricated token is a plain 401', async ({ baseURL }) => {
- const bearer = await tokenContext(baseURL, `cfbd_${'0'.repeat(64)}`);
- try {
- const deploy = await bearer.post(hostingDeployPath(rootId, appName), deployBody);
- expect(deploy.status()).toBe(401);
- } finally {
- await bearer.dispose();
- }
- });
-});
diff --git a/e2e/github-connect.api.spec.ts b/e2e/github-connect.api.spec.ts
deleted file mode 100644
index 63c77fa6..00000000
--- a/e2e/github-connect.api.spec.ts
+++ /dev/null
@@ -1,213 +0,0 @@
-import { request as playwrightRequest, expect, test } from '@playwright/test';
-import {
- githubConnectionPath,
- githubConnectionsPath,
- githubInstallPath,
- githubReposPath,
- githubStatePath,
- GITHUB_WEBHOOK_PATH,
- hostingDeployPath,
- projectBranchesPath,
- registryProjectPath,
- SEED_PROJECT
-} from './helpers';
-
-/**
- * GitHub push-to-deploy (Phase B).
- *
- * This stack configures NO GitHub App, which is the self-hosted default and
- * the important half of the contract to pin: adding push-to-deploy must not
- * open a single new surface on an install that never asked for it. The
- * connect flow answers 503 honestly, the webhook does not exist, and none of
- * it is reachable without an operator session.
- *
- * The parts that need a real App (installations, repository reads, workflow
- * writes) need GitHub itself, so they are not e2e territory. The pure logic
- * underneath is unit tested in agents/hosting (`tar.unit.test.ts`).
- */
-
-const runId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.slice(0, 12);
-const rootId = `e2e-gh-${runId}`;
-
-/** A cookie-less context - the guard must not see a session. */
-function anonymous(baseURL: string | undefined, headers: Record = {}) {
- return playwrightRequest.newContext({
- baseURL,
- extraHTTPHeaders: { origin: baseURL ?? '', ...headers },
- storageState: { cookies: [], origins: [] }
- });
-}
-
-test.describe('github connect', () => {
- test.beforeAll(async ({ request }) => {
- const created = await request.post('/api/registry/projects', {
- data: { id: rootId, name: `GitHub e2e ${rootId}` }
- });
- expect(created.status(), await created.text()).toBe(201);
- });
-
- test.afterAll(async ({ request }) => {
- await request.delete(registryProjectPath(rootId));
- });
-
- test('an unconfigured console reports no App rather than half a flow', async ({ request }) => {
- const state = await request.get(githubStatePath(rootId));
- expect(state.status(), await state.text()).toBe(200);
- const body = (await state.json()) as {
- configured: boolean;
- installations: unknown[];
- connections: unknown[];
- };
- // False is what makes the Hosting page offer the manual token flow.
- expect(body.configured).toBe(false);
- expect(body.installations).toEqual([]);
- expect(body.connections).toEqual([]);
- });
-
- test('connecting without an App is an honest 503, never a silent no-op', async ({ request }) => {
- const install = await request.get(githubInstallPath(rootId));
- expect(install.status(), await install.text()).toBe(503);
-
- const repos = await request.get(githubReposPath(rootId));
- expect(repos.status(), await repos.text()).toBe(503);
-
- const connect = await request.post(githubConnectionsPath(rootId), {
- data: { installationId: 1, repoFullName: 'acme/site', appName: 'site', mode: 'build' }
- });
- expect(connect.status(), await connect.text()).toBe(503);
- });
-
- test('a connection that does not exist is a 404, not a 500', async ({ request }) => {
- const removed = await request.delete(githubConnectionPath(rootId, 'never-connected'));
- expect(removed.status(), await removed.text()).toBe(404);
- });
-
- test('build settings validate before the connection is even looked up', async ({ request }) => {
- // Charset-limited because these values embed into workflow YAML.
- for (const data of [
- { ignoredBranches: ["quo'te"] },
- { ignoredBranches: ['two words'] },
- { productionBranch: 'no spaces here' },
- { buildCommand: 'line one\nline two' },
- { unknownField: true }
- ]) {
- const patched = await request.patch(githubConnectionPath(rootId, 'never-connected'), {
- data
- });
- expect(patched.status(), JSON.stringify(data)).toBe(400);
- }
-
- // A valid body against a missing connection is the honest 404.
- const missing = await request.patch(githubConnectionPath(rootId, 'never-connected'), {
- data: { productionBranch: 'release' }
- });
- expect(missing.status(), await missing.text()).toBe(404);
- });
-
- test('every github surface requires an operator session', async ({ baseURL }) => {
- const guest = await anonymous(baseURL);
- try {
- // The callback is deliberately NOT in this list: it is a cross-site
- // return from github.com, authenticated by the signed state instead
- // (covered by its own test below).
- for (const path of [
- githubStatePath(rootId),
- githubInstallPath(rootId),
- githubReposPath(rootId),
- githubConnectionsPath(rootId)
- ]) {
- const response = await guest.get(path);
- expect(response.status(), `${path} must require an operator`).toBe(401);
- }
- const connect = await guest.post(githubConnectionsPath(rootId), {
- data: { installationId: 1, repoFullName: 'acme/site', appName: 'site', mode: 'build' }
- });
- expect(connect.status(), 'connecting must require an operator').toBe(401);
- const patched = await guest.patch(githubConnectionPath(rootId, 'site'), {
- data: { productionBranch: 'release' }
- });
- expect(patched.status(), 'editing build settings must require an operator').toBe(401);
- const removed = await guest.delete(githubConnectionPath(rootId, 'site'));
- expect(removed.status(), 'disconnecting must require an operator').toBe(401);
- } finally {
- await guest.dispose();
- }
- });
-
- test('demo projects cannot start an install', async ({ request }) => {
- // No demo hosting anywhere - the install entry point refuses too, so the
- // upsell is the only thing a demo visitor can reach.
- const install = await request.get(githubInstallPath('demo-abcdef123456'));
- expect(install.status(), await install.text()).toBe(403);
- });
-
- test('the webhook does not exist on a console with no App', async ({ baseURL }) => {
- const guest = await anonymous(baseURL, { 'x-github-event': 'ping' });
- try {
- const response = await guest.post(GITHUB_WEBHOOK_PATH, { data: { zen: 'hi' } });
- // 404, not 401: there is nothing here to authenticate against. What
- // matters is that it never 500s and never acts on an unsigned body.
- expect(response.status(), await response.text()).toBe(404);
- } finally {
- await guest.dispose();
- }
- });
-
- test('the install callback refuses a state it did not sign', async ({ baseURL }) => {
- // The callback is session-less by necessity - it is a cross-site return
- // from github.com - so the SIGNED STATE is its credential. A forged or
- // absent one must record nothing and bounce, never act on the
- // installation_id in the query.
- const guest = await anonymous(baseURL);
- try {
- for (const query of [
- 'installation_id=1&state=forged',
- 'installation_id=1&state=eyJwcm9qZWN0SWQiOiJ4In0.bm90LWEtc2ln',
- 'installation_id=1'
- ]) {
- const callback = await guest.get(`/api/github/callback?${query}`, { maxRedirects: 0 });
- // Always a bounce, never a 200 and never a 401 - the operator is
- // mid-install and must land somewhere useful.
- expect(callback.status(), `${query} must bounce`).toBe(303);
- // The invariant that matters: nothing was recorded, so the redirect
- // never carries the installation onward into the connect flow.
- expect(callback.headers()['location'], `${query} must not be honoured`).not.toContain(
- 'installation='
- );
- }
- } finally {
- await guest.dispose();
- }
- });
-
- test('a JWT-shaped bearer on the deploy surface is a plain 401', async ({ baseURL }) => {
- // GitHub's OIDC token is accepted on exactly the deploy-token surfaces,
- // and a forged one must never fall through to session resolution - the
- // same all-or-nothing contract a `cfbd_` bearer has.
- const forged =
- 'eyJhbGciOiJSUzI1NiIsImtpZCI6ImZha2UifQ.eyJyZXBvc2l0b3J5IjoiYWNtZS9zaXRlIn0.c2ln';
- const bearer = await anonymous(baseURL, { authorization: `Bearer ${forged}` });
- try {
- const deploy = await bearer.post(hostingDeployPath(rootId, 'forged'), {
- multipart: {
- meta: JSON.stringify({}),
- 'asset:/index.html': {
- name: 'index.html',
- mimeType: 'text/html',
- buffer: Buffer.from('nope ')
- }
- }
- });
- expect(deploy.status(), 'a forged identity token must not deploy').toBe(401);
-
- const branch = await bearer.post(projectBranchesPath(rootId), { data: { branch: 'forged' } });
- expect(branch.status(), 'a forged identity token must not create branches').toBe(401);
-
- // And it is not a session anywhere else either.
- const state = await bearer.get(githubStatePath(SEED_PROJECT));
- expect(state.status(), 'a forged identity token must not read console state').toBe(401);
- } finally {
- await bearer.dispose();
- }
- });
-});
diff --git a/e2e/helpers.ts b/e2e/helpers.ts
index b504eb2a..bc28ef72 100644
--- a/e2e/helpers.ts
+++ b/e2e/helpers.ts
@@ -225,89 +225,6 @@ export function dbViewSqlPath(projectId: string, view: string): string {
return `/api/projects/${projectId}/db/views/${view}/sql`;
}
-// --- Hosting (Phase B) ---
-
-export function hostingOverviewPath(projectId: string): string {
- return `/api/projects/${projectId}/hosting/overview`;
-}
-
-export function hostingClaimsPath(projectId: string): string {
- return `/api/projects/${projectId}/hosting/claims`;
-}
-
-export function hostingDeploysPath(projectId: string): string {
- return `/api/projects/${projectId}/hosting/deploys`;
-}
-
-export function hostingDeployPath(projectId: string, app: string): string {
- return `/api/projects/${projectId}/hosting/apps/${encodeURIComponent(app)}/deploys`;
-}
-
-export function hostingAppPath(projectId: string, app: string): string {
- return `/api/projects/${projectId}/hosting/apps/${encodeURIComponent(app)}`;
-}
-
-export function hostingVarsPath(projectId: string, app: string): string {
- return `/api/projects/${projectId}/hosting/apps/${encodeURIComponent(app)}/vars`;
-}
-
-export function hostingSecretsPath(projectId: string, app: string): string {
- return `/api/projects/${projectId}/hosting/apps/${encodeURIComponent(app)}/secrets`;
-}
-
-export function hostingSecretPath(projectId: string, app: string, name: string): string {
- return `/api/projects/${projectId}/hosting/apps/${encodeURIComponent(app)}/secrets/${encodeURIComponent(name)}`;
-}
-
-export function hostingBuildEnvPath(projectId: string, app: string): string {
- return `/api/projects/${projectId}/hosting/apps/${encodeURIComponent(app)}/build-env`;
-}
-
-export function hostingBuildVarsPath(projectId: string, app: string): string {
- return `/api/projects/${projectId}/hosting/apps/${encodeURIComponent(app)}/build-vars`;
-}
-
-export function hostingBuildSecretPath(projectId: string, app: string, name: string): string {
- return `/api/projects/${projectId}/hosting/apps/${encodeURIComponent(app)}/build-secrets/${encodeURIComponent(name)}`;
-}
-
-export function hostingAppAnalyticsPath(projectId: string, app: string, days = 7): string {
- return `/api/projects/${projectId}/hosting/apps/${encodeURIComponent(app)}/analytics?days=${days}`;
-}
-
-export function hostingTokensPath(projectId: string): string {
- return `/api/projects/${projectId}/hosting/tokens`;
-}
-
-export function hostingTokenPath(projectId: string, tokenId: string): string {
- return `/api/projects/${projectId}/hosting/tokens/${encodeURIComponent(tokenId)}`;
-}
-
-// --- GitHub push-to-deploy (Phase B) ---
-
-export function githubStatePath(projectId: string): string {
- return `/api/projects/${projectId}/hosting/github`;
-}
-
-export function githubInstallPath(projectId: string): string {
- return `/api/projects/${projectId}/hosting/github/install`;
-}
-
-export function githubReposPath(projectId: string, installation = 1): string {
- return `/api/projects/${projectId}/hosting/github/repos?installation=${installation}`;
-}
-
-export function githubConnectionsPath(projectId: string): string {
- return `/api/projects/${projectId}/hosting/github/connections`;
-}
-
-export function githubConnectionPath(projectId: string, app: string): string {
- return `/api/projects/${projectId}/hosting/github/connections/${encodeURIComponent(app)}`;
-}
-
-/** The webhook is public by exception - its HMAC signature is the credential. */
-export const GITHUB_WEBHOOK_PATH = '/api/github/webhook';
-
// --- Storage (S1) ---
/** Project the storage specs self-seed. Buckets use FIXED names (creates are
diff --git a/e2e/hosting-env.api.spec.ts b/e2e/hosting-env.api.spec.ts
deleted file mode 100644
index 1bcc730b..00000000
--- a/e2e/hosting-env.api.spec.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-import { expect, test } from '@playwright/test';
-import {
- hostingBuildEnvPath,
- hostingBuildSecretPath,
- hostingBuildVarsPath,
- hostingDeployPath,
- hostingSecretPath,
- hostingSecretsPath,
- hostingTokensPath,
- hostingVarsPath,
- registryProjectPath
-} from './helpers';
-
-/**
- * Stored app environments (runtime half): vars round-trip as a
- * replace-the-set store applied at deploy time, and secrets are write-through
- * names the console can list and delete. Everything runs against the stub -
- * which stores names and skips the Cloudflare calls - so the whole contract
- * is exercised on every run; the live settings-PATCH/DELETE shapes are pinned
- * by unit tests and the opt-in hosting-real spec.
- */
-
-const runId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.slice(0, 12);
-const projectId = `e2e-env-${runId}`;
-const appName = `env-${runId}`;
-
-const INDEX_HTML = {
- name: 'index.html',
- mimeType: 'text/html',
- buffer: Buffer.from('env spec ')
-};
-
-test.describe('hosting app environment (stubbed)', () => {
- test.beforeAll(async ({ request }) => {
- const created = await request.post('/api/registry/projects', {
- data: { id: projectId, name: `Hosting env e2e ${runId}` }
- });
- expect(created.status(), await created.text()).toBe(201);
- });
-
- test.afterAll(async ({ request }) => {
- await request.delete(registryProjectPath(projectId));
- });
-
- test('vars round-trip, and the set is replaced - not merged', async ({ request }) => {
- // Vars need no apps row: a claim-only app can be configured before its
- // first deploy.
- const put = await request.put(hostingVarsPath(projectId, appName), {
- data: { vars: { ALPHA: 'one', BETA: 'two' } }
- });
- expect(put.status(), await put.text()).toBe(200);
- const putBody = (await put.json()) as {
- vars: { name: string; value: string }[];
- patched: boolean;
- };
- expect(putBody.vars.map((entry) => entry.name)).toEqual(['ALPHA', 'BETA']);
- // Stub mode has no live script to patch.
- expect(putBody.patched).toBe(false);
-
- const replaced = await request.put(hostingVarsPath(projectId, appName), {
- data: { vars: { BETA: 'changed', GAMMA: 'three' } }
- });
- expect(replaced.status(), await replaced.text()).toBe(200);
-
- const list = await request.get(hostingVarsPath(projectId, appName));
- expect(list.status(), await list.text()).toBe(200);
- const listBody = (await list.json()) as { vars: { name: string; value: string }[] };
- // ALPHA is gone (absent = deleted), BETA updated, GAMMA added.
- expect(listBody.vars.map((entry) => [entry.name, entry.value])).toEqual([
- ['BETA', 'changed'],
- ['GAMMA', 'three']
- ]);
- });
-
- test('invalid names and multi-line values are refused', async ({ request }) => {
- for (const vars of [
- { 'lower-case': 'x' },
- { '9STARTS_WITH_DIGIT': 'x' },
- { OK: 'line one\nline two' }
- ]) {
- const put = await request.put(hostingVarsPath(projectId, appName), { data: { vars } });
- expect(put.status(), JSON.stringify(vars)).toBe(400);
- }
- });
-
- test('secrets require a deployed app', async ({ request }) => {
- const put = await request.post(hostingSecretsPath(projectId, appName), {
- data: { name: 'API_KEY', value: 'hunter2' }
- });
- expect(put.status(), await put.text()).toBe(404);
- });
-
- test('secrets store names in stub mode, list them, and delete idempotently', async ({
- request
- }) => {
- const deployed = await request.post(hostingDeployPath(projectId, appName), {
- multipart: { meta: JSON.stringify({}), 'asset:/index.html': INDEX_HTML }
- });
- expect(deployed.status(), await deployed.text()).toBe(201);
-
- // This 200 pins the 501 -> ok change: stub mode records the name so the
- // list/delete contract is exercisable locally.
- const put = await request.post(hostingSecretsPath(projectId, appName), {
- data: { name: 'API_KEY', value: 'hunter2' }
- });
- expect(put.status(), await put.text()).toBe(200);
-
- const list = await request.get(hostingSecretsPath(projectId, appName));
- expect(list.status(), await list.text()).toBe(200);
- const listBody = (await list.json()) as { secrets: { name: string }[] };
- expect(listBody.secrets.map((secret) => secret.name)).toEqual(['API_KEY']);
- // Names only - the value must never come back out.
- expect(JSON.stringify(listBody)).not.toContain('hunter2');
-
- const deleted = await request.delete(hostingSecretPath(projectId, appName, 'API_KEY'));
- expect(deleted.status(), await deleted.text()).toBe(200);
-
- const after = await request.get(hostingSecretsPath(projectId, appName));
- expect(((await after.json()) as { secrets: unknown[] }).secrets).toEqual([]);
-
- // Idempotent: deleting what is already gone is a 200, not an error.
- const again = await request.delete(hostingSecretPath(projectId, appName, 'API_KEY'));
- expect(again.status(), await again.text()).toBe(200);
- });
-
- test('deploys apply stored vars over CLI meta.vars', async ({ request }) => {
- const put = await request.put(hostingVarsPath(projectId, appName), {
- data: { vars: { SHARED: 'console-wins' } }
- });
- expect(put.status(), await put.text()).toBe(200);
-
- // The stub records the deploy without uploading, so the merge itself is
- // pinned by unit tests; here the contract is that a deploy with
- // conflicting meta.vars still succeeds and the stored value survives.
- const deployed = await request.post(hostingDeployPath(projectId, appName), {
- multipart: {
- meta: JSON.stringify({ vars: { SHARED: 'cli-loses', CLI_ONLY: 'kept' } }),
- 'asset:/index.html': INDEX_HTML
- }
- });
- expect(deployed.status(), await deployed.text()).toBe(201);
-
- const list = await request.get(hostingVarsPath(projectId, appName));
- const listBody = (await list.json()) as { vars: { name: string; value: string }[] };
- expect(listBody.vars).toEqual([
- expect.objectContaining({ name: 'SHARED', value: 'console-wins' })
- ]);
- });
-
- test('build env stores vars, keeps secret values write-only', async ({ request }) => {
- const putVars = await request.put(hostingBuildVarsPath(projectId, appName), {
- data: { vars: { VITE_API_URL: 'https://api.example' } }
- });
- expect(putVars.status(), await putVars.text()).toBe(200);
-
- // The env.test stack pins HOSTING_MASTER_KEY, so encryption is REAL here
- // - stub mode stubs the Cloudflare API, never the crypto.
- const putSecret = await request.put(hostingBuildSecretPath(projectId, appName, 'NPM_TOKEN'), {
- data: { value: 'npm_supersecret' }
- });
- expect(putSecret.status(), await putSecret.text()).toBe(200);
-
- const env = await request.get(hostingBuildEnvPath(projectId, appName));
- expect(env.status(), await env.text()).toBe(200);
- const body = (await env.json()) as {
- vars: { name: string; value: string }[];
- secrets: { name: string }[];
- encryptionConfigured: boolean;
- };
- expect(body.vars).toEqual([
- expect.objectContaining({ name: 'VITE_API_URL', value: 'https://api.example' })
- ]);
- expect(body.secrets.map((secret) => secret.name)).toEqual(['NPM_TOKEN']);
- expect(body.encryptionConfigured).toBe(true);
- // The operator surface never returns a decrypted value.
- expect(JSON.stringify(body)).not.toContain('npm_supersecret');
-
- const deleted = await request.delete(hostingBuildSecretPath(projectId, appName, 'NPM_TOKEN'));
- expect(deleted.status(), await deleted.text()).toBe(200);
- const after = await request.get(hostingBuildEnvPath(projectId, appName));
- expect(((await after.json()) as { secrets: unknown[] }).secrets).toEqual([]);
- });
-
- test('build-env refuses every wrong bearer', async ({ request, playwright, baseURL }) => {
- // A deploy token can ship code, but it is a long-lived credential that
- // lives in repository settings - it must never read build secrets.
- const minted = await request.post(hostingTokensPath(projectId), {
- data: { name: 'env-spec' }
- });
- expect(minted.status(), await minted.text()).toBe(201);
- const token = ((await minted.json()) as { token: string }).token;
- const asDeployToken = await playwright.request.newContext({
- baseURL,
- extraHTTPHeaders: { origin: baseURL ?? '', authorization: `Bearer ${token}` },
- storageState: { cookies: [], origins: [] }
- });
- try {
- const denied = await asDeployToken.get(hostingBuildEnvPath(projectId, appName));
- expect(denied.status(), await denied.text()).toBe(401);
- } finally {
- await asDeployToken.dispose();
- }
-
- // An OIDC-shaped bearer that verifies to nothing is a plain 401, never a
- // fall-through to session resolution.
- const asGarbageOidc = await playwright.request.newContext({
- baseURL,
- extraHTTPHeaders: { origin: baseURL ?? '', authorization: 'Bearer x.y.z' },
- storageState: { cookies: [], origins: [] }
- });
- try {
- const denied = await asGarbageOidc.get(hostingBuildEnvPath(projectId, appName));
- expect(denied.status(), await denied.text()).toBe(401);
- } finally {
- await asGarbageOidc.dispose();
- }
- });
-
- test('deleting the app erases its environment', async ({ request }) => {
- // Re-seed a build var so the erase has something to prove on.
- const putVars = await request.put(hostingBuildVarsPath(projectId, appName), {
- data: { vars: { VITE_API_URL: 'https://api.example' } }
- });
- expect(putVars.status(), await putVars.text()).toBe(200);
-
- const deleted = await request.delete(`/api/projects/${projectId}/hosting/apps/${appName}`);
- expect(deleted.status(), await deleted.text()).toBe(200);
-
- const vars = await request.get(hostingVarsPath(projectId, appName));
- expect(((await vars.json()) as { vars: unknown[] }).vars).toEqual([]);
- const secrets = await request.get(hostingSecretsPath(projectId, appName));
- expect(((await secrets.json()) as { secrets: unknown[] }).secrets).toEqual([]);
- const buildEnv = await request.get(hostingBuildEnvPath(projectId, appName));
- expect(((await buildEnv.json()) as { vars: unknown[] }).vars).toEqual([]);
- });
-});
-
-test.describe('hosting env guard', () => {
- // LOAD-BEARING: without this override the context inherits the operator
- // session and proves nothing.
- test.use({ storageState: { cookies: [], origins: [] } });
-
- test('every environment surface requires an operator session', async ({ request }) => {
- const closed = [
- ['GET', hostingVarsPath(projectId, appName)],
- ['PUT', hostingVarsPath(projectId, appName)],
- ['GET', hostingSecretsPath(projectId, appName)],
- ['POST', hostingSecretsPath(projectId, appName)],
- ['DELETE', hostingSecretPath(projectId, appName, 'API_KEY')],
- ['GET', hostingBuildEnvPath(projectId, appName)],
- ['PUT', hostingBuildVarsPath(projectId, appName)],
- ['PUT', hostingBuildSecretPath(projectId, appName, 'NPM_TOKEN')],
- ['DELETE', hostingBuildSecretPath(projectId, appName, 'NPM_TOKEN')]
- ] as const;
- for (const [method, path] of closed) {
- const response = await request.fetch(path, {
- method,
- data: method === 'GET' || method === 'DELETE' ? undefined : {}
- });
- expect(response.status(), `${method} ${path} must be closed`).toBe(401);
- }
- });
-
- test('the direct agent hop is closed too', async ({ request }) => {
- // The console guard has to cover the proxy AND the passthrough, or it
- // covers neither.
- const direct = await request.get(`/agents/hosting-agent/${projectId}/apps/${appName}/vars`);
- expect(direct.status(), await direct.text()).toBe(401);
- });
-});
diff --git a/e2e/hosting-real.api.spec.ts b/e2e/hosting-real.api.spec.ts
deleted file mode 100644
index 9d5b3c9d..00000000
--- a/e2e/hosting-real.api.spec.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import { expect, test } from '@playwright/test';
-import {
- hostingDeployPath,
- hostingOverviewPath,
- hostingSecretPath,
- hostingSecretsPath,
- hostingVarsPath,
- registryProjectPath
-} from './helpers';
-
-/**
- * Real Workers for Platforms upload - opt-in, the RUN_AI_E2E precedent.
- *
- * Point BASE_URL at a stack whose hosting agent has a real dispatch
- * namespace (the -preview namespace on cloudflarebase.com's account) and set
- * RUN_HOSTING_E2E=1: this deploys a tiny asset app for real and expects a
- * `live` deploy, which exercises the asset upload session, the salted
- * manifest hashing, and the script PUT against the actual Cloudflare API.
- * Serving is NOT asserted here - preview has no cfbase.dev route, so the
- * wildcard only exists in production.
- *
- * This spec is ALSO the live verification of two API shapes nothing else
- * proves (the stub skips both):
- * - the settings PATCH that replaces a script's plain_text set works on an
- * assets-only script (`PUT vars` answering `patched: true`), and
- * - the per-script secrets DELETE endpoint
- * (`.../scripts/{script}/secrets/{name}`) exists and answers.
- * Do not trust the console's live-edit or secret-delete buttons on a real
- * install until this has passed against the target account.
- */
-
-const runId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.slice(0, 12);
-const rootId = `e2e-real-${runId}`;
-const appName = `real-${runId}`;
-
-test.describe('real dispatch-namespace upload', () => {
- test.describe.configure({ mode: 'serial' });
- test.skip(
- !process.env.RUN_HOSTING_E2E,
- 'set RUN_HOSTING_E2E=1 (against a WfP-configured stack) to test the real upload'
- );
-
- test.beforeAll(async ({ request }) => {
- const created = await request.post('/api/registry/projects', {
- data: { id: rootId, name: `Real hosting e2e ${rootId}` }
- });
- expect(created.status(), await created.text()).toBe(201);
- });
-
- test.afterAll(async ({ request }) => {
- // Deleting the project also erases the uploaded script by tag.
- await request.delete(registryProjectPath(rootId));
- });
-
- test('a deploy lands live in the namespace', async ({ request }) => {
- const before = await request.get(hostingOverviewPath(rootId));
- expect(before.status(), await before.text()).toBe(200);
- const overview = (await before.json()) as { stub: boolean; configured: boolean };
- expect(
- overview.stub,
- 'the target stack is in HOSTING_STUB mode - this spec needs real credentials'
- ).toBe(false);
- expect(overview.configured, 'the hosting agent is missing its Cloudflare API config').toBe(
- true
- );
-
- const deploy = await request.post(hostingDeployPath(rootId, appName), {
- multipart: {
- meta: JSON.stringify({}),
- 'asset:/index.html': {
- name: 'index.html',
- mimeType: 'text/html',
- buffer: Buffer.from(`cloudflarebase real e2e ${runId} `)
- }
- }
- });
- expect(deploy.status(), await deploy.text()).toBe(201);
- const body = (await deploy.json()) as { deploy: { status: string }; subdomain: string };
- expect(body.deploy.status).toBe('live');
- expect(body.subdomain).toBe(appName);
- });
-
- test('a var edit patches the live assets-only script', async ({ request }) => {
- // RISK GATE: proves the settings PATCH (bindings replacement with
- // keep_bindings [secret_text, assets]) is accepted for a script that
- // has no main module.
- const put = await request.put(hostingVarsPath(rootId, appName), {
- data: { vars: { REAL_E2E: runId } }
- });
- expect(put.status(), await put.text()).toBe(200);
- const body = (await put.json()) as { patched: boolean; warning?: string };
- expect(body.warning ?? '').toBe('');
- expect(body.patched, 'the live settings PATCH must succeed on an assets-only script').toBe(
- true
- );
- });
-
- test('secrets set, list, survive a redeploy, and DELETE for real', async ({ request }) => {
- const set = await request.post(hostingSecretsPath(rootId, appName), {
- data: { name: 'REAL_SECRET', value: `real-${runId}` }
- });
- expect(set.status(), await set.text()).toBe(200);
-
- const listed = await request.get(hostingSecretsPath(rootId, appName));
- expect(
- ((await listed.json()) as { secrets: { name: string }[] }).secrets.map((s) => s.name)
- ).toContain('REAL_SECRET');
-
- // keep_bindings holds the secret across a redeploy.
- const redeploy = await request.post(hostingDeployPath(rootId, appName), {
- multipart: {
- meta: JSON.stringify({}),
- 'asset:/index.html': {
- name: 'index.html',
- mimeType: 'text/html',
- buffer: Buffer.from(`redeploy ${runId} `)
- }
- }
- });
- expect(redeploy.status(), await redeploy.text()).toBe(201);
- const after = await request.get(hostingSecretsPath(rootId, appName));
- expect(
- ((await after.json()) as { secrets: { name: string }[] }).secrets.map((s) => s.name)
- ).toContain('REAL_SECRET');
-
- // RISK GATE: THE live verification of the WfP per-script secrets DELETE
- // endpoint - the console's delete button is not "done" until this passes.
- const deleted = await request.delete(hostingSecretPath(rootId, appName, 'REAL_SECRET'));
- expect(deleted.status(), await deleted.text()).toBe(200);
- const gone = await request.get(hostingSecretsPath(rootId, appName));
- expect(
- ((await gone.json()) as { secrets: { name: string }[] }).secrets.map((s) => s.name)
- ).not.toContain('REAL_SECRET');
- });
-});
diff --git a/e2e/hosting.api.spec.ts b/e2e/hosting.api.spec.ts
deleted file mode 100644
index 425e086a..00000000
--- a/e2e/hosting.api.spec.ts
+++ /dev/null
@@ -1,367 +0,0 @@
-import { expect, test } from '@playwright/test';
-import {
- hostingAppAnalyticsPath,
- hostingAppPath,
- hostingClaimsPath,
- hostingDeployPath,
- hostingDeploysPath,
- hostingOverviewPath,
- hostingTokensPath,
- projectBranchesPath,
- registryProjectPath
-} from './helpers';
-
-/**
- * The stubbed CLI -> console -> agent hosting contract (Phase B): subdomain
- * claims resolve in the control
- * plane with auto-numbering, deploys are multipart uploads the console
- * forwards after pushing the claim, and HOSTING_STUB records everything
- * without a dispatch namespace. Real upload coverage is the opt-in
- * hosting-real spec; this file is what pins the shape for every run.
- */
-
-// Fresh ids per run: claims and registry rows are permanent on locally
-// reused stacks. Short enough that `--stg` fits the 48-char ceiling.
-const runId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.slice(0, 12);
-const rootId = `e2e-host-${runId}`;
-const neighborId = `e2e-hostb-${runId}`;
-const appName = `app-${runId}`;
-
-const INDEX_HTML = {
- name: 'index.html',
- mimeType: 'text/html',
- buffer: Buffer.from('hello from e2e ')
-};
-
-function deployBody(extra: Record = {}) {
- return {
- multipart: {
- meta: JSON.stringify(extra),
- 'asset:/index.html': INDEX_HTML
- }
- };
-}
-
-test.describe('hosting deploys (stubbed)', () => {
- test.beforeAll(async ({ request }) => {
- for (const id of [rootId, neighborId]) {
- const created = await request.post('/api/registry/projects', {
- data: { id, name: `Hosting e2e ${id}` }
- });
- expect(created.status(), await created.text()).toBe(201);
- }
- });
-
- test.afterAll(async ({ request }) => {
- // Deleting the roots also deletes branches (child-first) and releases
- // the subdomain claims - keeps reused stacks under the project cap.
- for (const id of [rootId, neighborId]) {
- await request.delete(registryProjectPath(id));
- }
- });
-
- test('a dry claim previews without claiming', async ({ request }) => {
- const preview = await request.post(hostingClaimsPath(rootId), {
- data: { app: appName, dry: true }
- });
- expect(preview.status(), await preview.text()).toBe(200);
- const body = (await preview.json()) as { subdomain: string; created: boolean };
- expect(body.subdomain).toBe(appName);
- expect(body.created).toBe(false);
- });
-
- test('the first deploy claims the subdomain and records a stub deploy', async ({ request }) => {
- const deploy = await request.post(hostingDeployPath(rootId, appName), deployBody());
- expect(deploy.status(), await deploy.text()).toBe(201);
- const body = (await deploy.json()) as {
- subdomain: string;
- url: string | null;
- deploy: { status: string; assetCount: number; hasWorker: boolean };
- };
- expect(body.subdomain).toBe(appName);
- expect(body.url).toBe(`https://${appName}.cfbase.dev`);
- expect(body.deploy.status).toBe('stub');
- expect(body.deploy.assetCount).toBe(1);
- expect(body.deploy.hasWorker).toBe(false);
- });
-
- test('the overview lists the app and its deploy', async ({ request }) => {
- const overview = await request.get(hostingOverviewPath(rootId));
- expect(overview.status(), await overview.text()).toBe(200);
- const body = (await overview.json()) as {
- apps: { name: string; subdomain: string }[];
- recentDeploys: unknown[];
- totalDeploys: number;
- stub: boolean;
- configured: boolean;
- };
- expect(body.apps.map((app) => app.name)).toContain(appName);
- expect(body.totalDeploys).toBeGreaterThanOrEqual(1);
- expect(body.stub).toBe(true);
- expect(body.configured).toBe(true);
- });
-
- test('deploy history pages over keyset cursors', async ({ request }) => {
- // A second deploy so there are two pages at limit=1.
- const again = await request.post(hostingDeployPath(rootId, appName), deployBody());
- expect(again.status(), await again.text()).toBe(201);
-
- const first = await request.get(`${hostingDeploysPath(rootId)}?limit=1`);
- expect(first.status(), await first.text()).toBe(200);
- const page1 = (await first.json()) as {
- deploys: { id: string }[];
- total: number;
- cursor: string | null;
- };
- expect(page1.deploys).toHaveLength(1);
- expect(page1.total).toBeGreaterThanOrEqual(2);
- expect(page1.cursor).not.toBeNull();
-
- const second = await request.get(
- `${hostingDeploysPath(rootId)}?limit=1&cursor=${encodeURIComponent(page1.cursor!)}`
- );
- expect(second.status(), await second.text()).toBe(200);
- const page2 = (await second.json()) as { deploys: { id: string }[] };
- expect(page2.deploys).toHaveLength(1);
- // Keyset, not offset: the pages never overlap.
- expect(page2.deploys[0].id).not.toBe(page1.deploys[0].id);
- });
-
- test('a taken subdomain auto-numbers instead of failing', async ({ request }) => {
- const claim = await request.post(hostingClaimsPath(neighborId), {
- data: { app: appName }
- });
- expect(claim.status(), await claim.text()).toBe(201);
- const body = (await claim.json()) as { subdomain: string; created: boolean };
- expect(body.subdomain).toBe(`${appName}-2`);
-
- // The persisted row is reused verbatim afterwards - never re-derived.
- const again = await request.post(hostingClaimsPath(neighborId), {
- data: { app: appName }
- });
- expect(again.status(), await again.text()).toBe(200);
- expect(((await again.json()) as { subdomain: string }).subdomain).toBe(`${appName}-2`);
- });
-
- test('branch deploys serve at -', async ({ request }) => {
- const branch = await request.post(projectBranchesPath(rootId), {
- data: { branch: 'stg' }
- });
- expect(branch.status(), await branch.text()).toBe(201);
-
- const deploy = await request.post(hostingDeployPath(`${rootId}--stg`, appName), deployBody());
- expect(deploy.status(), await deploy.text()).toBe(201);
- const body = (await deploy.json()) as { subdomain: string };
- // Single dash - `main` never appears in a URL, and dispatch never
- // parses this back apart; the claims table resolved it here.
- expect(body.subdomain).toBe(`${appName}-stg`);
- });
-
- test('served requests land in the analytics tab (local mode)', async ({
- request,
- playwright
- }) => {
- // The serve path is only dialable on the local stack.
- test.skip(!!process.env.BASE_URL, 'direct agent access needs the local stack');
-
- // One request through the wildcard host mints one data point; the test
- // env has no read credentials, so it lands in the LOCAL_ANALYTICS D1 and
- // the analytics endpoint reads it back as `status: 'local'`.
- const agent = await playwright.request.newContext({ baseURL: 'http://localhost:8800' });
- try {
- const served = await agent.get('/', {
- headers: { 'x-cfbase-host': `${appName}.cfbase.dev` }
- });
- expect(served.status(), await served.text()).toBe(200);
- } finally {
- await agent.dispose();
- }
-
- // The write rides waitUntil and the read has a 5s cache - poll past both.
- await expect
- .poll(
- async () => {
- const analytics = await request.get(hostingAppAnalyticsPath(rootId, appName));
- if (analytics.status() !== 200) return { status: analytics.status() };
- const body = (await analytics.json()) as {
- totals: { requests: number };
- engine: { status: string };
- };
- return { status: 200, engine: body.engine.status, requests: body.totals.requests };
- },
- { timeout: 15_000 }
- )
- .toMatchObject({ status: 200, engine: 'local', requests: expect.any(Number) });
-
- const analytics = await request.get(hostingAppAnalyticsPath(rootId, appName));
- const body = (await analytics.json()) as {
- appName: string;
- subdomain: string;
- totals: { requests: number };
- byDay: { day: string; requests: number }[];
- };
- expect(body.appName).toBe(appName);
- expect(body.subdomain).toBe(appName);
- expect(body.totals.requests).toBeGreaterThanOrEqual(1);
- expect(body.byDay.length).toBeGreaterThanOrEqual(1);
- });
-
- test('demo projects get the 403 upsell, never a deploy', async ({ request }) => {
- const deploy = await request.post(
- hostingDeployPath('demo-abcdef123456', appName),
- deployBody()
- );
- expect(deploy.status(), await deploy.text()).toBe(403);
- });
-
- test('deleting an app erases it and frees the subdomain', async ({ request }) => {
- // Self-contained app so the shared appName claims stay untouched for
- // the release test below.
- const delApp = `del-${runId}`;
- const deployed = await request.post(hostingDeployPath(rootId, delApp), deployBody());
- expect(deployed.status(), await deployed.text()).toBe(201);
-
- const before = await request.get(hostingOverviewPath(rootId));
- expect(
- ((await before.json()) as { apps: { name: string }[] }).apps.map((a) => a.name)
- ).toContain(delApp);
-
- const deleted = await request.delete(hostingAppPath(rootId, delApp));
- expect(deleted.status(), await deleted.text()).toBe(200);
- const body = (await deleted.json()) as { deleted: boolean; subdomain: string };
- expect(body.deleted).toBe(true);
- expect(body.subdomain).toBe(delApp);
-
- // Gone from the agent - deploy history included.
- const after = await request.get(hostingOverviewPath(rootId));
- expect(
- ((await after.json()) as { apps: { name: string }[] }).apps.map((a) => a.name)
- ).not.toContain(delApp);
-
- // The subdomain is free again: a neighbor's dry claim gets the base name.
- const freed = await request.post(hostingClaimsPath(neighborId), {
- data: { app: delApp, dry: true }
- });
- expect(((await freed.json()) as { subdomain: string }).subdomain).toBe(delApp);
-
- // Deleting it again is a 404, not a 500 - the claim row is the gate.
- const again = await request.delete(hostingAppPath(rootId, delApp));
- expect(again.status(), await again.text()).toBe(404);
- });
-
- test('a claim that never deployed can still be deleted', async ({ request }) => {
- // Connecting a repository claims immediately; the agent only learns at
- // first deploy. Deleting must work on that claim-only state - the exact
- // shape that used to be stuck forever on the Hosting page.
- const ghost = `ghost-${runId}`;
- const claimed = await request.post(hostingClaimsPath(neighborId), { data: { app: ghost } });
- expect(claimed.status(), await claimed.text()).toBe(201);
-
- const deleted = await request.delete(hostingAppPath(neighborId, ghost));
- expect(deleted.status(), await deleted.text()).toBe(200);
-
- const freed = await request.post(hostingClaimsPath(neighborId), {
- data: { app: ghost, dry: true }
- });
- expect(((await freed.json()) as { subdomain: string }).subdomain).toBe(ghost);
- });
-
- test('the 11th app claim is refused - the cap is the squatting guard', async ({ request }) => {
- const capProbe = `e2e-hostcap-${runId}`;
- const created = await request.post('/api/registry/projects', {
- data: { id: capProbe, name: 'cap probe' }
- });
- expect(created.status(), await created.text()).toBe(201);
- try {
- for (let n = 1; n <= 10; n += 1) {
- const claim = await request.post(hostingClaimsPath(capProbe), {
- data: { app: `cap-${runId}-${n}` }
- });
- expect(claim.status(), await claim.text()).toBe(201);
- }
- const eleventh = await request.post(hostingClaimsPath(capProbe), {
- data: { app: `cap-${runId}-11` }
- });
- expect(eleventh.status(), await eleventh.text()).toBe(409);
- expect(((await eleventh.json()) as { error: string }).error).toContain('10');
- } finally {
- // Cascades the ten claims with the row.
- await request.delete(registryProjectPath(capProbe));
- }
- });
-
- test('deleting the project releases its claims', async ({ request }) => {
- const releaseProbe = `e2e-hostc-${runId}`;
- const created = await request.post('/api/registry/projects', {
- data: { id: releaseProbe, name: 'release probe' }
- });
- expect(created.status(), await created.text()).toBe(201);
- try {
- // Before the delete the base name belongs to rootId.
- const taken = await request.post(hostingClaimsPath(releaseProbe), {
- data: { app: appName, dry: true }
- });
- expect(((await taken.json()) as { subdomain: string }).subdomain).not.toBe(appName);
-
- const deleted = await request.delete(registryProjectPath(rootId));
- expect([200, 207], await deleted.text()).toContain(deleted.status());
-
- const freed = await request.post(hostingClaimsPath(releaseProbe), {
- data: { app: appName, dry: true }
- });
- expect(((await freed.json()) as { subdomain: string }).subdomain).toBe(appName);
- } finally {
- await request.delete(registryProjectPath(releaseProbe));
- }
- });
-});
-
-test.describe('stub serving', () => {
- // The serve path is only dialable on the local stack: the agent worker has
- // no public route on a deployed target.
- test.skip(!!process.env.BASE_URL, 'direct agent access needs the local stack');
-
- test('the wildcard host serves the stub page', async ({ playwright }) => {
- const agent = await playwright.request.newContext({ baseURL: 'http://localhost:8800' });
- try {
- const served = await agent.get('/', {
- headers: { 'x-cfbase-host': 'anything-at-all.cfbase.dev' }
- });
- expect(served.status(), await served.text()).toBe(200);
- expect(await served.text()).toContain('data-cfbase-stub="anything-at-all"');
- } finally {
- await agent.dispose();
- }
- });
-});
-
-test.describe('hosting guard', () => {
- // LOAD-BEARING: without this override the context inherits the operator
- // session and proves nothing.
- test.use({ storageState: { cookies: [], origins: [] } });
-
- test('every hosting surface requires an operator session', async ({ request }) => {
- const closed = [
- ['GET', hostingOverviewPath(rootId)],
- ['GET', hostingDeploysPath(rootId)],
- ['POST', hostingClaimsPath(rootId)],
- ['GET', hostingTokensPath(rootId)],
- ['POST', hostingTokensPath(rootId)],
- ['POST', hostingDeployPath(rootId, appName)],
- ['GET', hostingAppAnalyticsPath(rootId, appName)],
- // App deletion is session-only by design: a deploy token that can
- // ship code must never be able to erase an app, and the guard's
- // token surface admits POST deploys/branches alone.
- ['DELETE', hostingAppPath(rootId, appName)]
- ] as const;
- for (const [method, path] of closed) {
- const response =
- method === 'GET'
- ? await request.get(path)
- : method === 'DELETE'
- ? await request.delete(path)
- : await request.post(path, { data: {} });
- expect(response.status(), `${method} ${path} must be closed`).toBe(401);
- }
- });
-});
diff --git a/e2e/hosting.ui.spec.ts b/e2e/hosting.ui.spec.ts
deleted file mode 100644
index 424cf5c1..00000000
--- a/e2e/hosting.ui.spec.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-import { expect, test, type Page } from '@playwright/test';
-import { CONSOLE_STORAGE_STATE, hostingDeployPath, registryProjectPath } from './helpers';
-
-/**
- * Hosting pages (frontend): the hub's app list, the per-app page's URL tabs,
- * and the variables-and-secrets editor - the first UI coverage hosting has.
- * Uses a per-run project so reused local stacks never collide on permanent
- * subdomain claims, and serial mode because the tests walk one app's life.
- */
-
-const runId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`.slice(0, 12);
-const projectId = `e2e-hostui-${runId}`;
-const appName = `ui-${runId}`;
-
-async function gotoHosting(page: Page) {
- await page.goto(`/dashboard/${projectId}/hosting`);
- await expect(page.getByTestId('hosting-page')).toHaveAttribute('data-hydrated', 'true');
-}
-
-async function gotoApp(page: Page, tab = '') {
- await page.goto(`/dashboard/${projectId}/hosting/apps/${appName}${tab ? `/${tab}` : ''}`);
- await expect(page.getByTestId('hosting-app-page')).toHaveAttribute('data-hydrated', 'true');
-}
-
-test.describe('hosting pages (frontend)', () => {
- test.describe.configure({ mode: 'serial' });
-
- test.beforeAll(async ({ playwright, baseURL }) => {
- // The chromium project's request fixture sends no Origin header, and the
- // multipart deploy would trip the CSRF check without one.
- const api = await playwright.request.newContext({
- baseURL,
- extraHTTPHeaders: { origin: baseURL ?? '' },
- storageState: CONSOLE_STORAGE_STATE
- });
- try {
- const created = await api.post('/api/registry/projects', {
- data: { id: projectId, name: `Hosting UI e2e ${runId}` }
- });
- expect(created.status(), await created.text()).toBe(201);
- // A deployed app: the detail page's secrets editor needs one (secrets
- // attach to the deployed script).
- const deployed = await api.post(hostingDeployPath(projectId, appName), {
- multipart: {
- meta: JSON.stringify({}),
- 'asset:/index.html': {
- name: 'index.html',
- mimeType: 'text/html',
- buffer: Buffer.from('ui spec ')
- }
- }
- });
- expect(deployed.status(), await deployed.text()).toBe(201);
- } finally {
- await api.dispose();
- }
- });
-
- test.afterAll(async ({ request }) => {
- await request.delete(registryProjectPath(projectId));
- });
-
- test('the hub lists the app and opens its page', async ({ page }) => {
- await gotoHosting(page);
- await page.getByTestId(`open-app-${appName}`).click();
- await expect(page.getByTestId('hosting-app-page')).toHaveAttribute('data-hydrated', 'true');
- await expect(page).toHaveURL(new RegExp(`/hosting/apps/${appName}$`));
- await expect(page.getByTestId('app-stat-deploys').getByTestId('stat-value')).toHaveText('1');
- });
-
- test('tabs are URL-addressed sibling routes', async ({ page }) => {
- await gotoApp(page);
- await page.getByTestId('tab-app-deployments').click();
- await expect(page).toHaveURL(new RegExp(`/hosting/apps/${appName}/deployments$`));
- await expect(page.getByTestId('hosting-deploys-range')).toHaveText('1–1 of 1');
-
- await page.getByTestId('tab-app-analytics').click();
- await expect(page).toHaveURL(new RegExp(`/hosting/apps/${appName}/analytics$`));
- // The test stack reads the local D1 stand-in: the chart renders (zeroed
- // is fine), never the write-only upsell.
- await expect(page.getByTestId('hosting-analytics-chart')).toBeVisible();
-
- await page.getByTestId('tab-app-settings').click();
- await expect(page).toHaveURL(new RegExp(`/hosting/apps/${appName}/settings$`));
- await expect(page.getByTestId('hosting-vars-card')).toBeVisible();
- });
-
- test('runtime vars round-trip through the editor', async ({ page }) => {
- await gotoApp(page, 'settings');
-
- await page.getByTestId('hosting-vars-add').click();
- await page.getByTestId('hosting-vars-name-0').fill('API_BASE');
- await page.getByTestId('hosting-vars-value-0').fill('https://api.example');
- await page.getByTestId('hosting-vars-save').click();
- await expect(page.getByTestId('hosting-vars-feedback')).toHaveText('Saved.');
-
- // Server truth, not optimistic state: a reload renders the stored row.
- await gotoApp(page, 'settings');
- await expect(page.getByTestId('hosting-vars-name-0')).toHaveValue('API_BASE');
- await expect(page.getByTestId('hosting-vars-value-0')).toHaveValue('https://api.example');
- });
-
- test('secrets are write-only rows', async ({ page }) => {
- await gotoApp(page, 'settings');
-
- // Row 0 is the stored var; add row 1 and flip it to Secret.
- await page.getByTestId('hosting-vars-add').click();
- await page.getByTestId('hosting-vars-type-1').click();
- await page.getByRole('option', { name: 'Secret' }).click();
- await page.getByTestId('hosting-vars-name-1').fill('API_KEY');
- await page.getByTestId('hosting-vars-value-1').fill('hunter2');
- await page.getByTestId('hosting-vars-save').click();
- await expect(page.getByTestId('hosting-vars-feedback')).toHaveText('Saved.');
-
- // Reloaded, the secret is a write-only row: named, but the value is
- // gone for good - only the placeholder admits it exists.
- await gotoApp(page, 'settings');
- await expect(page.getByTestId('hosting-vars-name-1')).toHaveValue('API_KEY');
- await expect(page.getByTestId('hosting-vars-value-1')).toHaveValue('');
- await expect(page.getByTestId('hosting-vars-value-1')).toHaveAttribute(
- 'placeholder',
- /Value encrypted/
- );
-
- // Removing the row deletes the secret on Save.
- await page.getByTestId('hosting-vars-remove-1').click();
- await page.getByTestId('hosting-vars-save').click();
- await expect(page.getByTestId('hosting-vars-feedback')).toHaveText('Saved.');
- await gotoApp(page, 'settings');
- await expect(page.getByTestId('hosting-vars-name-1')).toHaveCount(0);
- });
-
- test('the danger zone deletes the app with a typed confirm', async ({ page }) => {
- await gotoApp(page, 'settings');
- await page.getByTestId(`delete-app-${appName}`).click();
-
- const submit = page.getByTestId('confirm-delete-app');
- await expect(submit).toBeDisabled();
- await page.getByTestId('delete-app-confirm').fill(appName);
- await submit.click();
-
- // Deleting navigates back to the hub, where the app is gone.
- await expect(page.getByTestId('hosting-page')).toHaveAttribute('data-hydrated', 'true');
- await expect(page.getByTestId(`open-app-${appName}`)).toHaveCount(0);
- });
-});
diff --git a/e2e/landing.ui.spec.ts b/e2e/landing.ui.spec.ts
index efddf5cc..c39d4798 100644
--- a/e2e/landing.ui.spec.ts
+++ b/e2e/landing.ui.spec.ts
@@ -19,7 +19,7 @@ test.describe('landing page (frontend)', () => {
// card is a signup someone regrets. Every primitive listed is live.
const primitives = page.locator('#roadmap');
await expect(primitives.getByText('Planned')).toHaveCount(0);
- await expect(primitives.getByText('Live')).toHaveCount(5);
+ await expect(primitives.getByText('Live')).toHaveCount(4);
await expect(primitives).toContainText('Storage');
// The hero visual opens on the db agent and the tabs are real controls.
diff --git a/e2e/service-keys.api.spec.ts b/e2e/service-keys.api.spec.ts
index 3fdf2971..68a36508 100644
--- a/e2e/service-keys.api.spec.ts
+++ b/e2e/service-keys.api.spec.ts
@@ -23,7 +23,7 @@ import {
* 2. it must not reach another project - a sibling branch included, because
* for data the branch IS the isolation boundary;
* 3. it must not escalate: no registry (so it cannot delete its own project),
- * no console, no CLI, no hosting, and no minting or revoking keys;
+ * no console, no CLI, and no minting or revoking keys;
* 4. revocation must bite immediately.
*
* The suite's stored operator session is what mints keys, so the key-using
@@ -570,7 +570,6 @@ test.describe('service keys', () => {
['GET', `/api/registry/projects`],
['DELETE', `/api/registry/projects/${KEY_PROJECT}`],
['POST', `/api/projects/${KEY_PROJECT}/branches`],
- ['GET', `/api/projects/${KEY_PROJECT}/hosting/apps`],
['POST', `/api/cli/token`]
];
for (const [method, path] of escalations) {
diff --git a/eslint.config.js b/eslint.config.js
index 3467ddac..9f77d4ec 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -37,7 +37,6 @@ export default defineConfig(
'**/worker-configuration.d.ts',
'agents/auth/dist/',
'agents/db/dist/',
- 'agents/hosting/dist/',
'agents/storage/dist/',
'cli/dist/'
]
diff --git a/package.json b/package.json
index 7700de44..27cdbed1 100644
--- a/package.json
+++ b/package.json
@@ -15,11 +15,11 @@
"lint": "prettier --check --cache . && eslint --cache --cache-location node_modules/.cache/eslint/ .",
"format": "prettier --write --cache .",
"deploy": "npm run build && wrangler deploy",
- "deploy:all": "npm --prefix agents/auth run deploy && npm --prefix agents/db run deploy && npm --prefix agents/storage run deploy && npm --prefix agents/hosting run deploy && npm run deploy",
+ "deploy:all": "npm --prefix agents/auth run deploy && npm --prefix agents/db run deploy && npm --prefix agents/storage run deploy && npm run deploy",
"cf-typegen": "wrangler types ./src/worker-configuration.d.ts",
"postinstall": "cross-env POSTINSTALL=true node scripts/dev.js",
"test": "playwright test",
- "test:unit": "node --import tsx --test src/lib/server/frameworks.unit.test.ts src/lib/server/crawler.unit.test.ts src/lib/hosting-workflow.unit.test.ts src/lib/branch-match.unit.test.ts src/lib/stale-build.unit.test.ts",
+ "test:unit": "node --import tsx --test src/lib/server/crawler.unit.test.ts src/lib/stale-build.unit.test.ts",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"deploy:production": "npm run build && wrangler deploy --env production"
diff --git a/playwright.config.ts b/playwright.config.ts
index 423d39dd..acc325d1 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -89,13 +89,6 @@ export default defineConfig({
reuseExistingServer: !process.env.CI,
timeout: 120_000
},
- {
- command:
- 'node scripts/kill-port.mjs 8800 && node scripts/clean-dir.mjs .wrangler/test-state/hosting-agent && npm run dev:test --prefix agents/hosting',
- url: 'http://localhost:8800/health',
- reuseExistingServer: !process.env.CI,
- timeout: 120_000
- },
{
command:
'node scripts/kill-port.mjs 8801 && node scripts/clean-dir.mjs .wrangler/test-state/storage-agent && npm run dev:test --prefix agents/storage',
diff --git a/scripts/local-dev.json b/scripts/local-dev.json
index 551d49a9..4a55de1e 100644
--- a/scripts/local-dev.json
+++ b/scripts/local-dev.json
@@ -11,17 +11,13 @@
"name": "db-agent",
"command": "npm run dev --prefix ./agents/db"
},
- {
- "name": "hosting-agent",
- "command": "npm run dev --prefix ./agents/hosting"
- },
{
"name": "storage-agent",
"command": "npm run dev --prefix ./agents/storage"
},
{
"name": "web",
- "command": "wait-on tcp:localhost:8788 tcp:localhost:8789 tcp:localhost:8790 tcp:localhost:8791 && vite dev --port 5173 --open --host"
+ "command": "wait-on tcp:localhost:8788 tcp:localhost:8789 tcp:localhost:8791 && vite dev --port 5173 --open --host"
}
]
}
diff --git a/scripts/postinstall.json b/scripts/postinstall.json
index 52d33c70..82816a90 100644
--- a/scripts/postinstall.json
+++ b/scripts/postinstall.json
@@ -11,10 +11,6 @@
"name": "db-agent",
"command": "cd ./agents/db && npm install"
},
- {
- "name": "hosting-agent",
- "command": "cd ./agents/hosting && npm install"
- },
{
"name": "storage-agent",
"command": "cd ./agents/storage && npm install"
diff --git a/src/app.d.ts b/src/app.d.ts
index 89ed2ac8..58767851 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -17,18 +17,6 @@ declare global {
consoleIdentity: import('$lib/console').ConsoleIdentity | null;
/** Whether this deployment runs as a public demo (DEMO_MODE=true). */
demoMode: boolean;
- /**
- * Deploy-token grant when the request authenticated with a `cfbd_`
- * bearer (Phase B). Only ever set on
- * the deploy and branch-create endpoints; never a session.
- */
- deployToken: import('$lib/server/hosting').DeployTokenGrant | null;
- /**
- * Grant when the request authenticated with a GitHub Actions OIDC
- * token from a `build`-mode connection - the credential-less CI path.
- * Set on exactly the same surfaces as `deployToken`, never a session.
- */
- githubDeploy: import('$lib/server/github-connect').GithubDeployGrant | null;
/**
* Grant when the request authenticated with a `cfbs_` service key
*. Set only on its own project's DATA
@@ -44,8 +32,6 @@ declare global {
AUTH_AGENT: Fetcher;
/** Service binding to the db-agent worker (fetch-only interface). */
DB_AGENT: Fetcher;
- /** Service binding to the hosting-agent worker (fetch-only interface). */
- HOSTING_AGENT: Fetcher;
/**
* Optional per-tenant ceiling overrides (registry.ts defaults both
* to 5). Not in any deployed config's vars, so they are typed here
@@ -54,19 +40,6 @@ declare global {
*/
MAX_PROJECTS_PER_ORG?: string;
MAX_BRANCHES_PER_ROOT?: string;
- /**
- * GitHub App credentials for push-to-deploy (server/github.ts).
- * Optional secrets - typed here rather than in the generated
- * worker-configuration.d.ts because no deployed config declares
- * them. All four together or the App reads as unconfigured, which
- * is the self-hosted default: the Hosting page then offers the
- * manual deploy-token flow and nothing GitHub-side is reachable.
- * The private key may be either PEM encoding GitHub hands out.
- */
- GITHUB_APP_ID?: string;
- GITHUB_APP_SLUG?: string;
- GITHUB_APP_PRIVATE_KEY?: string;
- GITHUB_APP_WEBHOOK_SECRET?: string;
};
cf: CfProperties;
ctx: ExecutionContext;
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index abbe0f8f..bd775d02 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -10,13 +10,6 @@ import { projectIdSchema } from '$lib/schemas/auth';
import { agentFetcher, agentUrl, serverError } from '$lib/server/agents';
import { isDemoMode, isDemoProjectId, resolveConsoleIdentity } from '$lib/server/console';
import { guardConsoleClaim } from '$lib/server/console-setup';
-import { verifyGithubDeployGrant } from '$lib/server/github-connect';
-import {
- deployTokenCoversProject,
- isBuildEnvSurface,
- isDeployTokenSurface,
- verifyDeployToken
-} from '$lib/server/hosting';
import { isServiceKeySurface, verifyServiceKey } from '$lib/server/service-keys';
import { getProjectOwnership, projectExists, type ProjectOwnership } from '$lib/server/registry';
import { handleErrorWithSentry, initCloudflareSentryHandle, sentryHandle } from '@sentry/sveltekit';
@@ -254,30 +247,10 @@ function classifyAccess(pathname: string): Access {
// Everything under /api is operator surface unless published below, so a
// route added later is private until someone deliberately opens it.
if (segments[0] === 'api') {
- // The two GitHub routes the guard cannot gate, both authenticated by an
- // HMAC we control rather than by a session:
- //
- // - `webhook`: GitHub carries no session and never will. Its
- // X-Hub-Signature-256 is the credential, checked over the raw body.
- // - `callback`: the return leg of an App install. It arrives as a
- // cross-site top-level navigation from github.com, where a session
- // cookie is not reliably present - requiring one stranded operators
- // mid-install. The signed install state IS the credential here: it is
- // minted only for a signed-in operator on a specific project, expires
- // in minutes, and cannot be forged without the webhook secret. The
- // route verifies it before writing anything, and still cross-checks a
- // session when the browser does send one.
- if (
- segments[1] === 'github' &&
- segments.length === 3 &&
- (segments[2] === 'webhook' || segments[2] === 'callback')
- ) {
- return { scope: 'open' };
- }
- // The first-run setup unlock. Public for the same reason as the two
- // above: an unclaimed console has no session to authenticate against,
- // and CONSOLE_SETUP_TOKEN - writable only with Cloudflare account
- // credentials - is the credential the route checks.
+ // The first-run setup unlock. Public because an unclaimed console has
+ // no session to authenticate against, and CONSOLE_SETUP_TOKEN - writable
+ // only with Cloudflare account credentials - is the credential the
+ // route checks.
if (segments[1] === 'console' && segments[2] === 'setup' && segments.length === 3) {
return { scope: 'open' };
}
@@ -398,8 +371,6 @@ const consoleGuardHandle: Handle = async ({ event, resolve }) => {
event.locals.demoMode = isDemoMode(event.platform);
event.locals.consoleUser = null;
event.locals.consoleIdentity = null;
- event.locals.deployToken = null;
- event.locals.githubDeploy = null;
event.locals.serviceKey = null;
const access = classifyAccess(event.url.pathname);
@@ -434,33 +405,6 @@ const consoleGuardHandle: Handle = async ({ event, resolve }) => {
return noSuchProject(access.kind);
}
- // Deploy tokens (Phase B): a `cfbd_` bearer
- // is CI's durable credential, accepted SOLELY on the deploy and
- // branch-create endpoints for the token's root project and its branches.
- // Any other use of one - wrong surface, wrong project, revoked - is a
- // plain 401 here, never a fall-through to session resolution: a deploy
- // token must never behave like a session.
- const bearer = event.request.headers
- .get('authorization')
- ?.match(/^Bearer\s+(cfbd_[0-9a-f]{64})$/i)?.[1];
- if (bearer) {
- if (
- access.projectId &&
- isDeployTokenSurface(event.url.pathname, event.request.method) &&
- !isDemoProjectId(access.projectId)
- ) {
- const grant = await verifyDeployToken(event.platform, bearer.toLowerCase());
- if (
- grant &&
- (await deployTokenCoversProject(event.platform, grant.projectId, access.projectId))
- ) {
- event.locals.deployToken = grant;
- return resolve(event);
- }
- }
- return Response.json({ error: 'invalid deploy token' }, { status: 401 });
- }
-
// Service keys (SK1): a `cfbs_` bearer is the
// credential a SERVER holds for the cases with no user to relay - crons,
// queue consumers, webhook handlers, seed scripts. It reaches the DATA
@@ -476,8 +420,8 @@ const consoleGuardHandle: Handle = async ({ event, resolve }) => {
// A REQUEST CARRYING AN `Origin` IS REFUSED, whatever the key. Server
// fetches send no Origin and browsers always do, so a key pasted into
// frontend code fails immediately at the developer's desk instead of
- // shipping inside a JS bundle on a CDN. Same all-or-nothing contract as a
- // deploy token: never a fall-through to session resolution.
+ // shipping inside a JS bundle on a CDN. All or nothing: a key bearer never
+ // falls through to session resolution.
const serviceBearer = event.request.headers
.get('authorization')
?.match(/^Bearer\s+(cfbs_[0-9a-f]{64})$/i)?.[1];
@@ -515,40 +459,6 @@ const consoleGuardHandle: Handle = async ({ event, resolve }) => {
return Response.json({ error: 'invalid service key' }, { status: 401 });
}
- // GitHub Actions OIDC (Phase B): a
- // `build`-mode connection deploys with NO stored credential at all - the
- // workflow presents a short-lived token GitHub signed, describing the
- // repository it ran in, and the connection table says which project that
- // repository may deploy to. Same surfaces and same all-or-nothing contract
- // as a deploy token: never a fall-through to session resolution. The
- // build-env GET is the one read the bearer also opens - the workflow
- // fetches its build vars and secrets before the build step - and it is
- // deliberately NOT a deploy-token surface (see isBuildEnvSurface).
- //
- // Only attempted on those surfaces, so a three-segment console session
- // bearer on any other route still reaches the session path below.
- const oidcBearer =
- access.projectId &&
- (isDeployTokenSurface(event.url.pathname, event.request.method) ||
- isBuildEnvSurface(event.url.pathname, event.request.method))
- ? event.request.headers
- .get('authorization')
- ?.match(/^Bearer\s+([\w-]+\.[\w-]+\.[\w-]+)$/)?.[1]
- : undefined;
- if (oidcBearer) {
- const grant = await verifyGithubDeployGrant(
- event.platform,
- oidcBearer,
- event.url.origin,
- access.projectId!
- );
- if (grant) {
- event.locals.githubDeploy = grant;
- return resolve(event);
- }
- return Response.json({ error: 'invalid GitHub deploy token' }, { status: 401 });
- }
-
// Public demo: anonymous visitors may drive ephemeral demo projects, whose
// ids are unguessable and whose data self-destructs after the TTL. Demos
// are throwaway - never registry rows, never owned - so the visit skips
@@ -701,8 +611,7 @@ const cloudflareSentryHandle: Handle = async (input) => {
* requests keep the full check.
*
* The skip is BEARER-shaped only, not any Authorization header: every
- * credential this app accepts is a bearer (session token, `cfbd_`, `cfbs_`,
- * OIDC), while `Basic` is one a browser CAN attach by itself (a
+ * credential this app accepts is a bearer (session token or `cfbs_`), while `Basic` is one a browser CAN attach by itself (a
* `user:pass@host` top-level navigation), riding beside the victim's cookies
* on a request the guard would then authenticate from those cookies. A
* non-bearer Authorization therefore keeps the full check.
diff --git a/src/lib/agent-registry.ts b/src/lib/agent-registry.ts
index 7518c3aa..2521e4d2 100644
--- a/src/lib/agent-registry.ts
+++ b/src/lib/agent-registry.ts
@@ -1,7 +1,6 @@
import { z } from 'zod';
import authManifestJson from '../../agents/auth/cloudflarebase.agent.json';
import dbManifestJson from '../../agents/db/cloudflarebase.agent.json';
-import hostingManifestJson from '../../agents/hosting/cloudflarebase.agent.json';
import storageManifestJson from '../../agents/storage/cloudflarebase.agent.json';
/**
@@ -113,7 +112,7 @@ export type RouteAccess = z.infer;
*/
export interface AppAgentEntry {
manifest: AgentManifest;
- binding: 'AUTH_AGENT' | 'DB_AGENT' | 'HOSTING_AGENT' | 'STORAGE_AGENT';
+ binding: 'AUTH_AGENT' | 'DB_AGENT' | 'STORAGE_AGENT';
devHost: string;
}
@@ -130,11 +129,6 @@ export const AGENT_REGISTRY: Record = {
binding: 'DB_AGENT',
devHost: 'localhost:8789'
},
- hosting: {
- manifest: agentManifestSchema.parse(hostingManifestJson),
- binding: 'HOSTING_AGENT',
- devHost: 'localhost:8790'
- },
storage: {
manifest: agentManifestSchema.parse(storageManifestJson),
binding: 'STORAGE_AGENT',
diff --git a/src/lib/agents.ts b/src/lib/agents.ts
index 1c5d4041..524d4718 100644
--- a/src/lib/agents.ts
+++ b/src/lib/agents.ts
@@ -737,317 +737,6 @@ export const dbServerFrameSchema = z
'Server frames on the live-query WebSocket: an initial snapshot in query order, then added/modified/removed deltas as writes happen.'
});
-// --- Hosting agent (mirrors agents/hosting/src/agent.ts) ---
-
-export const hostingAppSchema = z
- .object({
- name: z.string().describe('The operator-chosen app name.'),
- subdomain: z
- .string()
- .describe('What was ACTUALLY claimed - auto-numbered when the wanted name was taken.'),
- url: z.string().nullable().describe('Live URL; null while no serving domain is configured.'),
- deployCount: z.number().int(),
- lastDeployAt: z.iso.datetime().nullable(),
- createdAt: z.iso.datetime()
- })
- .meta({
- id: 'HostingApp',
- description:
- 'One deployed app: a user Worker (assets and/or modules) in the dispatch namespace.'
- });
-
-export const hostingDeploySchema = z
- .object({
- id: z.string(),
- appName: z.string(),
- subdomain: z.string(),
- url: z.string().nullable(),
- status: z.enum(['live', 'stub']),
- hasWorker: z.boolean(),
- assetCount: z.number().int(),
- assetBytes: z.number().int(),
- moduleBytes: z.number().int(),
- createdAt: z.iso.datetime()
- })
- .meta({
- id: 'HostingDeploy',
- description: 'One deploy. `stub` means recorded without a dispatch namespace (local/e2e).'
- });
-
-export const hostingOverviewSchema = z
- .object({
- projectId: z.string(),
- provisionedAt: z.iso.datetime().nullable(),
- apps: z.array(hostingAppSchema),
- recentDeploys: z.array(hostingDeploySchema),
- totalDeploys: z.number().int(),
- configured: z.boolean().describe('Whether deploys can complete on this install.'),
- stub: z.boolean()
- })
- .meta({ id: 'HostingOverview' });
-
-export const hostingDeployPageSchema = z
- .object({
- deploys: z.array(hostingDeploySchema),
- total: z.number().int(),
- cursor: z.string().nullable().describe('Keyset cursor for the next page; null on the last one.')
- })
- .meta({ id: 'HostingDeployPage' });
-
-export const hostingClaimSchema = z
- .object({
- subdomain: z.string(),
- appName: z.string(),
- created: z.boolean().describe('False when an existing claim was reused (or on a dry run).')
- })
- .meta({
- id: 'HostingClaim',
- description:
- 'A resolved subdomain claim. Taken names auto-number (`name-2`, `name-3`, ...) - the resolved subdomain is persisted on first claim and reused verbatim afterwards.'
- });
-
-export const hostingClaimRequestSchema = z
- .object({
- app: z.string().describe('The wanted app name (subdomain charset, 3-48 chars).'),
- dry: z.boolean().optional().describe('Only report what would be claimed.')
- })
- .meta({ id: 'HostingClaimRequest' });
-
-export const deployTokenSchema = z
- .object({
- id: z.string(),
- name: z.string(),
- createdAt: z.iso.datetime(),
- lastUsedAt: z.iso.datetime().nullable()
- })
- .meta({
- id: 'DeployToken',
- description:
- 'Deploy-token metadata. The secret is shown once at mint and stored only as a SHA-256 digest.'
- });
-
-export const mintDeployTokenSchema = z
- .object({ name: z.string().describe('A label, e.g. the repository this token deploys from.') })
- .meta({ id: 'MintDeployTokenRequest' });
-
-export const githubConnectionSchema = z
- .object({
- id: z.string(),
- projectId: z.string().describe('The ROOT project; branches ride the same connection.'),
- appName: z.string(),
- installationId: z.number().int(),
- repoId: z.number().int(),
- repoFullName: z.string(),
- defaultBranch: z.string(),
- mode: z
- .enum(['build', 'direct'])
- .describe(
- '`build` adds a workflow and trusts the Actions OIDC token; `direct` deploys the pushed tree from the webhook, with no runner and no file in the repository.'
- ),
- assetsDir: z
- .string()
- .nullable()
- .describe(
- 'Direct mode: repo-relative directory published as assets. Build mode: where the build lands.'
- ),
- buildCommand: z
- .string()
- .nullable()
- .describe('Build mode: the workflow build command; null = the generic default.'),
- rootDir: z
- .string()
- .nullable()
- .describe('Build mode: monorepo root the build runs in; null = repository root.'),
- productionBranch: z
- .string()
- .nullable()
- .describe("Branch that deploys the root project; null = the repository's default branch."),
- ignoredBranches: z
- .array(z.string())
- .describe('Branch names and `*` globs whose pushes never deploy.'),
- packageManager: z.string().nullable(),
- createdAt: z.iso.datetime(),
- lastEventAt: z.iso.datetime().nullable()
- })
- .meta({
- id: 'GithubConnection',
- description:
- 'A repository connected to one project+app. Made on a ROOT project; a push to the production branch deploys the root and any other branch deploys `--`.'
- });
-
-export const githubConnectionPatchSchema = z
- .object({
- buildCommand: z.string().nullable().optional().describe('Null resets to the generic default.'),
- rootDir: z.string().nullable().optional(),
- assetsDir: z.string().nullable().optional(),
- productionBranch: z
- .string()
- .nullable()
- .optional()
- .describe("Null resets to the repository's default branch."),
- ignoredBranches: z
- .array(z.string())
- .max(20)
- .optional()
- .describe('Branch names or simple `*` globs (e.g. `renovate/*`).')
- })
- .meta({
- id: 'GithubConnectionPatch',
- description:
- 'Editable build settings. Absent fields keep their value; null resets to the default. On a build-mode connection, saving rewrites the workflow file in the repository.'
- });
-
-export const mintedDeployTokenSchema = z
- .object({
- id: z.string(),
- name: z.string(),
- token: z.string().describe('The `cfbd_...` secret - shown exactly once.'),
- createdAt: z.iso.datetime()
- })
- .meta({ id: 'MintedDeployToken' });
-
-export const githubConnectRequestSchema = z
- .object({
- installationId: z.number().int().positive(),
- repoFullName: z.string().describe('`owner/repository`.'),
- appName: z.string(),
- mode: z.enum(['build', 'direct']),
- assetsDir: z
- .string()
- .optional()
- .describe('Direct: the published directory. Build: where the build lands.'),
- buildCommand: z.string().optional(),
- rootDir: z.string().optional().describe('Monorepo root (build mode).'),
- packageManager: z.enum(['npm', 'pnpm', 'yarn', 'bun']).optional(),
- wranglerTemplate: z
- .string()
- .optional()
- .describe('A known wrangler.jsonc template id to commit for a repo that has none.')
- })
- .meta({ id: 'GithubConnectRequest' });
-
-export const hostingVarSchema = z
- .object({
- name: z.string(),
- value: z.string(),
- createdAt: z.iso.datetime(),
- updatedAt: z.iso.datetime()
- })
- .meta({
- id: 'HostingVar',
- description:
- 'One stored runtime variable: applied as a plain_text binding on every deploy, and patched onto the live script when edited.'
- });
-
-export const hostingVarListSchema = z
- .object({ vars: z.array(hostingVarSchema) })
- .meta({ id: 'HostingVarList' });
-
-export const hostingVarsUpdateSchema = z
- .object({
- vars: z
- .record(z.string(), z.string())
- .describe(
- 'The FULL set (UPPER_SNAKE names, single-line values <=5000 chars) - absent names are deleted.'
- )
- })
- .meta({ id: 'HostingVarsUpdate' });
-
-export const hostingVarsUpdatedSchema = z
- .object({
- vars: z.array(hostingVarSchema),
- patched: z
- .boolean()
- .describe(
- 'Whether the live script was updated in place; false means the change applies at the next deploy.'
- ),
- warning: z.string().optional()
- })
- .meta({ id: 'HostingVarsUpdated' });
-
-export const hostingSecretMetaSchema = z
- .object({
- name: z.string(),
- createdAt: z.iso.datetime(),
- updatedAt: z.iso.datetime()
- })
- .meta({
- id: 'HostingSecretMeta',
- description:
- 'Secret name and timestamps. Values are write-through to Cloudflare and unrecoverable by design.'
- });
-
-export const hostingSecretListSchema = z
- .object({ secrets: z.array(hostingSecretMetaSchema) })
- .meta({ id: 'HostingSecretList' });
-
-export const hostingSecretRequestSchema = z
- .object({
- name: z.string().describe('UPPER_SNAKE_CASE.'),
- value: z.string().describe('1-5000 characters. Stored by Cloudflare, never by Cloudflarebase.')
- })
- .meta({ id: 'HostingSecretRequest' });
-
-export const hostingBuildEnvSchema = z
- .object({
- vars: z.array(hostingVarSchema),
- secrets: z.array(hostingSecretMetaSchema),
- encryptionConfigured: z
- .boolean()
- .describe('Whether this install can store build secrets (HOSTING_MASTER_KEY is set).')
- })
- .meta({
- id: 'HostingBuildEnv',
- description:
- "The operator view of an app's build-time environment: vars with values, secret names only - decrypted values never cross the operator surface."
- });
-
-export const hostingBuildEnvBundleSchema = z
- .object({
- vars: z.record(z.string(), z.string()),
- secrets: z.record(z.string(), z.string())
- })
- .meta({
- id: 'HostingBuildEnvBundle',
- description:
- 'The decrypted bundle a GitHub Actions runner exports before its build step. Served only to a verified OIDC bearer of the connection that owns the app.'
- });
-
-export const hostingBuildSecretRequestSchema = z
- .object({
- value: z
- .string()
- .describe("1-5000 characters, single-line. Encrypted at rest under the install's master key.")
- })
- .meta({ id: 'HostingBuildSecretRequest' });
-
-export const hostingAnalyticsSchema = z
- .object({
- appName: z.string(),
- subdomain: z.string(),
- days: z.number().int().describe('The window that was queried: 7, 30, or 90.'),
- totals: z.object({
- requests: z.number(),
- errors: z.number().describe('Responses with a 5xx status.'),
- avgDurationMs: z.number()
- }),
- byDay: z.array(z.object({ day: z.string(), requests: z.number(), errors: z.number() })),
- engine: z.object({
- dataset: z.string(),
- enabled: z.boolean(),
- status: z
- .enum(['connected', 'local', 'write-only', 'error'])
- .describe(
- 'connected = Analytics Engine reads work; local = the dev stand-in; write-only = events are recorded but reads need CF_ANALYTICS_API_TOKEN; error = the query failed (see `error`).'
- ),
- error: z.string().optional()
- })
- })
- .meta({
- id: 'HostingAnalytics',
- description: 'Per-app request analytics: daily requests/errors and totals.'
- });
-
// --- Storage agent (mirrors agents/storage/src/{agent,bucket,schemas}.ts) ---
/** `none` closes a side to the public API entirely - operator surfaces only.
@@ -1210,17 +899,6 @@ export type DbRestorePoints = z.infer;
export type DbReplicationMode = z.infer;
export type DbReplica = z.infer;
export type DbReplicationStatus = z.infer;
-export type HostingApp = z.infer;
-export type HostingDeploy = z.infer;
-export type HostingOverview = z.infer;
-export type HostingDeployPage = z.infer;
-export type HostingClaim = z.infer;
-export type DeployTokenInfo = z.infer;
-export type GithubConnectionInfo = z.infer;
-export type MintedDeployToken = z.infer;
-export type HostingVar = z.infer;
-export type HostingSecretMeta = z.infer;
-export type HostingAnalytics = z.infer;
export type StorageAccessMode = z.infer;
export type StorageBucketSummary = z.infer;
export type StorageBucketInfo = z.infer;
diff --git a/src/lib/branch-match.ts b/src/lib/branch-match.ts
deleted file mode 100644
index 528376dd..00000000
--- a/src/lib/branch-match.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { z } from 'zod';
-
-/**
- * Branch filters for a GitHub connection: which pushes never deploy, and
- * which branch is production. Pure on purpose (no server imports) - the
- * webhook handler, the OIDC grant verification, and the connection PATCH all
- * import it, and `node:test` runs it directly.
- */
-
-/** A git branch name we accept as production. Charset-limited because these
- * values are embedded into workflow YAML - quoting is a bug class, refusing
- * is not (the assetsDirSchema precedent). */
-export const gitBranchSchema = z
- .string()
- .min(1)
- .max(120)
- .regex(/^[A-Za-z0-9._/-]+$/, 'branch names may use letters, digits, ., _, / and -');
-
-/** An ignore filter: a branch name, or a simple `*` glob (`renovate/*`). */
-export const branchFilterSchema = z
- .string()
- .min(1)
- .max(120)
- .regex(/^[A-Za-z0-9._/*-]+$/, 'branch filters may use letters, digits, ., _, /, - and *');
-
-/** Tolerant parse of the `ignored_branches` D1 column - a row written by
- * hand (or a future format) degrades to "nothing ignored", never a throw. */
-export function parseIgnoredBranches(json: string | null): string[] {
- if (!json) return [];
- try {
- const parsed: unknown = JSON.parse(json);
- if (!Array.isArray(parsed)) return [];
- return parsed.filter((entry): entry is string => typeof entry === 'string');
- } catch {
- return [];
- }
-}
-
-const escapeRegex = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-
-/**
- * Whether a branch matches any filter. Exact match, or `*` as "any run of
- * characters" (it crosses `/` on purpose - `release/*` matching
- * `release/1.2/hotfix` is the least surprising reading, and GitHub's own
- * `branches-ignore` treats `**` that way).
- */
-export function branchIsIgnored(patterns: string[], branch: string): boolean {
- return patterns.some((pattern) => {
- if (!pattern.includes('*')) return pattern === branch;
- const regex = new RegExp(`^${pattern.split('*').map(escapeRegex).join('.*')}$`);
- return regex.test(branch);
- });
-}
diff --git a/src/lib/branch-match.unit.test.ts b/src/lib/branch-match.unit.test.ts
deleted file mode 100644
index 727fef9f..00000000
--- a/src/lib/branch-match.unit.test.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import assert from 'node:assert/strict';
-import test from 'node:test';
-import { branchFilterSchema, branchIsIgnored, parseIgnoredBranches } from './branch-match';
-
-/**
- * Three places enforce ignored branches (webhook skip, workflow
- * branches-ignore, OIDC grant refusal) and all three call THIS matcher, so
- * its edges are pinned here once.
- */
-
-test('exact names match exactly', () => {
- assert.equal(branchIsIgnored(['tmp'], 'tmp'), true);
- assert.equal(branchIsIgnored(['tmp'], 'tmp2'), false);
- assert.equal(branchIsIgnored(['tmp'], 'a/tmp'), false);
- assert.equal(branchIsIgnored([], 'anything'), false);
-});
-
-test('a * glob matches any run of characters, across slashes', () => {
- assert.equal(branchIsIgnored(['renovate/*'], 'renovate/react-19'), true);
- assert.equal(branchIsIgnored(['renovate/*'], 'renovate/deep/nesting'), true);
- assert.equal(branchIsIgnored(['renovate/*'], 'renovate'), false);
- assert.equal(branchIsIgnored(['*-wip'], 'feature-wip'), true);
- assert.equal(branchIsIgnored(['*'], 'anything/at-all'), true);
-});
-
-test('regex metacharacters in a filter are literal, not syntax', () => {
- assert.equal(branchIsIgnored(['v1.0'], 'v1.0'), true);
- // An unescaped dot would let `v1.0` match `v1x0`.
- assert.equal(branchIsIgnored(['v1.0'], 'v1x0'), false);
- assert.equal(branchIsIgnored(['a+b'], 'a+b'), true);
- assert.equal(branchIsIgnored(['a+b'], 'aab'), false);
-});
-
-test('parseIgnoredBranches tolerates every malformed column value', () => {
- assert.deepEqual(parseIgnoredBranches(null), []);
- assert.deepEqual(parseIgnoredBranches(''), []);
- assert.deepEqual(parseIgnoredBranches('not json'), []);
- assert.deepEqual(parseIgnoredBranches('{"a":1}'), []);
- assert.deepEqual(parseIgnoredBranches('[1, "keep", null]'), ['keep']);
- assert.deepEqual(parseIgnoredBranches('["tmp","renovate/*"]'), ['tmp', 'renovate/*']);
-});
-
-test('the filter schema refuses what YAML embedding cannot carry', () => {
- assert.equal(branchFilterSchema.safeParse('renovate/*').success, true);
- assert.equal(branchFilterSchema.safeParse('release-1.2_x').success, true);
- for (const bad of ['', "quo'te", 'two words', 'new\nline', 'tick`s', '$(sub)']) {
- assert.equal(branchFilterSchema.safeParse(bad).success, false, bad);
- }
-});
diff --git a/src/lib/components/console-shell.svelte b/src/lib/components/console-shell.svelte
index cdee4ba3..0fa48809 100644
--- a/src/lib/components/console-shell.svelte
+++ b/src/lib/components/console-shell.svelte
@@ -71,11 +71,11 @@
-
$ npx cloudflarebase init my-app
-
$ cloudflarebase deploy
+
$ npx cloudflarebase init
+
$ cloudflarebase key create server --env-file
- ✓ Deployed
- - my-app.cfbase.dev
+ ✓ Key written
+ - .env.local
diff --git a/src/lib/components/github-mark.svelte b/src/lib/components/github-mark.svelte
deleted file mode 100644
index 256313f8..00000000
--- a/src/lib/components/github-mark.svelte
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
diff --git a/src/lib/hosting-workflow.ts b/src/lib/hosting-workflow.ts
deleted file mode 100644
index 33741af2..00000000
--- a/src/lib/hosting-workflow.ts
+++ /dev/null
@@ -1,330 +0,0 @@
-/**
- * The official GitHub Actions deploy workflow (* Phase B) - Workers-Builds-style push-to-deploy WITHOUT us running a build
- * farm: GitHub's runners do the user's build, `cloudflarebase deploy` ships
- * the output with the repo's deploy token. Single-sourced here so the
- * dashboard's Connect GitHub card can never drift from what we document.
- *
- * Branch mapping happens in the CLI: the default git branch deploys the root
- * project, any other branch deploys `--` (auto-created), so a
- * preview per git branch falls out. The two env vars exist because
- * actions/checkout leaves a detached HEAD - git alone cannot name the branch.
- *
- * Connected workflows carry the framework preset resolved at connect time
- * (`src/lib/server/frameworks.ts`): the build command, the output directory
- * (as CLOUDFLAREBASE_ASSETS - the CLI already reads it), and install steps
- * matched to the repository's package manager.
- */
-
-export const DEPLOY_TOKEN_SECRET_NAME = 'CLOUDFLAREBASE_DEPLOY_TOKEN';
-/** The legacy single-file path. Connections made before per-app filenames
- * keep it forever (their row's `workflowPath` is null); the manual
- * paste-a-workflow flow still uses it too. */
-export const WORKFLOW_FILENAME = '.github/workflows/cloudflarebase.yml';
-
-/** Per-app workflow path - what closes the two-apps-one-repo collision: each
- * connection owns its own file. App names are already filename-safe
- * (`[a-z0-9-]`). */
-export function workflowPathFor(appName: string): string {
- return `.github/workflows/cloudflarebase-${appName}.yml`;
-}
-
-export type WorkflowPackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
-
-export interface WorkflowBuildOptions {
- packageManager?: WorkflowPackageManager;
- /** Preset or operator-edited command; null keeps `npm run build --if-present`. */
- buildCommand?: string | null;
- /** Monorepo root: install, build, and deploy all run HERE. Null = repo
- * root. hashFiles guards are prefixed because they always resolve from
- * the workspace root, working-directory or not. */
- rootDir?: string | null;
-}
-
-/**
- * The dependency install step per package manager. npm and yarn ride
- * setup-node's cache (both binaries are preinstalled on the runner); pnpm and
- * bun are provisioned in the step itself - corepack first, so a repository
- * pinning a version via the `packageManager` field gets exactly that version,
- * with a global install as the fallback for repositories that pin nothing.
- */
-function installSteps(pm: WorkflowPackageManager, dir: string | null): string {
- const inDir = (file: string): string => (dir ? `${dir}/${file}` : file);
- const wd = dir ? `\n working-directory: ${dir}` : '';
- const guard = ` if: hashFiles('${inDir('package.json')}') != ''${wd}`;
- if (pm === 'pnpm') {
- return ` - name: Install dependencies
-${guard}
- run: |
- corepack enable 2>/dev/null || true
- command -v pnpm >/dev/null 2>&1 || npm install -g pnpm
- pnpm install --frozen-lockfile || pnpm install`;
- }
- if (pm === 'bun') {
- return ` - name: Install dependencies
-${guard}
- run: |
- command -v bun >/dev/null 2>&1 || npm install -g bun
- bun install --frozen-lockfile || bun install`;
- }
- if (pm === 'yarn') {
- return ` - name: Install dependencies
-${guard}
- # --immutable is Yarn Berry, --frozen-lockfile is classic; the plain
- # install covers a lockfile that has drifted, which both refuse.
- run: |
- corepack enable 2>/dev/null || true
- yarn install --immutable || yarn install --frozen-lockfile || yarn install`;
- }
- return ` - name: Install dependencies
-${guard}
- # ci skips dependency resolution entirely; the fallback covers a
- # lockfile that has drifted, which ci refuses and install repairs.
- # --no-audit/--no-fund drop two registry round trips nothing reads here.
- run: |
- if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then
- npm ci --prefer-offline --no-audit --no-fund || npm install --no-audit --no-fund
- else
- npm install --no-audit --no-fund
- fi`;
-}
-
-/**
- * Everything between checkout and deploy. This runs on every push, so the
- * install is worth tuning: restoring the package cache and using the frozen
- * install turns a cold ~60s install into ~15s. Cache is CONDITIONAL, because
- * a connected repository is not guaranteed to have a lockfile - and
- * `cache: npm` with no lockfile FAILS the job outright rather than quietly
- * skipping the cache, which would break the deploy we are supposed to be
- * speeding up. pnpm and bun skip the cache: setup-node's cache needs the
- * binary on PATH before this step, and they are installed after it.
- */
-function buildSteps(options: WorkflowBuildOptions = {}): string {
- const pm = options.packageManager ?? 'npm';
- const dir = options.rootDir ?? null;
- const inDir = (file: string): string => (dir ? `${dir}/${file}` : file);
- const cache =
- pm === 'npm'
- ? `\${{ hashFiles('${inDir('package-lock.json')}', '${inDir('npm-shrinkwrap.json')}') != '' && 'npm' || '' }}`
- : pm === 'yarn'
- ? `\${{ hashFiles('${inDir('yarn.lock')}') != '' && 'yarn' || '' }}`
- : `''`;
- // setup-node's cache hashes the ROOT lockfile by default; a monorepo's
- // lives beside (or above) the root directory, so point it explicitly.
- const cachePath =
- dir && (pm === 'npm' || pm === 'yarn')
- ? `\n cache-dependency-path: ${pm === 'yarn' ? inDir('yarn.lock') : inDir('package-lock.json')}`
- : '';
- const wd = dir ? `\n working-directory: ${dir}` : '';
- // A custom command runs unconditionally (Hugo has no package.json at all);
- // the default is guarded because `npm run` without one is a hard error.
- const buildIf = options.buildCommand
- ? wd
- : `\n if: hashFiles('${inDir('package.json')}') != ''${wd}`;
- const command = options.buildCommand ?? 'npm run build --if-present';
- return ` - uses: actions/setup-node@v4
- with:
- node-version: 22
- # Restores the package cache. That also holds npx's cache, so the
- # deploy step reuses it - npx still checks the registry for a newer
- # CLI, since a name-only spec is re-resolved on every run.
- cache: ${cache}${cachePath}
-${installSteps(pm, dir)}
- # Adjust to your stack; the command was chosen when the repository was
- # connected and reconnecting rewrites it.
- - name: Build${buildIf}
- # Bounded separately from the job so a hang is ATTRIBUTED. A job-level
- # timeout alone reports "the job was cancelled", which does not say
- # which step stopped - and a build that never exits is the likeliest
- # thing to stop here, since the runner cannot start the deploy until
- # this process exits.
- timeout-minutes: 10
- run: |
- # Node picks its own heap limit from the machine, which is fine
- # until a large SSR build reaches it - and reaching it does not
- # crash cleanly, it degrades into GC thrashing that looks exactly
- # like a hang, for hours. Set it explicitly to two thirds of this
- # runner's RAM: enough that a bigger runner is actually used,
- # conservative enough that the heap plus everything else stays
- # inside the box (overshooting trades a clean heap error for the
- # OOM killer, which is the worse failure).
- # Guarded on \`free\` so a non-Linux runs-on still builds, and
- # appended so a NODE_OPTIONS you set yourself still wins.
- if command -v free >/dev/null 2>&1; then
- export NODE_OPTIONS="--max-old-space-size=$(free -m | awk '/^Mem:/ {print int($2 * 2 / 3)}') $NODE_OPTIONS"
- fi
- ${command}`;
-}
-
-export function deployWorkflowYaml(): string {
- return `# Deploys this repository to Cloudflarebase on every push.
-# The default branch deploys production; any other branch deploys an
-# isolated preview at -.cfbase.dev. Requires the
-# ${DEPLOY_TOKEN_SECRET_NAME} repository secret (minted on the Hosting page).
-name: Deploy to Cloudflarebase
-
-on:
- push:
- branches: ['**']
-
-concurrency:
- group: cloudflarebase-\${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- deploy:
- runs-on: ubuntu-latest
- # GitHub's default is 360 minutes. A deploy that has not finished in 15 is
- # not slow, it is stuck - a build waiting on a prompt, a prerender fetching
- # a URL that never answers - and six hours of billed minutes is a bad way
- # to find that out. Raise it if a genuine build needs longer.
- timeout-minutes: 15
- steps:
- - uses: actions/checkout@v4
-${buildSteps()}
- - name: Deploy
- # Bounded so a stalled upload is attributed here, not to a job cancel.
- timeout-minutes: 5
- run: npx --yes @cloudflarebase/cli deploy
- env:
- ${DEPLOY_TOKEN_SECRET_NAME}: \${{ secrets.${DEPLOY_TOKEN_SECRET_NAME} }}
- CLOUDFLAREBASE_GIT_BRANCH: \${{ github.ref_name }}
- CLOUDFLAREBASE_DEFAULT_BRANCH: \${{ github.event.repository.default_branch }}
-`;
-}
-
-export interface ConnectedWorkflowInput {
- /** Console origin - also the OIDC audience the token is minted for. */
- origin: string;
- /** ROOT project id; the branch is derived from the pushed ref. */
- projectId: string;
- appName: string;
- /** Framework preset, resolved at connect time. All optional: a repo we
- * could not inspect gets the same generic workflow as before. */
- packageManager?: WorkflowPackageManager;
- buildCommand?: string | null;
- /** Published as CLOUDFLAREBASE_ASSETS, relative to rootDir; null lets the
- * CLI autodetect. */
- outputDir?: string | null;
- /** Monorepo root directory - every step runs here. Null = repo root. */
- rootDir?: string | null;
- /** Branch that deploys the root project. Null = the repository's default
- * branch, resolved dynamically (`github.event.repository.default_branch`),
- * which is what pre-existing workflows do. */
- productionBranch?: string | null;
- /** Pushes to these never build - emitted as `branches-ignore` (GitHub
- * refuses `branches` and `branches-ignore` on the same event, so the
- * trigger emits exactly one). Charset pre-validated by branchFilterSchema. */
- ignoredBranches?: string[];
-}
-
-/** The trigger block: catch-all, or the ignore list - never both. */
-function pushTrigger(ignoredBranches: string[] | undefined): string {
- if (!ignoredBranches?.length) return ` branches: ['**']`;
- return ` branches-ignore: [${ignoredBranches.map((branch) => `'${branch}'`).join(', ')}]`;
-}
-
-/**
- * The step that turns the app's stored build environment into env vars for
- * the build. Same OIDC identity the deploy step presents, minted here because
- * $GITHUB_ENV is the only way to reach the BUILD step's environment. Secret
- * values are masked before export so they never print in the job log. An app
- * with no build env gets `{vars:{},secrets:{}}` - the step only fails when
- * something is genuinely wrong (curl -sSf), and a build silently missing its
- * secrets is worse than one that fails attributed.
- */
-function fetchBuildEnvStep(input: { origin: string; projectId: string; appName: string }): string {
- return ` - name: Fetch build environment
- timeout-minutes: 2
- run: |
- OIDC=$(curl -sSf -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \\
- "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$CLOUDFLAREBASE_URL" | jq -r '.value')
- ENV_JSON=$(curl -sSf -H "Authorization: Bearer $OIDC" \\
- "$CLOUDFLAREBASE_URL/api/projects/$CLOUDFLAREBASE_PROJECT/hosting/apps/$CLOUDFLAREBASE_APP/build-env")
- echo "$ENV_JSON" | jq -r '.secrets | to_entries[] | .value' | while IFS= read -r v; do echo "::add-mask::$v"; done
- echo "$ENV_JSON" | jq -r '(.vars + .secrets) | to_entries[] | "\\(.key)=\\(.value)"' >> "$GITHUB_ENV"
- env:
- CLOUDFLAREBASE_URL: ${input.origin}
- CLOUDFLAREBASE_PROJECT: ${input.projectId}
- CLOUDFLAREBASE_APP: ${input.appName}`;
-}
-
-/**
- * The workflow written INTO the repository by the GitHub App (`build` mode).
- *
- * Two differences from the manual one above, both consequences of the App
- * knowing who this repo is:
- *
- * - **No secret.** `id-token: write` lets the job mint a short-lived OIDC
- * token describing the repository; the console verifies it against
- * GitHub's public keys and looks the repository up in its connection
- * table. Nothing is stored in the repo, so nothing can leak or need
- * rotating. The CLI mints it - there is no extra step here.
- * - **No `cloudflarebase.json` needed.** The console knows the project and
- * app at write time and passes them as env, so connecting in the console
- * is the whole setup: the operator never runs the CLI locally.
- */
-export function connectedWorkflowYaml(input: ConnectedWorkflowInput): string {
- const assetsLine = input.outputDir ? `\n CLOUDFLAREBASE_ASSETS: ${input.outputDir}` : '';
- // The CLI resolves root-vs-branch by comparing GIT_BRANCH against
- // DEFAULT_BRANCH, so a user-set production branch simply becomes the
- // literal value here - zero CLI changes, and pre-existing workflows keep
- // the dynamic default.
- const defaultBranch = input.productionBranch
- ? input.productionBranch
- : `\${{ github.event.repository.default_branch }}`;
- return `# Deploys this repository to Cloudflarebase on every push.
-# Managed by Cloudflarebase - saving build settings in the console (or
-# reconnecting the repository) rewrites this file.
-# The production branch deploys the root project; any other branch deploys an
-# isolated preview at -. Authentication is GitHub's OIDC token,
-# so this repository holds no Cloudflarebase secret.
-name: Deploy to Cloudflarebase (${input.appName})
-
-on:
- push:
-${pushTrigger(input.ignoredBranches)}
-
-concurrency:
- group: cloudflarebase-${input.appName}-\${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- deploy:
- runs-on: ubuntu-latest
- # GitHub's default is 360 minutes. A deploy that has not finished in 15 is
- # not slow, it is stuck - a build waiting on a prompt, a prerender fetching
- # a URL that never answers - and six hours of billed minutes is a bad way
- # to find that out. Raise it if a genuine build needs longer.
- timeout-minutes: 15
- permissions:
- contents: read
- # Mints the identity token the console verifies. Without it the deploy
- # has no credential at all and will be refused.
- id-token: write
- steps:
- - uses: actions/checkout@v4
-${fetchBuildEnvStep(input)}
-${buildSteps({ packageManager: input.packageManager, buildCommand: input.buildCommand, rootDir: input.rootDir })}
- - name: Deploy${input.rootDir ? `\n working-directory: ${input.rootDir}` : ''}
- # Bounded so a stalled upload is attributed here, not to a job cancel.
- timeout-minutes: 5
- run: npx --yes @cloudflarebase/cli deploy
- env:
- CLOUDFLAREBASE_URL: ${input.origin}
- CLOUDFLAREBASE_PROJECT: ${input.projectId}
- CLOUDFLAREBASE_APP: ${input.appName}${assetsLine}
- CLOUDFLAREBASE_GIT_BRANCH: \${{ github.ref_name }}
- CLOUDFLAREBASE_DEFAULT_BRANCH: ${defaultBranch}
-`;
-}
-
-/** GitHub's new-file editor with the workflow pre-filled - the manual path,
- * for consoles with no GitHub App configured. */
-export function workflowCreateUrl(repo: string): string {
- return `https://github.com/${repo}/new/main?filename=${encodeURIComponent(WORKFLOW_FILENAME)}&value=${encodeURIComponent(deployWorkflowYaml())}`;
-}
-
-/** Where the deploy token gets pasted. */
-export function secretsUrl(repo: string): string {
- return `https://github.com/${repo}/settings/secrets/actions/new`;
-}
diff --git a/src/lib/hosting-workflow.unit.test.ts b/src/lib/hosting-workflow.unit.test.ts
deleted file mode 100644
index 030f184e..00000000
--- a/src/lib/hosting-workflow.unit.test.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-import assert from 'node:assert/strict';
-import { test } from 'node:test';
-import {
- connectedWorkflowYaml,
- deployWorkflowYaml,
- WORKFLOW_FILENAME,
- workflowPathFor
-} from './hosting-workflow';
-
-const base = { origin: 'https://cloudflarebase.com', projectId: 'demo-app', appName: 'landmatch' };
-
-test('the default connected workflow matches the pre-preset shape', () => {
- const yaml = connectedWorkflowYaml(base);
- assert.match(yaml, /npm ci --prefer-offline/);
- assert.match(yaml, /npm run build --if-present/);
- // The default build is guarded: `npm run` without a package.json is a
- // hard error, not a skip.
- assert.match(yaml, /- name: Build\n {8}if: hashFiles\('package\.json'\) != ''/);
- assert.doesNotMatch(yaml, /CLOUDFLAREBASE_ASSETS/);
- assert.match(yaml, /id-token: write/);
-});
-
-test('a preset command replaces the default and drops its package.json guard', () => {
- const yaml = connectedWorkflowYaml({ ...base, buildCommand: 'hugo --minify' });
- assert.match(yaml, /\n {10}hugo --minify\n/);
- assert.doesNotMatch(yaml, /npm run build --if-present/);
- assert.doesNotMatch(yaml, /- name: Build\n {8}if:/);
-});
-
-test('the output directory travels as CLOUDFLAREBASE_ASSETS', () => {
- const yaml = connectedWorkflowYaml({ ...base, outputDir: '.output/public' });
- assert.match(yaml, /\n {10}CLOUDFLAREBASE_ASSETS: \.output\/public\n/);
-});
-
-test('pnpm repositories install with pnpm and skip the setup-node cache', () => {
- const yaml = connectedWorkflowYaml({
- ...base,
- packageManager: 'pnpm',
- buildCommand: 'pnpm run build'
- });
- assert.match(yaml, /pnpm install --frozen-lockfile \|\| pnpm install/);
- assert.match(yaml, /cache: ''/);
- assert.match(yaml, /corepack enable/);
- assert.doesNotMatch(yaml, /npm ci/);
-});
-
-test('yarn keeps the cache and tries immutable before frozen-lockfile', () => {
- const yaml = connectedWorkflowYaml({ ...base, packageManager: 'yarn' });
- assert.match(yaml, /yarn install --immutable \|\| yarn install --frozen-lockfile/);
- assert.match(yaml, /hashFiles\('yarn\.lock'\)/);
-});
-
-test('every step that can hang carries its own timeout', () => {
- for (const yaml of [connectedWorkflowYaml(base), deployWorkflowYaml()]) {
- const buildTimeouts = yaml.match(/timeout-minutes: 10/g) ?? [];
- const deployTimeouts = yaml.match(/timeout-minutes: 5/g) ?? [];
- assert.equal(buildTimeouts.length, 1, 'the build step is bounded');
- assert.equal(deployTimeouts.length, 1, 'the deploy step is bounded');
- }
-});
-
-test('a root directory scopes every step and re-points the hashFiles guards', () => {
- const yaml = connectedWorkflowYaml({ ...base, rootDir: 'sites/blabla' });
- // hashFiles resolves from the workspace root regardless of
- // working-directory, so every guard must carry the prefix.
- assert.match(yaml, /hashFiles\('sites\/blabla\/package\.json'\)/);
- assert.doesNotMatch(yaml, /hashFiles\('package\.json'\)/);
- assert.match(
- yaml,
- /hashFiles\('sites\/blabla\/package-lock\.json', 'sites\/blabla\/npm-shrinkwrap\.json'\)/
- );
- assert.match(yaml, /cache-dependency-path: sites\/blabla\/package-lock\.json/);
- // Install, build, and deploy all run in the subdirectory.
- const wd = yaml.match(/working-directory: sites\/blabla/g) ?? [];
- assert.equal(wd.length, 3);
-});
-
-test('no root directory means no working-directory lines at all', () => {
- const yaml = connectedWorkflowYaml(base);
- assert.doesNotMatch(yaml, /working-directory/);
- assert.doesNotMatch(yaml, /cache-dependency-path/);
-});
-
-test('per-app workflow paths never collide with the legacy shared file', () => {
- assert.equal(workflowPathFor('landmatch'), '.github/workflows/cloudflarebase-landmatch.yml');
- assert.notEqual(workflowPathFor('landmatch'), workflowPathFor('docs'));
- assert.notEqual(workflowPathFor('landmatch'), WORKFLOW_FILENAME);
-});
-
-test('the trigger emits branches XOR branches-ignore, never both', () => {
- // GitHub refuses a push trigger that carries both keys.
- const open = connectedWorkflowYaml(base);
- assert.match(open, /branches: \['\*\*'\]/);
- assert.doesNotMatch(open, /branches-ignore/);
-
- const filtered = connectedWorkflowYaml({ ...base, ignoredBranches: ['tmp', 'renovate/*'] });
- assert.match(filtered, /branches-ignore: \['tmp', 'renovate\/\*'\]/);
- assert.doesNotMatch(filtered, /branches: \['\*\*'\]/);
-});
-
-test('a user-set production branch becomes the literal DEFAULT_BRANCH', () => {
- // The CLI resolves root-vs-branch by comparing GIT_BRANCH against
- // DEFAULT_BRANCH, so the literal is the whole mechanism - no CLI change.
- const yaml = connectedWorkflowYaml({ ...base, productionBranch: 'release' });
- assert.match(yaml, /CLOUDFLAREBASE_DEFAULT_BRANCH: release\n/);
- assert.doesNotMatch(yaml, /github\.event\.repository\.default_branch/);
-
- const dynamic = connectedWorkflowYaml(base);
- assert.match(
- dynamic,
- /CLOUDFLAREBASE_DEFAULT_BRANCH: \$\{\{ github\.event\.repository\.default_branch \}\}/
- );
-});
-
-test('the build-env fetch runs before the build and masks secrets', () => {
- const yaml = connectedWorkflowYaml(base);
- assert.match(yaml, /- name: Fetch build environment/);
- // Order is the contract: env must exist before the build step reads it.
- assert.ok(yaml.indexOf('Fetch build environment') < yaml.indexOf('- name: Build'));
- // Secrets are masked BEFORE they land in $GITHUB_ENV, so they never print.
- assert.ok(yaml.indexOf('::add-mask::') < yaml.indexOf('>> "$GITHUB_ENV"'));
- // The OIDC audience is the console origin, same as the deploy step's token.
- assert.match(yaml, /audience=\$CLOUDFLAREBASE_URL/);
- // -sSf: a failed fetch fails the step - a build silently missing its
- // secrets is worse than one that fails attributed.
- assert.match(yaml, /curl -sSf/);
-});
-
-test('a fully-optioned workflow carries every setting at once', () => {
- const yaml = connectedWorkflowYaml({
- ...base,
- packageManager: 'pnpm',
- buildCommand: 'pnpm run build',
- outputDir: 'dist',
- rootDir: 'apps/web',
- productionBranch: 'release/stable',
- ignoredBranches: ['tmp', 'wip-*']
- });
- assert.match(yaml, /branches-ignore: \['tmp', 'wip-\*'\]/);
- assert.match(yaml, /CLOUDFLAREBASE_DEFAULT_BRANCH: release\/stable\n/);
- assert.match(yaml, /CLOUDFLAREBASE_ASSETS: dist\n/);
- assert.match(yaml, /pnpm run build/);
- assert.match(yaml, /name: Deploy to Cloudflarebase \(landmatch\)/);
- // Two apps in one repo must not cancel each other's runs.
- assert.match(yaml, /group: cloudflarebase-landmatch-\$\{\{ github\.ref \}\}/);
-});
-
-test('YAML stays tab-free and the run blocks stay indented', () => {
- const yaml = connectedWorkflowYaml({
- ...base,
- packageManager: 'bun',
- buildCommand: 'bun run build && bun run postbuild',
- outputDir: 'dist',
- rootDir: 'apps/web'
- });
- assert.doesNotMatch(yaml, /\t/);
- // Every non-empty line inside the file is indented or a top-level key.
- for (const line of yaml.split('\n')) {
- if (!line) continue;
- assert.ok(/^( |#|name:|on:|concurrency:|jobs:)/.test(line), `unexpected line: ${line}`);
- }
-});
diff --git a/src/lib/openapi/hosting.ts b/src/lib/openapi/hosting.ts
deleted file mode 100644
index 15824a8b..00000000
--- a/src/lib/openapi/hosting.ts
+++ /dev/null
@@ -1,470 +0,0 @@
-import {
- deployTokenSchema,
- githubConnectionPatchSchema,
- githubConnectionSchema,
- githubConnectRequestSchema,
- hostingAnalyticsSchema,
- hostingBuildEnvBundleSchema,
- hostingBuildEnvSchema,
- hostingBuildSecretRequestSchema,
- hostingClaimRequestSchema,
- hostingClaimSchema,
- hostingDeployPageSchema,
- hostingOverviewSchema,
- hostingSecretListSchema,
- hostingSecretRequestSchema,
- hostingVarListSchema,
- hostingVarsUpdateSchema,
- hostingVarsUpdatedSchema,
- mintDeployTokenSchema,
- mintedDeployTokenSchema
-} from '$lib/agents';
-import { jsonBody, jsonResponse, ref, UNAUTHORIZED, type AgentOpenApiModule } from './shared';
-
-/**
- * Hosting agent module (Phase B). Every
- * endpoint here is operator-plane: apps are deployed BY the project owner,
- * not called by their application, so nothing is public. Claims and deploy
- * tokens are console-plane (control-plane D1) but documented under the same
- * tag - to an operator they are one Hosting surface.
- */
-
-const HOSTING_TAG = 'Hosting';
-
-const appParam = {
- name: 'app',
- in: 'path',
- required: true,
- description: 'The app name (not the subdomain - taken names auto-number).',
- schema: { type: 'string' }
-};
-
-export const hostingOpenApi: AgentOpenApiModule = {
- tags: [
- {
- name: HOSTING_TAG,
- description: [
- 'Apps and functions on Workers for Platforms: static assets and server',
- 'code in one deploy, served at `.cfbase.dev` (root project) or',
- '`-.cfbase.dev` (branch). Deploys ride an operator session',
- 'or a project-scoped deploy token (`cfbd_...` bearer), which is also',
- 'accepted on `POST /branches` so CI can create preview branches.'
- ].join('\n')
- }
- ],
- schemas: [
- hostingOverviewSchema,
- hostingDeployPageSchema,
- hostingClaimSchema,
- hostingClaimRequestSchema,
- hostingVarListSchema,
- hostingVarsUpdateSchema,
- hostingVarsUpdatedSchema,
- hostingSecretListSchema,
- hostingSecretRequestSchema,
- hostingBuildEnvSchema,
- hostingBuildEnvBundleSchema,
- hostingBuildSecretRequestSchema,
- hostingAnalyticsSchema,
- deployTokenSchema,
- mintDeployTokenSchema,
- mintedDeployTokenSchema,
- githubConnectionSchema,
- githubConnectRequestSchema,
- githubConnectionPatchSchema
- ],
- paths: {
- '/hosting/overview': {
- get: {
- tags: [HOSTING_TAG],
- summary: 'Hosting overview',
- description: 'Apps, recent deploys, and whether this install can complete real deploys.',
- security: [{ sessionCookie: [] }],
- responses: {
- '200': jsonResponse(hostingOverviewSchema, 'The overview.'),
- '401': UNAUTHORIZED
- }
- }
- },
- '/hosting/deploys': {
- get: {
- tags: [HOSTING_TAG],
- summary: 'List deploys',
- description: 'Keyset-paged deploy history, newest first. Filter with `?app=`.',
- security: [{ sessionCookie: [] }],
- parameters: [
- { name: 'app', in: 'query', schema: { type: 'string' } },
- { name: 'cursor', in: 'query', schema: { type: 'string' } },
- { name: 'limit', in: 'query', schema: { type: 'integer', maximum: 100 } }
- ],
- responses: {
- '200': jsonResponse(hostingDeployPageSchema, 'One page of deploys.'),
- '401': UNAUTHORIZED
- }
- }
- },
- '/hosting/claims': {
- post: {
- tags: [HOSTING_TAG],
- summary: 'Claim a subdomain',
- description:
- 'Resolves the subdomain for this project+app: the persisted claim wins; otherwise the wanted name (`` on the root, `-` on a branch) auto-numbers past collisions - it never fails on a taken name. `dry: true` previews without claiming.',
- security: [{ sessionCookie: [] }],
- requestBody: jsonBody(hostingClaimRequestSchema, 'The app to claim for.'),
- responses: {
- '200': jsonResponse(hostingClaimSchema, 'Existing claim reused (or dry run).'),
- '201': jsonResponse(hostingClaimSchema, 'Subdomain claimed.'),
- '400': { description: 'Invalid app name.' },
- '401': UNAUTHORIZED,
- '403': { description: 'Demo projects cannot deploy apps.' },
- '409': { description: 'App limit reached.' }
- }
- }
- },
- '/hosting/apps/{app}/deploys': {
- post: {
- tags: [HOSTING_TAG],
- summary: 'Deploy an app',
- description:
- 'Multipart deploy: a `meta` JSON part plus `module:` and `asset:` file parts. The console resolves the subdomain claim, then the agent runs the Workers for Platforms upload. Accepts an operator session or a deploy token. The response reports the subdomain that was ACTUALLY claimed.',
- security: [{ sessionCookie: [] }, { bearerAuth: [] }],
- parameters: [appParam],
- responses: {
- '201': { description: 'Deployed; the body carries the deploy, subdomain, and URL.' },
- '400': { description: 'Invalid deploy payload or a cap was exceeded.' },
- '401': UNAUTHORIZED,
- '403': { description: 'Demo projects cannot deploy apps.' },
- '429': { description: 'Daily deploy limit reached.' },
- '503': { description: 'Hosting is not configured on this install.' }
- }
- }
- },
- '/hosting/apps/{app}/vars': {
- get: {
- tags: [HOSTING_TAG],
- summary: 'List runtime variables',
- description:
- 'The stored plain-text variables for one app. They apply as bindings on every deploy; stored values win over the CLI-declared `meta.vars` of the same name.',
- security: [{ sessionCookie: [] }],
- parameters: [appParam],
- responses: {
- '200': jsonResponse(hostingVarListSchema, 'The variables.'),
- '401': UNAUTHORIZED
- }
- },
- put: {
- tags: [HOSTING_TAG],
- summary: 'Replace runtime variables',
- description:
- 'Replaces the whole set - absent names are deleted. The store always succeeds; when the app has a live deploy the script is patched in place, reported by `patched` (false = the change applies at the next deploy).',
- security: [{ sessionCookie: [] }],
- parameters: [appParam],
- requestBody: jsonBody(hostingVarsUpdateSchema, 'The full variable set.'),
- responses: {
- '200': jsonResponse(hostingVarsUpdatedSchema, 'The stored set and patch outcome.'),
- '400': { description: 'Invalid name/value, or the 64-variable cap was exceeded.' },
- '401': UNAUTHORIZED
- }
- }
- },
- '/hosting/apps/{app}/secrets': {
- get: {
- tags: [HOSTING_TAG],
- summary: 'List secrets',
- description: 'Names and timestamps only - values are write-through and unrecoverable.',
- security: [{ sessionCookie: [] }],
- parameters: [appParam],
- responses: {
- '200': jsonResponse(hostingSecretListSchema, 'The secret names.'),
- '401': UNAUTHORIZED
- }
- },
- post: {
- tags: [HOSTING_TAG],
- summary: 'Set a secret',
- description:
- 'Writes one secret through to the deployed script (`cloudflarebase secret put`). Deploys use `keep_bindings`, so redeploys never drop it. Requires a deployed app.',
- security: [{ sessionCookie: [] }],
- parameters: [appParam],
- requestBody: jsonBody(hostingSecretRequestSchema, 'The secret.'),
- responses: {
- '200': { description: 'Set.' },
- '400': { description: 'Invalid body, or the 64-secret cap was exceeded.' },
- '401': UNAUTHORIZED,
- '404': { description: 'No such app (deploy first).' },
- '503': { description: 'Hosting is not configured on this install.' }
- }
- }
- },
- '/hosting/apps/{app}/secrets/{name}': {
- delete: {
- tags: [HOSTING_TAG],
- summary: 'Delete a secret',
- description: 'Removes the script binding and the name record. Idempotent.',
- security: [{ sessionCookie: [] }],
- parameters: [
- appParam,
- { name: 'name', in: 'path', required: true, schema: { type: 'string' } }
- ],
- responses: {
- '200': { description: 'Deleted (or was already gone).' },
- '401': UNAUTHORIZED,
- '502': { description: 'Cloudflare refused the deletion; the secret is still set.' }
- }
- }
- },
- '/hosting/apps/{app}/analytics': {
- get: {
- tags: [HOSTING_TAG],
- summary: 'App request analytics',
- description:
- 'Daily requests and 5xx errors for one app, from Analytics Engine. Degrades honestly: `engine.status` reports connected/local/write-only/error and the route never 5xxes on a query failure.',
- security: [{ sessionCookie: [] }],
- parameters: [
- appParam,
- { name: 'days', in: 'query', schema: { type: 'integer', enum: [7, 30, 90] } }
- ],
- responses: {
- '200': jsonResponse(hostingAnalyticsSchema, 'The series and totals.'),
- '401': UNAUTHORIZED,
- '404': { description: 'No such app (deploy first).' }
- }
- }
- },
- '/hosting/apps/{app}/build-env': {
- get: {
- tags: [HOSTING_TAG],
- summary: 'Read the build-time environment',
- description:
- 'Two personas. An operator session gets `HostingBuildEnv`: vars with values, secret NAMES only. A GitHub Actions OIDC bearer (the workflow of the connection that owns this app) gets `HostingBuildEnvBundle`: the decrypted values it exports before the build step. Build env is connection-scoped - branch builds read the root project.',
- security: [{ sessionCookie: [] }, { bearerAuth: [] }],
- parameters: [appParam],
- responses: {
- '200': {
- description: 'The environment, shaped by the caller (see description).',
- content: {
- 'application/json': {
- schema: {
- oneOf: [ref(hostingBuildEnvSchema), ref(hostingBuildEnvBundleSchema)]
- }
- }
- }
- },
- '401': UNAUTHORIZED,
- '503': {
- description: 'Build secrets exist but HOSTING_MASTER_KEY is not set on this install.'
- }
- }
- }
- },
- '/hosting/apps/{app}/build-vars': {
- put: {
- tags: [HOSTING_TAG],
- summary: 'Replace build-time variables',
- description:
- 'Replaces the whole set - absent names are deleted. Exported into the build environment by the connected workflow.',
- security: [{ sessionCookie: [] }],
- parameters: [appParam],
- requestBody: jsonBody(hostingVarsUpdateSchema, 'The full variable set.'),
- responses: {
- '200': jsonResponse(hostingVarListSchema, 'The stored set.'),
- '400': { description: 'Invalid name/value, or the 64-variable cap was exceeded.' },
- '401': UNAUTHORIZED
- }
- }
- },
- '/hosting/apps/{app}/build-secrets/{name}': {
- put: {
- tags: [HOSTING_TAG],
- summary: 'Set a build secret',
- description:
- "Encrypted at rest (AES-256-GCM) under the install's master key. Write-only on this surface: the console never reads a value back, and the runner's copy travels the OIDC build-env route.",
- security: [{ sessionCookie: [] }],
- parameters: [
- appParam,
- { name: 'name', in: 'path', required: true, schema: { type: 'string' } }
- ],
- requestBody: jsonBody(hostingBuildSecretRequestSchema, 'The secret value.'),
- responses: {
- '200': { description: 'Set.' },
- '400': { description: 'Invalid name/value, or the 32-secret cap was exceeded.' },
- '401': UNAUTHORIZED,
- '503': { description: 'HOSTING_MASTER_KEY is not set on this install.' }
- }
- },
- delete: {
- tags: [HOSTING_TAG],
- summary: 'Delete a build secret',
- security: [{ sessionCookie: [] }],
- parameters: [
- appParam,
- { name: 'name', in: 'path', required: true, schema: { type: 'string' } }
- ],
- responses: {
- '200': { description: 'Deleted (or was already gone).' },
- '401': UNAUTHORIZED
- }
- }
- },
- '/hosting/tokens': {
- get: {
- tags: [HOSTING_TAG],
- summary: 'List deploy tokens',
- description: 'Metadata only - secrets are stored as SHA-256 digests and unrecoverable.',
- security: [{ sessionCookie: [] }],
- responses: {
- '200': {
- description: 'The tokens.',
- content: {
- 'application/json': {
- schema: {
- type: 'object',
- properties: {
- tokens: { type: 'array', items: { $ref: '#/components/schemas/DeployToken' } }
- },
- required: ['tokens']
- }
- }
- }
- },
- '401': UNAUTHORIZED
- }
- },
- post: {
- tags: [HOSTING_TAG],
- summary: 'Mint a deploy token',
- description:
- 'Root projects only; the token covers the root and all its branches. The secret appears exactly once in this response.',
- security: [{ sessionCookie: [] }],
- requestBody: jsonBody(mintDeployTokenSchema, 'A label for the token.'),
- responses: {
- '201': jsonResponse(mintedDeployTokenSchema, 'The minted token.'),
- '400': { description: 'Invalid name, or the project is a branch.' },
- '401': UNAUTHORIZED,
- '409': { description: 'Token limit reached.' }
- }
- }
- },
- '/hosting/tokens/{tokenId}': {
- delete: {
- tags: [HOSTING_TAG],
- summary: 'Revoke a deploy token',
- security: [{ sessionCookie: [] }],
- parameters: [{ name: 'tokenId', in: 'path', required: true, schema: { type: 'string' } }],
- responses: {
- '200': { description: 'Revoked.' },
- '401': UNAUTHORIZED,
- '404': { description: 'No such token.' }
- }
- }
- },
- '/hosting/github': {
- get: {
- tags: [HOSTING_TAG],
- summary: 'GitHub push-to-deploy state',
- description:
- 'Whether a GitHub App is configured on this console, the installations visible to this organization, and the repository connections.',
- security: [{ sessionCookie: [] }],
- responses: {
- '200': {
- description: 'The state.',
- content: {
- 'application/json': {
- schema: {
- type: 'object',
- properties: {
- configured: { type: 'boolean' },
- installations: {
- type: 'array',
- items: {
- type: 'object',
- properties: {
- id: { type: 'integer' },
- accountLogin: { type: 'string' }
- },
- required: ['id', 'accountLogin']
- }
- },
- connections: {
- type: 'array',
- items: { $ref: '#/components/schemas/GithubConnection' }
- }
- },
- required: ['configured', 'installations', 'connections']
- }
- }
- }
- },
- '401': UNAUTHORIZED
- }
- }
- },
- '/hosting/github/connections': {
- post: {
- tags: [HOSTING_TAG],
- summary: 'Connect a repository',
- description:
- 'Connects a repository to this project+app (root projects only). `build` mode commits a deploy workflow into the repository and trusts its Actions OIDC token; `direct` mode deploys the pushed tree from the webhook with no file in the repository. Claims the subdomain first.',
- security: [{ sessionCookie: [] }],
- requestBody: jsonBody(githubConnectRequestSchema, 'The repository and its settings.'),
- responses: {
- '201': {
- description: 'Connected; the body carries the connection and the claimed subdomain.'
- },
- '400': { description: 'Invalid request, or the project is a branch.' },
- '401': UNAUTHORIZED,
- '403': { description: 'The installation belongs to another organization.' },
- '404': { description: 'The repository is not in this installation.' },
- '503': { description: 'No GitHub App is configured on this console.' }
- }
- }
- },
- '/hosting/github/connections/{app}': {
- patch: {
- tags: [HOSTING_TAG],
- summary: 'Edit build settings',
- description:
- 'Edits a connection at any time: build command, root directory, output directory, production branch, ignored branches. On a build-mode connection the workflow file is rewritten in the repository FIRST - a failed commit changes nothing.',
- security: [{ sessionCookie: [] }],
- parameters: [appParam],
- requestBody: jsonBody(githubConnectionPatchSchema, 'The fields to change.'),
- responses: {
- '200': {
- description: 'Updated.',
- content: {
- 'application/json': {
- schema: {
- type: 'object',
- properties: {
- connection: { $ref: '#/components/schemas/GithubConnection' },
- workflowRewritten: { type: 'boolean' }
- },
- required: ['connection', 'workflowRewritten']
- }
- }
- }
- },
- '400': {
- description:
- 'Invalid value, build fields on a direct connection, or the production branch would be ignored.'
- },
- '401': UNAUTHORIZED,
- '404': { description: 'No such connection.' },
- '503': { description: 'No GitHub App is configured on this console.' }
- }
- },
- delete: {
- tags: [HOSTING_TAG],
- summary: 'Disconnect a repository',
- description: 'Deletes the connection; the committed workflow file is removed best-effort.',
- security: [{ sessionCookie: [] }],
- parameters: [appParam],
- responses: {
- '200': { description: 'Disconnected.' },
- '401': UNAUTHORIZED,
- '404': { description: 'No such connection.' }
- }
- }
- }
- }
-};
diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts
index 47ea2d01..b54c0c68 100644
--- a/src/lib/openapi/index.ts
+++ b/src/lib/openapi/index.ts
@@ -2,7 +2,6 @@ import { z } from 'zod';
import { authOpenApi } from './auth';
import { consoleOpenApi } from './console';
import { dbOpenApi } from './db';
-import { hostingOpenApi } from './hosting';
import { storageOpenApi } from './storage';
import type { AgentOpenApiModule } from './shared';
@@ -21,13 +20,7 @@ import type { AgentOpenApiModule } from './shared';
* what z.toJSONSchema emits, so the component schemas need no translation.
*/
-const MODULES: AgentOpenApiModule[] = [
- authOpenApi,
- dbOpenApi,
- hostingOpenApi,
- storageOpenApi,
- consoleOpenApi
-];
+const MODULES: AgentOpenApiModule[] = [authOpenApi, dbOpenApi, storageOpenApi, consoleOpenApi];
/** Named schemas that become components.schemas entries. */
const registry = z.registry<{ id: string }>();
@@ -107,7 +100,7 @@ export function buildOpenApiDocument({ projectId, origin }: OpenApiOptions) {
'it bypasses access modes, document validators, and permission keys exactly as an operator ' +
'session does, and it is scoped to THIS project only - not to sibling branches, because for ' +
'data the branch is the isolation boundary. It cannot mint credentials, re-role operators, ' +
- 'deploy, or delete its own project. ' +
+ 'or delete its own project. ' +
'**Any request carrying an `Origin` header is refused, whatever the key**, so a key pasted ' +
'into frontend code fails at your desk instead of shipping in a JS bundle; server HTTP ' +
'clients send no Origin. Note that SvelteKit refuses form content types (`text/plain`, ' +
diff --git a/src/lib/openapi/shared.ts b/src/lib/openapi/shared.ts
index bf06080e..6c9be7f4 100644
--- a/src/lib/openapi/shared.ts
+++ b/src/lib/openapi/shared.ts
@@ -49,7 +49,7 @@ export const UNAUTHORIZED = {
*
* Apply this only where `isServiceKeySurface` actually admits a key - the db
* and storage prefixes, the auth agent's `/admin/*`, `/overview`, and
- * `/analytics`. Everything else (hosting, the registry, `/chat`) stays
+ * `/analytics`. Everything else (the registry, `/chat`) stays
* session-only, and saying otherwise in the reference would send people to
* write code against a 401.
*/
diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts
index b1302f02..19ba663c 100644
--- a/src/lib/server/db/index.ts
+++ b/src/lib/server/db/index.ts
@@ -44,23 +44,6 @@ const SCHEMA_STATEMENTS = [
created_at integer DEFAULT (unixepoch() * 1000) NOT NULL
)`,
`CREATE INDEX IF NOT EXISTS chat_message_thread ON chat_message (project_id, client_key, created_at)`,
- `CREATE TABLE IF NOT EXISTS app (
- subdomain text PRIMARY KEY NOT NULL,
- project_id text NOT NULL,
- app_name text NOT NULL,
- created_at integer DEFAULT (unixepoch() * 1000) NOT NULL
- )`,
- `CREATE INDEX IF NOT EXISTS app_project ON app (project_id, app_name)`,
- `CREATE TABLE IF NOT EXISTS deploy_token (
- id text PRIMARY KEY NOT NULL,
- project_id text NOT NULL,
- name text NOT NULL,
- token_hash text NOT NULL,
- created_at integer DEFAULT (unixepoch() * 1000) NOT NULL,
- last_used_at integer
- )`,
- `CREATE INDEX IF NOT EXISTS deploy_token_project ON deploy_token (project_id)`,
- `CREATE INDEX IF NOT EXISTS deploy_token_hash ON deploy_token (token_hash)`,
`CREATE TABLE IF NOT EXISTS service_key (
id text PRIMARY KEY NOT NULL,
project_id text NOT NULL,
@@ -71,37 +54,7 @@ const SCHEMA_STATEMENTS = [
last_used_at integer
)`,
`CREATE INDEX IF NOT EXISTS service_key_project ON service_key (project_id)`,
- `CREATE INDEX IF NOT EXISTS service_key_hash ON service_key (key_hash)`,
- `CREATE TABLE IF NOT EXISTS github_installation (
- id integer PRIMARY KEY NOT NULL,
- org_id text,
- account_login text NOT NULL,
- installed_by text NOT NULL,
- created_at integer DEFAULT (unixepoch() * 1000) NOT NULL
- )`,
- `CREATE INDEX IF NOT EXISTS github_installation_org ON github_installation (org_id)`,
- `CREATE TABLE IF NOT EXISTS github_connection (
- id text PRIMARY KEY NOT NULL,
- project_id text NOT NULL,
- app_name text NOT NULL,
- installation_id integer NOT NULL,
- repo_id integer NOT NULL,
- repo_full_name text NOT NULL,
- default_branch text NOT NULL,
- mode text NOT NULL,
- assets_dir text,
- build_command text,
- root_dir text,
- production_branch text,
- ignored_branches text,
- workflow_path text,
- package_manager text,
- created_at integer DEFAULT (unixepoch() * 1000) NOT NULL,
- last_event_at integer
- )`,
- `CREATE UNIQUE INDEX IF NOT EXISTS github_connection_app ON github_connection (project_id, app_name)`,
- `CREATE INDEX IF NOT EXISTS github_connection_repo ON github_connection (repo_id)`,
- `CREATE INDEX IF NOT EXISTS github_connection_installation ON github_connection (installation_id)`
+ `CREATE INDEX IF NOT EXISTS service_key_hash ON service_key (key_hash)`
];
/**
@@ -115,12 +68,6 @@ const UPGRADE_STATEMENTS = [
`ALTER TABLE project ADD COLUMN parent_id text`,
`ALTER TABLE project ADD COLUMN branch_name text`,
`ALTER TABLE project ADD COLUMN org_id text`,
- `ALTER TABLE github_connection ADD COLUMN build_command text`,
- `ALTER TABLE github_connection ADD COLUMN root_dir text`,
- `ALTER TABLE github_connection ADD COLUMN production_branch text`,
- `ALTER TABLE github_connection ADD COLUMN ignored_branches text`,
- `ALTER TABLE github_connection ADD COLUMN workflow_path text`,
- `ALTER TABLE github_connection ADD COLUMN package_manager text`,
`CREATE INDEX IF NOT EXISTS project_parent ON project (parent_id)`,
`CREATE INDEX IF NOT EXISTS project_org ON project (org_id)`
];
diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts
index aaf1bd91..ab1c96c2 100644
--- a/src/lib/server/db/schema.ts
+++ b/src/lib/server/db/schema.ts
@@ -1,12 +1,5 @@
import { sql } from 'drizzle-orm';
-import {
- index,
- integer,
- primaryKey,
- sqliteTable,
- text,
- uniqueIndex
-} from 'drizzle-orm/sqlite-core';
+import { index, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
/**
* Control-plane schema, held in D1 on the dashboard Worker.
@@ -71,68 +64,13 @@ export const demoProject = sqliteTable(
(table) => [index('demo_project_created_at').on(table.createdAt)]
);
-/**
- * Hosting subdomain claims (Phase B). The
- * dispatch namespace is global, so claims are control-plane state - no agent
- * may own the namespace without every project depending on that one instance.
- * One row per project+app: `project_id` is the FULL registry id (a branch is
- * its own registry row, so it is its own claim row), and `subdomain` is what
- * was ACTUALLY claimed under the auto-numbering rule - persisted on first
- * claim and reused verbatim, never re-derived, so URLs stay stable when
- * neighboring claims appear or are released.
- */
-export const app = sqliteTable(
- 'app',
- {
- /** The claimed subdomain of cfbase.dev; also the dispatch script name. */
- subdomain: text('subdomain').primaryKey(),
- projectId: text('project_id').notNull(),
- /** The operator-chosen app name the subdomain was derived from. */
- appName: text('app_name').notNull(),
- createdAt: integer('created_at', { mode: 'timestamp_ms' })
- .notNull()
- .default(sql`(unixepoch() * 1000)`)
- },
- (table) => [index('app_project').on(table.projectId, table.appName)]
-);
-
-export type AppRow = typeof app.$inferSelect;
-
-/**
- * Project-scoped deploy tokens (Phase B) -
- * the durable credential CI deploys ride, minted on ROOT projects and valid
- * for the root and its branches. Only the SHA-256 digest is stored, so a
- * control-plane leak never yields a working credential; the guard accepts
- * the `cfbd_` bearer solely on the deploy and branch-create endpoints.
- */
-export const deployToken = sqliteTable(
- 'deploy_token',
- {
- id: text('id').primaryKey(),
- projectId: text('project_id').notNull(),
- name: text('name').notNull(),
- tokenHash: text('token_hash').notNull(),
- createdAt: integer('created_at', { mode: 'timestamp_ms' })
- .notNull()
- .default(sql`(unixepoch() * 1000)`),
- lastUsedAt: integer('last_used_at', { mode: 'timestamp_ms' })
- },
- (table) => [
- index('deploy_token_project').on(table.projectId),
- index('deploy_token_hash').on(table.tokenHash)
- ]
-);
-
-export type DeployTokenRow = typeof deployToken.$inferSelect;
-
/**
* Project service keys - the credential a SERVER
* can hold, for the cases with no user to relay: crons, queue consumers,
- * webhook handlers, seed scripts, and backends we do not host.
+ * webhook handlers, seed scripts, and backends.
*
- * Scoped to ONE registry row, deliberately NOT to the root-and-branches family
- * deploy tokens use: for data the branch IS the isolation boundary, and a
- * preview key that reached production rows would make branches a lie.
+ * Scoped to ONE registry row, deliberately NOT to a root-and-branches family:
+ * for data the branch IS the isolation boundary, and a preview key that reached production rows would make branches a lie.
*
* Only the SHA-256 digest is stored, so a control-plane leak yields no working
* credential, and the secret is unrecoverable after the one-time reveal.
@@ -159,108 +97,6 @@ export const serviceKey = sqliteTable(
export type ServiceKeyRow = typeof serviceKey.$inferSelect;
-/**
- * GitHub App installations, bound to the console org that installed them.
- *
- * The binding is what stops one operator connecting another account's repo:
- * `installation_id` arrives from GitHub on a redirect the operator controls,
- * so it is only trustworthy at the moment the signed install `state` comes
- * back. The callback records the binding once; every later connect checks it
- * instead of trusting the id again. Null `org_id` matches a null-org project
- * (legacy/self-hosted rows, already visible to any operator).
- */
-export const githubInstallation = sqliteTable(
- 'github_installation',
- {
- /** GitHub's numeric installation id - the unit access tokens are minted for. */
- id: integer('id').primaryKey(),
- orgId: text('org_id'),
- /** The GitHub account the App is installed on (`acme`), for display. */
- accountLogin: text('account_login').notNull(),
- /** Console user id that completed the install - audit, and the fallback
- * owner when the project carries no org. */
- installedBy: text('installed_by').notNull(),
- createdAt: integer('created_at', { mode: 'timestamp_ms' })
- .notNull()
- .default(sql`(unixepoch() * 1000)`)
- },
- (table) => [index('github_installation_org').on(table.orgId)]
-);
-
-export type GithubInstallationRow = typeof githubInstallation.$inferSelect;
-
-/**
- * A repository connected to one project+app - the push-to-deploy link.
- *
- * Minted on a ROOT project and valid for its branches, exactly like a deploy
- * token: a push to the default branch deploys the root, any other branch
- * deploys `--`. Two modes, chosen per connection:
- *
- * - `build` writes `.github/workflows/cloudflarebase.yml` into the repo and
- * trusts the Actions OIDC token it presents. GitHub's runners do the build;
- * we run no build farm and hold no repo secret.
- * - `direct` needs no runner and no file in the repo at all: the push webhook
- * hands the pushed tree straight to the hosting agent. Only viable when
- * there is nothing to build, which is why `assets_dir` is captured here.
- *
- * `repo_id` is the numeric id, not the name: repositories get renamed and the
- * webhook must still find its connection. `repo_full_name` is what the OIDC
- * `repository` claim is matched against, and is re-synced from webhook
- * payloads so a rename cannot silently break build-mode trust.
- */
-export const githubConnection = sqliteTable(
- 'github_connection',
- {
- id: text('id').primaryKey(),
- /** The ROOT project; branches ride the same connection. */
- projectId: text('project_id').notNull(),
- appName: text('app_name').notNull(),
- installationId: integer('installation_id').notNull(),
- repoId: integer('repo_id').notNull(),
- repoFullName: text('repo_full_name').notNull(),
- defaultBranch: text('default_branch').notNull(),
- mode: text('mode').$type<'build' | 'direct'>().notNull(),
- /** Direct mode: repo-relative directory published as assets ('' = root).
- * Build mode: where the build lands (null = CLI autodetects). */
- assetsDir: text('assets_dir'),
- /** Build mode: the workflow's build command (framework preset or
- * operator-edited); null = the generic `npm run build --if-present`. */
- buildCommand: text('build_command'),
- /** Build mode: monorepo root - install/build/deploy run here.
- * Null = repository root. */
- rootDir: text('root_dir'),
- /** Branch that deploys the ROOT project; null = the repo's default
- * branch. Any other branch still deploys `--`. */
- productionBranch: text('production_branch'),
- /** JSON array of branch names and simple `*` globs; pushes to a matching
- * branch never deploy (webhook skip, workflow branches-ignore, and OIDC
- * grant refusal - all three enforce it). */
- ignoredBranches: text('ignored_branches'),
- /** Workflow file this connection committed (per-app names fix the
- * two-apps-one-repo collision); null = the legacy
- * `.github/workflows/cloudflarebase.yml`. */
- workflowPath: text('workflow_path'),
- /** Package manager detected at connect (npm/pnpm/yarn/bun), persisted so
- * a build-settings edit can re-render the workflow's install steps.
- * Null (legacy rows) = generic npm steps. */
- packageManager: text('package_manager'),
- createdAt: integer('created_at', { mode: 'timestamp_ms' })
- .notNull()
- .default(sql`(unixepoch() * 1000)`),
- /** Last push we accepted - the Hosting page's "last deploy from GitHub". */
- lastEventAt: integer('last_event_at', { mode: 'timestamp_ms' })
- },
- (table) => [
- // One repo per app. Not unique on repo_id: the same repository legitimately
- // deploys to several projects (a monorepo, or prod and a scratch project).
- uniqueIndex('github_connection_app').on(table.projectId, table.appName),
- index('github_connection_repo').on(table.repoId),
- index('github_connection_installation').on(table.installationId)
- ]
-);
-
-export type GithubConnectionRow = typeof githubConnection.$inferSelect;
-
/**
* Which agents a project has enabled. Groundwork from the agent contract: v1
* default-enables every registry agent and offers no opt-out UI, and deletion
diff --git a/src/lib/server/frameworks.ts b/src/lib/server/frameworks.ts
deleted file mode 100644
index f32b71d7..00000000
--- a/src/lib/server/frameworks.ts
+++ /dev/null
@@ -1,314 +0,0 @@
-/**
- * Framework presets for GitHub connections (* Phase B) - the CF-Pages-style table that turns "we read your package.json"
- * into a populated build command and output directory at connect time.
- *
- * Deliberately a pure module with zero imports so `npm run test:unit` can pin
- * the detection matrix without booting anything. The connect dialog shows what
- * was detected and the operator can always override; detection only decides
- * the PREFILL, never the contract.
- *
- * Two failure philosophies encoded here:
- * - A framework that needs a Cloudflare adapter it does not have gets a NOTE,
- * not a silent generic preset - the build would succeed and deploy the
- * wrong thing (or nothing), which is worse than saying so up front.
- * - A preset whose output directory we know is stated EXPLICITLY even when
- * the deploy would fail without it (Next.js without `output: 'export'`),
- * because a loud "out does not exist" beats autodetection finding the
- * repo's `public/` SOURCE directory and publishing that as the site.
- */
-
-export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
-
-export interface RepoFacts {
- /** dependencies + devDependencies, merged (version ranges unused). */
- dependencies: Record;
- scripts: Record;
- packageManager: PackageManager;
- /** File and directory names at the repository root. */
- rootEntries: string[];
- /** wrangler.jsonc / wrangler.json / wrangler.toml at the root. */
- hasWranglerConfig: boolean;
- /** next.config.* source, fetched only when `next` is a dependency. */
- nextConfigSource: string | null;
-}
-
-export interface FrameworkPreset {
- id: string;
- label: string;
- /** 'build' needs a runner; 'direct' is deployable as committed. */
- mode: 'build' | 'direct';
- /** Null = no runnable build (direct mode, or a missing script). */
- buildCommand: string | null;
- /** Null = wrangler-driven or autodetected at deploy time. */
- outputDir: string | null;
- /** Guidance the dialog surfaces - adapter missing, path caveats. */
- note: string | null;
- /** Set when the repo NEEDS a wrangler config it does not have and we
- * know exactly what it should say - connect commits it, nobody types it. */
- wrangler: WranglerTemplate | null;
-}
-
-export interface WranglerTemplate {
- main: string;
- assetsDirectory: string;
- compatibilityFlags: string[];
-}
-
-/**
- * The wrangler configs connect can commit on the operator's behalf. Only
- * frameworks whose adapter has a FIXED output contract belong here - the
- * server generates the file from this map and never from client content.
- */
-export const WRANGLER_TEMPLATES = {
- 'sveltekit-cloudflare': {
- main: '.svelte-kit/cloudflare/_worker.js',
- assetsDirectory: '.svelte-kit/cloudflare',
- compatibilityFlags: ['nodejs_als']
- },
- 'astro-cloudflare': {
- main: 'dist/_worker.js/index.js',
- assetsDirectory: 'dist',
- compatibilityFlags: ['nodejs_compat']
- },
- 'nextjs-opennext': {
- main: '.open-next/worker.js',
- assetsDirectory: '.open-next/assets',
- compatibilityFlags: ['nodejs_compat']
- }
-} as const satisfies Record;
-
-export type WranglerTemplateId = keyof typeof WRANGLER_TEMPLATES;
-
-/** Matches the hosting agent's default compatibility date. */
-const TEMPLATE_COMPATIBILITY_DATE = '2026-07-10';
-
-/** The wrangler.jsonc connect commits. The CLI reads main, assets.directory,
- * and the compatibility settings out of it at deploy time. */
-export function wranglerConfigJsonc(id: WranglerTemplateId, appName: string): string {
- const template = WRANGLER_TEMPLATES[id];
- const flags = template.compatibilityFlags.map((flag) => `"${flag}"`).join(', ');
- return `// Generated by Cloudflarebase when this repository was connected.
-// The build output declared here is what deploys; edit freely -
-// reconnecting never overwrites this file.
-{
- "name": "${appName}",
- "main": "${template.main}",
- "compatibility_date": "${TEMPLATE_COMPATIBILITY_DATE}",
- "compatibility_flags": [${flags}],
- "assets": {
- "directory": "${template.assetsDirectory}",
- "binding": "ASSETS"
- }
-}
-`;
-}
-
-/** Lockfile first, `packageManager` field as the tiebreak - a repo with a
- * pnpm lockfile is a pnpm repo no matter what the field says is newest. */
-export function detectPackageManager(
- rootEntries: string[],
- packageManagerField: string | null | undefined
-): PackageManager {
- const has = (name: string): boolean => rootEntries.includes(name);
- if (has('pnpm-lock.yaml')) return 'pnpm';
- if (has('bun.lockb') || has('bun.lock')) return 'bun';
- if (has('yarn.lock')) return 'yarn';
- if (has('package-lock.json') || has('npm-shrinkwrap.json')) return 'npm';
- const field = packageManagerField?.split('@')[0];
- if (field === 'pnpm' || field === 'yarn' || field === 'bun') return field;
- return 'npm';
-}
-
-const WRANGLER_CONFIGS = ['wrangler.jsonc', 'wrangler.json', 'wrangler.toml'];
-
-export function hasWranglerConfig(rootEntries: string[]): boolean {
- return WRANGLER_CONFIGS.some((name) => rootEntries.includes(name));
-}
-
-/** ` run
-
-
- Hosting · {data.projectId} · Cloudflarebase
-
-
-
-
-
-
-
-
Hosting
-
- Apps and functions on Workers for Platforms - one deploy, served at your subdomain.
-
-
-
-
- {#if data.demo}
-
-
-
-
- Hosting needs a real project
-
-
- Demo projects are throwaway and cannot deploy apps. Sign in and create a project - then
- cloudflarebase init connects a repository and every
- deploy ships to <app>.cfbase.dev.
-
-
-
- {:else if overview}
- {#if !overview.configured}
-
-
- Hosting is not configured
-
- This install has no Workers for Platforms dispatch namespace. Deploys are refused with a
- 503 until DISPATCH,
- DISPATCH_NAMESPACE,
- CF_ACCOUNT_ID, and
- CF_HOSTING_API_TOKEN are set on the hosting worker.
-
-
-
- {/if}
-
-
-
-
- Apps
-
- An app is one Worker in the dispatch namespace: static assets, server code, or both. The
- subdomain shown is what was actually claimed - taken names auto-number instead of failing.
-
-
-
- {#if apps.length === 0}
-
-
- No apps yet
-
-
- In your app's repository:
- npx @cloudflarebase/cli init
- then
- npx @cloudflarebase/cli deploy
-
-
- {:else}
-
- {#each apps as app (app.name)}
-
-
-
-
-
- {#if app.deployCount === 0}
-
Deploys on the next push
- {:else}
-
{app.deployCount} deploy{app.deployCount === 1 ? '' : 's'}
- {#if app.lastDeployAt}
-
last {timeAgo(app.lastDeployAt)}
- {/if}
- {/if}
-
-
-
- {/each}
-
- {/if}
-
-
-
-
-
-
- Deploys
-
-
-
-
-
-
- {#if data.isRoot}
-
-
-
-
- Deploy access
-
-
- How a deploy authenticates. Pick one - you do not need both.
-
-
-
-
-
- GitHub
- Deploy token
-
-
-
-
- Optional. CI's durable credential, for pipelines GitHub does not run: valid only for
- deploys and branch creation on this project and its branches. Secrets are shown once
- and stored hashed - revoking deletes the hash.
-
- {#if data.tokens.length}
-
- {#each data.tokens as token (token.id)}
-
-
-
{token.name}
-
- minted {timeAgo(token.createdAt)}{token.lastUsedAt
- ? ` · last used ${timeAgo(token.lastUsedAt)}`
- : ' · never used'}
-
-
-
(revokeTarget = token)}
- data-testid={`revoke-token-${token.name}`}
- >
- Revoke
-
-
- {/each}
-
- {:else}
- No tokens minted.
- {/if}
- (mintOpen = true)}
- data-testid="mint-token"
- >
- Mint token
-
-
-
-
-
- Push-to-deploy without a build farm, and without a credential you have to store. The
- default branch ships production; every other branch gets its own isolated preview at <app>-<branch>.
-
- {#if data.github.configured}
- {#if data.github.connections.length}
-
-
-
- {#each data.github.connections as connection (connection.appName)}
-
-
-
-
- {connection.repoFullName}
-
-
-
- deploys {connection.appName}
-
- on push to
- {connection.productionBranch ?? connection.defaultBranch}
-
-
- {connection.mode === 'direct'
- ? 'No build step - published directly'
- : "Builds on GitHub's runners, authenticated by identity token"}{connection.lastEventAt
- ? ` · last push ${timeAgo(connection.lastEventAt)}`
- : ''}
-
-
-
(disconnectTarget = connection)}
- data-testid={`disconnect-github-${connection.appName}`}
- >
- Disconnect
-
-
- {/each}
-
-
(connectOpen = true)}
- data-testid="connect-github"
- >
- Connect another repository
-
-
- {:else}
-
-
- Connect a repository and every push deploys itself. A site with no build step
- needs no workflow file at all; one that builds gets a workflow that
- authenticates with GitHub's identity token instead of a stored secret.
-
-
(connectOpen = true)}
- data-testid="connect-github"
- >
- Connect repository
-
-
- {/if}
- {:else if !githubToken}
-
- {#if githubError}
- {githubError}
- {/if}
- {:else}
-
-
-
- 1. Add the deploy token as a repository secret named
- {DEPLOY_TOKEN_SECRET_NAME}
-
-
- {githubToken}
- copy('token', githubToken!)}
- >
- {#if copied === 'token'} {:else} {/if} Copy
-
-
- Open repo secrets
-
-
-
- Shown once - it is stored hashed. Revoke it any time above.
-
-
-
-
- 2. Commit the workflow as
- {WORKFLOW_FILENAME}
-
-
-
- Create the file on GitHub (pre-filled)
-
- copy('yaml', deployWorkflowYaml())}
- >
- {#if copied === 'yaml'} {:else} {/if} Copy YAML
-
-
- {deployWorkflowYaml()}
-
-
- 3. Push
-
- Every commit deploys automatically from then on - Workers-Builds-style, with
- GitHub running the build. Make sure the repo carries the
- cloudflarebase.json written by
- cloudflarebase init.
-
-
-
- {/if}
-
-
-
-
- {/if}
- {/if}
-
-
- claim.appName)}
- {preferInstallation}
-/>
-
-
- {
- if (!open) disconnectTarget = null;
- }}
->
-
-
- Disconnect {disconnectTarget?.repoFullName}?
-
- Pushes stop deploying immediately.
- {#if disconnectTarget?.mode === 'build'}
- The workflow file this connection added is removed from the repository too.
- {/if}
- Everything already deployed keeps serving.
-
-
-
- Cancel
-
- {disconnectBusy ? 'Disconnecting…' : 'Disconnect'}
-
-
-
-
-
-
-
-
-
- Mint a deploy token
-
- Valid for deploys and branch creation on this project family. Stored hashed - the secret
- below is shown exactly once.
-
-
- {#if minted}
-
-
- Token {minted.name} :
-
-
- {minted.token}
- copy('minted', minted!.token)}>
- {#if copied === 'minted'} {:else} {/if}
-
-
-
{
- minted = null;
- mintOpen = false;
- }}
- >
- Done
-
-
- {:else}
-
- {/if}
-
-
-
-
- !open && (revokeTarget = null)}
->
-
-
- Revoke {revokeTarget?.name}?
-
- Deploys using this token stop working immediately. This cannot be undone - mint a new token
- to reconnect CI.
-
-
-
- Cancel
-
- {revokeBusy ? 'Revoking…' : 'Revoke'}
-
-
-
-
diff --git a/src/routes/(app)/dashboard/[projectId]/hosting/apps/[appName]/[[tab=hostingapp]]/+page.server.ts b/src/routes/(app)/dashboard/[projectId]/hosting/apps/[appName]/[[tab=hostingapp]]/+page.server.ts
deleted file mode 100644
index 0c49f726..00000000
--- a/src/routes/(app)/dashboard/[projectId]/hosting/apps/[appName]/[[tab=hostingapp]]/+page.server.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import {
- hostingBuildEnvSchema,
- hostingOverviewSchema,
- hostingSecretListSchema,
- hostingVarListSchema
-} from '$lib/agents';
-import { isDemoProjectId } from '$lib/console';
-import {
- agentSegment,
- agentUrl,
- assertProjectId,
- requireAgent,
- serverError
-} from '$lib/server/agents';
-import { getConnection } from '$lib/server/github-connect';
-import { appNameSchema, listAppClaims } from '$lib/server/hosting';
-import { getBranchContext } from '$lib/server/registry';
-import { error, redirect } from '@sveltejs/kit';
-import type { PageServerLoad } from './$types';
-
-/**
- * The per-app page (the CF Workers dashboard shape): Overview, Deployments,
- * Analytics, Settings as URL tabs. One load serves them all - the pieces are
- * small, and tab switches are client-side navigations to sibling routes.
- * Analytics data is client-fetched (range changes should not re-run this).
- */
-export const load: PageServerLoad = async ({ params, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- if (isDemoProjectId(projectId)) {
- // No demo hosting - the hub shows the upsell.
- redirect(303, `/dashboard/${projectId}/hosting`);
- }
- if (!appNameSchema.safeParse(params.appName).success) {
- error(404, 'no such app');
- }
- const appName = params.appName;
-
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const fetchAgent = async (subPath: string) => {
- const response = await agent.fetch(agentUrl(url.origin, entry, projectId, subPath));
- if (!response.ok) serverError(502, 'the hosting agent is unavailable');
- return response.json();
- };
-
- const overview = hostingOverviewSchema.safeParse(await fetchAgent('/overview'));
- if (!overview.success) serverError(502, 'the hosting agent is unavailable');
-
- // The agent only learns an app at first deploy; a claim-only app (a
- // connected repository that has not pushed yet) still gets a page.
- const deployed = overview.data.apps.find((app) => app.name === appName) ?? null;
- const claim = deployed
- ? null
- : ((await listAppClaims(platform, projectId)).find((row) => row.appName === appName) ?? null);
- if (!deployed && !claim) error(404, 'no such app');
- const app = deployed ?? {
- name: appName,
- subdomain: claim!.subdomain,
- url: null,
- deployCount: 0,
- lastDeployAt: null,
- createdAt: claim!.createdAt
- };
-
- const segment = agentSegment(appName);
- const vars = hostingVarListSchema.safeParse(await fetchAgent(`/apps/${segment}/vars`));
- const secrets = hostingSecretListSchema.safeParse(await fetchAgent(`/apps/${segment}/secrets`));
-
- // Connections live on ROOT projects only; a branch page simply has no
- // build section (its build env is the root connection's).
- const context = await getBranchContext(platform, projectId);
- const isRoot = !context || context.current === null;
- const connection = isRoot ? await getConnection(platform, projectId, appName) : null;
- const buildEnv = connection
- ? hostingBuildEnvSchema.safeParse(await fetchAgent(`/apps/${segment}/build-env`))
- : null;
-
- return {
- projectId,
- appName,
- tab: params.tab ?? 'overview',
- app,
- configured: overview.data.configured,
- stub: overview.data.stub,
- isRoot,
- connection,
- vars: vars.success ? vars.data.vars : [],
- secrets: secrets.success ? secrets.data.secrets : [],
- buildEnv: buildEnv?.success ? buildEnv.data : null
- };
-};
diff --git a/src/routes/(app)/dashboard/[projectId]/hosting/apps/[appName]/[[tab=hostingapp]]/+page.svelte b/src/routes/(app)/dashboard/[projectId]/hosting/apps/[appName]/[[tab=hostingapp]]/+page.svelte
deleted file mode 100644
index 9fc2fde7..00000000
--- a/src/routes/(app)/dashboard/[projectId]/hosting/apps/[appName]/[[tab=hostingapp]]/+page.svelte
+++ /dev/null
@@ -1,706 +0,0 @@
-
-
-
- {data.appName} · Hosting · {data.projectId} · Cloudflarebase
-
-
-
-
-
-
- All apps
-
-
-
-
- {#if data.app.deployCount === 0}
-
Awaiting first deploy
- {/if}
-
-
-
-
- {#if data.tab === 'overview'}
-
-
-
- Deploys
- {data.app.deployCount}
-
-
-
-
- Last deploy
-
- {data.app.lastDeployAt ? timeAgo(data.app.lastDeployAt) : '—'}
-
-
-
-
-
- Source
-
- {#if data.connection}
-
-
- {data.connection.repoFullName}
-
- {:else}
- CLI deploys
- {/if}
-
-
-
-
-
- {#if data.connection}
-
-
- Push to deploy
-
- Pushes to
- {data.connection.productionBranch ?? data.connection.defaultBranch}
- deploy production; every other branch gets an isolated preview at
- <app>-<branch>.
- {#if data.connection.ignoredBranches.length}
- Ignored: {data.connection.ignoredBranches.join(', ')}.
- {/if}
-
-
-
- {/if}
-
-
-
- Latest deploys
-
-
-
-
-
- {:else if data.tab === 'deployments'}
-
-
- Deployments
- Every deploy of this app, newest first.
-
-
-
-
-
- {:else if data.tab === 'analytics'}
-
-
-
- Requests
-
- Requests and 5xx errors served at
- {data.app.subdomain}.
-
-
-
-
-
- {analyticsRangeOptions.find((option) => option.value === analyticsRange)?.label}
-
-
- {#each analyticsRangeOptions as option (option.value)}
-
- {/each}
-
-
-
-
-
- {#if !analytics}
-
- {:else if ['connected', 'local'].includes(analytics.engine.status)}
-
-
-
Requests
-
{analytics.totals.requests.toLocaleString()}
-
-
-
Errors (5xx)
-
{analytics.totals.errors.toLocaleString()}
-
-
-
Avg duration
-
{Math.round(analytics.totals.avgDurationMs)} ms
-
-
-
- point.date) : 6,
- format: (value: Date) =>
- value.toLocaleDateString(
- undefined,
- analyticsRange === '7'
- ? { weekday: 'short' }
- : { month: 'short', day: 'numeric' }
- )
- }
- }}
- >
- {#snippet marks()}
-
-
-
- {/snippet}
- {#snippet tooltip()}
-
- value instanceof Date
- ? value.toLocaleDateString(undefined, {
- weekday: 'long',
- month: 'long',
- day: 'numeric'
- })
- : String(value)}
- />
- {/snippet}
-
-
- {:else}
-
- {analytics.engine.status === 'write-only'
- ? 'Requests are being recorded. Add Analytics Engine read credentials (CF_ACCOUNT_ID + CF_ANALYTICS_API_TOKEN) to visualize them.'
- : 'Analytics Engine reads are temporarily unavailable.'}
-
- {/if}
-
-
- {:else if data.tab === 'settings'}
-
-
- Variables and secrets
-
- The app's environment, at runtime and at build time. Text values upload as plain-text
- bindings on every deploy (they win over CLI-declared vars of the same name) and apply to
- the live script when saved; secrets are written through to Cloudflare and survive
- redeploys - their values are never shown again. GitHub builds export this whole set before
- the build step, so framework-inlined values are present too.
-
-
-
- 0}
- secretsDisabledReason="Secrets attach to the deployed script - deploy once first."
- save={saveRuntimeEnv}
- />
-
-
-
- {#if data.connection}
-
-
- Build environment
-
- Build-only overrides. Variables and secrets above are already exported into the GitHub
- Actions build; anything set here exists only at build time and wins over a matching
- name. Secrets are encrypted at rest and fetched by the workflow with its OIDC identity;
- branch builds share this set.
-
-
-
-
-
-
-
-
-
- Build settings
-
- {#if data.connection.mode === 'build'}
- Saving updates the workflow file in
- {data.connection.repoFullName} - the committed workflow
- is where these settings live.
- {:else}
- This connection deploys the pushed tree directly - no build step. Reconnect the
- repository to change the mode.
- {/if}
-
-
-
-
- {#if data.connection.mode === 'build'}
-
- Build command
-
-
-
- Root directory
-
-
- {/if}
-
-
- {data.connection.mode === 'build' ? 'Output directory' : 'Publish directory'}
-
-
-
-
-
Production branch
-
-
- Deploys the root project; every other branch deploys its own preview.
-
-
-
-
Ignored branches
-
-
- Comma-separated names or * globs; pushes to these never
- deploy.
-
-
-
-
-
- {settingsBusy ? 'Saving…' : 'Save build settings'}
-
- {#if settingsError}
- {settingsError}
- {/if}
- {#if settingsNotice}
- {settingsNotice}
- {/if}
-
-
-
- {/if}
-
-
-
- Danger zone
-
- Deleting stops {data.app.subdomain} serving, erases its
- deploy history and environment, and releases the subdomain for anyone to claim.
-
-
-
- {
- deleteOpen = true;
- deleteConfirm = '';
- deleteError = null;
- }}
- data-testid={`delete-app-${data.appName}`}
- >
- Delete app
-
-
-
- {/if}
-
-
-
-
-
-
- Delete {data.appName}?
-
- The deployed site at {data.app.subdomain} stops serving,
- its deploy history is erased, and the subdomain is released for anyone to claim. A connected repository
- is disconnected too. This cannot be undone.
-
-
-
-
- Type {data.appName} to confirm
-
-
- {#if deleteError}
-
{deleteError}
- {/if}
-
-
- (deleteOpen = false)}>Cancel
-
- {deleteBusy ? 'Deleting…' : 'Delete app'}
-
-
-
-
diff --git a/src/routes/(app)/dashboard/[projectId]/hosting/connect-github-dialog.svelte b/src/routes/(app)/dashboard/[projectId]/hosting/connect-github-dialog.svelte
deleted file mode 100644
index af997ceb..00000000
--- a/src/routes/(app)/dashboard/[projectId]/hosting/connect-github-dialog.svelte
+++ /dev/null
@@ -1,497 +0,0 @@
-
-
-
-
-
-
- Connect a repository
-
-
- Push to deploy. The default branch ships production; every other branch gets its own
- isolated preview.
-
-
-
- {#if !installations.length}
-
-
- Install the Cloudflarebase app on your GitHub account and pick the repositories it may
- reach. You can change that selection on GitHub at any time.
-
-
- Install on GitHub
-
-
- {:else}
-
-
- Account
- (installationId = value ? Number(value) : null)}
- >
-
- {accountLabel}
-
-
- {#each installations as installation (installation.id)}
-
- {installation.accountLogin}
-
- {/each}
-
-
-
-
-
-
-
-
-
-
- {#if reposLoading}
-
Loading repositories…
- {:else if !filtered.length}
-
- {repos.length ? 'No repository matches that search.' : 'No repositories available.'}
-
- {:else}
- {#each filtered as repo (repo.id)}
-
select(repo)}
- >
- {repo.fullName}
- {#if repo.private}
-
- {/if}
- {#if selected?.id === repo.id}
-
- {/if}
-
- {/each}
- {/if}
-
-
- Repository missing? Adjust the app's access on GitHub
-
-
-
- {#if selected}
-
- {#if inspecting}
-
- Inspecting {selected.fullName}…
-
- {:else}
-
-
Deploy as
-
- {#if takenApps.includes(appName.trim())}
-
- Reuses the existing app {appName.trim()}.
-
- {/if}
-
-
- {#if framework}
-
- Detected {framework.label}
- {#if rootDir}in {rootDir}{/if}
-
- {/if}
- {#if mode === 'build'}
-
-
Root directory
-
selected && inspect(selected, rootDir.trim())}
- />
- {#if rootDirMissing}
-
- {rootDir.trim()} does not exist on
- {selected.defaultBranch}.
-
- {/if}
-
-
- {/if}
- {#if mode === 'build' && writesWrangler}
-
- A wrangler.jsonc pointing at the adapter's output is
- committed with the workflow - there is nothing to write by hand.
-
- {/if}
- {#if framework?.note}
-
- {framework.note}
-
- {/if}
-
-
{summary}
-
- {#if !showSettings}
-
(showSettings = true)}
- >
- Change how it deploys
-
- {:else}
-
-
-
-
- Build on GitHub Actions
-
- Adds a workflow to your repository. Needed when the app has a build step.
-
-
-
-
-
-
- Deploy the files as they are
-
- No workflow, no runner, no Actions minutes. For prebuilt or static sites.
-
-
-
-
- {#if mode === 'direct'}
-
- Directory to publish
-
-
- {/if}
- {/if}
- {/if}
-
- {/if}
-
- {#if error}
-
{error}
- {/if}
-
-
-
- (open = false)}>Cancel
-
- {busy ? 'Connecting…' : 'Connect'}
-
-
- {/if}
-
-
diff --git a/src/routes/(app)/dashboard/[projectId]/hosting/deploys-table.svelte b/src/routes/(app)/dashboard/[projectId]/hosting/deploys-table.svelte
deleted file mode 100644
index 8786abc6..00000000
--- a/src/routes/(app)/dashboard/[projectId]/hosting/deploys-table.svelte
+++ /dev/null
@@ -1,125 +0,0 @@
-
-
-{#if deploys.length === 0}
- No deploys yet.
-{:else}
-
- {#each deploys as deploy (deploy.id)}
-
-
- {deploy.status}
-
-
-
- {showApp ? `${deploy.appName} · ` : ''}{deploy.subdomain}
-
-
- {deploy.hasWorker ? 'worker + ' : ''}{deploy.assetCount} asset{deploy.assetCount === 1
- ? ''
- : 's'} · {kb(deploy.assetBytes + deploy.moduleBytes)}
-
-
-
{timeAgo(deploy.createdAt)}
-
- {/each}
-
-
-
- {pageStart + 1}–{pageStart + deploys.length} of {total}
-
-
-
- Prev
-
- Next
-
-
-{/if}
diff --git a/src/routes/(app)/dashboard/[projectId]/hosting/vars-editor.svelte b/src/routes/(app)/dashboard/[projectId]/hosting/vars-editor.svelte
deleted file mode 100644
index 7c0fd821..00000000
--- a/src/routes/(app)/dashboard/[projectId]/hosting/vars-editor.svelte
+++ /dev/null
@@ -1,256 +0,0 @@
-
-
-
-
-
- {#if rows.length === 0}
-
- No variables yet. Text values upload as plain-text bindings; secrets are encrypted and never
- shown again.
-
- {:else}
-
- {/if}
-
- {#if !secretsEnabled && secretsDisabledReason}
-
{secretsDisabledReason}
- {/if}
-
-
-
- Add variable
-
-
- {busy ? 'Saving…' : 'Save'}
-
- {#if invalid && dirty}
-
{invalid}
- {/if}
- {#if error}
-
{error}
- {/if}
- {#if notice}
-
{notice}
- {/if}
-
-
diff --git a/src/routes/(app)/dashboard/[projectId]/settings/+page.svelte b/src/routes/(app)/dashboard/[projectId]/settings/+page.svelte
index 3bd3ae7d..400d91d9 100644
--- a/src/routes/(app)/dashboard/[projectId]/settings/+page.svelte
+++ b/src/routes/(app)/dashboard/[projectId]/settings/+page.svelte
@@ -361,10 +361,10 @@
? 'a branch'
: 'a project'}.
{:else if isBranch}
- Deleting this branch erases its users, data, and deploys. The root project is
+ Deleting this branch erases its users, data, and files. The root project is
untouched.
{:else}
- Deleting this project erases its users, data, and deploys in every agent - branches
+ Deleting this project erases its users, data, and files in every agent - branches
included.
{/if}
diff --git a/src/routes/(marketing)/+page.svelte b/src/routes/(marketing)/+page.svelte
index 13302680..f08e46e5 100644
--- a/src/routes/(marketing)/+page.svelte
+++ b/src/routes/(marketing)/+page.svelte
@@ -27,7 +27,6 @@
Radio,
Star,
Boxes,
- Zap,
HardDrive,
Minus
} from '@lucide/svelte';
@@ -145,14 +144,12 @@
}
];
- // Every primitive here has shipped: Functions is hosting on Workers for
- // Platforms, Realtime is the db gateway. Nothing on this page is a promise -
+ // Every primitive here has shipped: Realtime is the db gateway. Nothing on this page is a promise -
// an unbuilt card is a liability the moment someone signs up for it.
const roadmap = [
{ icon: KeyRound, name: 'Auth', live: true },
{ icon: Database, name: 'Database', live: true },
{ icon: HardDrive, name: 'Storage', live: true },
- { icon: Zap, name: 'Functions', live: true },
{ icon: Radio, name: 'Realtime', live: true }
];
@@ -167,7 +164,7 @@
},
{
q: 'Can I run it on my own Cloudflare account?',
- a: 'Yes - it is open source under Apache-2.0 at github.com/cloudflarebase/cloudflarebase. It is three Workers deployed in order with one command (npm run deploy:all), and the README walks through it for your own account. No secrets are required: each project generates its own signing key.'
+ a: 'Yes - it is open source under Apache-2.0 at github.com/cloudflarebase/cloudflarebase. It is four Workers deployed in order with one command (npm run deploy:all), and the README walks through it for your own account. No secrets are required: each project generates its own signing key.'
},
{
q: 'Is this production-ready?',
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index dfa191c8..ef4798b0 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -99,11 +99,11 @@
{/if}
diff --git a/src/routes/api/github/callback/+server.ts b/src/routes/api/github/callback/+server.ts
deleted file mode 100644
index 5971d9c4..00000000
--- a/src/routes/api/github/callback/+server.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import {
- appJwt,
- githubAppConfig,
- githubFetch,
- INSTALL_STATE_COOKIE,
- verifyInstallState
-} from '$lib/server/github';
-import { getConsoleSession } from '$lib/server/console';
-import { recordInstallation } from '$lib/server/github-connect';
-import { getProjectOwnership } from '$lib/server/registry';
-import { redirect } from '@sveltejs/kit';
-import type { RequestHandler } from './$types';
-
-/**
- * Where GitHub returns after an App install (* Phase B).
- *
- * This is the ONE moment an `installation_id` is trustworthy, and recording
- * the binding here is what every later connect checks instead of trusting the
- * id again. What makes it trustworthy is the SIGNED STATE, not a session:
- * this arrives as a cross-site top-level navigation from github.com, and a
- * session cookie is not reliably present on one - requiring it stranded
- * operators mid-install with a bare 401. The state is minted only for a
- * signed-in operator on a specific project, expires in minutes, and cannot
- * be forged without the webhook secret.
- *
- * The residual risk is narrow and deliberate: someone holding a live state
- * string could bind THEIR installation to that operator's project. They gain
- * nothing by it - `installationCoversProject` still gates every use on org
- * membership, so the installation becomes usable by the victim, not the
- * attacker - and the window is minutes. The session is still cross-checked
- * whenever the browser sends one.
- *
- * It always lands the operator somewhere useful; a failure is a message on
- * the Hosting page, never a bare error.
- */
-export const GET: RequestHandler = async ({ url, platform, request, cookies }) => {
- const config = githubAppConfig(platform);
- if (!config) redirect(303, '/dashboard');
-
- // Query first, cookie second. GitHub does echo `state` on the install
- // redirect, but the cookie keeps the return leg working if that ever
- // changes or the App is configured differently.
- const state = await verifyInstallState(
- config,
- url.searchParams.get('state') ?? cookies.get(INSTALL_STATE_COOKIE) ?? null
- );
- cookies.delete(INSTALL_STATE_COOKIE, { path: '/' });
- const installationId = Number(url.searchParams.get('installation_id'));
- if (!state || !Number.isSafeInteger(installationId) || installationId <= 0) {
- redirect(303, '/dashboard?github=invalid');
- }
-
- // This is an open route, so the guard never resolved a session (the /login
- // precedent). Resolve it ourselves: absent is expected and fine, but a
- // session belonging to someone ELSE means the state was replayed in a
- // different browser and must not be honoured.
- const operator = await getConsoleSession(
- platform,
- url.origin,
- request.headers.get('cookie'),
- null,
- request.headers.get('cf-connecting-ip')
- );
- if (operator && operator.id !== state.userId) {
- redirect(303, '/dashboard?github=invalid');
- }
-
- const hosting = `/dashboard/${state.projectId}/hosting`;
-
- // GitHub is the authority on which account was installed on - the operator
- // picks it inside GitHub's own UI and never tells us.
- const response = await githubFetch(
- await appJwt(config),
- 'GET',
- `/app/installations/${installationId}`
- );
- const account = (response.body as { account?: { login?: string } } | null)?.account?.login;
- if (!response.ok || !account) {
- redirect(303, `${hosting}?github=unreachable`);
- }
-
- const ownership = await getProjectOwnership(platform, state.projectId);
- await recordInstallation(platform, {
- installationId,
- orgId: ownership.orgId,
- accountLogin: account,
- userId: state.userId
- });
-
- redirect(303, `${hosting}?installation=${installationId}`);
-};
diff --git a/src/routes/api/github/webhook/+server.ts b/src/routes/api/github/webhook/+server.ts
deleted file mode 100644
index f255d17f..00000000
--- a/src/routes/api/github/webhook/+server.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import { githubAppConfig, verifyWebhookSignature } from '$lib/server/github';
-import { forgetInstallation } from '$lib/server/github-connect';
-import { handlePush, type PushPayload } from '$lib/server/github-deploy';
-import type { RequestHandler } from './$types';
-
-/**
- * GitHub's webhook endpoint (Phase B).
- *
- * PUBLIC BY EXCEPTION, and the only route under `/api` that is: GitHub has no
- * session and never will. The HMAC signature IS the authentication - it is
- * checked before the payload is parsed, and an unsigned or wrongly signed
- * request is a 401 that touches nothing.
- *
- * Answers 200 for anything it understands but does not act on, so GitHub's
- * delivery log shows green for tag pushes and build-mode repositories rather
- * than a wall of red the operator has to learn to ignore.
- */
-export const POST: RequestHandler = async ({ request, url, platform }) => {
- const config = githubAppConfig(platform);
- if (!config) {
- return Response.json({ error: 'no GitHub App is configured' }, { status: 404 });
- }
-
- // The RAW body, not re-serialized JSON: the digest is over exactly the
- // bytes GitHub sent, and JSON.stringify would not reproduce them.
- const raw = await request.text();
- const signed = await verifyWebhookSignature(
- config.webhookSecret,
- raw,
- request.headers.get('x-hub-signature-256')
- );
- if (!signed) {
- return Response.json({ error: 'invalid signature' }, { status: 401 });
- }
-
- const event = request.headers.get('x-github-event') ?? '';
- let payload: Record;
- try {
- payload = JSON.parse(raw) as Record;
- } catch {
- return Response.json({ error: 'invalid payload' }, { status: 400 });
- }
-
- if (event === 'ping') return Response.json({ ok: true });
-
- // An uninstalled app must stop deploying immediately - the connections are
- // dead the moment the installation is, and leaving them would strand rows
- // that can never mint a token again.
- if (event === 'installation' && payload.action === 'deleted') {
- const installationId = (payload.installation as { id?: number } | undefined)?.id;
- if (installationId) await forgetInstallation(platform, installationId);
- return Response.json({ ok: true });
- }
-
- if (event !== 'push') return Response.json({ ok: true, ignored: event });
-
- const outcomes = await handlePush(platform, payload as PushPayload, url.origin);
- return Response.json({ ok: true, outcomes });
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/+server.ts b/src/routes/api/projects/[projectId]/hosting/apps/[appName]/+server.ts
deleted file mode 100644
index 1c3f9efb..00000000
--- a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/+server.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import { isDemoProjectId } from '$lib/console';
-import { getDb } from '$lib/server/db';
-import { app } from '$lib/server/db/schema';
-import { assertProjectId, requireAgent } from '$lib/server/agents';
-import { disconnectRepository } from '$lib/server/github-connect';
-import { releaseAppClaim } from '$lib/server/hosting';
-import { and, eq } from 'drizzle-orm';
-import type { RequestHandler } from './$types';
-
-/**
- * Deletes an app: its deployed script and history in the agent, its GitHub
- * connection (so the next push cannot quietly resurrect it), and finally its
- * subdomain claim. Strictly in that order - the claim is released LAST,
- * because a freed name whose script still serves would hand the subdomain to
- * the next claimant while the old tenant's code answers on it.
- *
- * Operator sessions only (guard default; deploy tokens are deliberately not
- * admitted here - a CI credential that can deploy must not be able to erase).
- */
-export const DELETE: RequestHandler = async ({ params, platform }) => {
- const projectId = assertProjectId(params.projectId);
- if (isDemoProjectId(projectId)) {
- return Response.json({ error: 'demo projects cannot deploy apps' }, { status: 403 });
- }
- const appName = params.appName;
-
- // 404 before any side effect: deleting an app this project never claimed
- // must not touch connections or the agent.
- const db = await getDb(platform);
- const [claim] = await db
- .select()
- .from(app)
- .where(and(eq(app.projectId, projectId), eq(app.appName, appName)))
- .limit(1);
- if (!claim) {
- return Response.json({ error: 'no such app' }, { status: 404 });
- }
-
- // Drop the connection first: a surviving one would re-register the app on
- // the very next push. Best effort on the workflow file, like disconnect.
- const disconnected = await disconnectRepository(platform, projectId, appName);
-
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const erase = await agent.fetch(
- `https://${entry.manifest.worker}/internal/projects/${encodeURIComponent(projectId)}/apps/${encodeURIComponent(appName)}`,
- { method: 'DELETE' }
- );
- if (!erase.ok) {
- const body = (await erase.json().catch(() => null)) as { error?: string } | null;
- return Response.json(
- { error: body?.error ?? 'the hosting agent could not erase the app' },
- { status: 502 }
- );
- }
-
- await releaseAppClaim(platform, projectId, appName);
- return Response.json({
- deleted: true,
- subdomain: claim.subdomain,
- workflowRemoved: disconnected.workflowRemoved
- });
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/analytics/+server.ts b/src/routes/api/projects/[projectId]/hosting/apps/[appName]/analytics/+server.ts
deleted file mode 100644
index e9bf9781..00000000
--- a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/analytics/+server.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import {
- agentSegment,
- agentUrl,
- assertProjectId,
- requireAgent,
- toNativeResponse
-} from '$lib/server/agents';
-import type { RequestHandler } from './$types';
-
-/** Operator proxy: per-app request analytics (`?days=7|30|90`). */
-export const GET: RequestHandler = async ({ params, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const target = agentUrl(
- url.origin,
- entry,
- projectId,
- `/apps/${agentSegment(params.appName)}/analytics${url.search}`
- );
- const response = await agent.fetch(target);
- return toNativeResponse(response as unknown as Response);
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-env/+server.ts b/src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-env/+server.ts
deleted file mode 100644
index f57819ac..00000000
--- a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-env/+server.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import {
- agentSegment,
- agentUrl,
- assertProjectId,
- requireAgent,
- toNativeResponse
-} from '$lib/server/agents';
-import type { RequestHandler } from './$types';
-
-/**
- * The build-env read, two personas:
- *
- * - **GitHub Actions OIDC** (`locals.githubDeploy`): the runner fetching the
- * env it exports before the build step. Answers the DECRYPTED bundle over
- * the agent's service-binding-only /internal route. The grant names exactly
- * one connection, and that connection's app is the only build env the token
- * may read - an ordinary 404 otherwise, indistinguishable from absence.
- * Build env is connection-scoped: branch builds read the ROOT project's
- * store (`grant.projectId` IS the root).
- * - **Operator session** (the guard's default): the settings UI's read -
- * vars with values, secret NAMES only, never a decrypted value.
- */
-export const GET: RequestHandler = async ({ params, url, platform, locals }) => {
- const projectId = assertProjectId(params.projectId);
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
-
- const grant = locals.githubDeploy;
- if (grant) {
- if (grant.appName !== params.appName) {
- return Response.json({ error: 'not found' }, { status: 404 });
- }
- // Synthetic host: the internal route sits outside /agents/* on purpose,
- // reachable only over the service binding (the erase-route precedent).
- const response = await agent.fetch(
- `https://${entry.manifest.worker}/internal/projects/${encodeURIComponent(grant.projectId)}/apps/${encodeURIComponent(grant.appName)}/build-env`
- );
- return toNativeResponse(response as unknown as Response);
- }
-
- const target = agentUrl(
- url.origin,
- entry,
- projectId,
- `/apps/${agentSegment(params.appName)}/build-env`
- );
- const response = await agent.fetch(target);
- return toNativeResponse(response as unknown as Response);
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-secrets/[name]/+server.ts b/src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-secrets/[name]/+server.ts
deleted file mode 100644
index f6062a76..00000000
--- a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-secrets/[name]/+server.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import {
- agentSegment,
- agentUrl,
- assertProjectId,
- requireAgent,
- toNativeResponse
-} from '$lib/server/agents';
-import type { RequestHandler } from './$types';
-
-/**
- * Operator proxy: one build secret. PUT encrypts the value at rest in the
- * hosting agent (503 until the install sets HOSTING_MASTER_KEY); DELETE drops
- * it. Values are write-only on this surface - the console never reads one
- * back, and the runner's copy travels the OIDC build-env route instead.
- */
-
-const targetFor = (
- origin: string,
- projectId: string,
- appName: string,
- name: string
-): { entry: typeof AGENT_REGISTRY.hosting; target: string } => {
- const entry = AGENT_REGISTRY.hosting;
- return {
- entry,
- target: agentUrl(
- origin,
- entry,
- projectId,
- `/apps/${agentSegment(appName)}/build-secrets/${agentSegment(name)}`
- )
- };
-};
-
-export const PUT: RequestHandler = async ({ params, request, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const { entry, target } = targetFor(url.origin, projectId, params.appName, params.name);
- const agent = requireAgent(platform, entry);
- const response = await agent.fetch(target, {
- method: 'PUT',
- headers: { 'content-type': request.headers.get('content-type') ?? 'application/json' },
- body: await request.arrayBuffer()
- });
- return toNativeResponse(response as unknown as Response);
-};
-
-export const DELETE: RequestHandler = async ({ params, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const { entry, target } = targetFor(url.origin, projectId, params.appName, params.name);
- const agent = requireAgent(platform, entry);
- const response = await agent.fetch(target, { method: 'DELETE' });
- return toNativeResponse(response as unknown as Response);
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-vars/+server.ts b/src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-vars/+server.ts
deleted file mode 100644
index 3193a284..00000000
--- a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/build-vars/+server.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import {
- agentSegment,
- agentUrl,
- assertProjectId,
- requireAgent,
- toNativeResponse
-} from '$lib/server/agents';
-import type { RequestHandler } from './$types';
-
-/** Operator proxy: replace an app's build-time vars (the whole set - absent
- * names are deletions). The workflow fetches them before its build step. */
-export const PUT: RequestHandler = async ({ params, request, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const target = agentUrl(
- url.origin,
- entry,
- projectId,
- `/apps/${agentSegment(params.appName)}/build-vars`
- );
- const response = await agent.fetch(target, {
- method: 'PUT',
- headers: { 'content-type': request.headers.get('content-type') ?? 'application/json' },
- body: await request.arrayBuffer()
- });
- return toNativeResponse(response as unknown as Response);
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/deploys/+server.ts b/src/routes/api/projects/[projectId]/hosting/apps/[appName]/deploys/+server.ts
deleted file mode 100644
index b888d12d..00000000
--- a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/deploys/+server.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import { isDemoProjectId } from '$lib/console';
-import { agentUrl, assertProjectId, requireAgent, toNativeResponse } from '$lib/server/agents';
-import { resolveAppClaim } from '$lib/server/hosting';
-import type { RequestHandler } from './$types';
-
-/**
- * The deploy endpoint (Phase B). The console
- * resolves the subdomain claim BEFORE proxying - claims are control-plane
- * state, the agent owns no global namespace - then pushes the result to the
- * agent's service-binding-only link route and forwards the multipart deploy.
- * The agent only ever deploys to subdomains recorded for this project, which
- * is what makes the operator surface safe.
- *
- * Reachable with an operator session (guard default) OR a deploy token (the
- * guard admits `cfbd_` bearers on exactly this POST for the token's family).
- */
-export const POST: RequestHandler = async ({ params, request, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- if (isDemoProjectId(projectId)) {
- // No demo hosting: anonymous code execution is an abuse machine. The
- // dashboard shows an upsell card instead.
- return Response.json({ error: 'demo projects cannot deploy apps' }, { status: 403 });
- }
-
- // Reuse the persisted claim or mint one under the auto-numbering rule.
- const claim = await resolveAppClaim(platform, projectId, params.appName);
- if (!claim.ok) {
- return Response.json({ error: claim.error }, { status: claim.status });
- }
-
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
-
- // Synthetic host: the link route sits outside /agents/* on purpose,
- // reachable only over the service binding (the erase-route precedent).
- const push = await agent.fetch(
- `https://${entry.manifest.worker}/internal/projects/${encodeURIComponent(projectId)}/apps/${encodeURIComponent(claim.appName)}`,
- {
- method: 'PUT',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ subdomain: claim.subdomain })
- }
- );
- if (!push.ok) {
- const body = (await push.json().catch(() => null)) as { error?: string } | null;
- return Response.json(
- { error: body?.error ?? 'the hosting agent refused the app' },
- { status: push.status }
- );
- }
-
- const target = agentUrl(
- url.origin,
- entry,
- projectId,
- `/apps/${encodeURIComponent(claim.appName)}/deploys`
- );
- // Pass url + init (not a Request object): in dev the service binding is a
- // miniflare proxy that can't consume Requests from the Node realm. Full
- // headers so the multipart boundary survives.
- const response = await agent.fetch(target, {
- method: 'POST',
- headers: [...request.headers],
- body: await request.arrayBuffer()
- });
- return toNativeResponse(response as unknown as Response);
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/secrets/+server.ts b/src/routes/api/projects/[projectId]/hosting/apps/[appName]/secrets/+server.ts
deleted file mode 100644
index fa835ebf..00000000
--- a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/secrets/+server.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import {
- agentSegment,
- agentUrl,
- assertProjectId,
- requireAgent,
- toNativeResponse
-} from '$lib/server/agents';
-import type { RequestHandler } from './$types';
-
-/** Operator proxy: list an app's secret NAMES - the values live at Cloudflare
- * and are unrecoverable by design. */
-export const GET: RequestHandler = async ({ params, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const target = agentUrl(
- url.origin,
- entry,
- projectId,
- `/apps/${agentSegment(params.appName)}/secrets`
- );
- const response = await agent.fetch(target);
- return toNativeResponse(response as unknown as Response);
-};
-
-/** Operator proxy: set one secret on a deployed app (`cloudflarebase secret put`). */
-export const POST: RequestHandler = async ({ params, request, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const target = agentUrl(
- url.origin,
- entry,
- projectId,
- `/apps/${agentSegment(params.appName)}/secrets`
- );
- const response = await agent.fetch(target, {
- method: 'POST',
- headers: { 'content-type': request.headers.get('content-type') ?? 'application/json' },
- body: await request.arrayBuffer()
- });
- return toNativeResponse(response as unknown as Response);
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/secrets/[name]/+server.ts b/src/routes/api/projects/[projectId]/hosting/apps/[appName]/secrets/[name]/+server.ts
deleted file mode 100644
index cc2ecebb..00000000
--- a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/secrets/[name]/+server.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import {
- agentSegment,
- agentUrl,
- assertProjectId,
- requireAgent,
- toNativeResponse
-} from '$lib/server/agents';
-import type { RequestHandler } from './$types';
-
-/** Operator proxy: delete one secret from a deployed app. Idempotent - the
- * agent removes the script binding (404-tolerant) and drops the name row. */
-export const DELETE: RequestHandler = async ({ params, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const target = agentUrl(
- url.origin,
- entry,
- projectId,
- `/apps/${agentSegment(params.appName)}/secrets/${agentSegment(params.name)}`
- );
- const response = await agent.fetch(target, { method: 'DELETE' });
- return toNativeResponse(response as unknown as Response);
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/vars/+server.ts b/src/routes/api/projects/[projectId]/hosting/apps/[appName]/vars/+server.ts
deleted file mode 100644
index 8b3eb3ee..00000000
--- a/src/routes/api/projects/[projectId]/hosting/apps/[appName]/vars/+server.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import {
- agentSegment,
- agentUrl,
- assertProjectId,
- requireAgent,
- toNativeResponse
-} from '$lib/server/agents';
-import type { RequestHandler } from './$types';
-
-/**
- * Operator proxy: an app's stored runtime vars. GET lists; PUT replaces the
- * whole set (the console form submits the full table, so absent names are
- * deletions) and best-effort patches the live script.
- */
-
-export const GET: RequestHandler = async ({ params, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const target = agentUrl(
- url.origin,
- entry,
- projectId,
- `/apps/${agentSegment(params.appName)}/vars`
- );
- const response = await agent.fetch(target);
- return toNativeResponse(response as unknown as Response);
-};
-
-export const PUT: RequestHandler = async ({ params, request, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const target = agentUrl(
- url.origin,
- entry,
- projectId,
- `/apps/${agentSegment(params.appName)}/vars`
- );
- const response = await agent.fetch(target, {
- method: 'PUT',
- headers: { 'content-type': request.headers.get('content-type') ?? 'application/json' },
- body: await request.arrayBuffer()
- });
- return toNativeResponse(response as unknown as Response);
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/claims/+server.ts b/src/routes/api/projects/[projectId]/hosting/claims/+server.ts
deleted file mode 100644
index f2cba3df..00000000
--- a/src/routes/api/projects/[projectId]/hosting/claims/+server.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import { assertProjectId, requireAgent } from '$lib/server/agents';
-import { listAppClaims, resolveAppClaim } from '$lib/server/hosting';
-import { z } from 'zod';
-import type { RequestHandler } from './$types';
-
-/**
- * Subdomain claims, console-plane (the control plane owns the global
- * namespace). GET lists this project's claims; POST resolves one - with
- * `dry: true` it only reports what WOULD be claimed, which is how
- * bare `cloudflarebase init` shows the auto-numbered suggestion before
- * claiming.
- */
-
-const claimBodySchema = z.object({
- app: z.unknown(),
- dry: z.boolean().optional()
-});
-
-export const GET: RequestHandler = async ({ params, platform }) => {
- const projectId = assertProjectId(params.projectId);
- return Response.json({ claims: await listAppClaims(platform, projectId) });
-};
-
-export const POST: RequestHandler = async ({ params, request, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const body = claimBodySchema.safeParse(await request.json().catch(() => null));
- if (!body.success) {
- return Response.json({ error: 'expected { app, dry? }' }, { status: 400 });
- }
-
- const claim = await resolveAppClaim(platform, projectId, body.data.app, {
- dry: body.data.dry
- });
- if (!claim.ok) {
- return Response.json({ error: claim.error }, { status: claim.status });
- }
-
- // A real claim is pushed to the agent right away so the app shows up on
- // the Hosting page before its first deploy. Best-effort: the deploy route
- // re-pushes, so an unreachable agent here costs nothing but the preview.
- if (!body.data.dry) {
- const entry = AGENT_REGISTRY.hosting;
- try {
- const agent = requireAgent(platform, entry);
- await agent.fetch(
- `https://${entry.manifest.worker}/internal/projects/${encodeURIComponent(projectId)}/apps/${encodeURIComponent(claim.appName)}`,
- {
- method: 'PUT',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ subdomain: claim.subdomain })
- }
- );
- } catch {
- // the claim row is persisted; the first deploy completes the link
- }
- }
-
- return Response.json(
- { subdomain: claim.subdomain, appName: claim.appName, created: claim.created },
- { status: claim.created ? 201 : 200 }
- );
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/deploys/+server.ts b/src/routes/api/projects/[projectId]/hosting/deploys/+server.ts
deleted file mode 100644
index 87f7dbf0..00000000
--- a/src/routes/api/projects/[projectId]/hosting/deploys/+server.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import { agentUrl, assertProjectId, requireAgent, toNativeResponse } from '$lib/server/agents';
-import type { RequestHandler } from './$types';
-
-/** Operator proxy: keyset-paged deploy history (?app=&cursor=&limit=). */
-export const GET: RequestHandler = async ({ params, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const response = await agent.fetch(
- agentUrl(url.origin, entry, projectId, `/deploys${url.search}`)
- );
- return toNativeResponse(response as unknown as Response);
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/github/+server.ts b/src/routes/api/projects/[projectId]/hosting/github/+server.ts
deleted file mode 100644
index 7c9fa0c5..00000000
--- a/src/routes/api/projects/[projectId]/hosting/github/+server.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { assertProjectId } from '$lib/server/agents';
-import { githubAppConfig } from '$lib/server/github';
-import { listConnections, listInstallationsForOrg } from '$lib/server/github-connect';
-import { getProjectOwnership } from '$lib/server/registry';
-import type { RequestHandler } from './$types';
-
-/**
- * GitHub push-to-deploy state for the Hosting page (* Phase B). `configured: false` is the self-hosted default - no App
- * credentials, so the page offers the manual deploy-token flow instead.
- */
-export const GET: RequestHandler = async ({ params, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const config = githubAppConfig(platform);
- if (!config) {
- return Response.json({ configured: false, installations: [], connections: [] });
- }
-
- // Installations are offered per owning org, so a teammate's install is
- // reusable and a stranger's is invisible.
- const ownership = await getProjectOwnership(platform, projectId);
- return Response.json({
- configured: true,
- installations: await listInstallationsForOrg(platform, ownership.orgId),
- connections: await listConnections(platform, projectId)
- });
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/github/connections/+server.ts b/src/routes/api/projects/[projectId]/hosting/github/connections/+server.ts
deleted file mode 100644
index f46bdd73..00000000
--- a/src/routes/api/projects/[projectId]/hosting/github/connections/+server.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { assertProjectId } from '$lib/server/agents';
-import { connectRepository, listConnections } from '$lib/server/github-connect';
-import type { RequestHandler } from './$types';
-
-/**
- * Repository connections, console-plane (* Phase B). Made on ROOT projects; a push to a git branch deploys
- * `--` through the same connection.
- */
-
-export const GET: RequestHandler = async ({ params, platform }) => {
- const projectId = assertProjectId(params.projectId);
- return Response.json({ connections: await listConnections(platform, projectId) });
-};
-
-export const POST: RequestHandler = async ({ params, request, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const result = await connectRepository(
- platform,
- projectId,
- await request.json().catch(() => null),
- // The origin is baked into the workflow AND is the OIDC audience, so it
- // has to be this deployment's own - never a value from the request body.
- url.origin
- );
- if (!result.ok) {
- return Response.json({ error: result.error }, { status: result.status });
- }
- return Response.json(
- {
- connection: result.connection,
- subdomain: result.subdomain,
- workflowWritten: result.workflowWritten
- },
- { status: 201 }
- );
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/github/connections/[appName]/+server.ts b/src/routes/api/projects/[projectId]/hosting/github/connections/[appName]/+server.ts
deleted file mode 100644
index 78a0ba4b..00000000
--- a/src/routes/api/projects/[projectId]/hosting/github/connections/[appName]/+server.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { assertProjectId } from '$lib/server/agents';
-import { disconnectRepository, updateConnectionSettings } from '$lib/server/github-connect';
-import type { RequestHandler } from './$types';
-
-/**
- * Edits build settings at any time. For a build-mode connection the workflow
- * file in the repository is rewritten FIRST (the settings live in it), then
- * the row - a failed commit changes nothing.
- */
-export const PATCH: RequestHandler = async ({ params, request, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const result = await updateConnectionSettings(
- platform,
- projectId,
- params.appName,
- await request.json().catch(() => null),
- url.origin
- );
- if (!result.ok) {
- return Response.json({ error: result.error }, { status: result.status });
- }
- return Response.json({
- connection: result.connection,
- workflowRewritten: result.workflowRewritten
- });
-};
-
-/**
- * Disconnecting IS deleting the row - a repository with no connection cannot
- * deploy, whatever it still holds in `.github/workflows`. Removing the
- * workflow file is attempted too, and reported, but never blocks.
- */
-export const DELETE: RequestHandler = async ({ params, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const result = await disconnectRepository(platform, projectId, params.appName);
- if (!result.ok) {
- return Response.json({ error: 'no such connection' }, { status: 404 });
- }
- return Response.json({ ok: true, workflowRemoved: result.workflowRemoved });
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/github/install/+server.ts b/src/routes/api/projects/[projectId]/hosting/github/install/+server.ts
deleted file mode 100644
index 2cf0f2a6..00000000
--- a/src/routes/api/projects/[projectId]/hosting/github/install/+server.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { assertProjectId } from '$lib/server/agents';
-import { isDemoProjectId } from '$lib/console';
-import {
- githubAppConfig,
- INSTALL_STATE_COOKIE,
- installUrl,
- signInstallState
-} from '$lib/server/github';
-import type { RequestHandler } from './$types';
-
-/**
- * Where the operator goes to install the App and pick repositories.
- *
- * The `state` is signed and carries the operator and the project, which is
- * what lets the callback trust the `installation_id` GitHub hands back on a
- * redirect anyone could otherwise craft.
- *
- * It rides BOTH the URL and a short-lived cookie. GitHub's install redirect
- * is a different flow from user authorization, and whether it echoes `state`
- * back depends on how the App is configured - so the cookie is what makes
- * the return leg work regardless, without weakening anything: it is the same
- * signed token, verified the same way.
- */
-export const GET: RequestHandler = async ({ params, platform, locals, cookies, url }) => {
- const projectId = assertProjectId(params.projectId);
- if (isDemoProjectId(projectId)) {
- return Response.json({ error: 'demo projects cannot deploy apps' }, { status: 403 });
- }
- const config = githubAppConfig(platform);
- if (!config) {
- return Response.json({ error: 'no GitHub App is configured on this console' }, { status: 503 });
- }
- if (!locals.consoleUser) {
- // The guard already required a session; this keeps the type honest.
- return Response.json({ error: 'sign in first' }, { status: 401 });
- }
-
- const state = await signInstallState(config, { projectId, userId: locals.consoleUser.id });
- cookies.set(INSTALL_STATE_COOKIE, state, {
- path: '/',
- httpOnly: true,
- // The return leg is a top-level navigation from github.com, so Lax is
- // what lets the cookie ride along; Strict would drop it.
- sameSite: 'lax',
- // http in local dev would otherwise refuse to store it.
- secure: url.protocol === 'https:',
- maxAge: 15 * 60
- });
- return Response.json({ url: installUrl(config, state) });
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/github/repos/+server.ts b/src/routes/api/projects/[projectId]/hosting/github/repos/+server.ts
deleted file mode 100644
index f16e4672..00000000
--- a/src/routes/api/projects/[projectId]/hosting/github/repos/+server.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { assertProjectId } from '$lib/server/agents';
-import { githubAppConfig } from '$lib/server/github';
-import { installationCoversProject, reportInstallationFailure } from '$lib/server/github-connect';
-import { inspectRepo, listInstallationRepos } from '$lib/server/github-repo';
-import type { RequestHandler } from './$types';
-
-/**
- * The repository picker's data. Scoped twice: the installation must belong to
- * the org that owns this project, and the installation token itself only
- * reaches repositories the operator selected on GitHub.
- *
- * With `?repo=/` it also inspects that repository and reports
- * whether it needs a runner - which is what preselects build or direct mode.
- */
-export const GET: RequestHandler = async ({ params, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const config = githubAppConfig(platform);
- if (!config) {
- return Response.json({ error: 'no GitHub App is configured on this console' }, { status: 503 });
- }
-
- const installationId = Number(url.searchParams.get('installation'));
- if (!Number.isSafeInteger(installationId) || installationId <= 0) {
- return Response.json({ error: 'installation is required' }, { status: 400 });
- }
- const covers = await installationCoversProject(platform, installationId, projectId);
- if (!covers.ok) {
- return Response.json({ error: covers.error }, { status: 403 });
- }
-
- const listed = await listInstallationRepos(config, installationId);
- if (listed.repos === null) {
- const answer = await reportInstallationFailure(platform, installationId, listed);
- return Response.json(answer.body, { status: answer.status });
- }
- const repos = listed.repos;
-
- const wanted = url.searchParams.get('repo');
- if (!wanted) return Response.json({ repos });
-
- const repo = repos.find((candidate) => candidate.fullName === wanted);
- if (!repo) {
- return Response.json({ error: 'that repository is not in this installation' }, { status: 404 });
- }
- // Optional monorepo root: inspection reads THAT directory's package.json
- // and listing, so the presets describe the app, not the workspace shell.
- const dir = url.searchParams.get('dir')?.trim() ?? '';
- if (dir && !/^[A-Za-z0-9._/-]{1,200}$/.test(dir)) {
- return Response.json({ error: 'invalid root directory' }, { status: 400 });
- }
- if (dir.startsWith('/') || dir.split('/').includes('..')) {
- return Response.json({ error: 'invalid root directory' }, { status: 400 });
- }
- return Response.json({
- repos,
- inspection: await inspectRepo(
- config,
- installationId,
- repo.fullName,
- repo.defaultBranch,
- dir || null
- )
- });
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/overview/+server.ts b/src/routes/api/projects/[projectId]/hosting/overview/+server.ts
deleted file mode 100644
index 66315bd4..00000000
--- a/src/routes/api/projects/[projectId]/hosting/overview/+server.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { AGENT_REGISTRY } from '$lib/agent-registry';
-import { agentUrl, assertProjectId, requireAgent, toNativeResponse } from '$lib/server/agents';
-import type { RequestHandler } from './$types';
-
-/** Operator proxy: the project's hosting overview (apps, recent deploys). */
-export const GET: RequestHandler = async ({ params, url, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const entry = AGENT_REGISTRY.hosting;
- const agent = requireAgent(platform, entry);
- const response = await agent.fetch(agentUrl(url.origin, entry, projectId, '/overview'));
- return toNativeResponse(response as unknown as Response);
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/tokens/+server.ts b/src/routes/api/projects/[projectId]/hosting/tokens/+server.ts
deleted file mode 100644
index 9a6f95ca..00000000
--- a/src/routes/api/projects/[projectId]/hosting/tokens/+server.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { assertProjectId } from '$lib/server/agents';
-import { listDeployTokens, mintDeployToken } from '$lib/server/hosting';
-import type { RequestHandler } from './$types';
-
-/**
- * Deploy tokens, console-plane (Phase B).
- * Minted on ROOT projects from the Hosting page; the secret appears exactly
- * once in the mint response and only its SHA-256 digest is stored.
- */
-
-export const GET: RequestHandler = async ({ params, platform }) => {
- const projectId = assertProjectId(params.projectId);
- return Response.json({ tokens: await listDeployTokens(platform, projectId) });
-};
-
-export const POST: RequestHandler = async ({ params, request, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const result = await mintDeployToken(platform, projectId, await request.json().catch(() => null));
- if (!result.ok) {
- return Response.json({ error: result.error }, { status: result.status });
- }
- return Response.json(
- { id: result.id, name: result.name, token: result.token, createdAt: result.createdAt },
- { status: 201 }
- );
-};
diff --git a/src/routes/api/projects/[projectId]/hosting/tokens/[tokenId]/+server.ts b/src/routes/api/projects/[projectId]/hosting/tokens/[tokenId]/+server.ts
deleted file mode 100644
index efa3dcbd..00000000
--- a/src/routes/api/projects/[projectId]/hosting/tokens/[tokenId]/+server.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { assertProjectId } from '$lib/server/agents';
-import { revokeDeployToken } from '$lib/server/hosting';
-import type { RequestHandler } from './$types';
-
-/** Revocation IS deletion: a token whose row is gone can never verify again. */
-export const DELETE: RequestHandler = async ({ params, platform }) => {
- const projectId = assertProjectId(params.projectId);
- const revoked = await revokeDeployToken(platform, projectId, params.tokenId);
- if (!revoked) {
- return Response.json({ error: 'no such token' }, { status: 404 });
- }
- return Response.json({ revoked: true });
-};
diff --git a/src/worker-configuration.d.ts b/src/worker-configuration.d.ts
index 932fcb34..8d336965 100644
--- a/src/worker-configuration.d.ts
+++ b/src/worker-configuration.d.ts
@@ -1,5 +1,5 @@
/* eslint-disable */
-// Generated by Wrangler by running `wrangler types ./src/worker-configuration.d.ts` (hash: 3bf5ab17fc624cda1dd2a097eeabc421)
+// Generated by Wrangler by running `wrangler types ./src/worker-configuration.d.ts` (hash: 1a1f3d01bddc3bfa72182474e1b76628)
// Runtime types generated with workerd@1.20260722.1 2026-07-10 nodejs_als,nodejs_compat
interface __BaseEnv_Env {
KV?: KVNamespace;
@@ -12,16 +12,12 @@ interface __BaseEnv_Env {
DEMO_MODE?: "true";
AUTH_AGENT: Fetcher /* auth-agent-local */ | Fetcher /* auth-agent-test */ | Fetcher /* auth-agent */ | Fetcher /* auth-agent-preview */;
DB_AGENT: Fetcher /* db-agent-local */ | Fetcher /* db-agent-test */ | Fetcher /* db-agent */ | Fetcher /* db-agent-preview */;
- HOSTING_AGENT: Fetcher /* hosting-agent-local */ | Fetcher /* hosting-agent-test */ | Fetcher /* hosting-agent */ | Fetcher /* hosting-agent-preview */;
STORAGE_AGENT: Fetcher /* storage-agent-local */ | Fetcher /* storage-agent-test */ | Fetcher /* storage-agent */ | Fetcher /* storage-agent-preview */;
CONSOLE_SETUP_TOKEN?: "e2e-console-setup-token-not-a-secret";
PUBLIC_SENTRY_DSN?: "https://50ed6e26b886124ac39e983efb72144d@o4510375271530496.ingest.de.sentry.io/4511764380778576" | "https://093f7505d223a871223f9a15130ef116@o4510375271530496.ingest.de.sentry.io/4511764377043024" | "";
PUBLIC_SENTRY_ENV?: "production" | "preview";
}
declare namespace Cloudflare {
- interface GlobalProps {
- mainModule: typeof import("../.svelte-kit/cloudflare/_worker");
- }
interface LocalEnv {
KV: KVNamespace;
DB: D1Database;
@@ -33,7 +29,6 @@ declare namespace Cloudflare {
DEMO_MODE: "true";
AUTH_AGENT: Fetcher /* auth-agent-local */;
DB_AGENT: Fetcher /* db-agent-local */;
- HOSTING_AGENT: Fetcher /* hosting-agent-local */;
STORAGE_AGENT: Fetcher /* storage-agent-local */;
}
interface TestEnv {
@@ -46,7 +41,6 @@ declare namespace Cloudflare {
DEMO_MODE: "true";
AUTH_AGENT: Fetcher /* auth-agent-test */;
DB_AGENT: Fetcher /* db-agent-test */;
- HOSTING_AGENT: Fetcher /* hosting-agent-test */;
STORAGE_AGENT: Fetcher /* storage-agent-test */;
}
interface ProductionEnv {
@@ -60,7 +54,6 @@ declare namespace Cloudflare {
PUBLIC_SENTRY_ENV: "production";
AUTH_AGENT: Fetcher /* auth-agent */;
DB_AGENT: Fetcher /* db-agent */;
- HOSTING_AGENT: Fetcher /* hosting-agent */;
STORAGE_AGENT: Fetcher /* storage-agent */;
}
interface PreviewEnv {
@@ -74,7 +67,6 @@ declare namespace Cloudflare {
PUBLIC_SENTRY_ENV: "preview";
AUTH_AGENT: Fetcher /* auth-agent-preview */;
DB_AGENT: Fetcher /* db-agent-preview */;
- HOSTING_AGENT: Fetcher /* hosting-agent-preview */;
STORAGE_AGENT: Fetcher /* storage-agent-preview */;
}
interface Env extends __BaseEnv_Env {}
diff --git a/wrangler.e2e.jsonc b/wrangler.e2e.jsonc
index c5689521..d4e4319c 100644
--- a/wrangler.e2e.jsonc
+++ b/wrangler.e2e.jsonc
@@ -25,7 +25,6 @@
"services": [
{ "binding": "AUTH_AGENT", "service": "auth-agent-test" },
{ "binding": "DB_AGENT", "service": "db-agent-test" },
- { "binding": "HOSTING_AGENT", "service": "hosting-agent-test" },
{ "binding": "STORAGE_AGENT", "service": "storage-agent-test" }
],
// DEMO_MODE mirrors env.test so dashboard specs can drive demo projects
diff --git a/wrangler.jsonc b/wrangler.jsonc
index ddd6a084..9eb247ba 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -79,7 +79,6 @@
"services": [
{ "binding": "AUTH_AGENT", "service": "auth-agent" },
{ "binding": "DB_AGENT", "service": "db-agent" },
- { "binding": "HOSTING_AGENT", "service": "hosting-agent" },
{ "binding": "STORAGE_AGENT", "service": "storage-agent" }
],
@@ -171,7 +170,6 @@
"services": [
{ "binding": "AUTH_AGENT", "service": "auth-agent-local" },
{ "binding": "DB_AGENT", "service": "db-agent-local" },
- { "binding": "HOSTING_AGENT", "service": "hosting-agent-local" },
{ "binding": "STORAGE_AGENT", "service": "storage-agent-local" }
],
"ratelimits": [
@@ -218,7 +216,6 @@
"services": [
{ "binding": "AUTH_AGENT", "service": "auth-agent-test" },
{ "binding": "DB_AGENT", "service": "db-agent-test" },
- { "binding": "HOSTING_AGENT", "service": "hosting-agent-test" },
{ "binding": "STORAGE_AGENT", "service": "storage-agent-test" }
],
"ratelimits": [
@@ -265,7 +262,6 @@
"services": [
{ "binding": "AUTH_AGENT", "service": "auth-agent" },
{ "binding": "DB_AGENT", "service": "db-agent" },
- { "binding": "HOSTING_AGENT", "service": "hosting-agent" },
{ "binding": "STORAGE_AGENT", "service": "storage-agent" }
],
"ratelimits": [
@@ -289,7 +285,7 @@
// are the point: AUTH_AGENT targets the preview agent and DB binds the
// preview control plane. A separate Worker is also a separate SECRET
// STORE - preview inherits nothing from production, so optional secrets
- // (the GitHub App four, CONSOLE_SETUP_TOKEN) exist here only if put
+ // (CONSOLE_SETUP_TOKEN among them) exist here only if put
// here.
"preview": {
"name": "cloudflarebase-com-preview",
@@ -326,7 +322,6 @@
"services": [
{ "binding": "AUTH_AGENT", "service": "auth-agent-preview" },
{ "binding": "DB_AGENT", "service": "db-agent-preview" },
- { "binding": "HOSTING_AGENT", "service": "hosting-agent-preview" },
{ "binding": "STORAGE_AGENT", "service": "storage-agent-preview" }
]
}
From 309a98ddb6667cae21f7aa3970c1f847f721c22f Mon Sep 17 00:00:00 2001
From: clibequilibrium <105135724+clibequilibrium@users.noreply.github.com>
Date: Wed, 7 Oct 2026 20:09:37 -0400
Subject: [PATCH 2/8] CLI drops managed hosting
With hosting gone, bare `init` only links the directory to a console
project (no subdomain claim), `deploy` is the wrangler path alone, and
`secret` - which set secrets on hosted apps - is removed. `key` keeps
working off the same cloudflarebase.json; older files that still carry
app/vars/assets are read, not refused.
---
cli/src/commands/deploy.ts | 240 +-----------------------------
cli/src/commands/init.ts | 8 +-
cli/src/commands/key.ts | 17 +--
cli/src/commands/managed-init.ts | 78 ++--------
cli/src/commands/secret.ts | 178 ----------------------
cli/src/index.ts | 11 +-
cli/src/lib/agents.ts | 4 -
cli/src/lib/assetsignore.ts | 65 --------
cli/src/lib/managed.ts | 245 ++-----------------------------
cli/src/lib/oidc.ts | 63 --------
10 files changed, 44 insertions(+), 865 deletions(-)
delete mode 100644 cli/src/commands/secret.ts
delete mode 100644 cli/src/lib/assetsignore.ts
delete mode 100644 cli/src/lib/oidc.ts
diff --git a/cli/src/commands/deploy.ts b/cli/src/commands/deploy.ts
index 0d4598fd..ada2d250 100644
--- a/cli/src/commands/deploy.ts
+++ b/cli/src/commands/deploy.ts
@@ -1,242 +1,16 @@
-import { readdir, readFile } from 'node:fs/promises';
+import { readFile } from 'node:fs/promises';
import path from 'node:path';
-import { loadConfig } from '../lib/config.js';
-import { blank, bold, dim, info, step, success, UserError } from '../lib/log.js';
-import {
- collectAssets,
- findAssetsDirectory,
- hostingFetch,
- managedConfigFromEnv,
- readManagedConfig,
- readUserWranglerConfig,
- resolveGitBranch,
- sanitizeBranchName,
- targetProjectId,
- type ManagedConfig
-} from '../lib/managed.js';
-import { actionsIdToken, inGithubActions } from '../lib/oidc.js';
+import { blank, dim, info, step, success, UserError } from '../lib/log.js';
import { run, runOrFail } from '../lib/run.js';
import { readTrustedOrigins } from '../lib/wrangler-config.js';
/**
- * `cloudflarebase deploy` - deploy, branching on context:
- *
- * - `cloudflarebase.json` present (written by bare `cloudflarebase init`), OR
- * the CLOUDFLAREBASE_PROJECT/APP/URL trio in the environment (written into
- * the workflow by the console's GitHub App): MANAGED deploy against the
- * console's hosting API. The git branch decides the target: the default
- * branch deploys the root, anything else deploys `--`
- * (auto-created), so preview-per-git-branch falls out.
- * - otherwise: the self-hosted wrangler path, unchanged.
+ * `cloudflarebase deploy` - deploy this Worker with wrangler. There is nothing
+ * to configure before sign-in works: the agent trusts the deployment's own
+ * origin automatically, so a fresh deploy is usable the moment the URL exists.
*/
export async function deployCommand(projectDir: string, rest: string[] = []): Promise {
- // The file wins: a directory someone deliberately linked is never
- // redirected by an ambient variable.
- const managed = (await readManagedConfig(projectDir)) ?? managedConfigFromEnv();
- if (managed) {
- await managedDeploy(projectDir, managed, parseManagedFlags(rest));
- return;
- }
- if (rest.length) {
- throw new UserError(
- 'Deploy flags need an initialized project.',
- 'Run `cloudflarebase init` first - flags like --branch only apply to managed deploys.'
- );
- }
- await selfHostedDeploy(projectDir);
-}
-
-interface ManagedFlags {
- branch?: string;
- token?: string;
-}
-
-function parseManagedFlags(rest: string[]): ManagedFlags {
- const flags: ManagedFlags = {};
- for (let i = 0; i < rest.length; i += 1) {
- const arg = rest[i];
- if (arg === '--branch') flags.branch = rest[++i];
- else if (arg === '--token') flags.token = rest[++i];
- else throw new UserError(`Unknown flag "${arg}".`);
- }
- return flags;
-}
-
-/** The bundling output directory; keep it out of version control. */
-const BUNDLE_DIR = '.cloudflarebase/dist';
-
-async function managedDeploy(
- projectDir: string,
- managed: ManagedConfig,
- flags: ManagedFlags
-): Promise {
- // Three credentials, most explicit first: an operator-supplied deploy
- // token, GitHub's identity token (a connected repository stores nothing -
- // the console verifies the token against GitHub's public keys), then the
- // stored login for interactive runs.
- let token = flags.token ?? process.env.CLOUDFLAREBASE_DEPLOY_TOKEN;
- if (!token) {
- // The audience is the console origin; a token minted for anything else
- // is refused, which is what stops one being replayed here.
- token = (await actionsIdToken(managed.origin)) ?? undefined;
- if (token) step('Authenticating with the GitHub identity token');
- }
- if (!token) {
- const config = await loadConfig();
- if (config.origin !== managed.origin) {
- throw new UserError(
- `This directory is linked to ${managed.origin}, but you are signed in to ${config.origin}.`,
- `Run \`cloudflarebase login ${managed.origin}\` (or set CLOUDFLAREBASE_DEPLOY_TOKEN).`
- );
- }
- token = config.token;
- }
-
- // Git branch -> target project id. `--branch` overrides; `main` aliases
- // the root and never appears in a URL.
- const gitBranch = flags.branch ?? (await resolveGitBranch(projectDir));
- const branch = gitBranch ? sanitizeBranchName(gitBranch) : null;
- const target = targetProjectId(managed.project, branch);
-
- if (target !== managed.project) {
- // Ensure the branch row exists (409 = already does). This is one of the
- // two endpoints a deploy token is valid on, so CI's new git branches
- // mint their preview project on first deploy.
- const created = await hostingFetch(
- managed.origin,
- token,
- `/api/projects/${managed.project}/branches`,
- {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ branch })
- }
- );
- if (!created.ok && created.status !== 409) {
- throw new UserError(`Could not create branch "${branch}": ${await failureText(created)}`);
- }
- step(`Deploying branch ${bold(branch!)} (${target})`);
- } else {
- step(`Deploying ${bold(managed.project)}`);
- }
-
- // Bundle the Worker when the user's wrangler config declares one -
- // wrangler does the bundling (--dry-run --outdir), we upload the output.
- const wrangler = await readUserWranglerConfig(projectDir);
- const form = new FormData();
- let mainModule: string | undefined;
- let moduleCount = 0;
- if (wrangler?.main) {
- step('Bundling the Worker with wrangler');
- await runOrFail('npx', ['wrangler', 'deploy', '--dry-run', `--outdir=${BUNDLE_DIR}`], {
- cwd: projectDir,
- capture: true,
- failure: 'wrangler bundling failed.'
- });
- const outDir = path.join(projectDir, BUNDLE_DIR);
- const emitted = (await readdir(outDir)).filter((name) => /\.(?:js|mjs)$/.test(name));
- if (!emitted.length) {
- throw new UserError('wrangler emitted no JavaScript bundle.');
- }
- const entryGuess = `${path.basename(wrangler.main).replace(/\.[jt]sx?$/, '')}.js`;
- mainModule =
- emitted.length === 1
- ? emitted[0]
- : (emitted.find((name) => name === entryGuess) ?? 'index.js');
- if (!mainModule || !emitted.includes(mainModule)) {
- throw new UserError(
- 'Could not identify the entry module in the wrangler bundle.',
- `Emitted: ${emitted.join(', ')}`
- );
- }
- for (const name of emitted) {
- const bytes = await readFile(path.join(outDir, name));
- form.append(`module:${name}`, new Blob([new Uint8Array(bytes)]), name);
- moduleCount += 1;
- }
- }
-
- // Assets: wrangler's directory, the cloudflarebase.json override, or the
- // conventional build outputs. A bare assets directory deploys as an
- // assets-only Worker.
- const assetsDir = await findAssetsDirectory(
- projectDir,
- managed.assets,
- wrangler?.assetsDirectory
- );
- let assetCount = 0;
- if (assetsDir) {
- for (const file of await collectAssets(assetsDir)) {
- form.append(
- `asset:${file.name}`,
- new Blob([new Uint8Array(file.bytes)]),
- path.basename(file.name)
- );
- assetCount += 1;
- }
- }
- if (!moduleCount && !assetCount) {
- throw new UserError(
- 'Nothing to deploy - no Worker main and no assets directory.',
- 'Build your app first, or set "assets" in cloudflarebase.json.\n' +
- 'Framework apps need their Cloudflare adapter: SvelteKit @sveltejs/adapter-cloudflare, ' +
- 'Next.js @opennextjs/cloudflare (adapter-auto and plain `next build` emit nothing deployable).'
- );
- }
-
- form.append(
- 'meta',
- JSON.stringify({
- mainModule,
- compatibilityDate: wrangler?.compatibilityDate,
- compatibilityFlags: wrangler?.compatibilityFlags,
- vars: managed.vars
- })
- );
-
- step(`Uploading ${moduleCount} module(s) and ${assetCount} asset(s)`);
- const response = await hostingFetch(
- managed.origin,
- token,
- `/api/projects/${target}/hosting/apps/${managed.app}/deploys`,
- { method: 'POST', body: form }
- );
- if (!response.ok) {
- throw new UserError(`Deploy failed: ${await failureText(response)}`);
- }
- const result = (await response.json()) as {
- subdomain: string;
- url: string | null;
- deploy: { status: string };
- };
-
- blank();
- success(result.url ? `Deployed to ${bold(result.url)}` : `Deployed as ${bold(result.subdomain)}`);
- if (result.subdomain !== (branch ? `${managed.app}-${branch}` : managed.app)) {
- // Auto-numbering claimed a neighbor - say so, never hide it.
- info(` ${dim('·')} The wanted name was taken; this app is claimed as ${result.subdomain}.`);
- }
- if (result.deploy.status === 'stub') {
- info(` ${dim('·')} This console records deploys without a dispatch namespace (stub mode).`);
- }
-}
-
-async function failureText(response: Response): Promise {
- if (response.status === 401) {
- return inGithubActions()
- ? 'the credential was refused - reconnect the repository from the console Hosting page, or check CLOUDFLAREBASE_DEPLOY_TOKEN'
- : 'the credential was refused - the deploy token may be revoked, or sign in again';
- }
- const body = (await response.json().catch(() => null)) as { error?: string } | null;
- return body?.error ?? `the console responded ${response.status}`;
-}
-
-/**
- * The self-hosted wrangler path - unchanged. There is nothing to configure
- * before sign-in works: the agent trusts the deployment's own origin
- * automatically, so a fresh deploy is usable the moment the URL exists.
- */
-async function selfHostedDeploy(projectDir: string): Promise {
+ if (rest.length) throw new UserError(`Unknown flag "${rest[0]}".`);
const configPath = path.join(projectDir, 'wrangler.jsonc');
let configText: string;
try {
@@ -244,7 +18,7 @@ async function selfHostedDeploy(projectDir: string): Promise {
} catch {
throw new UserError(
'No wrangler.jsonc found - nothing to deploy.',
- 'Run `cloudflarebase init ` to scaffold a project, or bare `cloudflarebase init` for managed hosting.'
+ 'Run `cloudflarebase init ` to scaffold a project.'
);
}
diff --git a/cli/src/commands/init.ts b/cli/src/commands/init.ts
index a3cbe4dc..8b6f1703 100644
--- a/cli/src/commands/init.ts
+++ b/cli/src/commands/init.ts
@@ -15,10 +15,10 @@ import { runOrFail } from '../lib/run.js';
* agent package's own fragment, so there is exactly one definition of a
* working configuration and `init` can never drift from it.
*
- * BARE `cloudflarebase init` (no name) is the managed-hosting setup instead:
- * wrangler/Netlify vocabulary for "initialize cloudflarebase in the current
- * directory" - it connects this app to a console project and writes
- * cloudflarebase.json (managed-init.ts).
+ * BARE `cloudflarebase init` (no name) is the managed setup instead: wrangler
+ * vocabulary for "initialize cloudflarebase in the current directory" - it
+ * connects this directory to a console project and writes cloudflarebase.json
+ * (managed-init.ts).
*/
export async function initCommand(cwd: string, args: string[]): Promise {
const name = args[0];
diff --git a/cli/src/commands/key.ts b/cli/src/commands/key.ts
index 12d2cce0..ce0412e6 100644
--- a/cli/src/commands/key.ts
+++ b/cli/src/commands/key.ts
@@ -2,7 +2,7 @@ import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { loadConfig } from '../lib/config.js';
import { blank, bold, dim, info, success, UserError, warn } from '../lib/log.js';
-import { hostingFetch, readManagedConfig, targetProjectId } from '../lib/managed.js';
+import { projectFetch, readManagedConfig, targetProjectId } from '../lib/managed.js';
/**
* `cloudflarebase key create|list|revoke` - project service keys.
@@ -15,8 +15,8 @@ import { hostingFetch, readManagedConfig, targetProjectId } from '../lib/managed
* service keys, or it could grow and outlive itself. So this command needs
* `cloudflarebase login`, exactly like `secret put`.
*
- * Keys are scoped to ONE project - never a root and its branches, the way
- * deploy tokens are - because for data the branch IS the isolation boundary.
+ * Keys are scoped to ONE project - never a root and its branches - because
+ * for data the branch IS the isolation boundary.
* `--branch` therefore targets a specific registry row rather than a family.
*/
@@ -85,14 +85,13 @@ export async function keyCommand(projectDir: string, rest: string[]): Promise`
- * stays the self-hosted scaffold; the wrangler/Netlify-style bare form is
- * "initialize cloudflarebase here": pick (or create) a project, claim an app
- * subdomain - showing the auto-numbered suggestion first when the wanted
- * name is taken, because collisions never fail - and write
- * `cloudflarebase.json`, which is what flips `cloudflarebase deploy` into
- * managed mode.
+ * Bare `cloudflarebase init` - connect the CURRENT directory to a project on a
+ * managed console. `init ` stays the self-hosted scaffold; the
+ * wrangler-style bare form is "initialize cloudflarebase here": pick (or
+ * create) a project and write `cloudflarebase.json`, which is what `key`
+ * reads to know which project it acts on.
*/
interface Flags {
project?: string;
- app?: string;
- yes?: boolean;
}
function parseFlags(rest: string[]): Flags {
@@ -26,8 +21,6 @@ function parseFlags(rest: string[]): Flags {
for (let i = 0; i < rest.length; i += 1) {
const arg = rest[i];
if (arg === '--project') flags.project = rest[++i];
- else if (arg === '--app') flags.app = rest[++i];
- else if (arg === '--yes' || arg === '-y') flags.yes = true;
else throw new UserError(`Unknown flag "${arg}".`);
}
return flags;
@@ -45,7 +38,7 @@ export async function managedInitCommand(projectDir: string, rest: string[]): Pr
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
try {
- // 1. Pick or create the ROOT project (branches are decided per deploy).
+ // Pick or create the ROOT project; commands take --branch for branches.
let projectId = flags.project;
if (!projectId) {
const response = await consoleFetch(config, '/api/registry/projects');
@@ -83,69 +76,20 @@ export async function managedInitCommand(projectDir: string, rest: string[]): Pr
}
}
if (projectId.includes('--')) {
- throw new UserError('Initialize against the ROOT project - branches are decided per deploy.');
- }
-
- // 2. Choose the app name, defaulting to the project id.
- let appName = flags.app;
- if (!appName) {
- const suggestion = APP_NAME.test(projectId) ? projectId : undefined;
- const answer = (
- await rl.question(`App name${suggestion ? ` [${suggestion}]` : ''}: `)
- ).trim();
- appName = answer || suggestion || '';
- }
- if (!APP_NAME.test(appName) || appName.includes('--')) {
throw new UserError(
- `"${appName}" is not a valid app name.`,
- 'Use 3-48 lowercase letters, numbers, and hyphens (no "--").'
+ 'Initialize against the ROOT project - commands take --branch for branches.'
);
}
- // 3. Preview the claim, then take it. Taken names auto-number rather
- // than fail, so the preview is what makes the numbering consensual in
- // an interactive session - CI deploys just take it.
- const preview = await consoleFetch(config, `/api/projects/${projectId}/hosting/claims`, {
- method: 'POST',
- body: JSON.stringify({ app: appName, dry: true })
- });
- if (!preview.ok) {
- throw new UserError(`Could not claim "${appName}": ${await errorText(preview)}`);
- }
- const suggested = (await preview.json()) as { subdomain: string };
- if (suggested.subdomain !== appName) {
- info(`${bold(appName)} is taken - the next free subdomain is ${bold(suggested.subdomain)}.`);
- }
- if (!flags.yes) {
- const confirm = (
- await rl.question(`Claim ${bold(suggested.subdomain)}.cfbase.dev? [Y/n] `)
- ).trim();
- if (confirm && !/^y(es)?$/i.test(confirm)) {
- throw new UserError('Nothing claimed.');
- }
- }
- const claim = await consoleFetch(config, `/api/projects/${projectId}/hosting/claims`, {
- method: 'POST',
- body: JSON.stringify({ app: appName })
- });
- if (!claim.ok) {
- throw new UserError(`Could not claim "${appName}": ${await errorText(claim)}`);
- }
- const claimed = (await claim.json()) as { subdomain: string };
-
- // 4. Write the marker that flips `deploy` into managed mode.
const file = await writeManagedConfig(projectDir, {
project: projectId,
- app: appName,
origin: config.origin
});
blank();
- success(`Initialized: ${bold(projectId)} as ${bold(claimed.subdomain)}`);
- info(` ${dim('·')} ${file} written - commit it; \`cloudflarebase deploy\` is now managed.`);
- info(
- ` ${dim('·')} Root deploys serve at ${bold(`${claimed.subdomain}.cfbase.dev`)}; a branch serves at ${claimed.subdomain}-.cfbase.dev.`
- );
+ success(`Initialized: ${bold(projectId)} on ${config.origin}`);
+ info(` ${dim('·')} ${file} written - commit it.`);
+ info(` ${dim('·')} Next: \`cloudflarebase key create --env-file\` mints a server key.`);
} finally {
rl.close();
}
diff --git a/cli/src/commands/secret.ts b/cli/src/commands/secret.ts
deleted file mode 100644
index 2055a3c0..00000000
--- a/cli/src/commands/secret.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-import readline from 'node:readline/promises';
-import { loadConfig } from '../lib/config.js';
-import { blank, bold, dim, info, success, UserError } from '../lib/log.js';
-import {
- hostingFetch,
- readManagedConfig,
- resolveGitBranch,
- sanitizeBranchName,
- targetProjectId
-} from '../lib/managed.js';
-
-/**
- * `cloudflarebase secret put|list|delete` - secrets on the initialized app
- * (`wrangler secret` vocabulary on purpose).
- *
- * `put` PATCHes the deployed script's settings with `keep_bindings`, so
- * redeploys never drop what is set here; values are write-through and
- * unrecoverable, which is why `list` shows names only. Operator sessions only
- * - deploy tokens deliberately cannot touch secrets.
- */
-
-const USAGE = [
- 'Usage: cloudflarebase secret put [--value ] [--branch ]',
- ' cloudflarebase secret list [--branch ]',
- ' cloudflarebase secret delete [--branch ]'
-].join('\n');
-
-interface SecretTarget {
- origin: string;
- token: string;
- app: string;
- /** The branch-resolved registry project id the request dials. */
- projectId: string;
- /** The short branch name, for messages; null on the root. */
- branch: string | null;
-}
-
-/** The shared preamble: managed config, matching login, branch resolution. */
-async function resolveTarget(projectDir: string, branchFlag?: string): Promise {
- const managed = await readManagedConfig(projectDir);
- if (!managed) {
- throw new UserError('This directory is not initialized.', 'Run `cloudflarebase init` first.');
- }
- const config = await loadConfig();
- if (config.origin !== managed.origin) {
- throw new UserError(
- `This directory is linked to ${managed.origin}, but you are signed in to ${config.origin}.`,
- `Run \`cloudflarebase login ${managed.origin}\` first.`
- );
- }
- const gitBranch = branchFlag ?? (await resolveGitBranch(projectDir));
- const branch = gitBranch ? sanitizeBranchName(gitBranch) : null;
- return {
- origin: managed.origin,
- token: config.token,
- app: managed.app,
- projectId: targetProjectId(managed.project, branch),
- branch
- };
-}
-
-function assertSecretName(name: string | undefined): string {
- if (!name) throw new UserError(USAGE);
- if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {
- throw new UserError(`"${name}" is not a valid secret name.`, 'Use UPPER_SNAKE_CASE.');
- }
- return name;
-}
-
-async function fail(response: Response, verb: string): Promise {
- const body = (await response.json().catch(() => null)) as { error?: string } | null;
- throw new UserError(
- `${verb} failed: ${body?.error ?? `the console responded ${response.status}`}`
- );
-}
-
-export async function secretCommand(projectDir: string, rest: string[]): Promise {
- const [subcommand, ...args] = rest;
-
- if (subcommand === 'put') {
- const name = assertSecretName(args.shift());
- let value: string | undefined;
- let branchFlag: string | undefined;
- for (let i = 0; i < args.length; i += 1) {
- const arg = args[i];
- if (arg === '--value') value = args[++i];
- else if (arg === '--branch') branchFlag = args[++i];
- else throw new UserError(`Unknown flag "${arg}".`);
- }
-
- const target = await resolveTarget(projectDir, branchFlag);
- if (value === undefined) {
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
- try {
- value = await rl.question(`Value for ${bold(name)}: `);
- } finally {
- rl.close();
- }
- }
- if (!value) throw new UserError('The secret value is empty.');
-
- const response = await hostingFetch(
- target.origin,
- target.token,
- `/api/projects/${target.projectId}/hosting/apps/${target.app}/secrets`,
- {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ name, value })
- }
- );
- if (!response.ok) await fail(response, 'Setting the secret');
-
- blank();
- success(
- `Secret ${bold(name)} set on ${bold(target.app)}${target.branch ? ` (${target.branch})` : ''}`
- );
- return;
- }
-
- if (subcommand === 'list') {
- let branchFlag: string | undefined;
- for (let i = 0; i < args.length; i += 1) {
- if (args[i] === '--branch') branchFlag = args[++i];
- else throw new UserError(`Unknown flag "${args[i]}".`);
- }
-
- const target = await resolveTarget(projectDir, branchFlag);
- const response = await hostingFetch(
- target.origin,
- target.token,
- `/api/projects/${target.projectId}/hosting/apps/${target.app}/secrets`
- );
- if (!response.ok) await fail(response, 'Listing secrets');
- const body = (await response.json()) as {
- secrets: { name: string; updatedAt: string }[];
- };
-
- blank();
- if (!body.secrets.length) {
- info(
- `No secrets on ${bold(target.app)}${target.branch ? ` (${target.branch})` : ''}. ` +
- dim('Set one with `cloudflarebase secret put `.')
- );
- return;
- }
- for (const secret of body.secrets) {
- info(`${bold(secret.name)} ${dim(`updated ${secret.updatedAt.slice(0, 10)}`)}`);
- }
- return;
- }
-
- if (subcommand === 'delete') {
- const name = assertSecretName(args.shift());
- let branchFlag: string | undefined;
- for (let i = 0; i < args.length; i += 1) {
- if (args[i] === '--branch') branchFlag = args[++i];
- else throw new UserError(`Unknown flag "${args[i]}".`);
- }
-
- const target = await resolveTarget(projectDir, branchFlag);
- const response = await hostingFetch(
- target.origin,
- target.token,
- `/api/projects/${target.projectId}/hosting/apps/${target.app}/secrets/${name}`,
- { method: 'DELETE' }
- );
- if (!response.ok) await fail(response, 'Deleting the secret');
-
- blank();
- success(
- `Secret ${bold(name)} deleted from ${bold(target.app)}${target.branch ? ` (${target.branch})` : ''}`
- );
- return;
- }
-
- throw new UserError(USAGE);
-}
diff --git a/cli/src/index.ts b/cli/src/index.ts
index ad6b539d..39d17976 100644
--- a/cli/src/index.ts
+++ b/cli/src/index.ts
@@ -9,7 +9,6 @@ import { keyCommand } from './commands/key.js';
import { loginCommand } from './commands/login.js';
import { logoutCommand } from './commands/logout.js';
import { schemaCommand } from './commands/schema.js';
-import { secretCommand } from './commands/secret.js';
import { blank, bold, dim, error, info, UserError } from './lib/log.js';
const usage = (): void => {
@@ -21,12 +20,7 @@ const usage = (): void => {
` cloudflarebase init ${dim('connect this directory to a managed console project')}`
);
info(` cloudflarebase add ${dim('install an agent into an existing Worker')}`);
- info(
- ` cloudflarebase deploy ${dim('deploy - managed once initialized, wrangler otherwise')}`
- );
- info(
- ` cloudflarebase secret ${dim('put | list | delete a secret on the deployed app')}`
- );
+ info(` cloudflarebase deploy ${dim('deploy this Worker with wrangler')}`);
info(` cloudflarebase key ${dim('create | list | revoke a project service key')}`);
info(
` cloudflarebase login ${dim('authenticate against a console (browser approval)')}`
@@ -59,9 +53,6 @@ async function main(): Promise {
case 'deploy':
await deployCommand(cwd, rest);
return;
- case 'secret':
- await secretCommand(cwd, rest);
- return;
case 'key':
await keyCommand(cwd, rest);
return;
diff --git a/cli/src/lib/agents.ts b/cli/src/lib/agents.ts
index 3df24489..a6dbd095 100644
--- a/cli/src/lib/agents.ts
+++ b/cli/src/lib/agents.ts
@@ -33,10 +33,6 @@ export const AGENTS: Record = {
packageName: '@cloudflarebase/db',
description: 'Firestore-style documents with live queries - one Durable Object per collection'
},
- hosting: {
- packageName: '@cloudflarebase/hosting',
- description: 'Apps and functions on Workers for Platforms - assets and code in one deploy'
- },
storage: {
packageName: '@cloudflarebase/storage',
description: 'Object storage on R2 - buckets of files with per-bucket access modes'
diff --git a/cli/src/lib/assetsignore.ts b/cli/src/lib/assetsignore.ts
deleted file mode 100644
index 808dbb2d..00000000
--- a/cli/src/lib/assetsignore.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * `.assetsignore` support for managed deploys.
- *
- * Cloudflare's framework adapters write one into their output directory
- * (SvelteKit's adapter-cloudflare emits `_worker.js`, `_routes.json`,
- * `_headers`, `_redirects`), and wrangler honours it when IT uploads assets.
- * A managed deploy uploads through us instead, so ignoring the file meant
- * publishing the customer's SERVER bundle as a public asset - fetchable at
- * `/_worker.js` on their site. This is the wrangler-compatible subset:
- * gitignore-style lines, `*`/`**`/`?` wildcards, `#` comments. Negation is
- * not supported - a line starting with `!` is skipped rather than guessed at.
- */
-
-const RESERVED_ROOT_FILES = new Set(['_worker.js', '_routes.json', '_headers', '_redirects']);
-
-export interface AssetFilter {
- /** True when `path` (root-relative, no leading slash) must not deploy. */
- ignores(path: string): boolean;
-}
-
-function patternToRegex(pattern: string): RegExp | null {
- // Directory patterns match everything under them; root-anchored patterns
- // (any with a slash) match from the top, bare names match at any depth.
- const dirOnly = pattern.endsWith('/');
- let body = dirOnly ? pattern.slice(0, -1) : pattern;
- const anchored = body.includes('/');
- body = body.replace(/^\//, '').replace(/^\*\*\//, '');
- if (!body) return null;
-
- // Split on `**` first so it never collides with the single-`*` rewrite.
- const escaped = body
- .split('**')
- .map((piece) =>
- piece
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
- .replace(/\*/g, '[^/]*')
- .replace(/\?/g, '[^/]')
- )
- .join('.*');
- const prefix = anchored ? '^' : '(?:^|/)';
- // A match is the entry itself or anything beneath it (files under an
- // ignored directory are ignored with it).
- return new RegExp(`${prefix}${escaped}(?:$|/)`);
-}
-
-/**
- * Parses `.assetsignore` content into a filter. The four Cloudflare
- * convention files are ALWAYS ignored at the root, ignore file or not: they
- * are deploy configuration, never content - `_worker.js` doubly so.
- */
-export function assetFilter(ignoreFileContent: string | null): AssetFilter {
- const patterns: RegExp[] = [];
- for (const rawLine of (ignoreFileContent ?? '').split('\n')) {
- const line = rawLine.trim();
- if (!line || line.startsWith('#') || line.startsWith('!')) continue;
- const regex = patternToRegex(line);
- if (regex) patterns.push(regex);
- }
- return {
- ignores(path: string): boolean {
- if (RESERVED_ROOT_FILES.has(path)) return true;
- return patterns.some((pattern) => pattern.test(path));
- }
- };
-}
diff --git a/cli/src/lib/managed.ts b/cli/src/lib/managed.ts
index 98d2a6d3..eb3c445b 100644
--- a/cli/src/lib/managed.ts
+++ b/cli/src/lib/managed.ts
@@ -1,15 +1,11 @@
-import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
+import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
-import { parse as parseJsonc } from 'jsonc-parser';
-import { assetFilter } from './assetsignore.js';
import { UserError } from './log.js';
-import { run } from './run.js';
/**
- * Managed hosting support (Phase B):
- * `cloudflarebase.json` marks a project as linked to a console, and `deploy`
- * branches on its presence - present means managed deploy against the
- * console's hosting API, absent means the self-hosted wrangler path.
+ * Managed projects: `cloudflarebase.json` links a directory to a project on a
+ * console, so commands that act on that project (`key`) know which one and
+ * where without flags on every call. Written by bare `cloudflarebase init`.
*/
export const MANAGED_FILE = 'cloudflarebase.json';
@@ -17,20 +13,11 @@ export const MANAGED_FILE = 'cloudflarebase.json';
// Mirrors projectIdSchema in the console and the agents (48 chars: a branch
// id is `--`, so the ceiling has to hold both).
const PROJECT_ID = /^[a-z0-9][a-z0-9-]{0,47}$/;
-const BRANCH_NAME = /^[a-z0-9][a-z0-9-]{0,15}$/;
-/** Mirrors the console's appNameSchema. */
-export const APP_NAME = /^[a-z0-9][a-z0-9-]{2,47}$/;
export interface ManagedConfig {
- /** The ROOT project id; the branch is decided per deploy. */
+ /** The ROOT project id; commands take `--branch` to target a branch. */
project: string;
- app: string;
origin: string;
- /** Plain-text vars injected into the deployed Worker. */
- vars?: Record;
- /** Assets directory override (else wrangler.jsonc `assets.directory`,
- * else the first of dist/build/public/_site/out that exists). */
- assets?: string;
}
export async function readManagedConfig(projectDir: string): Promise {
@@ -40,51 +27,20 @@ export async function readManagedConfig(projectDir: string): Promise;
if (
typeof parsed.project !== 'string' ||
- typeof parsed.app !== 'string' ||
typeof parsed.origin !== 'string' ||
- !PROJECT_ID.test(parsed.project) ||
- !APP_NAME.test(parsed.app)
+ !PROJECT_ID.test(parsed.project)
) {
throw new UserError(
`${MANAGED_FILE} is malformed.`,
'Run `cloudflarebase init` again to reconnect this directory.'
);
}
- return {
- project: parsed.project,
- app: parsed.app,
- origin: new URL(parsed.origin).origin,
- vars: typeof parsed.vars === 'object' && parsed.vars ? parsed.vars : undefined,
- assets: typeof parsed.assets === 'string' ? parsed.assets : undefined
- };
-}
-
-/**
- * The same configuration from the environment, for a repository connected
- * through the console's GitHub App.
- *
- * The console knows the project and app when it writes the workflow, so it
- * passes them as env - which means connecting in the console is the entire
- * setup and the repository needs no `cloudflarebase.json` and no local CLI
- * run. Null unless all three are present, so a stray variable can never
- * silently redirect a deploy that has a file.
- */
-export function managedConfigFromEnv(): ManagedConfig | null {
- const origin = process.env.CLOUDFLAREBASE_URL?.trim();
- const project = process.env.CLOUDFLAREBASE_PROJECT?.trim();
- const app = process.env.CLOUDFLAREBASE_APP?.trim();
- if (!origin || !project || !app) return null;
- if (!PROJECT_ID.test(project) || !APP_NAME.test(app)) {
- throw new UserError(
- 'CLOUDFLAREBASE_PROJECT or CLOUDFLAREBASE_APP is malformed.',
- 'Reconnect the repository from the console Hosting page to rewrite the workflow.'
- );
- }
- const assets = process.env.CLOUDFLAREBASE_ASSETS?.trim();
- return { project, app, origin: new URL(origin).origin, assets: assets || undefined };
+ return { project: parsed.project, origin: new URL(parsed.origin).origin };
}
export async function writeManagedConfig(
@@ -96,16 +52,12 @@ export async function writeManagedConfig(
return file;
}
-/**
- * Authenticated fetch against the console's hosting surface. Deliberately not
- * `consoleFetch`: the bearer here may be a deploy token, whose 401 means
- * "revoked or wrong project", not "sign in again".
- */
-export async function hostingFetch(
+/** Authenticated fetch against a project surface on the linked console. */
+export async function projectFetch(
origin: string,
token: string,
route: string,
- init: { method?: string; body?: string | FormData; headers?: Record } = {}
+ init: { method?: string; body?: string; headers?: Record } = {}
): Promise {
return fetch(`${origin}${route}`, {
method: init.method ?? 'GET',
@@ -123,64 +75,7 @@ export async function hostingFetch(
});
}
-/**
- * Git branch -> cloudflarebase branch. The DEFAULT git branch maps to the
- * root project (`main` never appears anywhere - it aliases the root); any
- * other branch maps to `--` after charset sanitizing. Returns
- * null for the root.
- */
-export async function resolveGitBranch(projectDir: string): Promise {
- // CI first: actions/checkout leaves a detached HEAD, so `rev-parse` cannot
- // name the branch there. The official workflow passes both vars; plain
- // GitHub Actions still gets GITHUB_REF_NAME with a main/master heuristic.
- const envBranch =
- process.env.CLOUDFLAREBASE_GIT_BRANCH?.trim() || process.env.GITHUB_REF_NAME?.trim();
- if (envBranch) {
- const envDefault = process.env.CLOUDFLAREBASE_DEFAULT_BRANCH?.trim();
- const isDefault = envDefault
- ? envBranch === envDefault
- : envBranch === 'main' || envBranch === 'master';
- return isDefault ? null : envBranch;
- }
-
- const current = await run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
- cwd: projectDir,
- capture: true
- });
- if (current.code !== 0) return null; // not a git repository -> root
- const branch = current.stdout.trim();
- if (!branch || branch === 'HEAD') return null; // detached -> root
-
- const head = await run('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], {
- cwd: projectDir,
- capture: true
- });
- const defaultBranch = head.code === 0 ? head.stdout.trim().replace(/^origin\//, '') : null;
- if (defaultBranch ? branch === defaultBranch : branch === 'main' || branch === 'master') {
- return null;
- }
- return branch;
-}
-
-/** Charset-sanitizes a git branch name into a cloudflarebase branch name. */
-export function sanitizeBranchName(branch: string): string {
- const cleaned = branch
- .toLowerCase()
- .replace(/[^a-z0-9-]+/g, '-')
- .replace(/-{2,}/g, '-')
- .replace(/^-+|-+$/g, '')
- .slice(0, 16)
- .replace(/-+$/, '');
- if (!BRANCH_NAME.test(cleaned)) {
- throw new UserError(
- `Git branch "${branch}" cannot be mapped to a cloudflarebase branch name.`,
- 'Branch names are 1-16 lowercase letters, numbers, and hyphens. Pass --branch to pick one.'
- );
- }
- return cleaned;
-}
-
-/** Composes the target project id for a deploy, honouring the 48-char ceiling. */
+/** Composes the target project id, honouring the 48-char ceiling. */
export function targetProjectId(root: string, branch: string | null): string {
if (!PROJECT_ID.test(root) || root.includes('--')) {
throw new UserError(`"${root}" is not a valid root project id.`);
@@ -195,117 +90,3 @@ export function targetProjectId(root: string, branch: string | null): string {
}
return id;
}
-
-export interface CollectedFile {
- /** URL path (`/index.html`) for assets, file name for modules. */
- name: string;
- bytes: Buffer;
-}
-
-// Mirror the agent's caps so failures happen before any upload starts.
-const MAX_ASSET_COUNT = 5000;
-const MAX_ASSET_TOTAL_BYTES = 40 * 1024 * 1024;
-
-/**
- * Recursively collects an assets directory into `/path` entries, honouring
- * the directory's `.assetsignore` (framework adapters write one - SvelteKit's
- * lists `_worker.js`, which must never deploy as a public asset).
- */
-export async function collectAssets(dir: string): Promise {
- const files: CollectedFile[] = [];
- let total = 0;
- const ignore = assetFilter(
- await readFile(path.join(dir, '.assetsignore'), 'utf8').catch(() => null)
- );
-
- async function walk(current: string, prefix: string): Promise {
- const entries = await readdir(current, { withFileTypes: true });
- for (const entry of entries) {
- // Dotfiles stay out of a deploy - except `.well-known`, which is a
- // public directory by definition (specs put challenges there).
- if (entry.name === 'node_modules') continue;
- if (entry.name.startsWith('.') && entry.name !== '.well-known') continue;
- const relative = prefix ? `${prefix.slice(1)}/${entry.name}` : entry.name;
- if (ignore.ignores(relative)) continue;
- const full = path.join(current, entry.name);
- if (entry.isDirectory()) {
- await walk(full, `${prefix}/${entry.name}`);
- continue;
- }
- if (!entry.isFile()) continue;
- const bytes = await readFile(full);
- total += bytes.length;
- files.push({ name: `${prefix}/${entry.name}`, bytes });
- if (files.length > MAX_ASSET_COUNT) {
- throw new UserError(`More than ${MAX_ASSET_COUNT} asset files in ${dir}.`);
- }
- if (total > MAX_ASSET_TOTAL_BYTES) {
- throw new UserError(`Assets in ${dir} exceed 40 MB.`);
- }
- }
- }
-
- await walk(dir, '');
- return files;
-}
-
-/** The user's wrangler config, when one exists - the managed deploy respects
- * its `main`, `assets.directory`, and compatibility settings. */
-export async function readUserWranglerConfig(projectDir: string): Promise<{
- main?: string;
- assetsDirectory?: string;
- compatibilityDate?: string;
- compatibilityFlags?: string[];
-} | null> {
- for (const name of ['wrangler.jsonc', 'wrangler.json']) {
- let raw: string;
- try {
- raw = await readFile(path.join(projectDir, name), 'utf8');
- } catch {
- continue;
- }
- const config = parseJsonc(raw) as Record | undefined;
- if (!config) return null;
- const assets = config.assets as { directory?: string } | undefined;
- return {
- main: typeof config.main === 'string' ? config.main : undefined,
- assetsDirectory: typeof assets?.directory === 'string' ? assets.directory : undefined,
- compatibilityDate:
- typeof config.compatibility_date === 'string' ? config.compatibility_date : undefined,
- compatibilityFlags: Array.isArray(config.compatibility_flags)
- ? (config.compatibility_flags as string[]).filter(
- (flag) => typeof flag === 'string' && /^[a-z0-9_]+$/.test(flag)
- )
- : undefined
- };
- }
- return null;
-}
-
-/** Finds the assets directory: explicit override, wrangler config, then the
- * conventional build outputs. Null when nothing exists. */
-export async function findAssetsDirectory(
- projectDir: string,
- override: string | undefined,
- fromWrangler: string | undefined
-): Promise {
- const candidates = override
- ? [override]
- : fromWrangler
- ? [fromWrangler]
- : // `.output/public` before `public`: a Nuxt repo has BOTH, and
- // `public/` is its source directory, not the built site.
- ['dist', 'build', '.output/public', 'public', '_site', 'out'];
- for (const candidate of candidates) {
- const full = path.resolve(projectDir, candidate);
- try {
- if ((await stat(full)).isDirectory()) return full;
- } catch {
- // keep looking
- }
- }
- if (override || fromWrangler) {
- throw new UserError(`Assets directory "${override ?? fromWrangler}" does not exist.`);
- }
- return null;
-}
diff --git a/cli/src/lib/oidc.ts b/cli/src/lib/oidc.ts
deleted file mode 100644
index 3b53ccb9..00000000
--- a/cli/src/lib/oidc.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { UserError } from './log.js';
-
-/**
- * GitHub Actions OIDC (Phase B).
- *
- * A repository connected through the console's GitHub App deploys with NO
- * stored credential: the workflow asks GitHub for a short-lived token
- * describing the repository it is running in, and the console verifies that
- * against GitHub's public keys. There is nothing to rotate and nothing to
- * leak, which is the same reason this repository publishes to npm this way.
- *
- * The CLI mints it rather than the workflow, so the generated YAML stays a
- * checkout, a build, and a deploy - no token plumbing for anyone to get
- * wrong.
- */
-
-/** Whether this process is inside a GitHub Actions run. */
-export function inGithubActions(): boolean {
- return process.env.GITHUB_ACTIONS === 'true';
-}
-
-/**
- * Mints an identity token for `audience` (the console's origin - GitHub will
- * mint for any audience asked, so the console checks it to refuse tokens
- * minted for someone else).
- *
- * Null when this is not an Actions run. Throws when it IS one but the token
- * service is unreachable, because the cause is nearly always a missing
- * `id-token: write` permission and silently falling through to "no
- * credential" would report that as a confusing 401 much later.
- */
-export async function actionsIdToken(audience: string): Promise {
- const url = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
- const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
- if (!url || !requestToken) {
- if (!inGithubActions()) return null;
- throw new UserError(
- 'This workflow cannot mint a GitHub identity token.',
- 'Add `permissions: { id-token: write }` to the job, or set CLOUDFLAREBASE_DEPLOY_TOKEN.'
- );
- }
-
- const response = await fetch(`${url}&audience=${encodeURIComponent(audience)}`, {
- headers: { authorization: `Bearer ${requestToken}`, accept: 'application/json' }
- }).catch((cause: unknown) => {
- throw new UserError(
- "Could not reach GitHub's identity token service.",
- cause instanceof Error ? cause.message : undefined
- );
- });
- if (!response.ok) {
- throw new UserError(
- `GitHub refused to mint an identity token (${response.status}).`,
- 'Check that the job declares `permissions: { id-token: write }`.'
- );
- }
-
- const body = (await response.json().catch(() => null)) as { value?: string } | null;
- if (!body?.value) {
- throw new UserError("GitHub's identity token response carried no token.");
- }
- return body.value;
-}
From c99fbe433a7c58300199254b1cd1857585440362 Mon Sep 17 00:00:00 2001
From: clibequilibrium <105135724+clibequilibrium@users.noreply.github.com>
Date: Wed, 7 Oct 2026 20:09:37 -0400
Subject: [PATCH 3/8] Remove the hosting agent package
The console and CLI no longer reference it, so the package, its deploy
workflows, its quality/release entries, and the docs describing hosting
go too.
---
.../hosting-agent-deploy-preview.yaml | 60 -
.../workflows/hosting-agent-deploy-prod.yaml | 60 -
.github/workflows/quality.yaml | 10 +-
.github/workflows/release.yaml | 4 +-
AGENTS.md | 9 +-
README.md | 14 +-
agents/auth/wrangler.jsonc | 2 +-
agents/hosting/.gitignore | 167 -
agents/hosting/.prettierrc | 6 -
agents/hosting/AGENTS.md | 135 -
agents/hosting/CLAUDE.md | 4 -
agents/hosting/LICENSE | 202 -
agents/hosting/NOTICE | 18 -
agents/hosting/README.md | 57 -
agents/hosting/cloudflarebase.agent.json | 55 -
agents/hosting/drizzle.config.ts | 11 -
agents/hosting/drizzle/0000_red_la_nuit.sql | 19 -
.../hosting/drizzle/0001_slippery_prism.sql | 36 -
.../hosting/drizzle/0002_amazing_kronos.sql | 1 -
.../hosting/drizzle/meta/0000_snapshot.json | 137 -
.../hosting/drizzle/meta/0001_snapshot.json | 349 -
.../hosting/drizzle/meta/0002_snapshot.json | 356 -
agents/hosting/drizzle/meta/_journal.json | 27 -
agents/hosting/drizzle/migrations.js | 14 -
agents/hosting/package-lock.json | 5912 ------
agents/hosting/package.json | 73 -
.../hosting/scripts/generate-migrations.mjs | 73 -
agents/hosting/src/agent.ts | 1433 --
agents/hosting/src/bindings.test-d.ts | 42 -
agents/hosting/src/bindings.ts | 73 -
agents/hosting/src/build-env.ts | 20 -
agents/hosting/src/build-env.unit.test.ts | 32 -
agents/hosting/src/cloudflare.ts | 447 -
agents/hosting/src/cloudflare.unit.test.ts | 168 -
agents/hosting/src/crypto.ts | 73 -
agents/hosting/src/crypto.unit.test.ts | 71 -
agents/hosting/src/db/schema.ts | 116 -
agents/hosting/src/env.d.ts | 8 -
agents/hosting/src/index.ts | 424 -
agents/hosting/src/migrations.ts | 102 -
agents/hosting/src/outbound.ts | 20 -
agents/hosting/src/route-access.ts | 105 -
agents/hosting/src/route-access.unit.test.ts | 73 -
agents/hosting/src/schemas.ts | 97 -
agents/hosting/src/tar.fixture.ts | 20 -
agents/hosting/src/tar.ts | 171 -
agents/hosting/src/tar.unit.test.ts | 94 -
agents/hosting/template/worker-entry.ts | 20 -
.../hosting/template/wrangler-fragment.jsonc | 71 -
agents/hosting/tsconfig.build.json | 32 -
agents/hosting/tsconfig.json | 50 -
agents/hosting/worker-configuration.d.ts | 14893 ----------------
agents/hosting/wrangler.jsonc | 277 -
agents/hosting/wrangler.outbound.jsonc | 38 -
agents/storage/src/access.ts | 4 +-
agents/storage/src/demo.ts | 2 +-
agents/storage/src/index.ts | 4 +-
agents/storage/src/schemas.ts | 2 +-
agents/storage/wrangler.jsonc | 2 +-
59 files changed, 18 insertions(+), 26777 deletions(-)
delete mode 100644 .github/workflows/hosting-agent-deploy-preview.yaml
delete mode 100644 .github/workflows/hosting-agent-deploy-prod.yaml
delete mode 100644 agents/hosting/.gitignore
delete mode 100644 agents/hosting/.prettierrc
delete mode 100644 agents/hosting/AGENTS.md
delete mode 100644 agents/hosting/CLAUDE.md
delete mode 100644 agents/hosting/LICENSE
delete mode 100644 agents/hosting/NOTICE
delete mode 100644 agents/hosting/README.md
delete mode 100644 agents/hosting/cloudflarebase.agent.json
delete mode 100644 agents/hosting/drizzle.config.ts
delete mode 100644 agents/hosting/drizzle/0000_red_la_nuit.sql
delete mode 100644 agents/hosting/drizzle/0001_slippery_prism.sql
delete mode 100644 agents/hosting/drizzle/0002_amazing_kronos.sql
delete mode 100644 agents/hosting/drizzle/meta/0000_snapshot.json
delete mode 100644 agents/hosting/drizzle/meta/0001_snapshot.json
delete mode 100644 agents/hosting/drizzle/meta/0002_snapshot.json
delete mode 100644 agents/hosting/drizzle/meta/_journal.json
delete mode 100644 agents/hosting/drizzle/migrations.js
delete mode 100644 agents/hosting/package-lock.json
delete mode 100644 agents/hosting/package.json
delete mode 100644 agents/hosting/scripts/generate-migrations.mjs
delete mode 100644 agents/hosting/src/agent.ts
delete mode 100644 agents/hosting/src/bindings.test-d.ts
delete mode 100644 agents/hosting/src/bindings.ts
delete mode 100644 agents/hosting/src/build-env.ts
delete mode 100644 agents/hosting/src/build-env.unit.test.ts
delete mode 100644 agents/hosting/src/cloudflare.ts
delete mode 100644 agents/hosting/src/cloudflare.unit.test.ts
delete mode 100644 agents/hosting/src/crypto.ts
delete mode 100644 agents/hosting/src/crypto.unit.test.ts
delete mode 100644 agents/hosting/src/db/schema.ts
delete mode 100644 agents/hosting/src/env.d.ts
delete mode 100644 agents/hosting/src/index.ts
delete mode 100644 agents/hosting/src/migrations.ts
delete mode 100644 agents/hosting/src/outbound.ts
delete mode 100644 agents/hosting/src/route-access.ts
delete mode 100644 agents/hosting/src/route-access.unit.test.ts
delete mode 100644 agents/hosting/src/schemas.ts
delete mode 100644 agents/hosting/src/tar.fixture.ts
delete mode 100644 agents/hosting/src/tar.ts
delete mode 100644 agents/hosting/src/tar.unit.test.ts
delete mode 100644 agents/hosting/template/worker-entry.ts
delete mode 100644 agents/hosting/template/wrangler-fragment.jsonc
delete mode 100644 agents/hosting/tsconfig.build.json
delete mode 100644 agents/hosting/tsconfig.json
delete mode 100644 agents/hosting/worker-configuration.d.ts
delete mode 100644 agents/hosting/wrangler.jsonc
delete mode 100644 agents/hosting/wrangler.outbound.jsonc
diff --git a/.github/workflows/hosting-agent-deploy-preview.yaml b/.github/workflows/hosting-agent-deploy-preview.yaml
deleted file mode 100644
index 4087cacb..00000000
--- a/.github/workflows/hosting-agent-deploy-preview.yaml
+++ /dev/null
@@ -1,60 +0,0 @@
-name: Hosting Agent Preview
-
-on:
- workflow_dispatch:
- push:
- branches:
- - preview
- paths:
- - 'agents/hosting/**'
- - 'src/lib/**'
-
-permissions:
- contents: read
-
-jobs:
- deploy-preview:
- runs-on: ubuntu-latest
- name: Deploy Preview
- # Cloudflare credentials live in this GitHub environment, not repository
- # secrets, so only jobs that declare it - running refs its protection
- # rules allow - can read them.
- environment: preview
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: ${{ vars.NODE_VERSION }}
-
- - name: Install dependencies
- run: npm ci
-
- # The dispatch-namespace binding in env.preview names this service, so
- # it must exist before the agent deploys. Preview has its OWN outbound
- # (hosting-outbound-preview): this workflow redeploys it on every push,
- # and a shared instance would put preview-branch outbound code on
- # production apps' egress.
- - name: Deploy Outbound Worker
- uses: cloudflare/wrangler-action@v3
- with:
- wranglerVersion: ${{ vars.WRANGLER_VERSION }}
- apiToken: ${{ secrets.CF_API_TOKEN }}
- accountId: ${{ secrets.CF_ACCOUNT_ID}}
- workingDirectory: 'agents/hosting'
- packageManager: npm
- command: deploy --config wrangler.outbound.jsonc --env preview
-
- - name: Deploy Preview
- uses: cloudflare/wrangler-action@v3
- with:
- wranglerVersion: ${{ vars.WRANGLER_VERSION }}
- apiToken: ${{ secrets.CF_API_TOKEN }}
- accountId: ${{ secrets.CF_ACCOUNT_ID}}
- workingDirectory: 'agents/hosting'
- packageManager: npm
- # Needs the cfbase-apps-preview namespace to exist first (Phase B
- # launch checklist); serving stays off (env.preview has no
- # HOSTING_DOMAIN), so this only exercises the upload path.
- environment: preview
diff --git a/.github/workflows/hosting-agent-deploy-prod.yaml b/.github/workflows/hosting-agent-deploy-prod.yaml
deleted file mode 100644
index 72ccaf5f..00000000
--- a/.github/workflows/hosting-agent-deploy-prod.yaml
+++ /dev/null
@@ -1,60 +0,0 @@
-name: Hosting Agent Prod
-
-on:
- workflow_dispatch:
- push:
- branches:
- - main
- paths:
- - 'agents/hosting/**'
- - 'src/lib/**'
-
-permissions:
- contents: read
-
-jobs:
- deploy-prod:
- runs-on: ubuntu-latest
- name: Deploy Prod
- # Cloudflare credentials live in this GitHub environment, not repository
- # secrets, so only jobs that declare it - running refs its protection
- # rules allow - can read them.
- environment: production
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: ${{ vars.NODE_VERSION }}
-
- - name: Install dependencies
- run: npm ci
-
- # The dispatch-namespace binding in env.production names this service,
- # so it must exist before the agent deploys.
- - name: Deploy Outbound Worker
- uses: cloudflare/wrangler-action@v3
- with:
- wranglerVersion: ${{ vars.WRANGLER_VERSION }}
- apiToken: ${{ secrets.CF_API_TOKEN }}
- accountId: ${{ secrets.CF_ACCOUNT_ID}}
- workingDirectory: 'agents/hosting'
- packageManager: npm
- command: deploy --config wrangler.outbound.jsonc
-
- - name: Deploy Prod
- uses: cloudflare/wrangler-action@v3
- with:
- wranglerVersion: ${{ vars.WRANGLER_VERSION }}
- apiToken: ${{ secrets.CF_API_TOKEN }}
- accountId: ${{ secrets.CF_ACCOUNT_ID}}
- workingDirectory: 'agents/hosting'
- packageManager: npm
- # The top level of wrangler.jsonc is the self-hosted default;
- # cloudflarebase.com's own configuration lives in env.production,
- # which pins the Worker name back to `hosting-agent`. This deploy
- # needs the cfbase.dev zone, the cfbase-apps namespace, and the
- # CF_ACCOUNT_ID/CF_HOSTING_API_TOKEN worker secrets to exist first
- # (Phase B launch checklist).
- environment: production
diff --git a/.github/workflows/quality.yaml b/.github/workflows/quality.yaml
index 16685684..a988af9a 100644
--- a/.github/workflows/quality.yaml
+++ b/.github/workflows/quality.yaml
@@ -63,15 +63,7 @@ jobs:
run: npm run test:unit
working-directory: agents/storage
- - name: Typecheck the hosting agent
- run: npx tsc --noEmit
- working-directory: agents/hosting
-
- - name: Unit-test the hosting agent
- run: npm run test:unit
- working-directory: agents/hosting
-
- # The CLI is a fifth npm project; the root install does not cover it.
+ # The CLI is a fourth npm project; the root install does not cover it.
- name: Install CLI dependencies
run: npm ci
working-directory: cli
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 277f18e0..8d4f2cae 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -13,7 +13,7 @@ name: Release
# for an approval click.
#
# Tags remain the manual escape hatch: pushing auth-v1.2.3, db-v1.2.3,
-# storage-v1.2.3, hosting-v1.2.3, or cli-v1.2.3 publishes that one package,
+# storage-v1.2.3, or cli-v1.2.3 publishes that one package,
# refusing a tag whose version does not match the package.json it points at.
#
# Publishing uses npm trusted publishing (OIDC): npmjs.com is configured to
@@ -38,7 +38,6 @@ on:
- 'auth-v*.*.*'
- 'db-v*.*.*'
- 'storage-v*.*.*'
- - 'hosting-v*.*.*'
- 'cli-v*.*.*'
permissions:
@@ -69,7 +68,6 @@ jobs:
{ name: '@cloudflarebase/auth', dir: 'agents/auth', prefix: 'auth-v' },
{ name: '@cloudflarebase/db', dir: 'agents/db', prefix: 'db-v' },
{ name: '@cloudflarebase/storage', dir: 'agents/storage', prefix: 'storage-v' },
- { name: '@cloudflarebase/hosting', dir: 'agents/hosting', prefix: 'hosting-v' },
{ name: '@cloudflarebase/cli', dir: 'cli', prefix: 'cli-v' },
];
const refType = process.env.GITHUB_REF_TYPE;
diff --git a/AGENTS.md b/AGENTS.md
index 03325160..dd469dbb 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -9,7 +9,7 @@ that only matter inside it. Read that one before changing an agent.
## Repository shape
-Six separate npm projects, separate Wrangler configs, separate generated `Env`
+Five separate npm projects, separate Wrangler configs, separate generated `Env`
types.
| Path | Worker | Durable Objects |
@@ -18,7 +18,6 @@ types.
| `agents/auth` | `auth-agent` | `AuthAgent` |
| `agents/db` | `db-agent` | `DbAgent` `DbCollection` `DbTable` `DbGateway` `DbView` |
| `agents/storage` | `storage-agent` | `StorageAgent` `StorageBucket` |
-| `agents/hosting` | `hosting-agent` | `HostingAgent` (+ an outbound worker) |
| `cli` | none | `@cloudflarebase/cli`, runs on a consumer's machine |
**Never import runtime code or generated Worker types across those
@@ -86,15 +85,15 @@ service binding the console already authorized. No agent ever sees the bearer.
## Commands
```bash
-npm run dev # auth :8788, db :8789, hosting :8790, storage :8791, web :5173
+npm run dev # auth :8788, db :8789, storage :8791, web :5173
npm run check # svelte-check
npm run lint # prettier + eslint
npm test # full Playwright suite against real workerd
```
All three checks run in CI and all three must pass. The e2e suite boots a
-production-mirroring stack — the built SvelteKit worker on `:8797` plus the four
-agents on `:8798`–`:8801`, real service bindings, real DO SQLite. It is the
+production-mirroring stack — the built SvelteKit worker on `:8797` plus the three
+agents on `:8798`, `:8799` and `:8801`, real service bindings, real DO SQLite. It is the
check that actually catches things.
Each agent is its own TypeScript project with its own unit tests:
diff --git a/README.md b/README.md
index 5bf07c8f..f7be76ca 100644
--- a/README.md
+++ b/README.md
@@ -28,8 +28,6 @@ isolation by architecture, not by a `WHERE` clause.
flipped without shipping a release.
- **Storage** — buckets of files on R2: public/auth/owner access modes,
signed URLs, multipart uploads, a file browser in the console.
-- **Hosting** — static sites and Workers at `.cfbase.dev`, deployed from
- the CLI or on every git push.
Use it hosted at [cloudflarebase.com](https://cloudflarebase.com), or run the
whole stack on your own Cloudflare account. Same code either way.
@@ -46,18 +44,16 @@ const baseUrl = 'https://cloudflarebase.com/api/projects/';
Add your app's origin under the project's **Settings** — that list is the CSRF
allowlist, and an unlisted origin gets a 403.
-To host the front end too:
+For a server credential, from the CLI:
```bash
npm install -g @cloudflarebase/cli
cloudflarebase login
-cloudflarebase init # links this directory to a project + app
-cloudflarebase deploy # -> https://.cfbase.dev
+cloudflarebase init # links this directory to a project
+cloudflarebase key create server --env-file # writes a service key to .env.local
```
-Or connect the GitHub repo from the Hosting page and every push deploys.
-Branches serve at `-.cfbase.dev`. Limits: 5 projects per org,
-5 branches per project, 10 apps per project.
+Limits: 5 projects per org, 5 branches per project.
## Self-host
@@ -83,7 +79,6 @@ Everything else is optional and degrades cleanly when absent:
- **Storage** needs R2 — create a bucket and add the `BUCKET` binding
described in `agents/storage/wrangler.jsonc`.
-- **Hosting** needs Workers for Platforms (paid); deploys 503 without it.
- **Auth-event charts** need Analytics Engine, a free dashboard toggle; then
add the two lines shown in `agents/auth/wrangler.jsonc`.
- Google/GitHub sign-in, email, and Sentry are opt-in secrets.
@@ -92,7 +87,6 @@ Prefer buttons? One per Worker, same order as `deploy:all`:
[auth](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflarebase/cloudflarebase/tree/main/agents/auth) ·
[db](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflarebase/cloudflarebase/tree/main/agents/db) ·
[storage](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflarebase/cloudflarebase/tree/main/agents/storage) ·
-[hosting](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflarebase/cloudflarebase/tree/main/agents/hosting) ·
[dashboard](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflarebase/cloudflarebase)
## Add the agents to a Worker you already have
diff --git a/agents/auth/wrangler.jsonc b/agents/auth/wrangler.jsonc
index 0c9cd067..42d2ea10 100644
--- a/agents/auth/wrangler.jsonc
+++ b/agents/auth/wrangler.jsonc
@@ -158,7 +158,7 @@
// verification, the same configuration preview rehearses. It took
// effect the moment this agent deployed, which is safe because the
// ownership-aware web worker was already live (its OpenAPI serves
- // /branches and /hosting/*) - a stranger who registers gets their
+ // /branches) - a stranger who registers gets their
// own personal org and sees nothing else. Mail (the EMAIL binding
// + EMAIL_FROM below) is what makes "open" effective; without it
// the agent reports "claimed" and 503s sign-up rather than
diff --git a/agents/hosting/.gitignore b/agents/hosting/.gitignore
deleted file mode 100644
index 4138168d..00000000
--- a/agents/hosting/.gitignore
+++ /dev/null
@@ -1,167 +0,0 @@
-# Logs
-
-logs
-_.log
-npm-debug.log_
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
-.pnpm-debug.log*
-
-# Diagnostic reports (https://nodejs.org/api/report.html)
-
-report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
-
-# Runtime data
-
-pids
-_.pid
-_.seed
-\*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-
-lib-cov
-
-# Coverage directory used by tools like istanbul
-
-coverage
-\*.lcov
-
-# nyc test coverage
-
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-
-bower_components
-
-# node-waf configuration
-
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-
-build/Release
-
-# Dependency directories
-
-node_modules/
-jspm_packages/
-
-# Snowpack dependency directory (https://snowpack.dev/)
-
-web_modules/
-
-# TypeScript cache
-
-\*.tsbuildinfo
-
-# Optional npm cache directory
-
-.npm
-
-# Optional eslint cache
-
-.eslintcache
-
-# Optional stylelint cache
-
-.stylelintcache
-
-# Microbundle cache
-
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-
-.node_repl_history
-
-# Output of 'npm pack'
-
-\*.tgz
-
-# Yarn Integrity file
-
-.yarn-integrity
-
-# parcel-bundler cache (https://parceljs.org/)
-
-.cache
-.parcel-cache
-
-# Next.js build output
-
-.next
-out
-
-# Nuxt.js build / generate output
-
-.nuxt
-dist
-
-# Gatsby files
-
-.cache/
-
-# Comment in the public line in if your project uses Gatsby and not Next.js
-
-# https://nextjs.org/blog/next-9-1#public-directory-support
-
-# public
-
-# vuepress build output
-
-.vuepress/dist
-
-# vuepress v2.x temp and cache directory
-
-.temp
-.cache
-
-# Docusaurus cache and generated files
-
-.docusaurus
-
-# Serverless directories
-
-.serverless/
-
-# FuseBox cache
-
-.fusebox/
-
-# DynamoDB Local files
-
-.dynamodb/
-
-# TernJS port file
-
-.tern-port
-
-# Stores VSCode versions used for testing VSCode extensions
-
-.vscode-test
-
-# yarn v2
-
-.yarn/cache
-.yarn/unplugged
-.yarn/build-state.yml
-.yarn/install-state.gz
-.pnp.\*
-
-# wrangler project
-
-.dev.vars*
-!.dev.vars.example
-.env*
-!.env.example
-.wrangler/
diff --git a/agents/hosting/.prettierrc b/agents/hosting/.prettierrc
deleted file mode 100644
index d910b4e4..00000000
--- a/agents/hosting/.prettierrc
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "printWidth": 100,
- "singleQuote": true,
- "semi": true,
- "useTabs": true
-}
\ No newline at end of file
diff --git a/agents/hosting/AGENTS.md b/agents/hosting/AGENTS.md
deleted file mode 100644
index 065c2095..00000000
--- a/agents/hosting/AGENTS.md
+++ /dev/null
@@ -1,135 +0,0 @@
-# @cloudflarebase/hosting
-
-Static sites and Workers on Workers for Platforms, served at
-`.`. One `HostingAgent` per project.
-
-Read the root [AGENTS.md](../../AGENTS.md) first.
-
-## Two workers, and one of them has two roles
-
-`wrangler.jsonc` deploys `hosting-agent`; `wrangler.outbound.jsonc` deploys the
-dispatch namespace's **outbound worker** separately. `deploy:production` runs
-both, outbound first — deploy them together or the namespace loses its egress
-hook.
-
-`hosting-agent` itself branches on **hostname**:
-
-- **Serving** — a request on the `*.` wildcard route takes the first
- host label and does `env.DISPATCH.get(hostLabel)`. Zero lookup, because the
- script name *is* the full subdomain; dispatch never parses it back into app
- and branch. An unclaimed subdomain is simply no script, and gets the branded
- 404.
-- **Agent surface** on every other hostname — `/health`, `/internal/*`, and
- `routeAgentRequest`. `/internal/*` is reachable only over the dashboard's
- service binding: this Worker has no public route beyond the wildcard, and the
- serve branch swallows every wildcard request before dispatch gets here.
-
-The serve path never touches `HostingAgent`. Control plane and data plane are
-fully separate, same as storage.
-
-## What only holds here
-
-- **Subdomains are pushed by the console**, resolved from the control-plane
- `app` claims table. The agent never derives or accepts one from a request —
- that is what makes the deploy route safe on the operator surface: it can only
- ever deploy to subdomains the console recorded for *this* project.
-- **The cache shim is not optional.** Workers for Platforms runs namespaced
- scripts in untrusted mode, which is what isolates tenants — and in that mode
- `caches.default` throws and `request.cf` is absent. Frameworks call it
- unconditionally (SvelteKit's adapter opens every request with
- `caches.default.match(req)`), so an unmodified build 500s on every SSR route.
- Each module deploy gets a generated entry that imports the shim **first** and
- re-exports the customer's entry: ES modules evaluate imports depth-first in
- source order, so a module-scope capture like `var s2 = caches.default` picks up
- the neutralised methods. Generated names carry a `__cfbase` prefix so a
- customer file can never be shadowed. The namespace-level `trusted_workers`
- flag would fix it by turning tenant isolation off for every app at once — the
- opposite trade.
-- **Asset manifest hashes are salted with the project id** (SHA-256 over
- `\0`, truncated to the API's 32 hex chars). Assets are
- deduplicated *by hash* within a namespace, so unsalted hashes would let one
- tenant probe another's content.
-- **A tarball is attacker-controlled input.** `tar.ts` is bounded everywhere: the
- gunzip reader checks the ceiling *as bytes arrive*, never after — waiting
- until the end is what a decompression bomb wants.
-- **Deploys are parsed in memory**, so the limits in `agent.ts` are sized against
- the 128 MB isolate: 20 MB modules, 5000 assets, 40 MB asset total, 25 MB per
- file. Cloudflare's own 10 MB-compressed script ceiling still applies at upload.
-- **Hosting has no public routes at all.** Its whole manifest route table is
- operator: `/overview`, `/apps/*`, `/deploys`.
-
-## App environments
-
-Three stores per app, three different trust shapes — never mix them up:
-
-- **Runtime vars** (`app_vars`): plaintext at rest on purpose — they upload as
- `plain_text` bindings anyway, and DO storage is the trust boundary. Applied
- on every deploy as platform > stored > the CLI's `meta.vars` (the console is
- the canonical editor), and PATCHed onto the live script on edit. The patch
- replaces the WHOLE `plain_text` set (that is how a deletion disappears), so
- `apps.last_deploy_vars` snapshots the CLI's vars to rebuild it without the
- CLI present. Store-first: an edit is never lost to a transient API failure.
-- **Runtime secrets** (`app_secrets`): the value is write-through to
- Cloudflare's script settings (that is the runtime binding; deploys carry
- `keep_bindings: ['secret_text']` so it survives), AND stored AES-256-GCM
- under `HOSTING_MASTER_KEY` when the install has one, so builds receive it —
- the Pages model: frameworks inline env at build time. The AAD carries a
- `runtime\0` prefix so this ciphertext and a build secret's are never
- interchangeable. Null ciphertext (keyless install, or rows from before the
- column existed) means runtime-only: the bundle skips it rather than failing
- the build. The operator surface still answers names only. Delete uses the
- per-script secrets endpoint (404-tolerant), then drops the row — a failed
- CF delete keeps the row, which is the honest state.
-- **Build env** (`build_vars` / `build_secrets`, ROOT project only —
- connection-scoped): build-only OVERRIDES. The bundle the GitHub Actions
- workflow fetches via its OIDC token is runtime vars+secrets merged under
- these (`src/build-env.ts` pins build-wins-on-collision). Build secrets are
- AES-256-GCM under `HOSTING_MASTER_KEY` (`src/crypto.ts`: `v1::`
- versioned format, row-bound AAD `\0`). Decrypted values only
- ever transit the service-binding-only `/internal/.../build-env` route; the
- operator surface answers names only. No key ⇒ build-secret writes 503;
- encrypted rows with no key ⇒ the bundle route fails loud rather than build
- without secrets.
-
-`eraseApp` clears all three even without an `apps` row — claim-only apps can
-be configured before their first deploy.
-
-## Analytics
-
-The serve path writes one Analytics Engine data point per dispatched request
-(`index1` = subdomain, `blob1` = status, `double1` = duration ms) — the
-subdomain IS the script name, so the DO joins it back to an app with zero
-lookups. Unclaimed-subdomain 404s are deliberately not recorded. The read
-(`GET /apps/:app/analytics`) mirrors the auth agent's ladder: `connected`
-(CF_ACCOUNT_ID + CF_ANALYTICS_API_TOKEN + WAE_DATASET) → `local` (the
-LOCAL_ANALYTICS D1 stand-in local dev and e2e read back) → `write-only` →
-`error`, and a failed query answers 200 with zeroed series, never a 5xx. The
-top-level wrangler.jsonc declares NO dataset — Analytics Engine is an
-account-level opt-in and the binding fails a fresh clone's deploy (code
-10089, the auth precedent).
-
-## Degradation
-
-Workers for Platforms is a paid product. Without `CF_ACCOUNT_ID` +
-`CF_HOSTING_API_TOKEN` + a dispatch namespace, deploys answer 503 and the rest
-of the stack is unaffected. `HOSTING_DOMAIN` empty disables the serving path.
-`HOSTING_MASTER_KEY` unset disables only build-secret writes.
-
-Stub mode (`HOSTING_STUB=true`) stubs the Cloudflare API, never the logic:
-deploys are recorded, secrets store their names (and answer ok), build-secret
-encryption is REAL (local/test pin a dev master key as a var), and analytics
-read the local D1 — which is what lets e2e exercise every contract on every
-run. The two shapes only a live namespace proves — the settings PATCH on an
-assets-only script and the per-script secrets DELETE — are gated by the
-opt-in `RUN_HOSTING_E2E` spec; do not call those surfaces "verified" on a new
-account until it has passed.
-
-## Commands
-
-```bash
-npm run dev # wrangler dev --env local, :8790
-npm run typecheck
-npm run test:unit # route-access, tar, cloudflare, crypto
-npm run deploy:outbound # the outbound worker — needed before the agent
-npm run migrations
-```
diff --git a/agents/hosting/CLAUDE.md b/agents/hosting/CLAUDE.md
deleted file mode 100644
index ff2b4a18..00000000
--- a/agents/hosting/CLAUDE.md
+++ /dev/null
@@ -1,4 +0,0 @@
-Guidance for this package lives in AGENTS.md, imported here so the two can
-never drift.
-
-@AGENTS.md
diff --git a/agents/hosting/LICENSE b/agents/hosting/LICENSE
deleted file mode 100644
index dd8e76f4..00000000
--- a/agents/hosting/LICENSE
+++ /dev/null
@@ -1,202 +0,0 @@
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2026 The Cloudflarebase Authors
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/agents/hosting/NOTICE b/agents/hosting/NOTICE
deleted file mode 100644
index 5e2a3b13..00000000
--- a/agents/hosting/NOTICE
+++ /dev/null
@@ -1,18 +0,0 @@
-Cloudflarebase
-Copyright 2026 The Cloudflarebase Authors
-
-This product includes software developed by The Cloudflarebase Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License"); you may not use
-this file except in compliance with the License. You may obtain a copy of the
-License at http://www.apache.org/licenses/LICENSE-2.0
-
-------------------------------------------------------------------------------
-
-Cloudflarebase is an independent open-source project. It is not affiliated with,
-endorsed by, or sponsored by Cloudflare, Inc.
-
-"Cloudflare", "Workers", "Durable Objects", "D1", "R2", "KV", and related marks
-are trademarks of Cloudflare, Inc. They are used here only to describe the
-platform this software is built on, as permitted by nominative fair use. This
-project claims no rights in them.
diff --git a/agents/hosting/README.md b/agents/hosting/README.md
deleted file mode 100644
index 673eb11a..00000000
--- a/agents/hosting/README.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# @cloudflarebase/hosting
-
-Apps and functions on Cloudflare Workers for Platforms - static assets and
-server code in one deploy, served at your subdomain. The hosting primitive
-behind [Cloudflarebase](https://github.com/cloudflarebase/cloudflarebase).
-
-One `HostingAgent` Durable Object per project orchestrates deploys (asset
-upload sessions, script puts, per-script tags and limits) while the same
-worker serves every app on a wildcard domain by dispatching the host label
-straight into the namespace - the script name IS the subdomain, so serving
-needs zero lookups.
-
-## Install
-
-```sh
-npx @cloudflarebase/cli add hosting
-```
-
-The CLI installs this package, merges `template/wrangler-fragment.jsonc` into
-your `wrangler.jsonc`, prepends the entrypoint re-export, and reruns
-`wrangler types`. Workers for Platforms is a paid Cloudflare add-on; the
-fragment's header documents the manual steps that turn recorded deploys into
-served apps (dispatch namespace, API token, wildcard route).
-
-## What it serves
-
-- An "app" is one user Worker in the dispatch namespace: a static site, an
- SSR frontend, backend functions, or all three in one artifact (modules plus
- an asset manifest).
-- Deploys go through the Cloudflarebase console (bare `cloudflarebase init`,
- then `cloudflarebase deploy`), which resolves the subdomain claim in its
- control plane and pushes it here - this agent owns no global state.
-- Every script is uploaded with a `pid-` tag (project erase
- deletes by tag), fixed CPU/subrequest limits applied at dispatch, and
- injected `PROJECT_ID` / `CLOUDFLAREBASE_URL` vars so the SDK works out of
- the box.
-
-## What your Worker serves
-
-Nothing, over HTTP. Every route this agent has - `/overview`, `/apps/*`
-(deploys, secrets), `/deploys`, the state-sync socket, `/internal/*` -
-deploys code, mints subdomains, or writes secrets, and none of them
-authenticate a caller: they are meant to sit behind a console that already
-has. Mounted on your own public Worker they answer 404.
-
-Serving deployed apps is unaffected - that runs on your wildcard hostname,
-ahead of everything else.
-
-Drive deploys from your own code through the `HostingAgent` Durable Object
-namespace binding, which no HTTP caller can reach. To serve the routes over
-HTTP instead, put your own authentication in front and set
-`"EXPOSE_OPERATOR_API": "true"` - only on a Worker with no public hostname of
-its own.
-
-## License
-
-Apache-2.0. See NOTICE.
diff --git a/agents/hosting/cloudflarebase.agent.json b/agents/hosting/cloudflarebase.agent.json
deleted file mode 100644
index 301b1a46..00000000
--- a/agents/hosting/cloudflarebase.agent.json
+++ /dev/null
@@ -1,55 +0,0 @@
-{
- "manifestVersion": 1,
- "name": "hosting",
- "title": "Hosting",
- "description": "Apps and functions on Workers for Platforms - static assets and server code in one deploy, served at your subdomain.",
- "packageName": "@cloudflarebase/hosting",
- "worker": "hosting-agent",
- "durableObjects": [{ "class": "HostingAgent", "scope": "perProject" }],
- "entrypoint": { "assertEnvType": "AssertHostingAgentEnv" },
- "erase": { "method": "DELETE", "path": "/internal/projects/:projectId" },
- "bindings": {
- "analyticsEngine": [
- { "binding": "HOSTING_REQUESTS", "dataset": "cloudflarebase_hosting_requests" }
- ]
- },
- "secrets": {
- "generated": [],
- "optional": [
- "CF_ACCOUNT_ID",
- "CF_HOSTING_API_TOKEN",
- "HOSTING_MASTER_KEY",
- "CF_ANALYTICS_API_TOKEN"
- ]
- },
- "vars": {
- "SENTRY_DSN": {
- "default": "",
- "hint": "Error reporting; empty disables it. A var, not a secret - a DSN is public by design."
- },
- "HOSTING_DOMAIN": {
- "default": "",
- "hint": "Apex domain apps serve under (e.g. cfbase.dev). Empty disables the serving path."
- },
- "DISPATCH_NAMESPACE": {
- "default": "",
- "hint": "Workers for Platforms dispatch namespace name the DISPATCH binding targets."
- },
- "WAE_DATASET": {
- "default": "cloudflarebase_hosting_requests",
- "hint": "Analytics Engine dataset for per-request analytics; reads need CF_ANALYTICS_API_TOKEN."
- }
- },
- "routes": [
- { "path": "/overview", "access": "operator" },
- { "path": "/apps/*", "access": "operator" },
- { "path": "/deploys", "access": "operator" }
- ],
- "proxy": { "apiPrefix": "hosting", "agentBasePath": "" },
- "permissions": [],
- "console": {
- "section": "Hosting",
- "icon": "rocket",
- "pages": [{ "path": "/hosting", "title": "Apps", "testId": "nav-hosting", "icon": "rocket" }]
- }
-}
diff --git a/agents/hosting/drizzle.config.ts b/agents/hosting/drizzle.config.ts
deleted file mode 100644
index 0e7c3343..00000000
--- a/agents/hosting/drizzle.config.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { defineConfig } from 'drizzle-kit';
-
-// Durable Object SQLite: drizzle-kit emits an importable migrations bundle
-// (./drizzle/migrations.js) that the agent applies at runtime via
-// drizzle-orm/durable-sqlite/migrator.
-export default defineConfig({
- out: './drizzle',
- schema: './src/db/schema.ts',
- dialect: 'sqlite',
- driver: 'durable-sqlite',
-});
diff --git a/agents/hosting/drizzle/0000_red_la_nuit.sql b/agents/hosting/drizzle/0000_red_la_nuit.sql
deleted file mode 100644
index fd1219f9..00000000
--- a/agents/hosting/drizzle/0000_red_la_nuit.sql
+++ /dev/null
@@ -1,19 +0,0 @@
-CREATE TABLE `apps` (
- `name` text PRIMARY KEY NOT NULL,
- `subdomain` text NOT NULL,
- `created_at` integer NOT NULL,
- `last_deploy_at` integer,
- `deploy_count` integer DEFAULT 0 NOT NULL
-);
---> statement-breakpoint
-CREATE TABLE `deploys` (
- `id` text PRIMARY KEY NOT NULL,
- `app_name` text NOT NULL,
- `subdomain` text NOT NULL,
- `status` text NOT NULL,
- `has_worker` integer NOT NULL,
- `asset_count` integer NOT NULL,
- `asset_bytes` integer NOT NULL,
- `module_bytes` integer NOT NULL,
- `created_at` integer NOT NULL
-);
diff --git a/agents/hosting/drizzle/0001_slippery_prism.sql b/agents/hosting/drizzle/0001_slippery_prism.sql
deleted file mode 100644
index cce5281c..00000000
--- a/agents/hosting/drizzle/0001_slippery_prism.sql
+++ /dev/null
@@ -1,36 +0,0 @@
-CREATE TABLE `app_secrets` (
- `app_name` text NOT NULL,
- `name` text NOT NULL,
- `created_at` integer NOT NULL,
- `updated_at` integer NOT NULL,
- PRIMARY KEY(`app_name`, `name`)
-);
---> statement-breakpoint
-CREATE TABLE `app_vars` (
- `app_name` text NOT NULL,
- `name` text NOT NULL,
- `value` text NOT NULL,
- `created_at` integer NOT NULL,
- `updated_at` integer NOT NULL,
- PRIMARY KEY(`app_name`, `name`)
-);
---> statement-breakpoint
-CREATE TABLE `build_secrets` (
- `app_name` text NOT NULL,
- `name` text NOT NULL,
- `ciphertext` text NOT NULL,
- `created_at` integer NOT NULL,
- `updated_at` integer NOT NULL,
- PRIMARY KEY(`app_name`, `name`)
-);
---> statement-breakpoint
-CREATE TABLE `build_vars` (
- `app_name` text NOT NULL,
- `name` text NOT NULL,
- `value` text NOT NULL,
- `created_at` integer NOT NULL,
- `updated_at` integer NOT NULL,
- PRIMARY KEY(`app_name`, `name`)
-);
---> statement-breakpoint
-ALTER TABLE `apps` ADD `last_deploy_vars` text;
\ No newline at end of file
diff --git a/agents/hosting/drizzle/0002_amazing_kronos.sql b/agents/hosting/drizzle/0002_amazing_kronos.sql
deleted file mode 100644
index 3c1abd3d..00000000
--- a/agents/hosting/drizzle/0002_amazing_kronos.sql
+++ /dev/null
@@ -1 +0,0 @@
-ALTER TABLE `app_secrets` ADD `ciphertext` text;
\ No newline at end of file
diff --git a/agents/hosting/drizzle/meta/0000_snapshot.json b/agents/hosting/drizzle/meta/0000_snapshot.json
deleted file mode 100644
index 2a6caeaf..00000000
--- a/agents/hosting/drizzle/meta/0000_snapshot.json
+++ /dev/null
@@ -1,137 +0,0 @@
-{
- "version": "6",
- "dialect": "sqlite",
- "id": "d74ad2e0-6cb5-473f-a27d-6cfff48bdde7",
- "prevId": "00000000-0000-0000-0000-000000000000",
- "tables": {
- "apps": {
- "name": "apps",
- "columns": {
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "subdomain": {
- "name": "subdomain",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "last_deploy_at": {
- "name": "last_deploy_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "deploy_count": {
- "name": "deploy_count",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": 0
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "deploys": {
- "name": "deploys",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "subdomain": {
- "name": "subdomain",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "has_worker": {
- "name": "has_worker",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "asset_count": {
- "name": "asset_count",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "asset_bytes": {
- "name": "asset_bytes",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "module_bytes": {
- "name": "module_bytes",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- }
- },
- "views": {},
- "enums": {},
- "_meta": {
- "schemas": {},
- "tables": {},
- "columns": {}
- },
- "internal": {
- "indexes": {}
- }
-}
\ No newline at end of file
diff --git a/agents/hosting/drizzle/meta/0001_snapshot.json b/agents/hosting/drizzle/meta/0001_snapshot.json
deleted file mode 100644
index 188d22cc..00000000
--- a/agents/hosting/drizzle/meta/0001_snapshot.json
+++ /dev/null
@@ -1,349 +0,0 @@
-{
- "version": "6",
- "dialect": "sqlite",
- "id": "fd9d68a8-b16b-44c9-9c15-a258777a870e",
- "prevId": "d74ad2e0-6cb5-473f-a27d-6cfff48bdde7",
- "tables": {
- "app_secrets": {
- "name": "app_secrets",
- "columns": {
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {
- "app_secrets_app_name_name_pk": {
- "columns": [
- "app_name",
- "name"
- ],
- "name": "app_secrets_app_name_name_pk"
- }
- },
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "app_vars": {
- "name": "app_vars",
- "columns": {
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "value": {
- "name": "value",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {
- "app_vars_app_name_name_pk": {
- "columns": [
- "app_name",
- "name"
- ],
- "name": "app_vars_app_name_name_pk"
- }
- },
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "apps": {
- "name": "apps",
- "columns": {
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "subdomain": {
- "name": "subdomain",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "last_deploy_at": {
- "name": "last_deploy_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "deploy_count": {
- "name": "deploy_count",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": 0
- },
- "last_deploy_vars": {
- "name": "last_deploy_vars",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "build_secrets": {
- "name": "build_secrets",
- "columns": {
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "ciphertext": {
- "name": "ciphertext",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {
- "build_secrets_app_name_name_pk": {
- "columns": [
- "app_name",
- "name"
- ],
- "name": "build_secrets_app_name_name_pk"
- }
- },
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "build_vars": {
- "name": "build_vars",
- "columns": {
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "value": {
- "name": "value",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {
- "build_vars_app_name_name_pk": {
- "columns": [
- "app_name",
- "name"
- ],
- "name": "build_vars_app_name_name_pk"
- }
- },
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "deploys": {
- "name": "deploys",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "subdomain": {
- "name": "subdomain",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "has_worker": {
- "name": "has_worker",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "asset_count": {
- "name": "asset_count",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "asset_bytes": {
- "name": "asset_bytes",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "module_bytes": {
- "name": "module_bytes",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- }
- },
- "views": {},
- "enums": {},
- "_meta": {
- "schemas": {},
- "tables": {},
- "columns": {}
- },
- "internal": {
- "indexes": {}
- }
-}
\ No newline at end of file
diff --git a/agents/hosting/drizzle/meta/0002_snapshot.json b/agents/hosting/drizzle/meta/0002_snapshot.json
deleted file mode 100644
index 69171256..00000000
--- a/agents/hosting/drizzle/meta/0002_snapshot.json
+++ /dev/null
@@ -1,356 +0,0 @@
-{
- "version": "6",
- "dialect": "sqlite",
- "id": "3bd97b28-75fd-40aa-bf0f-cd89f32ee8d1",
- "prevId": "fd9d68a8-b16b-44c9-9c15-a258777a870e",
- "tables": {
- "app_secrets": {
- "name": "app_secrets",
- "columns": {
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "ciphertext": {
- "name": "ciphertext",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {
- "app_secrets_app_name_name_pk": {
- "columns": [
- "app_name",
- "name"
- ],
- "name": "app_secrets_app_name_name_pk"
- }
- },
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "app_vars": {
- "name": "app_vars",
- "columns": {
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "value": {
- "name": "value",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {
- "app_vars_app_name_name_pk": {
- "columns": [
- "app_name",
- "name"
- ],
- "name": "app_vars_app_name_name_pk"
- }
- },
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "apps": {
- "name": "apps",
- "columns": {
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "subdomain": {
- "name": "subdomain",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "last_deploy_at": {
- "name": "last_deploy_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- },
- "deploy_count": {
- "name": "deploy_count",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false,
- "default": 0
- },
- "last_deploy_vars": {
- "name": "last_deploy_vars",
- "type": "text",
- "primaryKey": false,
- "notNull": false,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "build_secrets": {
- "name": "build_secrets",
- "columns": {
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "ciphertext": {
- "name": "ciphertext",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {
- "build_secrets_app_name_name_pk": {
- "columns": [
- "app_name",
- "name"
- ],
- "name": "build_secrets_app_name_name_pk"
- }
- },
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "build_vars": {
- "name": "build_vars",
- "columns": {
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "value": {
- "name": "value",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {
- "build_vars_app_name_name_pk": {
- "columns": [
- "app_name",
- "name"
- ],
- "name": "build_vars_app_name_name_pk"
- }
- },
- "uniqueConstraints": {},
- "checkConstraints": {}
- },
- "deploys": {
- "name": "deploys",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true,
- "autoincrement": false
- },
- "app_name": {
- "name": "app_name",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "subdomain": {
- "name": "subdomain",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "status": {
- "name": "status",
- "type": "text",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "has_worker": {
- "name": "has_worker",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "asset_count": {
- "name": "asset_count",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "asset_bytes": {
- "name": "asset_bytes",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "module_bytes": {
- "name": "module_bytes",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- },
- "created_at": {
- "name": "created_at",
- "type": "integer",
- "primaryKey": false,
- "notNull": true,
- "autoincrement": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "checkConstraints": {}
- }
- },
- "views": {},
- "enums": {},
- "_meta": {
- "schemas": {},
- "tables": {},
- "columns": {}
- },
- "internal": {
- "indexes": {}
- }
-}
\ No newline at end of file
diff --git a/agents/hosting/drizzle/meta/_journal.json b/agents/hosting/drizzle/meta/_journal.json
deleted file mode 100644
index f22283c4..00000000
--- a/agents/hosting/drizzle/meta/_journal.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "version": "7",
- "dialect": "sqlite",
- "entries": [
- {
- "idx": 0,
- "version": "6",
- "when": 1786556374566,
- "tag": "0000_red_la_nuit",
- "breakpoints": true
- },
- {
- "idx": 1,
- "version": "6",
- "when": 1787197325964,
- "tag": "0001_slippery_prism",
- "breakpoints": true
- },
- {
- "idx": 2,
- "version": "6",
- "when": 1787253423135,
- "tag": "0002_amazing_kronos",
- "breakpoints": true
- }
- ]
-}
\ No newline at end of file
diff --git a/agents/hosting/drizzle/migrations.js b/agents/hosting/drizzle/migrations.js
deleted file mode 100644
index e14960b1..00000000
--- a/agents/hosting/drizzle/migrations.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import journal from './meta/_journal.json';
-import m0000 from './0000_red_la_nuit.sql';
-import m0001 from './0001_slippery_prism.sql';
-import m0002 from './0002_amazing_kronos.sql';
-
- export default {
- journal,
- migrations: {
- m0000,
-m0001,
-m0002
- }
- }
-
\ No newline at end of file
diff --git a/agents/hosting/package-lock.json b/agents/hosting/package-lock.json
deleted file mode 100644
index bc2f21d6..00000000
--- a/agents/hosting/package-lock.json
+++ /dev/null
@@ -1,5912 +0,0 @@
-{
- "name": "@cloudflarebase/hosting",
- "version": "0.4.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "@cloudflarebase/hosting",
- "version": "0.4.0",
- "license": "Apache-2.0",
- "dependencies": {
- "@sentry/cloudflare": "^10.66.0",
- "agents": "^0.17.4",
- "drizzle-orm": "^0.45.2",
- "zod": "^4.4.3"
- },
- "devDependencies": {
- "@types/node": "^26.1.1",
- "drizzle-kit": "^0.31.10",
- "tsx": "^4.20.0",
- "typescript": "^5.5.2",
- "wrangler": "^4.118.0"
- }
- },
- "node_modules/@apm-js-collab/code-transformer": {
- "version": "0.18.1",
- "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer/-/code-transformer-0.18.1.tgz",
- "integrity": "sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@types/estree": "^1.0.8",
- "astring": "^1.9.0",
- "esquery": "^1.7.0",
- "meriyah": "^6.1.4",
- "semifies": "^1.0.0",
- "source-map": "^0.6.0"
- },
- "bin": {
- "code-transformer": "cli.js"
- }
- },
- "node_modules/@apm-js-collab/code-transformer-bundler-plugins": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/@apm-js-collab/code-transformer-bundler-plugins/-/code-transformer-bundler-plugins-0.7.4.tgz",
- "integrity": "sha512-nAfOeZPSUAQvJa1iFT/5oCrTm5YQhMMrfCNthNnaXHZiOQhu1KGuLoIx7HtbAi3wfwaBYLaICPIeenIaEwcXIg==",
- "license": "MIT",
- "dependencies": {
- "@apm-js-collab/code-transformer": "^0.18.1",
- "es-module-lexer": "^2.1.0",
- "magic-string": "^0.30.21",
- "module-details-from-path": "^1.0.4"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@apm-js-collab/tracing-hooks": {
- "version": "0.13.0",
- "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.13.0.tgz",
- "integrity": "sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@apm-js-collab/code-transformer": "^0.18.0",
- "debug": "^4.4.1",
- "module-details-from-path": "^1.0.4"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.0.tgz",
- "integrity": "sha512-dYYg153EyN2Ekbqw2zAsbd6/JR+9N2SEoC7YV2GyyqMM7x9bLDTjBD6XBhSMLH0wtIVyJj03jWNriQhaN+eoCw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^8.0.0",
- "js-tokens": "^10.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.0.tgz",
- "integrity": "sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.1.tgz",
- "integrity": "sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/code-frame": "^8.0.0",
- "@babel/generator": "^8.0.0",
- "@babel/helper-compilation-targets": "^8.0.0",
- "@babel/helpers": "^8.0.0",
- "@babel/parser": "^8.0.0",
- "@babel/template": "^8.0.0",
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0",
- "@types/gensync": "^1.0.5",
- "convert-source-map": "^2.0.0",
- "empathic": "^2.0.1",
- "gensync": "^1.0.0-beta.2",
- "import-meta-resolve": "^4.2.0",
- "json5": "^2.2.3",
- "obug": "^2.1.1",
- "semver": "^7.7.3"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/generator": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz",
- "integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==",
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^8.0.0",
- "@babel/types": "^8.0.0",
- "@jridgewell/gen-mapping": "^0.3.12",
- "@jridgewell/trace-mapping": "^0.3.28",
- "@types/jsesc": "^2.5.0",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-annotate-as-pure": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-8.0.0.tgz",
- "integrity": "sha512-NSpMkMsvvZqzThJ0p1B02cbtA2ObEyfBvq950bmNkyxsxvcxwhvvCB036rKhlEnuBBo30bOrk13u3FzlKSoRrw==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.0.tgz",
- "integrity": "sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/compat-data": "^8.0.0",
- "@babel/helper-validator-option": "^8.0.0",
- "browserslist": "^4.24.0",
- "lru-cache": "^11.0.0",
- "semver": "^7.7.3"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-create-class-features-plugin": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-8.0.1.tgz",
- "integrity": "sha512-++t3ZktzlLmASAxIlxeXQK9Z2YwUafYGYcvGBFevqOqt16HozVHStUoQvWD09fzAZOb/uJGpUTBuGK41AJAuOA==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-annotate-as-pure": "^8.0.0",
- "@babel/helper-member-expression-to-functions": "^8.0.0",
- "@babel/helper-optimise-call-expression": "^8.0.0",
- "@babel/helper-replace-supers": "^8.0.1",
- "@babel/helper-skip-transparent-expression-wrappers": "^8.0.0",
- "@babel/traverse": "^8.0.0",
- "semver": "^7.7.3"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/helper-globals": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.0.tgz",
- "integrity": "sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-member-expression-to-functions": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-8.0.0.tgz",
- "integrity": "sha512-xkXrMbtk87Gk7+oKBVmBc6EORg/Qwx++AHESldmHkpvG8wgccdhJJFwrzqlF382Fk8wfXhJHWE/g/43QvEGNPQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-optimise-call-expression": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-8.0.0.tgz",
- "integrity": "sha512-3W6satvtPuCUkUx63S2jMoW9EQNYkADgs1HTfufmL7gCmAulHMKupA/12WNz4A0GMMFn/YnWWwqOT9IZrJHQjg==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-plugin-utils": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-8.0.1.tgz",
- "integrity": "sha512-3PKFgjTyPlhFhorfP+SjKQxLViIL++zWjFOO4hGriYU+Bsm983DxEM1JmDRJVWXV0O9npu+xXRqz7Pbd3mh70g==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/helper-replace-supers": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-8.0.1.tgz",
- "integrity": "sha512-B1SZADIcy3tmH8CmWvj4SHi/oAPom4UL3uknTc2QRNsPVLFk/sPnZvQL/8kj7Y5omvjMqie0vklvs6XM4OLW5Q==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-member-expression-to-functions": "^8.0.0",
- "@babel/helper-optimise-call-expression": "^8.0.0",
- "@babel/traverse": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-8.0.0.tgz",
- "integrity": "sha512-xmCA9kP3IhySsqhzwIdWGlDN/1A4cCKNBO/uwZx/3YzmDoMePwno2Q5/Bq0q+tYaKbeF940YiKV/kaW8Mzvpjw==",
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
- "integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
- "integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
- "license": "MIT",
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz",
- "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.0.tgz",
- "integrity": "sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@babel/template": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
- "integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^8.0.4"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/plugin-proposal-decorators": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-8.0.2.tgz",
- "integrity": "sha512-+C6O6KKXU7BBq1GNaIkFJxrALUVGRcr+WeWm4OcuRl3h+l/CmNfcTLMrT2Lm3uvGBimBH/8pEBRrXJFLoO67Gg==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-create-class-features-plugin": "^8.0.1",
- "@babel/helper-plugin-utils": "^8.0.1",
- "@babel/plugin-syntax-decorators": "^8.0.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/plugin-syntax-decorators": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-8.0.1.tgz",
- "integrity": "sha512-NI+0S/6MvR6GlcQFwjDZ+WIc2qvG6TXN534lYs9llNldwW4b7Dh6KTtk030FA0xWdYGs4t1lWo+OEWN8wGB+Nw==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-plugin-utils": "^8.0.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- },
- "peerDependencies": {
- "@babel/core": "^8.0.0"
- }
- },
- "node_modules/@babel/runtime": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
- "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/runtime-corejs3": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.7.tgz",
- "integrity": "sha512-ppj9ouYku+RX0ljtgZd+KMO5mkM2bCqg8H2PYAFWnLsHEIKIdRojqbJ2i3eVHrisuxy7nOFCmngTDdWtUCdXUQ==",
- "license": "MIT",
- "dependencies": {
- "core-js-pure": "^3.48.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/template": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz",
- "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^8.0.0",
- "@babel/parser": "^8.0.0",
- "@babel/types": "^8.0.0"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.4.tgz",
- "integrity": "sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==",
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^8.0.0",
- "@babel/generator": "^8.0.0",
- "@babel/helper-globals": "^8.0.0",
- "@babel/parser": "^8.0.4",
- "@babel/template": "^8.0.0",
- "@babel/types": "^8.0.4",
- "obug": "^2.1.1"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "8.0.4",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
- "integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^8.0.0",
- "@babel/helper-validator-identifier": "^8.0.4"
- },
- "engines": {
- "node": "^22.18.0 || >=24.11.0"
- }
- },
- "node_modules/@cfworker/json-schema": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
- "integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
- "license": "MIT"
- },
- "node_modules/@cloudflare/codemode": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/@cloudflare/codemode/-/codemode-0.4.4.tgz",
- "integrity": "sha512-GL0IF7NiHwAC222riEYGX75NHcnhTornAPfRI0Mj3Ig7C1y9H89FL0xs+pdflIF74eecNkzynsA+JdCIkGvKeA==",
- "license": "MIT",
- "dependencies": {
- "@types/json-schema": "^7.0.15",
- "acorn": "^8.17.0"
- },
- "peerDependencies": {
- "@modelcontextprotocol/sdk": "^1.25.0",
- "@tanstack/ai": ">=0.8.0 <1.0.0",
- "ai": "^6.0.0",
- "zod": "^4.0.0"
- },
- "peerDependenciesMeta": {
- "@modelcontextprotocol/sdk": {
- "optional": true
- },
- "@tanstack/ai": {
- "optional": true
- },
- "ai": {
- "optional": true
- },
- "zod": {
- "optional": true
- }
- }
- },
- "node_modules/@cloudflare/kv-asset-handler": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
- "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",
- "devOptional": true,
- "license": "MIT OR Apache-2.0",
- "engines": {
- "node": ">=22.0.0"
- }
- },
- "node_modules/@cloudflare/unenv-preset": {
- "version": "2.16.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",
- "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",
- "devOptional": true,
- "license": "MIT OR Apache-2.0",
- "peerDependencies": {
- "unenv": "2.0.0-rc.24",
- "workerd": ">1.20260305.0 <2.0.0-0"
- },
- "peerDependenciesMeta": {
- "workerd": {
- "optional": true
- }
- }
- },
- "node_modules/@cloudflare/workerd-darwin-64": {
- "version": "1.20260811.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260811.1.tgz",
- "integrity": "sha512-i5jqz+ywtOefr0AJbiAc8qxBLfSim/B0WJG7aW3B+pWnoVfMJdUQvi+BWcFKZJ0MoCci3KadTx6g31VfuEEqpQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@cloudflare/workerd-darwin-arm64": {
- "version": "1.20260811.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260811.1.tgz",
- "integrity": "sha512-NoOUM/nvaDdm2Onlnz33FikWjtatzulNtvwvy4xs0IrHaTCHwC0c8NwIt6s+AI13FkDs02/vm2I3GTPLCT9+hQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@cloudflare/workerd-linux-64": {
- "version": "1.20260811.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260811.1.tgz",
- "integrity": "sha512-sdYq2jL1AD1supa3fsi5O4zTB28wSjvTHj7Migh6/ts8EROPdvrSwv+rdGHhv8HJNAz/wbIAY3wZsi1Rw4uUIg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@cloudflare/workerd-linux-arm64": {
- "version": "1.20260811.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260811.1.tgz",
- "integrity": "sha512-RIRv4shbu1kg05sD+DHTpSFCNnb5Dl2SkPDMUykqZa508tkPqe7VVw7gO0Q5msTBGyL0FfFrLuRxwwfA8u5Sow==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@cloudflare/workerd-windows-64": {
- "version": "1.20260811.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260811.1.tgz",
- "integrity": "sha512-g6VquwjASlYAibcNW/0E6Zszht4qLkmnXOGwIjjRHl2A0Qz48kVeMcGvyH6eA0G9U3OzZojjYFpP+YeyQmmdjw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/@cloudflare/workers-types": {
- "version": "5.20260812.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260812.1.tgz",
- "integrity": "sha512-eG2aQdUWH8RYEubpBjUE98L2G57JYzGEcI8PgROTjt9YjunbEnPBHP4371Usj8v8pTWcXtbOmI+LRCXF4KhJgQ==",
- "license": "MIT OR Apache-2.0",
- "peer": true
- },
- "node_modules/@cspotcode/source-map-support": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
- "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/trace-mapping": "0.3.9"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.9",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
- "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.0.3",
- "@jridgewell/sourcemap-codec": "^1.4.10"
- }
- },
- "node_modules/@drizzle-team/brocli": {
- "version": "0.10.2",
- "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
- "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.11.3",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
- "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@esbuild-kit/core-utils": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
- "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
- "deprecated": "Merged into tsx: https://tsx.hirok.io",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "~0.18.20",
- "source-map-support": "^0.5.21"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
- "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
- "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
- "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
- "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
- "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
- "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
- "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
- "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
- "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
- "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
- "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
- "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
- "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
- "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
- "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
- "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
- "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
- "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
- "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
- "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
- "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
- "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
- "version": "0.18.20",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
- "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=12"
- },
- "optionalDependencies": {
- "@esbuild/android-arm": "0.18.20",
- "@esbuild/android-arm64": "0.18.20",
- "@esbuild/android-x64": "0.18.20",
- "@esbuild/darwin-arm64": "0.18.20",
- "@esbuild/darwin-x64": "0.18.20",
- "@esbuild/freebsd-arm64": "0.18.20",
- "@esbuild/freebsd-x64": "0.18.20",
- "@esbuild/linux-arm": "0.18.20",
- "@esbuild/linux-arm64": "0.18.20",
- "@esbuild/linux-ia32": "0.18.20",
- "@esbuild/linux-loong64": "0.18.20",
- "@esbuild/linux-mips64el": "0.18.20",
- "@esbuild/linux-ppc64": "0.18.20",
- "@esbuild/linux-riscv64": "0.18.20",
- "@esbuild/linux-s390x": "0.18.20",
- "@esbuild/linux-x64": "0.18.20",
- "@esbuild/netbsd-x64": "0.18.20",
- "@esbuild/openbsd-x64": "0.18.20",
- "@esbuild/sunos-x64": "0.18.20",
- "@esbuild/win32-arm64": "0.18.20",
- "@esbuild/win32-ia32": "0.18.20",
- "@esbuild/win32-x64": "0.18.20"
- }
- },
- "node_modules/@esbuild-kit/esm-loader": {
- "version": "2.6.5",
- "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
- "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
- "deprecated": "Merged into tsx: https://tsx.hirok.io",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@esbuild-kit/core-utils": "^3.3.2",
- "get-tsconfig": "^4.7.0"
- }
- },
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
- "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
- "cpu": [
- "ppc64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
- "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
- "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
- "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
- "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
- "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
- "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
- "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
- "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
- "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
- "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
- "cpu": [
- "ia32"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
- "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
- "cpu": [
- "loong64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
- "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
- "cpu": [
- "mips64el"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
- "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
- "cpu": [
- "ppc64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
- "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
- "cpu": [
- "riscv64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
- "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
- "cpu": [
- "s390x"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
- "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
- "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
- "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
- "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
- "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
- "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
- "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
- "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
- "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
- "cpu": [
- "ia32"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/win32-x64": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
- "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@hono/node-server": {
- "version": "1.19.17",
- "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz",
- "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18.14.1"
- },
- "peerDependencies": {
- "hono": "^4"
- }
- },
- "node_modules/@img/colour": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
- "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@img/sharp-darwin-arm64": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz",
- "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.3.1"
- }
- },
- "node_modules/@img/sharp-darwin-x64": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz",
- "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.3.1"
- }
- },
- "node_modules/@img/sharp-freebsd-wasm32": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz",
- "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "dependencies": {
- "@img/sharp-wasm32": "0.35.2"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz",
- "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz",
- "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz",
- "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz",
- "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-ppc64": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz",
- "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-riscv64": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz",
- "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz",
- "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz",
- "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz",
- "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz",
- "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-linux-arm": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz",
- "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.3.1"
- }
- },
- "node_modules/@img/sharp-linux-arm64": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz",
- "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.3.1"
- }
- },
- "node_modules/@img/sharp-linux-ppc64": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz",
- "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-ppc64": "1.3.1"
- }
- },
- "node_modules/@img/sharp-linux-riscv64": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz",
- "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-riscv64": "1.3.1"
- }
- },
- "node_modules/@img/sharp-linux-s390x": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz",
- "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.3.1"
- }
- },
- "node_modules/@img/sharp-linux-x64": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz",
- "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.3.1"
- }
- },
- "node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz",
- "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.3.1"
- }
- },
- "node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz",
- "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.3.1"
- }
- },
- "node_modules/@img/sharp-wasm32": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz",
- "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==",
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/runtime": "^1.11.1"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-webcontainers-wasm32": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz",
- "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "dependencies": {
- "@img/sharp-wasm32": "0.35.2"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-arm64": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz",
- "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-ia32": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz",
- "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-x64": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz",
- "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@modelcontextprotocol/sdk": {
- "version": "1.29.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
- "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
- "license": "MIT",
- "dependencies": {
- "@hono/node-server": "^1.19.9",
- "ajv": "^8.17.1",
- "ajv-formats": "^3.0.1",
- "content-type": "^1.0.5",
- "cors": "^2.8.5",
- "cross-spawn": "^7.0.5",
- "eventsource": "^3.0.2",
- "eventsource-parser": "^3.0.0",
- "express": "^5.2.1",
- "express-rate-limit": "^8.2.1",
- "hono": "^4.11.4",
- "jose": "^6.1.3",
- "json-schema-typed": "^8.0.2",
- "pkce-challenge": "^5.0.0",
- "raw-body": "^3.0.0",
- "zod": "^3.25 || ^4.0",
- "zod-to-json-schema": "^3.25.1"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@cfworker/json-schema": "^4.1.1",
- "zod": "^3.25 || ^4.0"
- },
- "peerDependenciesMeta": {
- "@cfworker/json-schema": {
- "optional": true
- },
- "zod": {
- "optional": false
- }
- }
- },
- "node_modules/@opentelemetry/api": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
- "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/@oxc-project/types": {
- "version": "0.144.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz",
- "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==",
- "license": "MIT",
- "peer": true,
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
- "node_modules/@poppinss/colors": {
- "version": "4.1.6",
- "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
- "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "kleur": "^4.1.5"
- }
- },
- "node_modules/@poppinss/dumper": {
- "version": "0.6.5",
- "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",
- "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@poppinss/colors": "^4.1.5",
- "@sindresorhus/is": "^7.0.2",
- "supports-color": "^10.0.0"
- }
- },
- "node_modules/@poppinss/exception": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",
- "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz",
- "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz",
- "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz",
- "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz",
- "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz",
- "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz",
- "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz",
- "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz",
- "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==",
- "cpu": [
- "ppc64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz",
- "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==",
- "cpu": [
- "s390x"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz",
- "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz",
- "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz",
- "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz",
- "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz",
- "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "peer": true,
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/plugin-babel": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/@rolldown/plugin-babel/-/plugin-babel-0.2.3.tgz",
- "integrity": "sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==",
- "license": "MIT",
- "dependencies": {
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=22.12.0 || ^24.0.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.29.0 || ^8.0.0-rc.1",
- "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1",
- "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1",
- "rolldown": "^1.0.0-rc.5",
- "vite": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "@babel/plugin-transform-runtime": {
- "optional": true
- },
- "@babel/runtime": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
- "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@sentry/cloudflare": {
- "version": "10.70.0",
- "resolved": "https://registry.npmjs.org/@sentry/cloudflare/-/cloudflare-10.70.0.tgz",
- "integrity": "sha512-jlr9txw3lQrbc6KQn9FzlpgC1KcB36u7aIGSlUhXAZ2MB93DgwjuTnHF20qQHxmHbZylg7CtHoMq3rGiTXAgpg==",
- "license": "MIT",
- "dependencies": {
- "@opentelemetry/api": "^1.9.1",
- "@sentry/core": "10.70.0",
- "@sentry/server-utils": "10.70.0",
- "magic-string": "~0.30.21"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@cloudflare/workers-types": "^4.x || ^5.x",
- "wrangler": "^4.x"
- },
- "peerDependenciesMeta": {
- "@cloudflare/workers-types": {
- "optional": true
- },
- "wrangler": {
- "optional": true
- }
- }
- },
- "node_modules/@sentry/conventions": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz",
- "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==",
- "license": "MIT",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@sentry/core": {
- "version": "10.70.0",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.70.0.tgz",
- "integrity": "sha512-ozhCTDqg89oB4XmWfAwuHshABpvT7AkRpaPnogopPfMAaI61G1t8EKCJ4W7aum8JSBonlfyjPCyW5oYZFm0KvA==",
- "license": "MIT",
- "dependencies": {
- "@sentry/conventions": "^0.16.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@sentry/server-utils": {
- "version": "10.70.0",
- "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.70.0.tgz",
- "integrity": "sha512-rzegZjMFFgCp3o+N8+XU13rfSvz4B+f8rU0ijBGrQcHdMNyfsFDTu1UTm262JofmrV2u+s+D0u0vFTnqtOGkbA==",
- "license": "MIT",
- "dependencies": {
- "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.3",
- "@apm-js-collab/tracing-hooks": "^0.13.0",
- "@sentry/conventions": "^0.16.0",
- "@sentry/core": "10.70.0",
- "meriyah": "^6.1.4"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@sindresorhus/is": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",
- "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/is?sponsor=1"
- }
- },
- "node_modules/@speed-highlight/core": {
- "version": "1.2.24",
- "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz",
- "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
- "devOptional": true,
- "license": "CC0-1.0"
- },
- "node_modules/@types/estree": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
- "license": "MIT"
- },
- "node_modules/@types/gensync": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz",
- "integrity": "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@types/jsesc": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz",
- "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==",
- "license": "MIT"
- },
- "node_modules/@types/json-schema": {
- "version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "26.2.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
- "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~8.3.0"
- }
- },
- "node_modules/accepts": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
- "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "^3.0.0",
- "negotiator": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/acorn": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
- "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/agents": {
- "version": "0.17.4",
- "resolved": "https://registry.npmjs.org/agents/-/agents-0.17.4.tgz",
- "integrity": "sha512-K6YRbpD3VcwdTOPBlDgI4dILAwkhXo5cdxTlVF0IvUwQEKfMPawmH8E/QMXTN8CPGHqVYgYFACxTyk6nKlK+vg==",
- "license": "MIT",
- "dependencies": {
- "@babel/plugin-proposal-decorators": "^8.0.2",
- "@cfworker/json-schema": "^4.1.1",
- "@cloudflare/codemode": "^0.4.3",
- "@modelcontextprotocol/sdk": "1.29.0",
- "@rolldown/plugin-babel": "^0.2.3",
- "cron-schedule": "^6.0.0",
- "esbuild": "^0.28.1",
- "mimetext": "^3.0.28",
- "nanoid": "^5.1.16",
- "partyserver": "^0.5.8",
- "partysocket": "1.3.0",
- "yaml": "^2.9.0",
- "yargs": "^18.0.0"
- },
- "bin": {
- "agents": "dist/cli/index.js"
- },
- "peerDependencies": {
- "@ai-sdk/react": "^3.0.204",
- "@tanstack/ai": ">=0.10.2 <1.0.0",
- "@x402/core": "^2.0.0",
- "@x402/evm": "^2.0.0",
- "ai": "^6.0.0",
- "chat": "^4.29.0",
- "just-bash": "^3.0.0",
- "react": "^19.0.0",
- "vite": ">=6.0.0 <9.0.0",
- "zod": "^4.0.0"
- },
- "peerDependenciesMeta": {
- "@ai-sdk/react": {
- "optional": true
- },
- "@tanstack/ai": {
- "optional": true
- },
- "@x402/core": {
- "optional": true
- },
- "@x402/evm": {
- "optional": true
- },
- "ai": {
- "optional": true
- },
- "chat": {
- "optional": true
- },
- "just-bash": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/ajv": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
- "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ajv-formats": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
- "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
- "license": "MIT",
- "dependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "ajv": {
- "optional": true
- }
- }
- },
- "node_modules/ansi-regex": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
- "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/astring": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz",
- "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==",
- "license": "MIT",
- "bin": {
- "astring": "bin/astring"
- }
- },
- "node_modules/baseline-browser-mapping": {
- "version": "2.11.13",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz",
- "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==",
- "license": "Apache-2.0",
- "peer": true,
- "bin": {
- "baseline-browser-mapping": "dist/cli.cjs"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/blake3-wasm": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
- "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/body-parser": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
- "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
- "license": "MIT",
- "dependencies": {
- "bytes": "^3.1.2",
- "content-type": "^2.0.0",
- "debug": "^4.4.3",
- "http-errors": "^2.0.1",
- "iconv-lite": "^0.7.2",
- "on-finished": "^2.4.1",
- "qs": "^6.15.2",
- "raw-body": "^3.0.2",
- "type-is": "^2.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/body-parser/node_modules/content-type": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
- "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/browserslist": {
- "version": "4.28.8",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
- "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "baseline-browser-mapping": "^2.11.12",
- "caniuse-lite": "^1.0.30001809",
- "electron-to-chromium": "^1.5.402",
- "node-releases": "^2.0.53",
- "update-browserslist-db": "^1.3.0"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/bytes": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
- "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/call-bind-apply-helpers": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
- "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/call-bound": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
- "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "get-intrinsic": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001809",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz",
- "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0",
- "peer": true
- },
- "node_modules/cliui": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
- "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
- "license": "ISC",
- "dependencies": {
- "string-width": "^7.2.0",
- "strip-ansi": "^7.1.0",
- "wrap-ansi": "^9.0.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/cliui/node_modules/string-width": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
- "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^10.3.0",
- "get-east-asian-width": "^1.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/content-disposition": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
- "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/content-type": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
- "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/cookie": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
- "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
- "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
- "license": "MIT",
- "engines": {
- "node": ">=6.6.0"
- }
- },
- "node_modules/core-js-pure": {
- "version": "3.50.0",
- "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.50.0.tgz",
- "integrity": "sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==",
- "hasInstallScript": true,
- "license": "MIT",
- "engines": {
- "node": "*"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/core-js"
- }
- },
- "node_modules/cors": {
- "version": "2.8.6",
- "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
- "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
- "license": "MIT",
- "dependencies": {
- "object-assign": "^4",
- "vary": "^1"
- },
- "engines": {
- "node": ">= 0.10"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/cron-schedule": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/cron-schedule/-/cron-schedule-6.0.0.tgz",
- "integrity": "sha512-BoZaseYGXOo5j5HUwTaegIog3JJbuH4BbrY9A1ArLjXpy+RWb3mV28F/9Gv1dDA7E2L8kngWva4NWisnLTyfgQ==",
- "license": "MIT",
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/depd": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
- "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "devOptional": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/drizzle-kit": {
- "version": "0.31.10",
- "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz",
- "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@drizzle-team/brocli": "^0.10.2",
- "@esbuild-kit/esm-loader": "^2.5.5",
- "esbuild": "^0.25.4",
- "tsx": "^4.21.0"
- },
- "bin": {
- "drizzle-kit": "bin.cjs"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/drizzle-kit/node_modules/esbuild": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.12",
- "@esbuild/android-arm": "0.25.12",
- "@esbuild/android-arm64": "0.25.12",
- "@esbuild/android-x64": "0.25.12",
- "@esbuild/darwin-arm64": "0.25.12",
- "@esbuild/darwin-x64": "0.25.12",
- "@esbuild/freebsd-arm64": "0.25.12",
- "@esbuild/freebsd-x64": "0.25.12",
- "@esbuild/linux-arm": "0.25.12",
- "@esbuild/linux-arm64": "0.25.12",
- "@esbuild/linux-ia32": "0.25.12",
- "@esbuild/linux-loong64": "0.25.12",
- "@esbuild/linux-mips64el": "0.25.12",
- "@esbuild/linux-ppc64": "0.25.12",
- "@esbuild/linux-riscv64": "0.25.12",
- "@esbuild/linux-s390x": "0.25.12",
- "@esbuild/linux-x64": "0.25.12",
- "@esbuild/netbsd-arm64": "0.25.12",
- "@esbuild/netbsd-x64": "0.25.12",
- "@esbuild/openbsd-arm64": "0.25.12",
- "@esbuild/openbsd-x64": "0.25.12",
- "@esbuild/openharmony-arm64": "0.25.12",
- "@esbuild/sunos-x64": "0.25.12",
- "@esbuild/win32-arm64": "0.25.12",
- "@esbuild/win32-ia32": "0.25.12",
- "@esbuild/win32-x64": "0.25.12"
- }
- },
- "node_modules/drizzle-orm": {
- "version": "0.45.2",
- "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
- "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
- "license": "Apache-2.0",
- "peerDependencies": {
- "@aws-sdk/client-rds-data": ">=3",
- "@cloudflare/workers-types": ">=4",
- "@electric-sql/pglite": ">=0.2.0",
- "@libsql/client": ">=0.10.0",
- "@libsql/client-wasm": ">=0.10.0",
- "@neondatabase/serverless": ">=0.10.0",
- "@op-engineering/op-sqlite": ">=2",
- "@opentelemetry/api": "^1.4.1",
- "@planetscale/database": ">=1.13",
- "@prisma/client": "*",
- "@tidbcloud/serverless": "*",
- "@types/better-sqlite3": "*",
- "@types/pg": "*",
- "@types/sql.js": "*",
- "@upstash/redis": ">=1.34.7",
- "@vercel/postgres": ">=0.8.0",
- "@xata.io/client": "*",
- "better-sqlite3": ">=7",
- "bun-types": "*",
- "expo-sqlite": ">=14.0.0",
- "gel": ">=2",
- "knex": "*",
- "kysely": "*",
- "mysql2": ">=2",
- "pg": ">=8",
- "postgres": ">=3",
- "sql.js": ">=1",
- "sqlite3": ">=5"
- },
- "peerDependenciesMeta": {
- "@aws-sdk/client-rds-data": {
- "optional": true
- },
- "@cloudflare/workers-types": {
- "optional": true
- },
- "@electric-sql/pglite": {
- "optional": true
- },
- "@libsql/client": {
- "optional": true
- },
- "@libsql/client-wasm": {
- "optional": true
- },
- "@neondatabase/serverless": {
- "optional": true
- },
- "@op-engineering/op-sqlite": {
- "optional": true
- },
- "@opentelemetry/api": {
- "optional": true
- },
- "@planetscale/database": {
- "optional": true
- },
- "@prisma/client": {
- "optional": true
- },
- "@tidbcloud/serverless": {
- "optional": true
- },
- "@types/better-sqlite3": {
- "optional": true
- },
- "@types/pg": {
- "optional": true
- },
- "@types/sql.js": {
- "optional": true
- },
- "@upstash/redis": {
- "optional": true
- },
- "@vercel/postgres": {
- "optional": true
- },
- "@xata.io/client": {
- "optional": true
- },
- "better-sqlite3": {
- "optional": true
- },
- "bun-types": {
- "optional": true
- },
- "expo-sqlite": {
- "optional": true
- },
- "gel": {
- "optional": true
- },
- "knex": {
- "optional": true
- },
- "kysely": {
- "optional": true
- },
- "mysql2": {
- "optional": true
- },
- "pg": {
- "optional": true
- },
- "postgres": {
- "optional": true
- },
- "prisma": {
- "optional": true
- },
- "sql.js": {
- "optional": true
- },
- "sqlite3": {
- "optional": true
- }
- }
- },
- "node_modules/dunder-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
- "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.1",
- "es-errors": "^1.3.0",
- "gopd": "^1.2.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
- "license": "MIT"
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.405",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz",
- "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==",
- "license": "ISC",
- "peer": true
- },
- "node_modules/emoji-regex": {
- "version": "10.6.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
- "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
- "license": "MIT"
- },
- "node_modules/empathic": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz",
- "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/encodeurl": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
- "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/error-stack-parser-es": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
- "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
- "devOptional": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/antfu"
- }
- },
- "node_modules/es-define-property": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
- "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/es-module-lexer": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
- "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
- "license": "MIT"
- },
- "node_modules/es-object-atoms": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
- "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/esbuild": {
- "version": "0.28.2",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
- "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.2",
- "@esbuild/android-arm": "0.28.2",
- "@esbuild/android-arm64": "0.28.2",
- "@esbuild/android-x64": "0.28.2",
- "@esbuild/darwin-arm64": "0.28.2",
- "@esbuild/darwin-x64": "0.28.2",
- "@esbuild/freebsd-arm64": "0.28.2",
- "@esbuild/freebsd-x64": "0.28.2",
- "@esbuild/linux-arm": "0.28.2",
- "@esbuild/linux-arm64": "0.28.2",
- "@esbuild/linux-ia32": "0.28.2",
- "@esbuild/linux-loong64": "0.28.2",
- "@esbuild/linux-mips64el": "0.28.2",
- "@esbuild/linux-ppc64": "0.28.2",
- "@esbuild/linux-riscv64": "0.28.2",
- "@esbuild/linux-s390x": "0.28.2",
- "@esbuild/linux-x64": "0.28.2",
- "@esbuild/netbsd-arm64": "0.28.2",
- "@esbuild/netbsd-x64": "0.28.2",
- "@esbuild/openbsd-arm64": "0.28.2",
- "@esbuild/openbsd-x64": "0.28.2",
- "@esbuild/openharmony-arm64": "0.28.2",
- "@esbuild/sunos-x64": "0.28.2",
- "@esbuild/win32-arm64": "0.28.2",
- "@esbuild/win32-ia32": "0.28.2",
- "@esbuild/win32-x64": "0.28.2"
- }
- },
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "license": "MIT"
- },
- "node_modules/esquery": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
- "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "estraverse": "^5.1.0"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/etag": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
- "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/event-target-polyfill": {
- "version": "0.0.4",
- "resolved": "https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz",
- "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==",
- "license": "MIT"
- },
- "node_modules/eventsource": {
- "version": "3.0.7",
- "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
- "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
- "license": "MIT",
- "dependencies": {
- "eventsource-parser": "^3.0.1"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/eventsource-parser": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz",
- "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/express": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
- "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
- "license": "MIT",
- "dependencies": {
- "accepts": "^2.0.0",
- "body-parser": "^2.2.1",
- "content-disposition": "^1.0.0",
- "content-type": "^1.0.5",
- "cookie": "^0.7.1",
- "cookie-signature": "^1.2.1",
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "finalhandler": "^2.1.0",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.0",
- "merge-descriptors": "^2.0.0",
- "mime-types": "^3.0.0",
- "on-finished": "^2.4.1",
- "once": "^1.4.0",
- "parseurl": "^1.3.3",
- "proxy-addr": "^2.0.7",
- "qs": "^6.14.0",
- "range-parser": "^1.2.1",
- "router": "^2.2.0",
- "send": "^1.1.0",
- "serve-static": "^2.2.0",
- "statuses": "^2.0.1",
- "type-is": "^2.0.1",
- "vary": "^1.1.2"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/express-rate-limit": {
- "version": "8.6.2",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz",
- "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.3",
- "ip-address": "^10.2.0"
- },
- "engines": {
- "node": ">= 16"
- },
- "funding": {
- "url": "https://github.com/sponsors/express-rate-limit"
- },
- "peerDependencies": {
- "express": ">= 4.11"
- }
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "license": "MIT"
- },
- "node_modules/fast-uri": {
- "version": "3.1.5",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
- "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "license": "BSD-3-Clause"
- },
- "node_modules/finalhandler": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
- "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "on-finished": "^2.4.1",
- "parseurl": "^1.3.3",
- "statuses": "^2.0.1"
- },
- "engines": {
- "node": ">= 18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/forwarded": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
- "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/fresh": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
- "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/function-bind": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
- "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "license": "ISC",
- "engines": {
- "node": "6.* || 8.* || >= 10.*"
- }
- },
- "node_modules/get-east-asian-width": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
- "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/get-intrinsic": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
- "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
- "license": "MIT",
- "dependencies": {
- "call-bind-apply-helpers": "^1.0.2",
- "es-define-property": "^1.0.1",
- "es-errors": "^1.3.0",
- "es-object-atoms": "^1.1.1",
- "function-bind": "^1.1.2",
- "get-proto": "^1.0.1",
- "gopd": "^1.2.0",
- "has-symbols": "^1.1.0",
- "hasown": "^2.0.2",
- "math-intrinsics": "^1.1.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/get-proto": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
- "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
- "license": "MIT",
- "dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/get-tsconfig": {
- "version": "4.14.2",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.2.tgz",
- "integrity": "sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "resolve-pkg-maps": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
- }
- },
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
- "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/hono": {
- "version": "4.13.1",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz",
- "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==",
- "license": "MIT",
- "engines": {
- "node": ">=16.9.0"
- }
- },
- "node_modules/http-errors": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
- "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
- "license": "MIT",
- "dependencies": {
- "depd": "~2.0.0",
- "inherits": "~2.0.4",
- "setprototypeof": "~1.2.0",
- "statuses": "~2.0.2",
- "toidentifier": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/iconv-lite": {
- "version": "0.7.3",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
- "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/import-meta-resolve": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
- "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
- "license": "MIT",
- "peer": true,
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/wooorm"
- }
- },
- "node_modules/inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "license": "ISC"
- },
- "node_modules/ip-address": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz",
- "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==",
- "license": "MIT",
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/ipaddr.js": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
- "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/is-promise": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
- "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
- "license": "MIT"
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "license": "ISC"
- },
- "node_modules/jose": {
- "version": "6.2.8",
- "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz",
- "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/panva"
- }
- },
- "node_modules/js-base64": {
- "version": "3.9.2",
- "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.9.2.tgz",
- "integrity": "sha512-6zayE8QlUdiweYI6cETD/XBSqFcoCUlufn/29PJR99r82x1yDnIprRca0YvAYpAW+ez0GuQkVBC6xG5QkD7OjA==",
- "license": "BSD-3-Clause"
- },
- "node_modules/js-tokens": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
- "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
- "license": "MIT"
- },
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "license": "MIT"
- },
- "node_modules/json-schema-typed": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
- "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
- "license": "BSD-2-Clause"
- },
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "license": "MIT",
- "peer": true,
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/kleur": {
- "version": "4.1.5",
- "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
- "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/lru-cache": {
- "version": "11.5.2",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
- "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
- "license": "BlueOak-1.0.0",
- "peer": true,
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/media-typer": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
- "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
- "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/meriyah": {
- "version": "6.1.4",
- "resolved": "https://registry.npmjs.org/meriyah/-/meriyah-6.1.4.tgz",
- "integrity": "sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==",
- "license": "ISC",
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/mime-db": {
- "version": "1.54.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
- "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
- "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "^1.54.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/mimetext": {
- "version": "3.0.28",
- "resolved": "https://registry.npmjs.org/mimetext/-/mimetext-3.0.28.tgz",
- "integrity": "sha512-eQXpbNrtxLCjUtiVbR/qR09dbPgZ2o+KR1uA7QKqGhbn8QV7HIL16mXXsobBL4/8TqoYh1us31kfz+dNfCev9g==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.26.0",
- "@babel/runtime-corejs3": "^7.26.0",
- "js-base64": "^3.7.7",
- "mime-types": "^2.1.35"
- },
- "funding": {
- "type": "patreon",
- "url": "https://patreon.com/muratgozel"
- }
- },
- "node_modules/mimetext/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mimetext/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/miniflare": {
- "version": "5.20260811.0-alpha",
- "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260811.0-alpha.tgz",
- "integrity": "sha512-sypXsD5fjY88fZNedPqnwrwR1dwfnfbfW7MfvMyIfPJdtRiCCOpUnjWGeFVYYZ+0fQVICye6Juu+vZgzTEx8XA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@cspotcode/source-map-support": "0.8.1",
- "sharp": "0.35.2",
- "undici": "7.29.0",
- "workerd": "1.20260811.1",
- "ws": "8.21.0",
- "youch": "4.1.0-beta.10"
- },
- "engines": {
- "node": ">=22.0.0"
- }
- },
- "node_modules/module-details-from-path": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
- "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
- "license": "MIT"
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "5.1.16",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
- "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.js"
- },
- "engines": {
- "node": "^18 || >=20"
- }
- },
- "node_modules/negotiator": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
- "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/node-releases": {
- "version": "2.0.53",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
- "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/object-assign": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/object-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/obug": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
- "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
- "funding": [
- "https://github.com/sponsors/sxzz",
- "https://opencollective.com/debug"
- ],
- "license": "MIT",
- "engines": {
- "node": ">=12.20.0"
- }
- },
- "node_modules/on-finished": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
- "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.1.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
- "node_modules/parseurl": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
- "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/partyserver": {
- "version": "0.5.10",
- "resolved": "https://registry.npmjs.org/partyserver/-/partyserver-0.5.10.tgz",
- "integrity": "sha512-t2B3mhTL1IOxCsj8rZgn4TxTuub7oLhypjc1/fGPNsbgnn9OsHms6uR3QEd5bFJ/7xrFo771j3DdUlll8cxgBg==",
- "license": "ISC",
- "dependencies": {
- "nanoid": "^5.1.9"
- },
- "peerDependencies": {
- "@cloudflare/workers-types": "^4.20260424.1 || ^5.20260703.1"
- }
- },
- "node_modules/partysocket": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.3.0.tgz",
- "integrity": "sha512-1zToNyolZFK/7nuAw/K2bZrNzFqaZyRoCEkS+9vG6WSC5ikrN6qWRe96q6ImU51uptz2r+dAwSkwhJVdQi4LiA==",
- "license": "MIT",
- "dependencies": {
- "event-target-polyfill": "^0.0.4"
- },
- "peerDependencies": {
- "react": ">=17"
- },
- "peerDependenciesMeta": {
- "react": {
- "optional": true
- }
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-to-regexp": {
- "version": "8.4.2",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
- "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "license": "ISC",
- "peer": true
- },
- "node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/pkce-challenge": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
- "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
- "license": "MIT",
- "engines": {
- "node": ">=16.20.0"
- }
- },
- "node_modules/proxy-addr": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
- "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
- "license": "MIT",
- "dependencies": {
- "forwarded": "0.2.0",
- "ipaddr.js": "1.9.1"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/qs": {
- "version": "6.15.3",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
- "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "es-define-property": "^1.0.1",
- "side-channel": "^1.1.1"
- },
- "engines": {
- "node": ">=0.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/range-parser": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
- "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/raw-body": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
- "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.7.0",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.10"
- }
- },
- "node_modules/react": {
- "version": "19.2.8",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
- "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/require-from-string": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/resolve-pkg-maps": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
- "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
- }
- },
- "node_modules/rolldown": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz",
- "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@oxc-project/types": "=0.144.0",
- "@rolldown/pluginutils": "^1.0.0"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.2.4",
- "@rolldown/binding-darwin-arm64": "1.2.4",
- "@rolldown/binding-darwin-x64": "1.2.4",
- "@rolldown/binding-freebsd-x64": "1.2.4",
- "@rolldown/binding-linux-arm-gnueabihf": "1.2.4",
- "@rolldown/binding-linux-arm64-gnu": "1.2.4",
- "@rolldown/binding-linux-arm64-musl": "1.2.4",
- "@rolldown/binding-linux-ppc64-gnu": "1.2.4",
- "@rolldown/binding-linux-s390x-gnu": "1.2.4",
- "@rolldown/binding-linux-x64-gnu": "1.2.4",
- "@rolldown/binding-linux-x64-musl": "1.2.4",
- "@rolldown/binding-openharmony-arm64": "1.2.4",
- "@rolldown/binding-win32-arm64-msvc": "1.2.4",
- "@rolldown/binding-win32-x64-msvc": "1.2.4"
- }
- },
- "node_modules/router": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
- "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "is-promise": "^4.0.0",
- "parseurl": "^1.3.3",
- "path-to-regexp": "^8.0.0"
- },
- "engines": {
- "node": ">= 18"
- }
- },
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "license": "MIT"
- },
- "node_modules/semifies": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/semifies/-/semifies-1.0.0.tgz",
- "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==",
- "license": "Apache-2.0"
- },
- "node_modules/semver": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/send": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
- "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.3",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.1",
- "mime-types": "^3.0.2",
- "ms": "^2.1.3",
- "on-finished": "^2.4.1",
- "range-parser": "^1.2.1",
- "statuses": "^2.0.2"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/serve-static": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
- "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
- "license": "MIT",
- "dependencies": {
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "parseurl": "^1.3.3",
- "send": "^1.2.0"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/setprototypeof": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
- "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
- "license": "ISC"
- },
- "node_modules/sharp": {
- "version": "0.35.2",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz",
- "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==",
- "devOptional": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@img/colour": "^1.1.0",
- "detect-libc": "^2.1.2",
- "semver": "^7.8.4"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-darwin-arm64": "0.35.2",
- "@img/sharp-darwin-x64": "0.35.2",
- "@img/sharp-freebsd-wasm32": "0.35.2",
- "@img/sharp-libvips-darwin-arm64": "1.3.1",
- "@img/sharp-libvips-darwin-x64": "1.3.1",
- "@img/sharp-libvips-linux-arm": "1.3.1",
- "@img/sharp-libvips-linux-arm64": "1.3.1",
- "@img/sharp-libvips-linux-ppc64": "1.3.1",
- "@img/sharp-libvips-linux-riscv64": "1.3.1",
- "@img/sharp-libvips-linux-s390x": "1.3.1",
- "@img/sharp-libvips-linux-x64": "1.3.1",
- "@img/sharp-libvips-linuxmusl-arm64": "1.3.1",
- "@img/sharp-libvips-linuxmusl-x64": "1.3.1",
- "@img/sharp-linux-arm": "0.35.2",
- "@img/sharp-linux-arm64": "0.35.2",
- "@img/sharp-linux-ppc64": "0.35.2",
- "@img/sharp-linux-riscv64": "0.35.2",
- "@img/sharp-linux-s390x": "0.35.2",
- "@img/sharp-linux-x64": "0.35.2",
- "@img/sharp-linuxmusl-arm64": "0.35.2",
- "@img/sharp-linuxmusl-x64": "0.35.2",
- "@img/sharp-webcontainers-wasm32": "0.35.2",
- "@img/sharp-win32-arm64": "0.35.2",
- "@img/sharp-win32-ia32": "0.35.2",
- "@img/sharp-win32-x64": "0.35.2"
- }
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/side-channel": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
- "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.4",
- "side-channel-list": "^1.0.1",
- "side-channel-map": "^1.0.1",
- "side-channel-weakmap": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-list": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
- "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
- "license": "MIT",
- "dependencies": {
- "es-errors": "^1.3.0",
- "object-inspect": "^1.13.4"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-map": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
- "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/side-channel-weakmap": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
- "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "license": "MIT",
- "dependencies": {
- "call-bound": "^1.0.2",
- "es-errors": "^1.3.0",
- "get-intrinsic": "^1.2.5",
- "object-inspect": "^1.13.3",
- "side-channel-map": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
- "node_modules/statuses": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
- "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/string-width": {
- "version": "8.2.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
- "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
- "license": "MIT",
- "dependencies": {
- "get-east-asian-width": "^1.5.0",
- "strip-ansi": "^7.1.2"
- },
- "engines": {
- "node": ">=20"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/strip-ansi": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
- "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.2.2"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/supports-color": {
- "version": "10.2.2",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
- "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
- "node_modules/toidentifier": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
- "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.6"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "dev": true,
- "license": "0BSD",
- "optional": true
- },
- "node_modules/tsx": {
- "version": "4.23.12",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz",
- "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "esbuild": "~0.28.0"
- },
- "bin": {
- "tsx": "dist/cli.mjs"
- },
- "engines": {
- "node": ">=18.0.0"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- }
- },
- "node_modules/type-is": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
- "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
- "license": "MIT",
- "dependencies": {
- "content-type": "^2.0.0",
- "media-typer": "^1.1.0",
- "mime-types": "^3.0.0"
- },
- "engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/type-is/node_modules/content-type": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
- "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/undici": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
- "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=20.18.1"
- }
- },
- "node_modules/undici-types": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
- "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/unenv": {
- "version": "2.0.0-rc.24",
- "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
- "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "pathe": "^2.0.3"
- }
- },
- "node_modules/unpipe": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
- "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/update-browserslist-db": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz",
- "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.1"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/vary": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
- "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/workerd": {
- "version": "1.20260811.1",
- "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260811.1.tgz",
- "integrity": "sha512-kh+FFm55JQ4ssxhHZV9VPdMQq3D1nHxNJgwxMtWGD4dGppJvLySdguTRDKgeNTvgq6heSz+6TTXyPSDGj8Yllw==",
- "devOptional": true,
- "hasInstallScript": true,
- "license": "Apache-2.0",
- "bin": {
- "workerd": "bin/workerd"
- },
- "engines": {
- "node": ">=16"
- },
- "optionalDependencies": {
- "@cloudflare/workerd-darwin-64": "1.20260811.1",
- "@cloudflare/workerd-darwin-arm64": "1.20260811.1",
- "@cloudflare/workerd-linux-64": "1.20260811.1",
- "@cloudflare/workerd-linux-arm64": "1.20260811.1",
- "@cloudflare/workerd-windows-64": "1.20260811.1"
- }
- },
- "node_modules/wrangler": {
- "version": "4.122.0",
- "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.122.0.tgz",
- "integrity": "sha512-qkskzgQ76Y1qvVe5JARgvc3RISq6BC2rPoxQhFoKH1dKIwQc3GDFttQ/7m2OfeQ+tmQRzynv2dy/DXnxFCj2Lw==",
- "devOptional": true,
- "license": "MIT OR Apache-2.0",
- "dependencies": {
- "@cloudflare/kv-asset-handler": "0.5.0",
- "@cloudflare/unenv-preset": "2.16.1",
- "blake3-wasm": "2.1.5",
- "esbuild": "0.28.1",
- "miniflare": "5.20260811.0-alpha",
- "path-to-regexp": "6.3.0",
- "unenv": "2.0.0-rc.24",
- "workerd": "1.20260811.1"
- },
- "bin": {
- "cf-wrangler": "bin/cf-wrangler.js",
- "wrangler": "bin/wrangler.js",
- "wrangler2": "bin/wrangler.js"
- },
- "engines": {
- "node": ">=22.0.0"
- },
- "optionalDependencies": {
- "fsevents": "2.3.3"
- },
- "peerDependencies": {
- "@cloudflare/workers-types": "^5.20260811.1"
- },
- "peerDependenciesMeta": {
- "@cloudflare/workers-types": {
- "optional": true
- }
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
- "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/android-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
- "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/android-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
- "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/android-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
- "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
- "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/darwin-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
- "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
- "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
- "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/linux-arm": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
- "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/linux-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
- "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/linux-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
- "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/linux-loong64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
- "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
- "cpu": [
- "loong64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
- "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
- "cpu": [
- "mips64el"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
- "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
- "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/linux-s390x": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
- "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/linux-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
- "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
- "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
- "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
- "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
- "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/sunos-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
- "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/win32-arm64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
- "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/win32-ia32": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
- "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/@esbuild/win32-x64": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
- "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/wrangler/node_modules/esbuild": {
- "version": "0.28.1",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
- "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
- "devOptional": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.28.1",
- "@esbuild/android-arm": "0.28.1",
- "@esbuild/android-arm64": "0.28.1",
- "@esbuild/android-x64": "0.28.1",
- "@esbuild/darwin-arm64": "0.28.1",
- "@esbuild/darwin-x64": "0.28.1",
- "@esbuild/freebsd-arm64": "0.28.1",
- "@esbuild/freebsd-x64": "0.28.1",
- "@esbuild/linux-arm": "0.28.1",
- "@esbuild/linux-arm64": "0.28.1",
- "@esbuild/linux-ia32": "0.28.1",
- "@esbuild/linux-loong64": "0.28.1",
- "@esbuild/linux-mips64el": "0.28.1",
- "@esbuild/linux-ppc64": "0.28.1",
- "@esbuild/linux-riscv64": "0.28.1",
- "@esbuild/linux-s390x": "0.28.1",
- "@esbuild/linux-x64": "0.28.1",
- "@esbuild/netbsd-arm64": "0.28.1",
- "@esbuild/netbsd-x64": "0.28.1",
- "@esbuild/openbsd-arm64": "0.28.1",
- "@esbuild/openbsd-x64": "0.28.1",
- "@esbuild/openharmony-arm64": "0.28.1",
- "@esbuild/sunos-x64": "0.28.1",
- "@esbuild/win32-arm64": "0.28.1",
- "@esbuild/win32-ia32": "0.28.1",
- "@esbuild/win32-x64": "0.28.1"
- }
- },
- "node_modules/wrangler/node_modules/path-to-regexp": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
- "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
- "devOptional": true,
- "license": "MIT"
- },
- "node_modules/wrap-ansi": {
- "version": "9.0.2",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
- "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.2.1",
- "string-width": "^7.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi/node_modules/string-width": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
- "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^10.3.0",
- "get-east-asian-width": "^1.0.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC"
- },
- "node_modules/ws": {
- "version": "8.21.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
- "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "node_modules/y18n": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
- "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/yaml": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
- "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
- "license": "ISC",
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/eemeli"
- }
- },
- "node_modules/yargs": {
- "version": "18.1.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz",
- "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==",
- "license": "MIT",
- "dependencies": {
- "cliui": "^9.0.1",
- "escalade": "^3.1.1",
- "get-caller-file": "^2.0.5",
- "string-width": "^8.2.1",
- "y18n": "^5.0.5",
- "yargs-parser": "^22.0.0"
- },
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=23"
- }
- },
- "node_modules/yargs-parser": {
- "version": "22.0.0",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
- "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
- "license": "ISC",
- "engines": {
- "node": "^20.19.0 || ^22.12.0 || >=23"
- }
- },
- "node_modules/youch": {
- "version": "4.1.0-beta.10",
- "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
- "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@poppinss/colors": "^4.1.5",
- "@poppinss/dumper": "^0.6.4",
- "@speed-highlight/core": "^1.2.7",
- "cookie": "^1.0.2",
- "youch-core": "^0.3.3"
- }
- },
- "node_modules/youch-core": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",
- "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",
- "devOptional": true,
- "license": "MIT",
- "dependencies": {
- "@poppinss/exception": "^1.2.2",
- "error-stack-parser-es": "^1.0.5"
- }
- },
- "node_modules/youch/node_modules/cookie": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
- "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
- "devOptional": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/zod": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
- }
- },
- "node_modules/zod-to-json-schema": {
- "version": "3.25.2",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
- "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
- "license": "ISC",
- "peerDependencies": {
- "zod": "^3.25.28 || ^4"
- }
- }
- }
-}
diff --git a/agents/hosting/package.json b/agents/hosting/package.json
deleted file mode 100644
index 91fdab0e..00000000
--- a/agents/hosting/package.json
+++ /dev/null
@@ -1,73 +0,0 @@
-{
- "name": "@cloudflarebase/hosting",
- "version": "0.7.0",
- "description": "Apps and functions on Cloudflare Workers for Platforms - static assets and server code in one deploy, one Durable Object orchestrator per project - the hosting primitive behind Cloudflarebase.",
- "license": "Apache-2.0",
- "type": "module",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/cloudflarebase/cloudflarebase.git",
- "directory": "agents/hosting"
- },
- "homepage": "https://github.com/cloudflarebase/cloudflarebase/tree/main/agents/hosting#readme",
- "bugs": "https://github.com/cloudflarebase/cloudflarebase/issues",
- "keywords": [
- "cloudflare",
- "workers",
- "workers-for-platforms",
- "durable-objects",
- "hosting",
- "static-assets",
- "agents",
- "cloudflarebase"
- ],
- "publishConfig": {
- "access": "public"
- },
- "main": "./dist/index.js",
- "types": "./dist/index.d.ts",
- "exports": {
- ".": {
- "types": "./dist/index.d.ts",
- "default": "./dist/index.js"
- },
- "./wrangler-fragment.jsonc": "./template/wrangler-fragment.jsonc",
- "./cloudflarebase.agent.json": "./cloudflarebase.agent.json",
- "./package.json": "./package.json"
- },
- "files": [
- "dist",
- "template",
- "NOTICE",
- "cloudflarebase.agent.json"
- ],
- "scripts": {
- "build": "tsc -p tsconfig.build.json",
- "typecheck": "tsc --noEmit",
- "test:unit": "node --import tsx --test src/route-access.unit.test.ts src/tar.unit.test.ts src/cloudflare.unit.test.ts src/crypto.unit.test.ts src/build-env.unit.test.ts",
- "prepublishOnly": "npm run build",
- "deploy": "wrangler deploy",
- "deploy:outbound": "wrangler deploy --config wrangler.outbound.jsonc",
- "deploy:outbound:preview": "wrangler deploy --config wrangler.outbound.jsonc --env preview",
- "deploy:preview": "npm run deploy:outbound:preview && wrangler deploy --env preview",
- "dev": "wrangler dev --env local --persist-to=../../.wrangler/state/ --ip 0.0.0.0 --port 8790",
- "start": "wrangler dev",
- "cf-typegen": "wrangler types",
- "dev:test": "wrangler dev --env test --ip 127.0.0.1 --port 8800 --persist-to ../../.wrangler/test-state/hosting-agent",
- "deploy:production": "npm run deploy:outbound && wrangler deploy --env production",
- "migrations": "drizzle-kit generate && node scripts/generate-migrations.mjs"
- },
- "devDependencies": {
- "@types/node": "^26.1.1",
- "drizzle-kit": "^0.31.10",
- "tsx": "^4.20.0",
- "typescript": "^5.5.2",
- "wrangler": "^4.118.0"
- },
- "dependencies": {
- "@sentry/cloudflare": "^10.66.0",
- "agents": "^0.17.4",
- "drizzle-orm": "^0.45.2",
- "zod": "^4.4.3"
- }
-}
diff --git a/agents/hosting/scripts/generate-migrations.mjs b/agents/hosting/scripts/generate-migrations.mjs
deleted file mode 100644
index 93ae36be..00000000
--- a/agents/hosting/scripts/generate-migrations.mjs
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Inlines the generated Drizzle migrations into a plain TypeScript module.
- *
- * drizzle-kit emits `drizzle/migrations.js`, which imports each `.sql` file as
- * a module. That only works because Wrangler is configured to bundle `.sql` as
- * a Text module - a `rules` entry that every consumer of this agent would have
- * to replicate in their own wrangler config for the package to build at all.
- *
- * Emitting the SQL as ordinary string literals removes that requirement: the
- * agent becomes an npm dependency you can import with no bundler
- * configuration, and this repository loses a gotcha it had to document.
- *
- * Run after `drizzle-kit generate`; `npm run migrations` does both.
- */
-import fs from 'node:fs';
-import path from 'node:path';
-import { fileURLToPath } from 'node:url';
-
-const here = path.dirname(fileURLToPath(import.meta.url));
-const drizzleDir = path.join(here, '..', 'drizzle');
-const outFile = path.join(here, '..', 'src', 'migrations.ts');
-
-const journalPath = path.join(drizzleDir, 'meta', '_journal.json');
-if (!fs.existsSync(journalPath)) {
- console.error(`No journal at ${journalPath} - run \`npx drizzle-kit generate\` first.`);
- process.exit(1);
-}
-
-const journal = JSON.parse(fs.readFileSync(journalPath, 'utf8'));
-const entries = journal.entries ?? [];
-
-/** Backtick-safe: migrations are SQL, but never assume about generated text. */
-function toTemplateLiteral(sql) {
- return '`' + sql.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${') + '`';
-}
-
-const parts = [];
-for (const entry of entries) {
- const file = path.join(drizzleDir, `${entry.tag}.sql`);
- if (!fs.existsSync(file)) {
- console.error(`Journal lists ${entry.tag} but ${file} is missing.`);
- process.exit(1);
- }
- const key = `m${String(entry.idx).padStart(4, '0')}`;
- parts.push({ key, sql: fs.readFileSync(file, 'utf8') });
-}
-
-const banner = `// Generated by scripts/generate-migrations.mjs - do not edit.
-//
-// The SQL is inlined rather than imported from .sql files so this agent can be
-// consumed as a plain npm dependency, with no Wrangler Text-module rule needed
-// in the consumer's config. Regenerate with \`npm run migrations\`.
-`;
-
-// No `as const`: drizzle's MigrationConfig takes a mutable entries array, and a
-// readonly tuple is not assignable to it.
-const body = `
-const journal = ${JSON.stringify(journal, null, '\t')};
-
-${parts.map((p) => `const ${p.key} = ${toTemplateLiteral(p.sql)};`).join('\n\n')}
-
-export default {
- journal,
- migrations: {
-${parts.map((p) => `\t\t${p.key}`).join(',\n')}
- }
-};
-`;
-
-fs.writeFileSync(outFile, `${banner}${body}`, 'utf8');
-console.log(
- `Inlined ${parts.length} migration${parts.length === 1 ? '' : 's'} into src/migrations.ts`,
-);
diff --git a/agents/hosting/src/agent.ts b/agents/hosting/src/agent.ts
deleted file mode 100644
index bc2649c3..00000000
--- a/agents/hosting/src/agent.ts
+++ /dev/null
@@ -1,1433 +0,0 @@
-import * as Sentry from '@sentry/cloudflare';
-import { Agent, type AgentContext } from 'agents';
-import { and, count, desc, eq, gt, lt, or, sql } from 'drizzle-orm';
-import { drizzle, type DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite';
-import { migrate } from 'drizzle-orm/durable-sqlite/migrator';
-import {
- deleteScript,
- deleteScriptSecret,
- deployVars,
- deleteScriptsByTag,
- patchScriptSecret,
- patchScriptVars,
- putScript,
- uploadAssets,
- type AssetFile,
- type CfApi,
- type ModuleFile,
-} from './cloudflare';
-import * as schema from './db/schema';
-import {
- apps,
- appSecrets,
- appVars,
- buildSecrets,
- buildVars,
- deploys,
- type AppRecord,
- type DeployRecord,
-} from './db/schema';
-import { mergeBuildEnv } from './build-env';
-import { decryptValue, encryptValue, importMasterKey } from './crypto';
-import migrations from './migrations';
-import {
- DEMO_PROJECT_PATTERN,
- analyticsApiResponseSchema,
- appNameSchema,
- buildSecretBodySchema,
- deployCursorSchema,
- deployMetaSchema,
- projectIdSchema,
- secretBodySchema,
- storedVarsSchema,
- subdomainSchema,
- varNameSchema,
- varsBodySchema,
- type DeployMeta,
-} from './schemas';
-import { gunzip, parseTar, toAssetPaths } from './tar';
-
-/**
- * HostingAgent - one Durable Object per project: the app registry for that
- * project, deploy history, and the orchestration of the Cloudflare API.
- * Control plane, not data plane: code and assets live in the dispatch
- * namespace, and the SERVE path (src/index.ts) never touches this object.
- *
- * Subdomains are pushed by the console after it resolves the claim in the
- * control-plane `app` table - the agent never derives or accepts one from a
- * request, which is what makes the deploy route safe to expose on the
- * operator surface: it can only ever deploy to subdomains the console
- * recorded for THIS project.
- */
-
-const MAX_APPS = 10;
-const MAX_DEPLOYS_PER_DAY = 50;
-const MAX_VARS_PER_APP = 64;
-const MAX_SECRETS_PER_APP = 64;
-const MAX_BUILD_SECRETS_PER_APP = 32;
-const ANALYTICS_CACHE_MS = 5_000;
-// Sized for framework output, not just hand-rolled Workers: an OpenNext
-// worker bundle routinely passes 10 MB uncompressed, and a Next site blows
-// 1000 files on `_next/static` alone. The DO parses deploys in memory, so
-// the asset total stays well under the 128 MB isolate limit; Cloudflare's
-// own 10 MB-compressed script ceiling still applies at upload.
-const MAX_MODULE_BYTES = 20 * 1024 * 1024;
-const MAX_ASSET_COUNT = 5000;
-const MAX_ASSET_TOTAL_BYTES = 40 * 1024 * 1024;
-const MAX_ASSET_FILE_BYTES = 25 * 1024 * 1024;
-const RECENT_DEPLOYS = 10;
-/** Decompressed ceiling for a direct deploy's tarball. Above the 40 MB asset
- * cap because the archive also carries source we filter out - the deploy
- * itself is still bounded by the asset caps in publish(). */
-const MAX_ARCHIVE_BYTES = 96 * 1024 * 1024;
-const MAX_ARCHIVE_FILES = 20_000;
-
-/** Cloudflare's Pages/Workers convention files: deploy CONFIGURATION, never
- * content. `_worker.js` doubly so - for any framework whose assets directory
- * is also its build output (SvelteKit, Astro SSR), it is the customer's
- * server bundle, and publishing it would hand out their server source. The
- * CLI filters these too (via .assetsignore); this is the backstop for older
- * CLIs and direct tarball deploys. */
-const RESERVED_ROOT_ASSETS = new Set(['/_worker.js', '/_routes.json', '/_headers', '/_redirects']);
-
-export interface HostingAppSummary {
- name: string;
- subdomain: string;
- url: string | null;
- deployCount: number;
- lastDeployAt: string | null;
- createdAt: string;
-}
-
-export interface HostingDeploySummary {
- id: string;
- appName: string;
- subdomain: string;
- url: string | null;
- status: 'live' | 'stub';
- hasWorker: boolean;
- assetCount: number;
- assetBytes: number;
- moduleBytes: number;
- createdAt: string;
-}
-
-export interface HostingAgentState {
- projectId: string;
- provisionedAt: string | null;
- apps: HostingAppSummary[];
- recentDeploys: HostingDeploySummary[];
- totalDeploys: number;
- /** Bumped on any reported change; dashboards refetch when it moves. */
- rev: number;
-}
-
-const EXTENSION_TYPES: Record = {
- html: 'text/html',
- css: 'text/css',
- js: 'text/javascript',
- mjs: 'text/javascript',
- json: 'application/json',
- svg: 'image/svg+xml',
- png: 'image/png',
- jpg: 'image/jpeg',
- jpeg: 'image/jpeg',
- gif: 'image/gif',
- webp: 'image/webp',
- avif: 'image/avif',
- ico: 'image/x-icon',
- txt: 'text/plain',
- xml: 'application/xml',
- pdf: 'application/pdf',
- wasm: 'application/wasm',
- woff: 'font/woff',
- woff2: 'font/woff2',
- map: 'application/json',
- webmanifest: 'application/manifest+json',
-};
-
-function contentTypeFor(path: string, provided: string): string {
- if (provided && provided !== 'application/octet-stream') return provided;
- const extension = path.slice(path.lastIndexOf('.') + 1).toLowerCase();
- return EXTENSION_TYPES[extension] ?? 'application/octet-stream';
-}
-
-/** Answering a body-bearing request without consuming the body wedges the
- * worker; every route that skips the body drains it before answering. */
-async function drainUnusedBody(request: Request): Promise {
- try {
- if (request.body && !request.bodyUsed) await request.body.cancel();
- } catch {
- // draining is belt-and-braces, never a failure
- }
-}
-
-export class HostingAgent extends Agent {
- initialState: HostingAgentState = {
- projectId: '',
- provisionedAt: null,
- apps: [],
- recentDeploys: [],
- totalDeploys: 0,
- rev: 0,
- };
-
- db: DrizzleSqliteDODatabase;
-
- constructor(ctx: AgentContext, env: Env) {
- super(ctx, env);
- this.db = drizzle(ctx.storage, { schema });
- }
-
- async onStart(): Promise {
- // Idempotent - drizzle tracks applied migrations in its own table.
- await migrate(this.db, migrations);
- if (!this.state.projectId) {
- this.setState({
- ...this.state,
- projectId: this.name,
- provisionedAt: new Date().toISOString(),
- });
- }
- }
-
- private get stub(): boolean {
- return this.env.HOSTING_STUB === 'true';
- }
-
- /** The REST driver's config, or null while any piece is missing. */
- private cfApi(): CfApi | null {
- const { CF_ACCOUNT_ID, CF_HOSTING_API_TOKEN, DISPATCH_NAMESPACE } = this.env;
- if (!CF_ACCOUNT_ID || !CF_HOSTING_API_TOKEN || !DISPATCH_NAMESPACE) return null;
- return {
- accountId: CF_ACCOUNT_ID,
- apiToken: CF_HOSTING_API_TOKEN,
- namespace: DISPATCH_NAMESPACE,
- };
- }
-
- private appUrl(subdomain: string): string | null {
- return this.env.HOSTING_DOMAIN ? `https://${subdomain}.${this.env.HOSTING_DOMAIN}` : null;
- }
-
- private toAppSummary(row: AppRecord): HostingAppSummary {
- return {
- name: row.name,
- subdomain: row.subdomain,
- url: this.appUrl(row.subdomain),
- deployCount: row.deployCount,
- lastDeployAt: row.lastDeployAt?.toISOString() ?? null,
- createdAt: row.createdAt.toISOString(),
- };
- }
-
- private toDeploySummary(row: DeployRecord): HostingDeploySummary {
- return {
- id: row.id,
- appName: row.appName,
- subdomain: row.subdomain,
- url: this.appUrl(row.subdomain),
- status: row.status,
- hasWorker: row.hasWorker,
- assetCount: row.assetCount,
- assetBytes: row.assetBytes,
- moduleBytes: row.moduleBytes,
- createdAt: row.createdAt.toISOString(),
- };
- }
-
- private async syncState(): Promise {
- const appRows = await this.db.select().from(apps).orderBy(apps.createdAt);
- const recent = await this.db
- .select()
- .from(deploys)
- .orderBy(desc(deploys.createdAt), desc(deploys.id))
- .limit(RECENT_DEPLOYS);
- const [total] = await this.db.select({ value: count() }).from(deploys);
- this.setState({
- ...this.state,
- apps: appRows.map((row) => this.toAppSummary(row)),
- recentDeploys: recent.map((row) => this.toDeploySummary(row)),
- totalDeploys: total?.value ?? 0,
- rev: this.state.rev + 1,
- });
- }
-
- /**
- * RPC from the worker's service-binding-only `/internal` route: the console
- * pushes the claim it resolved (control plane owns the global namespace).
- * Idempotent per app; re-pushing the same claim is a no-op.
- */
- async registerApp(appName: string, subdomain: string): Promise<{ ok: true } | { error: string }> {
- if (!appNameSchema.safeParse(appName).success) return { error: 'invalid app name' };
- if (!subdomainSchema.safeParse(subdomain).success) return { error: 'invalid subdomain' };
-
- const [existing] = await this.db.select().from(apps).where(eq(apps.name, appName)).limit(1);
- if (existing) {
- if (existing.subdomain !== subdomain) {
- // The claim table is authoritative; a changed claim (release +
- // re-claim) updates the record.
- await this.db.update(apps).set({ subdomain }).where(eq(apps.name, appName));
- await this.syncState();
- }
- return { ok: true };
- }
- const [{ value: appCount }] = await this.db.select({ value: count() }).from(apps);
- if (appCount >= MAX_APPS) return { error: `projects are limited to ${MAX_APPS} apps` };
- await this.db.insert(apps).values({
- name: appName,
- subdomain,
- createdAt: new Date(),
- deployCount: 0,
- });
- await this.syncState();
- return { ok: true };
- }
-
- /**
- * RPC from the worker's service-binding-only route: deletes ONE app - its
- * namespace script, its deploy history, and its row. The console releases
- * the subdomain claim only after this succeeds, so a failure here never
- * frees a name whose script is still serving. Idempotent: an app the DO
- * does not know (claimed but never deployed - the DO row is only pushed
- * with the first deploy) still answers ok, because there is nothing here
- * to remove.
- */
- async eraseApp(appName: string): Promise<{ ok: true } | { error: string }> {
- if (!appNameSchema.safeParse(appName).success) return { error: 'invalid app name' };
-
- // Environment rows exist without an `apps` row (a claim-only app can be
- // configured before its first deploy), so they are cleared regardless.
- await this.db.delete(appVars).where(eq(appVars.appName, appName));
- await this.db.delete(appSecrets).where(eq(appSecrets.appName, appName));
- await this.db.delete(buildVars).where(eq(buildVars.appName, appName));
- await this.db.delete(buildSecrets).where(eq(buildSecrets.appName, appName));
-
- const [app] = await this.db.select().from(apps).where(eq(apps.name, appName)).limit(1);
- if (!app) return { ok: true };
-
- if (!this.stub) {
- const api = this.cfApi();
- // Configured installs must actually remove the script - answering ok
- // while user code keeps serving would be a lie with a subdomain
- // attached. deleteScript itself tolerates 404 (never deployed).
- if (this.env.DISPATCH && api) {
- await deleteScript(api, app.subdomain);
- }
- }
-
- await this.db.delete(deploys).where(eq(deploys.appName, appName));
- await this.db.delete(apps).where(eq(apps.name, appName));
- await this.syncState();
- return { ok: true };
- }
-
- /** Erase fan-in target. Deletes this project's namespace scripts by tag
- * first - nothing that deletes a project may leave user code running. */
- async destroy(): Promise {
- const api = this.cfApi();
- if (api && !this.stub) {
- // Tag deletion catches every script this project ever uploaded, even
- // ones a lost DO row no longer names.
- await deleteScriptsByTag(api, `pid-${this.name}`);
- }
- await this.ctx.storage.deleteAll();
- // deleteAll leaves the Durable Object's alarm armed; an orphaned alarm
- // would wake the erased object where the SDK's handler dies reading its
- // dropped cf_agents_schedules table.
- await this.ctx.storage.deleteAlarm();
- setTimeout(() => this.ctx.abort(), 0);
- }
-
- async onRequest(request: Request): Promise {
- try {
- const response = await this.routeRequest(request);
- await drainUnusedBody(request);
- return response;
- } catch (error) {
- // The Agents SDK's own _tryCatch converts handler exceptions into a
- // bare 500 before Sentry's DO instrumentation sees them - capture the
- // real stack first, then let the SDK answer.
- Sentry.captureException(error);
- throw error;
- }
- }
-
- private async routeRequest(request: Request): Promise {
- const url = new URL(request.url);
- if (!projectIdSchema.safeParse(this.name).success) {
- return Response.json({ error: 'invalid project id' }, { status: 400 });
- }
- const subPath = url.pathname.match(/\/agents\/[^/]+\/[^/]+(\/.*)?$/)?.[1] ?? '/';
-
- if (subPath === '/overview' && request.method === 'GET') {
- return Response.json(await this.getOverview());
- }
- if (subPath === '/deploys' && request.method === 'GET') {
- return this.listDeploys(url);
- }
- const deploy = subPath.match(/^\/apps\/([^/]+)\/deploys$/);
- if (deploy && request.method === 'POST') {
- return this.deployApp(decodeURIComponent(deploy[1]), request);
- }
- const vars = subPath.match(/^\/apps\/([^/]+)\/vars$/);
- if (vars && request.method === 'GET') {
- return this.listVars(decodeURIComponent(vars[1]));
- }
- if (vars && request.method === 'PUT') {
- return this.putVars(decodeURIComponent(vars[1]), request);
- }
- const secret = subPath.match(/^\/apps\/([^/]+)\/secrets$/);
- if (secret && request.method === 'GET') {
- return this.listSecrets(decodeURIComponent(secret[1]));
- }
- if (secret && request.method === 'POST') {
- return this.setSecret(decodeURIComponent(secret[1]), request);
- }
- const secretName = subPath.match(/^\/apps\/([^/]+)\/secrets\/([^/]+)$/);
- if (secretName && request.method === 'DELETE') {
- return this.deleteSecret(
- decodeURIComponent(secretName[1]),
- decodeURIComponent(secretName[2]),
- );
- }
- const analytics = subPath.match(/^\/apps\/([^/]+)\/analytics$/);
- if (analytics && request.method === 'GET') {
- return this.getAppAnalytics(decodeURIComponent(analytics[1]), url);
- }
- const buildEnv = subPath.match(/^\/apps\/([^/]+)\/build-env$/);
- if (buildEnv && request.method === 'GET') {
- return this.getBuildEnv(decodeURIComponent(buildEnv[1]));
- }
- const buildVarsPath = subPath.match(/^\/apps\/([^/]+)\/build-vars$/);
- if (buildVarsPath && request.method === 'PUT') {
- return this.putBuildVars(decodeURIComponent(buildVarsPath[1]), request);
- }
- const buildSecret = subPath.match(/^\/apps\/([^/]+)\/build-secrets\/([^/]+)$/);
- if (buildSecret && request.method === 'PUT') {
- return this.putBuildSecret(
- decodeURIComponent(buildSecret[1]),
- decodeURIComponent(buildSecret[2]),
- request,
- );
- }
- if (buildSecret && request.method === 'DELETE') {
- return this.deleteBuildSecret(
- decodeURIComponent(buildSecret[1]),
- decodeURIComponent(buildSecret[2]),
- );
- }
-
- return Response.json({ error: 'not found' }, { status: 404 });
- }
-
- async getOverview(): Promise> {
- const appRows = await this.db.select().from(apps).orderBy(apps.createdAt);
- const recent = await this.db
- .select()
- .from(deploys)
- .orderBy(desc(deploys.createdAt), desc(deploys.id))
- .limit(RECENT_DEPLOYS);
- const [total] = await this.db.select({ value: count() }).from(deploys);
- return {
- projectId: this.name,
- provisionedAt: this.state.provisionedAt,
- apps: appRows.map((row) => this.toAppSummary(row)),
- recentDeploys: recent.map((row) => this.toDeploySummary(row)),
- totalDeploys: total?.value ?? 0,
- // Honest config report, so the dashboard can explain instead of 502.
- configured: this.stub || (!!this.env.DISPATCH && this.cfApi() !== null),
- stub: this.stub,
- };
- }
-
- /** Keyset-paged deploy history (no offsets: rows land mid-scan). */
- private async listDeploys(url: URL): Promise {
- const appName = url.searchParams.get('app') ?? undefined;
- const cursor = deployCursorSchema.parse(url.searchParams.get('cursor') ?? undefined);
- const limit = Math.min(Math.max(Number(url.searchParams.get('limit')) || 20, 1), 100);
-
- const appFilter = appName ? eq(deploys.appName, appName) : undefined;
- let cursorFilter;
- if (cursor) {
- const [ms, id] = cursor.split(':');
- const at = new Date(Number(ms));
- cursorFilter = or(
- lt(deploys.createdAt, at),
- and(eq(deploys.createdAt, at), lt(deploys.id, id)),
- );
- }
- const rows = await this.db
- .select()
- .from(deploys)
- .where(and(appFilter, cursorFilter))
- .orderBy(desc(deploys.createdAt), desc(deploys.id))
- .limit(limit + 1);
- const page = rows.slice(0, limit);
- const next = rows.length > limit ? page[page.length - 1] : null;
- const [total] = await this.db
- .select({ value: count() })
- .from(deploys)
- .where(appFilter ?? sql`1 = 1`);
- return Response.json({
- deploys: page.map((row) => this.toDeploySummary(row)),
- total: total?.value ?? 0,
- cursor: next ? `${next.createdAt.getTime()}:${next.id}` : null,
- });
- }
-
- /**
- * The checks every deploy passes before its body is even read: no demo
- * hosting, the app must carry a console-pushed claim, and the daily
- * ceiling. Returns the app row, or the Response to send instead.
- */
- private async gateDeploy(appName: string): Promise {
- if (DEMO_PROJECT_PATTERN.test(this.name)) {
- // Belt to the console's braces: no demo hosting, ever.
- return Response.json({ error: 'demo projects cannot deploy apps' }, { status: 403 });
- }
- const [app] = await this.db.select().from(apps).where(eq(apps.name, appName)).limit(1);
- if (!app) {
- // The console pushes the claim before forwarding a deploy; reaching
- // this without one means the agent surface was dialled directly.
- return Response.json(
- { error: 'app is not linked - deploy through the console or the CLI' },
- { status: 409 },
- );
- }
-
- const since = new Date(Date.now() - 24 * 60 * 60 * 1000);
- const [recent] = await this.db
- .select({ value: count() })
- .from(deploys)
- .where(gt(deploys.createdAt, since));
- if ((recent?.value ?? 0) >= MAX_DEPLOYS_PER_DAY) {
- return Response.json(
- { error: `projects are limited to ${MAX_DEPLOYS_PER_DAY} deploys per day` },
- { status: 429 },
- );
- }
- return app;
- }
-
- private async deployApp(appName: string, request: Request): Promise {
- const gate = await this.gateDeploy(appName);
- if (gate instanceof Response) return gate;
- const app = gate;
-
- let form: FormData;
- try {
- form = await request.formData();
- } catch {
- return Response.json({ error: 'expected a multipart deploy body' }, { status: 400 });
- }
-
- const metaRaw = form.get('meta');
- const meta = deployMetaSchema.safeParse(
- typeof metaRaw === 'string' ? JSON.parse(metaRaw || 'null') : null,
- );
- if (!meta.success) {
- return Response.json({ error: 'invalid deploy meta' }, { status: 400 });
- }
-
- const modules: ModuleFile[] = [];
- const assets: AssetFile[] = [];
- for (const [key, value] of form.entries()) {
- if (typeof value === 'string') continue;
- const bytes = new Uint8Array(await value.arrayBuffer());
- if (key.startsWith('module:')) {
- const name = key.slice('module:'.length);
- if (!/^[A-Za-z0-9._-]+\.(?:js|mjs)$/.test(name)) {
- return Response.json({ error: `invalid module name "${name}"` }, { status: 400 });
- }
- modules.push({ name, bytes });
- } else if (key.startsWith('asset:')) {
- const path = key.slice('asset:'.length);
- if (!path.startsWith('/') || path.includes('..') || path.length > 512) {
- return Response.json({ error: `invalid asset path "${path}"` }, { status: 400 });
- }
- assets.push({ path, bytes, contentType: contentTypeFor(path, value.type) });
- }
- }
-
- // Size ceilings are enforced in publish(), so both deploy paths meet
- // exactly the same limits.
- return this.publish(appName, app, {
- modules,
- assets,
- meta: meta.data,
- origin: new URL(request.url).origin,
- });
- }
-
- /**
- * Uploads a prepared deploy and records it. Shared by the multipart route
- * (the CLI's path) and the git route (a push webhook), so the caps and the
- * bookkeeping can never diverge between them - the two differ only in
- * where the bytes came from.
- */
- private async publish(
- appName: string,
- app: AppRecord,
- input: { modules: ModuleFile[]; assets: AssetFile[]; meta: DeployMeta; origin: string },
- ): Promise {
- const { modules, meta } = input;
- // Root-level convention files never publish, whatever path they arrived
- // by. A deploy REDUCED to nothing by this (a lone _worker.js) falls
- // through to the honest "nothing to deploy" below.
- const assets = input.assets.filter((asset) => !RESERVED_ROOT_ASSETS.has(asset.path));
- const moduleBytes = modules.reduce((total, module) => total + module.bytes.length, 0);
- const assetBytes = assets.reduce((total, asset) => total + asset.bytes.length, 0);
-
- if (moduleBytes > MAX_MODULE_BYTES) {
- return Response.json(
- { error: `the Worker bundle exceeds ${MAX_MODULE_BYTES / 1024 / 1024} MB` },
- { status: 400 },
- );
- }
- if (assets.length > MAX_ASSET_COUNT) {
- return Response.json(
- { error: `deploys are limited to ${MAX_ASSET_COUNT} assets` },
- { status: 400 },
- );
- }
- if (assetBytes > MAX_ASSET_TOTAL_BYTES) {
- return Response.json(
- { error: `assets exceed ${MAX_ASSET_TOTAL_BYTES / 1024 / 1024} MB` },
- { status: 400 },
- );
- }
- if (assets.some((asset) => asset.bytes.length > MAX_ASSET_FILE_BYTES)) {
- return Response.json(
- { error: `an asset exceeds ${MAX_ASSET_FILE_BYTES / 1024 / 1024} MB` },
- { status: 400 },
- );
- }
- if (!modules.length && !assets.length) {
- return Response.json({ error: 'nothing to deploy' }, { status: 400 });
- }
- if (modules.length && !meta.mainModule) {
- return Response.json({ error: 'deploys with modules need meta.mainModule' }, { status: 400 });
- }
- if (meta.mainModule && !modules.some((module) => module.name === meta.mainModule)) {
- return Response.json({ error: 'meta.mainModule is not among the modules' }, { status: 400 });
- }
-
- // Stored console vars sit between the CLI's meta.vars and the platform's:
- // the console is the canonical editor, so its values win over a stale
- // cloudflarebase.json, and the platform's facts stay uncontestable on top
- // (deployVars applies them last).
- const storedRows = await this.db.select().from(appVars).where(eq(appVars.appName, appName));
- const storedVars = Object.fromEntries(storedRows.map((row) => [row.name, row.value]));
-
- if (!this.stub) {
- const api = this.cfApi();
- if (!this.env.DISPATCH || !api) {
- return Response.json(
- {
- error:
- 'hosting is not configured - this install needs a Workers for Platforms dispatch namespace (DISPATCH binding, DISPATCH_NAMESPACE, CF_ACCOUNT_ID, CF_HOSTING_API_TOKEN)',
- },
- { status: 503 },
- );
- }
- try {
- const assetsJwt = await uploadAssets(api, app.subdomain, this.name, assets);
- await putScript(api, app.subdomain, {
- projectId: this.name,
- appName,
- mainModule: meta.mainModule,
- modules,
- compatibilityDate: meta.compatibilityDate ?? '2026-07-10',
- compatibilityFlags: meta.compatibilityFlags ?? [],
- assetsJwt,
- notFoundHandling: meta.notFoundHandling,
- vars: deployVars({ ...(meta.vars ?? {}), ...storedVars }, this.name, input.origin),
- });
- } catch (cause) {
- Sentry.captureException(cause, {
- tags: { operation: 'hosting-deploy', projectId: this.name },
- });
- const message = cause instanceof Error ? cause.message : 'upload failed';
- return Response.json({ error: `deploy failed - ${message}` }, { status: 502 });
- }
- }
-
- const record: DeployRecord = {
- id: crypto.randomUUID(),
- appName,
- subdomain: app.subdomain,
- status: this.stub ? 'stub' : 'live',
- hasWorker: modules.length > 0,
- assetCount: assets.length,
- assetBytes,
- moduleBytes,
- createdAt: new Date(),
- };
- await this.db.insert(deploys).values(record);
- await this.db
- .update(apps)
- .set({
- deployCount: app.deployCount + 1,
- lastDeployAt: record.createdAt,
- // Snapshot of what the CLI declared, so a later console var edit can
- // rebuild the script's full plain_text set without the CLI present.
- lastDeployVars: JSON.stringify(meta.vars ?? {}),
- })
- .where(eq(apps.name, appName));
- await this.syncState();
-
- return Response.json(
- {
- deploy: this.toDeploySummary(record),
- subdomain: app.subdomain,
- url: this.appUrl(app.subdomain),
- },
- { status: 201 },
- );
- }
-
- /**
- * Deploys a repository tarball - the no-runner path (Phase B, direct
- * deploys). The console resolved the claim and the download URL, so this
- * receives a plain URL and never holds a GitHub credential.
- *
- * Only static assets: a repository that needs a build is a build-mode
- * connection and arrives through the ordinary multipart route instead.
- */
- async gitDeploy(input: {
- appName: string;
- tarballUrl: string;
- assetsDir: string;
- origin: string;
- }): Promise<{ status: number; json: string }> {
- const gate = await this.gateDeploy(input.appName);
- if (gate instanceof Response) {
- return { status: gate.status, json: await gate.text() };
- }
-
- let assets: AssetFile[];
- try {
- const response = await fetch(input.tarballUrl);
- if (!response.ok || !response.body) {
- return {
- status: 502,
- json: JSON.stringify({ error: 'could not download the repository archive' }),
- };
- }
- const buffer = await gunzip(response.body, MAX_ARCHIVE_BYTES);
- const entries = parseTar(buffer, MAX_ARCHIVE_FILES);
- assets = toAssetPaths(entries, input.assetsDir).map(({ path, bytes }) => ({
- path,
- bytes,
- contentType: contentTypeFor(path, ''),
- }));
- } catch (cause) {
- Sentry.captureException(cause, {
- tags: { operation: 'hosting-git-deploy', projectId: this.name },
- });
- const message = cause instanceof Error ? cause.message : 'could not read the archive';
- return { status: 400, json: JSON.stringify({ error: message }) };
- }
-
- if (!assets.length) {
- return {
- status: 400,
- json: JSON.stringify({
- error: input.assetsDir
- ? `nothing to deploy - "${input.assetsDir}" is empty or missing in this commit`
- : 'nothing to deploy - the repository has no publishable files',
- }),
- };
- }
-
- const response = await this.publish(input.appName, gate, {
- modules: [],
- assets,
- meta: { notFoundHandling: 'single-page-application' },
- origin: input.origin,
- });
- return { status: response.status, json: await response.text() };
- }
-
- private async setSecret(appName: string, request: Request): Promise {
- const [app] = await this.db.select().from(apps).where(eq(apps.name, appName)).limit(1);
- if (!app) return Response.json({ error: 'no such app' }, { status: 404 });
- const body = secretBodySchema.safeParse(await request.json().catch(() => null));
- if (!body.success) {
- return Response.json({ error: 'invalid secret body' }, { status: 400 });
- }
- const name = body.data.name;
-
- const [existing] = await this.db
- .select()
- .from(appSecrets)
- .where(and(eq(appSecrets.appName, appName), eq(appSecrets.name, name)))
- .limit(1);
- if (!existing) {
- const [{ value: secretCount }] = await this.db
- .select({ value: count() })
- .from(appSecrets)
- .where(eq(appSecrets.appName, appName));
- if (secretCount >= MAX_SECRETS_PER_APP) {
- return Response.json(
- { error: `apps are limited to ${MAX_SECRETS_PER_APP} secrets` },
- { status: 400 },
- );
- }
- }
-
- if (!this.stub) {
- // Write-through: the value goes to Cloudflare's script settings and is
- // never at rest here. Stub mode skips the call (there is no script) but
- // still records the name, so local dev and e2e exercise the whole
- // list/delete contract.
- const api = this.cfApi();
- if (!api) {
- return Response.json({ error: 'hosting is not configured' }, { status: 503 });
- }
- try {
- await patchScriptSecret(api, app.subdomain, name, body.data.value);
- } catch (cause) {
- Sentry.captureException(cause, {
- tags: { operation: 'hosting-secret', projectId: this.name },
- });
- return Response.json({ error: 'setting the secret failed' }, { status: 502 });
- }
- }
-
- // Also at rest, encrypted, when the install can: builds receive runtime
- // secrets too (frameworks inline env at build time), and the write-through
- // value above is unrecoverable by design. Keyless installs store null -
- // the secret works at runtime and simply stays out of builds. The AAD's
- // `runtime` prefix keeps this ciphertext and a build secret's from ever
- // being interchangeable, even for the same app and name.
- const master = this.env.HOSTING_MASTER_KEY;
- const ciphertext = master
- ? await encryptValue(
- await importMasterKey(master),
- body.data.value,
- `runtime\0${appName}\0${name}`,
- )
- : null;
- const now = new Date();
- await this.db
- .insert(appSecrets)
- .values({ appName, name, ciphertext, createdAt: now, updatedAt: now })
- .onConflictDoUpdate({
- target: [appSecrets.appName, appSecrets.name],
- set: { ciphertext, updatedAt: now },
- });
- return Response.json({ ok: true });
- }
-
- /** Secret NAMES and timestamps - the values live at Cloudflare, not here. */
- private async listSecrets(appName: string): Promise {
- if (!appNameSchema.safeParse(appName).success) {
- return Response.json({ error: 'invalid app name' }, { status: 400 });
- }
- const rows = await this.db
- .select()
- .from(appSecrets)
- .where(eq(appSecrets.appName, appName))
- .orderBy(appSecrets.name);
- return Response.json({
- secrets: rows.map((row) => ({
- name: row.name,
- createdAt: row.createdAt.toISOString(),
- updatedAt: row.updatedAt.toISOString(),
- })),
- });
- }
-
- /** Idempotent: the name row goes regardless; the script binding is removed
- * when there is a script to remove it from (404-tolerant at the API). */
- private async deleteSecret(appName: string, name: string): Promise {
- if (!appNameSchema.safeParse(appName).success) {
- return Response.json({ error: 'invalid app name' }, { status: 400 });
- }
- if (!varNameSchema.safeParse(name).success) {
- return Response.json({ error: 'invalid secret name' }, { status: 400 });
- }
- if (!this.stub) {
- const [app] = await this.db.select().from(apps).where(eq(apps.name, appName)).limit(1);
- const api = this.cfApi();
- if (app && api) {
- try {
- await deleteScriptSecret(api, app.subdomain, name);
- } catch (cause) {
- Sentry.captureException(cause, {
- tags: { operation: 'hosting-secret-delete', projectId: this.name },
- });
- // The row stays - the console keeps showing a secret that is
- // still set, which is the honest state.
- return Response.json({ error: 'deleting the secret failed' }, { status: 502 });
- }
- }
- }
- await this.db
- .delete(appSecrets)
- .where(and(eq(appSecrets.appName, appName), eq(appSecrets.name, name)));
- return Response.json({ ok: true });
- }
-
- /** Stored runtime vars. No `apps`-row requirement: a claim-only app can be
- * configured before its first deploy, which then picks the vars up. */
- private async listVars(appName: string): Promise {
- if (!appNameSchema.safeParse(appName).success) {
- return Response.json({ error: 'invalid app name' }, { status: 400 });
- }
- return Response.json({ vars: await this.varSummaries(appName) });
- }
-
- private async varSummaries(
- appName: string,
- ): Promise<{ name: string; value: string; createdAt: string; updatedAt: string }[]> {
- const rows = await this.db
- .select()
- .from(appVars)
- .where(eq(appVars.appName, appName))
- .orderBy(appVars.name);
- return rows.map((row) => ({
- name: row.name,
- value: row.value,
- createdAt: row.createdAt.toISOString(),
- updatedAt: row.updatedAt.toISOString(),
- }));
- }
-
- /**
- * Replace-the-set: the console form submits the whole table, so absent
- * names are deletions. The store always succeeds first; patching the live
- * script is best-effort and reported (`patched` / `warning`), never a way
- * to lose an edit to a transient API failure.
- */
- private async putVars(appName: string, request: Request): Promise {
- if (!appNameSchema.safeParse(appName).success) {
- return Response.json({ error: 'invalid app name' }, { status: 400 });
- }
- const body = varsBodySchema.safeParse(await request.json().catch(() => null));
- if (!body.success) {
- return Response.json({ error: 'invalid vars body' }, { status: 400 });
- }
- const wanted = body.data.vars;
- if (Object.keys(wanted).length > MAX_VARS_PER_APP) {
- return Response.json(
- { error: `apps are limited to ${MAX_VARS_PER_APP} variables` },
- { status: 400 },
- );
- }
-
- const now = new Date();
- const existing = await this.db.select().from(appVars).where(eq(appVars.appName, appName));
- const current = new Map(existing.map((row) => [row.name, row]));
- for (const row of existing) {
- if (!(row.name in wanted)) {
- await this.db
- .delete(appVars)
- .where(and(eq(appVars.appName, appName), eq(appVars.name, row.name)));
- }
- }
- for (const [name, value] of Object.entries(wanted)) {
- const row = current.get(name);
- if (!row) {
- await this.db
- .insert(appVars)
- .values({ appName, name, value, createdAt: now, updatedAt: now });
- } else if (row.value !== value) {
- await this.db
- .update(appVars)
- .set({ value, updatedAt: now })
- .where(and(eq(appVars.appName, appName), eq(appVars.name, name)));
- }
- }
-
- let patched = false;
- let warning: string | undefined;
- const [app] = await this.db.select().from(apps).where(eq(apps.name, appName)).limit(1);
- const api = this.cfApi();
- if (!this.stub && app?.lastDeployAt && api) {
- // The last CLI-declared vars ride the stored snapshot; the platform's
- // come out on top, same order as a deploy.
- let cliVars: Record = {};
- try {
- cliVars = storedVarsSchema.parse(JSON.parse(app.lastDeployVars ?? '{}'));
- } catch {
- // An unreadable snapshot degrades to "no CLI vars".
- }
- try {
- await patchScriptVars(
- api,
- app.subdomain,
- deployVars({ ...cliVars, ...wanted }, this.name, new URL(request.url).origin),
- );
- patched = true;
- } catch (cause) {
- Sentry.captureException(cause, {
- tags: { operation: 'hosting-vars', projectId: this.name },
- });
- warning = 'saved, but updating the live script failed - the next deploy applies them';
- }
- }
-
- return Response.json({
- vars: await this.varSummaries(appName),
- patched,
- ...(warning ? { warning } : {}),
- });
- }
-
- /**
- * Operator read of the build-time environment: vars with values, secret
- * NAMES only. Decrypted values never cross the operator surface - the
- * runner's copy travels the service-binding-only /internal route instead.
- */
- private async getBuildEnv(appName: string): Promise {
- if (!appNameSchema.safeParse(appName).success) {
- return Response.json({ error: 'invalid app name' }, { status: 400 });
- }
- const varRows = await this.db
- .select()
- .from(buildVars)
- .where(eq(buildVars.appName, appName))
- .orderBy(buildVars.name);
- const secretRows = await this.db
- .select()
- .from(buildSecrets)
- .where(eq(buildSecrets.appName, appName))
- .orderBy(buildSecrets.name);
- return Response.json({
- vars: varRows.map((row) => ({
- name: row.name,
- value: row.value,
- createdAt: row.createdAt.toISOString(),
- updatedAt: row.updatedAt.toISOString(),
- })),
- secrets: secretRows.map((row) => ({
- name: row.name,
- createdAt: row.createdAt.toISOString(),
- updatedAt: row.updatedAt.toISOString(),
- })),
- encryptionConfigured: !!this.env.HOSTING_MASTER_KEY,
- });
- }
-
- /** Replace-the-set for build vars, exactly like runtime vars - minus the
- * live patch, because build vars only exist at build time. */
- private async putBuildVars(appName: string, request: Request): Promise {
- if (!appNameSchema.safeParse(appName).success) {
- return Response.json({ error: 'invalid app name' }, { status: 400 });
- }
- const body = varsBodySchema.safeParse(await request.json().catch(() => null));
- if (!body.success) {
- return Response.json({ error: 'invalid vars body' }, { status: 400 });
- }
- const wanted = body.data.vars;
- if (Object.keys(wanted).length > MAX_VARS_PER_APP) {
- return Response.json(
- { error: `apps are limited to ${MAX_VARS_PER_APP} build variables` },
- { status: 400 },
- );
- }
-
- const now = new Date();
- const existing = await this.db.select().from(buildVars).where(eq(buildVars.appName, appName));
- const current = new Map(existing.map((row) => [row.name, row]));
- for (const row of existing) {
- if (!(row.name in wanted)) {
- await this.db
- .delete(buildVars)
- .where(and(eq(buildVars.appName, appName), eq(buildVars.name, row.name)));
- }
- }
- for (const [name, value] of Object.entries(wanted)) {
- const row = current.get(name);
- if (!row) {
- await this.db
- .insert(buildVars)
- .values({ appName, name, value, createdAt: now, updatedAt: now });
- } else if (row.value !== value) {
- await this.db
- .update(buildVars)
- .set({ value, updatedAt: now })
- .where(and(eq(buildVars.appName, appName), eq(buildVars.name, name)));
- }
- }
-
- const rows = await this.db
- .select()
- .from(buildVars)
- .where(eq(buildVars.appName, appName))
- .orderBy(buildVars.name);
- return Response.json({
- vars: rows.map((row) => ({
- name: row.name,
- value: row.value,
- createdAt: row.createdAt.toISOString(),
- updatedAt: row.updatedAt.toISOString(),
- })),
- });
- }
-
- /** Encrypts one build secret at rest. The 503 without a master key is the
- * degradation contract: everything else on the install keeps working. */
- private async putBuildSecret(appName: string, name: string, request: Request): Promise {
- if (!appNameSchema.safeParse(appName).success) {
- return Response.json({ error: 'invalid app name' }, { status: 400 });
- }
- if (!varNameSchema.safeParse(name).success) {
- return Response.json({ error: 'invalid secret name' }, { status: 400 });
- }
- const body = buildSecretBodySchema.safeParse(await request.json().catch(() => null));
- if (!body.success) {
- return Response.json({ error: 'invalid secret body' }, { status: 400 });
- }
- const master = this.env.HOSTING_MASTER_KEY;
- if (!master) {
- return Response.json(
- {
- error:
- 'build secrets are not configured on this install - set the HOSTING_MASTER_KEY secret on the hosting agent',
- },
- { status: 503 },
- );
- }
-
- const [existing] = await this.db
- .select()
- .from(buildSecrets)
- .where(and(eq(buildSecrets.appName, appName), eq(buildSecrets.name, name)))
- .limit(1);
- if (!existing) {
- const [{ value: secretCount }] = await this.db
- .select({ value: count() })
- .from(buildSecrets)
- .where(eq(buildSecrets.appName, appName));
- if (secretCount >= MAX_BUILD_SECRETS_PER_APP) {
- return Response.json(
- { error: `apps are limited to ${MAX_BUILD_SECRETS_PER_APP} build secrets` },
- { status: 400 },
- );
- }
- }
-
- const key = await importMasterKey(master);
- // The AAD binds the ciphertext to its row: copied to another name or
- // app, it fails authentication instead of decrypting.
- const ciphertext = await encryptValue(key, body.data.value, `${appName}\0${name}`);
- const now = new Date();
- await this.db
- .insert(buildSecrets)
- .values({ appName, name, ciphertext, createdAt: now, updatedAt: now })
- .onConflictDoUpdate({
- target: [buildSecrets.appName, buildSecrets.name],
- set: { ciphertext, updatedAt: now },
- });
- return Response.json({ ok: true });
- }
-
- private async deleteBuildSecret(appName: string, name: string): Promise {
- if (!appNameSchema.safeParse(appName).success) {
- return Response.json({ error: 'invalid app name' }, { status: 400 });
- }
- if (!varNameSchema.safeParse(name).success) {
- return Response.json({ error: 'invalid secret name' }, { status: 400 });
- }
- await this.db
- .delete(buildSecrets)
- .where(and(eq(buildSecrets.appName, appName), eq(buildSecrets.name, name)));
- return Response.json({ ok: true });
- }
-
- /**
- * RPC from the worker's service-binding-only build-env route: the DECRYPTED
- * bundle a GitHub Actions runner exports before its build step. The console
- * verified the OIDC grant before dialling this; decrypted values only ever
- * transit the dashboard's service binding, never the operator HTTP surface.
- */
- async buildEnvBundle(
- appName: string,
- ): Promise<
- | { vars: Record; secrets: Record }
- | { error: string; status: number }
- > {
- if (!appNameSchema.safeParse(appName).success) {
- return { error: 'invalid app name', status: 400 };
- }
- // Runtime env rides into the build too - frameworks inline env at build
- // time, so a PUBLIC_* var that exists only as a runtime binding never
- // reaches the client bundle. Build-specific values win on collision
- // (mergeBuildEnv pins the rule).
- const appVarRows = await this.db.select().from(appVars).where(eq(appVars.appName, appName));
- const buildVarRows = await this.db
- .select()
- .from(buildVars)
- .where(eq(buildVars.appName, appName));
- const appSecretRows = await this.db
- .select()
- .from(appSecrets)
- .where(eq(appSecrets.appName, appName));
- const buildSecretRows = await this.db
- .select()
- .from(buildSecrets)
- .where(eq(buildSecrets.appName, appName));
-
- // Rows without ciphertext are runtime-only: written before values were
- // stored at rest, or on an install with no master key. They were never
- // promised to builds, so their absence is not an error - unlike a row
- // that HAS ciphertext nobody can decrypt.
- const encryptedAppSecrets = appSecretRows.filter(
- (row): row is typeof row & { ciphertext: string } => row.ciphertext !== null,
- );
- const master = this.env.HOSTING_MASTER_KEY;
- if ((buildSecretRows.length || encryptedAppSecrets.length) && !master) {
- // Fail loud: a build that silently runs without its secrets is worse
- // than one that fails attributed to configuration.
- return {
- error: 'stored secrets exist but HOSTING_MASTER_KEY is not set on this install',
- status: 503,
- };
- }
-
- const runtimeSecrets: Record = {};
- const buildOnlySecrets: Record = {};
- if (master) {
- const key = await importMasterKey(master);
- const decrypt = async (
- into: Record,
- row: { appName: string; name: string; ciphertext: string },
- aad: string,
- ) => {
- try {
- into[row.name] = await decryptValue(key, row.ciphertext, aad);
- return null;
- } catch (cause) {
- Sentry.captureException(cause, {
- tags: { operation: 'hosting-build-env', projectId: this.name },
- });
- return {
- error: `secret ${row.name} cannot be decrypted - was the master key rotated?`,
- status: 503,
- };
- }
- };
- for (const row of encryptedAppSecrets) {
- const failed = await decrypt(runtimeSecrets, row, `runtime\0${row.appName}\0${row.name}`);
- if (failed) return failed;
- }
- for (const row of buildSecretRows) {
- const failed = await decrypt(buildOnlySecrets, row, `${row.appName}\0${row.name}`);
- if (failed) return failed;
- }
- }
-
- return mergeBuildEnv(
- {
- vars: Object.fromEntries(appVarRows.map((row) => [row.name, row.value])),
- secrets: runtimeSecrets,
- },
- {
- vars: Object.fromEntries(buildVarRows.map((row) => [row.name, row.value])),
- secrets: buildOnlySecrets,
- },
- );
- }
-
- // --- Analytics ------------------------------------------------------------
-
- private analyticsCache: {
- key: string;
- expiresAt: number;
- data: Record;
- } | null = null;
-
- private get waeConfig(): { accountId: string; token: string; dataset: string } | null {
- const accountId = this.env.CF_ACCOUNT_ID;
- const token = this.env.CF_ANALYTICS_API_TOKEN;
- const dataset = this.env.WAE_DATASET;
- return accountId && token && dataset ? { accountId, token, dataset } : null;
- }
-
- private async analyticsSql(query: string): Promise {
- const config = this.waeConfig;
- if (!config) return [];
- const response = await fetch(
- `https://api.cloudflare.com/client/v4/accounts/${config.accountId}/analytics_engine/sql`,
- {
- method: 'POST',
- headers: { authorization: `Bearer ${config.token}` },
- body: `${query} FORMAT JSON`,
- },
- );
- if (!response.ok) {
- throw new Error(`Analytics Engine query failed (${response.status})`);
- }
- const result = analyticsApiResponseSchema.parse(await response.json());
- return (result.data ?? []) as T[];
- }
-
- /**
- * Per-app requests/errors: daily buckets plus totals, from Analytics Engine
- * (or the local D1 stand-in). Errors never 5xx the route - a broken
- * analytics token would otherwise read as "the app is down" forever; the
- * chart answers zeroed with `engine.status: 'error'` instead (the auth
- * agent's rule).
- */
- private async getAppAnalytics(appName: string, url: URL): Promise {
- if (!appNameSchema.safeParse(appName).success) {
- return Response.json({ error: 'invalid app name' }, { status: 400 });
- }
- const wanted = Number(url.searchParams.get('days'));
- const days = wanted === 30 || wanted === 90 ? wanted : 7;
- const [app] = await this.db.select().from(apps).where(eq(apps.name, appName)).limit(1);
- if (!app) return Response.json({ error: 'no such app' }, { status: 404 });
-
- const cacheKey = `${appName}:${days}`;
- if (
- this.analyticsCache &&
- this.analyticsCache.key === cacheKey &&
- this.analyticsCache.expiresAt > Date.now()
- ) {
- return Response.json(this.analyticsCache.data);
- }
-
- let analyticsError: string | undefined;
- let byDay: { day: string; requests: number; errors: number }[] = [];
- let totals = { requests: 0, errors: 0, avgDurationMs: 0 };
- const config = this.waeConfig;
- try {
- if (config) {
- const series = await this.queryWaeSeries(config, app.subdomain, days);
- byDay = series.byDay;
- totals = series.totals;
- } else if (this.env.LOCAL_ANALYTICS) {
- const series = await this.queryLocalSeries(app.subdomain, days);
- byDay = series.byDay;
- totals = series.totals;
- }
- } catch (cause) {
- analyticsError = cause instanceof Error ? cause.message : 'Analytics Engine query failed';
- Sentry.captureException(cause, {
- tags: { operation: 'hosting-analytics', projectId: this.name },
- });
- byDay = [];
- totals = { requests: 0, errors: 0, avgDurationMs: 0 };
- }
-
- const data = {
- appName,
- subdomain: app.subdomain,
- days,
- totals,
- byDay,
- engine: {
- dataset: this.env.WAE_DATASET ?? 'cloudflarebase_hosting_requests',
- enabled: config !== null || !!this.env.LOCAL_ANALYTICS,
- status: analyticsError
- ? ('error' as const)
- : config
- ? ('connected' as const)
- : this.env.LOCAL_ANALYTICS
- ? ('local' as const)
- : ('write-only' as const),
- ...(analyticsError ? { error: analyticsError } : {}),
- },
- };
- this.analyticsCache = { key: cacheKey, expiresAt: Date.now() + ANALYTICS_CACHE_MS, data };
- return Response.json(data);
- }
-
- private async queryWaeSeries(
- config: { accountId: string; token: string; dataset: string },
- subdomain: string,
- days: number,
- ): Promise<{
- byDay: { day: string; requests: number; errors: number }[];
- totals: { requests: number; errors: number; avgDurationMs: number };
- }> {
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(config.dataset)) {
- throw new Error('WAE_DATASET must be a valid Analytics Engine identifier');
- }
- const sub = subdomain.replaceAll("'", "''");
- const from = `FROM ${config.dataset} WHERE index1 = '${sub}' AND timestamp > NOW() - INTERVAL '${days}' DAY`;
- // Status rides blob1 as a 3-digit string, so 5xx is a lexicographic range.
- const [requests, errors, sums] = await Promise.all([
- this.analyticsSql<{ day: string; n: number | string }>(
- `SELECT formatDateTime(timestamp, '%Y-%m-%d') AS day, SUM(_sample_interval) AS n ${from} GROUP BY day ORDER BY day`,
- ),
- this.analyticsSql<{ day: string; n: number | string }>(
- `SELECT formatDateTime(timestamp, '%Y-%m-%d') AS day, SUM(_sample_interval) AS n ${from} AND blob1 >= '500' AND blob1 <= '599' GROUP BY day ORDER BY day`,
- ),
- this.analyticsSql<{ n: number | string; duration: number | string }>(
- `SELECT SUM(_sample_interval) AS n, SUM(double1 * _sample_interval) AS duration ${from}`,
- ),
- ]);
- const dayMap = new Map();
- const entry = (day: string) => {
- let value = dayMap.get(day);
- if (!value) {
- value = { day, requests: 0, errors: 0 };
- dayMap.set(day, value);
- }
- return value;
- };
- for (const row of requests) entry(row.day.slice(0, 10)).requests += Number(row.n);
- for (const row of errors) entry(row.day.slice(0, 10)).errors += Number(row.n);
- const byDay = [...dayMap.values()].sort((a, b) => a.day.localeCompare(b.day));
- const totalRequests = Number(sums[0]?.n ?? 0);
- const totalDuration = Number(sums[0]?.duration ?? 0);
- return {
- byDay,
- totals: {
- requests: totalRequests,
- errors: byDay.reduce((sum, row) => sum + row.errors, 0),
- avgDurationMs: totalRequests ? totalDuration / totalRequests : 0,
- },
- };
- }
-
- private async queryLocalSeries(
- subdomain: string,
- days: number,
- ): Promise<{
- byDay: { day: string; requests: number; errors: number }[];
- totals: { requests: number; errors: number; avgDurationMs: number };
- }> {
- const db = this.env.LOCAL_ANALYTICS!;
- const since = Date.now() - days * 86_400_000;
- // The serve path (src/index.ts) creates the table on first write; the
- // CREATE here covers reading an app nothing has ever requested.
- const [, , rows, sums] = await db.batch([
- db.prepare(
- `CREATE TABLE IF NOT EXISTS hosting_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, subdomain TEXT NOT NULL, timestamp INTEGER NOT NULL, status INTEGER NOT NULL, duration_ms REAL NOT NULL)`,
- ),
- db.prepare(
- `CREATE INDEX IF NOT EXISTS hosting_requests_sub_time ON hosting_requests(subdomain, timestamp)`,
- ),
- db
- .prepare(
- `SELECT strftime('%Y-%m-%d', timestamp / 1000, 'unixepoch') AS day, COUNT(*) AS requests, SUM(CASE WHEN status BETWEEN 500 AND 599 THEN 1 ELSE 0 END) AS errors FROM hosting_requests WHERE subdomain = ? AND timestamp > ? GROUP BY day ORDER BY day`,
- )
- .bind(subdomain, since),
- db
- .prepare(
- `SELECT COUNT(*) AS requests, SUM(CASE WHEN status BETWEEN 500 AND 599 THEN 1 ELSE 0 END) AS errors, AVG(duration_ms) AS avg FROM hosting_requests WHERE subdomain = ? AND timestamp > ?`,
- )
- .bind(subdomain, since),
- ]);
- const byDay = (rows.results as { day: string; requests: number; errors: number | null }[]).map(
- (row) => ({ day: row.day, requests: Number(row.requests), errors: Number(row.errors ?? 0) }),
- );
- const total = (
- sums.results as { requests: number; errors: number | null; avg: number | null }[]
- )[0];
- return {
- byDay,
- totals: {
- requests: Number(total?.requests ?? 0),
- errors: Number(total?.errors ?? 0),
- avgDurationMs: Number(total?.avg ?? 0),
- },
- };
- }
-}
diff --git a/agents/hosting/src/bindings.test-d.ts b/agents/hosting/src/bindings.test-d.ts
deleted file mode 100644
index 899f5de5..00000000
--- a/agents/hosting/src/bindings.test-d.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Compile-time contract test, executed by `tsc --noEmit` (excluded from the
- * publish build). The @ts-expect-error negatives fail the typecheck in BOTH
- * directions: weakening the contract makes a suppression unused, which
- * TypeScript reports.
- */
-import type { AssertHostingAgentEnv } from './bindings';
-
-// This repository's own generated Env satisfies the contract.
-type _SelfCheck = AssertHostingAgentEnv;
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type AnyNamespace = DurableObjectNamespace;
-
-// The minimal correct deployment: just the Durable Object namespace.
-type _Minimal = AssertHostingAgentEnv<{ HostingAgent: AnyNamespace }>;
-
-// A fully configured deployment.
-type _Full = AssertHostingAgentEnv<{
- HostingAgent: AnyNamespace;
- DISPATCH: DispatchNamespace;
- CF_ACCOUNT_ID: string;
- CF_HOSTING_API_TOKEN: string;
- DISPATCH_NAMESPACE: string;
- HOSTING_DOMAIN: string;
- DISPATCH_OUTBOUND: 'true';
- HOSTING_APEX_REDIRECT: string;
- HOSTING_STUB: 'true';
- SENTRY_DSN: string;
- SENTRY_ENV: string;
-}>;
-
-// @ts-expect-error - the Durable Object namespace is required.
-type _MissingAgent = AssertHostingAgentEnv<{ DISPATCH: DispatchNamespace }>;
-
-// @ts-expect-error - DISPATCH must be a dispatch namespace, not a Fetcher.
-type _WrongDispatch = AssertHostingAgentEnv<{ HostingAgent: AnyNamespace; DISPATCH: Fetcher }>;
-
-// @ts-expect-error - vars are strings.
-type _WrongOptional = AssertHostingAgentEnv<{ HostingAgent: AnyNamespace; HOSTING_DOMAIN: number }>;
-
-export type {};
diff --git a/agents/hosting/src/bindings.ts b/agents/hosting/src/bindings.ts
deleted file mode 100644
index f064c097..00000000
--- a/agents/hosting/src/bindings.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * The binding contract for a Worker hosting `@cloudflarebase/hosting`.
- *
- * `AssertHostingAgentEnv` is an identity type whose CONSTRAINT does the
- * checking: the consumer's entrypoint template instantiates it with their
- * generated `Env`, so a missing or mistyped binding fails their typecheck
- * with the field named, before anything deploys. Required vs optional here
- * describes what a correct deployment provides, not what the runtime happens
- * to tolerate.
- */
-
-// The namespace type is effectively invariant, so `never` and `unknown` each
-// fail one direction; `any` is what accepts every concrete class.
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-type AnyDurableObjectNamespace = DurableObjectNamespace;
-
-export interface HostingAgentBindings {
- /** The orchestrator Durable Object, one instance per project. */
- HostingAgent: AnyDurableObjectNamespace;
- /** Workers for Platforms dispatch namespace. Optional on purpose: WfP is a
- * paid add-on, and without it the agent answers hosting requests 503
- * instead of failing the whole install's deploy. */
- DISPATCH?: DispatchNamespace;
- /** Cloudflare account that owns the dispatch namespace (REST uploads). */
- CF_ACCOUNT_ID?: string;
- /** API token with Workers Scripts edit, scoped to namespace operations. */
- CF_HOSTING_API_TOKEN?: string;
- /** Namespace NAME for the REST API - the binding does not reveal it. */
- DISPATCH_NAMESPACE?: string;
- /** Apex domain apps serve under; empty disables the serving path. */
- HOSTING_DOMAIN?: string;
- /** Pass outbound parameters at dispatch - set only beside an `outbound`
- * block in the namespace binding, because they throw without one. */
- DISPATCH_OUTBOUND?: 'true';
- /** Where the bare apex redirects; without it the apex answers 404. */
- HOSTING_APEX_REDIRECT?: string;
- /** Record deploys and serve a stub page instead of calling Cloudflare -
- * local dev and e2e, where dynamic upload has no simulator. */
- HOSTING_STUB?: 'true';
- /** Master key for build secrets (AES-GCM at rest, src/crypto.ts). Optional:
- * without it build-secret writes answer 503 and everything else works. */
- HOSTING_MASTER_KEY?: string;
- /** Per-request analytics writes from the serve path. Optional: Analytics
- * Engine is an account-level opt-in (see wrangler.jsonc); unset, writes
- * are skipped and nothing else changes. */
- HOSTING_REQUESTS?: AnalyticsEngineDataset;
- /** Dataset NAME for the SQL read API - the binding does not reveal it. */
- WAE_DATASET?: string;
- /** Account Analytics Read token; with CF_ACCOUNT_ID it unlocks the
- * analytics tab's reads. Optional - without it the tab reports write-only. */
- CF_ANALYTICS_API_TOKEN?: string;
- /** Local-dev stand-in: with no read credentials, served requests land in
- * this D1 and the analytics endpoint reads them back. */
- LOCAL_ANALYTICS?: D1Database;
- SENTRY_DSN?: string;
- SENTRY_ENV?: string;
-
- /**
- * Serves the agent's own routes (`/overview`, `/apps/*`, `/deploys`, the
- * state-sync socket, `/internal/*`) over HTTP. Every one of them deploys
- * code, mints subdomains, or writes secrets, and none authenticate a
- * caller - set this only on a Worker with no public hostname, fronted by
- * something that authenticates operators.
- *
- * Unset (the default, and what `template/wrangler-fragment.jsonc` ships)
- * they 404. Serving deployed apps on the wildcard hostname is unaffected,
- * and your own code reaches the agent through the Durable Object namespace
- * either way.
- */
- EXPOSE_OPERATOR_API?: 'true';
-}
-
-export type AssertHostingAgentEnv = E;
diff --git a/agents/hosting/src/build-env.ts b/agents/hosting/src/build-env.ts
deleted file mode 100644
index ccd5ba80..00000000
--- a/agents/hosting/src/build-env.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * The build bundle's merge rule, extracted pure so a unit test can pin it:
- * the decrypted-bundle route is reachable only through a GitHub Actions OIDC
- * grant, which e2e cannot mint.
- *
- * Runtime env rides into builds because frameworks inline env at build time -
- * a SvelteKit `PUBLIC_*` value that exists only as a runtime binding never
- * reaches the client bundle. The build-specific stores stay separate and WIN
- * on a name collision: they exist precisely to say "at build time, this
- * instead".
- */
-export function mergeBuildEnv(
- runtime: { vars: Record; secrets: Record },
- build: { vars: Record; secrets: Record },
-): { vars: Record; secrets: Record } {
- return {
- vars: { ...runtime.vars, ...build.vars },
- secrets: { ...runtime.secrets, ...build.secrets },
- };
-}
diff --git a/agents/hosting/src/build-env.unit.test.ts b/agents/hosting/src/build-env.unit.test.ts
deleted file mode 100644
index c20e90f7..00000000
--- a/agents/hosting/src/build-env.unit.test.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import assert from 'node:assert/strict';
-import { test } from 'node:test';
-import { mergeBuildEnv } from './build-env';
-
-test('runtime env reaches the build, and build-specific values win', () => {
- const merged = mergeBuildEnv(
- {
- vars: { PUBLIC_API_URL: 'https://api.example', REGION: 'weur' },
- secrets: { DATABASE_URL: 'postgres://runtime' },
- },
- {
- vars: { PUBLIC_API_URL: 'https://staging-api.example' },
- secrets: { NPM_TOKEN: 'npm_x' },
- },
- );
- // The build-specific override wins; everything else flows through.
- assert.deepEqual(merged.vars, {
- PUBLIC_API_URL: 'https://staging-api.example',
- REGION: 'weur',
- });
- assert.deepEqual(merged.secrets, {
- DATABASE_URL: 'postgres://runtime',
- NPM_TOKEN: 'npm_x',
- });
-});
-
-test('empty stores merge to empty, not to errors', () => {
- assert.deepEqual(mergeBuildEnv({ vars: {}, secrets: {} }, { vars: {}, secrets: {} }), {
- vars: {},
- secrets: {},
- });
-});
diff --git a/agents/hosting/src/cloudflare.ts b/agents/hosting/src/cloudflare.ts
deleted file mode 100644
index db5c79a3..00000000
--- a/agents/hosting/src/cloudflare.ts
+++ /dev/null
@@ -1,447 +0,0 @@
-/**
- * Cloudflare REST driver for Workers for Platforms uploads. Kept apart from
- * the agent so the orchestration reads as the three documented steps: asset
- * upload session -> wanted buckets -> script put.
- *
- * Asset manifest hashes are salted with the project id (SHA-256 over
- * `\0`, truncated to the API's 32 hex chars): identical
- * assets are deduplicated within a namespace BY HASH, so unsalted hashes
- * would let one tenant probe another's content. The salt scopes dedup to the
- * project, which is the correctness/privacy trade the design doc picks.
- */
-
-export interface CfApi {
- accountId: string;
- apiToken: string;
- namespace: string;
-}
-
-export interface AssetFile {
- /** URL path the asset serves at, always starting with `/`. */
- path: string;
- bytes: Uint8Array;
- contentType: string;
-}
-
-export interface ModuleFile {
- /** Module file name (`index.js`); also its part name in the upload. */
- name: string;
- bytes: Uint8Array;
-}
-
-export interface PutScriptOptions {
- projectId: string;
- appName: string;
- /** Entry module name; undefined = assets-only Worker. */
- mainModule?: string;
- modules: ModuleFile[];
- compatibilityDate: string;
- compatibilityFlags: string[];
- /** Completion token from the asset session; undefined = no assets. */
- assetsJwt?: string;
- notFoundHandling?: 'single-page-application' | '404-page' | 'none';
- /** Injected beside the user's own vars so the SDK works out of the box. */
- vars: Record;
-}
-
-const API_BASE = 'https://api.cloudflare.com/client/v4';
-
-/**
- * Workers for Platforms runs namespaced scripts in UNTRUSTED mode, which is
- * what isolates one tenant from the next - and in that mode `caches.default`
- * is disabled: touching it throws `This Worker is not permitted to access the
- * default cache` (`request.cf` is absent for the same reason). Frameworks call
- * it unconditionally - SvelteKit's Cloudflare adapter opens EVERY request with
- * `caches.default.match(req)` - so an unmodified framework build answers 500
- * on every path the asset layer does not serve, which is every SSR route.
- *
- * The namespace-level `trusted_workers` flag would lift it by turning tenant
- * isolation off for every app at once, which is the opposite trade a managed
- * platform wants. So each module deploy gets a generated entry instead: it
- * imports the shim FIRST and re-exports the customer's entry. ES modules
- * evaluate imports depth-first in source order, so the shim runs before the
- * customer bundle's own body - which is what makes a module-scope capture
- * (`var s2 = caches.default`, exactly what the SvelteKit adapter emits) pick
- * up the neutralised methods rather than the forbidden ones.
- *
- * Named caches (`caches.open(...)`) are per-Worker in untrusted mode and keep
- * working; only the shared default cache is patched.
- */
-const CACHE_SHIM_SOURCE = `// Injected by Cloudflarebase: Workers for Platforms disables the default
-// cache for namespaced scripts, and frameworks call it unconditionally.
-try {
- const cache = globalThis.caches?.default;
- if (cache) {
- cache.match = async () => undefined;
- cache.put = async () => undefined;
- cache.delete = async () => false;
- }
-} catch {
- // A runtime that exposes no Cache API needs no shim.
-}
-`;
-
-/**
- * Returns the modules to upload and the entry to declare, with the cache shim
- * wrapped around the customer's entry. Assets-only deploys (no main module)
- * are returned untouched - there is no code to shim.
- *
- * Generated names carry a `__cfbase` prefix and a counter, so a customer file
- * of the same name can never be shadowed by ours.
- */
-export function wrapEntry(
- modules: ModuleFile[],
- mainModule: string | undefined,
-): { modules: ModuleFile[]; mainModule: string | undefined } {
- if (!mainModule) return { modules, mainModule };
-
- const taken = new Set(modules.map((module) => module.name));
- const free = (base: string) => {
- let name = `__cfbase_${base}.js`;
- for (let suffix = 2; taken.has(name); suffix += 1) name = `__cfbase_${base}_${suffix}.js`;
- taken.add(name);
- return name;
- };
- const shimName = free('runtime');
- const entryName = free('entry');
-
- // JSON.stringify the specifiers: module names arrive from a CLI upload or a
- // repository tarball, so they are never assumed to be quote-free.
- const target = JSON.stringify(`./${mainModule}`);
- const source = `import ${JSON.stringify(`./${shimName}`)};
-import * as entry from ${target};
-export * from ${target};
-export default entry.default;
-`;
-
- const encoder = new TextEncoder();
- return {
- modules: [
- ...modules,
- { name: shimName, bytes: encoder.encode(CACHE_SHIM_SOURCE) },
- { name: entryName, bytes: encoder.encode(source) },
- ],
- mainModule: entryName,
- };
-}
-
-interface CfEnvelope {
- success: boolean;
- errors?: { code?: number; message?: string }[];
- result?: T;
-}
-
-async function cfFetch(api: CfApi, path: string, init: RequestInit = {}): Promise {
- const response = await fetch(`${API_BASE}/accounts/${api.accountId}${path}`, {
- ...init,
- headers: {
- authorization: `Bearer ${api.apiToken}`,
- ...(init.headers ?? {}),
- },
- });
- const body = (await response.json().catch(() => null)) as CfEnvelope | null;
- if (!response.ok || !body?.success) {
- const detail =
- body?.errors?.map((error) => `${error.code ?? '?'}: ${error.message ?? '?'}`).join('; ') ??
- `HTTP ${response.status}`;
- throw new Error(`Cloudflare API ${path} failed - ${detail}`);
- }
- return body.result as T;
-}
-
-async function saltedHash(projectId: string, bytes: Uint8Array): Promise {
- const salt = new TextEncoder().encode(`${projectId}\0`);
- const input = new Uint8Array(salt.length + bytes.length);
- input.set(salt, 0);
- input.set(bytes, salt.length);
- const digest = await crypto.subtle.digest('SHA-256', input);
- return [...new Uint8Array(digest)]
- .map((byte) => byte.toString(16).padStart(2, '0'))
- .join('')
- .slice(0, 32);
-}
-
-function toBase64(bytes: Uint8Array): string {
- let binary = '';
- const chunk = 0x8000;
- for (let i = 0; i < bytes.length; i += chunk) {
- binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
- }
- return btoa(binary);
-}
-
-/**
- * Runs the asset upload session for a script: manifest -> wanted buckets ->
- * base64 uploads -> completion token. Returns undefined when there are no
- * assets at all. When every asset is already known to the namespace the
- * session's own jwt IS the completion token.
- */
-export async function uploadAssets(
- api: CfApi,
- scriptName: string,
- projectId: string,
- files: AssetFile[],
-): Promise {
- if (!files.length) return undefined;
-
- const byHash = new Map();
- const manifest: Record = {};
- for (const file of files) {
- const hash = await saltedHash(projectId, file.bytes);
- byHash.set(hash, file);
- manifest[file.path] = { hash, size: file.bytes.length };
- }
-
- const session = await cfFetch<{ jwt: string; buckets?: string[][] }>(
- api,
- `/workers/dispatch/namespaces/${api.namespace}/scripts/${scriptName}/assets-upload-session`,
- {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ manifest }),
- },
- );
-
- const buckets = session.buckets ?? [];
- if (!buckets.length) return session.jwt;
-
- let completion: string | undefined;
- for (const bucket of buckets) {
- const form = new FormData();
- for (const hash of bucket) {
- const file = byHash.get(hash);
- if (!file) throw new Error(`Cloudflare asked for an unknown asset hash ${hash}`);
- form.append(hash, new Blob([toBase64(file.bytes)], { type: file.contentType }), hash);
- }
- const response = await fetch(
- `${API_BASE}/accounts/${api.accountId}/workers/assets/upload?base64=true`,
- {
- method: 'POST',
- headers: { authorization: `Bearer ${session.jwt}` },
- body: form,
- },
- );
- const body = (await response.json().catch(() => null)) as CfEnvelope<{
- jwt?: string;
- }> | null;
- if (!response.ok || !body?.success) {
- throw new Error(`asset upload failed - HTTP ${response.status}`);
- }
- if (body.result?.jwt) completion = body.result.jwt;
- }
-
- if (!completion) throw new Error('asset upload finished without a completion token');
- return completion;
-}
-
-/** Uploads the script to the dispatch namespace. The script name IS the
- * subdomain; the `pid-` tag is what erase deletes by. */
-export async function putScript(
- api: CfApi,
- scriptName: string,
- options: PutScriptOptions,
-): Promise {
- const bindings: Record[] = Object.entries(options.vars).map(([name, text]) => ({
- type: 'plain_text',
- name,
- text,
- }));
- if (options.assetsJwt && options.mainModule) {
- bindings.push({ type: 'assets', name: 'ASSETS' });
- }
-
- // Every code deploy is entered through the generated shim entry.
- const wrapped = wrapEntry(options.modules, options.mainModule);
-
- const metadata: Record = {
- compatibility_date: options.compatibilityDate,
- compatibility_flags: options.compatibilityFlags,
- bindings,
- // `pid-` rather than `project:` - the scripts-by-tag filter grammar is
- // `?tags=:yes`, so a colon inside a tag collides with it.
- tags: [`pid-${options.projectId}`, `app-${options.appName}`],
- // Secrets are PATCHed separately; redeploys must never drop them.
- keep_bindings: ['secret_text'],
- };
- if (wrapped.mainModule) metadata.main_module = wrapped.mainModule;
- if (options.assetsJwt) {
- metadata.assets = {
- jwt: options.assetsJwt,
- config: {
- html_handling: 'auto-trailing-slash',
- not_found_handling:
- options.notFoundHandling ?? (options.mainModule ? 'none' : 'single-page-application'),
- },
- };
- }
-
- const form = new FormData();
- form.append(
- 'metadata',
- new Blob([JSON.stringify(metadata)], { type: 'application/json' }),
- 'metadata',
- );
- for (const module of wrapped.modules) {
- form.append(
- module.name,
- new Blob([module.bytes], { type: 'application/javascript+module' }),
- module.name,
- );
- }
-
- await cfFetch(api, `/workers/dispatch/namespaces/${api.namespace}/scripts/${scriptName}`, {
- method: 'PUT',
- body: form,
- });
-}
-
-/** Deletes one script from the namespace. 404-tolerant: an app whose only
- * deploys were stubbed (or that never deployed) has no script to remove. */
-export async function deleteScript(api: CfApi, scriptName: string): Promise {
- const response = await fetch(
- `${API_BASE}/accounts/${api.accountId}/workers/dispatch/namespaces/${api.namespace}/scripts/${scriptName}`,
- {
- method: 'DELETE',
- headers: { authorization: `Bearer ${api.apiToken}` },
- },
- );
- if (!response.ok && response.status !== 404) {
- throw new Error(`deleting script ${scriptName} failed - HTTP ${response.status}`);
- }
-}
-
-/** Bulk-delete every script carrying the project tag (the erase fan-in). */
-export async function deleteScriptsByTag(api: CfApi, tag: string): Promise {
- const response = await fetch(
- `${API_BASE}/accounts/${api.accountId}/workers/dispatch/namespaces/${api.namespace}/scripts?tags=${encodeURIComponent(`${tag}:yes`)}`,
- {
- method: 'DELETE',
- headers: { authorization: `Bearer ${api.apiToken}` },
- },
- );
- // 404 means nothing carried the tag - erase is idempotent.
- if (!response.ok && response.status !== 404) {
- throw new Error(`deleting scripts by tag failed - HTTP ${response.status}`);
- }
-}
-
-/** Sets one secret on a deployed script without touching anything else. */
-export async function patchScriptSecret(
- api: CfApi,
- scriptName: string,
- name: string,
- value: string,
-): Promise {
- const form = new FormData();
- form.append(
- 'settings',
- new Blob(
- [
- JSON.stringify({
- bindings: [{ type: 'secret_text', name, text: value }],
- // Keep everything the deploy put there - this call only adds.
- keep_bindings: ['plain_text', 'secret_text', 'assets'],
- keep_assets: true,
- }),
- ],
- { type: 'application/json' },
- ),
- 'settings',
- );
- await cfFetch(
- api,
- `/workers/dispatch/namespaces/${api.namespace}/scripts/${scriptName}/settings`,
- {
- method: 'PATCH',
- body: form,
- },
- );
-}
-
-/**
- * Replaces the script's WHOLE `plain_text` set in one settings PATCH; secrets
- * and assets stay. Callers pass the FULL effective set (platform over stored
- * over the last deploy's CLI vars) - replacement, not addition, is how a
- * deleted var actually disappears from the live script.
- */
-export async function patchScriptVars(
- api: CfApi,
- scriptName: string,
- vars: Record,
-): Promise {
- const form = new FormData();
- form.append(
- 'settings',
- new Blob(
- [
- JSON.stringify({
- bindings: Object.entries(vars).map(([name, text]) => ({
- type: 'plain_text',
- name,
- text,
- })),
- // Deliberately NOT 'plain_text': this call replaces that set.
- keep_bindings: ['secret_text', 'assets'],
- keep_assets: true,
- }),
- ],
- { type: 'application/json' },
- ),
- 'settings',
- );
- await cfFetch(
- api,
- `/workers/dispatch/namespaces/${api.namespace}/scripts/${scriptName}/settings`,
- {
- method: 'PATCH',
- body: form,
- },
- );
-}
-
-/** Deletes one secret from a namespaced script. 404-tolerant: a secret set
- * before name-tracking existed, or already gone, is not an error. */
-export async function deleteScriptSecret(
- api: CfApi,
- scriptName: string,
- name: string,
-): Promise {
- const response = await fetch(
- `${API_BASE}/accounts/${api.accountId}/workers/dispatch/namespaces/${api.namespace}/scripts/${scriptName}/secrets/${encodeURIComponent(name)}`,
- {
- method: 'DELETE',
- headers: { authorization: `Bearer ${api.apiToken}` },
- },
- );
- if (!response.ok && response.status !== 404) {
- throw new Error(`deleting secret ${name} failed - HTTP ${response.status}`);
- }
-}
-
-/**
- * The plain-text vars a deployed app is born with.
- *
- * The customer's declared vars come first and the platform's are applied over
- * them, deliberately: which project an app belongs to is the platform's fact,
- * not a value the app gets to disagree with. An app that could point
- * `CLOUDFLAREBASE_PROJECT` at another project would be reaching into another
- * tenant's data plane by editing a config file.
- *
- * `CLOUDFLAREBASE_PROJECT` + `CLOUDFLAREBASE_URL` are exactly the names the
- * admin SDKs resolve from, so `createDbAdmin()` with no arguments works inside
- * a hosted Worker and `db.remoteConfig()` needs no setup. `PROJECT_ID` predates
- * them and is kept because apps already deployed against it would otherwise
- * break for nothing.
- */
-export function deployVars(
- declared: Record | undefined,
- projectId: string,
- consoleOrigin: string,
-): Record {
- return {
- ...(declared ?? {}),
- CLOUDFLAREBASE_PROJECT: projectId,
- PROJECT_ID: projectId,
- CLOUDFLAREBASE_URL: consoleOrigin,
- };
-}
diff --git a/agents/hosting/src/cloudflare.unit.test.ts b/agents/hosting/src/cloudflare.unit.test.ts
deleted file mode 100644
index 98be1aca..00000000
--- a/agents/hosting/src/cloudflare.unit.test.ts
+++ /dev/null
@@ -1,168 +0,0 @@
-import assert from 'node:assert/strict';
-import test from 'node:test';
-import { deleteScriptSecret, deployVars, patchScriptVars, wrapEntry } from './cloudflare';
-
-/**
- * The shim entry only matters against a REAL dispatch namespace (untrusted
- * mode is where `caches.default` is forbidden), and the e2e stack runs on the
- * stub, which uploads nothing. So the shape of what we upload is pinned here.
- */
-
-const encoder = new TextEncoder();
-const decoder = new TextDecoder();
-const module = (name: string, source = 'export default {};') => ({
- name,
- bytes: encoder.encode(source),
-});
-const sourceOf = (modules: { name: string; bytes: Uint8Array }[], name: string) =>
- decoder.decode(modules.find((entry) => entry.name === name)!.bytes);
-
-test('an assets-only deploy is left alone', () => {
- const result = wrapEntry([], undefined);
- assert.deepEqual(result.modules, []);
- assert.equal(result.mainModule, undefined);
-});
-
-test('the customer entry is entered through the shim', () => {
- const result = wrapEntry([module('_worker.js')], '_worker.js');
-
- assert.equal(result.mainModule, '__cfbase_entry.js');
- assert.deepEqual(
- result.modules.map((entry) => entry.name),
- ['_worker.js', '__cfbase_runtime.js', '__cfbase_entry.js'],
- );
-
- const entry = sourceOf(result.modules, '__cfbase_entry.js');
- // Import order is the whole mechanism: the shim must be evaluated before
- // the customer bundle's module body captures `caches.default`.
- assert.ok(entry.indexOf('__cfbase_runtime.js') < entry.indexOf('_worker.js'));
- // Named exports (Durable Object classes) survive the wrap.
- assert.match(entry, /export \* from "\.\/_worker\.js"/);
- assert.match(entry, /export default entry\.default/);
-
- const shim = sourceOf(result.modules, '__cfbase_runtime.js');
- assert.match(shim, /caches\?\.default/);
- assert.match(shim, /cache\.match = async \(\) => undefined/);
-});
-
-test('a nested entry keeps resolving from the generated root entry', () => {
- const result = wrapEntry([module('dist/index.js'), module('dist/chunk.js')], 'dist/index.js');
- assert.match(sourceOf(result.modules, '__cfbase_entry.js'), /from "\.\/dist\/index\.js"/);
-});
-
-test('customer modules of the generated names are never shadowed', () => {
- const result = wrapEntry(
- [module('_worker.js'), module('__cfbase_runtime.js'), module('__cfbase_entry.js')],
- '_worker.js',
- );
-
- assert.equal(result.mainModule, '__cfbase_entry_2.js');
- // The customer's own files are uploaded untouched.
- assert.equal(sourceOf(result.modules, '__cfbase_runtime.js'), 'export default {};');
- assert.match(sourceOf(result.modules, '__cfbase_entry_2.js'), /__cfbase_runtime_2\.js/);
-});
-
-test('deployVars: the platform is authoritative about which project an app is', () => {
- const vars = deployVars({ API_BASE: 'https://api.example' }, 'acme', 'https://console.example');
- // The customer's own vars survive untouched.
- assert.equal(vars.API_BASE, 'https://api.example');
- // And the app is born knowing its project: these are exactly the names
- // createDbAdmin()/createStorageAdmin() resolve from, so a hosted Worker
- // self-configures with no setup step.
- assert.equal(vars.CLOUDFLAREBASE_PROJECT, 'acme');
- assert.equal(vars.CLOUDFLAREBASE_URL, 'https://console.example');
- // Kept for apps already deployed against it.
- assert.equal(vars.PROJECT_ID, 'acme');
-});
-
-test('deployVars: an app cannot point itself at another project', () => {
- // Which project an app belongs to is the platform's fact, not a value the
- // app gets to disagree with - otherwise editing a config file would reach
- // into another tenant's data plane.
- const vars = deployVars(
- { CLOUDFLAREBASE_PROJECT: 'someone-else', CLOUDFLAREBASE_URL: 'https://evil.example' },
- 'acme',
- 'https://console.example',
- );
- assert.equal(vars.CLOUDFLAREBASE_PROJECT, 'acme');
- assert.equal(vars.CLOUDFLAREBASE_URL, 'https://console.example');
-});
-
-/**
- * The settings PATCH shapes are pinned here for the same reason as the shim:
- * they only run against a REAL dispatch namespace, which the e2e stub never
- * dials. A captured fetch is the whole harness.
- */
-
-const API = { accountId: 'acct', apiToken: 'tok', namespace: 'ns' };
-
-async function withFetchCapture(
- response: Response,
- run: () => Promise,
-): Promise<{ url: string; init: RequestInit }> {
- const original = globalThis.fetch;
- let captured: { url: string; init: RequestInit } | undefined;
- globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
- captured = { url: String(input), init: init ?? {} };
- return response;
- }) as typeof fetch;
- try {
- await run();
- } finally {
- globalThis.fetch = original;
- }
- assert.ok(captured, 'fetch was never called');
- return captured!;
-}
-
-const ok = () => new Response(JSON.stringify({ success: true, result: {} }), { status: 200 });
-
-test('patchScriptVars replaces the plain_text set and keeps everything else', async () => {
- const { url, init } = await withFetchCapture(ok(), () =>
- patchScriptVars(API, 'site', { A: '1', B: '2' }),
- );
- assert.equal(
- url,
- 'https://api.cloudflare.com/client/v4/accounts/acct/workers/dispatch/namespaces/ns/scripts/site/settings',
- );
- assert.equal(init.method, 'PATCH');
- const part = (init.body as FormData).get('settings') as Blob;
- const settings = JSON.parse(await part.text()) as {
- bindings: { type: string; name: string; text: string }[];
- keep_bindings: string[];
- keep_assets: boolean;
- };
- assert.deepEqual(settings.bindings, [
- { type: 'plain_text', name: 'A', text: '1' },
- { type: 'plain_text', name: 'B', text: '2' },
- ]);
- // NOT 'plain_text': replacing the whole set is how a deleted var actually
- // disappears from the live script.
- assert.deepEqual(settings.keep_bindings, ['secret_text', 'assets']);
- assert.equal(settings.keep_assets, true);
-});
-
-test('deleteScriptSecret dials the per-script secret and tolerates 404', async () => {
- const { url, init } = await withFetchCapture(ok(), () =>
- deleteScriptSecret(API, 'site', 'API_KEY'),
- );
- assert.equal(
- url,
- 'https://api.cloudflare.com/client/v4/accounts/acct/workers/dispatch/namespaces/ns/scripts/site/secrets/API_KEY',
- );
- assert.equal(init.method, 'DELETE');
-
- // Already gone is not an error - the row deletion must proceed.
- await withFetchCapture(new Response('', { status: 404 }), () =>
- deleteScriptSecret(API, 'site', 'API_KEY'),
- );
-
- // Anything else is.
- const original = globalThis.fetch;
- globalThis.fetch = (async () => new Response('', { status: 500 })) as typeof fetch;
- try {
- await assert.rejects(deleteScriptSecret(API, 'site', 'API_KEY'), /HTTP 500/);
- } finally {
- globalThis.fetch = original;
- }
-});
diff --git a/agents/hosting/src/crypto.ts b/agents/hosting/src/crypto.ts
deleted file mode 100644
index 58bba58e..00000000
--- a/agents/hosting/src/crypto.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * Encryption at rest for build secrets - the one place in Cloudflarebase where
- * a secret VALUE must be stored and later recovered (the GitHub Actions
- * runner fetches it at build time), so hashing and write-through both fall
- * short. Everything else stays hashed or write-through; reach for this only
- * when a value genuinely has to come back out.
- *
- * AES-256-GCM under a master key derived from the `HOSTING_MASTER_KEY`
- * wrangler secret. The stored format is versioned (`v1::`)
- * so a key or algorithm rotation can introduce `v2` while still reading old
- * rows. Ciphertexts are bound to their row via GCM's additional data
- * (`\0`): a value copied between rows fails authentication
- * instead of decrypting under the wrong name.
- */
-
-const VERSION = 'v1';
-
-function toBase64Url(bytes: Uint8Array): string {
- let binary = '';
- for (const byte of bytes) binary += String.fromCharCode(byte);
- return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
-}
-
-function fromBase64Url(value: string): Uint8Array {
- const padded = value.replaceAll('-', '+').replaceAll('_', '/');
- const binary = atob(padded + '='.repeat((4 - (padded.length % 4)) % 4));
- return Uint8Array.from(binary, (char) => char.charCodeAt(0));
-}
-
-/**
- * SHA-256 of the secret string as the raw AES-256 key. The env secret is
- * high-entropy operator input, not a password, so a KDF with salt/stretching
- * would add parameters without adding security.
- */
-export async function importMasterKey(secret: string): Promise {
- const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(secret));
- return crypto.subtle.importKey('raw', digest, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
-}
-
-/** Encrypts to `v1::`, fresh random
- * 12-byte IV per call - two encryptions of the same value never match. */
-export async function encryptValue(
- key: CryptoKey,
- plaintext: string,
- aad: string,
-): Promise {
- const iv = crypto.getRandomValues(new Uint8Array(12));
- const ciphertext = await crypto.subtle.encrypt(
- { name: 'AES-GCM', iv, additionalData: new TextEncoder().encode(aad) },
- key,
- new TextEncoder().encode(plaintext),
- );
- return `${VERSION}:${toBase64Url(iv)}:${toBase64Url(new Uint8Array(ciphertext))}`;
-}
-
-/** Throws on an unknown version, malformed input, a wrong key, a wrong AAD,
- * or any bit of tampering - GCM authenticates before it decrypts. */
-export async function decryptValue(key: CryptoKey, stored: string, aad: string): Promise {
- const [version, iv, ciphertext, ...rest] = stored.split(':');
- if (version !== VERSION || !iv || !ciphertext || rest.length) {
- throw new Error(`unsupported ciphertext format "${stored.slice(0, 8)}"`);
- }
- const plaintext = await crypto.subtle.decrypt(
- {
- name: 'AES-GCM',
- iv: fromBase64Url(iv),
- additionalData: new TextEncoder().encode(aad),
- },
- key,
- fromBase64Url(ciphertext),
- );
- return new TextDecoder().decode(plaintext);
-}
diff --git a/agents/hosting/src/crypto.unit.test.ts b/agents/hosting/src/crypto.unit.test.ts
deleted file mode 100644
index 2dbe43ab..00000000
--- a/agents/hosting/src/crypto.unit.test.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import assert from 'node:assert/strict';
-import test from 'node:test';
-import { decryptValue, encryptValue, importMasterKey } from './crypto';
-
-/**
- * Build secrets are the repo's only encryption at rest, so the contract is
- * pinned end to end: round-trips work, everything else - tampering, a wrong
- * key, a value copied to another row, an unknown format - throws.
- */
-
-const AAD = 'site\0API_KEY';
-
-test('round-trips a value', async () => {
- const key = await importMasterKey('a master key of respectable length!!');
- const stored = await encryptValue(key, 'hunter2', AAD);
- assert.match(stored, /^v1:[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$/);
- assert.equal(await decryptValue(key, stored, AAD), 'hunter2');
-});
-
-test('the same plaintext never encrypts to the same ciphertext', async () => {
- const key = await importMasterKey('a master key of respectable length!!');
- const first = await encryptValue(key, 'hunter2', AAD);
- const second = await encryptValue(key, 'hunter2', AAD);
- assert.notEqual(first, second); // fresh IV per call
- assert.equal(await decryptValue(key, second, AAD), 'hunter2');
-});
-
-test('round-trips edge-case values', async () => {
- const key = await importMasterKey('a master key of respectable length!!');
- for (const value of ['', 'x'.repeat(5000), 'newline\\n literal and unïcøde ✨']) {
- const stored = await encryptValue(key, value, AAD);
- assert.equal(await decryptValue(key, stored, AAD), value);
- }
-});
-
-test('a flipped ciphertext bit fails authentication', async () => {
- const key = await importMasterKey('a master key of respectable length!!');
- const stored = await encryptValue(key, 'hunter2', AAD);
- const tampered = stored.slice(0, -1) + (stored.endsWith('A') ? 'B' : 'A');
- await assert.rejects(decryptValue(key, tampered, AAD));
-});
-
-test('a ciphertext copied to another row fails authentication', async () => {
- const key = await importMasterKey('a master key of respectable length!!');
- const stored = await encryptValue(key, 'hunter2', AAD);
- await assert.rejects(decryptValue(key, stored, 'site\0OTHER_NAME'));
- await assert.rejects(decryptValue(key, stored, 'other-app\0API_KEY'));
-});
-
-test('the wrong key fails authentication', async () => {
- const key = await importMasterKey('a master key of respectable length!!');
- const other = await importMasterKey('a different master key entirely!!!!!');
- const stored = await encryptValue(key, 'hunter2', AAD);
- await assert.rejects(decryptValue(other, stored, AAD));
-});
-
-test('unknown versions and malformed inputs are refused, not guessed at', async () => {
- const key = await importMasterKey('a master key of respectable length!!');
- const stored = await encryptValue(key, 'hunter2', AAD);
- const [, iv, ciphertext] = stored.split(':');
- for (const bad of [
- `v9:${iv}:${ciphertext}`, // future version
- `${stored}:extra`, // trailing segment
- 'v1:only-two',
- 'v1',
- '',
- 'plaintext-that-was-never-encrypted',
- ]) {
- await assert.rejects(decryptValue(key, bad, AAD), /unsupported ciphertext|error/i);
- }
-});
diff --git a/agents/hosting/src/db/schema.ts b/agents/hosting/src/db/schema.ts
deleted file mode 100644
index 2a958bec..00000000
--- a/agents/hosting/src/db/schema.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
-
-/**
- * HostingAgent storage: the per-project app registry, deploy history, and
- * per-app environment. Control plane, not data plane - the code and assets
- * live in the dispatch namespace; these rows are what the dashboard lists and
- * what erase walks.
- */
-
-/** Apps this project has deployed. `subdomain` is pushed by the console after
- * it resolves the claim (the control plane owns the global namespace); the
- * agent never derives one. */
-export const apps = sqliteTable('apps', {
- name: text('name').primaryKey(),
- subdomain: text('subdomain').notNull(),
- createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
- lastDeployAt: integer('last_deploy_at', { mode: 'timestamp_ms' }),
- deployCount: integer('deploy_count').notNull().default(0),
- /** JSON snapshot of the CLI's `meta.vars` from the most recent deploy.
- * Editing a var in the console replaces the script's WHOLE plain_text set,
- * so the patch must reconstitute platform > stored > CLI without the CLI
- * present - without this snapshot a console edit would silently drop every
- * CLI-declared var from the live script. */
- lastDeployVars: text('last_deploy_vars'),
-});
-
-export type AppRecord = typeof apps.$inferSelect;
-
-export const deploys = sqliteTable('deploys', {
- id: text('id').primaryKey(),
- appName: text('app_name').notNull(),
- subdomain: text('subdomain').notNull(),
- /** `live` = uploaded to the namespace; `stub` = recorded under HOSTING_STUB. */
- status: text('status').$type<'live' | 'stub'>().notNull(),
- /** Whether the deploy carried a Worker module (vs assets-only). */
- hasWorker: integer('has_worker', { mode: 'boolean' }).notNull(),
- assetCount: integer('asset_count').notNull(),
- assetBytes: integer('asset_bytes').notNull(),
- moduleBytes: integer('module_bytes').notNull(),
- createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
-});
-
-export type DeployRecord = typeof deploys.$inferSelect;
-
-/** Runtime plain-text vars, stored per app: applied as `plain_text` bindings
- * on every deploy and patched onto the live script when edited. Plaintext at
- * rest on purpose - they upload as plain_text bindings anyway, and DO storage
- * is the trust boundary (the storage agent's signing secret precedent). */
-export const appVars = sqliteTable(
- 'app_vars',
- {
- appName: text('app_name').notNull(),
- name: text('name').notNull(),
- value: text('value').notNull(),
- createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
- updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
- },
- (table) => [primaryKey({ columns: [table.appName, table.name] })],
-);
-
-export type AppVarRecord = typeof appVars.$inferSelect;
-
-/** Runtime secrets. The VALUE is write-through to Cloudflare's script
- * settings (that is what binds it at runtime); `ciphertext` additionally
- * holds it AES-GCM under `HOSTING_MASTER_KEY` so builds can receive it too -
- * frameworks inline env at build time, and a secret that exists only at
- * runtime never reaches them. Null on installs without the master key (and
- * on rows written before this column existed): the secret still works at
- * runtime, it just cannot ride into builds. */
-export const appSecrets = sqliteTable(
- 'app_secrets',
- {
- appName: text('app_name').notNull(),
- name: text('name').notNull(),
- ciphertext: text('ciphertext'),
- createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
- updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
- },
- (table) => [primaryKey({ columns: [table.appName, table.name] })],
-);
-
-export type AppSecretRecord = typeof appSecrets.$inferSelect;
-
-/** Build-time vars, fetched by the GitHub Actions workflow before the build
- * step. Connection-scoped: they live in the ROOT project's agent only, and
- * every branch build of the connection reads the same set. */
-export const buildVars = sqliteTable(
- 'build_vars',
- {
- appName: text('app_name').notNull(),
- name: text('name').notNull(),
- value: text('value').notNull(),
- createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
- updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
- },
- (table) => [primaryKey({ columns: [table.appName, table.name] })],
-);
-
-export type BuildVarRecord = typeof buildVars.$inferSelect;
-
-/** Build-time secrets - the one value Cloudflarebase stores AND must recover
- * (the runner fetches it at build time), so it is AES-GCM ciphertext under
- * `HOSTING_MASTER_KEY` (src/crypto.ts), never plaintext at rest. */
-export const buildSecrets = sqliteTable(
- 'build_secrets',
- {
- appName: text('app_name').notNull(),
- name: text('name').notNull(),
- ciphertext: text('ciphertext').notNull(),
- createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
- updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
- },
- (table) => [primaryKey({ columns: [table.appName, table.name] })],
-);
-
-export type BuildSecretRecord = typeof buildSecrets.$inferSelect;
diff --git a/agents/hosting/src/env.d.ts b/agents/hosting/src/env.d.ts
deleted file mode 100644
index 16e9c0bf..00000000
--- a/agents/hosting/src/env.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// Optional secrets set via `wrangler secret put` - not present in wrangler.jsonc
-// vars, so they are augmented here instead of hand-editing the generated
-// worker-configuration.d.ts (the auth agent's precedent).
-interface Env {
- /** Account Analytics Read token: with CF_ACCOUNT_ID it unlocks the
- * analytics tab's SQL reads. Writes need no credentials. */
- CF_ANALYTICS_API_TOKEN?: string;
-}
diff --git a/agents/hosting/src/index.ts b/agents/hosting/src/index.ts
deleted file mode 100644
index 3d5a2b68..00000000
--- a/agents/hosting/src/index.ts
+++ /dev/null
@@ -1,424 +0,0 @@
-import * as Sentry from '@sentry/cloudflare';
-import { getAgentByName, routeAgentRequest } from 'agents';
-import { WorkerEntrypoint } from 'cloudflare:workers';
-import { HostingAgent as HostingAgentBase } from './agent';
-import { gateOperatorRoutes } from './route-access';
-import { appNameSchema, projectIdSchema, subdomainSchema } from './schemas';
-
-/**
- * hosting-agent worker - two roles, decided by hostname (the LiveShard
- * precedent):
- *
- * - **Serving**: requests on the `*.cfbase.dev` wildcard route take the first
- * host label and `env.DISPATCH.get(hostLabel)` - zero lookup, because the
- * script name IS the full subdomain. Dispatch never parses the subdomain
- * into app and branch; the control plane's claims table resolved that at
- * deploy time. Unclaimed subdomain = no script = branded 404.
- * - **Agent surface** on every other hostname: `/health`, the
- * service-binding-only `/internal/*` routes (erase fan-in and the console's
- * claim push - outside /agents/* on purpose: this Worker has no public
- * route beyond the wildcard, and the serve branch swallows every wildcard
- * request before dispatch gets here, so /internal is reachable only over
- * the dashboard's service binding), and `routeAgentRequest` for the
- * operator surface. Never with `cors: true`.
- */
-
-const sentryOptions = (env: Env) => ({
- dsn: env.SENTRY_DSN,
- environment: env.SENTRY_ENV,
- tracesSampleRate: 0.1,
- enableRpcTracePropagation: true,
-});
-
-const DO_RESET_PATTERN = /abort\(\) to reset|durable object reset/i;
-function isDurableObjectReset(error: unknown): boolean {
- return DO_RESET_PATTERN.test(error instanceof Error ? error.message : String(error));
-}
-
-/**
- * Local-dev stand-in for Analytics Engine, ensured once per isolate. Keyed on
- * the binding (the control-plane getDb precedent) so tests that swap
- * databases still bootstrap the new one; a failure clears the cache so the
- * next request retries instead of wedging analytics for the isolate.
- */
-const localAnalyticsReady = new WeakMap>();
-function ensureHostingRequestsTable(db: D1Database): Promise {
- let pending = localAnalyticsReady.get(db);
- if (!pending) {
- pending = db
- .batch([
- db.prepare(
- `CREATE TABLE IF NOT EXISTS hosting_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, subdomain TEXT NOT NULL, timestamp INTEGER NOT NULL, status INTEGER NOT NULL, duration_ms REAL NOT NULL)`,
- ),
- db.prepare(
- `CREATE INDEX IF NOT EXISTS hosting_requests_sub_time ON hosting_requests(subdomain, timestamp)`,
- ),
- ])
- .then(() => undefined)
- .catch((cause: unknown) => {
- localAnalyticsReady.delete(db);
- throw cause;
- });
- localAnalyticsReady.set(db, pending);
- }
- return pending;
-}
-
-async function drainUnusedBody(request: Request): Promise {
- try {
- if (request.body && !request.bodyUsed) await request.body.cancel();
- } catch {
- // draining is belt-and-braces, never a failure
- }
-}
-
-function brandedNotFound(subdomain: string, domain: string): Response {
- return new Response(
- `
-
-
-
-
- Nothing here yet
-
-
-
-
- Nothing is deployed at ${subdomain}.${domain}
- Claim it with cloudflarebase init and ship with cloudflarebase deploy.
- cloudflarebase.com
-
-
-`,
- { status: 404, headers: { 'content-type': 'text/html; charset=utf-8' } },
- );
-}
-
-/** What the dispatch path serves under HOSTING_STUB - local dev and e2e have
- * no namespace, so the contract test asserts this page instead. */
-function stubPage(subdomain: string, domain: string): Response {
- return new Response(
- `
-
- ${subdomain}.${domain} (stub)
-
- cfbase.dev stub
- This deployment records apps without a dispatch namespace; ${subdomain}.${domain} would serve your deploy in production.
-
-`,
- { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } },
- );
-}
-
-class HostingService extends WorkerEntrypoint {
- override async fetch(request: Request): Promise {
- const response = await this.dispatch(request);
- await drainUnusedBody(request);
- return response;
- }
-
- private async dispatch(request: Request): Promise {
- const url = new URL(request.url);
-
- // Serving first: on a wildcard host EVERY path belongs to the user's
- // app - our own surfaces exist only on non-serving hostnames.
- const served = await this.serveIfAppHost(request, url);
- if (served) {
- // Reported as the DEPLOYED APP failing, never as this worker failing:
- // a customer script that throws is their bug, and titling it
- // "Hosting agent returned HTTP 500" sends every one of them to us
- // wearing our own name. Kept (rather than dropped) because it is the
- // only signal a whole class of apps is broken - which is how the
- // Workers for Platforms default-cache restriction was found - and
- // kept under ONE title so a hundred broken apps stay one issue with a
- // subdomain tag, not a hundred issues.
- await this.reportDispatchError(request, url, served);
- return served;
- }
-
- if (url.pathname === '/health') {
- return Response.json({ service: 'hosting-agent', status: 'ok' });
- }
-
- // Past the serving path, every surface this worker has deploys code,
- // mints subdomains, or writes secrets, and none of them authenticate a
- // caller: the console guard is the gate, and the package cannot assume
- // one exists - the documented consumer install mounts this handler on
- // their own PUBLIC Worker. Closed unless the deployment says otherwise
- // (src/route-access.ts); a no-op wherever EXPOSE_OPERATOR_API is set.
- const gated = gateOperatorRoutes(url, this.env);
- if (gated) return gated;
-
- const erase = url.pathname.match(/^\/internal\/projects\/([^/]+)$/);
- if (erase && request.method === 'DELETE') {
- const projectId = decodeURIComponent(erase[1]);
- if (!projectIdSchema.safeParse(projectId).success) {
- return Response.json({ error: 'invalid project id' }, { status: 400 });
- }
- const agent = await getAgentByName(this.env.HostingAgent, projectId);
- try {
- await agent.destroy();
- } catch (error) {
- // The agent's own deferred abort can outrace the RPC reply in
- // production; scripts and storage are already gone by then.
- if (!isDurableObjectReset(error)) throw error;
- }
- return Response.json({ erased: true });
- }
-
- // The console's claim push: it resolved the subdomain in the control
- // plane and records it here so the deploy route can never be steered to
- // a subdomain this project does not own. Service-binding-only by
- // topology, like the erase route.
- const link = url.pathname.match(/^\/internal\/projects\/([^/]+)\/apps\/([^/]+)$/);
- if (link && (request.method === 'PUT' || request.method === 'DELETE')) {
- const projectId = decodeURIComponent(link[1]);
- const appName = decodeURIComponent(link[2]);
- if (
- !projectIdSchema.safeParse(projectId).success ||
- !appNameSchema.safeParse(appName).success
- ) {
- return Response.json({ error: 'invalid claim push' }, { status: 400 });
- }
- const agent = await getAgentByName(this.env.HostingAgent, projectId);
- // DELETE is the app erase: script, deploy history, and row - the
- // console releases the subdomain claim only after this answers ok.
- if (request.method === 'DELETE') {
- const result = await agent.eraseApp(appName);
- if ('error' in result) return Response.json(result, { status: 400 });
- return Response.json(result);
- }
- const body = (await request.json().catch(() => null)) as { subdomain?: string } | null;
- if (!subdomainSchema.safeParse(body?.subdomain).success) {
- return Response.json({ error: 'invalid claim push' }, { status: 400 });
- }
- const result = await agent.registerApp(appName, body!.subdomain!);
- if ('error' in result) return Response.json(result, { status: 409 });
- return Response.json(result);
- }
-
- // The build-env fetch: the console verified a GitHub Actions OIDC grant
- // and relays the DECRYPTED bundle to the runner. Values only ever
- // transit the dashboard's service binding - the operator HTTP surface
- // answers names only. Service-binding-only, like the routes above.
- const buildEnv = url.pathname.match(
- /^\/internal\/projects\/([^/]+)\/apps\/([^/]+)\/build-env$/,
- );
- if (buildEnv && request.method === 'GET') {
- const projectId = decodeURIComponent(buildEnv[1]);
- const appName = decodeURIComponent(buildEnv[2]);
- if (
- !projectIdSchema.safeParse(projectId).success ||
- !appNameSchema.safeParse(appName).success
- ) {
- return Response.json({ error: 'invalid build-env request' }, { status: 400 });
- }
- const agent = await getAgentByName(this.env.HostingAgent, projectId);
- const result = await agent.buildEnvBundle(appName);
- if ('error' in result) {
- return Response.json({ error: result.error }, { status: result.status });
- }
- return Response.json(result);
- }
-
- // Direct deploys (Phase B): a push webhook the console verified, whose
- // repository needs no build. The console resolved the claim and the
- // download URL first, so the agent receives a plain URL and never holds
- // a GitHub credential. Service-binding-only, like the routes above.
- const gitDeploy = url.pathname.match(
- /^\/internal\/projects\/([^/]+)\/apps\/([^/]+)\/git-deploy$/,
- );
- if (gitDeploy && request.method === 'POST') {
- const projectId = decodeURIComponent(gitDeploy[1]);
- const appName = decodeURIComponent(gitDeploy[2]);
- const body = (await request.json().catch(() => null)) as {
- tarballUrl?: string;
- assetsDir?: string;
- } | null;
- if (
- !projectIdSchema.safeParse(projectId).success ||
- !appNameSchema.safeParse(appName).success ||
- typeof body?.tarballUrl !== 'string' ||
- // Only GitHub's own download hosts - this URL is fetched server side.
- !/^https:\/\/(codeload|api)\.github\.com\//.test(body.tarballUrl)
- ) {
- return Response.json({ error: 'invalid git deploy' }, { status: 400 });
- }
- const agent = await getAgentByName(this.env.HostingAgent, projectId);
- const result = await agent.gitDeploy({
- appName,
- tarballUrl: body.tarballUrl,
- assetsDir: typeof body.assetsDir === 'string' ? body.assetsDir : '',
- origin: url.origin,
- });
- return new Response(result.json, {
- status: result.status,
- headers: { 'content-type': 'application/json' },
- });
- }
-
- const response =
- (await routeAgentRequest(request, this.env)) ??
- Response.json({ error: 'not found' }, { status: 404 });
-
- await this.reportServerError(request, url, response);
- return response;
- }
-
- /**
- * The dispatch path. Null when the hostname is not a serving host - agent
- * traffic (service binding, dev ports) falls through to the surfaces
- * above. Under HOSTING_STUB an `x-cfbase-host` header stands in for the
- * Host header, because local workerd is dialled by port, not subdomain;
- * the header is ignored everywhere else.
- */
- private async serveIfAppHost(request: Request, url: URL): Promise {
- const domain = this.env.HOSTING_DOMAIN;
- if (!domain) return null;
-
- let host = url.hostname;
- if (this.env.HOSTING_STUB === 'true') {
- const override = request.headers.get('x-cfbase-host');
- if (override) host = override.split(':')[0];
- }
-
- if (host === domain) {
- const apex = this.env.HOSTING_APEX_REDIRECT;
- return apex ? Response.redirect(apex, 302) : brandedNotFound('www', domain);
- }
- if (!host.endsWith(`.${domain}`)) return null;
-
- const label = host.slice(0, -(domain.length + 1));
- // Universal SSL covers exactly one wildcard level; anything deeper (or
- // charset-invalid) can never be a claimed subdomain.
- if (label.includes('.') || !subdomainSchema.safeParse(label).success) {
- return brandedNotFound(label.replaceAll('.', '-'), domain);
- }
-
- if (this.env.HOSTING_STUB === 'true') {
- // The stub records too - that is what makes the analytics tab real in
- // local dev and e2e.
- this.recordRequest(label, 200, 0);
- return stubPage(label, domain);
- }
- if (!this.env.DISPATCH) {
- return brandedNotFound(label, domain);
- }
-
- const options: DynamicDispatchOptions = {
- // Fixed v1 caps, applied at dispatch (plan-driven in Phase C - which
- // then changes THIS call, never the deployed scripts).
- limits: { cpuMs: 50, subRequests: 50 },
- };
- if (this.env.DISPATCH_OUTBOUND === 'true') {
- // The subdomain rides the outbound parameters - joinable to a project
- // offline via the claims table, so the serve path stays zero-lookup.
- options.outbound = { subdomain: label };
- }
-
- const startedAt = Date.now();
- try {
- const worker = this.env.DISPATCH.get(label, {}, options);
- const response = await worker.fetch(request);
- this.recordRequest(label, response.status, Date.now() - startedAt);
- return response;
- } catch (error) {
- if (/worker not found/i.test(error instanceof Error ? error.message : String(error))) {
- // Unclaimed subdomains are deliberately NOT recorded - a scanner
- // walking names would otherwise write a data point per probe.
- return brandedNotFound(label, domain);
- }
- throw error;
- }
- }
-
- /**
- * One data point per served request: subdomain (the index - it IS the
- * script name, so the DO joins it back to an app for free), status, and
- * duration. Never allowed to affect the response; with no Analytics Engine
- * read credentials the local D1 stand-in records instead, which is what
- * the local/e2e stacks read back.
- */
- private recordRequest(subdomain: string, status: number, durationMs: number): void {
- try {
- this.env.HOSTING_REQUESTS?.writeDataPoint({
- indexes: [subdomain],
- blobs: [String(status)],
- doubles: [durationMs],
- });
- } catch {
- // metrics failure must never affect the serve path
- }
- const readable =
- this.env.CF_ACCOUNT_ID && this.env.CF_ANALYTICS_API_TOKEN && this.env.WAE_DATASET;
- const local = this.env.LOCAL_ANALYTICS;
- if (!readable && local) {
- this.ctx.waitUntil(
- ensureHostingRequestsTable(local)
- .then(() =>
- local
- .prepare(
- `INSERT INTO hosting_requests (subdomain, timestamp, status, duration_ms) VALUES (?, ?, ?, ?)`,
- )
- .bind(subdomain, Date.now(), status, durationMs)
- .run(),
- )
- .then(() => undefined)
- .catch(() => undefined),
- );
- }
- }
-
- /** Records a 5xx a DEPLOYED APP answered with. Never replaces the response. */
- private async reportDispatchError(request: Request, url: URL, response: Response): Promise {
- if (response.status < 500) return;
- try {
- const body = (await response.clone().text()).slice(0, 512);
- Sentry.captureMessage('Deployed app returned HTTP 5xx', {
- level: 'warning',
- tags: {
- 'http.method': request.method,
- 'http.status_code': response.status,
- subdomain: url.hostname.split('.')[0],
- },
- contexts: { response: { body } },
- extra: { pathname: url.pathname },
- });
- } catch {
- // reporting must never replace the response
- }
- }
-
- /** Records any 5xx leaving this worker. Never replaces the response. */
- private async reportServerError(request: Request, url: URL, response: Response): Promise {
- if (response.status < 500) return;
- try {
- const body = (await response.clone().text()).slice(0, 2048);
- Sentry.captureMessage(`Hosting agent returned HTTP ${response.status}`, {
- level: 'error',
- tags: { 'http.method': request.method, 'http.status_code': response.status },
- contexts: {
- response: { body, contentType: response.headers.get('content-type') ?? '' },
- },
- extra: { pathname: url.pathname },
- });
- } catch {
- // reporting must never replace the response
- }
- }
-}
-
-export const HostingAgent = Sentry.instrumentDurableObjectWithSentry(
- sentryOptions,
- HostingAgentBase,
-);
-
-export type { HostingAgentState, HostingAppSummary, HostingDeploySummary } from './agent';
-export type { AssertHostingAgentEnv, HostingAgentBindings } from './bindings';
-
-export default Sentry.withSentry(sentryOptions, HostingService);
diff --git a/agents/hosting/src/migrations.ts b/agents/hosting/src/migrations.ts
deleted file mode 100644
index 067676ca..00000000
--- a/agents/hosting/src/migrations.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-// Generated by scripts/generate-migrations.mjs - do not edit.
-//
-// The SQL is inlined rather than imported from .sql files so this agent can be
-// consumed as a plain npm dependency, with no Wrangler Text-module rule needed
-// in the consumer's config. Regenerate with `npm run migrations`.
-
-const journal = {
- version: '7',
- dialect: 'sqlite',
- entries: [
- {
- idx: 0,
- version: '6',
- when: 1786556374566,
- tag: '0000_red_la_nuit',
- breakpoints: true,
- },
- {
- idx: 1,
- version: '6',
- when: 1787197325964,
- tag: '0001_slippery_prism',
- breakpoints: true,
- },
- {
- idx: 2,
- version: '6',
- when: 1787253423135,
- tag: '0002_amazing_kronos',
- breakpoints: true,
- },
- ],
-};
-
-const m0000 = `CREATE TABLE \`apps\` (
- \`name\` text PRIMARY KEY NOT NULL,
- \`subdomain\` text NOT NULL,
- \`created_at\` integer NOT NULL,
- \`last_deploy_at\` integer,
- \`deploy_count\` integer DEFAULT 0 NOT NULL
-);
---> statement-breakpoint
-CREATE TABLE \`deploys\` (
- \`id\` text PRIMARY KEY NOT NULL,
- \`app_name\` text NOT NULL,
- \`subdomain\` text NOT NULL,
- \`status\` text NOT NULL,
- \`has_worker\` integer NOT NULL,
- \`asset_count\` integer NOT NULL,
- \`asset_bytes\` integer NOT NULL,
- \`module_bytes\` integer NOT NULL,
- \`created_at\` integer NOT NULL
-);
-`;
-
-const m0001 = `CREATE TABLE \`app_secrets\` (
- \`app_name\` text NOT NULL,
- \`name\` text NOT NULL,
- \`created_at\` integer NOT NULL,
- \`updated_at\` integer NOT NULL,
- PRIMARY KEY(\`app_name\`, \`name\`)
-);
---> statement-breakpoint
-CREATE TABLE \`app_vars\` (
- \`app_name\` text NOT NULL,
- \`name\` text NOT NULL,
- \`value\` text NOT NULL,
- \`created_at\` integer NOT NULL,
- \`updated_at\` integer NOT NULL,
- PRIMARY KEY(\`app_name\`, \`name\`)
-);
---> statement-breakpoint
-CREATE TABLE \`build_secrets\` (
- \`app_name\` text NOT NULL,
- \`name\` text NOT NULL,
- \`ciphertext\` text NOT NULL,
- \`created_at\` integer NOT NULL,
- \`updated_at\` integer NOT NULL,
- PRIMARY KEY(\`app_name\`, \`name\`)
-);
---> statement-breakpoint
-CREATE TABLE \`build_vars\` (
- \`app_name\` text NOT NULL,
- \`name\` text NOT NULL,
- \`value\` text NOT NULL,
- \`created_at\` integer NOT NULL,
- \`updated_at\` integer NOT NULL,
- PRIMARY KEY(\`app_name\`, \`name\`)
-);
---> statement-breakpoint
-ALTER TABLE \`apps\` ADD \`last_deploy_vars\` text;`;
-
-const m0002 = `ALTER TABLE \`app_secrets\` ADD \`ciphertext\` text;`;
-
-export default {
- journal,
- migrations: {
- m0000,
- m0001,
- m0002,
- },
-};
diff --git a/agents/hosting/src/outbound.ts b/agents/hosting/src/outbound.ts
deleted file mode 100644
index 1524b0d7..00000000
--- a/agents/hosting/src/outbound.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Outbound Worker for the dispatch namespaces (wrangler.outbound.jsonc).
- *
- * Every `fetch()` a hosted app makes is re-dispatched here with the
- * parameters the serve path passed (`subdomain` - joinable to a project via
- * the control plane's claims table, so the serve path stays zero-lookup).
- * v1 forwards untouched; Phase C hooks egress metering and policy blocking
- * into exactly this spot without touching user scripts.
- */
-
-interface OutboundEnv {
- /** Dispatch parameter: the subdomain (= script name) that made the fetch. */
- subdomain?: string;
-}
-
-export default {
- async fetch(request: Request, _env: OutboundEnv): Promise {
- return fetch(request);
- },
-} satisfies ExportedHandler;
diff --git a/agents/hosting/src/route-access.ts b/agents/hosting/src/route-access.ts
deleted file mode 100644
index e6aaf869..00000000
--- a/agents/hosting/src/route-access.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-/**
- * Who may call which route - enforced by the agent itself, not only by
- * whatever sits in front of it.
- *
- * On cloudflarebase.com the console guard IS the gate, and that is sound
- * there: this worker claims a wildcard route that serves deployed APPS and
- * nothing of ours, `workers_dev` and `preview_urls` are false, so our own
- * surfaces are reachable only over the dashboard's service binding - where
- * every request has already been classified against the manifest's route
- * table and checked for an operator session or a deploy token.
- *
- * A consumer of the published package deploys the opposite shape. The
- * documented install re-exports the default handler from THEIR Worker - the
- * public one their app already runs on - and there the manifest is a file
- * nobody reads at request time. This agent has NO public routes at all:
- * every surface it serves deploys code, mints subdomains, writes secrets, or
- * reports what is deployed.
- *
- * So the whole surface is CLOSED unless the deployment declares its Worker
- * control-plane-only. Two properties matter:
- *
- * - Default deny. An undeclared route is operator, which is the same default
- * the agent contract states for the console guard. Drift between
- * this table and the manifest can therefore only ever close something that
- * should be open - a loud, testable failure - never open something that
- * should be closed.
- * - The refusal is the ordinary 404, byte for byte, so a closed surface is
- * not enumerable either.
- *
- * Serving a deployed app is unaffected: `serveIfAppHost` answers on the
- * wildcard hostname before any of this runs.
- *
- * `ROUTES` mirrors `cloudflarebase.agent.json` and
- * `route-access.unit.test.ts` fails if the two disagree, so the manifest
- * stays the single declaration. It is a copy rather than an import because
- * the JSON sits outside the build's `rootDir`, and the manifest has to stay
- * at the package root where the console and the CLI read it.
- */
-
-export type RouteAccess = 'public' | 'operator';
-
-export interface RouteRule {
- path: string;
- access: RouteAccess;
-}
-
-/** Mirrors the `routes` block of `cloudflarebase.agent.json`. */
-export const ROUTES: readonly RouteRule[] = [
- { path: '/overview', access: 'operator' },
- { path: '/apps/*', access: 'operator' },
- { path: '/deploys', access: 'operator' },
-];
-
-/** `/agents//` plus the sub-path the agent dispatches on. */
-const AGENT_PATH = /^\/agents\/[^/]+\/[^/]+(\/.*)?$/;
-
-/**
- * The declared access for one agent sub-path. Undeclared is `operator`: the
- * SDK's state-sync socket (sub-path `/`) lands here too, and broadcasting
- * agent state to an anonymous caller is exactly the leak this prevents.
- */
-export function routeAccess(subPath: string): RouteAccess {
- for (const rule of ROUTES) {
- if (rule.path.endsWith('/*')) {
- const prefix = rule.path.slice(0, -2);
- if (subPath === prefix || subPath.startsWith(`${prefix}/`)) return rule.access;
- } else if (subPath === rule.path) {
- return rule.access;
- }
- }
- return 'operator';
-}
-
-/**
- * Set only on a Worker that serves no public hostname of its own, where a
- * control plane in front has already authorized the caller.
- */
-export function operatorApiExposed(env: { EXPOSE_OPERATOR_API?: string }): boolean {
- return env.EXPOSE_OPERATOR_API === 'true';
-}
-
-/**
- * The 404 to answer with, or null to carry on. Everything that is not a
- * declared-public agent route is refused, `/internal/*` included - that one
- * is service-binding-only by deployment shape alone, which is a property of
- * our topology and not of the package.
- */
-export function gateOperatorRoutes(
- url: URL,
- env: { EXPOSE_OPERATOR_API?: string },
-): Response | null {
- if (operatorApiExposed(env)) return null;
- // `new URL` resolves real dot segments before anything reads the path, but
- // it leaves `%2e` alone - and classifying an encoded dot segment means
- // betting on whether something downstream decodes it. Nothing this agent
- // serves has one in its path, so refuse rather than guess.
- if (/%2e/i.test(url.pathname)) return notFound();
- const match = AGENT_PATH.exec(url.pathname);
- if (match && routeAccess(match[1] ?? '/') === 'public') return null;
- return notFound();
-}
-
-function notFound(): Response {
- return Response.json({ error: 'not found' }, { status: 404 });
-}
diff --git a/agents/hosting/src/route-access.unit.test.ts b/agents/hosting/src/route-access.unit.test.ts
deleted file mode 100644
index 2a3e3f4c..00000000
--- a/agents/hosting/src/route-access.unit.test.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import assert from 'node:assert/strict';
-import { readFileSync } from 'node:fs';
-import { fileURLToPath } from 'node:url';
-import test from 'node:test';
-import { ROUTES, gateOperatorRoutes, routeAccess } from './route-access';
-
-/**
- * The gate only ever runs in deployments we do not operate (a consumer's
- * public Worker, where `EXPOSE_OPERATOR_API` is unset), so nothing in the
- * e2e stack exercises it - every environment we run sets the flag and the
- * gate is a no-op. These tests are the coverage for the other shape.
- */
-
-const manifest = JSON.parse(
- // The manifest is written with a BOM; JSON.parse chokes on one.
- // .href, then fileURLToPath: the ambient URL here is workerd's, which is
- // not assignable to node:fs's PathOrFileDescriptor, and a bare pathname
- // would lose the drive letter on Windows.
- readFileSync(
- fileURLToPath(new URL('../cloudflarebase.agent.json', import.meta.url).href),
- 'utf8',
- ).replace(/^\uFEFF/, ''),
-) as { routes: { path: string; access: string }[] };
-
-test('the route table mirrors the manifest', () => {
- assert.deepEqual([...ROUTES], manifest.routes);
-});
-
-test('this agent declares no public route', () => {
- assert.equal(
- ROUTES.every((rule) => rule.access === 'operator'),
- true,
- );
- assert.equal(routeAccess('/apps/site/deploys'), 'operator');
- assert.equal(routeAccess('/apps/site/secrets'), 'operator');
- assert.equal(routeAccess('/apps/site/secrets/API_KEY'), 'operator');
- assert.equal(routeAccess('/apps/site/vars'), 'operator');
- assert.equal(routeAccess('/apps/site/build-env'), 'operator');
- assert.equal(routeAccess('/apps/site/analytics'), 'operator');
- assert.equal(routeAccess('/overview'), 'operator');
- assert.equal(routeAccess('/deploys'), 'operator');
- assert.equal(routeAccess('/'), 'operator');
- assert.equal(routeAccess('/whatever-comes-next'), 'operator');
-});
-
-const gate = (path: string, env: { EXPOSE_OPERATOR_API?: string } = {}) =>
- gateOperatorRoutes(new URL(`https://app.example${path}`), env);
-
-test('a public Worker refuses every surface', () => {
- for (const path of [
- '/agents/hosting-agent/p1/apps/site/deploys',
- '/agents/hosting-agent/p1/apps/site/secrets',
- '/agents/hosting-agent/p1/overview',
- '/agents/hosting-agent/p1',
- '/internal/projects/p1',
- '/internal/projects/p1/apps/site',
- '/internal/projects/p1/apps/site/build-env',
- ]) {
- assert.equal(gate(path)?.status, 404, path);
- }
-});
-
-test('the flag opens the surface for a control-plane-only Worker', () => {
- assert.equal(
- gate('/agents/hosting-agent/p1/apps/site/deploys', { EXPOSE_OPERATOR_API: 'true' }),
- null,
- );
- assert.equal(gate('/internal/projects/p1/apps/site', { EXPOSE_OPERATOR_API: 'true' }), null);
- assert.equal(
- gate('/agents/hosting-agent/p1/overview', { EXPOSE_OPERATOR_API: '1' })?.status,
- 404,
- );
-});
diff --git a/agents/hosting/src/schemas.ts b/agents/hosting/src/schemas.ts
deleted file mode 100644
index 30b07cfc..00000000
--- a/agents/hosting/src/schemas.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { z } from 'zod';
-
-/**
- * Trust-boundary schemas: strictObject for request bodies, `.catch` for
- * env/storage reads, and a schema - never a cast - for anything that crosses
- * a trust boundary.
- */
-
-// 48 characters: branch ids are `--`, so the ceiling has to hold
-// a root plus a usable branch name. Mirrored in the console's
-// src/lib/schemas/auth.ts and in agents/auth + agents/db - keep all four in
-// sync.
-export const projectIdSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,47}$/);
-
-// Demo roots are demo-<12..20 hex>; a demo BRANCH is demo---
-// (branches-design.md). Hosting refuses the whole family - anonymous code
-// execution is an abuse machine - so the pattern is the refusal key here,
-// not a cap selector. Mirrored in the console's $lib/console.ts and the
-// other agents.
-export const DEMO_PROJECT_PATTERN = /^demo-[a-f0-9]{12,20}(?:--[a-z0-9][a-z0-9-]{0,15})?$/;
-
-/** App names: subdomain charset, 3-48 chars, no `--` (mirrors the console's
- * appNameSchema - the console owns claims; this copy only guards the DO's
- * own registry keys). */
-export const appNameSchema = z
- .string()
- .regex(/^[a-z0-9][a-z0-9-]{2,47}$/)
- .refine((value) => !value.includes('--'));
-
-/** Subdomains are DNS labels: app name, optional branch and numbering. */
-export const subdomainSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/);
-
-/** The `meta` part of a deploy's multipart body. */
-export const deployMetaSchema = z.strictObject({
- /** Part name of the Worker entry module; absent = assets-only app. */
- mainModule: z
- .string()
- .regex(/^[A-Za-z0-9._-]+\.(?:js|mjs)$/)
- .optional(),
- compatibilityDate: z
- .string()
- .regex(/^\d{4}-\d{2}-\d{2}$/)
- .optional(),
- compatibilityFlags: z
- .array(z.string().regex(/^[a-z0-9_]+$/))
- .max(10)
- .optional(),
- /** Plain-text vars injected as bindings beside PROJECT_ID/CLOUDFLAREBASE_URL. */
- vars: z.record(z.string().regex(/^[A-Z][A-Z0-9_]*$/), z.string().max(5000)).optional(),
- /** Static-asset serving behaviour; defaults chosen for app-shaped sites. */
- notFoundHandling: z.enum(['single-page-application', '404-page', 'none']).optional(),
-});
-
-export type DeployMeta = z.infer;
-
-export const secretBodySchema = z.strictObject({
- name: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
- value: z.string().min(1).max(5000),
-});
-
-/** Env var and secret names: UPPER_SNAKE, bounded. */
-export const varNameSchema = z
- .string()
- .regex(/^[A-Z][A-Z0-9_]*$/)
- .max(128);
-
-/** Stored env values are single-line on purpose: build-time env is exported
- * line-by-line into the runner's $GITHUB_ENV, and one contract for both
- * stores keeps the console editor honest. */
-export const varValueSchema = z
- .string()
- .max(5000)
- .regex(/^[^\r\n]*$/);
-
-/** Replace-the-set body for the vars stores (runtime and build). */
-export const varsBodySchema = z.strictObject({
- vars: z.record(varNameSchema, varValueSchema),
-});
-
-/** `apps.last_deploy_vars` is a storage read, so it gets a schema - a row
- * written by an older version (or by hand) degrades to "no CLI vars". */
-export const storedVarsSchema = z.record(z.string(), z.string()).catch({});
-
-/** One build secret value - encrypted at rest, name rides the path. */
-export const buildSecretBodySchema = z.strictObject({
- value: varValueSchema.min(1),
-});
-
-/** Analytics Engine SQL API envelope (mirrors the auth agent's copy). */
-export const analyticsApiResponseSchema = z.object({ data: z.array(z.unknown()).optional() });
-
-/** Keyset cursor for the deploy list: `:`. */
-export const deployCursorSchema = z
- .string()
- .regex(/^\d{1,16}:[0-9a-f-]{36}$/)
- .optional()
- .catch(undefined);
diff --git a/agents/hosting/src/tar.fixture.ts b/agents/hosting/src/tar.fixture.ts
deleted file mode 100644
index e25fd19c..00000000
--- a/agents/hosting/src/tar.fixture.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * A REAL gzipped tar produced by bsdtar, inlined so the reader is tested
- * against the format as another implementation writes it - a parser checked
- * only against its own writer proves nothing.
- *
- * It has the shape GitHub sends: one --/ wrapper directory,
- * a build output directory, repository furniture that must never be
- * published, and a path too long for tar's 100-byte name field, which
- * therefore travels as a PAX extended header.
- */
-export const TARBALL_BASE64 =
- 'H4sIAIfffWoAA+2azY6bMBSFWVfqO2TZLsz4B7BUTStV6iy76QtUBN8AHYKRbZqJqj5ZF32kvkIdZRMREoaEWpPibwNESkxy' +
- 'uOf4XkVualBIQSNRuswIZQJWd8G0YIw554vd0dI97i9ITBlnMU8wX2BCOY+CRRw4oNUmVfZWrv2c7pe7EXr1D/PSFO1yqudg' +
- 'nP6J1T9ijHn9XXBS/zKvpYJJ1rC/R5IkZ/SnpFP/EY/jYIEDB8xc/z+/ftdSwNe1FG0F+vWrwDMneutflNpMuAkYn//2lHv/' +
- 'd0Gv/oeOMMFzMD7/4yjGXn8X9Or/5eHjp88P4VpMs8Zg/hPWzX8a+fx3gs1/BalYg0/+WTLs/81jfmUGjM//hDLq/d8Fz9O/' +
- 'rAU8hd/0ZWsM+j+NO/rzZDf/8f7/77H+vxc6hKdGKqPfE58EM+J0/5c2TZhpPcEa4/d/MaXM178LbP0vpdj++OmrfpacqX+t' +
- 'wUzR/l/S/9PI9/9OOK3/fs9XmHV17RrD+78j/+eUe/93gfX/+4J8KKCq5P2dPfM5MCsOrT5F30FtUSXrHIlSQWakvazTNSAj' +
- 'UdPqApkCUAGpsJ7RpNogWdvrthYKBMqKVKWZAdUNjdH+zxi5+f6/z1iDl8d0mtegjX1l1Sr7BoW0Katq/yDgsfMfxinh/6H+' +
- 'hxur4GXQq5oAaC6e9hwzOv8jHCW3Pv8f3FhPVnlX3bbN/0zWWlYQVjJ/Q976/J8VZ///tZHqcVXJzZVdIB4//2ec+P7PBc/U' +
- 'PyvD7cWN4KD/k6P5f4z9/M8Ju/9/2ch5t8hK7/wej8czG4LgLwGrArYAMgAA';
diff --git a/agents/hosting/src/tar.ts b/agents/hosting/src/tar.ts
deleted file mode 100644
index 5067777a..00000000
--- a/agents/hosting/src/tar.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-/**
- * Just enough tar+gzip to unpack a GitHub source tarball (Phase B, direct
- * deploys). No dependency: the format is 512-byte headers and the runtime
- * already gunzips, so pulling in a tar library would be more surface than
- * code.
- *
- * Everything here is bounded. A tarball is attacker-controlled input - a
- * decompression bomb is a handful of bytes on the wire - so the reader stops
- * at a byte ceiling while still decompressing, never after.
- */
-
-const BLOCK = 512;
-
-export interface TarEntry {
- /** Path as recorded in the archive, including the leading directory. */
- name: string;
- /** A view into the decompressed buffer, not a copy. */
- bytes: Uint8Array;
-}
-
-export class ArchiveTooLarge extends Error {
- constructor(limit: number) {
- super(`the repository archive exceeds ${Math.round(limit / (1024 * 1024))} MB`);
- }
-}
-
-/** Decompresses a gzip stream, refusing to buffer more than `maxBytes`. */
-export async function gunzip(
- stream: ReadableStream,
- maxBytes: number,
-): Promise {
- const reader = stream.pipeThrough(new DecompressionStream('gzip')).getReader();
- const chunks: Uint8Array[] = [];
- let total = 0;
- try {
- for (;;) {
- const { done, value } = await reader.read();
- if (done) break;
- total += value.length;
- // Checked as it arrives: waiting until the end is what a bomb wants.
- if (total > maxBytes) throw new ArchiveTooLarge(maxBytes);
- chunks.push(value);
- }
- } finally {
- await reader.cancel().catch(() => {});
- }
-
- const buffer = new Uint8Array(total);
- let offset = 0;
- for (const chunk of chunks) {
- buffer.set(chunk, offset);
- offset += chunk.length;
- }
- return buffer;
-}
-
-function readString(bytes: Uint8Array): string {
- let end = bytes.indexOf(0);
- if (end === -1) end = bytes.length;
- return new TextDecoder().decode(bytes.subarray(0, end));
-}
-
-function readOctal(bytes: Uint8Array): number {
- let text = '';
- for (const byte of bytes) {
- // Fields are octal digits padded with NULs or spaces.
- if (byte === 0 || byte === 0x20) break;
- text += String.fromCharCode(byte);
- }
- const value = text ? Number.parseInt(text, 8) : 0;
- return Number.isSafeInteger(value) && value >= 0 ? value : 0;
-}
-
-/** `path=` from a PAX extended header, which is how long names travel. */
-function paxPath(data: Uint8Array): string | null {
- const text = new TextDecoder().decode(data);
- const match = text.match(/\d+ path=([^\n]+)\n/);
- return match ? match[1] : null;
-}
-
-/**
- * Parses a tar buffer into its regular files.
- *
- * Directories, symlinks, and every other entry type are dropped: a static
- * deploy is a set of files, and honouring a symlink out of the archive would
- * be a path-traversal primitive.
- */
-export function parseTar(buffer: Uint8Array, maxFiles: number): TarEntry[] {
- const entries: TarEntry[] = [];
- let offset = 0;
- let pendingName: string | null = null;
-
- while (offset + BLOCK <= buffer.length) {
- const header = buffer.subarray(offset, offset + BLOCK);
- // Two zero blocks end the archive; one is enough to stop reading.
- if (header.every((byte) => byte === 0)) break;
-
- const size = readOctal(header.subarray(124, 136));
- const type = String.fromCharCode(header[156]);
- const prefix = readString(header.subarray(345, 500));
- const base = readString(header.subarray(0, 100));
-
- offset += BLOCK;
- const data = buffer.subarray(offset, offset + size);
- offset += Math.ceil(size / BLOCK) * BLOCK;
-
- // GNU long name and PAX extended headers both describe the NEXT entry.
- if (type === 'L') {
- pendingName = readString(data);
- continue;
- }
- if (type === 'x' || type === 'g') {
- pendingName = paxPath(data) ?? pendingName;
- continue;
- }
-
- const name = pendingName ?? (prefix ? `${prefix}/${base}` : base);
- pendingName = null;
-
- // '0' and NUL are both "regular file"; anything else is not ours.
- if (type !== '0' && type !== '\0') continue;
- entries.push({ name, bytes: data });
- if (entries.length > maxFiles) {
- throw new Error(`the repository holds more than ${maxFiles} files`);
- }
- }
-
- return entries;
-}
-
-/**
- * Turns archive entries into deployable asset paths.
- *
- * GitHub wraps every tarball in one `--/` directory, so the
- * first segment is always stripped. `assetsDir` then selects a subtree, and
- * the result is rooted at `/` - `dist/index.html` under `assetsDir: 'dist'`
- * serves as `/index.html`.
- *
- * Dotfiles and `node_modules` are skipped, mirroring the CLI's collector:
- * `.github`, `.gitignore`, and friends are repository furniture, not content,
- * and publishing them is at best noise and at worst a leak.
- */
-export function toAssetPaths(
- entries: TarEntry[],
- assetsDir: string,
-): { path: string; bytes: Uint8Array }[] {
- const prefix = assetsDir.replace(/^\/+|\/+$/g, '');
- const assets: { path: string; bytes: Uint8Array }[] = [];
-
- for (const entry of entries) {
- const segments = entry.name.split('/');
- segments.shift(); // GitHub's wrapper directory
- if (!segments.length) continue;
-
- let relative = segments.join('/');
- if (prefix) {
- if (relative !== prefix && !relative.startsWith(`${prefix}/`)) continue;
- relative = relative.slice(prefix.length).replace(/^\/+/, '');
- }
- if (!relative) continue;
-
- const parts = relative.split('/');
- if (parts.some((part) => part.startsWith('.') || part === 'node_modules')) continue;
- // Belt and braces: a crafted name must never escape the asset root.
- if (parts.includes('..')) continue;
-
- assets.push({ path: `/${relative}`, bytes: entry.bytes });
- }
-
- return assets;
-}
diff --git a/agents/hosting/src/tar.unit.test.ts b/agents/hosting/src/tar.unit.test.ts
deleted file mode 100644
index 41625010..00000000
--- a/agents/hosting/src/tar.unit.test.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import assert from 'node:assert/strict';
-import test from 'node:test';
-import { ArchiveTooLarge, gunzip, parseTar, toAssetPaths } from './tar.js';
-import { TARBALL_BASE64 } from './tar.fixture.js';
-
-/**
- * The tar reader is fed attacker-controlled input from a GitHub tarball, so
- * the fixture is a REAL archive from another tar implementation rather than
- * one written by these tests.
- */
-
-function fixtureStream(): ReadableStream {
- const bytes = Uint8Array.from(Buffer.from(TARBALL_BASE64, 'base64'));
- return new ReadableStream({
- start(controller) {
- controller.enqueue(bytes);
- controller.close();
- },
- });
-}
-
-async function fixtureEntries() {
- return parseTar(await gunzip(fixtureStream(), 16 * 1024 * 1024), 1000);
-}
-
-test('parses every regular file out of a real tarball', async () => {
- const names = (await fixtureEntries()).map((entry) => entry.name).sort();
- assert.deepEqual(
- names,
- [
- 'owner-repo-abc123def/.github/workflows/ci.yml',
- 'owner-repo-abc123def/.gitignore',
- 'owner-repo-abc123def/README.md',
- 'owner-repo-abc123def/dist/app.css',
- 'owner-repo-abc123def/dist/index.html',
- 'owner-repo-abc123def/dist/assets/a-very-long-directory-name-to-push-the-header-past-one-hundred-characters/nested-further-still/deep.js',
- 'owner-repo-abc123def/node_modules/pkg/index.js',
- ].sort(),
- );
-});
-
-test('reads a path too long for the tar name field', async () => {
- // The 100-byte name field cannot hold this, so it arrives as a PAX record.
- // Getting it wrong silently truncates paths rather than failing loudly.
- const entries = await fixtureEntries();
- const deep = entries.find((entry) => entry.name.endsWith('deep.js'));
- assert.ok(deep, 'the long-path entry survived');
- assert.ok(deep.name.length > 100);
- assert.equal(new TextDecoder().decode(deep.bytes).trim(), 'console.log(1)');
-});
-
-test('directories and file contents come through intact', async () => {
- const entries = await fixtureEntries();
- const index = entries.find((entry) => entry.name.endsWith('dist/index.html'));
- assert.ok(index);
- assert.equal(new TextDecoder().decode(index.bytes).trim(), 'hello ');
-});
-
-test('assets are rooted at the selected directory', async () => {
- const assets = toAssetPaths(await fixtureEntries(), 'dist');
- const paths = assets.map((asset) => asset.path).sort();
- // The wrapper directory AND the `dist` prefix are both stripped.
- assert.deepEqual(paths, [
- '/app.css',
- '/assets/a-very-long-directory-name-to-push-the-header-past-one-hundred-characters/nested-further-still/deep.js',
- '/index.html',
- ]);
-});
-
-test('repository furniture is never published', async () => {
- // Deploying from the repo root must not ship .github, .gitignore, or a
- // committed node_modules - at best noise, at worst a leak.
- const paths = toAssetPaths(await fixtureEntries(), '').map((asset) => asset.path);
- assert.ok(paths.includes('/README.md'));
- assert.ok(paths.includes('/dist/index.html'));
- assert.ok(!paths.some((path) => path.includes('.github')));
- assert.ok(!paths.some((path) => path.includes('.gitignore')));
- assert.ok(!paths.some((path) => path.includes('node_modules')));
-});
-
-test('a missing directory selects nothing rather than everything', async () => {
- // The failure that matters: falling back to the repo root here would
- // publish the whole source tree on a typo'd assets directory.
- assert.deepEqual(toAssetPaths(await fixtureEntries(), 'build'), []);
-});
-
-test('decompression stops at the byte ceiling', async () => {
- await assert.rejects(() => gunzip(fixtureStream(), 64), ArchiveTooLarge);
-});
-
-test('the file-count ceiling is enforced', async () => {
- const buffer = await gunzip(fixtureStream(), 16 * 1024 * 1024);
- assert.throws(() => parseTar(buffer, 2), /more than 2 files/);
-});
diff --git a/agents/hosting/template/worker-entry.ts b/agents/hosting/template/worker-entry.ts
deleted file mode 100644
index 8bb85fa7..00000000
--- a/agents/hosting/template/worker-entry.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Cloudflarebase hosting agent entrypoint (added by `cloudflarebase add hosting`).
- *
- * Re-exporting the Durable Object class from YOUR worker's entrypoint is not
- * optional plumbing - it is how the `HostingAgent` binding in wrangler.jsonc
- * resolves. If your project already has a default export, drop the `default`
- * re-export below and keep your own:
- *
- * export { HostingAgent } from '@cloudflarebase/hosting';
- */
-export { HostingAgent, default } from '@cloudflarebase/hosting';
-
-/**
- * Compile-time binding contract: if wrangler.jsonc is missing a binding the
- * agent needs (or types one wrongly), this line fails your typecheck with the
- * field named - before anything deploys. Run `wrangler types` after changing
- * bindings so `Env` is current.
- */
-import type { AssertHostingAgentEnv } from '@cloudflarebase/hosting';
-export type _HostingAgentBindings = AssertHostingAgentEnv;
diff --git a/agents/hosting/template/wrangler-fragment.jsonc b/agents/hosting/template/wrangler-fragment.jsonc
deleted file mode 100644
index 984e18e1..00000000
--- a/agents/hosting/template/wrangler-fragment.jsonc
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Wrangler fragment for the Cloudflarebase hosting agent.
- *
- * `cloudflarebase add hosting` merges these keys into your wrangler.jsonc:
- * comments survive, your existing values are never overwritten, and Durable
- * Object migrations append under your next free tag.
- *
- * The `migrations` block below is a FRESH v1 declaring the HostingAgent
- * class. Do not copy the tag history from the Cloudflarebase repository.
- *
- * Workers for Platforms is a PAID Cloudflare add-on, and this fragment
- * deliberately does not assume you have it: with only these keys the agent
- * deploys, records nothing, and answers hosting requests with a 503 pointing
- * at the setup steps. To actually serve apps you additionally need, by hand:
- *
- * 1. A dispatch namespace: `npx wrangler dispatch-namespace create `,
- * then a binding here -
- * "dispatch_namespaces": [{ "binding": "DISPATCH", "namespace": "" }]
- * (add "outbound": { "service": "", "parameters":
- * ["subdomain"] } and the var "DISPATCH_OUTBOUND": "true" if you deploy
- * one - optional but recommended; see src/outbound.ts in the package
- * repository for the v1 pass-through).
- * 2. Vars: DISPATCH_NAMESPACE (the namespace name - the REST API needs it)
- * and HOSTING_DOMAIN (the apex your apps serve under).
- * 3. Secrets: `wrangler secret put CF_ACCOUNT_ID` and
- * `wrangler secret put CF_HOSTING_API_TOKEN` (Workers Scripts edit).
- * 4. A wildcard route `*./*` to this worker on your zone.
- */
-{
- "migrations": [{ "new_sqlite_classes": ["HostingAgent"], "tag": "v1" }],
- "durable_objects": {
- "bindings": [{ "class_name": "HostingAgent", "name": "HostingAgent" }],
- },
- "vars": {
- // Empty disables the serving path; set your apex domain to enable it.
- "HOSTING_DOMAIN": "",
- // The Workers for Platforms dispatch namespace name (see header).
- "DISPATCH_NAMESPACE": "",
- /**
- * DELIBERATELY ABSENT: "EXPOSE_OPERATOR_API": "true".
- *
- * Every route this agent serves deploys code, mints subdomains, writes
- * secrets, or reports what is deployed, and none of them authenticate a
- * caller - they are meant to sit behind a console that already has. On
- * this Worker, which serves your application on a public hostname, they
- * answer 404. Serving deployed apps on your wildcard hostname is
- * unaffected.
- *
- * Your own code is unaffected too: reach the agent through the
- * HostingAgent Durable Object namespace binding, which no HTTP caller
- * can.
- *
- * Set it ONLY on a Worker that has no public hostname of its own
- * (workers_dev false, no route beyond the app wildcard) and is fronted
- * by something that authenticates operators.
- */
- /**
- * OPTIONAL. Empty disables error reporting, which is the default and a
- * perfectly good place to stay - nothing else about the agent changes.
- *
- * To turn it on, put your DSN on the line below. It belongs here as a
- * var and not in `wrangler secret put`: a Sentry DSN is public by
- * design, and this line is uploaded as a plain_text binding of the same
- * name, so it would override a secret you set under it.
- */
- "SENTRY_DSN": "",
- },
- "secrets": {
- "required": [],
- },
-}
diff --git a/agents/hosting/tsconfig.build.json b/agents/hosting/tsconfig.build.json
deleted file mode 100644
index 08c28bdc..00000000
--- a/agents/hosting/tsconfig.build.json
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Publish build for `@cloudflarebase/hosting`.
- *
- * Emits `dist/` from `src/` only. Deliberately left out of the published
- * package:
- *
- * - `worker-configuration.d.ts` is a `types` entry rather than an input, so it
- * is never emitted. Shipping it would declare a global `interface Env` in the
- * consumer's project and clobber their own generated bindings - the exact
- * opposite of what makes this package portable.
- * - `src/env.d.ts` is a declaration input, which tsc does not copy to `outDir`.
- * Its augmentation describes deployment shapes, not the consumer's config.
- * - `bindings.test-d.ts` (the binding-contract negatives), executed by
- * `tsc --noEmit` instead.
- * - `*.unit.test.ts` and the archive fixture they read, which are run by
- * `npm run test:unit` and have no business in a consumer's bundle.
- *
- * The emitted declarations name `Env` instead of inlining it, so in a consumer
- * project it resolves to their `wrangler types` output. `AssertHostingAgentEnv`
- * is what checks that output actually carries the bindings the agent reads.
- */
-{
- "extends": "./tsconfig.json",
- "compilerOptions": {
- "noEmit": false,
- "declaration": true,
- "outDir": "dist",
- "rootDir": "src"
- },
- "include": ["src/**/*"],
- "exclude": ["src/**/*.test-d.ts", "src/**/*.unit.test.ts", "src/**/*.fixture.ts"]
-}
diff --git a/agents/hosting/tsconfig.json b/agents/hosting/tsconfig.json
deleted file mode 100644
index 7e76e467..00000000
--- a/agents/hosting/tsconfig.json
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- "compilerOptions": {
- /* Visit https://aka.ms/tsconfig.json to read more about this file */
-
- /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
- "target": "es2024",
- /* Specify a set of bundled library declaration files that describe the target runtime environment. */
- "lib": ["es2024"],
- /* Specify what JSX code is generated. */
- "jsx": "react-jsx",
-
- /* Specify what module code is generated. */
- "module": "es2022",
- /* Specify how TypeScript looks up a file from a given module specifier. */
- "moduleResolution": "node",
- /* Enable importing .json files */
- "resolveJsonModule": true,
-
- /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
- "allowJs": true,
- /* Enable error reporting in type-checked JavaScript files. */
- "checkJs": false,
-
- /* Disable emitting files from a compilation. */
- "noEmit": true,
-
- /* Ensure that each file can be safely transpiled without relying on other imports. */
- "isolatedModules": true,
- /* Allow 'import x from y' when a module doesn't have a default export. */
- "allowSyntheticDefaultImports": true,
- /* Ensure that casing is correct in imports. */
- "forceConsistentCasingInFileNames": true,
-
- /* Enable all strict type-checking options. */
- "strict": true,
-
- /* Skip type checking all .d.ts files. */
- "skipLibCheck": true,
- "types": ["./worker-configuration.d.ts", "node"]
- },
-
- /*
- * `dist` is this project's own build output - with allowJs on it would be
- * typechecked alongside the sources it was emitted from. `template` holds
- * files meant to be copied into a consumer's project, so they import
- * `@cloudflarebase/auth` by package name, which does not resolve from
- * inside the package itself.
- */
- "exclude": ["dist", "template", "node_modules"]
-}
diff --git a/agents/hosting/worker-configuration.d.ts b/agents/hosting/worker-configuration.d.ts
deleted file mode 100644
index 04707545..00000000
--- a/agents/hosting/worker-configuration.d.ts
+++ /dev/null
@@ -1,14893 +0,0 @@
-/* eslint-disable */
-// Generated by Wrangler by running `wrangler types` (hash: 9133629e56c43a1f4c15afbe4ad4541d)
-// Runtime types generated with workerd@1.20260811.1 2026-07-10 nodejs_als,nodejs_compat
-interface __BaseEnv_Env {
- LOCAL_ANALYTICS?: D1Database;
- HOSTING_REQUESTS?: AnalyticsEngineDataset;
- EXPOSE_OPERATOR_API: "true";
- SENTRY_DSN: "" | "https://50ed6e26b886124ac39e983efb72144d@o4510375271530496.ingest.de.sentry.io/4511764380778576" | "https://093f7505d223a871223f9a15130ef116@o4510375271530496.ingest.de.sentry.io/4511764377043024";
- SENTRY_ENV: "development" | "test" | "production" | "preview";
- HOSTING_STUB?: "true";
- HOSTING_DOMAIN: "cfbase.dev" | "cfbase-preview.dev" | "";
- DISPATCH_NAMESPACE: "" | "cfbase-apps" | "cfbase-apps-preview";
- HOSTING_MASTER_KEY?: "local-dev-master-key-not-a-secret";
- WAE_DATASET?: "cloudflarebase_hosting_requests_local" | "cloudflarebase_hosting_requests_test" | "cloudflarebase_hosting_requests" | "cloudflarebase_hosting_requests_preview";
- HostingAgent: DurableObjectNamespace;
- DISPATCH?: DispatchNamespace;
- DISPATCH_OUTBOUND?: "true";
- HOSTING_APEX_REDIRECT?: "https://cloudflarebase.com" | "https://cloudflarebase-com-preview.cloudflarebase.workers.dev";
- CF_ACCOUNT_ID?: string;
- CF_HOSTING_API_TOKEN?: string;
-}
-declare namespace Cloudflare {
- interface GlobalProps {
- mainModule: typeof import("./src/index");
- durableNamespaces: "HostingAgent";
- }
- interface LocalEnv {
- LOCAL_ANALYTICS: D1Database;
- HOSTING_REQUESTS: AnalyticsEngineDataset;
- EXPOSE_OPERATOR_API: "true";
- SENTRY_DSN: "";
- SENTRY_ENV: "development";
- HOSTING_STUB: "true";
- HOSTING_DOMAIN: "cfbase.dev";
- DISPATCH_NAMESPACE: "";
- HOSTING_MASTER_KEY: "local-dev-master-key-not-a-secret";
- WAE_DATASET: "cloudflarebase_hosting_requests_local";
- HostingAgent: DurableObjectNamespace;
- }
- interface TestEnv {
- LOCAL_ANALYTICS: D1Database;
- HOSTING_REQUESTS: AnalyticsEngineDataset;
- EXPOSE_OPERATOR_API: "true";
- SENTRY_DSN: "";
- SENTRY_ENV: "test";
- HOSTING_STUB: "true";
- HOSTING_DOMAIN: "cfbase.dev";
- DISPATCH_NAMESPACE: "";
- HOSTING_MASTER_KEY: "local-dev-master-key-not-a-secret";
- WAE_DATASET: "cloudflarebase_hosting_requests_test";
- HostingAgent: DurableObjectNamespace;
- }
- interface ProductionEnv {
- HOSTING_REQUESTS: AnalyticsEngineDataset;
- DISPATCH: DispatchNamespace;
- EXPOSE_OPERATOR_API: "true";
- SENTRY_DSN: "https://50ed6e26b886124ac39e983efb72144d@o4510375271530496.ingest.de.sentry.io/4511764380778576";
- SENTRY_ENV: "production";
- HOSTING_DOMAIN: "cfbase.dev";
- DISPATCH_NAMESPACE: "cfbase-apps";
- WAE_DATASET: "cloudflarebase_hosting_requests";
- DISPATCH_OUTBOUND: "true";
- HOSTING_APEX_REDIRECT: "https://cloudflarebase.com";
- CF_ACCOUNT_ID: string;
- CF_HOSTING_API_TOKEN: string;
- HostingAgent: DurableObjectNamespace;
- }
- interface PreviewEnv {
- HOSTING_REQUESTS: AnalyticsEngineDataset;
- DISPATCH: DispatchNamespace;
- EXPOSE_OPERATOR_API: "true";
- SENTRY_DSN: "https://093f7505d223a871223f9a15130ef116@o4510375271530496.ingest.de.sentry.io/4511764377043024";
- SENTRY_ENV: "preview";
- HOSTING_DOMAIN: "cfbase-preview.dev";
- DISPATCH_NAMESPACE: "cfbase-apps-preview";
- DISPATCH_OUTBOUND: "true";
- HOSTING_APEX_REDIRECT: "https://cloudflarebase-com-preview.cloudflarebase.workers.dev";
- WAE_DATASET: "cloudflarebase_hosting_requests_preview";
- CF_ACCOUNT_ID: string;
- CF_HOSTING_API_TOKEN: string;
- HostingAgent: DurableObjectNamespace;
- }
- interface Env extends __BaseEnv_Env {}
-}
-interface Env extends __BaseEnv_Env {}
-type StringifyValues> = {
- [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
-};
-declare namespace NodeJS {
- interface ProcessEnv extends StringifyValues> {}
-}
-
-// Begin runtime types
-/*! *****************************************************************************
-Copyright (c) Cloudflare. All rights reserved.
-Copyright (c) Microsoft Corporation. All rights reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License"); you may not use
-this file except in compliance with the License. You may obtain a copy of the
-License at http://www.apache.org/licenses/LICENSE-2.0
-THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
-WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
-MERCHANTABLITY OR NON-INFRINGEMENT.
-See the Apache Version 2.0 License for specific language governing permissions
-and limitations under the License.
-***************************************************************************** */
-/* eslint-disable */
-// noinspection JSUnusedGlobalSymbols
-declare var onmessage: never;
-/**
- * The **`DOMException`** interface represents an abnormal event (called an **exception**) that occurs as a result of calling a method or accessing a property of a web API.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException)
- */
-declare class DOMException extends Error {
- constructor(message?: string, name?: string);
- /**
- * The **`message`** read-only property of the a message or description associated with the given error name.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/message)
- */
- readonly message: string;
- /**
- * The **`name`** read-only property of the one of the strings associated with an error name.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/name)
- */
- readonly name: string;
- /**
- * The **`code`** read-only property of the DOMException interface returns one of the legacy error code constants, or `0` if none match.
- * @deprecated
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/DOMException/code)
- */
- readonly code: number;
- static readonly INDEX_SIZE_ERR: number;
- static readonly DOMSTRING_SIZE_ERR: number;
- static readonly HIERARCHY_REQUEST_ERR: number;
- static readonly WRONG_DOCUMENT_ERR: number;
- static readonly INVALID_CHARACTER_ERR: number;
- static readonly NO_DATA_ALLOWED_ERR: number;
- static readonly NO_MODIFICATION_ALLOWED_ERR: number;
- static readonly NOT_FOUND_ERR: number;
- static readonly NOT_SUPPORTED_ERR: number;
- static readonly INUSE_ATTRIBUTE_ERR: number;
- static readonly INVALID_STATE_ERR: number;
- static readonly SYNTAX_ERR: number;
- static readonly INVALID_MODIFICATION_ERR: number;
- static readonly NAMESPACE_ERR: number;
- static readonly INVALID_ACCESS_ERR: number;
- static readonly VALIDATION_ERR: number;
- static readonly TYPE_MISMATCH_ERR: number;
- static readonly SECURITY_ERR: number;
- static readonly NETWORK_ERR: number;
- static readonly ABORT_ERR: number;
- static readonly URL_MISMATCH_ERR: number;
- static readonly QUOTA_EXCEEDED_ERR: number;
- static readonly TIMEOUT_ERR: number;
- static readonly INVALID_NODE_TYPE_ERR: number;
- static readonly DATA_CLONE_ERR: number;
- get stack(): any;
- set stack(value: any);
-}
-type WorkerGlobalScopeEventMap = {
- fetch: FetchEvent;
- scheduled: ScheduledEvent;
- queue: QueueEvent;
- unhandledrejection: PromiseRejectionEvent;
- rejectionhandled: PromiseRejectionEvent;
-};
-declare abstract class WorkerGlobalScope extends EventTarget {
- EventTarget: typeof EventTarget;
-}
-/* The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox). *
- * The **`console`** object provides access to the debugging console (e.g., the Web console in Firefox).
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console)
- */
-interface Console {
- "assert"(condition?: boolean, ...data: any[]): void;
- /**
- * The **`console.clear()`** static method clears the console if possible.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static)
- */
- clear(): void;
- /**
- * The **`console.count()`** static method logs the number of times that this particular call to `count()` has been called.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static)
- */
- count(label?: string): void;
- /**
- * The **`console.countReset()`** static method resets counter used with console/count_static.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/countReset_static)
- */
- countReset(label?: string): void;
- /**
- * The **`console.debug()`** static method outputs a message to the console at the 'debug' log level.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/debug_static)
- */
- debug(...data: any[]): void;
- /**
- * The **`console.dir()`** static method displays a list of the properties of the specified JavaScript object.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dir_static)
- */
- dir(item?: any, options?: any): void;
- /**
- * The **`console.dirxml()`** static method displays an interactive tree of the descendant elements of the specified XML/HTML element.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/dirxml_static)
- */
- dirxml(...data: any[]): void;
- /**
- * The **`console.error()`** static method outputs a message to the console at the 'error' log level.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/error_static)
- */
- error(...data: any[]): void;
- /**
- * The **`console.group()`** static method creates a new inline group in the Web console log, causing any subsequent console messages to be indented by an additional level, until console/groupEnd_static is called.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/group_static)
- */
- group(...data: any[]): void;
- /**
- * The **`console.groupCollapsed()`** static method creates a new inline group in the console.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupCollapsed_static)
- */
- groupCollapsed(...data: any[]): void;
- /**
- * The **`console.groupEnd()`** static method exits the current inline group in the console.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/groupEnd_static)
- */
- groupEnd(): void;
- /**
- * The **`console.info()`** static method outputs a message to the console at the 'info' log level.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/info_static)
- */
- info(...data: any[]): void;
- /**
- * The **`console.log()`** static method outputs a message to the console.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
- */
- log(...data: any[]): void;
- /**
- * The **`console.table()`** static method displays tabular data as a table.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/table_static)
- */
- table(tabularData?: any, properties?: string[]): void;
- /**
- * The **`console.time()`** static method starts a timer you can use to track how long an operation takes.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/time_static)
- */
- time(label?: string): void;
- /**
- * The **`console.timeEnd()`** static method stops a timer that was previously started by calling console/time_static.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeEnd_static)
- */
- timeEnd(label?: string): void;
- /**
- * The **`console.timeLog()`** static method logs the current value of a timer that was previously started by calling console/time_static.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/timeLog_static)
- */
- timeLog(label?: string, ...data: any[]): void;
- timeStamp(label?: string): void;
- /**
- * The **`console.trace()`** static method outputs a stack trace to the console.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/trace_static)
- */
- trace(...data: any[]): void;
- /**
- * The **`console.warn()`** static method outputs a warning message to the console at the 'warning' log level.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/warn_static)
- */
- warn(...data: any[]): void;
-}
-declare const console: Console;
-type BufferSource = ArrayBufferView | ArrayBuffer;
-type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array;
-declare namespace WebAssembly {
- class CompileError extends Error {
- constructor(message?: string);
- }
- class RuntimeError extends Error {
- constructor(message?: string);
- }
- type ValueType = "anyfunc" | "externref" | "f32" | "f64" | "i32" | "i64" | "v128";
- interface GlobalDescriptor {
- value: ValueType;
- mutable?: boolean;
- }
- class Global {
- constructor(descriptor: GlobalDescriptor, value?: any);
- value: any;
- valueOf(): any;
- }
- type ImportValue = ExportValue | number;
- type ModuleImports = Record;
- type Imports = Record;
- type ExportValue = Function | Global | Memory | Table;
- type Exports = Record;
- class Instance {
- constructor(module: Module, imports?: Imports);
- readonly exports: Exports;
- }
- interface MemoryDescriptor {
- initial: number;
- maximum?: number;
- shared?: boolean;
- }
- class Memory {
- constructor(descriptor: MemoryDescriptor);
- readonly buffer: ArrayBuffer;
- grow(delta: number): number;
- }
- type ImportExportKind = "function" | "global" | "memory" | "table";
- interface ModuleExportDescriptor {
- kind: ImportExportKind;
- name: string;
- }
- interface ModuleImportDescriptor {
- kind: ImportExportKind;
- module: string;
- name: string;
- }
- abstract class Module {
- static customSections(module: Module, sectionName: string): ArrayBuffer[];
- static exports(module: Module): ModuleExportDescriptor[];
- static imports(module: Module): ModuleImportDescriptor[];
- }
- type TableKind = "anyfunc" | "externref";
- interface TableDescriptor {
- element: TableKind;
- initial: number;
- maximum?: number;
- }
- class Table {
- constructor(descriptor: TableDescriptor, value?: any);
- readonly length: number;
- get(index: number): any;
- grow(delta: number, value?: any): number;
- set(index: number, value?: any): void;
- }
- function instantiate(module: Module, imports?: Imports): Promise;
- function validate(bytes: BufferSource): boolean;
-}
-/**
- * The **`ServiceWorkerGlobalScope`** interface of the Service Worker API represents the global execution context of a service worker.
- * Available only in secure contexts.
- *
- * [MDN Reference](https://developer.mozilla.org/docs/Web/API/ServiceWorkerGlobalScope)
- */
-interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
- DOMException: typeof DOMException;
- WorkerGlobalScope: typeof WorkerGlobalScope;
- btoa(data: string): string;
- atob(data: string): string;
- setTimeout(callback: (...args: any[]) => void, msDelay?: number): number;
- setTimeout(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
- clearTimeout(timeoutId: number | null): void;
- setInterval(callback: (...args: any[]) => void, msDelay?: number): number;
- setInterval(callback: (...args: Args) => void, msDelay?: number, ...args: Args): number;
- clearInterval(timeoutId: number | null): void;
- queueMicrotask(task: Function): void;
- structuredClone(value: T, options?: StructuredSerializeOptions): T;
- reportError(error: any): void;
- fetch(input: RequestInfo | URL, init?: RequestInit): Promise