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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions skills/xapi-workers/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
54 changes: 54 additions & 0 deletions skills/xapi-workers/references/domain-conflict-recovery.md
Original file line number Diff line number Diff line change
@@ -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 <existing-admin-session-or-token>
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": "<worker-id>",
"environmentId": "<environment-id>",
"domainId": "<domain-id>",
"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.
20 changes: 20 additions & 0 deletions src/tests/skill-workers-guide.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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');
Expand Down
Loading