diff --git a/skills/xapi-workers/SKILL.md b/skills/xapi-workers/SKILL.md index a24885c..207dfd3 100644 --- a/skills/xapi-workers/SKILL.md +++ b/skills/xapi-workers/SKILL.md @@ -22,6 +22,7 @@ Use the `xapi` CLI (`xapi-to` is the same executable). Verify `xapi workers --he - **How much did it cost?** Read [billing.md](references/billing.md) before answering, collecting, or reconciling consumption. - **Pause/recover/delete/refund:** read [lifecycle.md](references/lifecycle.md) before lifecycle mutations. - **Buy or bind an xdomain domain:** read [domains.md](references/domains.md). Use the combined CLI command; do not manually create a CNAME to the Dispatcher or expose Cloudflare zone IDs. +- **Recover a domain-conflict quarantine (administrator only):** read [domain-conflict-recovery.md](references/domain-conflict-recovery.md). This is a backend recovery operation, not a customer deployment command. ## Evidence and completion diff --git a/skills/xapi-workers/references/domain-conflict-recovery.md b/skills/xapi-workers/references/domain-conflict-recovery.md new file mode 100644 index 0000000..41ab278 --- /dev/null +++ b/skills/xapi-workers/references/domain-conflict-recovery.md @@ -0,0 +1,54 @@ +# Administrator domain-conflict recovery + +Use this procedure only when a Worker environment was quarantined after a +custom-domain attach returned Cloudflare HTTP `409` with provider error +`100117` (`DOMAIN_ALREADY_BOUND`). This is an administrator recovery contract, +not a normal customer deployment command. + +## Endpoint + +```text +POST /api/admin/workers/domain-conflict-recovery +Authorization: Bearer +Content-Type: application/json +``` + +The endpoint uses the backend's existing administrator authentication and +authorization. Do not create a temporary token, put a Cloudflare token in the +request, or send an xAPI key to the Worker hostname. The caller must already be +an xAPI platform administrator. + +Request the exact identifiers and revision from the backend audit record: + +```json +{ + "workerId": "", + "environmentId": "", + "domainId": "", + "hostname": "app.example.com", + "expectedRevision": 236, + "expectedProviderStatus": 409, + "expectedProviderCode": 100117 +} +``` + +## Preconditions and result + +xAPI checks that the environment is the requested Worker, the legacy writer is +the one-step custom-domain `PUT` that recorded `409 / 100117`, and the +`domainProvision` scope in that writer matches the submitted domain, hostname, +zone, and dispatch service. It also verifies that Cloudflare shows another +service owning the hostname and that the xAPI dispatch service does not own it. + +The platform route KV must be absent, or must point to the failed script and be +safe to remove. If ownership evidence is missing, ambiguous, or belongs to +another xAPI script, recovery fails closed. + +Successful recovery clears only the failed legacy writer, returns the +environment to `LEGACY`, records the local domain as `ERROR` with +`worker_domain_conflict`, and writes an audit record. It does not detach or +delete the binding owned by another Cloudflare service. The user must resolve +the hostname conflict separately before trying another bind. + +After recovery, verify the returned state, the audit event +`control.legacy_domain_conflict_recovered`, and the unchanged external binding. diff --git a/src/tests/skill-workers-guide.test.ts b/src/tests/skill-workers-guide.test.ts index a776d82..1dd62df 100644 --- a/src/tests/skill-workers-guide.test.ts +++ b/src/tests/skill-workers-guide.test.ts @@ -25,6 +25,13 @@ const domainGuide = readFileSync( new URL('../../skills/xapi-workers/references/domains.md', import.meta.url), 'utf8', ); +const domainConflictRecovery = readFileSync( + new URL( + '../../skills/xapi-workers/references/domain-conflict-recovery.md', + import.meta.url, + ), + 'utf8', +); describe('bundled xAPI Workers skill guide', () => { it('routes hosted Worker tasks to the progressively loaded guide', () => { @@ -46,6 +53,19 @@ describe('bundled xAPI Workers skill guide', () => { expect(domainGuide).not.toContain('wrangler deploy'); }); + it('documents the administrator-only domain conflict recovery contract', () => { + expect(dedicatedSkill).toContain( + '[domain-conflict-recovery.md](references/domain-conflict-recovery.md)', + ); + expect(domainConflictRecovery).toContain( + 'POST /api/admin/workers/domain-conflict-recovery', + ); + expect(domainConflictRecovery).toContain('100117'); + expect(domainConflictRecovery).toContain('platform administrator'); + expect(domainConflictRecovery).toContain('fails closed'); + expect(domainConflictRecovery).toContain('does not detach or'); + }); + it('prefers project deployment and covers import, CI, recovery, and rollback boundaries', () => { expect(guide).toContain('xapi workers templates'); expect(guide).toContain('xapi workers init my-agent --template persistent-agent');