@runeditrun/rer 0.0.0-stage → 0.1.0
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 +154 -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 +177 -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 +155 -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 +129 -0
- package/global/launch/facts.mjs +93 -0
- package/global/launch/failure.mjs +5 -0
- package/global/launch/local-defaults.mjs +4 -0
- package/global/launch/prepare.mjs +61 -0
- package/global/launch/render.mjs +44 -0
- package/global/launch/state.mjs +97 -0
- package/global/launch/steps.mjs +81 -0
- package/global/launch/worker.mjs +43 -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,120 @@
|
|
|
1
|
+
"""Recognize Python literal openers by syntax, without evaluating source."""
|
|
2
|
+
import ast
|
|
3
|
+
import io
|
|
4
|
+
import re
|
|
5
|
+
import textwrap
|
|
6
|
+
import tokenize
|
|
7
|
+
import warnings
|
|
8
|
+
from command_shell_quotes import output_quotes
|
|
9
|
+
|
|
10
|
+
PREFIX = re.compile(r'''(?i)(?:br|rb|fr|rf|[bruf])(?=["'])''')
|
|
11
|
+
PREFIX_BEFORE = re.compile(r'(?i)(?<!\w)(?:br|rb|fr|rf|[bruf])$')
|
|
12
|
+
STRING_TOKENS = {tokenize.STRING, getattr(tokenize, 'FSTRING_START', tokenize.STRING)}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parsed_quotes(source):
|
|
16
|
+
"""Return lexical quote offsets only when their containing source parses.
|
|
17
|
+
|
|
18
|
+
Tokenization alone accepts a possessive as a string token even in invalid
|
|
19
|
+
prose. Parsing alone accepts comments that contain possessives. Both are
|
|
20
|
+
needed before an apostrophe may truncate a later shell command.
|
|
21
|
+
"""
|
|
22
|
+
code = textwrap.dedent(source)
|
|
23
|
+
try:
|
|
24
|
+
# Inspected text must never be emitted through parser warnings.
|
|
25
|
+
with warnings.catch_warnings():
|
|
26
|
+
warnings.simplefilter('ignore')
|
|
27
|
+
ast.parse(code)
|
|
28
|
+
tokens = list(tokenize.generate_tokens(io.StringIO(code).readline))
|
|
29
|
+
except (SyntaxError, ValueError, tokenize.TokenError, RecursionError):
|
|
30
|
+
return None
|
|
31
|
+
original_lines = source.splitlines(keepends=True)
|
|
32
|
+
code_lines = code.splitlines(keepends=True)
|
|
33
|
+
offsets, offset = [], 0
|
|
34
|
+
for line in original_lines:
|
|
35
|
+
offsets.append(offset)
|
|
36
|
+
offset += len(line)
|
|
37
|
+
quotes = set()
|
|
38
|
+
for token in tokens:
|
|
39
|
+
prefix = PREFIX.match(token.string) if token.type in STRING_TOKENS else None
|
|
40
|
+
if prefix:
|
|
41
|
+
row, column = token.start
|
|
42
|
+
removed = len(original_lines[row - 1]) - len(code_lines[row - 1])
|
|
43
|
+
quotes.add(offsets[row - 1] + removed + column + prefix.end())
|
|
44
|
+
return quotes
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def literal_quotes(source):
|
|
48
|
+
quotes = parsed_quotes(source)
|
|
49
|
+
complete = quotes is not None
|
|
50
|
+
quotes = quotes if complete else set()
|
|
51
|
+
quotes.update(output_quotes(source))
|
|
52
|
+
# A complete statement still has its own grammar when neighboring prose
|
|
53
|
+
# or a later statement is malformed. Split at Python token boundaries,
|
|
54
|
+
# never at punctuation inside a string, comment or open expression.
|
|
55
|
+
offsets = [0]
|
|
56
|
+
for line in source.splitlines(keepends=True):
|
|
57
|
+
offsets.append(offsets[-1] + len(line))
|
|
58
|
+
start, depth = 0, 0
|
|
59
|
+
try:
|
|
60
|
+
for token in tokenize.generate_tokens(io.StringIO(source).readline):
|
|
61
|
+
if token.type == tokenize.COMMENT:
|
|
62
|
+
# Comments may document source code. Validate the entire
|
|
63
|
+
# explicitly marked body, so prose possessives remain prose.
|
|
64
|
+
body_quotes = parsed_quotes(token.string[1:])
|
|
65
|
+
body_quotes = (body_quotes or set()) | output_quotes(token.string[1:])
|
|
66
|
+
if body_quotes:
|
|
67
|
+
row, column = token.start
|
|
68
|
+
quotes.update(offsets[row - 1] + column + 1 + index for index in body_quotes)
|
|
69
|
+
if token.type == tokenize.OP:
|
|
70
|
+
if token.string in '([{':
|
|
71
|
+
depth += 1
|
|
72
|
+
elif token.string in ')]}':
|
|
73
|
+
depth -= 1
|
|
74
|
+
if not complete and (token.type == tokenize.NEWLINE or (token.string == ';' and depth == 0)):
|
|
75
|
+
row, column = token.end
|
|
76
|
+
end = min(len(source), offsets[row - 1] + column)
|
|
77
|
+
statement_quotes = parsed_quotes(source[start:end])
|
|
78
|
+
if statement_quotes is not None:
|
|
79
|
+
quotes.update(start + index for index in statement_quotes)
|
|
80
|
+
start = end
|
|
81
|
+
except (SyntaxError, ValueError, tokenize.TokenError, RecursionError):
|
|
82
|
+
pass
|
|
83
|
+
for comment in re.finditer(r'(?m)^[ \t]*(?://|/\*)([^\n]*)', source):
|
|
84
|
+
body = comment[1]
|
|
85
|
+
if comment[0].lstrip().startswith('/*'):
|
|
86
|
+
close = body.find('*/')
|
|
87
|
+
if close < 0:
|
|
88
|
+
continue
|
|
89
|
+
body = body[:close]
|
|
90
|
+
body_quotes = parsed_quotes(body)
|
|
91
|
+
body_quotes = (body_quotes or set()) | output_quotes(body)
|
|
92
|
+
if body_quotes:
|
|
93
|
+
quotes.update(comment.start(1) + index for index in body_quotes)
|
|
94
|
+
return quotes
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SourceQuotes:
|
|
98
|
+
def __init__(self, text):
|
|
99
|
+
self.text = text
|
|
100
|
+
self.regions = {}
|
|
101
|
+
|
|
102
|
+
def region(self, start, end):
|
|
103
|
+
key = (start, end)
|
|
104
|
+
if key not in self.regions:
|
|
105
|
+
self.regions[key] = literal_quotes(self.text[start:end])
|
|
106
|
+
return self.regions[key]
|
|
107
|
+
|
|
108
|
+
def contains(self, index, start=0, end=None):
|
|
109
|
+
if not PREFIX_BEFORE.search(self.text, max(0, index - 2), index):
|
|
110
|
+
return False
|
|
111
|
+
end = len(self.text) if end is None else end
|
|
112
|
+
quotes = self.region(start, end)
|
|
113
|
+
if index - start in quotes:
|
|
114
|
+
return True
|
|
115
|
+
# An earlier malformed token can prevent document tokenization from
|
|
116
|
+
# reaching a later source line. Only retry the whole physical line.
|
|
117
|
+
start = max(start, self.text.rfind('\n', start, index) + 1)
|
|
118
|
+
line_end = self.text.find('\n', index, end)
|
|
119
|
+
end = end if line_end < 0 else line_end
|
|
120
|
+
return index - start in self.region(start, end)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Classify module Git pins and prove Source keys at specific JSON locations.
|
|
2
|
+
|
|
3
|
+
This is classification, not Source authoring validation. A stale lock still
|
|
4
|
+
fails the authoring check. Missing original preimages remain ordinary findings.
|
|
5
|
+
"""
|
|
6
|
+
import base64
|
|
7
|
+
import hashlib
|
|
8
|
+
import re
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import subprocess
|
|
12
|
+
from json_identity import Document, HEX
|
|
13
|
+
from provenance_pins import module_pins
|
|
14
|
+
from copy_identities import copy_pointers
|
|
15
|
+
|
|
16
|
+
ARCHIVE = re.compile(r'\.edition/sources/([a-f0-9]{64})\.json\Z')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def candidate(path):
|
|
20
|
+
return path in ('edition.json', 'sources.lock.json', 'base-copies.json', 'base/PROVENANCE.json') or bool(ARCHIVE.fullmatch(path))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def safe_path(value):
|
|
24
|
+
return (isinstance(value, str) and value and not re.search(r'[\\\x00-\x1f\x7f]', value)
|
|
25
|
+
and all(part not in ('', '.', '..', '.git') for part in value.split('/')))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def valid_files(files):
|
|
29
|
+
if not isinstance(files, dict) or not files:
|
|
30
|
+
return False
|
|
31
|
+
for name, file in files.items():
|
|
32
|
+
if (not safe_path(name) or not isinstance(file, dict) or not isinstance(file.get('data'), str)
|
|
33
|
+
or type(file.get('executable')) is not bool):
|
|
34
|
+
return False
|
|
35
|
+
try:
|
|
36
|
+
if base64.b64encode(base64.b64decode(file['data'], validate=True)).decode() != file['data']:
|
|
37
|
+
return False
|
|
38
|
+
except ValueError:
|
|
39
|
+
return False
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def same_json(left, right):
|
|
44
|
+
# Python equates booleans and numbers; Source JSON identities do not.
|
|
45
|
+
return json.dumps(left, sort_keys=True) == json.dumps(right, sort_keys=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ContentIdentities:
|
|
49
|
+
def __init__(self, files, read_regular=None):
|
|
50
|
+
self.documents, self.allowed, self.archives, self.hashes = {}, {}, {}, {}
|
|
51
|
+
for path, data in files.items():
|
|
52
|
+
if candidate(path):
|
|
53
|
+
try:
|
|
54
|
+
self.documents[path] = Document(data)
|
|
55
|
+
except (ValueError, TypeError, IndexError, RecursionError, UnicodeError):
|
|
56
|
+
pass
|
|
57
|
+
if not self.documents:
|
|
58
|
+
return
|
|
59
|
+
for path, document in self.documents.items():
|
|
60
|
+
for pointer in module_pins(path, document.value):
|
|
61
|
+
self.allow(path, pointer)
|
|
62
|
+
if path in ('base-copies.json', 'base/PROVENANCE.json') and read_regular is not None:
|
|
63
|
+
for pointer in copy_pointers(document.value, read_regular):
|
|
64
|
+
self.allow(path, pointer)
|
|
65
|
+
recipe = self.document('edition.json')
|
|
66
|
+
origins = [self.origin(recipe.get('parent'), True)]
|
|
67
|
+
components = recipe.get('components', [])
|
|
68
|
+
if isinstance(components, list):
|
|
69
|
+
for component in components:
|
|
70
|
+
source = component.get('source') if isinstance(component, dict) else None
|
|
71
|
+
if isinstance(source, dict):
|
|
72
|
+
origins.append(self.origin(source if source.get('kind') == 'git' else source.get('origin')))
|
|
73
|
+
values = [recipe, *(v for v in origins if v is not None)]
|
|
74
|
+
for path, document in self.documents.items():
|
|
75
|
+
saved = document.value
|
|
76
|
+
if ARCHIVE.fullmatch(path) and isinstance(saved, dict):
|
|
77
|
+
values.extend([saved, saved.get('origin'), saved.get('files')])
|
|
78
|
+
self.hashes = {}
|
|
79
|
+
if values:
|
|
80
|
+
try:
|
|
81
|
+
result = subprocess.run(['node', str(Path(__file__).with_name('source_hashes.mjs'))],
|
|
82
|
+
input=json.dumps(values).encode(), capture_output=True, timeout=60, check=True)
|
|
83
|
+
hashes = json.loads(result.stdout)
|
|
84
|
+
if len(hashes) != len(values) or not all(isinstance(h, str) and HEX.fullmatch(h) for h in hashes):
|
|
85
|
+
return
|
|
86
|
+
self.hashes = {json.dumps(v): h for v, h in zip(values, hashes)}
|
|
87
|
+
except (OSError, ValueError, subprocess.SubprocessError):
|
|
88
|
+
return
|
|
89
|
+
# Authoring pins hash(json(saved)); a raw-byte pin alone cannot prove a
|
|
90
|
+
# differently encoded retained archive, even when its fields agree.
|
|
91
|
+
for path, document in self.documents.items():
|
|
92
|
+
saved, match = document.value, ARCHIVE.fullmatch(path)
|
|
93
|
+
if (match and isinstance(saved, dict) and saved.get('key') == match[1]
|
|
94
|
+
and isinstance(saved.get('origin'), dict) and saved['origin']
|
|
95
|
+
and valid_files(saved.get('files'))
|
|
96
|
+
and isinstance(saved.get('dependencies', []), list)
|
|
97
|
+
and saved['key'] == self.digest(saved['origin'])
|
|
98
|
+
and saved.get('digest') == self.digest(saved['files'])
|
|
99
|
+
and hashlib.sha256(document.data).hexdigest() == self.digest(saved)):
|
|
100
|
+
self.archives[saved['key']] = (saved, hashlib.sha256(document.data).hexdigest())
|
|
101
|
+
self.references()
|
|
102
|
+
|
|
103
|
+
def digest(self, value):
|
|
104
|
+
return self.hashes.get(json.dumps(value))
|
|
105
|
+
|
|
106
|
+
def allow(self, path, pointer):
|
|
107
|
+
self.allowed.setdefault(path, set()).add(pointer)
|
|
108
|
+
|
|
109
|
+
def document(self, path):
|
|
110
|
+
doc = self.documents.get(path)
|
|
111
|
+
return doc.value if doc and isinstance(doc.value, dict) else {}
|
|
112
|
+
|
|
113
|
+
def pin(self, pin, key):
|
|
114
|
+
if not isinstance(pin, dict) or not isinstance(key, str) or key not in self.archives:
|
|
115
|
+
return False
|
|
116
|
+
archive, raw_sha = self.archives[key]
|
|
117
|
+
return (pin.get('key') == key and pin.get('digest') == archive['digest']
|
|
118
|
+
and pin.get('sha256') == raw_sha)
|
|
119
|
+
|
|
120
|
+
def reference(self, ref, parent=False):
|
|
121
|
+
if not isinstance(ref, dict):
|
|
122
|
+
return None
|
|
123
|
+
if not self.valid_selection(ref):
|
|
124
|
+
return None
|
|
125
|
+
selection = dict(path=ref.get('path') or '.', paths=ref.get('paths') or None,
|
|
126
|
+
excludeSnapshots=True if parent else bool(ref.get('excludeSnapshots')))
|
|
127
|
+
projection = ref.get('projection')
|
|
128
|
+
if isinstance(projection, dict) and set(projection) == {'key', 'sha256'}:
|
|
129
|
+
key = projection['key']
|
|
130
|
+
if not isinstance(key, str) or key not in self.archives:
|
|
131
|
+
return None
|
|
132
|
+
saved, raw_sha = self.archives[key]
|
|
133
|
+
if (projection['sha256'] == raw_sha and same_json(saved['origin'].get('selection'), selection)):
|
|
134
|
+
return key
|
|
135
|
+
return None
|
|
136
|
+
origin = self.origin(ref, parent)
|
|
137
|
+
key = self.digest(origin)
|
|
138
|
+
if key in self.archives and same_json(self.archives[key][0]['origin'], origin):
|
|
139
|
+
return key
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def valid_selection(ref):
|
|
144
|
+
return (ref.get('path', '.') == '.' or safe_path(ref.get('path'))) and (
|
|
145
|
+
ref.get('paths') is None or isinstance(ref['paths'], list) and bool(ref['paths'])
|
|
146
|
+
and all(safe_path(p) for p in ref['paths'])) and (
|
|
147
|
+
'excludeSnapshots' not in ref or type(ref['excludeSnapshots']) is bool)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def origin(ref, parent=False):
|
|
151
|
+
if not isinstance(ref, dict) or not ContentIdentities.valid_selection(ref):
|
|
152
|
+
return None
|
|
153
|
+
projection = ref.get('projection')
|
|
154
|
+
if (not isinstance(ref.get('repository'), str) or not ref['repository']
|
|
155
|
+
or ref['repository'].startswith('-')):
|
|
156
|
+
return None
|
|
157
|
+
if 'projection' in ref:
|
|
158
|
+
if ('revision' in ref or not isinstance(projection, dict) or set(projection) != {'schemaVersion', 'originalSha256', 'recipeSha256'}
|
|
159
|
+
or type(projection['schemaVersion']) is not int or projection['schemaVersion'] != 1
|
|
160
|
+
or not all(isinstance(projection[k], str) and HEX.fullmatch(projection[k])
|
|
161
|
+
for k in ('originalSha256', 'recipeSha256'))):
|
|
162
|
+
return None
|
|
163
|
+
identity = dict(projection=projection)
|
|
164
|
+
elif isinstance(ref.get('revision'), str) and re.fullmatch(r'[a-f0-9]{40}', ref['revision']):
|
|
165
|
+
identity = dict(revision=ref['revision'])
|
|
166
|
+
else:
|
|
167
|
+
return None
|
|
168
|
+
return dict(repository=ref['repository'], **identity, path=ref.get('path') or '.',
|
|
169
|
+
paths=ref.get('paths') or None,
|
|
170
|
+
excludeSnapshots=True if parent else bool(ref.get('excludeSnapshots')))
|
|
171
|
+
|
|
172
|
+
def dependency_locations(self, key):
|
|
173
|
+
# Stage the entire selected closure before granting any of its locations.
|
|
174
|
+
# An unresolved later edge must not leave the root or earlier edges allowed.
|
|
175
|
+
pending, visited, locations = [key], set(), []
|
|
176
|
+
while pending:
|
|
177
|
+
key = pending.pop()
|
|
178
|
+
if key in visited:
|
|
179
|
+
continue
|
|
180
|
+
visited.add(key)
|
|
181
|
+
saved, _ = self.archives[key]
|
|
182
|
+
path = '.edition/sources/' + key + '.json'
|
|
183
|
+
locations.append((path, ('key',)))
|
|
184
|
+
for i, pin in enumerate(saved.get('dependencies', [])):
|
|
185
|
+
if not isinstance(pin, dict) or not self.pin(pin, pin.get('key')):
|
|
186
|
+
return None
|
|
187
|
+
locations.append((path, ('dependencies', i, 'key')))
|
|
188
|
+
pending.append(pin['key'])
|
|
189
|
+
return locations
|
|
190
|
+
|
|
191
|
+
def references(self):
|
|
192
|
+
recipe = self.document('edition.json')
|
|
193
|
+
lock = self.document('sources.lock.json')
|
|
194
|
+
if type(recipe.get('schemaVersion')) is not int or recipe['schemaVersion'] != 1 or not isinstance(recipe.get('components'), list):
|
|
195
|
+
return
|
|
196
|
+
components = lock.get('components')
|
|
197
|
+
locked = (type(lock.get('schemaVersion')) is int and lock['schemaVersion'] == 1
|
|
198
|
+
and isinstance(recipe.get('id'), str) and lock.get('edition') == recipe['id']
|
|
199
|
+
and lock.get('manifest') == self.digest(recipe) and isinstance(components, list))
|
|
200
|
+
if not locked:
|
|
201
|
+
return
|
|
202
|
+
def identifiers(entries):
|
|
203
|
+
ids = [entry.get('id') for entry in entries if isinstance(entry, dict)]
|
|
204
|
+
return (len(ids) == len(entries) and all(isinstance(i, str) and i for i in ids)
|
|
205
|
+
and len(set(ids)) == len(ids))
|
|
206
|
+
if not identifiers(recipe['components']) or not identifiers(components):
|
|
207
|
+
return
|
|
208
|
+
by_id = {c['id']: c for c in components}
|
|
209
|
+
if (len(components) != len(recipe['components']) or any(
|
|
210
|
+
c['id'] not in by_id or not same_json(c.get('source'), by_id[c['id']].get('source'))
|
|
211
|
+
for c in recipe['components'])):
|
|
212
|
+
return
|
|
213
|
+
selected = []
|
|
214
|
+
key = self.reference(recipe.get('parent'), parent=True)
|
|
215
|
+
if key and locked and self.pin(lock.get('parent'), key):
|
|
216
|
+
selected.append((key, [
|
|
217
|
+
('edition.json', ('parent', 'projection', 'key')),
|
|
218
|
+
('sources.lock.json', ('parent', 'key')),
|
|
219
|
+
]))
|
|
220
|
+
for index, component in enumerate(recipe['components']):
|
|
221
|
+
if not isinstance(component, dict):
|
|
222
|
+
continue
|
|
223
|
+
source = component.get('source')
|
|
224
|
+
if not isinstance(source, dict):
|
|
225
|
+
continue
|
|
226
|
+
suffix = () if source.get('kind') == 'git' else ('origin',) if source.get('kind') == 'local' else None
|
|
227
|
+
if suffix is None:
|
|
228
|
+
continue
|
|
229
|
+
key = self.reference(source if not suffix else source.get('origin'))
|
|
230
|
+
if not key:
|
|
231
|
+
continue
|
|
232
|
+
for i, entry in enumerate(components):
|
|
233
|
+
if (isinstance(entry, dict) and entry.get('id') == component.get('id')
|
|
234
|
+
and same_json(entry.get('source'), source) and self.pin(entry.get('baseline'), key)):
|
|
235
|
+
selected.append((key, [
|
|
236
|
+
('edition.json', ('components', index, 'source', *suffix, 'projection', 'key')),
|
|
237
|
+
('sources.lock.json', ('components', i, 'baseline', 'key')),
|
|
238
|
+
('sources.lock.json', ('components', i, 'source', *suffix, 'projection', 'key')),
|
|
239
|
+
]))
|
|
240
|
+
# Validate each selection independently: unselected archives and unrelated
|
|
241
|
+
# coherent selections cannot invalidate this graph's proof.
|
|
242
|
+
for key, locations in selected:
|
|
243
|
+
dependencies = self.dependency_locations(key)
|
|
244
|
+
if dependencies is not None:
|
|
245
|
+
for path, pointer in locations + dependencies:
|
|
246
|
+
self.allow(path, pointer)
|
|
247
|
+
|
|
248
|
+
def matches(self, path, hit):
|
|
249
|
+
document = self.documents.get(path)
|
|
250
|
+
return bool(document and document.matches(hit, self.allowed.get(path, ())))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Prove copy-inventory digests against regular files in the scanned tree."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import re
|
|
4
|
+
from json_identity import HEX, GIT_REVISION
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def relative_path(value):
|
|
8
|
+
return (isinstance(value, str) and value and not value.startswith('/')
|
|
9
|
+
and not re.search(r'[\\\x00-\x1f\x7f]', value)
|
|
10
|
+
and all(part not in ('', '.', '..', '.git') for part in value.split('/')))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def copy_pointers(document, read_regular):
|
|
14
|
+
if (not isinstance(document, dict) or type(document.get('schemaVersion')) is not int
|
|
15
|
+
or document['schemaVersion'] != 1 or not isinstance(document.get('copies'), list)):
|
|
16
|
+
return set()
|
|
17
|
+
allowed = set()
|
|
18
|
+
for index, copy in enumerate(document['copies']):
|
|
19
|
+
if (not isinstance(copy, dict) or not relative_path(copy.get('path'))
|
|
20
|
+
or not relative_path(copy.get('originPath'))
|
|
21
|
+
or not isinstance(copy.get('revision'), str)
|
|
22
|
+
or not GIT_REVISION.fullmatch(copy['revision'])):
|
|
23
|
+
continue
|
|
24
|
+
files, modes = copy.get('files'), copy.get('modes')
|
|
25
|
+
if (not isinstance(files, dict) or not files or not isinstance(modes, dict)
|
|
26
|
+
or set(files) != set(modes)):
|
|
27
|
+
continue
|
|
28
|
+
pending = set()
|
|
29
|
+
for name, digest in files.items():
|
|
30
|
+
if (not relative_path(name) or not (name == copy['path'] or name.startswith(copy['path'] + '/'))
|
|
31
|
+
or not isinstance(digest, str) or not HEX.fullmatch(digest)
|
|
32
|
+
or modes[name] not in ('100644', '100755')):
|
|
33
|
+
break
|
|
34
|
+
value = read_regular(name)
|
|
35
|
+
if (value is None or value[1] != modes[name]
|
|
36
|
+
or hashlib.sha256(value[0]).hexdigest() != digest):
|
|
37
|
+
break
|
|
38
|
+
pending.add(('copies', index, 'files', name))
|
|
39
|
+
else:
|
|
40
|
+
allowed.update(pending)
|
|
41
|
+
return allowed
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Strict, bounded DER reading for X.501 Names and SubjectPublicKeyInfo."""
|
|
2
|
+
SET, SEQUENCE, OID, BIT_STRING = 0x31, 0x30, 0x06, 0x03
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def tlv(data, offset, end):
|
|
6
|
+
"""(tag, content start, content end) of one minimally encoded definite TLV."""
|
|
7
|
+
if offset + 2 > end or data[offset] & 0x1f == 0x1f:
|
|
8
|
+
return None
|
|
9
|
+
start, length = offset + 2, data[offset + 1]
|
|
10
|
+
if length & 0x80:
|
|
11
|
+
count = length & 0x7f
|
|
12
|
+
if not 1 <= count <= 3 or start + count > end or data[start] == 0:
|
|
13
|
+
return None
|
|
14
|
+
length = int.from_bytes(data[start:start + count], 'big')
|
|
15
|
+
start += count
|
|
16
|
+
if length < 0x80:
|
|
17
|
+
return None
|
|
18
|
+
return (data[offset], start, start + length) if start + length <= end else None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def rdn_values(data, offset, end):
|
|
22
|
+
"""End and attribute value spans of one RelativeDistinguishedName SET."""
|
|
23
|
+
rdn = tlv(data, offset, end)
|
|
24
|
+
if not rdn or rdn[0] != SET or rdn[1] == rdn[2]:
|
|
25
|
+
return None
|
|
26
|
+
values, cursor = [], rdn[1]
|
|
27
|
+
while cursor < rdn[2]:
|
|
28
|
+
attribute = tlv(data, cursor, rdn[2])
|
|
29
|
+
if not attribute or attribute[0] != SEQUENCE:
|
|
30
|
+
return None
|
|
31
|
+
oid = tlv(data, attribute[1], attribute[2])
|
|
32
|
+
value = oid and oid[0] == OID and oid[1] < oid[2] and tlv(data, oid[2], attribute[2])
|
|
33
|
+
if not value or value[2] != attribute[2]:
|
|
34
|
+
return None
|
|
35
|
+
values.append((value[1], value[2]))
|
|
36
|
+
cursor = attribute[2]
|
|
37
|
+
return rdn[2], values
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def names(data):
|
|
41
|
+
"""Every (start, end, value spans) RDNSequence content run, by each prefix."""
|
|
42
|
+
offset = data.find(SET)
|
|
43
|
+
while offset >= 0:
|
|
44
|
+
cursor, values = offset, []
|
|
45
|
+
while (rdn := rdn_values(data, cursor, len(data))):
|
|
46
|
+
cursor = rdn[0]
|
|
47
|
+
values += rdn[1]
|
|
48
|
+
yield offset, cursor, tuple(values)
|
|
49
|
+
offset = data.find(SET, offset + 1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def public_keys(data):
|
|
53
|
+
"""Every (start, end) AlgorithmIdentifier + BIT STRING content run."""
|
|
54
|
+
offset = data.find(SEQUENCE)
|
|
55
|
+
while offset >= 0:
|
|
56
|
+
algorithm = tlv(data, offset, len(data))
|
|
57
|
+
if algorithm and algorithm[0] == SEQUENCE:
|
|
58
|
+
key = tlv(data, algorithm[2], len(data))
|
|
59
|
+
if key and key[0] == BIT_STRING:
|
|
60
|
+
yield offset, key[2]
|
|
61
|
+
offset = data.find(SEQUENCE, offset + 1)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Bind one pending public destination to explicit context and committed identities."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from github_refs import FULL
|
|
6
|
+
from project_identity import FIELDS, ordinary_document, tree_identity
|
|
7
|
+
|
|
8
|
+
RULE = 'pending-public-destination'
|
|
9
|
+
INVALID = 'destination.invalid'
|
|
10
|
+
MANIFESTS = ('edition.json', 'rer-project.json')
|
|
11
|
+
URL = re.compile(r'https://github\.com/([^/]+/[^/]+)', re.IGNORECASE | re.ASCII)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def identity(value, url=False):
|
|
15
|
+
if not isinstance(value, str):
|
|
16
|
+
raise ValueError('invalid destination identity')
|
|
17
|
+
if url:
|
|
18
|
+
match = URL.fullmatch(value)
|
|
19
|
+
if not match:
|
|
20
|
+
raise ValueError('invalid destination URL')
|
|
21
|
+
value = match[1]
|
|
22
|
+
if value.lower().endswith('.git'):
|
|
23
|
+
value = value[:-4]
|
|
24
|
+
if not FULL.fullmatch(value) or any(part.endswith('.') for part in value.split('/')):
|
|
25
|
+
raise ValueError('invalid destination identity')
|
|
26
|
+
return value.lower()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def unique_object(pairs):
|
|
30
|
+
result = {}
|
|
31
|
+
for key, value in pairs:
|
|
32
|
+
if key in result:
|
|
33
|
+
raise ValueError('duplicate manifest key')
|
|
34
|
+
result[key] = value
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def reject_constant(value):
|
|
39
|
+
raise ValueError('destination manifest contains a nonstandard JSON constant')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def repository_identity(value):
|
|
43
|
+
# Bare declarations are supported only by the explicit logical-project path.
|
|
44
|
+
# A .git suffix has transport meaning in a URL, not in a bare identity.
|
|
45
|
+
if isinstance(value, str) and URL.fullmatch(value):
|
|
46
|
+
return identity(value, url=True)
|
|
47
|
+
if isinstance(value, str) and value.lower().endswith('.git'):
|
|
48
|
+
raise ValueError('invalid bare destination identity')
|
|
49
|
+
return identity(value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def destination(history, project=None, repository=None):
|
|
53
|
+
# Metadata alone never enables the pending-destination classification.
|
|
54
|
+
if project is None and repository is None:
|
|
55
|
+
return None
|
|
56
|
+
# Logical projects use the recipe's canonical ID, not GitHub slug rules.
|
|
57
|
+
project_value = project
|
|
58
|
+
target = identity(repository, url=True)
|
|
59
|
+
# A recursive blob inventory omits empty trees. Read the actual root entries
|
|
60
|
+
# so every nonordinary declaration is present and rejected, including those trees.
|
|
61
|
+
named = {e['path']: e for e in history.root_entries(history.commit) if e['path'] in FIELDS}
|
|
62
|
+
present = any(name in named for name in MANIFESTS)
|
|
63
|
+
if not present:
|
|
64
|
+
identity(project)
|
|
65
|
+
return None # Legacy sources retain strict reference inspection.
|
|
66
|
+
documents = []
|
|
67
|
+
for name in MANIFESTS:
|
|
68
|
+
entry = named.get(name)
|
|
69
|
+
if not ordinary_document(entry):
|
|
70
|
+
raise ValueError('destination manifest must be an ordinary committed file')
|
|
71
|
+
document = json.loads(history.blob(entry['oid']), object_pairs_hook=unique_object,
|
|
72
|
+
parse_constant=reject_constant)
|
|
73
|
+
if not isinstance(document, dict):
|
|
74
|
+
raise ValueError('destination manifest must be an object')
|
|
75
|
+
documents.append(document)
|
|
76
|
+
edition, project_document = documents
|
|
77
|
+
if 'identityKind' in edition:
|
|
78
|
+
logical, invalid = tree_identity(history, named.values())
|
|
79
|
+
if invalid or logical is None or project != logical:
|
|
80
|
+
raise ValueError('invalid or inconsistent logical project identity')
|
|
81
|
+
# Validate the maintained recipe and same-tree lock before allowing bare
|
|
82
|
+
# repository declarations. Logical IDs and repository IDs bind separately.
|
|
83
|
+
project_values = [logical]
|
|
84
|
+
repository_value = repository_identity
|
|
85
|
+
else:
|
|
86
|
+
project_value = identity(project)
|
|
87
|
+
project_values = [identity(edition.get('id')), identity(project_document.get('projectId'))]
|
|
88
|
+
repository_value = lambda value: identity(value, url=True)
|
|
89
|
+
repository_values = [repository_value(project_document.get('sourceRepository'))]
|
|
90
|
+
if 'custody' in edition:
|
|
91
|
+
custody = edition['custody']
|
|
92
|
+
if not isinstance(custody, dict):
|
|
93
|
+
raise ValueError('invalid custody metadata')
|
|
94
|
+
if 'repository' in custody:
|
|
95
|
+
repository_values.append(repository_value(custody['repository']))
|
|
96
|
+
if (any(value != project_value for value in project_values) or
|
|
97
|
+
any(value != target for value in repository_values)):
|
|
98
|
+
raise ValueError('destination disagreement')
|
|
99
|
+
return target
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == '__main__':
|
|
103
|
+
# The producer uses this same validator before installs or output creation.
|
|
104
|
+
from git_history import History, InspectionError
|
|
105
|
+
parser = argparse.ArgumentParser()
|
|
106
|
+
for name in ('repo', 'ref', 'project', 'repository'):
|
|
107
|
+
parser.add_argument('--' + name, required=True)
|
|
108
|
+
args = parser.parse_args()
|
|
109
|
+
history = None
|
|
110
|
+
try:
|
|
111
|
+
history = History(args.repo, args.ref)
|
|
112
|
+
destination(history, args.project, args.repository)
|
|
113
|
+
except (ValueError, TypeError, UnicodeError, OSError, InspectionError):
|
|
114
|
+
raise SystemExit(1) from None
|
|
115
|
+
finally:
|
|
116
|
+
if history:
|
|
117
|
+
history.close()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Read dotenv values into in-memory digests; never retain or report plaintext."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def digest(value):
|
|
8
|
+
return hashlib.sha256(value).digest()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_hashes(path):
|
|
12
|
+
if path is None:
|
|
13
|
+
return set()
|
|
14
|
+
return load_hashes_and_digest(path)[0]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_hashes_and_digest(path):
|
|
18
|
+
if path is None:
|
|
19
|
+
return frozenset(), None
|
|
20
|
+
with Path(path).expanduser().open('rb') as stream:
|
|
21
|
+
raw = stream.read(16 * 1024 * 1024 + 1)
|
|
22
|
+
if len(raw) > 16 * 1024 * 1024:
|
|
23
|
+
raise ValueError("environment file too large")
|
|
24
|
+
# Digest exact bytes, including comments/line endings; mtime is irrelevant.
|
|
25
|
+
text = raw.decode('utf-8').replace('\r\n', '\n').replace('\r', '\n')
|
|
26
|
+
return frozenset(value_hashes(text)), hashlib.sha256(raw).hexdigest()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def value_hashes(text):
|
|
30
|
+
text = text.lstrip('\ufeff')
|
|
31
|
+
hashes = set()
|
|
32
|
+
# No evaluation, expansion, subprocesses or diagnostics containing input.
|
|
33
|
+
assignment = re.compile(r'(?:export[ \t]+)?[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*')
|
|
34
|
+
cursor = 0
|
|
35
|
+
while cursor < len(text):
|
|
36
|
+
if text[cursor].isspace():
|
|
37
|
+
cursor += 1
|
|
38
|
+
continue
|
|
39
|
+
if text[cursor] == '#':
|
|
40
|
+
end = text.find('\n', cursor)
|
|
41
|
+
cursor = len(text) if end < 0 else end + 1
|
|
42
|
+
continue
|
|
43
|
+
match = assignment.match(text, cursor)
|
|
44
|
+
if not match:
|
|
45
|
+
raise ValueError('invalid environment file')
|
|
46
|
+
cursor = match.end()
|
|
47
|
+
if cursor < len(text) and text[cursor] in "\"'":
|
|
48
|
+
quote = text[cursor]
|
|
49
|
+
cursor += 1
|
|
50
|
+
value = []
|
|
51
|
+
while cursor < len(text) and text[cursor] != quote:
|
|
52
|
+
char = text[cursor]
|
|
53
|
+
if quote == '"' and char == '\\' and cursor + 1 < len(text):
|
|
54
|
+
cursor += 1
|
|
55
|
+
char = {'n': '\n', 'r': '\r', 't': '\t', '"': '"', '\\': '\\'}.get(text[cursor], '\\' + text[cursor])
|
|
56
|
+
value.append(char)
|
|
57
|
+
cursor += 1
|
|
58
|
+
if cursor == len(text):
|
|
59
|
+
raise ValueError('invalid environment file')
|
|
60
|
+
cursor += 1
|
|
61
|
+
end = text.find('\n', cursor)
|
|
62
|
+
end = len(text) if end < 0 else end
|
|
63
|
+
tail = text[cursor:end].strip()
|
|
64
|
+
if tail and not tail.startswith('#'):
|
|
65
|
+
raise ValueError('invalid environment file')
|
|
66
|
+
value = ''.join(value)
|
|
67
|
+
else:
|
|
68
|
+
end = text.find('\n', cursor)
|
|
69
|
+
end = len(text) if end < 0 else end
|
|
70
|
+
value = re.split(r'\s+#', text[cursor:end], maxsplit=1)[0].strip()
|
|
71
|
+
cursor = end + 1
|
|
72
|
+
if value:
|
|
73
|
+
hashes.add(digest(value.encode()))
|
|
74
|
+
return hashes
|