wisewigdocs

Managed-isolated v2 tenant-isolation drill

This drill is retained for the future managed-isolated hosted service. It is not a self-hosted beta gate. Run it first in real workerd during normal CI, then against two explicitly disposable tenants in the target Cloudflare account before enabling v2 production hard deletion.

Local evidence

pnpm --filter @wise-wig/tenant-user-worker test:workerd
pnpm exec vitest run \
  packages/hosting-cloudflare/src/tenant-dispatch.test.ts \
  packages/hosting-cloudflare/src/tenant-archive.test.ts \
  packages/hosting-cloudflare/src/tenant-lifecycle.test.ts \
  apps/tenant-user-worker/src/operator-handler.test.ts

The workerd test creates tenant A and tenant B in distinct SQLite Durable Object identities while deliberately reusing the same site and plugin IDs. It proves:

  • a tenant B public/private dispatch grant is rejected by tenant A’s Worker;
  • tenant A’s private plugin MCP resource returns only tenant A’s record;
  • a browser-shaped tenant A media upload/public delivery/deletion, section edit, page creation, atomic page/navigation/redirect publication, audit query, and public form submission run through managed boundaries without changing or exposing tenant B’s colliding site, media, or submissions;
  • changing the MCP dispatch tenant is rejected before plugin code or storage runs;
  • an operator export contains only the pinned workspace; and
  • a checksum-valid tenant B archive cannot be restored into an empty tenant A workspace.

The archive/lifecycle tests additionally require the persisted Worker, namespace, and release identity; exact RESTORE <tenant-id> confirmation; archive tenant and Worker metadata; a 20 MiB ceiling; SHA-256 recomputation; and an empty restore destination.

Deployed gate

The local real-workerd gate also switches a migrated multi-page draft and theme pin in one tenant SQLite transaction, proves a stale theme version rolls back the page write, and exercises the permission/confirmation boundary in the tenant user Worker. The deployed drill must repeat the switch through the hostname dispatcher and verify tenant B’s theme and pages are unchanged.

Managed publish-operation discovery and scheduling also run in that selected tenant Worker. A schedule pins pages, navigation, redirects, and theme versions; the drill must show colliding site IDs produce independent operation ledgers and that changing any pinned draft resource causes alarm publication to fail without a partial public update.

The local workerd drill creates a managed backup through tenant A’s private command and proves tenant B’s physically separate backup bucket stays empty. The deployed drill must repeat create, list, verify, maintenance, and bookmark recovery through the dispatcher before enabling managed incident controls.

Use tenant IDs and Worker names beginning with wisewig-drill-. Do not reuse customer tenants. Record the account, namespace, release digest, UTC time, test output, archive receipts, and operator who ran the drill in the beta release evidence.

Run the guarded preflight before any provisioning command:

pnpm wise-wig cloudflare managed-isolated preflight \
  --cwd examples/morning-bell-coffee \
  --operator "release-operator" \
  --namespace wisewig-staging

This command is deliberately read-only. It refuses non-disposable tenant IDs and evidence paths outside the selected site, verifies a clean/pinned release and the managed Astro capability boundary, and checks Wrangler authentication plus dispatch-namespace entitlement. It records only stable outcomes in a private local JSON receipt; raw account output and secrets are neither inspected nor retained. Do not provision if its status is blocked or failed.

Before provisioning either tenant, prepare and inspect the browser-facing Astro deployment:

pnpm --filter @wise-wig/morning-bell-coffee build
pnpm wise-wig cloudflare prepare --managed-isolated --cwd examples/morning-bell-coffee --force
pnpm wise-wig doctor --managed-isolated --cwd examples/morning-bell-coffee
pnpm --dir examples/morning-bell-coffee exec wrangler deploy --dry-run --config dist/server/wrangler.wise-wig-managed-isolated.json

Do not continue if the doctor reports a direct TENANT_WORKSPACE or PLUGIN_SANDBOX binding, or if the dispatcher is missing or points to a different service.

  1. Deploy two disposable tenants from the same trusted release. Confirm D1 maps each tenant to a different Worker name and that each upload metadata document binds a script-local SQLite TenantWorkspaceDO, unique R2 bucket, and unique Queue.
  2. Call the operator’s POST /tenants/initialize-site for both tenants with the same site and plugin IDs but unmistakable A/B content and private plugin records. Repeat one exact initialization and confirm it is idempotent; change one existing value and confirm SITE_INITIALIZATION_CONFLICT without mutation.
  3. Verify public requests return only the hostname-selected tenant’s published marker. Repeat preview/edit requests with valid sessions and confirm each returns only its own draft marker.
  4. Use each tenant’s OAuth token to read its private MCP site context and plugin resource. Attempt tenant/header/site substitution and confirm a non-disclosing denial with no tenant B audit/storage mutation.
  5. Through each hostname, invite a distinct member and request a distinct domain. Reuse one idempotency key with changed input and require IDEMPOTENCY_KEY_REUSED; repeat the exact request and require the stored result without a duplicate effect. Attempt tenant/header/site substitution and verify D1 rows and platform audit remain scoped to the resolved tenant/site.
  6. Set tenant A read-only, prove routine writes stop while reads/recovery remain available, then reactivate it. Suspend tenant A and confirm routing stops while tenant B remains available. Verify success/replay audit contains actor, command, input digest, and no submitted personal content.
  7. Export both tenants. Submit tenant B’s receipt to tenant A’s restore endpoint with RESTORE <tenant-a> and confirm rejection before dispatch. Tamper one digest and confirm rejection.
  8. For a disposable tenant, retain the export receipt, execute confirmed erasure scheduling and deletion, redeploy the exact tenant identity from the trusted release, and restore into its empty workspace. Confirm public, editor, plugin, MCP, versions, and audit state match the pre-delete markers.
  9. Attempt a second restore and confirm the non-empty target is rejected. Confirm tenant B remained readable and unchanged throughout tenant A deletion/recovery.
  10. Verify the deleted tenant’s former script, Queue consumer, Queue, and R2 bucket were removed before redeployment, while the operator archive remained. Check for failed tombstones, unexpected provider resources, and sensitive values in logs.

Stop immediately on any cross-tenant data, successful forged request, shared resource binding, missing archive, checksum discrepancy, or unbounded error detail. Keep production hard deletion disabled until every item passes in the actual release account.

Operator restore request

The non-public hosting deployer accepts:

{
  "tenantId": "wisewig-drill-a",
  "workerName": "wisewig-drill-a",
  "dispatchNamespace": "wisewig-staging",
  "release": "beta-candidate",
  "exportId": "tenants/wisewig-drill-a/exports/…json",
  "sha256": "<64 lowercase hex characters>",
  "createdAt": 1784694400000,
  "confirmation": "RESTORE wisewig-drill-a"
}

Send it as POST /tenants/restore with the operator service bearer token. The endpoint is not a public management API: expose it only through an authenticated operator service binding or equivalent private control plane.