@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,118 @@
|
|
|
1
|
+
import { all, insist, quote, PortabilityError } from './storage.mjs';
|
|
2
|
+
import { canonicalJson, sha256, base64, unbase64, utf8 } from './hash.mjs';
|
|
3
|
+
const defaults=['sqlite_%','_cf_%','d1_migrations','_rer_deploy_migrations','_rer_portability_migrations','rer_portability_%','rer_mail_development_outbox','rer_ai_development_outbox','oa_%','oc_%','rer_dependency_keys','rer_dependency_pending','rer_dependency_codes'];
|
|
4
|
+
const fields=['product','excludeTables','includeTables','derivedTables','secretColumns','exportRow','replaceableTables','requiredConfiguration','validateStaged','acceptedSchemas','buckets','extraPayloads','partBytes'];
|
|
5
|
+
const name=value=>typeof value==='string' && value.length>0 && value.length<=512 && !/[\x00-\x1f]/.test(value);
|
|
6
|
+
const names=value=>Array.isArray(value) && value.every(name) && new Set(value).size===value.length;
|
|
7
|
+
export function policy(input) {
|
|
8
|
+
insist(input && typeof input==='object' && Object.keys(input).every(k=>fields.includes(k)) && name(input.product),'invalid_policy');
|
|
9
|
+
const result={excludeTables:[],includeTables:[],derivedTables:[],secretColumns:{},replaceableTables:[],requiredConfiguration:[],acceptedSchemas:{},buckets:{},extraPayloads:{},partBytes:16*1024*1024,...input};
|
|
10
|
+
for(const key of ['excludeTables','includeTables','derivedTables','replaceableTables','requiredConfiguration']) insist(names(result[key]),'invalid_policy_'+key);
|
|
11
|
+
for(const key of ['exportRow','validateStaged']) insist(result[key]===undefined || typeof result[key]==='function','invalid_policy_'+key);
|
|
12
|
+
insist(Number.isSafeInteger(result.partBytes) && result.partBytes>=1024 && result.partBytes<=16*1024*1024,'invalid_part_limit');
|
|
13
|
+
for(const [table,columns] of Object.entries(result.secretColumns)) insist(name(table) && names(columns),'invalid_secret_columns');
|
|
14
|
+
for(const [identity,entry] of Object.entries(result.acceptedSchemas)) insist(/^[a-f0-9]{64}$/.test(identity) && typeof entry.upgradeRow==='function','invalid_accepted_schema');
|
|
15
|
+
for(const [binding,entry] of Object.entries(result.buckets)) {
|
|
16
|
+
insist(name(binding) && entry && Object.keys(entry).every(k=>['prefix','objectReferences'].includes(k)),'invalid_bucket_policy');
|
|
17
|
+
insist(entry.prefix===undefined || typeof entry.prefix==='string','invalid_bucket_prefix');
|
|
18
|
+
if(entry.objectReferences) insist(name(entry.objectReferences.table) && name(entry.objectReferences.keyColumn),'invalid_object_references');
|
|
19
|
+
}
|
|
20
|
+
for(const [key,entry] of Object.entries(result.extraPayloads)) insist(name(key) && typeof entry.produce==='function' && typeof entry.consume==='function','invalid_extra_payload');
|
|
21
|
+
for(const key of ['secretColumns','acceptedSchemas','buckets','extraPayloads'])result[key]=Object.assign(Object.create(null),result[key]);
|
|
22
|
+
return {...result,excludeTables:[...defaults,...result.excludeTables]};
|
|
23
|
+
}
|
|
24
|
+
const identifier=value=>value.replace(/[A-Z]/g,letter=>letter.toLowerCase());
|
|
25
|
+
const matches=(table,pattern)=>pattern.endsWith('%') ? identifier(table).startsWith(identifier(pattern.slice(0,-1))) : identifier(table)===identifier(pattern);
|
|
26
|
+
async function archived(db,config) {
|
|
27
|
+
const objects=await all(db,"SELECT name,sql,type,tbl_name FROM sqlite_master WHERE sql IS NOT NULL ORDER BY name");
|
|
28
|
+
const kinds=await all(db,'PRAGMA table_list');
|
|
29
|
+
const derived=config.derivedTables;
|
|
30
|
+
const excluded=objects.filter(x=>x.type==='table' && !config.includeTables.includes(x.name) &&
|
|
31
|
+
(config.excludeTables.some(p=>matches(x.name,p)) || derived.some(p=>x.name===p || (kinds.find(k=>k.name===x.name)?.type==='shadow' && x.name.startsWith(p+'_')))));
|
|
32
|
+
objects.sort((a,b)=>a.name<b.name?-1:a.name>b.name?1:0);
|
|
33
|
+
return {objects,kinds,excluded,entries:objects.filter(x=>x.type==='table' && !excluded.some(y=>y.name===x.name))};
|
|
34
|
+
}
|
|
35
|
+
const identityOf=tables=>sha256(canonicalJson(tables.map(({name,sql})=>({name,sql}))));
|
|
36
|
+
// The archive schema identity alone, for databases that need not satisfy the policy.
|
|
37
|
+
export async function schemaIdentity(db,config) { return identityOf((await archived(db,config)).entries); }
|
|
38
|
+
export async function catalogue(db,config) {
|
|
39
|
+
const {objects,kinds,excluded,entries}=await archived(db,config);
|
|
40
|
+
const tables=[];
|
|
41
|
+
for(const entry of entries) {
|
|
42
|
+
insist(!/^CREATE\s+VIRTUAL\s+TABLE/i.test(entry.sql) && kinds.find(x=>x.name===entry.name)?.type!=='shadow','virtual_table:'+entry.name);
|
|
43
|
+
const columns=(await all(db,`PRAGMA table_xinfo(${quote(entry.name)})`)).map(c=>({name:c.name,type:c.type,notnull:c.notnull,pk:c.pk,hidden:c.hidden}));
|
|
44
|
+
insist(columns.every(c=>[0,2,3].includes(c.hidden) && !c.name.startsWith('rer_portability_')),'unsupported_column:'+entry.name);
|
|
45
|
+
columns.forEach(c=>{if(c.hidden)c.generated=c.hidden===2?'virtual':'stored';delete c.hidden;});
|
|
46
|
+
// Both archived base cells and visible staging reads must fit D1's SQL
|
|
47
|
+
// limit, with room for copy names and joins.
|
|
48
|
+
insist([false,true].every(encoded=>utf8(projectedColumns({columns},encoded)).length+1024<=100000),'transfer_sql_limit:'+entry.name);
|
|
49
|
+
const dependencies=[...new Set((await all(db,`PRAGMA foreign_key_list(${quote(entry.name)})`)).map(ref=>objects.find(x=>x.type==='table' && identifier(x.name)===identifier(ref.table))?.name??ref.table))].sort();
|
|
50
|
+
for(const parent of dependencies) insist(!excluded.some(x=>x.name===parent),'excluded_foreign_key:'+entry.name+':'+parent);
|
|
51
|
+
const rowidAlias=columns.some(c=>c.pk)?null:['rowid','_rowid_','oid'].find(alias=>!columns.some(c=>c.name.toLowerCase()===alias));
|
|
52
|
+
insist(rowidAlias!==undefined,'unsupported_rowid:'+entry.name);
|
|
53
|
+
insist(['rowid','_rowid_','oid'].some(alias=>!columns.some(c=>c.name.toLowerCase()===alias)),'unsupported_copy_rowid:'+entry.name);
|
|
54
|
+
tables.push({name:entry.name,sql:entry.sql,columns,dependencies,rowidAlias});
|
|
55
|
+
}
|
|
56
|
+
for(const [table,list] of Object.entries(config.secretColumns)) {
|
|
57
|
+
const entry=tables.find(t=>t.name===table); insist(entry,'secret_table_missing:'+table);
|
|
58
|
+
for(const key of list) {const column=entry.columns.find(c=>c.name===key);insist(column && !column.notnull && !column.pk && !column.generated,'invalid_secret_column:'+table+':'+key);}
|
|
59
|
+
}
|
|
60
|
+
for(const table of [...config.replaceableTables,...config.includeTables]) insist(tables.some(x=>x.name===table),'policy_table_missing:'+table);
|
|
61
|
+
for(const spec of Object.values(config.buckets)) if(spec.objectReferences) {
|
|
62
|
+
const {table,keyColumn}=spec.objectReferences;
|
|
63
|
+
insist(tables.find(x=>x.name===table)?.columns.some(c=>c.name===keyColumn),'reference_column_missing');
|
|
64
|
+
insist(!config.secretColumns[table]?.includes(keyColumn),'reference_column_excluded');
|
|
65
|
+
}
|
|
66
|
+
const identity=await identityOf(tables);
|
|
67
|
+
return {identity,tables,excludedTables:excluded.map(x=>x.name).sort(),triggers:objects.filter(x=>x.type==='trigger' && tables.some(t=>t.name===x.tbl_name))};
|
|
68
|
+
}
|
|
69
|
+
export function affinity(type) {
|
|
70
|
+
const text=type.toUpperCase();
|
|
71
|
+
if(text.includes('INT')) return 'INTEGER';
|
|
72
|
+
if(/CHAR|CLOB|TEXT/.test(text)) return 'TEXT';
|
|
73
|
+
if(text==='' || text.includes('BLOB')) return 'BLOB';
|
|
74
|
+
return 'NUMBER';
|
|
75
|
+
}
|
|
76
|
+
export function projectedColumns(table,encoded=false) {
|
|
77
|
+
return (encoded?writableColumns(table):table.columns).map(c=>encoded && affinity(c.type)==='INTEGER'?`CAST(s.${quote(c.name)} AS TEXT) AS ${quote(c.name)}`:`s.${quote(c.name)}`).join(',');
|
|
78
|
+
}
|
|
79
|
+
export const writableColumns=table=>table.columns.filter(c=>!c.generated);
|
|
80
|
+
export function validateRow(table,row,secrets=[]) {
|
|
81
|
+
const columns=writableColumns(table).filter(c=>!secrets.includes(c.name));
|
|
82
|
+
insist(row && typeof row==='object' && !Array.isArray(row) && canonicalJson(Object.keys(row).sort())===canonicalJson(columns.map(c=>c.name).sort()),'row_columns:'+table.name);
|
|
83
|
+
for(const column of columns) {
|
|
84
|
+
const value=row[column.name],type=affinity(column.type), label=table.name+':'+column.name;
|
|
85
|
+
if(value===null) {insist(!column.notnull,'row_not_null:'+label);continue;}
|
|
86
|
+
if(typeof value==='object') {
|
|
87
|
+
insist(Object.keys(value).length===1 && typeof value.blob==='string','row_type:'+label); unbase64(value.blob);
|
|
88
|
+
insist(type==='BLOB','row_type:'+label);continue;
|
|
89
|
+
}
|
|
90
|
+
insist(typeof value==='string' || (typeof value==='number' && Number.isFinite(value)),'row_type:'+label);
|
|
91
|
+
if(type==='INTEGER') insist(typeof value==='number' && Number.isSafeInteger(value),'unsafe_integer:'+label);
|
|
92
|
+
if(type==='NUMBER') insist(typeof value==='number','row_type:'+label);
|
|
93
|
+
if(type==='TEXT') insist(typeof value==='string','row_type:'+label);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function encodedRow(table,raw,secrets=[]) {
|
|
97
|
+
const row=Object.create(null);
|
|
98
|
+
for(const column of writableColumns(table)) {
|
|
99
|
+
if(secrets.includes(column.name)) continue;
|
|
100
|
+
let value=raw[column.name];
|
|
101
|
+
if(affinity(column.type)==='INTEGER' && value!==null) {
|
|
102
|
+
insist(/^-?\d+$/.test(String(value)) && Number.isSafeInteger(Number(value)),'unsafe_integer:'+table.name+':'+column.name);
|
|
103
|
+
value=Number(value);
|
|
104
|
+
}
|
|
105
|
+
if(Array.isArray(value) || value instanceof ArrayBuffer || ArrayBuffer.isView(value)) value={blob:base64(Array.isArray(value)?Uint8Array.from(value):new Uint8Array(value.buffer??value,value.byteOffset??0,value.byteLength))};
|
|
106
|
+
row[column.name]=value;
|
|
107
|
+
}
|
|
108
|
+
validateRow(table,row,secrets); return row;
|
|
109
|
+
}
|
|
110
|
+
export function bindCell(value) { return value && typeof value==='object' ? unbase64(value.blob).buffer : value; }
|
|
111
|
+
export function mapRow(config,table,row) {
|
|
112
|
+
if(!config.exportRow) return row;
|
|
113
|
+
const result=config.exportRow(table.name,structuredClone(row));
|
|
114
|
+
insist(!(result instanceof Promise),'export_row_must_be_pure');
|
|
115
|
+
validateRow(table,result,config.secretColumns[table.name]??[]);
|
|
116
|
+
insist(canonicalJson(config.exportRow(table.name,structuredClone(result)))===canonicalJson(result),'export_row_not_idempotent:'+table.name);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { lstat, readFile, mkdir, open, rename } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, dirname } from 'node:path';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
export function options(args) {
|
|
5
|
+
const command=args.shift(),result={command};
|
|
6
|
+
if(!['export','import','status','abort'].includes(command))throw new Error('Choose export, import, status or abort.');
|
|
7
|
+
const allowed=['origin','session-file','out','from','resume','base-path'];
|
|
8
|
+
while(args.length){const key=args.shift();if(!key?.startsWith('--') || !allowed.includes(key.slice(2)) || result[key.slice(2)]!==undefined || !args.length)throw new Error('Invalid command options.');result[key.slice(2)]=args.shift();}
|
|
9
|
+
let url;try{url=new URL(result.origin);}catch{throw new Error('Supply a valid origin.');}
|
|
10
|
+
if(url.username || url.password || url.pathname!=='/' || url.search || url.hash)throw new Error('The origin must have no credentials, path, query or fragment.');
|
|
11
|
+
if(url.protocol!=='https:' && !(url.protocol==='http:' && /^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?\/?$/.test(result.origin)))throw new Error('Use HTTPS or an exact loopback HTTP origin.');
|
|
12
|
+
result.origin=url.origin;
|
|
13
|
+
for(const key of ['session-file',...(command==='export'?['out']:command==='import'?['from']:[])])if(!isAbsolute(result[key]??''))throw new Error('File paths must be absolute.');
|
|
14
|
+
if(['status','abort'].includes(command) && !result.resume)throw new Error('Supply --resume with the job id.');
|
|
15
|
+
if(result.resume && !/^[a-zA-Z0-9-]{1,100}$/.test(result.resume))throw new Error('Invalid job id.');
|
|
16
|
+
result.basePath=result['base-path']??'/api/portability';
|
|
17
|
+
if(!/^\/[A-Za-z0-9/_-]+$/.test(result.basePath) || result.basePath.endsWith('/'))throw new Error('Invalid API base path.');
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
export async function cookieFile(path) {
|
|
21
|
+
const info=await lstat(path);if(!info.isFile() || info.size>4096)throw new Error('The session file must be a small regular file.');
|
|
22
|
+
const file=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW);
|
|
23
|
+
let value;try{value=(await file.readFile('utf8')).trim();}finally{await file.close();}
|
|
24
|
+
if(!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+=[!#-:<-~]+$/.test(value) || value.includes(';'))throw new Error('The session file must hold one cookie.');
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
export async function absent(path) {
|
|
28
|
+
try{await lstat(path);}catch(e){if(e.code==='ENOENT')return;throw e;}throw new Error('The output already exists.');
|
|
29
|
+
}
|
|
30
|
+
export async function privateFile(path,resume=false) {
|
|
31
|
+
await mkdir(dirname(path),{recursive:true,mode:0o700});
|
|
32
|
+
const file=await open(path,constants.O_WRONLY|constants.O_CREAT|constants.O_NOFOLLOW|(resume?constants.O_TRUNC:constants.O_EXCL),0o600);
|
|
33
|
+
await file.chmod(0o600);return file;
|
|
34
|
+
}
|
|
35
|
+
export async function checkpoint(path,value) {
|
|
36
|
+
const file=await privateFile(path,true);try{await file.writeFile(JSON.stringify(value)+'\n');await file.sync();}finally{await file.close();}
|
|
37
|
+
}
|
|
38
|
+
export async function promote(partial,path) {await rename(partial,path);}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createReadStream,existsSync,realpathSync } from 'node:fs';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
5
|
+
import { options, cookieFile, absent, privateFile, checkpoint, promote } from './cli-options.mjs';
|
|
6
|
+
import { readArchive } from './format.mjs';
|
|
7
|
+
import { canonicalJson } from './hash.mjs';
|
|
8
|
+
export async function main(args) {
|
|
9
|
+
const opts=options([...args]),cookie=await cookieFile(opts['session-file']);
|
|
10
|
+
const endpoint=opts.origin+opts.basePath;
|
|
11
|
+
async function request(path,method='GET',body) {
|
|
12
|
+
let response;
|
|
13
|
+
try{response=await fetch(endpoint+'/'+path,{method,redirect:'error',headers:{Cookie:cookie,Origin:opts.origin,...(body===undefined?{}:{'Content-Type':'application/json'})},body:body===undefined?undefined:JSON.stringify(body)});}
|
|
14
|
+
catch{throw new Error('The transfer request failed.');}
|
|
15
|
+
if(!response.ok) {await response.body?.cancel();throw new Error('The transfer was refused (HTTP '+response.status+').');}
|
|
16
|
+
return response;
|
|
17
|
+
}
|
|
18
|
+
const get=async(...args)=>(await request(...args)).json();
|
|
19
|
+
const shell=value=>"'"+value.replaceAll("'","'\\''")+"'";
|
|
20
|
+
const resumeCommand=(id)=>'node '+shell(fileURLToPath(import.meta.url))+' '+opts.command+' --origin '+shell(opts.origin)+' --session-file '+shell(opts['session-file'])+' '+(opts.command==='export'?'--out '+shell(opts.out):'--from '+shell(opts.from))+' --resume '+id;
|
|
21
|
+
if(opts.command==='export') {
|
|
22
|
+
await absent(opts.out);
|
|
23
|
+
const id=opts.resume??(await get('exports','POST',{})).id;
|
|
24
|
+
await checkpoint(opts.out+'.transfer.json',{version:1,kind:'export',id,origin:opts.origin});
|
|
25
|
+
process.stdout.write('Job '+id+'\nResume: '+resumeCommand(id)+'\n');
|
|
26
|
+
let status=await get('exports/'+id);
|
|
27
|
+
while(status.phase==='building')status=await get('exports/'+id+'/advance','POST',{expectedPart:status.completed,maxParts:16});
|
|
28
|
+
if(status.phase!=='complete')throw new Error('The export is not complete.');
|
|
29
|
+
const response=await request('exports/'+id+'/download'),file=await privateFile(opts.out+'.partial',Boolean(opts.resume));
|
|
30
|
+
try{for await(const record of readArchive(response.body))await file.writeFile(canonicalJson(record)+'\n');await file.sync();}
|
|
31
|
+
finally{await file.close();}
|
|
32
|
+
await promote(opts.out+'.partial',opts.out);process.stdout.write('Export complete.\n');return;
|
|
33
|
+
}
|
|
34
|
+
if(opts.command==='import') {
|
|
35
|
+
const source=Readable.toWeb(createReadStream(opts.from));let id=opts.resume,closing;
|
|
36
|
+
// Verify every record even when earlier uploads are already acknowledged.
|
|
37
|
+
for await(const record of readArchive(source)) {
|
|
38
|
+
if(record.kind==='manifest') {
|
|
39
|
+
id??=(await get('imports','POST',{confirmed:true,manifest:record})).id;
|
|
40
|
+
await checkpoint(opts.from+'.transfer.json',{version:1,kind:'import',id,origin:opts.origin});
|
|
41
|
+
process.stdout.write('Job '+id+'\nResume: '+resumeCommand(id)+'\n');
|
|
42
|
+
}else if(record.kind==='part')await get('imports/'+id+'/parts/'+record.index,'PUT',record);
|
|
43
|
+
else closing=record;
|
|
44
|
+
}
|
|
45
|
+
const result=await get('imports/'+id+'/finalize','POST',{contentHash:closing.contentHash,archiveHash:closing.archiveHash});
|
|
46
|
+
process.stdout.write('Import '+result.phase+'.\nRequired configuration: '+result.requiredConfiguration.join(', ')+'\n');return;
|
|
47
|
+
}
|
|
48
|
+
let collection='exports';
|
|
49
|
+
let status;try{status=await get(collection+'/'+opts.resume);}catch{collection='imports';status=await get(collection+'/'+opts.resume);}
|
|
50
|
+
if(opts.command==='abort')status=await get(collection+'/'+opts.resume+'/abort','POST',{});
|
|
51
|
+
process.stdout.write(JSON.stringify(status)+'\n');
|
|
52
|
+
}
|
|
53
|
+
if(process.argv[1] && existsSync(process.argv[1]) && import.meta.url===pathToFileURL(realpathSync(process.argv[1])).href)main(process.argv.slice(2)).catch(()=>{
|
|
54
|
+
// Errors may contain server diagnostics or filesystem paths. Do not echo them.
|
|
55
|
+
process.stderr.write('Transfer failed: check the origin, absolute paths, output, session file and job status.\n');process.exitCode=1;
|
|
56
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { all,statement,quote,insist } from './storage.mjs';
|
|
2
|
+
import { projectedColumns,encodedRow,mapRow,bindCell,writableColumns } from './catalogue.mjs';
|
|
3
|
+
import { canonicalJson } from './hash.mjs';
|
|
4
|
+
import { generatedCopySQL } from './generated.mjs';
|
|
5
|
+
|
|
6
|
+
export const sidecar=copy=>copy+'_meta';
|
|
7
|
+
export function copyRowid(table) {
|
|
8
|
+
const alias=['rowid','_rowid_','oid'].find(a=>!table.columns.some(c=>c.name.toLowerCase()===a));
|
|
9
|
+
insist(alias,'unsupported_copy_rowid:'+table.name);return quote(alias);
|
|
10
|
+
}
|
|
11
|
+
const keys=table=>table.columns.filter(c=>c.pk).sort((a,b)=>a.pk-b.pk);
|
|
12
|
+
const order=table=>keys(table).length?keys(table).map(c=>quote(c.name)).join(','):quote(table.rowidAlias);
|
|
13
|
+
export function createCopy(db,table,copy,snapshot=false) {
|
|
14
|
+
const ordering=order(table),meta=quote(sidecar(copy));
|
|
15
|
+
const columns=writableColumns(table).map(c=>quote(c.name)).join(',');
|
|
16
|
+
const product=table.columns.some(c=>c.generated)
|
|
17
|
+
?[db.prepare(generatedCopySQL(table,copy)),...(snapshot?[db.prepare(`INSERT INTO ${quote(copy)} SELECT ${columns} FROM ${quote(table.name)} ORDER BY ${ordering}`)]:[])]
|
|
18
|
+
:[db.prepare(`CREATE TABLE ${quote(copy)} AS SELECT ${table.added?columns:'*'} FROM ${quote(table.name)} ${snapshot?'ORDER BY '+ordering:'WHERE 0'}`)];
|
|
19
|
+
return [...product,
|
|
20
|
+
db.prepare(`CREATE TABLE ${meta}(rer_portability_row_order INTEGER PRIMARY KEY,rer_portability_copy_rowid INTEGER NOT NULL UNIQUE,rer_portability_source_rowid INTEGER,rer_portability_tombstone TEXT)`),
|
|
21
|
+
...(snapshot?[db.prepare(`INSERT INTO ${meta} SELECT ROW_NUMBER() OVER(ORDER BY ${ordering})-1,ROW_NUMBER() OVER(ORDER BY ${ordering}),${table.rowidAlias?quote(table.rowidAlias):'NULL'},NULL FROM ${quote(table.name)}`)]:[])];
|
|
22
|
+
}
|
|
23
|
+
export async function readCopy(db,table,copy,offset,limit,encoded=false) {
|
|
24
|
+
const columns=projectedColumns(table,encoded);
|
|
25
|
+
const raw=await all(db,`SELECT ${columns} FROM ${quote(copy)} s JOIN ${quote(sidecar(copy))} m ON s.${copyRowid(table)}=m.rer_portability_copy_rowid ORDER BY m.rer_portability_row_order LIMIT ? OFFSET ?`,limit,offset);
|
|
26
|
+
const meta=await all(db,`SELECT * FROM ${quote(sidecar(copy))} ORDER BY rer_portability_row_order LIMIT ? OFFSET ?`,limit,offset);
|
|
27
|
+
insist(raw.length===meta.length,'copy_inventory:'+table.name);
|
|
28
|
+
return raw.map((row,i)=>({row,meta:meta[i]}));
|
|
29
|
+
}
|
|
30
|
+
export async function normalizeCopy(context,table,copy) {
|
|
31
|
+
const {db,config}=context,secrets=config.secretColumns[table.name]??[];
|
|
32
|
+
// Generated reads must reflect exactly the base cells a restore will receive.
|
|
33
|
+
if(table.columns.some(c=>c.generated)&&secrets.length)await db.prepare(`UPDATE ${quote(copy)} SET ${secrets.map(name=>quote(name)+'=NULL').join(',')}`).run();
|
|
34
|
+
if(config.exportRow)for(let offset=0;offset<table.count;offset+=500) {
|
|
35
|
+
const statements=[];
|
|
36
|
+
for(const {row,meta} of await readCopy(db,table,copy,offset,500,true)) {
|
|
37
|
+
const before=encodedRow(table,row,secrets),after=mapRow(config,table,before);
|
|
38
|
+
if(canonicalJson(before)===canonicalJson(after))continue;
|
|
39
|
+
const columns=writableColumns(table).filter(c=>!secrets.includes(c.name));
|
|
40
|
+
// The rowid is internal, checked, and literal so a 100-column update needs only 100 bindings.
|
|
41
|
+
insist(Number.isSafeInteger(meta.rer_portability_copy_rowid),'copy_rowid');
|
|
42
|
+
statements.push(statement(db,`UPDATE ${quote(copy)} SET ${columns.map(c=>quote(c.name)+'=?').join(',')} WHERE ${copyRowid(table)}=${meta.rer_portability_copy_rowid}`,...columns.map(c=>bindCell(after[c.name]))));
|
|
43
|
+
}
|
|
44
|
+
if(statements.length)await db.batch(statements);
|
|
45
|
+
}
|
|
46
|
+
const pk=keys(table);if(!pk.length)return;
|
|
47
|
+
const normalizedOrder=pk.map(c=>'s.'+quote(c.name)).join(',')+',m.rer_portability_copy_rowid';
|
|
48
|
+
const temporary=sidecar(copy)+'_sorted';
|
|
49
|
+
await db.batch([db.prepare(`CREATE TABLE ${quote(temporary)} AS SELECT ROW_NUMBER() OVER(ORDER BY ${normalizedOrder})-1 AS rer_portability_row_order,m.rer_portability_copy_rowid,m.rer_portability_source_rowid,m.rer_portability_tombstone FROM ${quote(copy)} s JOIN ${quote(sidecar(copy))} m ON s.${copyRowid(table)}=m.rer_portability_copy_rowid`),
|
|
50
|
+
db.prepare(`DROP TABLE ${quote(sidecar(copy))}`),db.prepare(`ALTER TABLE ${quote(temporary)} RENAME TO ${quote(sidecar(copy))}`)]);
|
|
51
|
+
}
|
|
52
|
+
export function restoreCopy(db,table,copy) {
|
|
53
|
+
const columns=writableColumns(table).map(c=>quote(c.name)),names=columns.join(','),projection=table.columns.some(c=>c.generated)?columns.map(c=>'s.'+c).join(','):'s.*',meta=quote(sidecar(copy));
|
|
54
|
+
const from=`FROM ${quote(copy)} s JOIN ${meta} m ON s.${copyRowid(table)}=m.rer_portability_copy_rowid ORDER BY m.rer_portability_row_order`;
|
|
55
|
+
// SQLite's implicit insert list contains only writable columns. Keeping it
|
|
56
|
+
// implicit also avoids repeating long column names within D1's SQL limit.
|
|
57
|
+
// A bridged table names its archived columns so later ones take defaults.
|
|
58
|
+
const into=quote(table.name)+(table.added?` (${names})`:'');
|
|
59
|
+
if(!table.rowidAlias)return [db.prepare(`INSERT INTO ${into} SELECT ${projection} ${from}`)];
|
|
60
|
+
if(columns.length<100&&!table.columns.some(c=>c.generated))return [db.prepare(`INSERT INTO ${quote(table.name)} (${names},${quote(table.rowidAlias)}) SELECT ${projection},m.rer_portability_source_rowid ${from}`)];
|
|
61
|
+
// Avoid selecting 101 columns or repeating generated-table base column names.
|
|
62
|
+
// Restore rowids in the same atomic batch using temporary negative ids below
|
|
63
|
+
// both the destination and source ranges.
|
|
64
|
+
const alias=quote(table.rowidAlias),base=`(SELECT MIN(0,MIN(rer_portability_source_rowid))-COUNT(*) FROM ${meta})`;
|
|
65
|
+
return [db.prepare(`INSERT INTO ${into} SELECT ${projection} ${from}`),
|
|
66
|
+
db.prepare(`UPDATE ${quote(table.name)} SET ${alias}=${base}-${alias}`),
|
|
67
|
+
db.prepare(`UPDATE ${quote(table.name)} AS t SET ${alias}=(SELECT rer_portability_source_rowid FROM ${meta} WHERE rer_portability_row_order=${base}-t.${alias}-1)`)];
|
|
68
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { quote, all, insist } from './storage.mjs';
|
|
2
|
+
import { sidecar, copyRowid } from './copies.mjs';
|
|
3
|
+
|
|
4
|
+
// Local dev identity never travels. When a policy includes operator-auth tables,
|
|
5
|
+
// export drops dev sessions and every credential minted while using one, and
|
|
6
|
+
// suspends dev personas (<as>@dev.localhost); import refuses archives holding
|
|
7
|
+
// dev sessions or dev-minted credentials. These rules are not policy options.
|
|
8
|
+
const PERSONAS = "lower(email) LIKE '%@dev.localhost'";
|
|
9
|
+
export async function dropDevData(db, tables, copies) {
|
|
10
|
+
const live = (await all(db, "SELECT name FROM sqlite_master WHERE type='table' AND name='oa_dev_credentials'")).length;
|
|
11
|
+
const minted = copies.oa_dev_credentials ? quote(copies.oa_dev_credentials) : live ? 'oa_dev_credentials' : null;
|
|
12
|
+
// Tokens read the credential record, so the record itself is emptied last.
|
|
13
|
+
const rules = [['oa_sessions', "kind='dev'"], ['oa_credentials', "kind='dev'"], ...(minted ? [['oa_tokens', `id IN (SELECT id FROM ${minted} WHERE credential='oa_tokens')`]] : []), ['oa_dev_credentials', '1']];
|
|
14
|
+
// Only operator-auth's own tables: a product table may share a name prefix.
|
|
15
|
+
const has = (table, ...names) => names.every(name => tables.find(t => t.name === table)?.columns.some(c => c.name === name));
|
|
16
|
+
const shaped = { oa_credentials: has('oa_credentials', 'kind'), oa_sessions: has('oa_sessions', 'kind'), oa_tokens: has('oa_tokens', 'id'), oa_dev_credentials: has('oa_dev_credentials', 'credential', 'id') };
|
|
17
|
+
const statements = [];
|
|
18
|
+
for (const [table, condition] of rules.filter(([table]) => copies[table] && shaped[table])) {
|
|
19
|
+
const copy = quote(copies[table]), entry = tables.find(t => t.name === table);
|
|
20
|
+
statements.push(db.prepare(`DELETE FROM ${quote(sidecar(copies[table]))} WHERE rer_portability_copy_rowid IN (SELECT ${copyRowid(entry)} FROM ${copy} WHERE ${condition})`));
|
|
21
|
+
statements.push(db.prepare(`DELETE FROM ${copy} WHERE ${condition}`));
|
|
22
|
+
}
|
|
23
|
+
if (copies.oa_operators && has('oa_operators', 'email', 'status', 'version')) statements.push(db.prepare(`UPDATE ${quote(copies.oa_operators)} SET status='inactive',version=version+1 WHERE ${PERSONAS} AND status!='inactive'`));
|
|
24
|
+
if (statements.length) await db.batch(statements);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function refuseDevData(db, copies, tables) {
|
|
28
|
+
const has = (table, name) => tables.find(t => t.name === table)?.columns.some(c => c.name === name);
|
|
29
|
+
if(copies.oa_credentials && has('oa_credentials','kind'))insist(!(await all(db, `SELECT 1 FROM ${quote(copies.oa_credentials)} WHERE kind='dev' LIMIT 1`)).length,'dev_data:oa_credentials',409);
|
|
30
|
+
if (copies.oa_sessions && has('oa_sessions', 'kind')) insist(!(await all(db, `SELECT 1 FROM ${quote(copies.oa_sessions)} WHERE kind='dev' LIMIT 1`)).length, 'dev_data:oa_sessions', 409);
|
|
31
|
+
if (copies.oa_dev_credentials) insist(!(await all(db, `SELECT 1 FROM ${quote(copies.oa_dev_credentials)} LIMIT 1`)).length, 'dev_data:oa_dev_credentials', 409);
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readArchive } from './format.mjs';
|
|
2
|
+
import { canonicalJson } from './hash.mjs';
|
|
3
|
+
async function inventory(source) {
|
|
4
|
+
const tables = new Map(); let manifest, closing;
|
|
5
|
+
for await (const record of readArchive(source)) {
|
|
6
|
+
if (record.kind === 'manifest') { manifest = record; for (const table of manifest.tables) tables.set(table.name,new Map()); }
|
|
7
|
+
if (record.kind === 'closing') closing = record;
|
|
8
|
+
if (record.kind === 'part') for (const item of record.items) if (item.kind === 'row') {
|
|
9
|
+
const table = manifest.tables.find(table => table.name === item.table);
|
|
10
|
+
const primary = table.columns.filter(column => column.pk).sort((a,b) => a.pk-b.pk).map(column => column.name);
|
|
11
|
+
const key = primary.length ? canonicalJson(primary.map(name => item.row[name])) : table.rowidAlias ? String(item.rowid) : canonicalJson(item.row);
|
|
12
|
+
const rows = tables.get(item.table); if (rows.has(key)) throw new Error('Archive contains duplicate row keys'); rows.set(key,item.row);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {manifest,closing,tables};
|
|
16
|
+
}
|
|
17
|
+
export async function diffArchives(before,after) {
|
|
18
|
+
const [a,b] = await Promise.all([inventory(before),inventory(after)]);
|
|
19
|
+
if (a.manifest.product !== b.manifest.product) throw new Error('Archives describe different products');
|
|
20
|
+
const tables = [];
|
|
21
|
+
for (const name of [...new Set([...a.tables.keys(),...b.tables.keys()])].sort()) {
|
|
22
|
+
const left = a.tables.get(name) ?? new Map(),right = b.tables.get(name) ?? new Map(),added=[],removed=[],changed=[];
|
|
23
|
+
for (const [key,row] of left) { if (!right.has(key)) removed.push(key); else if(canonicalJson(row)!==canonicalJson(right.get(key))) changed.push(key); }
|
|
24
|
+
for (const key of right.keys()) if(!left.has(key))added.push(key);
|
|
25
|
+
if(added.length||removed.length||changed.length)tables.push({name,added,removed,changed});
|
|
26
|
+
}
|
|
27
|
+
const binary = kind => {
|
|
28
|
+
const key = item => canonicalJson(kind==='objects'?[item.bucket,item.key]:[item.name]);
|
|
29
|
+
const left=new Map(a.manifest[kind].map(item=>[key(item),item])),right=new Map(b.manifest[kind].map(item=>[key(item),item]));
|
|
30
|
+
return {added:[...right.keys()].filter(key=>!left.has(key)),removed:[...left.keys()].filter(key=>!right.has(key)),changed:[...left.keys()].filter(key=>right.has(key)&&canonicalJson(left.get(key))!==canonicalJson(right.get(key)))};
|
|
31
|
+
};
|
|
32
|
+
return {equal:a.closing.contentHash===b.closing.contentHash,product:a.manifest.product,schemaChanged:a.manifest.schema!==b.manifest.schema,tables,objects:binary('objects'),payloads:binary('payloads'),configurationChanged:canonicalJson(a.manifest.requiredConfiguration)!==canonicalJson(b.manifest.requiredConfiguration)};
|
|
33
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { all, statement, state, guard, job, save, partStatements, parts, readPart, cleanCopies, quote, stream, insist, run } from './storage.mjs';
|
|
2
|
+
import { catalogue, encodedRow } from './catalogue.mjs';
|
|
3
|
+
import { createCopy,readCopy,normalizeCopy } from './copies.mjs';
|
|
4
|
+
import { canonicalJson, utf8, base64, StreamHash } from './hash.mjs';
|
|
5
|
+
import { FORMAT, makePart, checkPart, ContentVerifier, ArchiveVerifier } from './format.mjs';
|
|
6
|
+
import { journal } from './journal.mjs';
|
|
7
|
+
import { appliedMigrations } from './bridge.mjs';
|
|
8
|
+
import { capturePayload, payloadRange } from './payloads.mjs';
|
|
9
|
+
import { dropProviderData } from './provider-data.mjs';
|
|
10
|
+
import { dropDevData } from './dev-data.mjs';
|
|
11
|
+
export async function createExport(context,actor='direct') {
|
|
12
|
+
const {db,config,buckets}=context,s=await state(db);insist(!s.applying,'import_applying',409);
|
|
13
|
+
const cat=await catalogue(db,config),id=crypto.randomUUID(),objects=[];
|
|
14
|
+
for(const [binding,spec] of Object.entries(config.buckets).sort()) {
|
|
15
|
+
let cursor;
|
|
16
|
+
do {
|
|
17
|
+
const page=await buckets[binding].list({prefix:spec.prefix??'',cursor,include:['httpMetadata','customMetadata']});
|
|
18
|
+
for(const object of page.objects) objects.push({bucket:binding,key:object.key,size:object.size,etag:object.etag,httpMetadata:object.httpMetadata??{},customMetadata:object.customMetadata??{}});
|
|
19
|
+
cursor=page.truncated?page.cursor:undefined;
|
|
20
|
+
}while(cursor);
|
|
21
|
+
}
|
|
22
|
+
objects.sort((a,b)=>a.bucket<b.bucket?-1:a.bucket>b.bucket?1:a.key<b.key?-1:a.key>b.key?1:0);
|
|
23
|
+
const copies=Object.fromEntries(cat.tables.map((t,i)=>[t.name,'rer_portability_snap_'+id.replaceAll('-','')+'_'+i]));
|
|
24
|
+
const migrations=await appliedMigrations(db);
|
|
25
|
+
const manifest={kind:'manifest',format:FORMAT,version:2,product:config.product,exportedAt:new Date().toISOString(),sourceIdentity:s.source_identity,schema:cat.identity,tables:cat.tables,objects,payloads:[],excludedTables:cat.excludedTables,excludedColumns:config.secretColumns,requiredConfiguration:config.requiredConfiguration,partCount:0,epoch:s.epoch,journal:await journal(db,s.source_identity),...(migrations?{migrations}:{})};
|
|
26
|
+
const row={id,kind:'export',actor,phase:'building',completed:0,metadata:{manifest,copies,cursor:{table:0,row:0,binary:0,offset:0},partBytes:config.partBytes}};
|
|
27
|
+
await db.batch([guard(db,'EXISTS(SELECT 1 FROM rer_portability_state WHERE id=1 AND applying=0 AND source_identity=? AND epoch=?)',s.source_identity,s.epoch),
|
|
28
|
+
...cat.tables.flatMap(t=>createCopy(db,t,copies[t.name],true)),statement(db,'INSERT INTO rer_portability_jobs(id,kind,actor,phase,metadata) VALUES(?,?,?,?,?)',id,'export',actor,row.phase,canonicalJson(row.metadata)),db.prepare('DELETE FROM rer_portability_guard')]);
|
|
29
|
+
try {
|
|
30
|
+
await dropDevData(db,cat.tables,copies);
|
|
31
|
+
await dropProviderData(db,cat.tables,copies);
|
|
32
|
+
for(const t of manifest.tables) t.count=(await statement(db,`SELECT COUNT(*) AS n FROM ${quote(copies[t.name])}`).first()).n;
|
|
33
|
+
for(const [name,producer] of Object.entries(config.extraPayloads).sort()) {
|
|
34
|
+
const descriptor=await capturePayload(db,id,manifest.payloads.length,await producer.produce());manifest.payloads.push({name,...descriptor});
|
|
35
|
+
}
|
|
36
|
+
await run(db,'UPDATE rer_portability_jobs SET metadata=? WHERE id=?',canonicalJson(row.metadata),id);
|
|
37
|
+
await checkReferences(context,row);
|
|
38
|
+
return id;
|
|
39
|
+
}catch(e){await abortExport(context,id);throw e;}
|
|
40
|
+
}
|
|
41
|
+
async function checkReferences({db,config},row) {
|
|
42
|
+
for(const [binding,spec] of Object.entries(config.buckets)) if(spec.objectReferences) {
|
|
43
|
+
const {table,keyColumn}=spec.objectReferences,keys=new Set((await all(db,`SELECT DISTINCT ${quote(keyColumn)} AS key FROM ${quote(row.metadata.copies[table])} WHERE ${quote(keyColumn)} IS NOT NULL`)).map(x=>x.key));
|
|
44
|
+
const objects=row.metadata.manifest.objects.filter(x=>x.bucket===binding);
|
|
45
|
+
insist(objects.length===keys.size && objects.every(x=>keys.has(x.key)),'object_references:'+binding);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function nextItems(context,row) {
|
|
49
|
+
const {db,config,buckets}=context,{manifest,cursor,copies,partBytes}=row.metadata;
|
|
50
|
+
while(cursor.table<manifest.tables.length && cursor.row===manifest.tables[cursor.table].count){cursor.table++;cursor.row=0;}
|
|
51
|
+
if(cursor.table<manifest.tables.length) {
|
|
52
|
+
const table=manifest.tables[cursor.table];
|
|
53
|
+
if(!row.metadata.normalized?.includes(table.name)) {
|
|
54
|
+
await normalizeCopy(context,table,copies[table.name]);
|
|
55
|
+
row.metadata.normalized??=[];row.metadata.normalized.push(table.name);await save(db,row).run();
|
|
56
|
+
}
|
|
57
|
+
const records=await readCopy(db,table,copies[table.name],cursor.row,Math.min(1000,Math.ceil(partBytes/128)),true);
|
|
58
|
+
const items=[];let size=200;
|
|
59
|
+
for(const {row:record,meta} of records) {
|
|
60
|
+
const value=encodedRow(table,record,config.secretColumns[table.name]??[]);
|
|
61
|
+
const item={kind:'row',table:table.name,index:cursor.row,row:value};
|
|
62
|
+
if(table.rowidAlias){insist(Number.isSafeInteger(meta.rer_portability_source_rowid),'unsafe_integer:'+table.name+':rowid');item.rowid=meta.rer_portability_source_rowid;}
|
|
63
|
+
const bytes=utf8(canonicalJson(item)).length+1;
|
|
64
|
+
insist(bytes+200<=partBytes,'row_part_too_large:'+table.name);
|
|
65
|
+
if(size+bytes>partBytes)break;items.push(item);size+=bytes;cursor.row++;
|
|
66
|
+
}
|
|
67
|
+
return items;
|
|
68
|
+
}
|
|
69
|
+
const entries=[...manifest.objects,...manifest.payloads];
|
|
70
|
+
if(cursor.binary===entries.length)return null;
|
|
71
|
+
const entry=entries[cursor.binary],length=Math.min(entry.size-cursor.offset,Math.max(1,Math.floor((partBytes-200-utf8(canonicalJson({bucket:entry.bucket??null,key:entry.key??null,name:entry.name??null})).length)*3/4)));
|
|
72
|
+
let bytes,item;
|
|
73
|
+
if(cursor.binary<manifest.objects.length) {
|
|
74
|
+
const object=await buckets[entry.bucket].get(entry.key,length?{range:{offset:cursor.offset,length}}:undefined);
|
|
75
|
+
insist(object && object.etag===entry.etag,'object_changed:'+entry.bucket+':'+entry.key);
|
|
76
|
+
bytes=new Uint8Array(await object.arrayBuffer());insist(bytes.length===length,'object_changed:'+entry.key);
|
|
77
|
+
item={kind:'object',bucket:entry.bucket,key:entry.key,offset:cursor.offset,data:base64(bytes)};
|
|
78
|
+
}else{
|
|
79
|
+
bytes=await payloadRange(db,row.id,cursor.binary-manifest.objects.length,cursor.offset,length);
|
|
80
|
+
item={kind:'payload',name:entry.name,offset:cursor.offset,data:base64(bytes)};
|
|
81
|
+
}
|
|
82
|
+
const hash=new StreamHash();if(cursor.hash){hash.h=Uint32Array.from(cursor.hash.h);hash.block=Uint8Array.from(cursor.hash.block);hash.size=cursor.hash.size;hash.used=cursor.hash.used;}
|
|
83
|
+
hash.update(bytes);cursor.offset+=length;
|
|
84
|
+
if(cursor.offset===entry.size) {
|
|
85
|
+
const digest=hash.digest();if(entry.sha256)insist(entry.sha256===digest,'payload_changed');entry.sha256=digest;delete entry.etag;
|
|
86
|
+
cursor.binary++;cursor.offset=0;delete cursor.hash;
|
|
87
|
+
}else cursor.hash={h:[...hash.h],block:[...hash.block],size:hash.size,used:hash.used};
|
|
88
|
+
return [item];
|
|
89
|
+
}
|
|
90
|
+
export async function advanceExport(context,id,{expectedPart,maxParts=1}={}) {
|
|
91
|
+
insist(Number.isSafeInteger(maxParts) && maxParts>0 && maxParts<=100,'advance_limit');
|
|
92
|
+
for(let n=0;n<maxParts;n++) {
|
|
93
|
+
const row=await job(context.db,id,'export');
|
|
94
|
+
if(row.phase==='complete')return exportStatus(context,id);
|
|
95
|
+
insist(row.phase==='building','export_phase',409);
|
|
96
|
+
if(expectedPart!==undefined && row.completed!==expectedPart)return exportStatus(context,id);
|
|
97
|
+
const before=row.completed,items=await nextItems(context,row);
|
|
98
|
+
if(items) {
|
|
99
|
+
const part=await makePart(before,items);await checkPart(part,before,row.metadata.partBytes);row.completed++;
|
|
100
|
+
try{await context.db.batch([guard(context.db,'(SELECT completed FROM rer_portability_jobs WHERE id=?)=?',id,before),...partStatements(context.db,id,part),save(context.db,row),context.db.prepare('DELETE FROM rer_portability_guard')]);}
|
|
101
|
+
catch(e){if((await job(context.db,id)).completed===before)throw e;}
|
|
102
|
+
if(expectedPart!==undefined)expectedPart++;
|
|
103
|
+
}else{
|
|
104
|
+
// References are checked again against the archived, normalized snapshot.
|
|
105
|
+
await checkReferences(context,row);
|
|
106
|
+
row.metadata.manifest.partCount=row.completed;
|
|
107
|
+
const verifier=new ContentVerifier(row.metadata.manifest),archive=new ArchiveVerifier(row.metadata.manifest);
|
|
108
|
+
for await(const part of parts(context.db,id,row.completed)){archive.add(part);for(const item of part.items)verifier.add(item);}
|
|
109
|
+
row.metadata.contentHash=verifier.digest();row.metadata.archiveHash=archive.digest();row.phase='complete';await save(context.db,row).run();
|
|
110
|
+
// Finished snapshots are kept until abort so erasure can invalidate them.
|
|
111
|
+
return exportStatus(context,id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return exportStatus(context,id);
|
|
115
|
+
}
|
|
116
|
+
export async function exportStatus(context,id) {
|
|
117
|
+
const row=await job(context.db,id,'export');return {id,kind:'export',phase:row.phase,completed:row.completed,total:row.phase==='complete'?row.completed:null,contentHash:row.metadata.contentHash??null};
|
|
118
|
+
}
|
|
119
|
+
export async function downloadExport(context,id) {
|
|
120
|
+
const row=await job(context.db,id,'export');insist(row.phase==='complete','export_incomplete',409);
|
|
121
|
+
return stream((async function*(){
|
|
122
|
+
yield utf8(canonicalJson(row.metadata.manifest)+'\n');
|
|
123
|
+
for(let i=0;i<row.completed;i++)yield utf8(canonicalJson(await readPart(context.db,id,i))+'\n');
|
|
124
|
+
yield utf8(canonicalJson({kind:'closing',partCount:row.completed,contentHash:row.metadata.contentHash,archiveHash:row.metadata.archiveHash})+'\n');
|
|
125
|
+
})());
|
|
126
|
+
}
|
|
127
|
+
export async function abortExport(context,id) {
|
|
128
|
+
const row=await job(context.db,id,'export');await cleanCopies(context.db,row);row.phase='aborted';await save(context.db,row).run();return exportStatus(context,id);
|
|
129
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { open, mkdir, link, unlink } from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { readArchive } from './index.mjs';
|
|
7
|
+
export async function* fileRecords(path,reader=readArchive) {
|
|
8
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
9
|
+
try { yield* reader(Readable.toWeb(file.createReadStream({ autoClose: false }))); }
|
|
10
|
+
finally { await file.close(); }
|
|
11
|
+
}
|
|
12
|
+
function receipt(manifest, closing, transient) {
|
|
13
|
+
return {
|
|
14
|
+
contentHash: closing.contentHash,
|
|
15
|
+
archiveHash: closing.archiveHash,
|
|
16
|
+
tables: manifest.tables.map(({ name, count }) => ({ name, count })),
|
|
17
|
+
objects: manifest.objects.length, payloads: manifest.payloads.length,
|
|
18
|
+
exclusions: { tables: manifest.excludedTables, columns: manifest.excludedColumns, configuration: true, transient },
|
|
19
|
+
requiredConfiguration: manifest.requiredConfiguration,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export async function exportArchive(service, path, transient) {
|
|
23
|
+
let id, temporary, file;
|
|
24
|
+
try {
|
|
25
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
26
|
+
temporary = path + '.' + randomUUID() + '.partial';
|
|
27
|
+
file = await open(temporary, 'wx', 0o600);
|
|
28
|
+
id = await service.exports.create();
|
|
29
|
+
while ((await service.exports.advance(id, { maxParts: 16 })).phase === 'building') {}
|
|
30
|
+
const reader = (await service.exports.download(id)).getReader();
|
|
31
|
+
try {
|
|
32
|
+
for (;;) {
|
|
33
|
+
const { done, value } = await reader.read(); if (done) break;
|
|
34
|
+
let offset = 0;
|
|
35
|
+
while (offset < value.length) offset += (await file.write(value, offset)).bytesWritten;
|
|
36
|
+
}
|
|
37
|
+
} finally { await reader.cancel(); reader.releaseLock(); }
|
|
38
|
+
await file.sync(); await file.close(); file = undefined;
|
|
39
|
+
let manifest, closing;
|
|
40
|
+
for await (const record of fileRecords(temporary)) { if (record.kind === 'manifest') manifest = record; if (record.kind === 'closing') closing = record; }
|
|
41
|
+
await link(temporary, path);
|
|
42
|
+
return receipt(manifest, closing, transient);
|
|
43
|
+
} finally {
|
|
44
|
+
await file?.close();
|
|
45
|
+
if (temporary) await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; });
|
|
46
|
+
if (id) await service.exports.abort(id);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function importArchive(service, path, transient, journal) {
|
|
50
|
+
let id, manifest, closing, complete = false;
|
|
51
|
+
try {
|
|
52
|
+
// Validate both complete archives before creating any restore state.
|
|
53
|
+
let later;
|
|
54
|
+
for (const file of [path, journal].filter(Boolean)) for await (const record of fileRecords(file)) {
|
|
55
|
+
if (file === journal && record.kind === 'manifest') later = { sourceIdentity: record.sourceIdentity, epoch: record.epoch, journal: record.journal };
|
|
56
|
+
}
|
|
57
|
+
for await (const record of fileRecords(path)) {
|
|
58
|
+
if (record.kind === 'manifest') { manifest = record; id = await service.imports.create(manifest); }
|
|
59
|
+
else if (record.kind === 'part') await service.imports.uploadPart(id, record.index, record);
|
|
60
|
+
else closing = record;
|
|
61
|
+
}
|
|
62
|
+
const status = await service.imports.finalize(id, { contentHash: closing.contentHash, archiveHash: closing.archiveHash, ...(later ? { laterJournal: later } : {}) }); complete = true;
|
|
63
|
+
return { ...receipt(manifest, closing, transient), ...(status.bridged ? { bridged: status.bridged } : {}) };
|
|
64
|
+
} finally { if (id && !complete) await service.imports.abort(id); }
|
|
65
|
+
}
|