@runeditrun/rer 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/bin/rer.mjs +3 -0
- package/cli/README.md +243 -0
- package/cli/adapter.mjs +10 -0
- package/cli/ai-local.mjs +65 -0
- package/cli/archive-path.mjs +52 -0
- package/cli/archive-vite.mjs +55 -0
- package/cli/archive.mjs +8 -0
- package/cli/auth-copy.mjs +80 -0
- package/cli/bounded.mjs +70 -0
- package/cli/cache-doctor.mjs +63 -0
- package/cli/container-transfer.mjs +35 -0
- package/cli/container.mjs +62 -0
- package/cli/contract.mjs +25 -0
- package/cli/dependencies.mjs +45 -0
- package/cli/dependency-approval.mjs +33 -0
- package/cli/dependency-doctor.mjs +34 -0
- package/cli/dependency-input.mjs +43 -0
- package/cli/deploy-config.mjs +118 -0
- package/cli/deploy.mjs +37 -0
- package/cli/dev-identity.mjs +80 -0
- package/cli/errors.mjs +14 -0
- package/cli/export.mjs +42 -0
- package/cli/font-cache.mjs +29 -0
- package/cli/hosted-output.mjs +110 -0
- package/cli/independent.mjs +29 -0
- package/cli/inspect.mjs +174 -0
- package/cli/jsonc.mjs +111 -0
- package/cli/lifecycle.mjs +148 -0
- package/cli/local.mjs +100 -0
- package/cli/main.mjs +57 -0
- package/cli/managed-server.mjs +143 -0
- package/cli/node-runtime-cache.mjs +68 -0
- package/cli/node-runtime-discovery.mjs +56 -0
- package/cli/node-runtime-download.mjs +59 -0
- package/cli/node-runtime.mjs +50 -0
- package/cli/output.mjs +22 -0
- package/cli/parse.mjs +77 -0
- package/cli/port.mjs +52 -0
- package/cli/portability-worker.mjs +56 -0
- package/cli/portability.mjs +66 -0
- package/cli/preview-cache.mjs +99 -0
- package/cli/project.mjs +48 -0
- package/cli/release.mjs +24 -0
- package/cli/runtime-front-door.mjs +46 -0
- package/cli/scheduled.mjs +81 -0
- package/cli/server-activity.mjs +12 -0
- package/cli/server-proxy.mjs +93 -0
- package/cli/server-registry.mjs +182 -0
- package/cli/server-resources.mjs +22 -0
- package/cli/settings.mjs +15 -0
- package/cli/setup-answers.mjs +29 -0
- package/cli/setup-input.mjs +113 -0
- package/cli/shared.mjs +7 -0
- package/cli/transfers.mjs +33 -0
- package/cli/version.mjs +36 -0
- package/composition/git-source.mjs +111 -0
- package/edition-authoring/files.mjs +81 -0
- package/edition-authoring/manifest.mjs +65 -0
- package/global/README.md +158 -0
- package/global/checkout.mjs +72 -0
- package/global/cli/command.mjs +13 -0
- package/global/cli/install.mjs +75 -0
- package/global/cli/layout.mjs +45 -0
- package/global/cli/status.mjs +54 -0
- package/global/cli/uninstall.mjs +36 -0
- package/global/delegate.mjs +13 -0
- package/global/launch/capacity.mjs +72 -0
- package/global/launch/dev.mjs +117 -0
- package/global/launch/facts.mjs +94 -0
- package/global/launch/failure.mjs +5 -0
- package/global/launch/local-defaults.mjs +4 -0
- package/global/launch/prepare.mjs +63 -0
- package/global/launch/render.mjs +44 -0
- package/global/launch/state.mjs +106 -0
- package/global/launch/steps.mjs +90 -0
- package/global/launch/stop.mjs +89 -0
- package/global/launch/worker.mjs +62 -0
- package/global/list.mjs +59 -0
- package/global/main.mjs +53 -0
- package/global/package-info.mjs +38 -0
- package/global/registry.mjs +24 -0
- package/global/rer.mjs +29 -0
- package/global/update/archive.mjs +89 -0
- package/global/update/command.mjs +90 -0
- package/global/update/latest.mjs +6 -0
- package/global/update/notice.mjs +58 -0
- package/global/update/registry.mjs +47 -0
- package/global/version.mjs +29 -0
- package/install.sh +144 -0
- package/modules/ai/LICENSE +21 -0
- package/modules/ai/README.md +90 -0
- package/modules/ai/client.mjs +71 -0
- package/modules/ai/config.mjs +39 -0
- package/modules/ai/dev/index.d.mts +6 -0
- package/modules/ai/dev/index.mjs +64 -0
- package/modules/ai/dev/outbox.mjs +33 -0
- package/modules/ai/errors.mjs +28 -0
- package/modules/ai/fake/index.d.mts +3 -0
- package/modules/ai/fake/index.mjs +82 -0
- package/modules/ai/index.d.mts +10 -0
- package/modules/ai/index.mjs +3 -0
- package/modules/ai/package.json +6 -0
- package/modules/container/README.md +156 -0
- package/modules/container/adapters/d1.mjs +34 -0
- package/modules/container/adapters/egress.mjs +40 -0
- package/modules/container/adapters/jobs.mjs +47 -0
- package/modules/container/adapters/objects.mjs +84 -0
- package/modules/container/adapters/retained.mjs +14 -0
- package/modules/container/adapters/sqlite-error.mjs +7 -0
- package/modules/container/adapters/sqlite-worker.mjs +26 -0
- package/modules/container/assets.mjs +14 -0
- package/modules/container/bindings.mjs +1 -0
- package/modules/container/boundary.mjs +38 -0
- package/modules/container/build.mjs +65 -0
- package/modules/container/deploy-input.mjs +17 -0
- package/modules/container/entrypoint.sh +5 -0
- package/modules/container/host.mjs +43 -0
- package/modules/container/ingress.mjs +23 -0
- package/modules/container/lock.mjs +11 -0
- package/modules/container/migrations.mjs +16 -0
- package/modules/container/node-engine.mjs +39 -0
- package/modules/container/portability-schema.mjs +14 -0
- package/modules/container/remote/deploy.py +76 -0
- package/modules/container/remote/files.py +275 -0
- package/modules/container/remote/resources.py +157 -0
- package/modules/container/runner.mjs +10 -0
- package/modules/container/ssh-plan.mjs +44 -0
- package/modules/container/ssh.mjs +63 -0
- package/modules/container/transfer.mjs +27 -0
- package/modules/container/worker-engine.mjs +32 -0
- package/modules/dependencies/README.md +211 -0
- package/modules/dependencies/approval.mjs +51 -0
- package/modules/dependencies/auth.mjs +40 -0
- package/modules/dependencies/authority.mjs +14 -0
- package/modules/dependencies/crypto.mjs +13 -0
- package/modules/dependencies/index.d.mts +23 -0
- package/modules/dependencies/index.mjs +113 -0
- package/modules/dependencies/logout.mjs +42 -0
- package/modules/dependencies/native.mjs +9 -0
- package/modules/dependencies/protocol.mjs +23 -0
- package/modules/dependencies/schema/approval-attempt.sql +3 -0
- package/modules/dependencies/schema/dependencies.sql +15 -0
- package/modules/dependencies/store.mjs +19 -0
- package/modules/deploy-kit/README.md +308 -0
- package/modules/deploy-kit/artifact.mjs +47 -0
- package/modules/deploy-kit/build-modules.mjs +21 -0
- package/modules/deploy-kit/check.mjs +13 -0
- package/modules/deploy-kit/cli.mjs +50 -0
- package/modules/deploy-kit/container-config.mjs +8 -0
- package/modules/deploy-kit/default-stage.mjs +14 -0
- package/modules/deploy-kit/deploy.mjs +80 -0
- package/modules/deploy-kit/health-request.mjs +86 -0
- package/modules/deploy-kit/health.mjs +99 -0
- package/modules/deploy-kit/index.mjs +12 -0
- package/modules/deploy-kit/local-only.mjs +10 -0
- package/modules/deploy-kit/migrations.mjs +48 -0
- package/modules/deploy-kit/owner-cli.mjs +63 -0
- package/modules/deploy-kit/owner.mjs +297 -0
- package/modules/deploy-kit/prepare.mjs +62 -0
- package/modules/deploy-kit/provider.mjs +99 -0
- package/modules/deploy-kit/queue-consumer.mjs +10 -0
- package/modules/deploy-kit/receipt.mjs +16 -0
- package/modules/deploy-kit/render.mjs +47 -0
- package/modules/deploy-kit/resources.mjs +106 -0
- package/modules/deploy-kit/service-grants.mjs +25 -0
- package/modules/deploy-kit/sql-safety.mjs +94 -0
- package/modules/deploy-kit/sql-tokens.mjs +49 -0
- package/modules/deploy-kit/teardown.mjs +100 -0
- package/modules/deploy-kit/templates.mjs +31 -0
- package/modules/deploy-kit/validation.mjs +58 -0
- package/modules/deploy-kit/worker.mjs +105 -0
- package/modules/erasure/CONTRACT.md +90 -0
- package/modules/erasure/README.md +221 -0
- package/modules/erasure/audit.mjs +23 -0
- package/modules/erasure/blobs.mjs +45 -0
- package/modules/erasure/cascade.mjs +91 -0
- package/modules/erasure/completion.mjs +62 -0
- package/modules/erasure/declaration.mjs +51 -0
- package/modules/erasure/fence.mjs +19 -0
- package/modules/erasure/index.d.mts +62 -0
- package/modules/erasure/index.mjs +85 -0
- package/modules/erasure/order.mjs +56 -0
- package/modules/erasure/plan.mjs +121 -0
- package/modules/erasure/rate.mjs +57 -0
- package/modules/erasure/receipt.mjs +32 -0
- package/modules/erasure/root.mjs +21 -0
- package/modules/erasure/schema/erasure-rates.sql +5 -0
- package/modules/erasure/schema/erasure.sql +27 -0
- package/modules/erasure/scope.mjs +29 -0
- package/modules/erasure/sql.mjs +23 -0
- package/modules/erasure/verification.mjs +69 -0
- package/modules/integrations/core/CONTRACT.md +50 -0
- package/modules/integrations/core/README.md +27 -0
- package/modules/integrations/core/crypto.ts +19 -0
- package/modules/integrations/core/database.ts +21 -0
- package/modules/integrations/core/delivery.ts +97 -0
- package/modules/integrations/core/destination.ts +13 -0
- package/modules/integrations/core/index.ts +7 -0
- package/modules/integrations/core/node-transport.ts +72 -0
- package/modules/integrations/core/public-address.ts +57 -0
- package/modules/integrations/core/repository.ts +74 -0
- package/modules/integrations/core/schema.sql +36 -0
- package/modules/integrations/core/sqlite.ts +20 -0
- package/modules/integrations/core/timestamped-body-signature.ts +38 -0
- package/modules/integrations/core/tokens.ts +71 -0
- package/modules/integrations/core/types.ts +52 -0
- package/modules/integrations/core/upgrade-token-usage-context.sql +5 -0
- package/modules/integrations/core/webhook.ts +73 -0
- package/modules/job-queue/README.md +173 -0
- package/modules/job-queue/cli.mjs +64 -0
- package/modules/job-queue/command.mjs +64 -0
- package/modules/job-queue/config.mjs +48 -0
- package/modules/job-queue/identity.mjs +48 -0
- package/modules/job-queue/index.mjs +135 -0
- package/modules/job-queue/membership.mjs +30 -0
- package/modules/job-queue/module.json +10 -0
- package/modules/job-queue/policy.mjs +29 -0
- package/modules/job-queue/processes.mjs +80 -0
- package/modules/job-queue/resources.mjs +37 -0
- package/modules/job-queue/storage.mjs +64 -0
- package/modules/job-queue/supervisor.mjs +30 -0
- package/modules/job-queue/waiting.mjs +16 -0
- package/modules/mail/LICENSE +21 -0
- package/modules/mail/README.md +336 -0
- package/modules/mail/doctor.mjs +81 -0
- package/modules/mail/index.mjs +8 -0
- package/modules/mail/live-proof/README.md +113 -0
- package/modules/mail/live-proof/run.mjs +55 -0
- package/modules/mail/package.json +19 -0
- package/modules/mail/pnpm-lock.yaml +537 -0
- package/modules/mail/src/adapters/cloudflare/README.md +84 -0
- package/modules/mail/src/adapters/cloudflare/headers.mjs +63 -0
- package/modules/mail/src/adapters/cloudflare/index.mjs +47 -0
- package/modules/mail/src/adapters/cloudflare/response.mjs +99 -0
- package/modules/mail/src/adapters/messaging/README.md +92 -0
- package/modules/mail/src/adapters/messaging/contract.mjs +66 -0
- package/modules/mail/src/adapters/messaging/event-schema.mjs +28 -0
- package/modules/mail/src/adapters/messaging/index.mjs +61 -0
- package/modules/mail/src/adapters/messaging/jws.mjs +68 -0
- package/modules/mail/src/adapters/messaging/request.mjs +52 -0
- package/modules/mail/src/adapters/messaging/serialize.mjs +21 -0
- package/modules/mail/src/adapters/resend/README.md +120 -0
- package/modules/mail/src/adapters/resend/index.mjs +22 -0
- package/modules/mail/src/adapters/resend/response.mjs +54 -0
- package/modules/mail/src/adapters/resend/serialize.mjs +46 -0
- package/modules/mail/src/adapters/resend/size.mjs +99 -0
- package/modules/mail/src/adapters/sendgrid/README.md +75 -0
- package/modules/mail/src/adapters/sendgrid/index.mjs +35 -0
- package/modules/mail/src/adapters/sendgrid/serialize.mjs +104 -0
- package/modules/mail/src/core/errors.mjs +9 -0
- package/modules/mail/src/core/transport.mjs +33 -0
- package/modules/mail/src/core/validation.mjs +172 -0
- package/modules/mail/src/development/index.d.mts +37 -0
- package/modules/mail/src/development/index.mjs +35 -0
- package/modules/mail/src/development/outbox.mjs +78 -0
- package/modules/mail/src/development/schema.sql +9 -0
- package/modules/mail/src/development/viewer-html.mjs +42 -0
- package/modules/mail/src/development/viewer.mjs +65 -0
- package/modules/mail/src/doctor/README.md +107 -0
- package/modules/mail/src/doctor/common.mjs +64 -0
- package/modules/mail/src/doctor/dns.mjs +102 -0
- package/modules/mail/src/doctor/index.mjs +28 -0
- package/modules/mail/src/doctor/probe.mjs +66 -0
- package/modules/mail/src/doctor/receive.mjs +81 -0
- package/modules/mail/src/doctor/records.mjs +45 -0
- package/modules/mail/src/events/README.md +268 -0
- package/modules/mail/src/events/d1-schema.sql +25 -0
- package/modules/mail/src/events/d1-storage.mjs +76 -0
- package/modules/mail/src/events/index.mjs +3 -0
- package/modules/mail/src/events/intake.mjs +201 -0
- package/modules/mail/src/events/storage.mjs +52 -0
- package/modules/mail/src/events/verify.mjs +92 -0
- package/modules/mail/src/inbound/README.md +137 -0
- package/modules/mail/src/inbound/bytes.mjs +91 -0
- package/modules/mail/src/inbound/headers.mjs +143 -0
- package/modules/mail/src/inbound/index.mjs +29 -0
- package/modules/mail/src/inbound/mime.mjs +36 -0
- package/modules/mail/src/inbound/multipart.mjs +27 -0
- package/modules/mail/src/inbound/resend-attachment.mjs +43 -0
- package/modules/mail/src/inbound/resend.mjs +54 -0
- package/modules/mail/src/inbound/sendgrid.mjs +60 -0
- package/modules/mail/src/sender.mjs +33 -0
- package/modules/mail/src/sendgrid-email/index.d.mts +33 -0
- package/modules/mail/src/sendgrid-email/index.mjs +86 -0
- package/modules/mail/src/unsubscribe/index.mjs +142 -0
- package/modules/operator-auth/README.md +599 -0
- package/modules/operator-auth/accounts/adapter.mjs +41 -0
- package/modules/operator-auth/accounts/admin.mjs +159 -0
- package/modules/operator-auth/accounts/credentials.mjs +27 -0
- package/modules/operator-auth/accounts/editor-owner.mjs +13 -0
- package/modules/operator-auth/accounts/index.d.mts +9 -0
- package/modules/operator-auth/accounts/index.mjs +9 -0
- package/modules/operator-auth/accounts/native.d.mts +38 -0
- package/modules/operator-auth/accounts/native.mjs +171 -0
- package/modules/operator-auth/accounts/oa.mjs +90 -0
- package/modules/operator-auth/accounts/provider-control.mjs +50 -0
- package/modules/operator-auth/accounts/provider-pin.mjs +14 -0
- package/modules/operator-auth/accounts/roles.mjs +26 -0
- package/modules/operator-auth/accounts/schema/dependency-setup.sql +17 -0
- package/modules/operator-auth/accounts/schema/dev-sessions.sql +22 -0
- package/modules/operator-auth/accounts/schema/domain-rules-step-up.sql +20 -0
- package/modules/operator-auth/accounts/schema/invitation-cancellation.sql +17 -0
- package/modules/operator-auth/accounts/schema/operator-auth.sql +142 -0
- package/modules/operator-auth/accounts/schema/provider-control.sql +30 -0
- package/modules/operator-auth/accounts/schema/provider-pins.sql +6 -0
- package/modules/operator-auth/accounts/settings-page.mjs +4 -0
- package/modules/operator-auth/accounts/store.mjs +246 -0
- package/modules/operator-auth/branding.mjs +66 -0
- package/modules/operator-auth/config.mjs +35 -0
- package/modules/operator-auth/conformance/README.md +81 -0
- package/modules/operator-auth/conformance/index.d.mts +43 -0
- package/modules/operator-auth/conformance/index.mjs +300 -0
- package/modules/operator-auth/conformance/presentation.mjs +57 -0
- package/modules/operator-auth/crypto.mjs +17 -0
- package/modules/operator-auth/dev/badge.mjs +62 -0
- package/modules/operator-auth/dev/index.d.mts +14 -0
- package/modules/operator-auth/dev/index.mjs +137 -0
- package/modules/operator-auth/dev/personas.mjs +17 -0
- package/modules/operator-auth/dev/sign-in-as.d.mts +3 -0
- package/modules/operator-auth/dev/sign-in-as.mjs +25 -0
- package/modules/operator-auth/dev-admission.d.mts +4 -0
- package/modules/operator-auth/dev-admission.mjs +10 -0
- package/modules/operator-auth/dev-gate.mjs +86 -0
- package/modules/operator-auth/docs/README.md +78 -0
- package/modules/operator-auth/docs/native-accounts.md +313 -0
- package/modules/operator-auth/docs/tanstack-start.md +218 -0
- package/modules/operator-auth/docs/vite-spa.md +144 -0
- package/modules/operator-auth/errors.mjs +9 -0
- package/modules/operator-auth/http.mjs +42 -0
- package/modules/operator-auth/identity/admission.mjs +105 -0
- package/modules/operator-auth/identity/after-confirm.mjs +18 -0
- package/modules/operator-auth/identity/email-link/confirmation.mjs +14 -0
- package/modules/operator-auth/identity/email-link/index.d.mts +48 -0
- package/modules/operator-auth/identity/email-link/index.mjs +128 -0
- package/modules/operator-auth/identity/email-link/message.mjs +16 -0
- package/modules/operator-auth/identity/index.d.mts +7 -0
- package/modules/operator-auth/identity/index.mjs +5 -0
- package/modules/operator-auth/identity/oidc/README.md +171 -0
- package/modules/operator-auth/identity/oidc/client-auth.mjs +16 -0
- package/modules/operator-auth/identity/oidc/config.mjs +102 -0
- package/modules/operator-auth/identity/oidc/crypto.mjs +29 -0
- package/modules/operator-auth/identity/oidc/errors.mjs +5 -0
- package/modules/operator-auth/identity/oidc/index.d.mts +33 -0
- package/modules/operator-auth/identity/oidc/index.mjs +287 -0
- package/modules/operator-auth/identity/oidc/jwt.mjs +102 -0
- package/modules/operator-auth/identity/oidc/network.mjs +93 -0
- package/modules/operator-auth/identity/oidc/presets.mjs +26 -0
- package/modules/operator-auth/identity/oidc/schema/oidc.sql +50 -0
- package/modules/operator-auth/identity/oidc/store.mjs +68 -0
- package/modules/operator-auth/identity/principal.mjs +28 -0
- package/modules/operator-auth/index.d.mts +223 -0
- package/modules/operator-auth/index.mjs +11 -0
- package/modules/operator-auth/input.mjs +55 -0
- package/modules/operator-auth/local-cli.mjs +16 -0
- package/modules/operator-auth/page.mjs +34 -0
- package/modules/operator-auth/policy.mjs +20 -0
- package/modules/operator-auth/routes.mjs +91 -0
- package/modules/operator-auth/service.mjs +152 -0
- package/modules/operator-auth/signin/index.mjs +67 -0
- package/modules/owner-claim/README.md +137 -0
- package/modules/owner-claim/idp.mjs +40 -0
- package/modules/owner-claim/index.d.mts +27 -0
- package/modules/owner-claim/index.mjs +39 -0
- package/modules/owner-claim/page.mjs +11 -0
- package/modules/owner-claim/schema/idp.sql +4 -0
- package/modules/owner-claim/schema/owner-claim.sql +18 -0
- package/modules/owner-claim/transaction.mjs +87 -0
- package/modules/owner-claim/verify.mjs +80 -0
- package/modules/portability/README.md +444 -0
- package/modules/portability/bridge.mjs +102 -0
- package/modules/portability/catalogue.mjs +118 -0
- package/modules/portability/cli-options.mjs +38 -0
- package/modules/portability/cli.mjs +56 -0
- package/modules/portability/copies.mjs +68 -0
- package/modules/portability/dev-data.mjs +32 -0
- package/modules/portability/diff.mjs +33 -0
- package/modules/portability/export.mjs +129 -0
- package/modules/portability/files.mjs +65 -0
- package/modules/portability/format.mjs +122 -0
- package/modules/portability/generated.mjs +60 -0
- package/modules/portability/hash.mjs +79 -0
- package/modules/portability/http.mjs +42 -0
- package/modules/portability/import.mjs +185 -0
- package/modules/portability/index.d.mts +58 -0
- package/modules/portability/index.mjs +22 -0
- package/modules/portability/journal.mjs +49 -0
- package/modules/portability/legacy-files.mjs +43 -0
- package/modules/portability/legacy-safety.mjs +27 -0
- package/modules/portability/lines.mjs +36 -0
- package/modules/portability/migration-history.mjs +18 -0
- package/modules/portability/migration-statements.mjs +23 -0
- package/modules/portability/payloads.mjs +52 -0
- package/modules/portability/provider-data.mjs +25 -0
- package/modules/portability/restore-safety.mjs +30 -0
- package/modules/portability/schema/portability.sql +25 -0
- package/modules/portability/storage.mjs +49 -0
- package/modules/release/README.md +212 -0
- package/modules/release/catalogue.mjs +79 -0
- package/modules/release/export.mjs +51 -0
- package/modules/release/produce.mjs +136 -0
- package/modules/release/public-child-transport-state.cjs +29 -0
- package/modules/release/public-child.mjs +71 -0
- package/modules/release/public-git.mjs +21 -0
- package/modules/release/publication-scan.mjs +22 -0
- package/modules/release/publish-handoff.mjs +37 -0
- package/modules/release/source.mjs +71 -0
- package/modules/release/test-summary.mjs +50 -0
- package/onboarding/README.md +163 -0
- package/onboarding/activity.mjs +31 -0
- package/onboarding/adapter-auth.mjs +88 -0
- package/onboarding/adapter-hosting.mjs +35 -0
- package/onboarding/adapter-labels.mjs +13 -0
- package/onboarding/adapter-settings.mjs +129 -0
- package/onboarding/adapter.mjs +238 -0
- package/onboarding/agent-handoff.mjs +9 -0
- package/onboarding/app-processes.mjs +21 -0
- package/onboarding/archive-output.d.mts +3 -0
- package/onboarding/archive-output.mjs +54 -0
- package/onboarding/command-custody.mjs +24 -0
- package/onboarding/command-recovery.mjs +8 -0
- package/onboarding/commands.mjs +166 -0
- package/onboarding/dependency-approval.mjs +36 -0
- package/onboarding/dev-identity.mjs +23 -0
- package/onboarding/email.mjs +24 -0
- package/onboarding/failures.mjs +42 -0
- package/onboarding/input-limits.mjs +7 -0
- package/onboarding/installer-inputs.mjs +24 -0
- package/onboarding/launcher.mjs +34 -0
- package/onboarding/local/build-files.mjs +64 -0
- package/onboarding/local/build.mjs +49 -0
- package/onboarding/local/environment.mjs +30 -0
- package/onboarding/local/files.mjs +35 -0
- package/onboarding/local/operation.mjs +166 -0
- package/onboarding/local/owner.mjs +34 -0
- package/onboarding/local/process-table.mjs +114 -0
- package/onboarding/local/recovery.mjs +79 -0
- package/onboarding/local/state.mjs +31 -0
- package/onboarding/metadata.mjs +7 -0
- package/onboarding/operator-install.mjs +45 -0
- package/onboarding/operator-installer.mjs +39 -0
- package/onboarding/operator-prerequisites.mjs +27 -0
- package/onboarding/package.json +6 -0
- package/onboarding/prefill.mjs +16 -0
- package/onboarding/private-files.mjs +97 -0
- package/onboarding/private.mjs +35 -0
- package/onboarding/process-cleanup.mjs +134 -0
- package/onboarding/process-diagnostics.mjs +91 -0
- package/onboarding/process-scope.mjs +118 -0
- package/onboarding/process-table.mjs +118 -0
- package/onboarding/process-tree.mjs +36 -0
- package/onboarding/production-modules.mjs +22 -0
- package/onboarding/production-options.mjs +40 -0
- package/onboarding/production-plan.mjs +8 -0
- package/onboarding/production-runtime.mjs +30 -0
- package/onboarding/project-vite.mjs +14 -0
- package/onboarding/providers.mjs +289 -0
- package/onboarding/queue.mjs +53 -0
- package/onboarding/queued-entry.mjs +34 -0
- package/onboarding/readiness.mjs +59 -0
- package/onboarding/redaction-jwt.mjs +32 -0
- package/onboarding/redaction.mjs +298 -0
- package/onboarding/schema/SETUP.schema.json +514 -0
- package/onboarding/schema/dependencies.mjs +35 -0
- package/onboarding/schema/json-schema.mjs +202 -0
- package/onboarding/schema/setup-validation.mjs +325 -0
- package/onboarding/secrets.mjs +21 -0
- package/onboarding/server.mjs +188 -0
- package/onboarding/setup-manifest.mjs +116 -0
- package/onboarding/state.mjs +46 -0
- package/onboarding/theme-files.mjs +37 -0
- package/onboarding/theme.mjs +118 -0
- package/onboarding/ui/activity.js +98 -0
- package/onboarding/ui/address-client.mjs +33 -0
- package/onboarding/ui/app.js +377 -0
- package/onboarding/ui/index.html +1 -0
- package/onboarding/ui/model-core.mjs +127 -0
- package/onboarding/ui/model.mjs +5 -0
- package/onboarding/ui/style.css +63 -0
- package/onboarding/ui/theme.css +37 -0
- package/onboarding/ui/workspace-art.js +30 -0
- package/onboarding/wrangler-auth.mjs +43 -0
- package/operations/README.md +246 -0
- package/operations/operations.mjs +168 -0
- package/operations/project-operations.mjs +157 -0
- package/package.json +11 -4
- package/scripts/publish-check/README.md +72 -0
- package/scripts/publish-check/checksum_maps.py +52 -0
- package/scripts/publish-check/cli.py +34 -0
- package/scripts/publish-check/command_arguments.py +51 -0
- package/scripts/publish-check/command_shell_quotes.py +41 -0
- package/scripts/publish-check/command_source_quotes.py +120 -0
- package/scripts/publish-check/content_identities.py +250 -0
- package/scripts/publish-check/copy_identities.py +41 -0
- package/scripts/publish-check/der.py +61 -0
- package/scripts/publish-check/destination.py +117 -0
- package/scripts/publish-check/env_values.py +74 -0
- package/scripts/publish-check/font_licence.py +23 -0
- package/scripts/publish-check/git_history.py +115 -0
- package/scripts/publish-check/github_refs.py +217 -0
- package/scripts/publish-check/gitleaks.py +72 -0
- package/scripts/publish-check/gitleaks.toml +40 -0
- package/scripts/publish-check/history_scan.py +176 -0
- package/scripts/publish-check/hit_location.py +18 -0
- package/scripts/publish-check/hygiene_config.py +42 -0
- package/scripts/publish-check/hygiene_text.py +11 -0
- package/scripts/publish-check/identity-recipe.mjs +54 -0
- package/scripts/publish-check/json_identity.py +79 -0
- package/scripts/publish-check/license-exceptions.json +11 -0
- package/scripts/publish-check/licenses.py +49 -0
- package/scripts/publish-check/policy.py +18 -0
- package/scripts/publish-check/project_identity.py +89 -0
- package/scripts/publish-check/provenance.py +91 -0
- package/scripts/publish-check/provenance_pins.py +33 -0
- package/scripts/publish-check/public_base.py +63 -0
- package/scripts/publish-check/public_hygiene.py +80 -0
- package/scripts/publish-check/publication-config.json +12 -0
- package/scripts/publish-check/publication_config.py +34 -0
- package/scripts/publish-check/report.py +52 -0
- package/scripts/publish-check/repository_commands.py +75 -0
- package/scripts/publish-check/repository_intent.py +52 -0
- package/scripts/publish-check/reviewed.py +41 -0
- package/scripts/publish-check/source_hashes.mjs +6 -0
- package/scripts/publish-check/tree_scan.py +88 -0
- package/scripts/publish-check/trust_bundle.py +87 -0
- package/scripts/publish-check.mjs +58 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { access, mkdir, readdir, copyFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { resolve, join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { assert } from './validation.mjs';
|
|
7
|
+
const base = resolve(dirname(fileURLToPath(import.meta.url)),'../..');
|
|
8
|
+
/** Generate alongside an edition. The existing edition tree is never an output directory. */
|
|
9
|
+
export async function createTemplates({destination,project,sourcePaths,commands,loops=[]}) {
|
|
10
|
+
assert(/^[a-z0-9-]+\/[a-z0-9-]+$/.test(project),'invalid-project');
|
|
11
|
+
assert(Array.isArray(sourcePaths) && sourcePaths.length && sourcePaths.every(x => typeof x === 'string' && x.length),'missing-actual-source-paths');
|
|
12
|
+
const root = resolve(destination);
|
|
13
|
+
await mkdir(root,{recursive:false});
|
|
14
|
+
// A new directory only: callers add actual edition sources then run the lock command.
|
|
15
|
+
const scripts = join(root,'scripts'); await mkdir(scripts);
|
|
16
|
+
await mkdir(join(scripts,'edition-authoring'));
|
|
17
|
+
for (const name of ['operations.mjs','project-operations.mjs']) await copyFile(join(base,'operations',name),join(scripts,name),constants.COPYFILE_EXCL);
|
|
18
|
+
for (const name of await readdir(join(base,'edition-authoring'))) if (name.endsWith('.mjs') && !name.endsWith('.test.mjs')) await copyFile(join(base,'edition-authoring',name),join(scripts,'edition-authoring',name),constants.COPYFILE_EXCL);
|
|
19
|
+
await writeFile(join(root,'OPERATIONS.json'),JSON.stringify({version:2,project,runtime:{requirements:[{name:'node',version:'>=22.12.0'}]},requiredEnvironment:[],commands,loops},null,2)+'\n',{flag:'wx'});
|
|
20
|
+
await writeFile(join(root,'edition.json'),JSON.stringify({schemaVersion:1,id:project,toolchain:{id:'runeditrun/edition-authoring',version:'1.1.0'},parent:null,components:[{id:project,role:'edition',source:{kind:'local',paths:sourcePaths}}]},null,2)+'\n',{flag:'wx'});
|
|
21
|
+
return {root,lock:['node','scripts/edition-authoring/cli.mjs','lock'],check:['node','scripts/edition-authoring/cli.mjs','check']};
|
|
22
|
+
}
|
|
23
|
+
/** Actual authoring tool creates the lock and retained sources; no hand-forged lock template. */
|
|
24
|
+
export async function lockTemplates(root) {
|
|
25
|
+
try { await access(join(root,'sources.lock.json')); throw new Error('existing-source-lock-refused'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
26
|
+
const env = {PATH:process.env.PATH,HOME:process.env.HOME};
|
|
27
|
+
for (const command of ['lock','check']) {
|
|
28
|
+
try { execFileSync(process.execPath,[join(base,'edition-authoring/cli.mjs'),command],{cwd:root,env,stdio:'pipe',timeout:60000,maxBuffer:1024*1024}); }
|
|
29
|
+
catch { throw new Error('edition-authoring-failed'); }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export const hash = value => createHash('sha256').update(typeof value === 'string' || Buffer.isBuffer(value) ? value : stable(value)).digest('hex');
|
|
3
|
+
export function stable(value) {
|
|
4
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
5
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`;
|
|
6
|
+
return JSON.stringify(value);
|
|
7
|
+
}
|
|
8
|
+
export function assert(value, code) { if (!value) throw new Error(code); }
|
|
9
|
+
export const identifier = value => typeof value === 'string' && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(value);
|
|
10
|
+
export const resourceName = value => typeof value === 'string' && /^[a-z][a-z0-9-]{2,62}$/.test(value);
|
|
11
|
+
export const uuid = value => typeof value === 'string' && /^(?!00000000-0000-0000-0000-000000000000)[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value);
|
|
12
|
+
export const hexId = value => typeof value === 'string' && /^[a-f0-9]{32}$/i.test(value) && !/^0+$/.test(value);
|
|
13
|
+
export function localPath(value) { assert(typeof value === 'string' && value.length && !value.startsWith('/') && !value.includes('\\') && value.split('/').every(x => x && x !== '.' && x !== '..') && !/[\x00-\x1f]/.test(value), 'invalid-local-path'); return value; }
|
|
14
|
+
/** Declared KV namespaces; absent means none. */
|
|
15
|
+
export const kvOf = plan => plan.kv ?? [];
|
|
16
|
+
export function validatePlan(plan) {
|
|
17
|
+
assert(plan?.version === 1 && resourceName(plan.worker) && /^\d{4}-\d{2}-\d{2}$/.test(plan.compatibilityDate), 'invalid-plan');
|
|
18
|
+
for (const key of ['d1','r2','queues','durableObjects','secrets','crons']) assert(Array.isArray(plan[key]), 'invalid-plan-resources');
|
|
19
|
+
// KV is optional so plans written before KV support keep their exact shape.
|
|
20
|
+
assert(plan.kv === undefined || Array.isArray(plan.kv), 'invalid-plan-resources');
|
|
21
|
+
const kv = kvOf(plan);
|
|
22
|
+
const bindings = new Set(['RER_DEPLOY_SOURCE','RER_DEPLOY_FINGERPRINT','RER_VERSION']);
|
|
23
|
+
// Service bindings and the workers.dev opt-out are optional so earlier plans keep their exact shape.
|
|
24
|
+
assert(plan.services === undefined || Array.isArray(plan.services) && plan.services.length, 'invalid-plan-resources');
|
|
25
|
+
assert(plan.workersDev === undefined || plan.workersDev === false, 'invalid-plan');
|
|
26
|
+
for (const entry of [...plan.d1,...plan.r2,...kv,...plan.queues,...plan.durableObjects,...plan.secrets,...(plan.services ?? [])]) {
|
|
27
|
+
assert(identifier(entry.binding) && !bindings.has(entry.binding), 'duplicate-or-invalid-binding'); bindings.add(entry.binding);
|
|
28
|
+
}
|
|
29
|
+
for (const entry of [...plan.d1,...plan.r2,...kv,...plan.queues]) assert(resourceName(entry.name), 'invalid-resource-name');
|
|
30
|
+
for (const list of [plan.d1,plan.r2,kv,plan.queues]) assert(new Set(list.map(x => x.name)).size === list.length, 'duplicate-resource-name');
|
|
31
|
+
for (const entry of plan.d1) assert(Array.isArray(entry.migrations), 'invalid-migrations');
|
|
32
|
+
for (const entry of plan.durableObjects) assert(identifier(entry.className) && ['sqlite','classic'].includes(entry.storage), 'invalid-durable-object');
|
|
33
|
+
for (const secret of plan.secrets) assert(identifier(secret.environment), 'invalid-secret-environment');
|
|
34
|
+
for (const item of plan.services ?? []) assert(Object.keys(item).every(k => ['binding','service','entrypoint','props','grant'].includes(k)) && resourceName(item.service) && item.service !== plan.worker
|
|
35
|
+
&& (item.entrypoint === undefined || identifier(item.entrypoint))
|
|
36
|
+
&& (item.props === undefined || Object.keys(item.props).join() === 'allowedHosts' && Array.isArray(item.props.allowedHosts)
|
|
37
|
+
&& item.props.allowedHosts.every(host => typeof host === 'string' && /^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host))), 'invalid-service-binding');
|
|
38
|
+
for (const item of plan.services ?? []) if (item.grant !== undefined) {
|
|
39
|
+
const grant = item.grant;
|
|
40
|
+
assert(item.entrypoint && !item.props && grant && Object.keys(grant).sort().join() === 'allowedHosts,fingerprint,provider,sourceSha'
|
|
41
|
+
&& /^[a-f0-9]{40}$/.test(grant.sourceSha) && /^[a-f0-9]{64}$/.test(grant.fingerprint)
|
|
42
|
+
&& grant.provider && Object.keys(grant.provider).sort().join() === 'deploymentId,etag,versionId'
|
|
43
|
+
&& uuid(grant.provider.versionId) && uuid(grant.provider.deploymentId) && typeof grant.provider.etag === 'string' && grant.provider.etag.length > 0
|
|
44
|
+
&& (grant.allowedHosts === null || Array.isArray(grant.allowedHosts) && new Set(grant.allowedHosts).size === grant.allowedHosts.length
|
|
45
|
+
&& grant.allowedHosts.every(host => typeof host === 'string' && /^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(host))), 'invalid-service-grant');
|
|
46
|
+
}
|
|
47
|
+
for (const cron of plan.crons) assert(typeof cron === 'string' && /^[\d*/,-]+(?: [\d*/,-]+){4}$/.test(cron), 'invalid-cron');
|
|
48
|
+
assert(new Set(plan.crons).size === plan.crons.length, 'duplicate-cron');
|
|
49
|
+
for (const [key,value] of Object.entries(plan.vars ?? {})) assert(identifier(key) && !bindings.has(key) && typeof value === 'string', 'invalid-public-variable');
|
|
50
|
+
for (const queue of plan.queues) {
|
|
51
|
+
if (queue.deadLetterQueue !== undefined) assert(queue.consumer && resourceName(queue.deadLetterQueue) && queue.deadLetterQueue !== queue.name && plan.queues.some(x => x.name === queue.deadLetterQueue && !x.consumer), 'invalid-dead-letter-queue');
|
|
52
|
+
if (queue.consumer) {
|
|
53
|
+
assert(Object.keys(queue.consumer).every(k => ['batch_size','max_retries','max_wait_time_ms','max_concurrency','retry_delay'].includes(k)), 'invalid-consumer-setting');
|
|
54
|
+
assert(Object.values(queue.consumer).every(x => Number.isSafeInteger(x) && x >= 0), 'invalid-consumer-setting');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return plan;
|
|
58
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { assert, hash, hexId, uuid } from './validation.mjs';
|
|
3
|
+
import { moduleType, assetType } from './artifact.mjs';
|
|
4
|
+
import { RUN_WORKER_FIRST } from './render.mjs';
|
|
5
|
+
|
|
6
|
+
export async function activeWorker(api, worker) {
|
|
7
|
+
let settings;
|
|
8
|
+
try { settings = await api.request(`/workers/scripts/${worker}/settings`); }
|
|
9
|
+
catch (error) { if (error.status === 404) return null; throw error; }
|
|
10
|
+
assert(Array.isArray(settings.bindings),'invalid-worker-settings');
|
|
11
|
+
const result = await api.request(`/workers/scripts/${worker}/deployments`);
|
|
12
|
+
assert(Array.isArray(result.deployments) && result.deployments.length > 0, 'invalid-deployments');
|
|
13
|
+
const deployment = result.deployments[0];
|
|
14
|
+
assert(uuid(deployment.id) && deployment.versions?.length === 1 && deployment.versions[0].percentage === 100 && uuid(deployment.versions[0].version_id), 'mixed-or-invalid-deployment');
|
|
15
|
+
const versionId = deployment.versions[0].version_id;
|
|
16
|
+
const version = await api.request(`/workers/scripts/${worker}/versions/${versionId}`);
|
|
17
|
+
assert(version.id === versionId && typeof version.resources?.script?.etag === 'string' && version.resources.script.etag.length > 0, 'invalid-active-version');
|
|
18
|
+
return {settings,versionId,deploymentId:deployment.id,etag:version.resources.script.etag,version};
|
|
19
|
+
}
|
|
20
|
+
/** Read authenticated runtime detection, never infer handlers from uploaded source text. */
|
|
21
|
+
export function verifyScheduledHandler(active, plan) {
|
|
22
|
+
if (plan.crons.length) assert(Array.isArray(active?.version?.resources?.script?.handlers) && active.version.resources.script.handlers.includes('scheduled'), 'missing-scheduled-handler');
|
|
23
|
+
}
|
|
24
|
+
/** The build fingerprint a Worker's settings (or an upload's metadata) carry as a plain-text binding. */
|
|
25
|
+
export const deployedFingerprint = settings => settings?.bindings?.find(x => x.type === 'plain_text' && x.name === 'RER_DEPLOY_FINGERPRINT')?.text;
|
|
26
|
+
export async function durableNamespaces(api, plan, { requireAll = false } = {}) {
|
|
27
|
+
if (!plan.durableObjects.length) return [];
|
|
28
|
+
const rows = await api.list('/workers/durable_objects/namespaces');
|
|
29
|
+
return plan.durableObjects.map(item => {
|
|
30
|
+
const selected = rows.filter(x => x.script === plan.worker && x.class === item.className);
|
|
31
|
+
assert(selected.length <= 1, 'ambiguous-durable-object');
|
|
32
|
+
if (!selected.length) { assert(!requireAll,'missing-durable-object'); return {...item,id:null}; }
|
|
33
|
+
const row = selected[0]; assert(hexId(row.id) && row.use_sqlite === (item.storage === 'sqlite'), 'incompatible-durable-object');
|
|
34
|
+
return {...item,id:row.id};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function durableMigration(namespaces, current) {
|
|
38
|
+
const missing = namespaces.filter(x => !x.id);
|
|
39
|
+
if (!missing.length) return undefined;
|
|
40
|
+
assert(missing.every(x => x.storage === 'sqlite'),'new-durable-objects-require-sqlite');
|
|
41
|
+
const old = current?.settings.migration_tag ?? '';
|
|
42
|
+
return {old_tag:old,new_tag:`rer-${hash({old,classes:missing.map(x => x.className)}).slice(0,24)}`,new_sqlite_classes:missing.map(x => x.className)};
|
|
43
|
+
}
|
|
44
|
+
export async function secretBindings(api, worker, plan, env, exists) {
|
|
45
|
+
const present = exists ? await api.request(`/workers/scripts/${worker}/secrets`) : [];
|
|
46
|
+
assert(Array.isArray(present) && present.every(x => typeof x.name === 'string' && x.type === 'secret_text'),'invalid-secret-list');
|
|
47
|
+
const bindings = []; const names = present.map(x => x.name);
|
|
48
|
+
for (const item of plan.secrets) {
|
|
49
|
+
if (names.includes(item.binding)) bindings.push({type:'inherit',name:item.binding});
|
|
50
|
+
else {
|
|
51
|
+
assert(typeof env[item.environment] === 'string' && env[item.environment].trim(), `missing-secret-${item.environment}`);
|
|
52
|
+
bindings.push({type:'secret_text',name:item.binding,text:env[item.environment]});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {bindings,names,missing:plan.secrets.filter(x => !names.includes(x.binding)).map(x => x.binding)};
|
|
56
|
+
}
|
|
57
|
+
export async function uploadAssets(api, worker, files) {
|
|
58
|
+
const manifest = {}; const byHash = new Map();
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const digest = hash(file.base64 + extname(file.path).slice(1)).slice(0,32);
|
|
61
|
+
manifest[`/${file.path}`] = {hash:digest,size:Buffer.from(file.base64,'base64').length};
|
|
62
|
+
byHash.set(digest,file);
|
|
63
|
+
}
|
|
64
|
+
const session = await api.request(`/workers/scripts/${worker}/assets-upload-session`,{method:'POST',body:{manifest}});
|
|
65
|
+
assert(typeof session.jwt === 'string' && session.jwt && Array.isArray(session.buckets),'invalid-asset-session');
|
|
66
|
+
let completion = session.buckets.length ? null : session.jwt;
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
for (const bucket of session.buckets) {
|
|
69
|
+
assert(Array.isArray(bucket) && bucket.length,'invalid-asset-bucket');
|
|
70
|
+
const data = new FormData();
|
|
71
|
+
for (const key of bucket) {
|
|
72
|
+
assert(byHash.has(key) && !seen.has(key),'invalid-asset-request'); seen.add(key);
|
|
73
|
+
const file = byHash.get(key); data.set(key,new Blob([file.base64],{type:assetType(file.path)}),file.path.split('/').at(-1));
|
|
74
|
+
}
|
|
75
|
+
const uploaded = await api.request('/workers/assets/upload?base64=true',{method:'POST',body:data,assetToken:session.jwt});
|
|
76
|
+
if (uploaded.jwt) completion = uploaded.jwt;
|
|
77
|
+
}
|
|
78
|
+
assert(typeof completion === 'string' && completion,'incomplete-asset-upload');
|
|
79
|
+
return completion;
|
|
80
|
+
}
|
|
81
|
+
export async function uploadWorker(api, {plan,artifact,bindings,secrets,migration,fingerprint}) {
|
|
82
|
+
const data = new FormData();
|
|
83
|
+
const metadata = {main_module:artifact.main,compatibility_date:plan.compatibilityDate,compatibility_flags:plan.compatibilityFlags ?? [],bindings:[...bindings,...secrets.bindings],keep_bindings:['secret_text'],annotations:{'workers/message':`rer-deploy:${fingerprint}`}};
|
|
84
|
+
if (migration) metadata.migrations = migration;
|
|
85
|
+
if (artifact.assets.length) { metadata.assets = {jwt:await uploadAssets(api,plan.worker,artifact.assets),config:{run_worker_first:RUN_WORKER_FIRST}}; }
|
|
86
|
+
data.set('metadata',new Blob([JSON.stringify(metadata)],{type:'application/json'}));
|
|
87
|
+
for (const file of artifact.modules) data.set(file.path,new Blob([Buffer.from(file.base64,'base64')],{type:moduleType(file.path)}),file.path);
|
|
88
|
+
await api.request(`/workers/scripts/${plan.worker}?bindings_inherit=strict`,{method:'PUT',body:data});
|
|
89
|
+
}
|
|
90
|
+
/** The workers.dev origin, enabling it when allowed. With `workersDev: false` the route is kept
|
|
91
|
+
* off (a Worker reached only through service bindings) and there is no origin. */
|
|
92
|
+
export async function workerOrigin(api, worker, { enable = false, workersDev = true } = {}) {
|
|
93
|
+
if (!workersDev) {
|
|
94
|
+
const state = await api.request(`/workers/scripts/${worker}/subdomain`);
|
|
95
|
+
assert(typeof state.enabled === 'boolean','invalid-worker-subdomain');
|
|
96
|
+
if (state.enabled) { assert(enable,'worker-subdomain-enabled'); await api.request(`/workers/scripts/${worker}/subdomain`,{method:'POST',body:{enabled:false,previews_enabled:false}}); }
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const result = await api.request('/workers/subdomain');
|
|
100
|
+
assert(typeof result.subdomain === 'string' && /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(result.subdomain),'invalid-account-subdomain');
|
|
101
|
+
const state = await api.request(`/workers/scripts/${worker}/subdomain`);
|
|
102
|
+
assert(typeof state.enabled === 'boolean','invalid-worker-subdomain');
|
|
103
|
+
if (!state.enabled) { assert(enable,'worker-subdomain-disabled'); await api.request(`/workers/scripts/${worker}/subdomain`,{method:'POST',body:{enabled:true,previews_enabled:false}}); }
|
|
104
|
+
return `https://${worker}.${result.subdomain}.workers.dev`;
|
|
105
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Erasure contract v1
|
|
2
|
+
|
|
3
|
+
The module erases one subject (an entity that holds personal data) and everything
|
|
4
|
+
declared as depending on it, for BASE-DATA-003. It keeps audit records with an
|
|
5
|
+
anonymised reference, as BASE-OPS-005 requires. Public signatures are in
|
|
6
|
+
`index.d.mts`. The seed owns authorization, confirmation UI, holds and any effect
|
|
7
|
+
outside D1 and R2.
|
|
8
|
+
|
|
9
|
+
- **ERASE-001 — Declared graph.** A seed declares one root table per entity and
|
|
10
|
+
the edges that reach its dependents: column equality (`on`) or a bound SQL
|
|
11
|
+
predicate (`where` + `params` taken from the parent row). A table may be
|
|
12
|
+
reached through several edges, and the closure runs to a fixed point. Kept
|
|
13
|
+
rows that must lose columns are declared as `redact` with a reason. Audit
|
|
14
|
+
references are declared as `audit`. `root.redact` keeps an opaque anonymous
|
|
15
|
+
root with a reason and explicit replacements for every non-identity column.
|
|
16
|
+
The same transaction advances the maintained portability redaction checkpoint;
|
|
17
|
+
old archives are refused with a trusted later journal, and post-redaction
|
|
18
|
+
archives preserve anonymous attribution. Structural identities are verified.
|
|
19
|
+
Root-return edges and cyclic deletion dependencies are refused before mutation.
|
|
20
|
+
- **ERASE-002 — No silent dependents.** Before preview or erasure, every table
|
|
21
|
+
with a foreign key into a tracked table, including a redacted root, must be declared (as an edge, a
|
|
22
|
+
redaction, an audit reference or a retained exception). Otherwise the call
|
|
23
|
+
refuses with `undeclared_dependent:<child>:<parent>` (409) and changes
|
|
24
|
+
nothing. This includes product tables named `rer_*`. Destructive FK actions
|
|
25
|
+
into retained/redacted/audit rows are refused; retained keys are verified
|
|
26
|
+
after effects, so `ON DELETE` actions never remove a promised retained row.
|
|
27
|
+
- **ERASE-003 — Preview without values.** `preview` returns counts per deleted,
|
|
28
|
+
redacted and audited table, the blob count, the retained exceptions, a hold
|
|
29
|
+
reason if any, and a plan hash. It never returns a stored value.
|
|
30
|
+
- **ERASE-004 — Cascade and fence in one commit.** Every deleted row (dependents
|
|
31
|
+
leaves-first, the subject row last) is deleted in a D1 batch that also holds
|
|
32
|
+
portability's `erasureStatements(table, key)` for that row and
|
|
33
|
+
`invalidateTransfersStatements()`. A batch holds at most `rows` dependents, so
|
|
34
|
+
deletion work is bounded. Scope/audit and completeness checks also read the
|
|
35
|
+
declared closure; their cost grows with that closure. A guard on the operation's step
|
|
36
|
+
counter makes a concurrent runner fail with `erasure_busy` (409) instead of
|
|
37
|
+
committing twice.
|
|
38
|
+
- **ERASE-005 — Blobs after the commit.** For a declared `blob` edge, each row's
|
|
39
|
+
object key is recorded in the same batch that deletes the row. Objects are
|
|
40
|
+
deleted only after that commit. Each recorded key is checked with a read, and
|
|
41
|
+
leaves the work list only once the object is gone. Repeating the blob phase is
|
|
42
|
+
safe.
|
|
43
|
+
- **ERASE-006 — Complete means complete.** A receipt reads `deleting` while rows
|
|
44
|
+
remain, `blobs` once every row is committed but some object may remain, and
|
|
45
|
+
`complete` only when the final fenced transaction has re-read every declared
|
|
46
|
+
dependent and audit path, retained/root-redacted rows have been verified, and
|
|
47
|
+
the blob work list is empty. New dependents restart collection; a persistently
|
|
48
|
+
changing scope refuses with `erasure_scope_unstable` and remains resumable. `completedAt` and `seconds` are
|
|
49
|
+
set only at `complete`.
|
|
50
|
+
- **ERASE-007 — Untraceable audit label.** Declared audit references are
|
|
51
|
+
replaced with one label per erasure, `erased:<128 random bits>`. The label is
|
|
52
|
+
not derived from the subject, so nobody who holds the database or an older
|
|
53
|
+
archive can recompute it or test a guess against it. Declared `clear` columns
|
|
54
|
+
are overwritten. Audit rows for selected dependents are relabelled in every batch. Temporary
|
|
55
|
+
scope preserves earlier keys until the final pass, catching audit rows written
|
|
56
|
+
after those dependents disappeared; that scope is then removed atomically.
|
|
57
|
+
- **ERASE-008 — Value-free receipt.** The receipt row holds the entity name,
|
|
58
|
+
label, planned and deleted counts per table, retained exceptions with reasons,
|
|
59
|
+
blob totals and times. It holds the subject key only while rows are still
|
|
60
|
+
being deleted, and clears it when the subject row commits.
|
|
61
|
+
- **ERASE-009 — Declared exceptions.** A table kept on purpose is declared in
|
|
62
|
+
`retain` with a reason. Preview and receipt list it, including a root redaction reason. Nothing is skipped
|
|
63
|
+
without a declaration.
|
|
64
|
+
- **ERASE-010 — One open erasure per subject.** `start` for a subject that has an
|
|
65
|
+
open erasure in `deleting` returns that erasure unchanged. Once the subject row
|
|
66
|
+
is gone, `start` answers `subject_not_found` (404) and changes nothing.
|
|
67
|
+
- **ERASE-011 — Seed policy hooks.** `hold` can refuse with a reason
|
|
68
|
+
(`held:<reason>`, 409). `statements` adds product statements to the batch that
|
|
69
|
+
commits the subject row, after deletion. `beforeStatements` adds durable
|
|
70
|
+
pre-deletion policy effects in the first batch. Hooks run in the seed's code.
|
|
71
|
+
- **ERASE-012 — import-kit composition.** `nativeStatements(entity, subject)`
|
|
72
|
+
returns the whole D1 phase as import-kit `D1Sql` (`{sql, params}`), opening
|
|
73
|
+
the receipt in the same statements. These are passed to
|
|
74
|
+
`eraseD1ImportTarget(kind, id, nativeStatements, context)`, so kit custody,
|
|
75
|
+
the product cascade and the fence commit in one batch. `deleteBlobs` then runs
|
|
76
|
+
as usual. `retireD1ImportCustody` takes the same statements for a whole
|
|
77
|
+
person's import custody and its erasure journal.
|
|
78
|
+
- **ERASE-013 — Stated limits.** An old archive restored into a brand-new store
|
|
79
|
+
that has no journal cannot be recognised. Raw database imports bypass every
|
|
80
|
+
product fence. Copies outside the installation are out of reach: downloaded
|
|
81
|
+
archives, delivered mail, provider records and platform backups.
|
|
82
|
+
- **ERASE-014 — Attributable counters.** A counter or rate row keyed by a person id,
|
|
83
|
+
email or digest is written with a `rer_erasure_rates` correlation in the same D1
|
|
84
|
+
batch (`rateWrite` / `rateCorrelation`), guarded on the subject row existing and,
|
|
85
|
+
for a redacted root, on its `live` predicate: a declaration with a redacted root
|
|
86
|
+
and a rate edge on it is refused without one, and erasure refuses a redaction that
|
|
87
|
+
leaves it true, so no write is admitted after erasure in either mode.
|
|
88
|
+
`rateEdges` declares the correlation and the counter rows it names as edges, so one
|
|
89
|
+
erasure plan deletes both and completion re-reads them. Uncorrelated aggregates are
|
|
90
|
+
untouched. A key that was never correlated cannot be found.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Subject erasure
|
|
2
|
+
|
|
3
|
+
This dependency-free source module erases one subject (a person, contact,
|
|
4
|
+
requester, booking and so on) and everything a seed declares as depending on
|
|
5
|
+
it, for BASE-DATA-003. Audit records survive with an anonymised reference
|
|
6
|
+
(BASE-OPS-005). It was generalised from the erasure code of Penny, Orbit, Dial,
|
|
7
|
+
HappyToHelp, Cradle and Slate. Each seed keeps its own authorization, confirmation UI, holds,
|
|
8
|
+
retained exceptions and effects outside D1 and R2. Clauses are in
|
|
9
|
+
[CONTRACT.md](CONTRACT.md).
|
|
10
|
+
|
|
11
|
+
## Declaring an entity
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { defineErasure, createErasure } from './vendor/base/modules/erasure/index.mjs';
|
|
15
|
+
export const person = defineErasure({
|
|
16
|
+
entity: 'person',
|
|
17
|
+
root: { table: 'people' },
|
|
18
|
+
edges: [
|
|
19
|
+
{ table: 'person_notes', from: 'people', on: { person_id: 'id' } },
|
|
20
|
+
{ table: 'person_files', from: 'people', on: { person_id: 'id' }, blob: { bucket: 'FILES', keyColumn: 'storage_key' } },
|
|
21
|
+
{ table: 'signals', from: 'people', where: 'instr(payload_json,?)>0', params: ['email'] },
|
|
22
|
+
],
|
|
23
|
+
redact: [{ table: 'messages', from: 'people', on: { author_id: 'id' }, set: { body: '' }, reason: 'transcript order is kept' }],
|
|
24
|
+
audit: [{ table: 'audit_events', column: 'entity_id', of: 'people', where: "entity_type='person'", clear: { details: '{}' } }],
|
|
25
|
+
retain: [{ table: 'opt_out_hashes', reason: 'keyed hash prevents re-contact' }],
|
|
26
|
+
hold: async ({ db, subject }) => null, // or a reason string: refuses with 409
|
|
27
|
+
beforeStatements: async ({ db, subject, label }) => [], // durable copy/unlink before the first child batch
|
|
28
|
+
statements: async ({ db, subject, label }) => [], // product SQL committed with the subject row
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Part | Meaning |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `root` | The subject's table; optional `redact: {set, reason, identityColumns?}` keeps an anonymous root. `set` overwrites every non-key, non-identity column with declared constants, or a function of `{subject,anonymousId}`. `identityColumns` may contain only opaque structural links (e.g. project/workspace IDs), never personal fields. Keys come from its primary key (`{rowid}` for keyless tables). |
|
|
35
|
+
| `edges` | Rows deleted with the subject. `on` maps child columns to parent columns. `where` is a bound SQL predicate whose `params` name parent columns. A table may appear in several edges. Overlapping paths are deduplicated and ordered by dependency. A cyclic deletion graph is refused before mutation; unlink cycles in seed policy before using this helper. |
|
|
36
|
+
| `blob` | On an edge: the R2 binding and the column holding the object key. Keys should be opaque (no file names). |
|
|
37
|
+
| `redact` | Kept rows whose columns are overwritten, each with a reason. |
|
|
38
|
+
| `audit` | Audit columns referencing rows of a deleted table (single-column key). They receive the erasure's label, and `clear` columns are overwritten. |
|
|
39
|
+
| `retain` | Tables kept on purpose, with reasons. Listed in preview and receipt. |
|
|
40
|
+
| `hold`, `beforeStatements`, `statements` | Refusal, durable pre-deletion effects (first batch), and final effects (after deletes). |
|
|
41
|
+
|
|
42
|
+
Any table with a foreign key into a deleted table must be declared. Otherwise
|
|
43
|
+
preview and erasure refuse with `undeclared_dependent:<child>:<parent>`, because a
|
|
44
|
+
`CASCADE` would remove a row that erasure never fenced. This includes `rer_*` product
|
|
45
|
+
tables. A destructive FK action into a retained/redacted/audit table is refused
|
|
46
|
+
with `unsafe_retained_fk:<child>:<parent>`. This is a schema-level refusal, even
|
|
47
|
+
when current links are null. Migrate such retained links to non-destructive
|
|
48
|
+
`NO ACTION` FKs and detach them before admission, or keep a redacted parent. Retained keys are
|
|
49
|
+
verified after each batch and before completion.
|
|
50
|
+
|
|
51
|
+
## Phases
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const erasure = createErasure({ db: env.DB, buckets: { FILES: env.FILES }, portability, declarations: [person] });
|
|
55
|
+
const preview = await erasure.preview('person', { id }); // counts, blob count, retained, planHash
|
|
56
|
+
const opened = await erasure.start('person', { id }, { planHash }); // receipt, phase 'deleting'
|
|
57
|
+
const receipt = await erasure.run(opened.id, { until: Date.now() + 20_000 });
|
|
58
|
+
if (receipt.phase !== 'complete') await env.JOBS.send({ erasure: opened.id }); // continue later
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`portability` is the seed's `createPortability({db,buckets,policy})` service.
|
|
62
|
+
`commit(id,{rows,until})` runs only the D1 phase: bounded atomic batches of at
|
|
63
|
+
most `rows` dependents (default 500), leaves first, with the subject row last.
|
|
64
|
+
Each deleted row's `erasureStatements` and `invalidateTransfersStatements()` go
|
|
65
|
+
in the same batch. `deleteBlobs(id,{until})` runs only the blob phase, after the
|
|
66
|
+
commit. `run` does both within the time budget. Every phase is safe to repeat,
|
|
67
|
+
and a concurrent runner gets `erasure_busy` (409). The receipt says `complete`
|
|
68
|
+
only after the completion transaction re-reads every declared dependent and audit path
|
|
69
|
+
inside the fence, then every recorded object has been deleted and read back as
|
|
70
|
+
absent. Scope changes roll back that step and resume collection. Temporary scope
|
|
71
|
+
remembers earlier parents through blob cleanup, so rows or audits added after the
|
|
72
|
+
first D1 fence return to deletion before completion. It is scrubbed atomically
|
|
73
|
+
with completion. A root deleted outside the helper is refused rather than reported complete.
|
|
74
|
+
|
|
75
|
+
For retained attribution, use `root: {table: 'operators', redact: {reason: 'stable
|
|
76
|
+
anonymous attribution', set: ({anonymousId}) => ({email: anonymousId + '@removed.invalid',
|
|
77
|
+
name: 'Removed operator', status: 'removed'})}}` and explicitly set any other
|
|
78
|
+
non-identity columns in that schema. Only opaque IDs survive; root redaction also
|
|
79
|
+
advances portability's redaction checkpoint; completion verifies the replacements
|
|
80
|
+
and preserved structural IDs. Older archives are refused when the destination or
|
|
81
|
+
a supplied trusted later journal knows that checkpoint, while a post-redaction
|
|
82
|
+
archive can restore the anonymous row. The portability adapter must forward
|
|
83
|
+
`redactionStatements()` for this mode.
|
|
84
|
+
`anonymousId` is independent of the audit label, so retained root values cannot
|
|
85
|
+
link that label back to the opaque identity. Root setters must be deterministic
|
|
86
|
+
for their input and must not read or copy personal data.
|
|
87
|
+
|
|
88
|
+
A subject too large for one request continues through the seed's queue, and a
|
|
89
|
+
scheduled sweep picks up any erasure whose message was lost. The default seed's
|
|
90
|
+
`src/core/features/people/routes.ts` shows both. The receipt (`receipt(id)`)
|
|
91
|
+
holds entity, label, planned and deleted counts, retained exceptions, blob totals
|
|
92
|
+
and times, and never a stored value. The subject key is held only while rows
|
|
93
|
+
are still being deleted.
|
|
94
|
+
|
|
95
|
+
## Attributable counters
|
|
96
|
+
|
|
97
|
+
A rate limit or counter whose key is a person id, an email or a digest of one is
|
|
98
|
+
personal data, and a hashed key cannot be traced back to its person afterwards. So the
|
|
99
|
+
write records who the key belongs to, in the same D1 batch as the counter upsert, and
|
|
100
|
+
the declaration erases both together.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import { rateEdges, rateWrite } from './vendor/base/modules/erasure/index.mjs';
|
|
104
|
+
const root = { table: 'people', column: 'id' };
|
|
105
|
+
// in the declaration
|
|
106
|
+
edges: [...rateEdges({ entity: 'person', root, counters: [{ table: 'rate_limits', keyColumn: 'key' }] })],
|
|
107
|
+
// at every write whose key identifies a known person
|
|
108
|
+
const [counted] = await rateWrite(db, { entity: 'person', root, subject: person.id, rateTable: 'rate_limits', rateKey: key },
|
|
109
|
+
[db.prepare('INSERT INTO rate_limits(key,window,count) VALUES(?,?,1) ON CONFLICT(key) DO UPDATE SET count=count+1 RETURNING count').bind(key, window)]);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`rateWrite` returns the results of your statements (so admission still reads `count`).
|
|
113
|
+
Where the seed owns its batch, put `rateCorrelation(db, spec)` (three statements) first
|
|
114
|
+
in that batch instead. They abort the batch when the subject row is already gone, so a
|
|
115
|
+
write admitted after erasure writes nothing and `rateWrite` refuses `subject_gone`
|
|
116
|
+
(409); a write admitted while an erasure runs is caught by the completion re-read.
|
|
117
|
+
|
|
118
|
+
A redacted root (retained attribution, above) keeps its row under the same id, so "the row
|
|
119
|
+
exists" no longer means "the person is still here". Give that root a `live` predicate
|
|
120
|
+
over its row, false once redacted, and use the same `root` in `rateEdges` and every
|
|
121
|
+
`rateWrite`/`rateCorrelation`:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
const root = { table: 'operators', column: 'id', live: "status<>'removed'" };
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The write guard then requires the row and the predicate. `defineErasure` refuses a
|
|
128
|
+
declaration with a redacted root and a rate edge on that root but no `live`
|
|
129
|
+
(`rate_root_live:<edge>`), and admission refuses (`rate_root_live`, 409) when the
|
|
130
|
+
predicate is still true for the redaction's `set` values, or names a column the
|
|
131
|
+
redaction does not set. A deleted root needs no `live`.
|
|
132
|
+
Preview and receipt count `rer_erasure_rates` and the counter table like any edge.
|
|
133
|
+
|
|
134
|
+
Seed adoption note:
|
|
135
|
+
|
|
136
|
+
1. Apply `schema/erasure-rates.sql` as the next numbered migration, after the erasure
|
|
137
|
+
migrations. Add `rer_erasure_rates` and each counter table to the portability
|
|
138
|
+
policy's `excludeTables` (rate windows are ephemeral).
|
|
139
|
+
2. Spread `rateEdges(...)` into the declaration of the entity the key identifies, one
|
|
140
|
+
`counters` item per rate table. Every counter table needs a primary key.
|
|
141
|
+
3. Route each write whose key is derived from a known person through `rateWrite` or
|
|
142
|
+
`rateCorrelation`. Keys that identify nobody (client IP, global or route scope)
|
|
143
|
+
are written directly and survive as aggregates; never correlate those.
|
|
144
|
+
4. One key must identify one person. Erasing a person deletes every counter row their
|
|
145
|
+
correlation names.
|
|
146
|
+
|
|
147
|
+
Limits: a row written before the person existed, or for an address with no person
|
|
148
|
+
record, has no subject and is not found; use an expiring window for it or key it by
|
|
149
|
+
something that identifies nobody. Rows written before adoption have no correlation:
|
|
150
|
+
the seed retires them in its own migration.
|
|
151
|
+
|
|
152
|
+
## Seams
|
|
153
|
+
|
|
154
|
+
- **Portability.** This module adds no second fence. It calls the service's
|
|
155
|
+
`erasureStatements` for every deleted row (children and files included, not
|
|
156
|
+
only the subject), so an older archive that contains any of those rows is
|
|
157
|
+
refused on restore. The fence's own limit stays: an old archive offered to a
|
|
158
|
+
brand-new store that has no journal cannot be recognised. Supply the owner's
|
|
159
|
+
later journal there (`rer import --journal LATER_ARCHIVE`).
|
|
160
|
+
- **import-kit.** `nativeStatements(entity, subject)` returns the whole D1 phase
|
|
161
|
+
as `{sql, params}` statements for `eraseD1ImportTarget(kind, id,
|
|
162
|
+
nativeStatements, context)`. Kit custody, the product cascade and the fence
|
|
163
|
+
then commit in one batch. Afterwards call `deleteBlobs(operationId)`. This
|
|
164
|
+
path is unbounded (one batch), so use it for imported targets of ordinary size.
|
|
165
|
+
To erase a person (not one record), pass the same statements to
|
|
166
|
+
`retireD1ImportCustody({ target, values }, nativeStatements, context)`: it also
|
|
167
|
+
retires imports whose headers hold the person's exact identifiers and writes
|
|
168
|
+
the import erasure journal (import-kit `src/d1/README.md`).
|
|
169
|
+
|
|
170
|
+
## Adopting
|
|
171
|
+
|
|
172
|
+
Copy these files byte for byte to a folder the Worker build serves, such as
|
|
173
|
+
`src/modules/erasure/`. A seed made by `rer` export also receives them under
|
|
174
|
+
`scripts/base-cli/modules/erasure/`, but `vite dev` serves only `src`, `public`
|
|
175
|
+
and `node_modules`, so runtime imports must not reach into `scripts/`. The
|
|
176
|
+
standalone default seed copies this module and portability into `src/modules/`.
|
|
177
|
+
The files are:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
modules/erasure/index.mjs
|
|
181
|
+
modules/erasure/index.d.mts
|
|
182
|
+
modules/erasure/declaration.mjs
|
|
183
|
+
modules/erasure/plan.mjs
|
|
184
|
+
modules/erasure/order.mjs
|
|
185
|
+
modules/erasure/root.mjs
|
|
186
|
+
modules/erasure/scope.mjs
|
|
187
|
+
modules/erasure/completion.mjs
|
|
188
|
+
modules/erasure/verification.mjs
|
|
189
|
+
modules/erasure/cascade.mjs
|
|
190
|
+
modules/erasure/blobs.mjs
|
|
191
|
+
modules/erasure/audit.mjs
|
|
192
|
+
modules/erasure/receipt.mjs
|
|
193
|
+
modules/erasure/fence.mjs
|
|
194
|
+
modules/erasure/sql.mjs
|
|
195
|
+
modules/erasure/schema/erasure.sql
|
|
196
|
+
modules/erasure/rate.mjs
|
|
197
|
+
modules/erasure/schema/erasure-rates.sql # only when the seed keeps attributable counters
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Apply `schema/erasure.sql` as the seed's next numbered migration, after
|
|
201
|
+
portability's. For existing installations apply the scope/guard additions as a new migration
|
|
202
|
+
(default-seed `0017_erasure_scope.sql`), leaving applied migrations unchanged.
|
|
203
|
+
Finish or reconcile open operations with the old helper before upgrading. Older
|
|
204
|
+
operations that discarded their scope refuse `erasure_scope_missing`; the helper
|
|
205
|
+
cannot prove completeness without the earlier parent keys.
|
|
206
|
+
Add `rer_erasure_blobs`, `rer_erasure_guard`, and `rer_erasure_scope` to the portability
|
|
207
|
+
policy's `excludeTables`. Receipts (`rer_erasure_operations`) travel in
|
|
208
|
+
archives; the blob work list does not.
|
|
209
|
+
|
|
210
|
+
## Historical measurements before the review fixes
|
|
211
|
+
|
|
212
|
+
Through the default seed's route in a local `rer dev` Worker, a subject with
|
|
213
|
+
10,000 dependent rows (9,500 notes and 500 file rows) and 500 R2 blobs was
|
|
214
|
+
erased in 10.7 s in one run and 15.0 s in another. That is one request: 20
|
|
215
|
+
bounded D1 steps, then the blob phase, with no queue continuation needed. The
|
|
216
|
+
full proof table is kept with the consolidation evidence for this change.
|
|
217
|
+
These pre-fix measurements do not establish the performance of the corrected
|
|
218
|
+
helper. They are local Miniflare measurements, not production capacity claims. Real D1
|
|
219
|
+
statement, CPU and request limits decide `rows` per step and the request budget.
|
|
220
|
+
Larger subjects continue on the queue, and the receipt stays `deleting` or
|
|
221
|
+
`blobs` until done.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { statement, quote, chunks, randomHex } from './sql.mjs';
|
|
2
|
+
|
|
3
|
+
/** A fresh random label per erasure: it is not derived from the subject, so
|
|
4
|
+
* nobody holding the database (or an old archive) can recompute or test it. */
|
|
5
|
+
export const newLabel = () => 'erased:' + randomHex(16);
|
|
6
|
+
|
|
7
|
+
export function auditStatements(db, d, plan, label, { only } = {}) {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const item of d.audit) {
|
|
10
|
+
if (only && item.of !== only) continue;
|
|
11
|
+
const keys = [...plan.known.get(item.of).values()].map(row => row[plan.keys.get(item.of).pk[0]]);
|
|
12
|
+
const clear = Object.entries(item.clear ?? {});
|
|
13
|
+
const set = [`${quote(item.column)}=?`, ...clear.map(([c]) => `${quote(c)}=?`)].join(',');
|
|
14
|
+
// A previously relabelled row is still this operation's audit: a writer
|
|
15
|
+
// may have changed its detail after the first cleanup.
|
|
16
|
+
out.push(statement(db, `UPDATE ${quote(item.table)} SET ${set} WHERE ${quote(item.column)}=?${item.where ? ` AND (${item.where})` : ''}`, label, ...clear.map(([, v]) => v), label));
|
|
17
|
+
for (const part of chunks(keys, 200)) {
|
|
18
|
+
out.push(statement(db, `UPDATE ${quote(item.table)} SET ${set} WHERE ${quote(item.column)} IN (SELECT value FROM json_each(?))${item.where ? ` AND (${item.where})` : ''}`,
|
|
19
|
+
label, ...clear.map(([, v]) => v), JSON.stringify(part)));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { all, statement, insist } from './sql.mjs';
|
|
2
|
+
import { settle } from './completion.mjs';
|
|
3
|
+
|
|
4
|
+
/** Runs only after the D1 commit recorded every key. Deleting an absent object
|
|
5
|
+
* succeeds, so an interrupted run repeats safely; a key leaves the work list
|
|
6
|
+
* only after a read shows the object gone. */
|
|
7
|
+
export async function deleteBlobs(ctx, d, id, { until = Infinity, page = 1000 } = {}) {
|
|
8
|
+
const { db, buckets } = ctx;
|
|
9
|
+
const op = await statement(db, 'SELECT phase FROM rer_erasure_operations WHERE id=?', id).first();
|
|
10
|
+
insist(op, 'erasure_not_found', 404);
|
|
11
|
+
if (op.phase !== 'blobs') return op.phase;
|
|
12
|
+
if (!(await all(db, "SELECT 1 FROM rer_erasure_scope WHERE operation=? AND kind='graph' AND table_name=? LIMIT 1", id, d.root.table)).length) {
|
|
13
|
+
const current = await statement(db, 'SELECT phase FROM rer_erasure_operations WHERE id=?', id).first();
|
|
14
|
+
if (current?.phase === 'complete') return 'complete';
|
|
15
|
+
insist(false, 'erasure_scope_missing', 409);
|
|
16
|
+
}
|
|
17
|
+
let changes = 0;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const rows = await all(db, 'SELECT bucket,key FROM rer_erasure_blobs WHERE operation=? ORDER BY bucket,key LIMIT ?', id, page);
|
|
20
|
+
if (!rows.length) {
|
|
21
|
+
const phase = await settle(ctx, d, id);
|
|
22
|
+
if (phase === 'complete' || phase === 'deleting') return phase;
|
|
23
|
+
if (phase === 'changed') insist(++changes < 3, 'erasure_scope_unstable', 409);
|
|
24
|
+
else changes = 0;
|
|
25
|
+
if (Date.now() > until) return 'blobs';
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
for (const name of new Set(rows.map(r => r.bucket))) {
|
|
29
|
+
const bucket = buckets[name];
|
|
30
|
+
insist(bucket && typeof bucket.delete === 'function', 'missing_bucket:' + name, 500);
|
|
31
|
+
const keys = rows.filter(r => r.bucket === name).map(r => r.key);
|
|
32
|
+
await bucket.delete(keys);
|
|
33
|
+
for (let i = 0; i < keys.length; i += 16) {
|
|
34
|
+
const left = await Promise.all(keys.slice(i, i + 16).map(k => bucket.head(k)));
|
|
35
|
+
insist(left.every(x => x === null), 'blob_remains', 503);
|
|
36
|
+
}
|
|
37
|
+
await db.batch([
|
|
38
|
+
statement(db, 'DELETE FROM rer_erasure_blobs WHERE operation=? AND bucket=? AND key IN (SELECT value FROM json_each(?))', id, name, JSON.stringify(keys)),
|
|
39
|
+
// Derived from what remains, so a concurrent or repeated run cannot double count.
|
|
40
|
+
statement(db, 'UPDATE rer_erasure_operations SET blobs_deleted=blobs_total-(SELECT count(*) FROM rer_erasure_blobs WHERE operation=?) WHERE id=?', id, id),
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
if (Date.now() > until) return 'blobs';
|
|
44
|
+
}
|
|
45
|
+
}
|