@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.8
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 +191 -0
- package/README.md +86 -25
- 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/docs/assurance-controls.md +12 -2
- package/docs/atelier-runtime.md +13 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/claims.md +28 -18
- package/docs/blocks/will-not-do.md +12 -3
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/decisions.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 +65 -9
- 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/local-review.md +84 -0
- package/docs/local-services.md +227 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +687 -0
- package/docs/obsidian.md +387 -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 +99 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +70 -2
- 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/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +61 -6
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-open/SKILL.md +5 -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-open/SKILL.md +5 -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 +9 -4
- package/src/boundary/policy.mjs +74 -42
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +61 -3
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +16 -33
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/extension-pack.mjs +15 -8
- package/src/commands/init.mjs +44 -5
- package/src/commands/obsidian.mjs +368 -0
- package/src/commands/review.mjs +147 -0
- 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 +13 -0
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +64 -2
- package/src/graph/knowledge-graph.mjs +548 -24
- package/src/guides/contracts.mjs +54 -0
- package/src/index.mjs +29 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +50 -49
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +166 -0
- package/src/project/private-state.mjs +11 -1
- 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 +139 -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 +110 -0
- package/src/runtime/obsidian/app-production-seams.mjs +96 -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 +243 -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-main.mjs +89 -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 +44 -0
- package/src/server/server.mjs +1 -0
- package/src/skills/steward.mjs +780 -0
- 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 +63 -28
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { verifyVaultPrivacy, createVaultContext } from './privacy.mjs'
|
|
2
|
+
import { renderVaultHome, vaultHomePolicy } from './interface.mjs'
|
|
3
|
+
/** Optional hosted artifact service. No default network, storage, or identity provider. */
|
|
4
|
+
const TYPES = Object.freeze({ html: 'text/html; charset=utf-8', css: 'text/css; charset=utf-8', txt: 'text/plain; charset=utf-8', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', pdf: 'application/pdf' })
|
|
5
|
+
const LIMIT = 4 * 1024 * 1024
|
|
6
|
+
const encoder = new TextEncoder()
|
|
7
|
+
const validId = value => typeof value === 'string' && /^[a-z][a-z0-9-]{0,62}$/.test(value)
|
|
8
|
+
const validPath = value => typeof value === 'string' && value.length <= 512 && value.split('/').every(p => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(p) && p !== '.' && p !== '..')
|
|
9
|
+
const validExt = value => value === undefined || (value !== null && typeof value === 'object' && !Array.isArray(value))
|
|
10
|
+
const sameOwner = (a, b) => a && b && typeof a.issuer === 'string' && a.issuer.length > 0 && typeof a.subject === 'string' && a.subject.length > 0 && a.issuer === b.issuer && a.subject === b.subject
|
|
11
|
+
const headers = {
|
|
12
|
+
'Cache-Control': 'private, no-store',
|
|
13
|
+
'Content-Security-Policy': "sandbox; default-src 'none'; img-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
14
|
+
'X-Content-Type-Options': 'nosniff',
|
|
15
|
+
'Referrer-Policy': 'no-referrer',
|
|
16
|
+
'X-Robots-Tag': 'noindex, nofollow',
|
|
17
|
+
}
|
|
18
|
+
export async function digest(bytes) {
|
|
19
|
+
return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), b => b.toString(16).padStart(2, '0')).join('')
|
|
20
|
+
}
|
|
21
|
+
function reply(status, text = '', extra = {}) { return new Response(text, { status, headers: { ...headers, 'Content-Type': 'text/plain; charset=utf-8', ...extra } }) }
|
|
22
|
+
async function boundedBody(request) {
|
|
23
|
+
if (!request.body) throw new Error('invalid')
|
|
24
|
+
const reader = request.body.getReader()
|
|
25
|
+
let size = 0
|
|
26
|
+
const chunks = []
|
|
27
|
+
try {
|
|
28
|
+
while (true) {
|
|
29
|
+
const { done, value } = await reader.read()
|
|
30
|
+
if (done) break
|
|
31
|
+
size += value.byteLength
|
|
32
|
+
if (size > LIMIT) { await reader.cancel(); throw new Error('too-large') }
|
|
33
|
+
chunks.push(value)
|
|
34
|
+
}
|
|
35
|
+
} finally { reader.releaseLock() }
|
|
36
|
+
const bytes = new Uint8Array(size)
|
|
37
|
+
let offset = 0
|
|
38
|
+
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.length }
|
|
39
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes))
|
|
40
|
+
}
|
|
41
|
+
export async function preparePublication(input) {
|
|
42
|
+
if (!input || input.schema !== 'atelier-vault-publication/v1' || (input.contractVersion !== undefined && (typeof input.contractVersion !== 'string' || !/^1\.[0-9]+\.[0-9]+$/.test(input.contractVersion))) || !validExt(input.ext) || !Number.isSafeInteger(input.expectedRevision) || input.expectedRevision < 0 || input.expectedRevision >= Number.MAX_SAFE_INTEGER || Object.keys(input).some(key => !['schema', 'contractVersion', 'expectedRevision', 'files', 'ext'].includes(key)) || !Array.isArray(input.files) || !input.files.length || input.files.length > 100) throw new Error('invalid')
|
|
43
|
+
if (encoder.encode(JSON.stringify(input)).length > LIMIT) throw new Error('too-large')
|
|
44
|
+
const seen = new Set()
|
|
45
|
+
const files = []
|
|
46
|
+
let total = 0
|
|
47
|
+
for (const file of input.files) {
|
|
48
|
+
if (!file || !validExt(file.ext) || Object.keys(file).some(key => !['path', 'base64', 'ext'].includes(key)) || !validPath(file.path) || seen.has(file.path) || typeof file.base64 !== 'string' || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(file.base64)) throw new Error('invalid')
|
|
49
|
+
const type = TYPES[file.path.split('.').at(-1)]
|
|
50
|
+
if (typeof type !== 'string') throw new Error('invalid')
|
|
51
|
+
seen.add(file.path)
|
|
52
|
+
const bytes = Uint8Array.from(atob(file.base64), c => c.charCodeAt(0))
|
|
53
|
+
total += bytes.length
|
|
54
|
+
if (total > LIMIT) throw new Error('too-large')
|
|
55
|
+
files.push({ path: file.path, type, size: bytes.length, sha256: await digest(bytes), bytes })
|
|
56
|
+
}
|
|
57
|
+
files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
|
58
|
+
const manifest = files.map(({ bytes, ...record }) => record)
|
|
59
|
+
return { files, manifest, publication: await digest(encoder.encode(JSON.stringify(manifest))) }
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* identity.read(request) -> verified {issuer,subject}, or null (revocation checked).
|
|
63
|
+
* identity.publish(request,vault) -> verified owner identity, or null; MUST use
|
|
64
|
+
* a revocable vault-scoped machine credential, never browser cookies.
|
|
65
|
+
* metadata.get(vault) -> {owner, revision, manifest}, or null.
|
|
66
|
+
* metadata.commit(vault,{owner,expectedRevision,manifest,publication}) -> boolean;
|
|
67
|
+
* MUST atomically compare both owner and revision before replacing manifest.
|
|
68
|
+
* storage.get/put accepts service-generated keys only and uses private storage.
|
|
69
|
+
*/
|
|
70
|
+
export function createVaultService({ identity, metadata, storage, privacy, deployment }) {
|
|
71
|
+
for (const [object, methods] of [[identity, ['read', 'publish']], [metadata, ['get', 'commit']], [storage, ['get', 'put']]]) {
|
|
72
|
+
if (!object || methods.some(method => typeof object[method] !== 'function')) throw new TypeError('Incomplete vault adapter')
|
|
73
|
+
}
|
|
74
|
+
return async function handle(request) {
|
|
75
|
+
try {
|
|
76
|
+
const url = new URL(request.url)
|
|
77
|
+
if (/%|\\/.test(url.pathname)) return reply(400)
|
|
78
|
+
const parts = url.pathname.slice(1).split('/')
|
|
79
|
+
const publishing = parts[0] === '_publish'
|
|
80
|
+
const vault = publishing ? parts[1] : parts[0]
|
|
81
|
+
if (!validId(vault)) return reply(404)
|
|
82
|
+
const home = !publishing && (parts.length === 1 || (parts.length === 2 && parts[1] === ''))
|
|
83
|
+
if (url.search && (publishing || (home && ( [...url.searchParams.keys()].some(key => key !== 'q') || url.searchParams.getAll('q').length > 1 || (url.searchParams.get('q') || '').length > 200)))) return reply(400)
|
|
84
|
+
if (publishing) {
|
|
85
|
+
if (parts.length !== 2 || !['GET', 'POST'].includes(request.method)) return reply(405, '', { Allow: 'GET, POST' })
|
|
86
|
+
// Browser-originated writes are not a machine publication channel.
|
|
87
|
+
if (request.headers.has('origin') || request.headers.has('cookie')) return reply(403)
|
|
88
|
+
const principal = await identity.publish(request, vault)
|
|
89
|
+
if (!principal) return reply(401, '', { 'WWW-Authenticate': 'Bearer realm="vault-publication"' })
|
|
90
|
+
const record = await metadata.get(vault)
|
|
91
|
+
if (!sameOwner(principal, record?.owner)) return reply(404)
|
|
92
|
+
if (request.method === 'GET') {
|
|
93
|
+
return new Response(JSON.stringify({ schema: 'atelier-vault-status/v1', vault, revision: record.revision, publication: record.publication ?? null }), { headers: { ...headers, 'Content-Type': 'application/json' } })
|
|
94
|
+
}
|
|
95
|
+
if (request.headers.get('content-type')?.split(';')[0].trim().toLowerCase() !== 'application/json') return reply(415)
|
|
96
|
+
let input, prepared
|
|
97
|
+
try { input = await boundedBody(request); prepared = await preparePublication(input) }
|
|
98
|
+
catch (error) { return reply(error.message === 'too-large' ? 413 : 400) }
|
|
99
|
+
if (input.expectedRevision !== record.revision) return reply(409)
|
|
100
|
+
const context = createVaultContext({ vault, record: { owner: record.owner, publication: prepared.publication, revision: input.expectedRevision + 1, manifest: prepared.manifest }, deployment, request: { origin: url.origin, path: url.pathname } })
|
|
101
|
+
const before = await verifyVaultPrivacy(privacy, { ...context, phase: 'before-upload' })
|
|
102
|
+
if (before.status !== 'verified') return reply(503, before.reason)
|
|
103
|
+
for (const file of prepared.files) await storage.put(`${vault}/${file.sha256}`, file.bytes)
|
|
104
|
+
const after = await verifyVaultPrivacy(privacy, { ...context, phase: 'before-activation' })
|
|
105
|
+
if (after.status !== 'verified' || after.policyRevision !== before.policyRevision) return reply(503, 'Protection changed or could not be verified. Publication was not activated.')
|
|
106
|
+
// Revalidate the credential after uploads and activation checks, immediately before commit.
|
|
107
|
+
const current = await identity.publish(request, vault)
|
|
108
|
+
if (!sameOwner(principal, current)) return reply(401, '', { 'WWW-Authenticate': 'Bearer realm="vault-publication"' })
|
|
109
|
+
const committed = await metadata.commit(vault, { owner: principal, expectedRevision: input.expectedRevision, manifest: prepared.manifest, publication: prepared.publication })
|
|
110
|
+
if (!committed) return reply(409)
|
|
111
|
+
return new Response(JSON.stringify({ schema: 'atelier-vault-receipt/v1', vault, revision: input.expectedRevision + 1, publication: prepared.publication }), { status: 201, headers: { ...headers, 'Content-Type': 'application/json' } })
|
|
112
|
+
}
|
|
113
|
+
if (!['GET', 'HEAD'].includes(request.method)) return reply(405, '', { Allow: 'GET, HEAD' })
|
|
114
|
+
const principal = await identity.read(request)
|
|
115
|
+
if (!principal) return reply(401)
|
|
116
|
+
const record = await metadata.get(vault)
|
|
117
|
+
if (!sameOwner(principal, record?.owner)) return reply(404)
|
|
118
|
+
if (!home && !record.manifest.some(file => file.path === parts.slice(1).join('/'))) return reply(404)
|
|
119
|
+
const protection = await verifyVaultPrivacy(privacy, createVaultContext({ vault, record, deployment, request: { origin: url.origin, path: url.pathname } }))
|
|
120
|
+
if (home) return new Response(request.method === 'HEAD' ? null : renderVaultHome({ vault, revision: record.revision, manifest: record.manifest, privacy: protection, query: url.searchParams.get('q') || '' }), { headers: { ...headers, 'Content-Type': 'text/html; charset=utf-8', 'Content-Security-Policy': vaultHomePolicy } })
|
|
121
|
+
if (protection.status !== 'verified') return reply(503, protection.reason)
|
|
122
|
+
const path = parts.slice(1).join('/')
|
|
123
|
+
if (!validPath(path)) return reply(404)
|
|
124
|
+
const file = record.manifest.find(entry => entry.path === path)
|
|
125
|
+
if (!file || !/^[a-f0-9]{64}$/.test(file.sha256) || !Object.values(TYPES).includes(file.type)) return reply(404)
|
|
126
|
+
const bytes = await storage.get(`${vault}/${file.sha256}`)
|
|
127
|
+
if (!bytes || bytes.length !== file.size || await digest(bytes) !== file.sha256) return reply(503)
|
|
128
|
+
return new Response(request.method === 'HEAD' ? null : bytes, { headers: { ...headers, 'Content-Type': file.type, 'Content-Length': String(bytes.length), ...(file.type === 'application/pdf' ? { 'Content-Disposition': 'attachment' } : {}) } })
|
|
129
|
+
} catch { return reply(503) }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { preparePublication } from './service.mjs'
|
|
4
|
+
const LIMIT = 4 * 1024 * 1024
|
|
5
|
+
/** Node-only bundle preparation. Select paths explicitly; never scan a repo. */
|
|
6
|
+
export async function prepareVaultSource({ root, paths, expectedRevision }) {
|
|
7
|
+
if (typeof root !== 'string' || !path.isAbsolute(root) || !Array.isArray(paths) || !paths.length || paths.length > 100) throw new TypeError('Absolute artifact root and explicit paths required')
|
|
8
|
+
if (fs.lstatSync(root).isSymbolicLink()) throw new Error('Artifact root must not be a symlink')
|
|
9
|
+
const realRoot = fs.realpathSync(root)
|
|
10
|
+
if (!fs.statSync(realRoot).isDirectory()) throw new Error('Artifact root must be a directory')
|
|
11
|
+
let total = 0
|
|
12
|
+
const files = []
|
|
13
|
+
for (const name of paths) {
|
|
14
|
+
if (typeof name !== 'string' || name.length > 512 || !name.split('/').every(part => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(part))) throw new Error('Invalid selected artifact path')
|
|
15
|
+
let file = realRoot
|
|
16
|
+
for (const part of name.split('/')) {
|
|
17
|
+
file = path.join(file, part)
|
|
18
|
+
if (fs.lstatSync(file).isSymbolicLink()) throw new Error('Selected artifacts must not traverse symlinks')
|
|
19
|
+
}
|
|
20
|
+
const before = fs.lstatSync(file)
|
|
21
|
+
if (!before.isFile() || before.nlink !== 1 || before.size > LIMIT) throw new Error('Selected artifact must be a bounded, unlinked regular file')
|
|
22
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0))
|
|
23
|
+
try {
|
|
24
|
+
const opened = fs.fstatSync(fd)
|
|
25
|
+
const resolved = fs.realpathSync(file)
|
|
26
|
+
if ((path.relative(realRoot, resolved).startsWith('..' + path.sep) || path.isAbsolute(path.relative(realRoot, resolved))) || opened.ino !== before.ino || opened.dev !== before.dev || !opened.isFile() || opened.nlink !== 1 || opened.size > LIMIT) throw new Error('Artifact changed during selection')
|
|
27
|
+
const data = Buffer.alloc(opened.size)
|
|
28
|
+
let offset = 0
|
|
29
|
+
while (offset < data.length) {
|
|
30
|
+
const count = fs.readSync(fd, data, offset, data.length - offset, offset)
|
|
31
|
+
if (!count) throw new Error('Artifact changed during read')
|
|
32
|
+
offset += count
|
|
33
|
+
}
|
|
34
|
+
const after = fs.fstatSync(fd)
|
|
35
|
+
const current = fs.lstatSync(file)
|
|
36
|
+
if (after.nlink !== 1 || current.nlink !== 1 || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs || current.ino !== opened.ino || current.dev !== opened.dev || fs.realpathSync(file) !== resolved) throw new Error('Artifact changed during read')
|
|
37
|
+
total += data.length
|
|
38
|
+
if (total > LIMIT) throw new Error('Artifact bundle too large')
|
|
39
|
+
files.push({ path: name, base64: data.toString('base64') })
|
|
40
|
+
} finally { fs.closeSync(fd) }
|
|
41
|
+
}
|
|
42
|
+
const request = { schema: 'atelier-vault-publication/v1', contractVersion: '1.0.0', expectedRevision, files }
|
|
43
|
+
const prepared = await preparePublication(request)
|
|
44
|
+
return { request, publication: prepared.publication, manifest: prepared.manifest }
|
|
45
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Provider SDKs/bindings are explicitly supplied by the host, never auto-loaded. */
|
|
2
|
+
export function r2PrivateStorage(bucket) {
|
|
3
|
+
return {
|
|
4
|
+
async put(key, bytes) { await bucket.put(key, bytes) },
|
|
5
|
+
async get(key) {
|
|
6
|
+
const object = await bucket.get(key)
|
|
7
|
+
return object ? new Uint8Array(await object.arrayBuffer()) : null
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function vercelPrivateStorage(blob) {
|
|
12
|
+
return {
|
|
13
|
+
async put(key, bytes) {
|
|
14
|
+
await blob.put(key, bytes, { access: 'private', addRandomSuffix: false, allowOverwrite: true, contentType: 'application/octet-stream' })
|
|
15
|
+
},
|
|
16
|
+
async get(key) {
|
|
17
|
+
const object = await blob.get(key, { access: 'private', useCache: false })
|
|
18
|
+
if (!object) return null
|
|
19
|
+
if (object.statusCode !== 200 || !object.stream) throw new Error('Private storage unavailable')
|
|
20
|
+
return new Uint8Array(await new Response(object.stream).arrayBuffer())
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
"configurations": [
|
|
4
4
|
{
|
|
5
5
|
"name": "atelier",
|
|
6
|
-
"runtimeExecutable": "
|
|
7
|
-
"runtimeArgs": [
|
|
6
|
+
"runtimeExecutable": "node",
|
|
7
|
+
"runtimeArgs": [
|
|
8
|
+
"--input-type=module",
|
|
9
|
+
"--eval",
|
|
10
|
+
"import { runCli } from '@mnstry/atelier/cli'; process.exit(await runCli({ argv: ['server', ...process.argv.slice(1)] }));"
|
|
11
|
+
],
|
|
8
12
|
"port": 8137,
|
|
9
13
|
"autoPort": true
|
|
10
14
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# External-project adapter starter
|
|
2
|
+
|
|
3
|
+
This invented example uses MNSTRY Atelier. The adapter owns configuration and
|
|
4
|
+
a sample extension pack; the separate source repository owns canonical text.
|
|
5
|
+
Runs, answers and review history remain ignored local state. Public audience
|
|
6
|
+
labels do not grant publication, consent or runtime access.
|
|
7
|
+
|
|
8
|
+
Create this scaffold with `atelier init --template external-project --target DIR`.
|
|
9
|
+
The target must not exist. No Git repository, hook or remote is created.
|
|
10
|
+
Initialize the adapter and its source directory as separate repositories when
|
|
11
|
+
ready; the adapter ignores `source/`. To connect an existing repository instead,
|
|
12
|
+
pass `--repo-path source=PATH` consistently, or use an ignored local overlay.
|
|
13
|
+
Keep domain vocabulary and methods in your own adapter and source repositories.
|
|
14
|
+
Replace the synthetic actor in the boundary policy with your actual policy.
|
|
15
|
+
|
|
16
|
+
Using the installed package's CLI, from the adapter directory:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
atelier lock provenance
|
|
20
|
+
atelier config check --explain
|
|
21
|
+
atelier extension-pack validate
|
|
22
|
+
atelier graph
|
|
23
|
+
atelier project
|
|
24
|
+
atelier readiness
|
|
25
|
+
atelier review packs
|
|
26
|
+
atelier review run sample.readiness:contract-gate --answers answers.example.json
|
|
27
|
+
atelier dev --review
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Open `/review` on the displayed local address. Use repository `source` and
|
|
31
|
+
`README.md` to leave a question. Use the returned run ID to review its claims.
|
|
32
|
+
The evaluator measures answer completeness only. Read and judge the evidence;
|
|
33
|
+
acceptance creates a handoff, never a canonical edit. Use `atelier review history`
|
|
34
|
+
and `atelier review handoff REQUEST_ID` to inspect records locally.
|
|
35
|
+
|
|
36
|
+
Real answers and responses belong in ignored local state, not the tracked
|
|
37
|
+
example answer file. Reopening recovers saved contributions; missing or failed
|
|
38
|
+
saves remain visible. Stop/start the foreground sidecar with the same project
|
|
39
|
+
and reopen the same document. This does not establish cross-computer access.
|
|
40
|
+
|
|
41
|
+
For CI, compose the root's existing consumer, portability, boundary and
|
|
42
|
+
private-disclosure checks. No new CI run or provider is triggered by this template.
|
|
43
|
+
Before changing the pack, run `atelier upgrade --dry-run`, retain historical
|
|
44
|
+
pack content, update its lifecycle declaration and lock through your reviewed
|
|
45
|
+
migration process, and run the checks again. Never reinterpret old run evidence.
|