You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
-`POST /v1/sites/{siteId}/administrator-claim` exchanges its one-time bearer capability for the persistent site-scoped `admin` credential after provisioning succeeds. It does not accept an API bearer token.
23
23
24
-
All responses use versioned WP Codebox provisioning schemas. Site IDs are allocated only from `WORDPRESS_SITE_CONTEXTS`; caller hostnames, DNS, and Cloudflare control APIs are not inputs. The allocation transaction reserves the same shipped D1 site identity used by legacy/operator operations, so both interfaces contend on one hostname and an existing site cannot be taken over. D1 stores allocation ownership, immutable artifact identity and import options, and API operation links. The scheduler resumes incomplete allocations for its selected site before it runs an operation; it verifies staged bytes and converges the conditional destination copy, operation, and API link without blocking publication or cron when recovery fails.
24
+
All responses use versioned WP Codebox provisioning schemas. With `WORDPRESS_PREVIEW_DOMAIN` and the secret `WORDPRESS_PREVIEW_HOST_SECRET` configured, `POST /v1/sites` allocates a collision-resistant signed site ID and returns the canonical `https://{siteId}.{preview-domain}` origin. Configure the suffix as a Worker wildcard route before serving those origins; no per-site Worker configuration is needed. The Worker validates that wildcard hostname and derives the site ID before any D1, coordinator, PHP, or SQLite work; anonymous published reads therefore remain R2/cache-only. D1 transactionally stores allocation ownership, lifecycle timestamps, the canonical hostname, immutable artifact identity, import options, and API operation links. `wp_codebox_site_aliases` is reserved for custom-domain aliases and is never consulted by wildcard routing. Without a preview domain, the legacy finite `WORDPRESS_SITE_CONTEXTS` allocator remains available for existing configured deployments.
25
25
26
26
`POST /v1/sites` validates `WORDPRESS_ADMIN_CLAIM_SECRET` and `WORDPRESS_ADMIN_PASSWORD` before allocation. Its create/replay response includes the deterministic pending capability while the configured root still derives the stored digest. Site reads expose only claim state, expiry, and the fixed endpoint. D1 stores capability and derived-credential digests, never either plaintext value. Scheduled allocation recovery issues a missing claim before provisioning work can run. Redemption requires the current site-scoped credential to match the digest pinned before bootstrap, then atomically consumes the capability exactly once and transfers that persistent administrator credential; it is not a one-time browser session. Rotating either root preserves the pending record but requires restoring the matching root before replay or redemption.
Copy file name to clipboardExpand all lines: packages/runtime-cloudflare/README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -20,7 +20,7 @@ The Worker forwards browser cookies directly to Playground and disables Playgrou
20
20
21
21
## Site Identity
22
22
23
-
The Worker resolves a validated `SiteContext` from the exact request hostname before reading a cache or constructing a coordinator. `WORDPRESS_SITE_CONTEXTS` is a JSON array of `{ "id", "hostname", "origin" }` records. Site IDs use lowercase letters, digits, and single hyphens; hostnames must be canonical lowercase DNS names or loopback addresses; origins must match the configured hostname and contain no path, query, or credentials. HTTPS is required except for loopback and `*.localhost` development origins. Unknown hostnames fail with `421` and never fall back to another site.
23
+
The Worker resolves a validated `SiteContext` from the exact request hostname before reading a cache or constructing a coordinator. `WORDPRESS_SITE_CONTEXTS` is a JSON array of `{ "id", "hostname", "origin" }` records and retains the production `default` mapping. Configure `WORDPRESS_PREVIEW_DOMAIN` plus the 32-byte-or-longer secret `WORDPRESS_PREVIEW_HOST_SECRET` to allocate elastic preview sites at signed `https://{siteId}.{preview-domain}` wildcard origins. Their hostname proves the random identity and generation without D1, so malformed, suffix-confused, forged, and stale-generation preview names fail with `421` before state access. D1 records canonical allocations and optional aliases, but aliases never participate in preview-host resolution. Site IDs use lowercase letters, digits, and single hyphens; hostnames must be canonical lowercase DNS names or loopback addresses; origins must match the configured hostname and contain no path, query, or credentials. HTTPS is required except for loopback and `*.localhost` development origins. Unknown hostnames fail with `421` and never fall back to another site.
24
24
25
25
Every mutable R2 key is rooted below `sites/{siteId}/`, D1 tables partition rows by site ID, Durable Object names include the site ID, and runtime and publication caches include it in their identity. The existing production hostname remains explicitly mapped to `default`, preserving its `sites/default/...` objects and credentials. Non-default sites derive distinct operator tokens and bootstrap admin passwords from the corresponding root Worker secrets with a versioned HMAC domain separator; the mapping itself contains no credentials. WordPress auth keys and salts are independently site-derived from `WORDPRESS_AUTH_SECRET`.
Copy file name to clipboardExpand all lines: packages/runtime-cloudflare/src/d1-operation-repository.ts
+10-1Lines changed: 10 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -70,6 +70,12 @@ export class D1OperationRepository {
70
70
returnrow ? this.hydrate(row) : null
71
71
}
72
72
73
+
asyncactiveSites(): Promise<SiteContext[]>{
74
+
awaitensureSchema(this.database)
75
+
constrows=awaitthis.database.prepare("SELECT site_id, hostname, origin FROM wp_codebox_sites WHERE state = 'active' ORDER BY site_id").all<{site_id: string;hostname: string;origin: string}>()
constcandidate=awaitthis.database.prepare(`SELECT operation_id FROM wp_codebox_operations WHERE site_id = ? AND (state = 'queued' OR (state = 'retryable' AND retry_at <= ?) OR (state = 'running' AND claim_expires_at <= ?)) ORDER BY created_at LIMIT 1`).bind(siteId,now,now).first<{operation_id: string}>()
@@ -180,7 +186,10 @@ async function ensureSchema(database: D1Database): Promise<void> {
180
186
constexisting=schemaReady.get(databaseasobject)
181
187
if(!existing){
182
188
constpending=(async()=>{
183
-
awaitdatabase.prepare(`CREATE TABLE IF NOT EXISTS wp_codebox_sites (site_id TEXT PRIMARY KEY, hostname TEXT NOT NULL, origin TEXT NOT NULL, state TEXT NOT NULL CHECK (state IN ('active')), created_at INTEGER NOT NULL, activated_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`).run()
189
+
awaitdatabase.prepare(`CREATE TABLE IF NOT EXISTS wp_codebox_sites (site_id TEXT PRIMARY KEY, hostname TEXT NOT NULL, origin TEXT NOT NULL, generation INTEGER NOT NULL DEFAULT 1, state TEXT NOT NULL CHECK (state IN ('active')), created_at INTEGER NOT NULL, activated_at INTEGER NOT NULL, updated_at INTEGER NOT NULL)`).run()
awaitdatabase.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS wp_codebox_site_hostname ON wp_codebox_sites(hostname)`).run()
192
+
awaitdatabase.prepare(`CREATE TABLE IF NOT EXISTS wp_codebox_site_aliases (hostname TEXT PRIMARY KEY, site_id TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY (site_id) REFERENCES wp_codebox_sites(site_id))`).run()
184
193
awaitdatabase.prepare(`CREATE TABLE IF NOT EXISTS wp_codebox_operations (site_id TEXT NOT NULL, operation_id TEXT NOT NULL, idempotency_key TEXT NOT NULL, fingerprint TEXT NOT NULL, artifact_key TEXT NOT NULL, artifact_sha256 TEXT NOT NULL, artifact_size INTEGER NOT NULL, slug TEXT NOT NULL, name TEXT NOT NULL, site_title TEXT NOT NULL, state TEXT NOT NULL CHECK (state IN ('queued','running','retryable','publication-pending','succeeded','failed')), stage TEXT NOT NULL, progress INTEGER NOT NULL CHECK (progress BETWEEN 0 AND 100), attempts INTEGER NOT NULL, retry_at INTEGER, claim_token TEXT, claim_expires_at INTEGER, prepared_version INTEGER, prepared_revision TEXT, prepared_manifest_key TEXT, prepared_persisted_at TEXT, prepared_result_json TEXT, prepared_publication_job TEXT, receipt_json TEXT, error_code TEXT, error_message TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, completed_at TEXT, PRIMARY KEY (site_id, operation_id), UNIQUE (site_id, idempotency_key), FOREIGN KEY (site_id) REFERENCES wp_codebox_sites(site_id))`).run()
185
194
awaitdatabase.prepare(`CREATE TABLE IF NOT EXISTS wp_codebox_operation_attempts (site_id TEXT NOT NULL, operation_id TEXT NOT NULL, attempt_number INTEGER NOT NULL, claim_token TEXT NOT NULL, started_at TEXT NOT NULL, completed_at TEXT, state TEXT NOT NULL, stage TEXT NOT NULL, error_code TEXT, error_message TEXT, PRIMARY KEY (site_id, operation_id, attempt_number), FOREIGN KEY (site_id, operation_id) REFERENCES wp_codebox_operations(site_id, operation_id))`).run()
186
195
awaitdatabase.prepare(`CREATE UNIQUE INDEX IF NOT EXISTS wp_codebox_one_active_operation ON wp_codebox_operations(site_id) WHERE state IN ('queued','running','retryable')`).run()
if(existing){if(existing.fingerprint!==input.fingerprint)thrownewOperationConflict("The idempotency key is already bound to a different immutable input.");returnexisting}
constcount=awaitthis.db.prepare("SELECT COUNT(*) AS count FROM wp_codebox_api_sites WHERE principal = ?").bind(token.principal).first<{count: number}>()
0 commit comments