@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,91 @@
|
|
|
1
|
+
import { statement, quote, guard, insist, ErasureError } from './sql.mjs';
|
|
2
|
+
import { rootStatements } from './root.mjs';
|
|
3
|
+
import { readScope, scopeStatements } from './scope.mjs';
|
|
4
|
+
import { verificationStatements, childAbsenceStatements } from './verification.mjs';
|
|
5
|
+
import { collect, matchSql, checkDeclared } from './plan.mjs';
|
|
6
|
+
import { auditStatements } from './audit.mjs';
|
|
7
|
+
import { progressStatement, committedStatement } from './receipt.mjs';
|
|
8
|
+
|
|
9
|
+
const where = (keys, table) => keys.get(table).pk.map(c => `${c === 'rowid' ? 'rowid' : quote(c)}=?`).join(' AND ');
|
|
10
|
+
const values = e => Object.values(e.key);
|
|
11
|
+
|
|
12
|
+
function redactStatements(db, d, plan) {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const item of d.redact) {
|
|
15
|
+
const set = Object.keys(item.set).map(c => `${quote(c)}=?`).join(',');
|
|
16
|
+
for (const m of matchSql(item, [...plan.known.get(item.from).values()])) {
|
|
17
|
+
out.push(statement(db, `UPDATE ${quote(item.table)} SET ${set} WHERE ${m.where}`, ...Object.values(item.set), ...m.values));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Deletes each entry with its blob key captured first and its portability tombstone beside it. */
|
|
24
|
+
export async function deletionStatements(ctx, d, plan, entries, id, deleted) {
|
|
25
|
+
const { db, fence } = ctx, out = [];
|
|
26
|
+
for (const e of entries) {
|
|
27
|
+
const match = where(plan.keys, e.table);
|
|
28
|
+
out.push(...childAbsenceStatements(db, plan, e));
|
|
29
|
+
for (const blob of new Map(d.edges.filter(x => x.table === e.table && x.blob).map(x => [x.blob.bucket + '\0' + x.blob.keyColumn, x.blob])).values()) {
|
|
30
|
+
out.push(statement(db, `INSERT OR IGNORE INTO rer_erasure_blobs(operation,bucket,key) SELECT ?,?,${quote(blob.keyColumn)} FROM ${quote(e.table)} WHERE ${match} AND ${quote(blob.keyColumn)} IS NOT NULL`, id, blob.bucket, ...values(e)));
|
|
31
|
+
}
|
|
32
|
+
out.push(statement(db, `DELETE FROM ${quote(e.table)} WHERE ${match}`, ...values(e)), ...await fence.row(e.table, e.key));
|
|
33
|
+
deleted[e.table] = (deleted[e.table] ?? 0) + 1;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
export async function firstStatements(ctx, d, plan, label) {
|
|
38
|
+
return d.beforeStatements ? await d.beforeStatements({ db: ctx.db, subject: plan.subject, label }) : [];
|
|
39
|
+
}
|
|
40
|
+
export const cleanupStatements = (ctx, d, plan, label) => [...redactStatements(ctx.db, d, plan), ...auditStatements(ctx.db, d, plan, label)];
|
|
41
|
+
/** Commits with the subject row: a second redaction/audit pass catches rows written
|
|
42
|
+
* during a long erasure. Verify before removing the root (so triggers cannot hide
|
|
43
|
+
* new children through CASCADE), then after final policy effects. */
|
|
44
|
+
export async function finalStatements(ctx, d, plan, id, label, now, deleted) {
|
|
45
|
+
const { db } = ctx, out = [];
|
|
46
|
+
if (plan) out.push(...cleanupStatements(ctx, d, plan, label), ...await rootStatements(ctx, d, plan, id));
|
|
47
|
+
out.push(...await verificationStatements(db, d, plan, id, label, { final: true }));
|
|
48
|
+
if (!d.root.redact) out.push(...await deletionStatements(ctx, d, plan, [plan.root], id, deleted));
|
|
49
|
+
if (plan && d.statements) out.push(...await d.statements({ db, subject: plan.subject, label }));
|
|
50
|
+
out.push(...cleanupStatements(ctx, d, plan, label));
|
|
51
|
+
out.push(...await verificationStatements(db, d, plan, id, label, { final: true }));
|
|
52
|
+
out.push(statement(db, 'UPDATE rer_erasure_operations SET blobs_total=(SELECT count(*) FROM rer_erasure_blobs WHERE operation=?) WHERE id=?', id, id), committedStatement(db, id, now));
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One bounded, atomic D1 commit of up to `rows` dependents, leaves first. The
|
|
57
|
+
* subject row commits with the last step. A concurrent runner loses the guard. */
|
|
58
|
+
export async function step(ctx, d, id, { rows = 500 } = {}) {
|
|
59
|
+
const { db, fence } = ctx;
|
|
60
|
+
const op = await statement(db, 'SELECT * FROM rer_erasure_operations WHERE id=?', id).first();
|
|
61
|
+
insist(op && op.entity === d.entity, 'erasure_not_found', 404);
|
|
62
|
+
if (op.phase !== 'deleting') return false;
|
|
63
|
+
const now = new Date().toISOString(), deleted = JSON.parse(op.deleted);
|
|
64
|
+
await checkDeclared(db, d);
|
|
65
|
+
insist(Number.isInteger(rows) && rows > 0, 'batch_rows');
|
|
66
|
+
const remembered = await readScope(db, id);
|
|
67
|
+
insist(op.step === 0 || remembered.some(e => e.table === d.root.table), 'erasure_scope_missing', 409);
|
|
68
|
+
const plan = await collect(db, d, JSON.parse(op.subject), remembered);
|
|
69
|
+
insist(plan && plan.root, 'subject_removed_outside_erasure', 409);
|
|
70
|
+
const batch = [db.prepare('PRAGMA defer_foreign_keys=ON'),
|
|
71
|
+
guard(db, "EXISTS(SELECT 1 FROM rer_erasure_operations WHERE id=? AND step=? AND phase='deleting')", id, op.step),
|
|
72
|
+
...await fence.transfers()];
|
|
73
|
+
const take = plan.order.slice(0, rows), final = take.length === plan.order.length;
|
|
74
|
+
batch.push(...await scopeStatements(db, d, plan, id, [...take, plan.root]));
|
|
75
|
+
if (op.step === 0) batch.push(...await firstStatements(ctx, d, plan, op.label));
|
|
76
|
+
batch.push(...await verificationStatements(db, d, plan, id, op.label));
|
|
77
|
+
batch.push(...cleanupStatements(ctx, d, plan, op.label));
|
|
78
|
+
batch.push(...await deletionStatements(ctx, d, plan, take, id, deleted));
|
|
79
|
+
if (final) batch.push(...await finalStatements(ctx, d, plan, id, op.label, now, deleted));
|
|
80
|
+
else batch.push(...await verificationStatements(db, d, plan, id, op.label, { intake: false }));
|
|
81
|
+
|
|
82
|
+
batch.push(progressStatement(db, id, op.step, deleted), db.prepare('DELETE FROM rer_erasure_guard'));
|
|
83
|
+
try { await db.batch(batch); }
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (/erasure_scope_changed/.test(String(error?.message ?? error))) return 'changed';
|
|
86
|
+
if (/erasure_retained_missing/.test(String(error?.message ?? error))) throw new ErasureError('retained_row_missing', 409);
|
|
87
|
+
if (/CHECK constraint failed/.test(String(error?.message ?? error))) throw new ErasureError('erasure_busy', 409);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
return !final;
|
|
91
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { statement, guard, insist, ErasureError } from './sql.mjs';
|
|
2
|
+
import { collect, checkDeclared } from './plan.mjs';
|
|
3
|
+
import { readScope, readRedaction, scopeStatements, forgetScope } from './scope.mjs';
|
|
4
|
+
import { rootValues, rootStatements } from './root.mjs';
|
|
5
|
+
import { cleanupStatements, deletionStatements } from './cascade.mjs';
|
|
6
|
+
import { verificationStatements, rootGoneStatement } from './verification.mjs';
|
|
7
|
+
import { progressStatement, completeStatement } from './receipt.mjs';
|
|
8
|
+
|
|
9
|
+
/** Reconcile the declared graph after the first D1 fence, using the remembered
|
|
10
|
+
* parents even when the root is gone. Completion and the last audit/retention
|
|
11
|
+
* checks share one transaction; late rows return to deletion and blob cleanup. */
|
|
12
|
+
export async function settle(ctx, d, id) {
|
|
13
|
+
const { db, fence } = ctx;
|
|
14
|
+
const op = await statement(db, 'SELECT * FROM rer_erasure_operations WHERE id=?', id).first();
|
|
15
|
+
insist(op && op.entity === d.entity, 'erasure_not_found', 404);
|
|
16
|
+
if (op.phase !== 'blobs') return op.phase;
|
|
17
|
+
const remembered = await readScope(db, id), saved = remembered.find(e => e.table === d.root.table);
|
|
18
|
+
if (!saved) {
|
|
19
|
+
const current = await statement(db, 'SELECT phase FROM rer_erasure_operations WHERE id=?', id).first();
|
|
20
|
+
if (current?.phase === 'complete') return 'complete';
|
|
21
|
+
// Older helpers discarded this information. Never claim an unprovable
|
|
22
|
+
// legacy operation complete or invent the erased person's key.
|
|
23
|
+
insist(false, 'erasure_scope_missing', 409);
|
|
24
|
+
}
|
|
25
|
+
await checkDeclared(db, d);
|
|
26
|
+
const plan = await collect(db, d, saved.key, remembered);
|
|
27
|
+
insist(plan, 'erasure_scope_missing', 409);
|
|
28
|
+
if (d.root.redact) {
|
|
29
|
+
const saved = await readRedaction(db, id);
|
|
30
|
+
insist(plan.root && saved.identity && /^[a-f0-9]{32}$/.test(saved.redaction?.anonymousId ?? ''), 'erasure_redaction_scope_missing', 409);
|
|
31
|
+
plan.anonymousId = saved.redaction.anonymousId; plan.rootIdentity = saved.identity;
|
|
32
|
+
plan.rootSet = await rootValues(d, plan.keys.get(d.root.table), plan.subject, plan.anonymousId);
|
|
33
|
+
}
|
|
34
|
+
const deleted = JSON.parse(op.deleted), take = plan.order.slice(0, 500), final = take.length === plan.order.length;
|
|
35
|
+
const newBlobs = take.some(e => d.edges.some(edge => edge.table === e.table && edge.blob && e.row[edge.blob.keyColumn] != null));
|
|
36
|
+
const batch = [db.prepare('PRAGMA defer_foreign_keys=ON'), guard(db, "EXISTS(SELECT 1 FROM rer_erasure_operations WHERE id=? AND step=? AND phase='blobs')", id, op.step),
|
|
37
|
+
...await fence.transfers(), ...await scopeStatements(db, d, plan, id, [...take, ...(plan.root ? [plan.root] : [])]),
|
|
38
|
+
...await verificationStatements(db, d, plan, id, op.label), ...cleanupStatements(ctx, d, plan, op.label),
|
|
39
|
+
...await deletionStatements(ctx, d, plan, take, id, deleted)];
|
|
40
|
+
if (final) {
|
|
41
|
+
if (d.root.redact) batch.push(...await rootStatements(ctx, d, plan, id));
|
|
42
|
+
else if (plan.root) batch.push(...await deletionStatements(ctx, d, plan, [plan.root], id, deleted));
|
|
43
|
+
batch.push(...cleanupStatements(ctx, d, plan, op.label), ...await verificationStatements(db, d, plan, id, op.label, { final: true }));
|
|
44
|
+
if (!d.root.redact) batch.push(rootGoneStatement(db, d, plan));
|
|
45
|
+
} else batch.push(...await verificationStatements(db, d, plan, id, op.label, { intake: false }));
|
|
46
|
+
batch.push(statement(db, 'UPDATE rer_erasure_operations SET blobs_total=blobs_deleted+(SELECT count(*) FROM rer_erasure_blobs WHERE operation=?) WHERE id=?', id, id), progressStatement(db, id, op.step, deleted));
|
|
47
|
+
if (final && !newBlobs) {
|
|
48
|
+
batch.push(guard(db, 'NOT EXISTS(SELECT 1 FROM rer_erasure_blobs WHERE operation=?)', id), completeStatement(db, id, new Date().toISOString()),
|
|
49
|
+
...cleanupStatements(ctx, d, plan, op.label), ...await verificationStatements(db, d, plan, id, op.label, { final: true }));
|
|
50
|
+
if (!d.root.redact) batch.push(rootGoneStatement(db, d, plan));
|
|
51
|
+
batch.push(forgetScope(db, id));
|
|
52
|
+
}
|
|
53
|
+
batch.push(db.prepare('DELETE FROM rer_erasure_guard'));
|
|
54
|
+
try { await db.batch(batch); }
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (/erasure_scope_changed/.test(String(error?.message ?? error))) return 'changed';
|
|
57
|
+
if (/erasure_retained_missing/.test(String(error?.message ?? error))) throw new ErasureError('retained_row_missing', 409);
|
|
58
|
+
if (/CHECK constraint failed/.test(String(error?.message ?? error))) throw new ErasureError('erasure_busy', 409);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
return (await statement(db, 'SELECT phase FROM rer_erasure_operations WHERE id=?', id).first()).phase;
|
|
62
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { insist } from './sql.mjs';
|
|
2
|
+
|
|
3
|
+
const name = value => typeof value === 'string' && /^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(value);
|
|
4
|
+
const text = value => typeof value === 'string' && value.trim().length > 0;
|
|
5
|
+
const match = (item, tables, where) => {
|
|
6
|
+
insist(tables.has(item.from), `unknown_from:${where}:${item.from}`);
|
|
7
|
+
insist((item.on === undefined) !== (item.where === undefined), `edge_match:${where}`);
|
|
8
|
+
if (item.on) insist(Object.keys(item.on).length > 0 && Object.entries(item.on).every(([a, b]) => name(a) && name(b)), `edge_columns:${where}`);
|
|
9
|
+
if (item.where !== undefined) insist(text(item.where) && Array.isArray(item.params ?? []) && (item.params ?? []).every(name), `edge_predicate:${where}`);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Validates one entity's erasure graph. Policy hooks stay functions owned by the seed. */
|
|
13
|
+
export function defineErasure(input) {
|
|
14
|
+
const d = { edges: [], redact: [], audit: [], retain: [], ...input };
|
|
15
|
+
insist(typeof d.entity === 'string' && /^[a-z][a-z0-9_-]{0,63}$/.test(d.entity), 'entity_name');
|
|
16
|
+
insist(d.root && name(d.root.table), 'root_table');
|
|
17
|
+
for (const key of ['edges', 'redact', 'audit', 'retain']) insist(Array.isArray(d[key]), `${key}_list`);
|
|
18
|
+
// A table may be reached through several edges (cycles such as request <-> evidence).
|
|
19
|
+
for (const [i, edge] of d.edges.entries()) insist(name(edge.table) && edge.table !== d.root.table, `edge_table:${i}`);
|
|
20
|
+
const tables = new Set([d.root.table, ...d.edges.map(e => e.table)]);
|
|
21
|
+
for (const [i, edge] of d.edges.entries()) {
|
|
22
|
+
match(edge, tables, `edges[${i}]`);
|
|
23
|
+
if (edge.blob) insist(name(edge.blob.bucket) && name(edge.blob.keyColumn), `edge_blob:${i}`);
|
|
24
|
+
}
|
|
25
|
+
for (const [i, item] of d.redact.entries()) {
|
|
26
|
+
insist(name(item.table) && !tables.has(item.table), `redact_table:${i}`);
|
|
27
|
+
match(item, tables, `redact[${i}]`);
|
|
28
|
+
insist(item.set && Object.keys(item.set).length > 0 && Object.keys(item.set).every(name), `redact_set:${i}`);
|
|
29
|
+
insist(text(item.reason), `redact_reason:${i}`);
|
|
30
|
+
}
|
|
31
|
+
for (const [i, item] of d.audit.entries()) {
|
|
32
|
+
insist(name(item.table) && name(item.column) && tables.has(item.of), `audit:${i}`);
|
|
33
|
+
insist(item.where === undefined || text(item.where), `audit_where:${i}`);
|
|
34
|
+
insist(item.clear === undefined || Object.keys(item.clear).every(name), `audit_clear:${i}`);
|
|
35
|
+
}
|
|
36
|
+
for (const [i, item] of d.retain.entries()) insist(name(item.table) && !tables.has(item.table) && text(item.reason), `retain:${i}`);
|
|
37
|
+
if (d.root.redact) {
|
|
38
|
+
insist(text(d.root.redact.reason), 'root_redact_reason');
|
|
39
|
+
insist(Array.isArray(d.root.redact.identityColumns ?? []) && (d.root.redact.identityColumns ?? []).every(name), 'root_identity_columns');
|
|
40
|
+
insist(typeof d.root.redact.set === 'function' || (d.root.redact.set && Object.keys(d.root.redact.set).length > 0 && Object.keys(d.root.redact.set).every(name)), 'root_redact_set');
|
|
41
|
+
// The retained root keeps its key, so rate writes naming it need a liveness
|
|
42
|
+
// predicate the redaction ends (rateEdges root.live).
|
|
43
|
+
for (const [i, edge] of d.edges.entries()) if (edge.rate && edge.from === d.root.table) insist(text(edge.rate.live), `rate_root_live:${i}`);
|
|
44
|
+
}
|
|
45
|
+
for (const hook of ['hold', 'beforeStatements', 'statements']) insist(d[hook] === undefined || typeof d[hook] === 'function', `${hook}_hook`);
|
|
46
|
+
return Object.freeze({ ...d, tracked: [...tables], deleted: [...tables].filter(t => t !== d.root.table || !d.root.redact) });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buckets(declarations) {
|
|
50
|
+
return [...new Set(declarations.flatMap(d => d.edges.filter(e => e.blob).map(e => e.blob.bucket)))];
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { insist } from './sql.mjs';
|
|
2
|
+
|
|
3
|
+
/** The restore fence is portability's: every deleted row gets that service's
|
|
4
|
+
* erasureStatements, and every commit drops in-flight transfer copies. */
|
|
5
|
+
export function fenceCalls(portability) {
|
|
6
|
+
insist(portability && typeof portability.erasureStatements === 'function' && typeof portability.invalidateTransfersStatements === 'function', 'portability_service');
|
|
7
|
+
return {
|
|
8
|
+
row: (table, key) => portability.erasureStatements(table, key),
|
|
9
|
+
redaction: () => portability.redactionStatements(),
|
|
10
|
+
transfers: () => portability.invalidateTransfersStatements(),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** import-kit takes `{sql, params}` (its D1Sql). Workers' D1 prepared statements
|
|
15
|
+
* carry their text and bindings as `statement` and `params`; refuse anything else. */
|
|
16
|
+
export function toD1Sql(prepared) {
|
|
17
|
+
insist(prepared && typeof prepared.statement === 'string' && Array.isArray(prepared.params), 'statement_not_convertible');
|
|
18
|
+
return { sql: prepared.statement, params: [...prepared.params] };
|
|
19
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type Cell = null | string | number;
|
|
2
|
+
export type Key = Record<string, Cell>;
|
|
3
|
+
type Match = { from: string; on: Record<string, string>; where?: never; params?: never } | { from: string; where: string; params?: string[]; on?: never };
|
|
4
|
+
export type Edge = Match & { table: string; blob?: { bucket: string; keyColumn: string } };
|
|
5
|
+
export type Redaction = Match & { table: string; set: Record<string, Cell>; reason: string };
|
|
6
|
+
export interface AuditReference { table: string; column: string; of: string; where?: string; clear?: Record<string, Cell> }
|
|
7
|
+
export interface Retained { table: string; reason: string }
|
|
8
|
+
export type PolicyInput = { db: D1Database; subject: Key; label: string };
|
|
9
|
+
export type RootRedaction = { identityColumns?: string[]; set: Record<string, Cell> | ((input: { subject: Key; anonymousId: string }) => Record<string, Cell> | Promise<Record<string, Cell>>); reason: string };
|
|
10
|
+
export interface Declaration {
|
|
11
|
+
entity: string;
|
|
12
|
+
root: { table: string; redact?: RootRedaction };
|
|
13
|
+
edges?: Edge[];
|
|
14
|
+
redact?: Redaction[];
|
|
15
|
+
audit?: AuditReference[];
|
|
16
|
+
retain?: Retained[];
|
|
17
|
+
/** Seed policy: a reason refuses the erasure (409 `held:<reason>`). */
|
|
18
|
+
hold?: (input: { db: D1Database; subject: Key }) => string | null | undefined | Promise<string | null | undefined>;
|
|
19
|
+
/** Durable policy SQL before the first dependent deletion; also used by nativeStatements. */
|
|
20
|
+
beforeStatements?: (input: PolicyInput) => D1PreparedStatement[] | Promise<D1PreparedStatement[]>;
|
|
21
|
+
/** Final policy effects after graph deletion, before verification and the receipt. */
|
|
22
|
+
statements?: (input: { db: D1Database; subject: Key; label: string }) => D1PreparedStatement[] | Promise<D1PreparedStatement[]>;
|
|
23
|
+
}
|
|
24
|
+
export interface Defined extends Readonly<Required<Omit<Declaration, 'hold' | 'beforeStatements' | 'statements'>>> { hold?: Declaration['hold']; beforeStatements?: Declaration['beforeStatements']; statements?: Declaration['statements']; deleted: string[]; tracked: string[] }
|
|
25
|
+
export interface Preview {
|
|
26
|
+
entity: string; planHash: string; blobs: number; hold: string | null;
|
|
27
|
+
deleted: Record<string, number>; redacted: Record<string, number>; audited: Record<string, number>; retained: Retained[];
|
|
28
|
+
}
|
|
29
|
+
export interface Receipt {
|
|
30
|
+
id: string; entity: string; phase: 'deleting' | 'blobs' | 'complete'; label: string;
|
|
31
|
+
planned: Record<string, number>; deleted: Record<string, number>; retained: Retained[];
|
|
32
|
+
blobs: { total: number; deleted: number };
|
|
33
|
+
requestedAt: string; committedAt: string | null; completedAt: string | null; seconds: number | null;
|
|
34
|
+
}
|
|
35
|
+
export interface D1Sql { sql: string; params: unknown[] }
|
|
36
|
+
export interface PortabilityFence {
|
|
37
|
+
erasureStatements(table: string, primaryKey: Key): Promise<D1PreparedStatement[]>;
|
|
38
|
+
/** Required when any declaration uses root.redact. */
|
|
39
|
+
redactionStatements?(): Promise<D1PreparedStatement[]>;
|
|
40
|
+
invalidateTransfersStatements(): Promise<D1PreparedStatement[]>;
|
|
41
|
+
}
|
|
42
|
+
export interface Erasure {
|
|
43
|
+
preview(entity: string, subject: Key): Promise<Preview>;
|
|
44
|
+
start(entity: string, subject: Key, options?: { planHash?: string }): Promise<Receipt>;
|
|
45
|
+
commit(id: string, options?: { rows?: number; until?: number }): Promise<Receipt>;
|
|
46
|
+
deleteBlobs(id: string, options?: { until?: number; page?: number }): Promise<Receipt>;
|
|
47
|
+
run(id: string, options?: { rows?: number; until?: number }): Promise<Receipt>;
|
|
48
|
+
receipt(id: string): Promise<Receipt>;
|
|
49
|
+
nativeStatements(entity: string, subject: Key): Promise<{ operationId: string; statements: D1Sql[] }>;
|
|
50
|
+
}
|
|
51
|
+
export function defineErasure(declaration: Declaration): Defined;
|
|
52
|
+
export function createErasure(options: { db: D1Database; buckets?: Record<string, R2Bucket>; portability: PortabilityFence; declarations: (Declaration | Defined)[] }): Erasure;
|
|
53
|
+
export function toD1Sql(statement: D1PreparedStatement): D1Sql;
|
|
54
|
+
export class ErasureError extends Error { code: string; status: number; constructor(code: string, status?: number) }
|
|
55
|
+
/** Attributable rate-limit or counter rows: see README "Attributable counters". */
|
|
56
|
+
/** `live`: SQL predicate over the root row, true while the subject may be counted.
|
|
57
|
+
* Required when the declaration's root is redacted (it must be false after redaction). */
|
|
58
|
+
export interface RateRoot { table: string; column: string; live?: string }
|
|
59
|
+
export interface RateSpec { entity: string; root: RateRoot; subject: Cell; rateTable: string; rateKey: string }
|
|
60
|
+
export function rateEdges(options: { entity: string; root: RateRoot; counters: { table: string; keyColumn: string }[] }): Edge[];
|
|
61
|
+
export function rateCorrelation(db: D1Database, spec: RateSpec): D1PreparedStatement[];
|
|
62
|
+
export function rateWrite(db: D1Database, spec: RateSpec, statements: D1PreparedStatement[]): Promise<D1Result[]>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { defineErasure, buckets as declaredBuckets } from './declaration.mjs';
|
|
2
|
+
import { collect, counts, planHash, checkDeclared } from './plan.mjs';
|
|
3
|
+
import { step, firstStatements, cleanupStatements, deletionStatements, finalStatements } from './cascade.mjs';
|
|
4
|
+
import { verificationStatements } from './verification.mjs';
|
|
5
|
+
import { scopeStatements } from './scope.mjs';
|
|
6
|
+
import { deleteBlobs } from './blobs.mjs';
|
|
7
|
+
import { newLabel } from './audit.mjs';
|
|
8
|
+
import { receipt, activeFor, openStatement, progressStatement } from './receipt.mjs';
|
|
9
|
+
import { fenceCalls, toD1Sql } from './fence.mjs';
|
|
10
|
+
import { insist, randomHex } from './sql.mjs';
|
|
11
|
+
export { defineErasure } from './declaration.mjs';
|
|
12
|
+
export { ErasureError } from './sql.mjs';
|
|
13
|
+
export { toD1Sql } from './fence.mjs';
|
|
14
|
+
export { rateEdges, rateCorrelation, rateWrite } from './rate.mjs';
|
|
15
|
+
|
|
16
|
+
export function createErasure({ db, buckets = {}, portability, declarations }) {
|
|
17
|
+
const list = declarations.map(d => d.deleted ? d : defineErasure(d));
|
|
18
|
+
insist(new Set(list.map(d => d.entity)).size === list.length, 'duplicate_entity');
|
|
19
|
+
for (const name of declaredBuckets(list)) insist(buckets[name] && typeof buckets[name].delete === 'function' && typeof buckets[name].head === 'function', 'missing_bucket:' + name);
|
|
20
|
+
if (list.some(d => d.root.redact)) insist(typeof portability?.redactionStatements === 'function', 'portability_redaction_service');
|
|
21
|
+
const ctx = { db, buckets, fence: fenceCalls(portability) };
|
|
22
|
+
const entity = name => { const d = list.find(x => x.entity === name); insist(d, 'unknown_entity', 404); return d; };
|
|
23
|
+
const subjectPlan = async (d, subject) => { const plan = await collect(db, d, subject); insist(plan, 'subject_not_found', 404); await checkDeclared(db, d); return plan; };
|
|
24
|
+
async function summary(d, plan) {
|
|
25
|
+
const totals = await counts(db, d, plan);
|
|
26
|
+
return { ...totals, retained: [...d.retain.map(({ table, reason }) => ({ table, reason })), ...(d.root.redact ? [{ table: d.root.table, reason: d.root.redact.reason }] : [])], planHash: await planHash(d, plan, totals) };
|
|
27
|
+
}
|
|
28
|
+
async function opened(id) { return entity((await receipt(db, id)).entity); }
|
|
29
|
+
const service = {
|
|
30
|
+
/** Counts only: no stored value of the subject or its dependents is returned. */
|
|
31
|
+
async preview(name, subject) {
|
|
32
|
+
const d = entity(name), plan = await subjectPlan(d, subject), result = await summary(d, plan);
|
|
33
|
+
return { entity: d.entity, ...result, hold: d.hold ? await d.hold({ db, subject: plan.subject }) ?? null : null };
|
|
34
|
+
},
|
|
35
|
+
/** Opens (or returns the already open) erasure for one subject. */
|
|
36
|
+
async start(name, subject, { planHash: expected } = {}) {
|
|
37
|
+
const d = entity(name), active = await activeFor(db, d, subject);
|
|
38
|
+
if (active) return active;
|
|
39
|
+
const plan = await subjectPlan(d, subject), totals = await summary(d, plan);
|
|
40
|
+
if (expected !== undefined) insist(expected === totals.planHash, 'plan_changed', 409);
|
|
41
|
+
const reason = d.hold ? await d.hold({ db, subject: plan.subject }) : null;
|
|
42
|
+
insist(!reason, 'held:' + reason, 409);
|
|
43
|
+
const id = randomHex(16);
|
|
44
|
+
try { await openStatement(db, d, { id, subject: plan.subject, label: newLabel(), planned: totals.deleted, blobs: totals.blobs, now: new Date().toISOString() }).run(); }
|
|
45
|
+
catch (error) { const raced = await activeFor(db, d, subject); if (raced) return raced; throw error; }
|
|
46
|
+
return receipt(db, id);
|
|
47
|
+
},
|
|
48
|
+
/** D1 phase: bounded atomic steps until every row is gone or `until` passes. */
|
|
49
|
+
async commit(id, { rows, until = Infinity } = {}) {
|
|
50
|
+
const d = await opened(id);
|
|
51
|
+
let changes = 0;
|
|
52
|
+
for (;;) {
|
|
53
|
+
const result = await step(ctx, d, id, { rows });
|
|
54
|
+
if (result === 'changed') {
|
|
55
|
+
insist(++changes < 3, 'erasure_scope_unstable', 409);
|
|
56
|
+
} else changes = 0;
|
|
57
|
+
if (!result || Date.now() > until) break;
|
|
58
|
+
}
|
|
59
|
+
return receipt(db, id);
|
|
60
|
+
},
|
|
61
|
+
/** Blob phase: separately callable, resumable, complete only when every object is gone. */
|
|
62
|
+
async deleteBlobs(id, options) { await deleteBlobs(ctx, await opened(id), id, options); return receipt(db, id); },
|
|
63
|
+
async run(id, { rows, until = Infinity } = {}) {
|
|
64
|
+
const after = await service.commit(id, { rows, until });
|
|
65
|
+
return after.phase === 'blobs' && Date.now() <= until ? service.deleteBlobs(id, { until }) : after;
|
|
66
|
+
},
|
|
67
|
+
receipt: id => receipt(db, id),
|
|
68
|
+
/** The whole D1 phase as import-kit `D1Sql`, for `eraseD1ImportTarget(kind,id,nativeStatements,ctx)`.
|
|
69
|
+
* Run `deleteBlobs(operationId)` after that batch commits. */
|
|
70
|
+
async nativeStatements(name, subject) {
|
|
71
|
+
const d = entity(name);
|
|
72
|
+
insist(!await activeFor(db, d, subject), 'erasure_open', 409);
|
|
73
|
+
const plan = await subjectPlan(d, subject), totals = await summary(d, plan);
|
|
74
|
+
const reason = d.hold ? await d.hold({ db, subject: plan.subject }) : null;
|
|
75
|
+
insist(!reason, 'held:' + reason, 409);
|
|
76
|
+
const id = randomHex(16), label = newLabel(), now = new Date().toISOString(), deleted = {};
|
|
77
|
+
const statements = [...await ctx.fence.transfers(), openStatement(db, d, { id, subject: plan.subject, label, planned: totals.deleted, blobs: totals.blobs, now }),
|
|
78
|
+
...await scopeStatements(db, d, plan, id), ...await firstStatements(ctx, d, plan, label), ...await verificationStatements(db, d, plan, id, label), ...cleanupStatements(ctx, d, plan, label), ...await deletionStatements(ctx, d, plan, plan.order, id, deleted),
|
|
79
|
+
...await finalStatements(ctx, d, plan, id, label, now, deleted), progressStatement(db, id, 0, deleted), db.prepare('DELETE FROM rer_erasure_guard')];
|
|
80
|
+
return { operationId: id, statements: statements.map(toD1Sql) };
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return service;
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { all, canonicalJson, insist, quote } from './sql.mjs';
|
|
2
|
+
import { matching } from './plan.mjs';
|
|
3
|
+
|
|
4
|
+
/** Topological row order across every path, not the depth of the first visit.
|
|
5
|
+
* Actual FK links also constrain order, even if a row was reached by another edge. */
|
|
6
|
+
export async function deletionOrder(db, d, plan) {
|
|
7
|
+
const id = e => e.table + '\0' + canonicalJson(e.key);
|
|
8
|
+
const selected = plan.entries.filter(e => d.deleted.includes(e.table));
|
|
9
|
+
const entries = new Map(selected.map(e => [id(e), e]));
|
|
10
|
+
const children = new Map(selected.map(e => [id(e), new Set()]));
|
|
11
|
+
const link = (parent, child) => {
|
|
12
|
+
if (id(parent) !== id(child) && children.has(id(parent)) && entries.has(id(child))) children.get(id(parent)).add(id(child));
|
|
13
|
+
};
|
|
14
|
+
for (const edge of d.edges) for (const parent of plan.rows.get(edge.from).values()) {
|
|
15
|
+
const keys = plan.keys.get(edge.table).pk;
|
|
16
|
+
for (const row of await matching(db, edge, [parent.row], keys.map(c => c === 'rowid' ? 'rowid AS rowid' : quote(c)).join(','))) {
|
|
17
|
+
const child = entries.get(edge.table + '\0' + canonicalJson(Object.fromEntries(keys.map(c => [c, row[c]]))));
|
|
18
|
+
if (child) link(parent, child);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
plan.foreignKeys = [];
|
|
22
|
+
for (const table of d.deleted) {
|
|
23
|
+
const groups = new Map();
|
|
24
|
+
for (const fk of await all(db, `PRAGMA foreign_key_list(${quote(table)})`)) {
|
|
25
|
+
if (!groups.has(fk.id)) groups.set(fk.id, []);
|
|
26
|
+
groups.get(fk.id).push(fk);
|
|
27
|
+
}
|
|
28
|
+
for (const group of groups.values()) {
|
|
29
|
+
const parentTable = d.deleted.find(name => name.toLowerCase() === group[0].table.toLowerCase());
|
|
30
|
+
const parents = parentTable && plan.rows.get(parentTable);
|
|
31
|
+
if (parentTable) plan.foreignKeys.push({ table, parentTable, group });
|
|
32
|
+
if (!parents) continue;
|
|
33
|
+
for (const parent of parents.values()) {
|
|
34
|
+
const parentKeys = plan.keys.get(parent.table).pk;
|
|
35
|
+
// Read the actual join: FK columns need not be part of a declared edge's projection.
|
|
36
|
+
const cols = plan.keys.get(table).pk;
|
|
37
|
+
const found = await all(db, `SELECT ${cols.map(c => c === 'rowid' ? 'rowid AS rowid' : quote(c)).join(',')} FROM ${quote(table)} WHERE ${group.map(f => `${quote(f.from)}=(SELECT ${quote(f.to || parentKeys[f.seq])} FROM ${quote(parent.table)} WHERE ${parentKeys.map(c => `${quote(c)}=?`).join(' AND ')})`).join(' AND ')}`, ...group.flatMap(() => parentKeys.map(c => parent.key[c])));
|
|
38
|
+
for (const row of found) {
|
|
39
|
+
const child = entries.get(table + '\0' + canonicalJson(Object.fromEntries(cols.map(c => [c, row[c]]))));
|
|
40
|
+
insist(child, `unreached_dependent:${table}:${parent.table}`, 409);
|
|
41
|
+
link(parent, child);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const remaining = new Map([...children].map(([key, deps]) => [key, deps.size]));
|
|
47
|
+
const parents = new Map(selected.map(e => [id(e), []]));
|
|
48
|
+
for (const [parent, deps] of children) for (const child of deps) parents.get(child).push(parent);
|
|
49
|
+
const queue = [...remaining].filter(([, n]) => n === 0).map(([key]) => key), result = [];
|
|
50
|
+
for (let i = 0; i < queue.length; i++) {
|
|
51
|
+
const key = queue[i]; result.push(entries.get(key));
|
|
52
|
+
for (const parent of parents.get(key)) { remaining.set(parent, remaining.get(parent) - 1); if (remaining.get(parent) === 0) queue.push(parent); }
|
|
53
|
+
}
|
|
54
|
+
insist(result.length === entries.size, 'cyclic_deletion_graph', 409);
|
|
55
|
+
return result.filter(e => e !== plan.root);
|
|
56
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { deletionOrder } from './order.mjs';
|
|
2
|
+
import { rootValues } from './root.mjs';
|
|
3
|
+
import { redactionEndsRates } from './rate.mjs';
|
|
4
|
+
import { all, statement, quote, insist, canonicalJson, sha256, chunks } from './sql.mjs';
|
|
5
|
+
|
|
6
|
+
const same = (a, b) => a.toLowerCase() === b.toLowerCase();
|
|
7
|
+
|
|
8
|
+
export async function tableKeys(db, table) {
|
|
9
|
+
const columns = await all(db, `PRAGMA table_info(${quote(table)})`);
|
|
10
|
+
insist(columns.length > 0, 'unknown_table:' + table);
|
|
11
|
+
const pk = columns.filter(c => c.pk).sort((a, b) => a.pk - b.pk).map(c => c.name);
|
|
12
|
+
return { columns: columns.map(c => c.name), pk: pk.length ? pk : ['rowid'] };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Every table holding a foreign key into a deleted table must itself be declared,
|
|
16
|
+
* so an ON DELETE action can never remove (or orphan) a row erasure did not fence. */
|
|
17
|
+
export async function checkDeclared(db, d) {
|
|
18
|
+
const declared = new Set([...d.tracked, ...d.redact.map(r => r.table), ...d.audit.map(a => a.table), ...d.retain.map(r => r.table)].map(t => t.toLowerCase()));
|
|
19
|
+
const tables = await all(db, "SELECT name,sql FROM sqlite_master WHERE type='table' AND sql IS NOT NULL ORDER BY name");
|
|
20
|
+
for (const table of tables.filter(t => /\bREFERENCES\b/i.test(t.sql))) {
|
|
21
|
+
for (const fk of await all(db, `PRAGMA foreign_key_list(${quote(table.name)})`)) {
|
|
22
|
+
if (d.tracked.some(t => same(t, fk.table))) {
|
|
23
|
+
insist(declared.has(table.name.toLowerCase()), `undeclared_dependent:${table.name}:${fk.table}`, 409);
|
|
24
|
+
if (d.deleted.some(t => same(t, fk.table)) && !d.deleted.some(t => same(t, table.name))) insist(!['CASCADE', 'SET NULL', 'SET DEFAULT'].includes(fk.on_delete), `unsafe_retained_fk:${table.name}:${fk.table}`, 409);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function needed(d, keys) {
|
|
31
|
+
const columns = new Map(d.tracked.map(t => [t, new Set(keys.get(t).pk)]));
|
|
32
|
+
for (const c of d.root.redact?.identityColumns ?? []) columns.get(d.root.table).add(c);
|
|
33
|
+
for (const item of [...d.edges, ...d.redact]) for (const c of item.on ? Object.values(item.on) : item.params ?? []) columns.get(item.from).add(c);
|
|
34
|
+
for (const edge of d.edges.filter(e => e.blob)) columns.get(edge.table).add(edge.blob.keyColumn);
|
|
35
|
+
return columns;
|
|
36
|
+
}
|
|
37
|
+
const projection = columns => [...columns].map(c => c === 'rowid' ? 'rowid AS rowid' : quote(c)).join(',');
|
|
38
|
+
|
|
39
|
+
export function matchSql(item, parents) {
|
|
40
|
+
if (!item.on) return parents.map(p => ({ where: `(${item.where})`, values: (item.params ?? []).map(c => p[c]) }));
|
|
41
|
+
const pairs = Object.entries(item.on);
|
|
42
|
+
const tuples = parents.map(p => pairs.map(([, parent]) => p[parent])).filter(t => t.every(v => v !== null && v !== undefined));
|
|
43
|
+
const where = `EXISTS(SELECT 1 FROM json_each(?) j WHERE ${pairs.map(([child], i) => `${quote(item.table)}.${quote(child)}=json_extract(j.value,'$[${i}]')`).join(' AND ')})`;
|
|
44
|
+
return chunks(tuples, 200).map(part => ({ where, values: [JSON.stringify(part)] }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Reads rows of the item's table reachable from `parents` through one edge. */
|
|
48
|
+
export async function matching(db, item, parents, select) {
|
|
49
|
+
const found = [];
|
|
50
|
+
for (const m of matchSql(item, parents)) found.push(...await all(db, `SELECT ${select} FROM ${quote(item.table)} WHERE ${m.where}`, ...m.values));
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Closes the declared graph from one subject to a fixed point. Rows carry only
|
|
55
|
+
* key, linking and blob-key columns: no personal value leaves this function. */
|
|
56
|
+
export async function collect(db, d, subject, remembered = []) {
|
|
57
|
+
const keys = new Map();
|
|
58
|
+
for (const t of new Set([...d.tracked, ...d.redact.map(r => r.table)])) keys.set(t, await tableKeys(db, t));
|
|
59
|
+
for (const item of d.audit) insist(keys.get(item.of).pk.length === 1, 'audit_composite_key:' + item.of);
|
|
60
|
+
const columns = needed(d, keys), rootKey = keys.get(d.root.table).pk;
|
|
61
|
+
if (d.root.redact) await redactionEndsRates(db, d, await rootValues(d, keys.get(d.root.table), subject, 'erased:admission'));
|
|
62
|
+
insist(subject && typeof subject === 'object' && canonicalJson(Object.keys(subject).sort()) === canonicalJson([...rootKey].sort()), 'subject_key');
|
|
63
|
+
const rows = new Map(d.tracked.map(t => [t, new Map()]));
|
|
64
|
+
const keyOf = (table, row) => Object.fromEntries(keys.get(table).pk.map(c => [c, row[c]]));
|
|
65
|
+
const root = await statement(db, `SELECT ${projection(columns.get(d.root.table))} FROM ${quote(d.root.table)} WHERE ${rootKey.map(c => `${c === 'rowid' ? 'rowid' : quote(c)}=?`).join(' AND ')}`, ...rootKey.map(c => subject[c])).first();
|
|
66
|
+
if (!root && !remembered.length) return null;
|
|
67
|
+
if (root) rows.get(d.root.table).set(canonicalJson(keyOf(d.root.table, root)), { table: d.root.table, key: keyOf(d.root.table, root), row: root, depth: 0 });
|
|
68
|
+
const known = new Map(d.tracked.map(t => [t, new Map()]));
|
|
69
|
+
const remember = (table, row) => known.get(table).set(canonicalJson(row), row);
|
|
70
|
+
for (const e of remembered) remember(e.table, e.row);
|
|
71
|
+
for (const [table, old] of known) {
|
|
72
|
+
const pk = keys.get(table).pk;
|
|
73
|
+
for (const part of chunks([...old.values()], 200)) {
|
|
74
|
+
const found = await all(db, `SELECT ${projection(columns.get(table))} FROM ${quote(table)} WHERE EXISTS(SELECT 1 FROM json_each(?) j WHERE ${pk.map((c, i) => `${quote(c)} IS json_extract(j.value,'$[${i}]')`).join(' AND ')})`, JSON.stringify(part.map(row => pk.map(c => row[c]))));
|
|
75
|
+
for (const row of found) { remember(table, row); const key = keyOf(table, row); rows.get(table).set(canonicalJson(key), { table, key, row, depth: 1 }); }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (root) remember(d.root.table, root);
|
|
79
|
+
let frontier = new Map([...known].filter(([, r]) => r.size).map(([t, r]) => [t, [...r.values()]])), depth = 0;
|
|
80
|
+
while (frontier.size) {
|
|
81
|
+
const next = new Map(); depth++;
|
|
82
|
+
for (const edge of d.edges) {
|
|
83
|
+
const parents = frontier.get(edge.from);
|
|
84
|
+
if (!parents?.length) continue;
|
|
85
|
+
for (const row of await matching(db, edge, parents, projection(columns.get(edge.table)))) {
|
|
86
|
+
const key = keyOf(edge.table, row), id = canonicalJson(key);
|
|
87
|
+
if (rows.get(edge.table).has(id)) continue;
|
|
88
|
+
remember(edge.table, row);
|
|
89
|
+
rows.get(edge.table).set(id, { table: edge.table, key, row, depth });
|
|
90
|
+
if (!next.has(edge.table)) next.set(edge.table, []);
|
|
91
|
+
next.get(edge.table).push(row);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
frontier = next;
|
|
95
|
+
}
|
|
96
|
+
const entries = [...rows.values()].flatMap(m => [...m.values()]);
|
|
97
|
+
const plan = { subject, root: root ? rows.get(d.root.table).get(canonicalJson(subject)) : null, keys, rows, entries, known };
|
|
98
|
+
plan.order = await deletionOrder(db, d, plan);
|
|
99
|
+
return plan;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function counts(db, d, plan) {
|
|
103
|
+
const deleted = Object.fromEntries(d.deleted.map(t => [t, plan.rows.get(t).size]).filter(([, n]) => n));
|
|
104
|
+
const parents = table => [...plan.rows.get(table).values()].map(e => e.row);
|
|
105
|
+
const redacted = d.root.redact ? { [d.root.table]: plan.rows.get(d.root.table).size } : {}, audited = {};
|
|
106
|
+
for (const item of d.redact) for (const m of matchSql(item, parents(item.from))) {
|
|
107
|
+
redacted[item.table] = (redacted[item.table] ?? 0) + (await statement(db, `SELECT count(*) AS n FROM ${quote(item.table)} WHERE ${m.where}`, ...m.values).first()).n;
|
|
108
|
+
}
|
|
109
|
+
for (const item of d.audit) for (const part of chunks([...plan.rows.get(item.of).values()].map(e => Object.values(e.key)[0]), 200)) {
|
|
110
|
+
audited[item.table] = (audited[item.table] ?? 0) + (await statement(db, `SELECT count(*) AS n FROM ${quote(item.table)} WHERE ${quote(item.column)} IN (SELECT value FROM json_each(?))${item.where ? ` AND (${item.where})` : ''}`, JSON.stringify(part)).first()).n;
|
|
111
|
+
}
|
|
112
|
+
const blobKeys = new Set();
|
|
113
|
+
for (const e of d.edges.filter(e => e.blob)) for (const x of plan.rows.get(e.table).values()) if (x.row[e.blob.keyColumn] != null) blobKeys.add(canonicalJson([e.blob.bucket, x.row[e.blob.keyColumn]]));
|
|
114
|
+
const blobs = blobKeys.size;
|
|
115
|
+
return { deleted, redacted, audited, blobs };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function planHash(d, plan, summary) {
|
|
119
|
+
const rows = Object.fromEntries([...plan.rows].map(([t, m]) => [t, [...m.keys()].sort()]));
|
|
120
|
+
return sha256(canonicalJson({ entity: d.entity, subject: plan.subject, rows, summary }));
|
|
121
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ErasureError, guard, insist, quote } from './sql.mjs';
|
|
2
|
+
|
|
3
|
+
const LINKS = 'rer_erasure_rates';
|
|
4
|
+
const ident = value => typeof value === 'string' && /^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(value);
|
|
5
|
+
const entityName = value => typeof value === 'string' && /^[a-z][a-z0-9_-]{0,63}$/.test(value);
|
|
6
|
+
|
|
7
|
+
// `root.live` is an SQL predicate over the root row that is true while its
|
|
8
|
+
// subject may still be counted. A redacted root survives erasure under the same
|
|
9
|
+
// key, so its declaration must give one that the redaction makes false.
|
|
10
|
+
const live = root => root.live === undefined || (typeof root.live === 'string' && root.live.trim().length > 0);
|
|
11
|
+
const present = root => `SELECT 1 FROM ${quote(root.table)} WHERE ${quote(root.column)}=?${root.live ? ` AND (${root.live})` : ''}`;
|
|
12
|
+
|
|
13
|
+
/** Edges for a declaration's `edges`: the correlation rows of one subject, then
|
|
14
|
+
* every counter row they name. Counters are deleted before their links. */
|
|
15
|
+
export function rateEdges({ entity, root, counters }) {
|
|
16
|
+
insist(entityName(entity) && ident(root?.table) && ident(root?.column) && live(root), 'rate_root');
|
|
17
|
+
insist(Array.isArray(counters) && counters.length > 0 && counters.every(c => ident(c.table) && ident(c.keyColumn)), 'rate_counters');
|
|
18
|
+
return [
|
|
19
|
+
{ table: LINKS, from: root.table, where: `entity='${entity}' AND subject=?`, params: [root.column], rate: { live: root.live ?? null } },
|
|
20
|
+
...counters.map(c => ({ table: c.table, from: LINKS, where: `${quote(c.keyColumn)}=? AND ?='${c.table}'`, params: ['rate_key', 'rate_table'] })),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Statements to put before the counter upsert in the SAME batch: they abort the
|
|
25
|
+
* batch when the subject row is already gone or no longer live, then record who
|
|
26
|
+
* the counter key belongs to. */
|
|
27
|
+
export function rateCorrelation(db, { entity, root, subject, rateTable, rateKey }) {
|
|
28
|
+
insist(entityName(entity) && ident(root?.table) && ident(root?.column) && live(root) && ident(rateTable), 'rate_root');
|
|
29
|
+
insist(subject !== null && subject !== undefined && typeof rateKey === 'string' && rateKey.length > 0, 'rate_subject');
|
|
30
|
+
return [
|
|
31
|
+
guard(db, `EXISTS(${present(root)})`, subject),
|
|
32
|
+
db.prepare('DELETE FROM rer_erasure_guard'),
|
|
33
|
+
db.prepare(`INSERT OR IGNORE INTO ${LINKS}(rate_table,rate_key,entity,subject) VALUES(?,?,?,?)`).bind(rateTable, rateKey, entity, String(subject)),
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Runs `statements` (the seed's counter upserts) with their correlation in one
|
|
38
|
+
* batch and returns their results. A subject erased before the write refuses with
|
|
39
|
+
* `subject_gone` (409) and nothing is written. */
|
|
40
|
+
export async function rateWrite(db, spec, statements) {
|
|
41
|
+
const link = rateCorrelation(db, spec);
|
|
42
|
+
try { return (await db.batch([...link, ...statements])).slice(link.length); }
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (!await db.prepare(present(spec.root)).bind(spec.subject).first()) throw new ErasureError('subject_gone', 409);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Refuses a root redaction that would leave a rate root live: correlated writes
|
|
50
|
+
* naming the retained key would then still be admitted after erasure. */
|
|
51
|
+
export async function redactionEndsRates(db, d, set) {
|
|
52
|
+
for (const edge of d.edges.filter(e => e.rate && e.from === d.root.table)) {
|
|
53
|
+
const columns = Object.keys(set);
|
|
54
|
+
const live = await db.prepare(`SELECT 1 AS live FROM (SELECT ${columns.map(c => `? AS ${quote(c)}`).join(',')}) WHERE (${edge.rate.live})`).bind(...Object.values(set)).first().catch(() => ({ live: 1 }));
|
|
55
|
+
insist(!live, 'rate_root_live', 409);
|
|
56
|
+
}
|
|
57
|
+
}
|