@mnstry/atelier 0.2.0-alpha.1 → 0.2.0-alpha.11
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/CHANGELOG.md +413 -0
- package/README.md +402 -205
- package/SECURITY.md +6 -1
- package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
- package/contracts/atelier-coauthor.v1.schema.json +163 -0
- package/contracts/atelier-decision-request.v1.schema.json +476 -0
- package/contracts/atelier-decision-result.v1.schema.json +500 -0
- package/contracts/atelier-guide.v1.schema.json +227 -0
- package/contracts/atelier-intake.v1.schema.json +151 -0
- package/contracts/atelier-migration.v2.schema.json +61 -0
- package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
- package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
- package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
- package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
- package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
- package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
- package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
- package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
- package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
- package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
- package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
- package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
- package/contracts/atelier-presentation.v1.schema.json +1293 -0
- package/contracts/atelier-repository-observation.v1.schema.json +163 -0
- package/contracts/atelier-review.v1.schema.json +1601 -0
- package/contracts/atelier-skill-steward.v1.schema.json +692 -0
- package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
- package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
- package/contracts/atelier-vault-publication.v1.schema.json +58 -0
- package/contracts/public-api-baseline.json +57 -0
- package/docs/assurance-controls.md +49 -0
- package/docs/atelier-runtime.md +28 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/audience-visibility.md +9 -0
- package/docs/blocks/claims.md +61 -0
- package/docs/blocks/conformance-admission.md +15 -0
- package/docs/blocks/will-not-do.md +18 -0
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/continuity.md +3 -4
- package/docs/decisions.md +167 -0
- package/docs/design.md +167 -0
- package/docs/exact-upgrades.md +159 -0
- package/docs/extension-pack-lifecycle.md +33 -0
- package/docs/guided-upgrades.md +99 -0
- package/docs/install.md +99 -16
- package/docs/intake-and-guides.md +65 -0
- package/docs/integration-and-review-plan.md +409 -0
- package/docs/integration-contract-decisions.md +22 -0
- package/docs/knowledge-graph.md +8 -4
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +328 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +698 -0
- package/docs/obsidian.md +484 -0
- package/docs/portable-presentation.md +328 -0
- package/docs/presentation-consumer-boundaries.md +75 -0
- package/docs/presentation-review-disposition.md +47 -0
- package/docs/project-command-inventory.md +38 -0
- package/docs/project-options.md +53 -0
- package/docs/release-engineering.md +174 -12
- package/docs/repo-boundary-guard.md +12 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +155 -5
- package/docs/vault-service.md +292 -0
- package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
- package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
- package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
- package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
- package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
- package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
- package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
- package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
- package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
- package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
- package/fixtures/atelier-intake/valid/source.v1.json +7 -0
- package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
- package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
- package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
- package/fixtures/atelier-review/valid/bound.v1.json +229 -0
- package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
- package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
- package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
- package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
- package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
- package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
- package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
- package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
- package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
- package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
- package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
- package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
- package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
- package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
- package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
- package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
- package/fixtures/decisions/request/valid/triage.v1.json +60 -0
- package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
- package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
- package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
- package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
- package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
- package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
- package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
- package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
- package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
- package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
- package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
- package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
- package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
- package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
- package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
- package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
- package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
- package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
- package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
- package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
- package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
- package/fixtures/obsidian/edits/workspace.json +65 -0
- package/fixtures/obsidian/materialization/expected-full.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
- package/fixtures/obsidian/materialization/workspace.json +54 -0
- package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
- package/fixtures/projects/sample-workspace/content/source.html.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +86 -10
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-local-service/SKILL.md +47 -0
- package/skills/claude/atelier-open/SKILL.md +11 -5
- package/skills/claude/atelier-public-boundary/SKILL.md +31 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
- package/skills/claude/mnstry-readiness/SKILL.md +5 -0
- package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/codex/atelier-local-service/SKILL.md +47 -0
- package/skills/codex/atelier-open/SKILL.md +11 -5
- package/skills/codex/atelier-public-boundary/SKILL.md +31 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
- package/skills/codex/mnstry-readiness/SKILL.md +5 -0
- package/src/access/preview-enforcer.mjs +84 -0
- package/src/boundary/content-rules.mjs +283 -20
- package/src/boundary/policy.mjs +213 -91
- package/src/cli/execute-command.mjs +36 -0
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +78 -10
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +348 -0
- package/src/collaboration/index.mjs +17 -0
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/proposals.mjs +265 -65
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/attestation.mjs +20 -6
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/disclosure.mjs +133 -0
- package/src/commands/distribution.mjs +2 -1
- package/src/commands/extension-pack.mjs +17 -9
- package/src/commands/init.mjs +46 -6
- package/src/commands/obsidian.mjs +371 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/server.mjs +1 -4
- package/src/commands/setup.mjs +51 -14
- package/src/commands/skills.mjs +199 -0
- package/src/commands/sync.mjs +107 -0
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +35 -0
- package/src/decisions/contracts.d.mts +96 -0
- package/src/decisions/contracts.mjs +321 -0
- package/src/disclosure/content-scan.mjs +206 -0
- package/src/egress/check.mjs +7 -38
- package/src/egress/forbidden-egress.mjs +32 -18
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +176 -316
- package/src/graph/knowledge-graph.mjs +643 -43
- package/src/guides/contracts.mjs +54 -0
- package/src/harness/context-client.mjs +9 -1
- package/src/index.mjs +41 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +112 -52
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +180 -0
- package/src/project/package-root.mjs +10 -0
- package/src/project/path-match.mjs +38 -15
- package/src/project/private-state.mjs +120 -0
- package/src/projection/obsidian/contracts.mjs +571 -0
- package/src/projection/obsidian/edits/align.mjs +169 -0
- package/src/projection/obsidian/edits/apply.mjs +889 -0
- package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
- package/src/projection/obsidian/edits/contribution.mjs +108 -0
- package/src/projection/obsidian/edits/index.mjs +19 -0
- package/src/projection/obsidian/edits/object-identity.mjs +61 -0
- package/src/projection/obsidian/edits/object-store.mjs +517 -0
- package/src/projection/obsidian/edits/observe.mjs +123 -0
- package/src/projection/obsidian/edits/policy.mjs +123 -0
- package/src/projection/obsidian/edits/regions.mjs +648 -0
- package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
- package/src/projection/obsidian/materialize/index.mjs +7 -0
- package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
- package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
- package/src/projection/obsidian/materialize/settings.mjs +57 -0
- package/src/projection/obsidian/materialize/stage.mjs +27 -0
- package/src/projection/obsidian/proposals/adapter.mjs +572 -0
- package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
- package/src/projection/obsidian/proposals/contribution.mjs +55 -0
- package/src/projection/obsidian/proposals/index.mjs +23 -0
- package/src/projection/obsidian/proposals/observation.mjs +216 -0
- package/src/projection/obsidian/proposals/queue.mjs +277 -0
- package/src/projection/obsidian/proposals/router.mjs +168 -0
- package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
- package/src/projection/obsidian/publication/exchange.mjs +117 -0
- package/src/projection/obsidian/publication/index.mjs +5 -0
- package/src/projection/obsidian/publication/publisher.mjs +588 -0
- package/src/projection/obsidian/publication/test-seam.mjs +16 -0
- package/src/projection/obsidian/publication/transport.mjs +212 -0
- package/src/projection/obsidian/recovery/index.mjs +6 -0
- package/src/projection/obsidian/recovery/journal.mjs +134 -0
- package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
- package/src/projection/obsidian/recovery/restart.mjs +316 -0
- package/src/projection/obsidian/recovery/store.mjs +234 -0
- package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
- package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
- package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
- package/src/projection/obsidian/selection-ui/index.mjs +12 -0
- package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
- package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
- package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
- package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
- package/src/projection/project.mjs +28 -19
- package/src/readiness-protocols/evidence.mjs +282 -0
- package/src/readiness-protocols/runtime.mjs +4 -3
- package/src/readiness-protocols/source-read.mjs +39 -0
- package/src/runtime/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -0
- package/src/runtime/obsidian/app-capability.mjs +138 -0
- package/src/runtime/obsidian/app-production-seams.mjs +107 -0
- package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
- package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
- package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
- package/src/runtime/obsidian/contributions.mjs +27 -0
- package/src/runtime/obsidian/documents.mjs +37 -0
- package/src/runtime/obsidian/enablement.mjs +53 -0
- package/src/runtime/obsidian/engine.mjs +532 -0
- package/src/runtime/obsidian/errors.mjs +14 -0
- package/src/runtime/obsidian/extension-points.mjs +103 -0
- package/src/runtime/obsidian/index.mjs +34 -0
- package/src/runtime/obsidian/lifecycle.mjs +258 -0
- package/src/runtime/obsidian/machine-settings.mjs +260 -0
- package/src/runtime/obsidian/observation.mjs +158 -0
- package/src/runtime/obsidian/opening.mjs +280 -0
- package/src/runtime/obsidian/pending-edits.mjs +141 -0
- package/src/runtime/obsidian/pipeline.mjs +122 -0
- package/src/runtime/obsidian/private-lock.mjs +156 -0
- package/src/runtime/obsidian/process-identity.mjs +58 -0
- package/src/runtime/obsidian/service-client.mjs +66 -0
- package/src/runtime/obsidian/service-entry-path.mjs +9 -0
- package/src/runtime/obsidian/service-main.mjs +93 -0
- package/src/runtime/obsidian/service-record.mjs +169 -0
- package/src/runtime/obsidian/service-server.mjs +121 -0
- package/src/runtime/obsidian/service.mjs +216 -0
- package/src/runtime/obsidian/startup-adapters.mjs +87 -0
- package/src/runtime/obsidian/state-store.mjs +180 -0
- package/src/runtime/obsidian/sync-notice.mjs +33 -0
- package/src/runtime/obsidian/tick-loop.mjs +84 -0
- package/src/runtime/obsidian/watchers.mjs +36 -0
- package/src/runtime/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +803 -0
- package/src/server/local-sidecar.mjs +125 -59
- package/src/server/security.mjs +89 -4
- package/src/server/server.mjs +4 -2
- package/src/skills/steward.mjs +780 -0
- package/src/support/feedback-report.mjs +4 -3
- package/src/ui/presentation/browser.mjs +278 -0
- package/src/ui/presentation/contract.mjs +116 -0
- package/src/ui/presentation/index.mjs +7 -0
- package/src/ui/presentation/native.mjs +153 -0
- package/src/ui/presentation/proof.mjs +57 -0
- package/src/ui/presentation/schema-check.mjs +30 -0
- package/src/ui/presentation/schema.generated.mjs +1371 -0
- package/src/ui/presentation/state.mjs +43 -0
- package/src/ui/presentation/styles.mjs +67 -0
- package/src/ui/presentation/tokens.mjs +74 -0
- package/src/ui/presentation/web.mjs +85 -0
- package/src/ui/review-page.mjs +32 -0
- package/src/upgrade/explanation.mjs +33 -0
- package/src/upgrade/provenance.mjs +338 -0
- package/src/upgrade/transaction-files.mjs +103 -0
- package/src/upgrade/transaction.mjs +475 -0
- package/src/upgrade/upgrade.mjs +65 -29
- package/src/vault/hosts.mjs +21 -0
- package/src/vault/identity.mjs +19 -0
- package/src/vault/index.mjs +7 -0
- package/src/vault/interface.mjs +11 -0
- package/src/vault/metadata.mjs +31 -0
- package/src/vault/privacy.mjs +148 -0
- package/src/vault/probe.mjs +89 -0
- package/src/vault/service.mjs +131 -0
- package/src/vault/source.mjs +45 -0
- package/src/vault/storage.mjs +23 -0
- package/templates/distribution-workspace/.claude/launch.json +6 -2
- package/templates/distribution-workspace/gitignore +1 -0
- package/templates/distribution-workspace/project/README.md +2 -2
- package/templates/external-project-workspace/README.md +45 -0
- package/templates/external-project-workspace/answers.example.json +4 -0
- package/templates/external-project-workspace/atelier.project.json +40 -0
- package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
- package/templates/external-project-workspace/gitignore +8 -0
- package/templates/external-project-workspace/pack-lifecycle.json +14 -0
- package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
- package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
- package/templates/external-project-workspace/repo-access.v1.json +9 -0
- package/templates/external-project-workspace/source/README.md +11 -0
- package/templates/launch.json +6 -2
- package/templates/private-domain-workspace/.claude/launch.json +6 -2
- package/templates/private-domain-workspace/domain/README.md +2 -2
- package/templates/private-domain-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/.claude/launch.json +6 -2
- package/templates/shared-project-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/project/README.md +2 -2
package/src/upgrade/upgrade.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import crypto from 'node:crypto'
|
|
|
3
3
|
import fs from 'node:fs'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { buildGraph } from '../graph/graph.mjs'
|
|
6
|
-
import { buildProjectProjection } from '../projection/project.mjs'
|
|
6
|
+
import { buildProjectProjection, buildProjectManifest } from '../projection/project.mjs'
|
|
7
7
|
import { buildReadiness } from '../readiness/readiness.mjs'
|
|
8
8
|
import { validateJsonSchema } from '../export/atelier-export-contract.mjs'
|
|
9
9
|
import {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
resolvePathValue,
|
|
15
15
|
writeJson,
|
|
16
16
|
} from '../project/config.mjs'
|
|
17
|
+
import { packageRootFrom } from '../project/package-root.mjs'
|
|
17
18
|
import {
|
|
18
19
|
BOUNDARY_POLICY_SCHEMA,
|
|
19
20
|
checkBoundaryPolicy,
|
|
@@ -22,6 +23,10 @@ import {
|
|
|
22
23
|
} from '../boundary/policy.mjs'
|
|
23
24
|
import { bundledMnstryReadinessPackV1 } from '../readiness-protocols/bundled-pack.mjs'
|
|
24
25
|
import { loadExtensionPacks } from '../extension-packs/loader.mjs'
|
|
26
|
+
import { inspectPackageProvenance, legacyPackageSource } from './provenance.mjs'
|
|
27
|
+
import { prepareUpgrade, applySavedUpgrade, upgradeOperationStatus, recoverUpgradeDryRun, explainSavedUpgrade } from './transaction.mjs'
|
|
28
|
+
import { renderUpgradeExplanation } from './explanation.mjs'
|
|
29
|
+
export { prepareUpgrade, applySavedUpgrade, upgradeOperationStatus, recoverUpgradeDryRun, explainSavedUpgrade }
|
|
25
30
|
|
|
26
31
|
export const ATELIER_LOCK_SCHEMA = 'mnstry.atelier-lock@v1'
|
|
27
32
|
export const ATELIER_MIGRATION_SCHEMA = 'mnstry.atelier-migration@v1'
|
|
@@ -36,7 +41,7 @@ export const MIGRATION_CLASSES = new Set([
|
|
|
36
41
|
'breaking',
|
|
37
42
|
])
|
|
38
43
|
|
|
39
|
-
const packageRoot =
|
|
44
|
+
const packageRoot = packageRootFrom(import.meta.url)
|
|
40
45
|
const packageJson = readJson(path.join(packageRoot, 'package.json'))
|
|
41
46
|
const lockSchema = readJson(path.join(packageRoot, 'contracts', 'atelier-lock.v1.schema.json'))
|
|
42
47
|
const REVIEW_MARKER_RE = /(Atelier-Boundary-Review|boundary-review)\s*:\s*(approved|reviewed)/i
|
|
@@ -76,18 +81,7 @@ function packageGitSha() {
|
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
function packageSource() {
|
|
79
|
-
|
|
80
|
-
const repository = packageGitRoot()
|
|
81
|
-
? git(packageRoot, ['config', '--get', 'remote.origin.url']) || packageJson.repository?.url || null
|
|
82
|
-
: packageJson.repository?.url || null
|
|
83
|
-
if (repository && gitSha) return { type: 'git', repository, gitSha }
|
|
84
|
-
if (repository) return { type: 'private_github', repository, gitSha: null }
|
|
85
|
-
return {
|
|
86
|
-
type: 'local_path',
|
|
87
|
-
path: '.',
|
|
88
|
-
gitSha,
|
|
89
|
-
repository,
|
|
90
|
-
}
|
|
84
|
+
return legacyPackageSource(inspectPackageProvenance(packageRoot, { includeInventory: false }))
|
|
91
85
|
}
|
|
92
86
|
|
|
93
87
|
export function lockPathForProject(project) {
|
|
@@ -171,7 +165,7 @@ export function buildAtelierLock({ project, templateId = 'existing-workspace', a
|
|
|
171
165
|
}
|
|
172
166
|
}
|
|
173
167
|
|
|
174
|
-
export function writeAtelierLock({ project, templateId
|
|
168
|
+
export function writeAtelierLock({ project, templateId } = {}) {
|
|
175
169
|
const lockPath = lockPathForProject(project)
|
|
176
170
|
// Packs load in throwing mode on purpose: a broken extension pack cannot be
|
|
177
171
|
// locked. Lock verification is disabled for this one load so re-pinning is
|
|
@@ -179,8 +173,18 @@ export function writeAtelierLock({ project, templateId = 'existing-workspace' }
|
|
|
179
173
|
// moved or set aside, so it exists at every observable point, and writeJson
|
|
180
174
|
// replaces it atomically (temp file + renameSync). A load failure throws
|
|
181
175
|
// before any write and leaves the previous lock untouched.
|
|
176
|
+
const previous = readOptionalJson(lockPath)
|
|
177
|
+
if (previous) {
|
|
178
|
+
const errors = validateJsonSchema(lockSchema, previous)
|
|
179
|
+
if (errors.length) throw new Error(`cannot preserve invalid lock history: ${errors.join('; ')}`)
|
|
180
|
+
}
|
|
182
181
|
const { packs } = loadExtensionPacks(project, { verifyLock: false })
|
|
183
|
-
const lock = buildAtelierLock({ project, templateId, packs })
|
|
182
|
+
const lock = buildAtelierLock({ project, templateId: templateId || previous?.template?.id || 'existing-workspace', packs })
|
|
183
|
+
if (previous) {
|
|
184
|
+
lock.appliedMigrations = previous.appliedMigrations
|
|
185
|
+
lock.lastSuccessfulUpgrade = previous.lastSuccessfulUpgrade
|
|
186
|
+
lock.template = { ...previous.template, ...(templateId ? { id: templateId } : {}) }
|
|
187
|
+
}
|
|
184
188
|
writeJson(lockPath, lock)
|
|
185
189
|
return lock
|
|
186
190
|
}
|
|
@@ -518,13 +522,7 @@ function runGeneratedRefresh(project) {
|
|
|
518
522
|
const projection = buildProjectProjection(project)
|
|
519
523
|
fs.mkdirSync(path.dirname(projection.output), { recursive: true })
|
|
520
524
|
fs.writeFileSync(projection.output, projection.html)
|
|
521
|
-
writeJson(path.join(project.outputRoot, 'atelier.manifest.json'),
|
|
522
|
-
schema: 'mnstry.atelier-manifest@v1',
|
|
523
|
-
generatedAt: 'deterministic',
|
|
524
|
-
graphPath: project.graphPath,
|
|
525
|
-
entry: 'index.html',
|
|
526
|
-
nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
|
|
527
|
-
})
|
|
525
|
+
writeJson(path.join(project.outputRoot, 'atelier.manifest.json'), buildProjectManifest(project, projection))
|
|
528
526
|
writeJson(project.readinessPath, buildReadiness({ project, graph }))
|
|
529
527
|
}
|
|
530
528
|
|
|
@@ -616,7 +614,7 @@ export function checkAtelierLock(project) {
|
|
|
616
614
|
if (lock && lock.package?.version !== packageJson.version) errors.push(`lock package version ${lock.package?.version} does not match ${packageJson.version}`)
|
|
617
615
|
if (lock && lock.contracts?.boundaryPolicy !== BOUNDARY_POLICY_SCHEMA) errors.push(`lock boundary policy contract must be ${BOUNDARY_POLICY_SCHEMA}`)
|
|
618
616
|
const loadedPolicy = loadPolicyForLock(project)
|
|
619
|
-
if (lock
|
|
617
|
+
if (lock && (lock.boundaryPolicy?.digest ?? null) !== loadedPolicy.digest) {
|
|
620
618
|
errors.push('boundary policy digest has changed; run upgrade --dry-run before applying')
|
|
621
619
|
}
|
|
622
620
|
if (lock) {
|
|
@@ -655,24 +653,62 @@ export function checkAtelierLock(project) {
|
|
|
655
653
|
export function runLockCommand(argv = process.argv.slice(2)) {
|
|
656
654
|
const args = parseArgs(argv)
|
|
657
655
|
const subcommand = args._[0] || 'check'
|
|
656
|
+
if (subcommand === 'provenance') {
|
|
657
|
+
const report = inspectPackageProvenance(packageRoot)
|
|
658
|
+
console.log(JSON.stringify(report, null, 2))
|
|
659
|
+
process.exitCode = args['exact-source-required'] && !report.sourceVerified ? 1 : 0
|
|
660
|
+
return
|
|
661
|
+
}
|
|
658
662
|
const project = commandProject({ argv })
|
|
659
663
|
if (subcommand === 'write') {
|
|
660
|
-
const lock = writeAtelierLock({ project, templateId: firstString(args.template, args['template-id'], project.config?.template?.id) ||
|
|
664
|
+
const lock = writeAtelierLock({ project, templateId: firstString(args.template, args['template-id'], project.config?.template?.id) || undefined })
|
|
661
665
|
console.log(JSON.stringify({ ok: true, path: lockPathForProject(project), lock }, null, 2))
|
|
662
|
-
process.
|
|
666
|
+
process.exitCode = 0
|
|
667
|
+
return
|
|
663
668
|
}
|
|
664
669
|
if (subcommand === 'check') {
|
|
665
670
|
const report = checkAtelierLock(project)
|
|
671
|
+
if (args['exact-source-required']) {
|
|
672
|
+
report.provenance = inspectPackageProvenance(packageRoot)
|
|
673
|
+
if (!report.provenance.sourceVerified) { report.ok = false; report.errors.push('installed package source is unverified') }
|
|
674
|
+
}
|
|
666
675
|
console.log(JSON.stringify(report, null, 2))
|
|
667
|
-
process.
|
|
676
|
+
process.exitCode = report.ok ? 0 : 1
|
|
677
|
+
return
|
|
668
678
|
}
|
|
669
679
|
console.error(`Unknown lock command: ${subcommand}`)
|
|
670
|
-
process.
|
|
680
|
+
process.exitCode = 1
|
|
671
681
|
}
|
|
672
682
|
|
|
673
683
|
export function runUpgradeCommand(argv = process.argv.slice(2)) {
|
|
674
684
|
const args = parseArgs(argv)
|
|
675
|
-
const project = commandProject({ argv })
|
|
685
|
+
const project = commandProject({ argv, writeLocalState: args._[0] !== 'explain' })
|
|
686
|
+
if (['plan', 'apply', 'status', 'recover', 'explain'].includes(args._[0])) {
|
|
687
|
+
try {
|
|
688
|
+
let result
|
|
689
|
+
if (args._[0] === 'explain') {
|
|
690
|
+
if (!firstString(args.plan)) throw new Error('--plan is required')
|
|
691
|
+
if (args.format != null && !['json', 'markdown'].includes(args.format)) throw new Error('--format must be json or markdown')
|
|
692
|
+
result = explainSavedUpgrade({ project, planFile: args.plan })
|
|
693
|
+
console.log(args.format === 'markdown' ? renderUpgradeExplanation(result) : JSON.stringify(result, null, 2))
|
|
694
|
+
return
|
|
695
|
+
} else if (args._[0] === 'plan') {
|
|
696
|
+
if (args.save !== true) throw new Error('exact planning requires --save')
|
|
697
|
+
result = prepareUpgrade({ project })
|
|
698
|
+
} else if (args._[0] === 'apply') {
|
|
699
|
+
if (!firstString(args.plan)) throw new Error('--plan is required')
|
|
700
|
+
result = applySavedUpgrade({ project, planFile: args.plan, confirm: args.confirm })
|
|
701
|
+
} else if (args._[0] === 'status') {
|
|
702
|
+
result = upgradeOperationStatus({ project, operationId: args.operation })
|
|
703
|
+
} else {
|
|
704
|
+
if (args['dry-run'] !== true) throw new Error('recovery supports --dry-run only; mutation requires a separately reviewed recovery plan')
|
|
705
|
+
result = recoverUpgradeDryRun({ project, operationId: args.operation })
|
|
706
|
+
}
|
|
707
|
+
console.log(JSON.stringify(result, null, 2))
|
|
708
|
+
process.exitCode = result.ok === false ? 1 : 0
|
|
709
|
+
} catch (error) { console.error(error.message); process.exitCode = 1 }
|
|
710
|
+
return
|
|
711
|
+
}
|
|
676
712
|
const confirmBreaking = asArray(args['confirm-breaking']).concat(firstString(args['confirm-breaking']) ? [String(args['confirm-breaking'])] : [])
|
|
677
713
|
const allowDirtyGenerated = Boolean(args['allow-dirty-generated'])
|
|
678
714
|
if (args.check) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createVaultService } from './service.mjs'
|
|
2
|
+
import { vaultIdentity } from './identity.mjs'
|
|
3
|
+
import { r2PrivateStorage, vercelPrivateStorage } from './storage.mjs'
|
|
4
|
+
import { d1VaultMetadata, postgresVaultMetadata } from './metadata.mjs'
|
|
5
|
+
|
|
6
|
+
/** Vercel host passes its established session verifier, credential store and SDKs. */
|
|
7
|
+
export function createVercelVault({ blob, postgres, verifySession, lookupCredential, privacy, deployment }) {
|
|
8
|
+
return createVaultService({ privacy, deployment, storage: vercelPrivateStorage(blob), metadata: postgresVaultMetadata(postgres), identity: vaultIdentity({ verifySession, lookupCredential, privacy, deployment }) })
|
|
9
|
+
}
|
|
10
|
+
/** Create a handler per request so D1 sessions cannot carry stale bookmarks. */
|
|
11
|
+
export function createCloudflareVault({ verifySession, lookupCredential, privacy, deployment }) {
|
|
12
|
+
vaultIdentity({ verifySession, lookupCredential })
|
|
13
|
+
async function handle(request, env) {
|
|
14
|
+
try {
|
|
15
|
+
const db = env.VAULT_DB.withSession('first-primary')
|
|
16
|
+
const identity = vaultIdentity({ verifySession: req => verifySession(req, env), lookupCredential: hash => lookupCredential(hash, env, db) })
|
|
17
|
+
return await createVaultService({ privacy, deployment, storage: r2PrivateStorage(env.VAULT_OBJECTS), metadata: d1VaultMetadata(db), identity })(request)
|
|
18
|
+
} catch { return new Response(null, { status: 503, headers: { "Cache-Control": "private, no-store", "X-Content-Type-Options": "nosniff" } }) }
|
|
19
|
+
}
|
|
20
|
+
return { fetch: handle }
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host verifies browser sessions using its established identity provider.
|
|
3
|
+
* Machine credentials are high-entropy secrets whose SHA-256 hashes are stored
|
|
4
|
+
* by the host. Lookup must read authoritative, current expiry/revocation state.
|
|
5
|
+
*/
|
|
6
|
+
import { digest } from './service.mjs'
|
|
7
|
+
export function vaultIdentity({ verifySession, lookupCredential, now = () => Date.now() }) {
|
|
8
|
+
if (typeof verifySession !== 'function' || typeof lookupCredential !== 'function') throw new TypeError('Identity bindings required')
|
|
9
|
+
return {
|
|
10
|
+
read: request => verifySession(request),
|
|
11
|
+
async publish(request, vault) {
|
|
12
|
+
const value = request.headers.get('authorization')
|
|
13
|
+
if (!value || !/^Bearer [A-Za-z0-9_-]{43,128}$/.test(value)) return null
|
|
14
|
+
const record = await lookupCredential(await digest(new TextEncoder().encode(value.slice(7))))
|
|
15
|
+
if (!record || record.vault !== vault || record.revoked !== false || !Number.isFinite(record.expiresAt) || record.expiresAt <= now()) return null
|
|
16
|
+
return record.owner
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createVaultService, preparePublication } from './service.mjs'
|
|
2
|
+
export { vaultIdentity } from './identity.mjs'
|
|
3
|
+
export { r2PrivateStorage, vercelPrivateStorage } from './storage.mjs'
|
|
4
|
+
export { VAULT_TABLE_SQL, d1VaultMetadata, postgresVaultMetadata } from './metadata.mjs'
|
|
5
|
+
export { createVercelVault, createCloudflareVault } from './hosts.mjs'
|
|
6
|
+
export { assessVaultPrivacy, createVaultPrivacyState, createVaultContext, VaultAlarmPersistenceError } from './privacy.mjs'
|
|
7
|
+
export { createVaultPrivacyProbe } from './probe.mjs'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const escape = value => String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
2
|
+
export function renderVaultHome({ vault, revision, manifest, privacy, query = '' }) {
|
|
3
|
+
const files = manifest.filter(file => /\.(html|pdf|txt)$/.test(file.path))
|
|
4
|
+
const matches = files.filter(file => file.path.toLowerCase().includes(query.toLowerCase()))
|
|
5
|
+
const verified = privacy.status === 'verified'
|
|
6
|
+
const label = verified ? 'Unauthorized-access checks passed' : privacy.status === 'exposed' ? 'Privacy failure — external routes may remain exposed' : 'Privacy not verified — access blocked'
|
|
7
|
+
const list = verified ? matches.map(file => `<li><a href="/${escape(vault)}/${file.path.split('/').map(encodeURIComponent).join('/')}">${escape(file.path)}</a><span>${escape(file.type.split(';')[0])}</span></li>`).join('') : ''
|
|
8
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(vault)} · Atelier</title><style>
|
|
9
|
+
*{box-sizing:border-box}body{margin:0;background:#f6f5ef;color:#20382d;font:17px/1.6 system-ui,sans-serif}main{max-width:960px;margin:auto;padding:clamp(20px,5vw,64px)}header{border-bottom:1px solid #cad3c7;padding-bottom:24px}h1{font-size:clamp(30px,6vw,52px);line-height:1.15;overflow-wrap:anywhere}a{color:inherit;text-underline-offset:4px}a:focus-visible,input:focus-visible,button:focus-visible{outline:3px solid #28734e;outline-offset:4px}.status{padding:16px;border:1px solid #738975;border-radius:8px;margin:24px 0}.blocked{background:#fff0dc;border-color:#936125}label{display:block;font-weight:600}form{display:flex;gap:12px;flex-wrap:wrap}input{flex:1;min-width:0;font:inherit;padding:12px;border:1px solid #738975;border-radius:6px}button{font:inherit;padding:12px 20px;background:#20382d;color:white;border:0;border-radius:6px}ul{list-style:none;padding:0}li{padding:20px 0;border-bottom:1px solid #cad3c7}li a{display:block;overflow-wrap:anywhere}li span,footer{font-size:14px;color:#506354}footer{margin-top:40px}.skip{position:absolute;left:-10000px}.skip:focus{left:16px;top:8px}</style></head><body><a class="skip" href="#artifacts">Skip to artifacts</a><main><header><p>ATELIER · PRIVATE VAULT</p><h1>${escape(vault)}</h1><p>Published revision ${escape(revision)} · ${files.length} artifact${files.length === 1 ? '' : 's'}</p></header><section class="status ${verified ? '' : 'blocked'}" aria-label="Privacy status"><strong>${label}</strong><p>${escape(privacy.reason)}</p>${verified ? `<small>Checked ${escape(new Date(privacy.checkedAt).toISOString())}. Authorization and evidence freshness are checked when you open an artifact.</small>` : '<p>Ask the workspace operator to verify destination protection, then reload. Artifacts are unavailable until the checks pass.</p>'}</section><label for="search">Find an artifact</label><form role="search" method="get" action="/${escape(vault)}/"><input id="search" name="q" type="search" value="${escape(query)}" maxlength="200"><button type="submit">Search</button></form>${verified ? `<ul id="artifacts" tabindex="-1" aria-label="Artifacts">${list}</ul>${matches.length ? '' : `<p>${files.length ? 'No artifacts match your search.' : 'No artifacts have been published yet.'}</p>`}` : ''}<footer>This website displays published artifacts. Saving or reviewing source material does not publish it. Generated artifacts run without script privileges.</footer></main></body></html>`
|
|
10
|
+
}
|
|
11
|
+
export const vaultHomePolicy = "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** SQL adapters require pre-provisioned vaults; neither endpoint creates owners. */
|
|
2
|
+
export const VAULT_TABLE_SQL = `CREATE TABLE atelier_vaults (
|
|
3
|
+
id TEXT PRIMARY KEY,
|
|
4
|
+
owner_issuer TEXT NOT NULL,
|
|
5
|
+
owner_subject TEXT NOT NULL,
|
|
6
|
+
revision INTEGER NOT NULL DEFAULT 0,
|
|
7
|
+
manifest TEXT NOT NULL DEFAULT '[]',
|
|
8
|
+
publication TEXT
|
|
9
|
+
)`
|
|
10
|
+
function record(row) {
|
|
11
|
+
return row ? { owner: { issuer: row.owner_issuer, subject: row.owner_subject }, revision: Number(row.revision), manifest: JSON.parse(row.manifest), publication: row.publication } : null
|
|
12
|
+
}
|
|
13
|
+
export function d1VaultMetadata(db) {
|
|
14
|
+
return {
|
|
15
|
+
async get(vault) { return record(await db.prepare('SELECT * FROM atelier_vaults WHERE id = ?').bind(vault).first()) },
|
|
16
|
+
async commit(vault, update) {
|
|
17
|
+
const result = await db.prepare('UPDATE atelier_vaults SET revision = revision + 1, manifest = ?, publication = ? WHERE id = ? AND revision = ? AND owner_issuer = ? AND owner_subject = ?')
|
|
18
|
+
.bind(JSON.stringify(update.manifest), update.publication, vault, update.expectedRevision, update.owner.issuer, update.owner.subject).run()
|
|
19
|
+
return result.success === true && result.meta.changes === 1
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function postgresVaultMetadata(db) {
|
|
24
|
+
return {
|
|
25
|
+
async get(vault) { return record((await db.query('SELECT * FROM atelier_vaults WHERE id = $1', [vault])).rows[0]) },
|
|
26
|
+
async commit(vault, update) {
|
|
27
|
+
const result = await db.query('UPDATE atelier_vaults SET revision = revision + 1, manifest = $1, publication = $2 WHERE id = $3 AND revision = $4 AND owner_issuer = $5 AND owner_subject = $6', [JSON.stringify(update.manifest), update.publication, vault, update.expectedRevision, update.owner.issuer, update.owner.subject])
|
|
28
|
+
return result.rowCount === 1
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Trusted host evidence only: never accept evidence from a publisher. */
|
|
2
|
+
const unknown = reason => ({ status: 'unknown', reason })
|
|
3
|
+
export function vaultObjects(vault, manifest) {
|
|
4
|
+
return [...new Map(manifest.map(file => [`${vault}/${file.sha256}`, { key: `${vault}/${file.sha256}`, sha256: file.sha256, size: file.size }])).values()]
|
|
5
|
+
}
|
|
6
|
+
export function validDeployment(deployment) {
|
|
7
|
+
if (!deployment || typeof deployment.id !== 'string' || !deployment.id || !Array.isArray(deployment.origins) || !deployment.origins.length || deployment.origins.length > 16) return false
|
|
8
|
+
return new Set(deployment.origins).size === deployment.origins.length && deployment.origins.every(origin => {
|
|
9
|
+
try { const u = new URL(origin); return u.protocol === 'https:' && u.origin === origin && !u.username && !u.password } catch { return false }
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
export function assessVaultPrivacy(evidence, context) {
|
|
13
|
+
const { vault, publication, revision, phase, owner, deployment, manifest = [], objects = [], request, now = Date.now() } = context
|
|
14
|
+
// Exposure is a sticky alarm, not authorization. Even stale/mismatched trusted
|
|
15
|
+
// evidence must not turn a known exposure into an ordinary availability error.
|
|
16
|
+
if (evidence?.targets?.some?.(t => t?.anonymous === 'content' || t?.otherUser === 'content')) return { status: 'exposed', reason: 'Unauthorized content observed. This handler refuses delivery; external routes may remain exposed.' }
|
|
17
|
+
if (!evidence || evidence.vault !== vault || evidence.publication !== publication) return unknown('Protection evidence does not match this publication.')
|
|
18
|
+
if (!Number.isSafeInteger(revision) || revision < 0 || evidence.revision !== revision) return unknown('Protection evidence does not match the metadata revision.')
|
|
19
|
+
if (evidence.phase !== phase || !['before-upload', 'before-activation', 'read'].includes(phase) || !owner?.issuer || !owner?.subject || evidence.owner?.issuer !== owner.issuer || evidence.owner?.subject !== owner.subject) return unknown('Protection evidence does not match the owner and operation.')
|
|
20
|
+
if (!validDeployment(deployment) || evidence.deployment?.id !== deployment.id || JSON.stringify(evidence.deployment?.origins) !== JSON.stringify(deployment.origins)) return unknown('Protection evidence does not match this deployment.')
|
|
21
|
+
if (request && (!deployment.origins.includes(request.origin) || (phase === 'read' && request.path !== `/${vault}` && request.path !== `/${vault}/` && !manifest.some(f => request.path === `/${vault}/${f.path}`)))) return unknown('Request is outside the verified inventory.')
|
|
22
|
+
if (!Number.isFinite(evidence.checkedAt) || !Number.isFinite(evidence.validUntil) || evidence.checkedAt > now || evidence.validUntil <= now || evidence.validUntil <= evidence.checkedAt || evidence.validUntil - evidence.checkedAt > 300000) return unknown('Protection evidence is missing or expired.')
|
|
23
|
+
if (typeof evidence.policyRevision !== 'string' || !evidence.policyRevision) return unknown('Provider policy identity is missing.')
|
|
24
|
+
const config = evidence.configuration
|
|
25
|
+
if (config?.ownerOnly !== true || config?.privateStorage !== true || config?.completeInventory !== true) return unknown('Provider protection or route inventory is unverified.')
|
|
26
|
+
if (!Array.isArray(evidence.targets) || !evidence.targets.length || evidence.targets.length > 2000) return unknown('Access checks are missing or oversized.')
|
|
27
|
+
const urls = new Set(), routes = new Set(), keys = new Set()
|
|
28
|
+
for (const target of evidence.targets) {
|
|
29
|
+
let url
|
|
30
|
+
try { url = new URL(target.url) } catch { return unknown('Access check URL is invalid.') }
|
|
31
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || urls.has(url.href)) return unknown('Access check inventory is invalid.')
|
|
32
|
+
urls.add(url.href)
|
|
33
|
+
if (target.anonymous !== 'denied' || target.otherUser !== 'denied') return unknown('Required access checks did not complete successfully.')
|
|
34
|
+
if (target.kind === 'storage') {
|
|
35
|
+
if (!objects.some(o => o.key === target.key && o.sha256 === target.sha256 && o.size === target.size)) return unknown('Storage check does not match a staged object.')
|
|
36
|
+
keys.add(target.key)
|
|
37
|
+
} else if (target.kind === 'route') {
|
|
38
|
+
if (!deployment.origins.includes(url.origin)) return unknown('Access check uses an unconfigured origin.')
|
|
39
|
+
const file = manifest.find(f => url.pathname === `/${vault}/${f.path}`)
|
|
40
|
+
const home = url.pathname === `/${vault}` || url.pathname === `/${vault}/`
|
|
41
|
+
if (!home && (!file || file.sha256 !== target.sha256)) return unknown('Access check does not match a manifest path and digest.')
|
|
42
|
+
routes.add(url.href)
|
|
43
|
+
} else return unknown('Access check target is invalid.')
|
|
44
|
+
}
|
|
45
|
+
for (const origin of deployment.origins) {
|
|
46
|
+
for (const path of [`/${vault}`, `/${vault}/`, ...manifest.map(f => `/${vault}/${f.path}`)]) if (!routes.has(origin + path)) return unknown('A configured route is missing from access checks.')
|
|
47
|
+
}
|
|
48
|
+
for (const object of objects) if (!keys.has(object.key)) return unknown('A staged storage object is missing from access checks.')
|
|
49
|
+
return { status: 'verified', reason: 'Configured routes refused unauthorized access at the recorded check time. Owner rendering is qualified separately.', checkedAt: evidence.checkedAt, validUntil: evidence.validUntil, policyRevision: evidence.policyRevision }
|
|
50
|
+
}
|
|
51
|
+
export async function verifyVaultPrivacy(privacy, context) {
|
|
52
|
+
const method = context.phase === 'read' ? 'current' : 'verify'
|
|
53
|
+
if (typeof privacy?.[method] !== 'function') return unknown('No provider privacy evidence is configured.')
|
|
54
|
+
try { return assessVaultPrivacy(await privacy[method](context), context) }
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof VaultAlarmPersistenceError) return { ...assessVaultPrivacy(error.evidence, context), persistence: 'unconfirmed' }
|
|
57
|
+
return unknown('Provider privacy verification is unavailable.')
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The host must alert/retry this failure; the local read latch remains set. */
|
|
62
|
+
export class VaultAlarmPersistenceError extends Error {
|
|
63
|
+
constructor(evidence, cause) {
|
|
64
|
+
super('Vault exposure is latched locally but durable persistence is unconfirmed.', { cause })
|
|
65
|
+
this.name = 'VaultAlarmPersistenceError'
|
|
66
|
+
this.code = 'VAULT_ALARM_NOT_PERSISTED'
|
|
67
|
+
this.evidence = evidence
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Public builder keeps refresh contexts aligned with service storage keys. */
|
|
72
|
+
export function createVaultContext({ vault, record, deployment, request, phase = 'read' }) {
|
|
73
|
+
if (!record || !Number.isSafeInteger(record.revision) || record.revision < 0 || !Array.isArray(record.manifest)) throw new TypeError('Authoritative revision and manifest required')
|
|
74
|
+
return { vault, owner: record.owner, publication: record.publication ?? null, revision: record.revision, manifest: record.manifest, objects: vaultObjects(vault, record.manifest), deployment, ...(request ? { request } : {}), phase }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Host-owned versioned evidence store. Refresh is explicit; reads never probe. */
|
|
78
|
+
export function createVaultPrivacyState({ probe, load, compareAndSet }) {
|
|
79
|
+
if (typeof probe?.verify !== 'function' || typeof load !== 'function' || typeof compareAndSet !== 'function') throw new TypeError('Probe and versioned evidence store required')
|
|
80
|
+
const alarms = new Map()
|
|
81
|
+
const key = context => {
|
|
82
|
+
if (!Number.isSafeInteger(context?.revision) || context.revision < 0) throw new TypeError('Authoritative nonnegative metadata revision required')
|
|
83
|
+
return JSON.stringify([context.deployment?.id, context.vault])
|
|
84
|
+
}
|
|
85
|
+
const exposed = (evidence, context) => assessVaultPrivacy(evidence, context).status === 'exposed'
|
|
86
|
+
async function snapshot(id, context) {
|
|
87
|
+
const record = await load(id)
|
|
88
|
+
if (!record || !Number.isSafeInteger(record.version) || record.version < 0 || !Object.hasOwn(record, 'evidence')) throw new TypeError('Invalid evidence store snapshot')
|
|
89
|
+
if (exposed(record.evidence, context)) alarms.set(id, { evidence: record.evidence, persisted: true })
|
|
90
|
+
return record
|
|
91
|
+
}
|
|
92
|
+
async function persistAlarm(id, context) {
|
|
93
|
+
// Alarm writes may replace newer green evidence regardless of revision.
|
|
94
|
+
// Reload on conflict, never reuse a stale version or retry green evidence.
|
|
95
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
96
|
+
if (alarms.get(id).persisted) return alarms.get(id).evidence
|
|
97
|
+
try {
|
|
98
|
+
const record = await snapshot(id, context)
|
|
99
|
+
if (alarms.get(id).persisted) return alarms.get(id).evidence
|
|
100
|
+
const evidence = alarms.get(id).evidence
|
|
101
|
+
if (await compareAndSet(id, evidence, { expectedVersion: record.version }) === true) {
|
|
102
|
+
alarms.set(id, { evidence, persisted: true })
|
|
103
|
+
return evidence
|
|
104
|
+
}
|
|
105
|
+
} catch (cause) { throw new VaultAlarmPersistenceError(alarms.get(id).evidence, cause) }
|
|
106
|
+
}
|
|
107
|
+
throw new VaultAlarmPersistenceError(alarms.get(id).evidence, new Error('Alarm write conflict limit reached'))
|
|
108
|
+
}
|
|
109
|
+
async function collect(context) {
|
|
110
|
+
const id = key(context)
|
|
111
|
+
// Pending latches retry persistence without re-probing a possibly vanished
|
|
112
|
+
// or staged-only exposure. Confirmed durable alarms need no rewrite.
|
|
113
|
+
if (alarms.has(id)) return persistAlarm(id, context)
|
|
114
|
+
const previous = await snapshot(id, context)
|
|
115
|
+
if (alarms.has(id)) return persistAlarm(id, context)
|
|
116
|
+
if (previous.evidence?.revision > context.revision) return null
|
|
117
|
+
const evidence = await probe.verify(context)
|
|
118
|
+
if (exposed(evidence, context)) {
|
|
119
|
+
if (!alarms.has(id)) alarms.set(id, { evidence, persisted: false })
|
|
120
|
+
return persistAlarm(id, context)
|
|
121
|
+
}
|
|
122
|
+
if (alarms.has(id)) return persistAlarm(id, context)
|
|
123
|
+
if (context.phase !== 'read') {
|
|
124
|
+
// Another instance may have latched an incident during the slow probe.
|
|
125
|
+
// This is still a check, not a transaction with metadata activation.
|
|
126
|
+
const latest = await snapshot(id, context)
|
|
127
|
+
if (alarms.has(id)) return persistAlarm(id, context)
|
|
128
|
+
return latest.evidence?.revision > context.revision ? null : evidence
|
|
129
|
+
}
|
|
130
|
+
// Green writes retain their original version. Conflicts and store failures
|
|
131
|
+
// must never be interpreted as a successful refresh.
|
|
132
|
+
if (await compareAndSet(id, evidence, { expectedVersion: previous.version }) !== true) {
|
|
133
|
+
await snapshot(id, context)
|
|
134
|
+
return alarms.has(id) ? persistAlarm(id, context) : null
|
|
135
|
+
}
|
|
136
|
+
return alarms.has(id) ? persistAlarm(id, context) : evidence
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
verify: collect,
|
|
140
|
+
async current(context) {
|
|
141
|
+
const id = key(context)
|
|
142
|
+
if (alarms.has(id)) return alarms.get(id).evidence
|
|
143
|
+
const record = await snapshot(id, context)
|
|
144
|
+
return alarms.get(id)?.evidence ?? record.evidence
|
|
145
|
+
},
|
|
146
|
+
async refresh(context) { return collect({ ...context, phase: 'read' }) },
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { digest } from './service.mjs'
|
|
2
|
+
const canonical = value => JSON.stringify(value, (_, item) => item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map(k => [k, item[k]])) : item)
|
|
3
|
+
const MAX_BYTES = 4 * 1024 * 1024
|
|
4
|
+
const cleanUrl = value => {
|
|
5
|
+
const url = new URL(value)
|
|
6
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) throw new Error('Unsafe probe URL')
|
|
7
|
+
return url.href
|
|
8
|
+
}
|
|
9
|
+
async function classify(response, target, loginUrls) {
|
|
10
|
+
if (!(response instanceof Response)) return 'unknown'
|
|
11
|
+
const reader = response.body?.getReader()
|
|
12
|
+
const chunks = []; let length = 0
|
|
13
|
+
try {
|
|
14
|
+
for (; reader;) {
|
|
15
|
+
const { value, done } = await reader.read()
|
|
16
|
+
if (done) break
|
|
17
|
+
length += value.byteLength
|
|
18
|
+
if (length > MAX_BYTES) { await reader.cancel(); return 'unknown' }
|
|
19
|
+
chunks.push(value)
|
|
20
|
+
}
|
|
21
|
+
} finally { reader?.releaseLock() }
|
|
22
|
+
const bytes = new Uint8Array(length); let offset = 0
|
|
23
|
+
for (const value of chunks) { bytes.set(value, offset); offset += value.length }
|
|
24
|
+
// Content exposure wins over misleading error or redirect status codes.
|
|
25
|
+
if (length > 0 && (await digest(bytes) === target.sha256 || (typeof target.canary === 'string' && target.canary.length >= 32 && new TextDecoder().decode(bytes).includes(target.canary)))) return 'content'
|
|
26
|
+
if (response.redirected) return 'unknown'
|
|
27
|
+
if ([401, 403, 404].includes(response.status)) return length === 0 ? 'denied' : 'unknown'
|
|
28
|
+
if ([302, 303, 307, 308].includes(response.status)) {
|
|
29
|
+
try {
|
|
30
|
+
const location = new URL(response.headers.get('location'), target.url)
|
|
31
|
+
if (location.protocol !== 'https:' || location.username || location.password || location.hash) return 'unknown'
|
|
32
|
+
const endpoint = loginUrls.find(item => item.url === location.origin + location.pathname)
|
|
33
|
+
return endpoint && [...location.searchParams.keys()].every(key => endpoint.queryKeys.includes(key)) ? 'denied' : 'unknown'
|
|
34
|
+
} catch { return 'unknown' }
|
|
35
|
+
}
|
|
36
|
+
return 'unknown'
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* inspect(context) is a trusted provider configuration/inventory collector.
|
|
40
|
+
* request({url,identity,redirect,signal}) is host transport; credentials never
|
|
41
|
+
* cross identities or redirects. No global fetch or ambient credentials here.
|
|
42
|
+
* Each collection re-inspects policy after probing to refuse changed settings.
|
|
43
|
+
*/
|
|
44
|
+
export function createVaultPrivacyProbe({ inspect, request, now = () => Date.now(), timeoutMs = 60000, onExposure }) {
|
|
45
|
+
if (typeof inspect !== 'function' || typeof request !== 'function' || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60000) throw new TypeError('Explicit probe bindings required')
|
|
46
|
+
return {
|
|
47
|
+
async verify(context) {
|
|
48
|
+
const controller = new AbortController()
|
|
49
|
+
let timer
|
|
50
|
+
const checkedAt = now()
|
|
51
|
+
const deadline = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new Error('Probe deadline exceeded')) }, timeoutMs) })
|
|
52
|
+
const collect = async () => {
|
|
53
|
+
const inventory = structuredClone(await inspect(context, { signal: controller.signal }))
|
|
54
|
+
if (!inventory || !Array.isArray(inventory.targets) || !inventory.targets.length || inventory.targets.length > 2000) throw new Error('Invalid probe inventory')
|
|
55
|
+
const targets = inventory.targets.map(target => {
|
|
56
|
+
if (!target || !/^[a-f0-9]{64}$/.test(target.sha256)) throw new Error('Expected content digest required')
|
|
57
|
+
return { ...target, url: cleanUrl(target.url) }
|
|
58
|
+
})
|
|
59
|
+
const loginUrls = (inventory.loginEndpoints ?? []).map(item => {
|
|
60
|
+
if (!Array.isArray(item.queryKeys) || item.queryKeys.some(key => !['state', 'redirect_uri', 'redirect_url', 'returnTo', 'next', 'client_id', 'response_type', 'scope', 'nonce', 'code_challenge', 'code_challenge_method'].includes(key))) throw new Error('Unsafe login parameters')
|
|
61
|
+
return { url: cleanUrl(item.url), queryKeys: item.queryKeys }
|
|
62
|
+
})
|
|
63
|
+
const evidence = results => ({ vault: context.vault, publication: context.publication, revision: context.revision, phase: context.phase, owner: context.owner, deployment: context.deployment, checkedAt, validUntil: checkedAt + 60000, policyRevision: inventory.policyRevision, configuration: inventory.configuration, targets: results })
|
|
64
|
+
const results = []
|
|
65
|
+
for (const target of targets) {
|
|
66
|
+
const result = { url: target.url, kind: target.kind, sha256: target.sha256, key: target.key, size: target.size }
|
|
67
|
+
for (const identity of ['anonymous', 'otherUser']) {
|
|
68
|
+
if (controller.signal.aborted) throw new Error('Probe aborted')
|
|
69
|
+
try { result[identity] = await classify(await request({ url: target.url, identity, redirect: 'manual', signal: controller.signal }), target, loginUrls) }
|
|
70
|
+
catch { result[identity] = 'unknown' }
|
|
71
|
+
if (result[identity] === 'content') {
|
|
72
|
+
const alarm = evidence([...results, result])
|
|
73
|
+
// Notification cannot delay or downgrade the exposure verdict. The
|
|
74
|
+
// host owns durable incident persistence and provider containment.
|
|
75
|
+
if (typeof onExposure === 'function') Promise.resolve().then(() => onExposure(alarm)).catch(() => {})
|
|
76
|
+
return alarm
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
results.push(result)
|
|
80
|
+
}
|
|
81
|
+
const after = await inspect(context, { signal: controller.signal })
|
|
82
|
+
if (canonical(after) !== canonical(inventory)) throw new Error('Provider protection changed during verification')
|
|
83
|
+
return evidence(results)
|
|
84
|
+
}
|
|
85
|
+
try { return await Promise.race([collect(), deadline]) }
|
|
86
|
+
finally { clearTimeout(timer); controller.abort() }
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
}
|