@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
|
@@ -5,9 +5,17 @@ import fs from 'node:fs'
|
|
|
5
5
|
import http from 'node:http'
|
|
6
6
|
import path from 'node:path'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import {
|
|
9
|
+
atomicReplacePrivateText,
|
|
10
|
+
ensureContainedPrivateDirectory,
|
|
11
|
+
readRegularTextNoFollow,
|
|
12
|
+
} from '../project/private-state.mjs'
|
|
8
13
|
import {
|
|
9
14
|
expectedOriginForRequest,
|
|
10
15
|
htmlDocumentHeaders,
|
|
16
|
+
isLoopbackHost,
|
|
17
|
+
loadPublishedWorkspaceManifest,
|
|
18
|
+
publishedStaticPathVerdict,
|
|
11
19
|
requestHeader,
|
|
12
20
|
resolveWorkspacePath,
|
|
13
21
|
staticHeaders,
|
|
@@ -22,6 +30,10 @@ import {
|
|
|
22
30
|
renderProposalListPageHtml,
|
|
23
31
|
} from '../ui/html-primitives.mjs'
|
|
24
32
|
|
|
33
|
+
import { createReviewStore } from '../collaboration/review-store.mjs'
|
|
34
|
+
import { loadBoundRun, currentRunEligibility } from '../readiness-protocols/evidence.mjs'
|
|
35
|
+
import { renderReviewPage } from '../ui/review-page.mjs'
|
|
36
|
+
|
|
25
37
|
export const ATELIER_CONTEXT_SCHEMA = 'atelier-context@v1'
|
|
26
38
|
export const ATELIER_PRESENCE_SCHEMA = 'atelier-presence@v1'
|
|
27
39
|
export const ATELIER_RESOLVE_SCHEMA = 'atelier-resolve@v1'
|
|
@@ -43,39 +55,37 @@ function nowMs() {
|
|
|
43
55
|
|
|
44
56
|
function readJsonFile(file, fallback) {
|
|
45
57
|
try {
|
|
46
|
-
return JSON.parse(
|
|
58
|
+
return JSON.parse(readRegularTextNoFollow(file))
|
|
47
59
|
} catch {
|
|
48
60
|
return fallback
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
function secureWriteJson(file, payload, mode = 0o600) {
|
|
53
|
-
|
|
54
|
-
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`
|
|
55
|
-
fs.writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`, { mode })
|
|
56
|
-
fs.renameSync(tmp, file)
|
|
57
|
-
try {
|
|
58
|
-
fs.chmodSync(file, mode)
|
|
59
|
-
} catch {
|
|
60
|
-
// Best effort on filesystems that do not support chmod.
|
|
61
|
-
}
|
|
65
|
+
atomicReplacePrivateText(file, `${JSON.stringify(payload, null, 2)}\n`, mode)
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
function readOrCreateNonce(noncePath) {
|
|
65
|
-
|
|
69
|
+
let existing = ''
|
|
70
|
+
if (fs.existsSync(noncePath)) {
|
|
71
|
+
const stat = fs.lstatSync(noncePath)
|
|
72
|
+
if (!stat.isFile()) throw new Error('nonce state leaf is not a regular file')
|
|
73
|
+
existing = readRegularTextNoFollow(noncePath).trim()
|
|
74
|
+
}
|
|
66
75
|
if (/^[a-f0-9]{64}$/.test(existing)) return existing
|
|
67
76
|
const nonce = crypto.randomBytes(32).toString('hex')
|
|
68
|
-
|
|
77
|
+
atomicReplacePrivateText(noncePath, `${nonce}\n`)
|
|
69
78
|
return nonce
|
|
70
79
|
}
|
|
71
80
|
|
|
72
|
-
function json(res, code, obj) {
|
|
81
|
+
function json(res, code, obj, headers = {}) {
|
|
73
82
|
const body = Buffer.from(`${JSON.stringify(obj, null, 2)}\n`)
|
|
74
83
|
res.writeHead(code, {
|
|
75
84
|
'Content-Type': 'application/json; charset=utf-8',
|
|
76
85
|
'Content-Length': String(body.length),
|
|
77
86
|
'Cache-Control': 'no-store',
|
|
78
87
|
'X-Content-Type-Options': 'nosniff',
|
|
88
|
+
...headers,
|
|
79
89
|
})
|
|
80
90
|
res.end(body)
|
|
81
91
|
}
|
|
@@ -200,21 +210,40 @@ function doctorEnvelope() {
|
|
|
200
210
|
export function createAtelierSidecarServer({
|
|
201
211
|
workspaceRoot = process.cwd(),
|
|
202
212
|
stateDir = workspaceRoot,
|
|
213
|
+
reviewProject = null,
|
|
203
214
|
port = 0,
|
|
204
215
|
presenceTtlMs = Number(process.env.ATELIER_PRESENCE_TTL_MS || 5 * 60 * 1000),
|
|
205
216
|
} = {}) {
|
|
206
217
|
const root = path.resolve(workspaceRoot)
|
|
207
218
|
const rootReal = fs.realpathSync(root)
|
|
219
|
+
const stateDirReal = ensureContainedPrivateDirectory({
|
|
220
|
+
workspaceRoot: root,
|
|
221
|
+
directory: stateDir,
|
|
222
|
+
label: 'Atelier state directory',
|
|
223
|
+
})
|
|
224
|
+
const publication = loadPublishedWorkspaceManifest(root)
|
|
225
|
+
const review = reviewProject ? createReviewStore(reviewProject) : null
|
|
208
226
|
const workspaceId = workspaceIdForRoot(root)
|
|
209
|
-
const noncePath = path.join(
|
|
210
|
-
const presencePath = path.join(
|
|
227
|
+
const noncePath = path.join(stateDirReal, '.atelier-nonce')
|
|
228
|
+
const presencePath = path.join(stateDirReal, '.atelier-presence.json')
|
|
211
229
|
const mutationNonce = readOrCreateNonce(noncePath)
|
|
212
230
|
const proposals = createProposalStore({
|
|
213
231
|
workspaceRoot: root,
|
|
214
|
-
proposalsDir: path.join(
|
|
232
|
+
proposalsDir: path.join(stateDirReal, '.atelier-proposals'),
|
|
215
233
|
workspaceId,
|
|
216
234
|
})
|
|
217
235
|
|
|
236
|
+
function resolvePublishedPath({ rel, requireFile = false, requireHtml = false } = {}) {
|
|
237
|
+
const resolved = resolveWorkspacePath({
|
|
238
|
+
workspaceRoot: root,
|
|
239
|
+
workspaceRootReal: rootReal,
|
|
240
|
+
rel,
|
|
241
|
+
requireFile,
|
|
242
|
+
requireHtml,
|
|
243
|
+
})
|
|
244
|
+
return publishedStaticPathVerdict({ resolved, publication })
|
|
245
|
+
}
|
|
246
|
+
|
|
218
247
|
function readPresence() {
|
|
219
248
|
const presence = readJsonFile(presencePath, defaultPresence())
|
|
220
249
|
return {
|
|
@@ -229,12 +258,7 @@ export function createAtelierSidecarServer({
|
|
|
229
258
|
}
|
|
230
259
|
|
|
231
260
|
function recordView(rel, hints = {}) {
|
|
232
|
-
const resolved =
|
|
233
|
-
workspaceRoot: root,
|
|
234
|
-
workspaceRootReal: rootReal,
|
|
235
|
-
rel,
|
|
236
|
-
requireHtml: true,
|
|
237
|
-
})
|
|
261
|
+
const resolved = resolvePublishedPath({ rel, requireHtml: true })
|
|
238
262
|
if (!resolved.ok) return { ok: false, error: 'unknown workspace html file' }
|
|
239
263
|
const entry = {
|
|
240
264
|
sessionId: cleanIdentity(hints.sessionId, 120),
|
|
@@ -295,12 +319,7 @@ export function createAtelierSidecarServer({
|
|
|
295
319
|
const viewId = cleanIdentity(url.searchParams.get('viewId'), 120)
|
|
296
320
|
const rel = cleanIdentity(url.searchParams.get('path') || 'index.html', 500)
|
|
297
321
|
const expectedWorkspaceId = cleanIdentity(url.searchParams.get('expectedWorkspaceId'), 120)
|
|
298
|
-
const resolved =
|
|
299
|
-
workspaceRoot: root,
|
|
300
|
-
workspaceRootReal: rootReal,
|
|
301
|
-
rel,
|
|
302
|
-
requireHtml: true,
|
|
303
|
-
})
|
|
322
|
+
const resolved = resolvePublishedPath({ rel, requireHtml: true })
|
|
304
323
|
const current = resolveCurrentContext({ sessionId })
|
|
305
324
|
const workspaceVerified = Boolean(expectedWorkspaceId && expectedWorkspaceId === workspaceId)
|
|
306
325
|
const caps = capabilityContract()
|
|
@@ -353,12 +372,31 @@ export function createAtelierSidecarServer({
|
|
|
353
372
|
json(res, 400, { ok: false, error: error.message })
|
|
354
373
|
return
|
|
355
374
|
}
|
|
375
|
+
// Browsers send Origin on POST; same-origin GET normally omits it. Keep
|
|
376
|
+
// the strict origin gate for nonce disclosure instead of relaxing the
|
|
377
|
+
// existing session-auth GET contract or accepting unclassified reads.
|
|
378
|
+
if (review && url.pathname === '/api/review/session') {
|
|
379
|
+
if (!trustedMutationRequest(req.headers) || !body || Array.isArray(body) || Object.keys(body).length) {
|
|
380
|
+
json(res, 403, { ok: false, error: 'review session requires a same-origin empty request' })
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
const published = resolvePublishedPath({ rel: 'index.html', requireHtml: true })
|
|
384
|
+
if (!published.ok) { json(res, 404, { ok: false, error: 'published workspace unavailable' }); return }
|
|
385
|
+
json(res, 200, { ok: true, schema: ATELIER_SESSION_AUTH_SCHEMA, workspaceId, mutationNonce })
|
|
386
|
+
return
|
|
387
|
+
}
|
|
356
388
|
const auth = requireMutationAuth(req, body, mutationNonce)
|
|
357
389
|
if (!auth.ok) {
|
|
358
390
|
json(res, auth.status, { ok: false, error: auth.error })
|
|
359
391
|
return
|
|
360
392
|
}
|
|
361
393
|
|
|
394
|
+
if (review && url.pathname === '/api/review/contributions') {
|
|
395
|
+
const result = review.contribute(body)
|
|
396
|
+
json(res, result.status, result)
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
|
|
362
400
|
if (url.pathname === '/api/seen') {
|
|
363
401
|
const result = recordView(body.rel || body.path, body)
|
|
364
402
|
json(res, 200, result.ok ? { ok: true, entry: result.entry } : result)
|
|
@@ -366,12 +404,7 @@ export function createAtelierSidecarServer({
|
|
|
366
404
|
}
|
|
367
405
|
|
|
368
406
|
if (url.pathname === '/api/proposals') {
|
|
369
|
-
const resolved =
|
|
370
|
-
workspaceRoot: root,
|
|
371
|
-
workspaceRootReal: rootReal,
|
|
372
|
-
rel: body.path || body.rel,
|
|
373
|
-
requireHtml: true,
|
|
374
|
-
})
|
|
407
|
+
const resolved = resolvePublishedPath({ rel: body.path || body.rel, requireHtml: true })
|
|
375
408
|
if (!resolved.ok) {
|
|
376
409
|
json(res, 200, { ok: false, error: 'unknown workspace html file' })
|
|
377
410
|
return
|
|
@@ -392,6 +425,7 @@ export function createAtelierSidecarServer({
|
|
|
392
425
|
schema: result.record.schema,
|
|
393
426
|
workspaceId,
|
|
394
427
|
proposal: result.record.proposal,
|
|
428
|
+
diagnostics: result.diagnostics,
|
|
395
429
|
})
|
|
396
430
|
return
|
|
397
431
|
}
|
|
@@ -416,6 +450,7 @@ export function createAtelierSidecarServer({
|
|
|
416
450
|
proposal: result.record.proposal,
|
|
417
451
|
diff: result.record.diff,
|
|
418
452
|
copyable: result.record.copyable,
|
|
453
|
+
diagnostics: result.diagnostics,
|
|
419
454
|
})
|
|
420
455
|
return
|
|
421
456
|
}
|
|
@@ -430,12 +465,7 @@ export function createAtelierSidecarServer({
|
|
|
430
465
|
}
|
|
431
466
|
|
|
432
467
|
const rel = url.pathname === '/' ? 'index.html' : url.pathname.slice(1)
|
|
433
|
-
const resolved =
|
|
434
|
-
workspaceRoot: root,
|
|
435
|
-
workspaceRootReal: rootReal,
|
|
436
|
-
rel,
|
|
437
|
-
requireFile: true,
|
|
438
|
-
})
|
|
468
|
+
const resolved = resolvePublishedPath({ rel, requireFile: true })
|
|
439
469
|
if (!resolved.ok) {
|
|
440
470
|
text(res, resolved.status || 403, `${resolved.error || 'forbidden'}\n`)
|
|
441
471
|
return
|
|
@@ -463,11 +493,40 @@ export function createAtelierSidecarServer({
|
|
|
463
493
|
return
|
|
464
494
|
}
|
|
465
495
|
|
|
496
|
+
if (!['GET', 'HEAD', 'POST'].includes(req.method || '')) {
|
|
497
|
+
json(res, 405, { ok: false, error: 'method not allowed' }, { Allow: 'GET, HEAD, POST' })
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (!trustedReadRequest(req.headers, { expectedOrigin })) {
|
|
502
|
+
json(res, 403, { ok: false, error: 'cross-origin or unclassified request refused' })
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
|
|
466
506
|
if (req.method === 'POST') {
|
|
467
507
|
await handlePost(req, res, url)
|
|
468
508
|
return
|
|
469
509
|
}
|
|
470
510
|
|
|
511
|
+
if (review && url.pathname === '/review') {
|
|
512
|
+
const body = renderReviewPage()
|
|
513
|
+
res.writeHead(200, htmlDocumentHeaders(Buffer.byteLength(body)))
|
|
514
|
+
res.end(body)
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
if (review && url.pathname.startsWith('/api/review/')) {
|
|
518
|
+
let result
|
|
519
|
+
if (url.pathname === '/api/review/records') result = review.records()
|
|
520
|
+
else if (url.pathname === '/api/review/document') result = review.document(url.searchParams.get('repo'), url.searchParams.get('path'))
|
|
521
|
+
else if (url.pathname === '/api/review/run') {
|
|
522
|
+
const bound = loadBoundRun(reviewProject, url.searchParams.get('id'))
|
|
523
|
+
result = bound.ok ? {...bound, current:currentRunEligibility(reviewProject,bound)} : bound
|
|
524
|
+
} else if (url.pathname === '/api/review/handoff') result = review.handoff(url.searchParams.get('id'))
|
|
525
|
+
else result = {ok:false,status:404,error:'unknown review action'}
|
|
526
|
+
json(res, result.status, result)
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
|
|
471
530
|
if (url.pathname === '/api/health') {
|
|
472
531
|
json(res, 200, { ok: true, workspaceId })
|
|
473
532
|
return
|
|
@@ -478,12 +537,7 @@ export function createAtelierSidecarServer({
|
|
|
478
537
|
return
|
|
479
538
|
}
|
|
480
539
|
const rel = url.searchParams.get('path') || 'index.html'
|
|
481
|
-
const resolved =
|
|
482
|
-
workspaceRoot: root,
|
|
483
|
-
workspaceRootReal: rootReal,
|
|
484
|
-
rel,
|
|
485
|
-
requireHtml: true,
|
|
486
|
-
})
|
|
540
|
+
const resolved = resolvePublishedPath({ rel, requireHtml: true })
|
|
487
541
|
if (!resolved.ok) {
|
|
488
542
|
json(res, 404, { ok: false, error: 'unknown workspace html file' })
|
|
489
543
|
return
|
|
@@ -509,12 +563,7 @@ export function createAtelierSidecarServer({
|
|
|
509
563
|
return
|
|
510
564
|
}
|
|
511
565
|
if (url.pathname === '/api/resolve') {
|
|
512
|
-
const resolved =
|
|
513
|
-
workspaceRoot: root,
|
|
514
|
-
workspaceRootReal: rootReal,
|
|
515
|
-
rel: url.searchParams.get('path') || 'index.html',
|
|
516
|
-
requireHtml: true,
|
|
517
|
-
})
|
|
566
|
+
const resolved = resolvePublishedPath({ rel: url.searchParams.get('path') || 'index.html', requireHtml: true })
|
|
518
567
|
json(res, resolved.ok ? 200 : (resolved.status || 404), {
|
|
519
568
|
ok: resolved.ok,
|
|
520
569
|
schema: ATELIER_RESOLVE_SCHEMA,
|
|
@@ -533,37 +582,49 @@ export function createAtelierSidecarServer({
|
|
|
533
582
|
return
|
|
534
583
|
}
|
|
535
584
|
if (url.pathname === '/api/proposals') {
|
|
585
|
+
const listed = proposals.listProposals()
|
|
586
|
+
if (!listed.ok) {
|
|
587
|
+
json(res, listed.status, { ok: false, error: listed.error, diagnostics: listed.diagnostics })
|
|
588
|
+
return
|
|
589
|
+
}
|
|
536
590
|
json(res, 200, {
|
|
537
591
|
ok: true,
|
|
538
592
|
schema: 'atelier-proposals@v1',
|
|
539
593
|
workspaceId,
|
|
540
|
-
proposals: proposals
|
|
594
|
+
proposals: listed.proposals,
|
|
541
595
|
})
|
|
542
596
|
return
|
|
543
597
|
}
|
|
544
598
|
const proposalReadMatch = url.pathname.match(/^\/api\/proposals\/([^/]+)$/)
|
|
545
599
|
if (proposalReadMatch) {
|
|
546
|
-
const
|
|
547
|
-
|
|
600
|
+
const result = proposals.readProposal(proposalReadMatch[1])
|
|
601
|
+
const record = result.record
|
|
602
|
+
json(res, result.ok ? 200 : result.status, result.ok ? {
|
|
548
603
|
ok: true,
|
|
549
604
|
schema: record.schema,
|
|
550
605
|
workspaceId,
|
|
551
606
|
proposal: record.proposal,
|
|
552
607
|
diff: record.diff,
|
|
553
608
|
copyable: record.copyable,
|
|
554
|
-
} : { ok: false, error:
|
|
609
|
+
} : { ok: false, error: result.error, diagnostics: result.diagnostics })
|
|
555
610
|
return
|
|
556
611
|
}
|
|
557
612
|
if (url.pathname === '/proposals') {
|
|
558
|
-
const
|
|
613
|
+
const listed = proposals.listProposals()
|
|
614
|
+
if (!listed.ok) {
|
|
615
|
+
text(res, listed.status, `${listed.error}\n`)
|
|
616
|
+
return
|
|
617
|
+
}
|
|
618
|
+
const body = renderProposalListPageHtml(listed.proposals)
|
|
559
619
|
res.writeHead(200, htmlDocumentHeaders(Buffer.byteLength(body)))
|
|
560
620
|
res.end(body)
|
|
561
621
|
return
|
|
562
622
|
}
|
|
563
623
|
const proposalPageMatch = url.pathname.match(/^\/proposals\/([^/]+)$/)
|
|
564
624
|
if (proposalPageMatch) {
|
|
565
|
-
const
|
|
566
|
-
|
|
625
|
+
const result = proposals.readProposal(proposalPageMatch[1])
|
|
626
|
+
const body = renderProposalDetailPageHtml(result.record)
|
|
627
|
+
res.writeHead(result.ok ? 200 : result.status, htmlDocumentHeaders(Buffer.byteLength(body)))
|
|
567
628
|
res.end(body)
|
|
568
629
|
return
|
|
569
630
|
}
|
|
@@ -595,8 +656,12 @@ export function createAtelierSidecarServer({
|
|
|
595
656
|
workspaceRootReal: rootReal,
|
|
596
657
|
workspaceId,
|
|
597
658
|
mutationNonce,
|
|
659
|
+
publication,
|
|
598
660
|
proposals,
|
|
599
661
|
listen(listenPort = port, host = '127.0.0.1') {
|
|
662
|
+
if (!isLoopbackHost(host)) {
|
|
663
|
+
return Promise.reject(new Error(`non-loopback listen host refused: ${host}`))
|
|
664
|
+
}
|
|
600
665
|
// A busy port is an ordinary condition, not a crash: surface it as a
|
|
601
666
|
// rejection the CLI can print, instead of an unhandled 'error' event.
|
|
602
667
|
return new Promise((resolve, reject) => {
|
|
@@ -615,6 +680,7 @@ export function createAtelierSidecarServer({
|
|
|
615
680
|
})
|
|
616
681
|
},
|
|
617
682
|
close() {
|
|
683
|
+
if (!server.listening) return Promise.resolve()
|
|
618
684
|
return new Promise((resolve, reject) => {
|
|
619
685
|
server.close((error) => {
|
|
620
686
|
if (error) reject(error)
|
package/src/server/security.mjs
CHANGED
|
@@ -37,6 +37,9 @@ export const LOOPBACK_HOSTS = new Set([
|
|
|
37
37
|
'[::1]',
|
|
38
38
|
])
|
|
39
39
|
|
|
40
|
+
export const ATELIER_MANIFEST_SCHEMA = 'mnstry.atelier-manifest@v1'
|
|
41
|
+
export const PUBLISHED_STATIC_EXTENSIONS = new Set(['.html', '.js', '.mjs', '.css', '.svg', '.png', '.jpg', '.jpeg', '.webp'])
|
|
42
|
+
|
|
40
43
|
export function parseHostHeader(value) {
|
|
41
44
|
const raw = String(value || '').trim().toLowerCase()
|
|
42
45
|
if (!raw) return ''
|
|
@@ -66,7 +69,7 @@ export function expectedOriginForRequest(headers, fallbackPort = null) {
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export function sameOrigin(origin, expectedOrigin) {
|
|
69
|
-
if (!origin) return
|
|
72
|
+
if (!origin) return false
|
|
70
73
|
if (!expectedOrigin) return false
|
|
71
74
|
try {
|
|
72
75
|
return new URL(origin).origin === new URL(expectedOrigin).origin
|
|
@@ -77,7 +80,7 @@ export function sameOrigin(origin, expectedOrigin) {
|
|
|
77
80
|
|
|
78
81
|
export function trustedFetchSite(headers) {
|
|
79
82
|
const value = requestHeader(headers, 'sec-fetch-site').toLowerCase()
|
|
80
|
-
return value === '
|
|
83
|
+
return value === 'none' || value === 'same-origin'
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
export function trustedHost(headers) {
|
|
@@ -85,11 +88,92 @@ export function trustedHost(headers) {
|
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
export function trustedReadRequest(headers, { expectedOrigin = expectedOriginForRequest(headers) } = {}) {
|
|
88
|
-
|
|
91
|
+
const origin = requestHeader(headers, 'origin')
|
|
92
|
+
return trustedHost(headers) && trustedFetchSite(headers) && (!origin || sameOrigin(origin, expectedOrigin))
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
export function trustedMutationRequest(headers, { expectedOrigin = expectedOriginForRequest(headers) } = {}) {
|
|
92
|
-
|
|
96
|
+
const origin = requestHeader(headers, 'origin')
|
|
97
|
+
return Boolean(origin) && trustedReadRequest(headers, { expectedOrigin }) && sameOrigin(origin, expectedOrigin)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function manifestPathValue(value) {
|
|
101
|
+
if (typeof value === 'string') return value
|
|
102
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return ''
|
|
103
|
+
return value.path || value.output || value.href || ''
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function secretShapedPath(value) {
|
|
107
|
+
return /(?:^|[._-])(?:secret|credential|token|password|nonce|private[._-]?key)(?:$|[._-])/i.test(value)
|
|
108
|
+
|| /(?:^|\/)\.env(?:\.|$)/i.test(value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function addManifestPath(paths, value, label) {
|
|
112
|
+
const candidate = manifestPathValue(value)
|
|
113
|
+
const normalized = normalizeWorkspacePath(candidate, { defaultFile: '' })
|
|
114
|
+
if (!candidate || !normalized.ok || /^[a-z][a-z0-9+.-]*:/i.test(candidate)) {
|
|
115
|
+
throw new Error(`atelier.manifest.json ${label} must be a workspace-relative path`)
|
|
116
|
+
}
|
|
117
|
+
if (deniedStaticStatePath(normalized.path) || normalized.path.split('/').some((segment) => segment.startsWith('.')) || secretShapedPath(normalized.path)) {
|
|
118
|
+
throw new Error(`atelier.manifest.json ${label} enrolls a hidden, state, or secret-shaped path`)
|
|
119
|
+
}
|
|
120
|
+
if (!PUBLISHED_STATIC_EXTENSIONS.has(path.extname(normalized.path).toLowerCase())) {
|
|
121
|
+
throw new Error(`atelier.manifest.json ${label} enrolls an unsupported static file type`)
|
|
122
|
+
}
|
|
123
|
+
paths.add(normalized.path)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function loadPublishedWorkspaceManifest(workspaceRoot) {
|
|
127
|
+
const root = path.resolve(workspaceRoot)
|
|
128
|
+
const rootReal = fs.realpathSync(root)
|
|
129
|
+
const manifestPath = path.join(root, 'atelier.manifest.json')
|
|
130
|
+
if (!fs.existsSync(manifestPath)) throw new Error('served workspace must contain generated atelier.manifest.json')
|
|
131
|
+
const manifestReal = fs.realpathSync(manifestPath)
|
|
132
|
+
if (!pathContainedBy(rootReal, manifestReal)) throw new Error('atelier.manifest.json realpath escapes served workspace')
|
|
133
|
+
let manifest
|
|
134
|
+
try {
|
|
135
|
+
manifest = JSON.parse(fs.readFileSync(manifestReal, 'utf8'))
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new Error(`atelier.manifest.json must be valid JSON: ${error.message}`)
|
|
138
|
+
}
|
|
139
|
+
if (manifest?.schema !== ATELIER_MANIFEST_SCHEMA) {
|
|
140
|
+
throw new Error(`atelier.manifest.json schema must be ${ATELIER_MANIFEST_SCHEMA}`)
|
|
141
|
+
}
|
|
142
|
+
const paths = new Set()
|
|
143
|
+
if (typeof manifest.entry !== 'string') throw new Error('atelier.manifest.json entry must be a workspace-relative HTML path')
|
|
144
|
+
addManifestPath(paths, manifest.entry, 'entry')
|
|
145
|
+
if (path.extname(manifest.entry).toLowerCase() !== '.html') throw new Error('atelier.manifest.json entry must be an HTML file')
|
|
146
|
+
for (const key of ['files', 'assets']) {
|
|
147
|
+
if (manifest[key] == null) continue
|
|
148
|
+
if (!Array.isArray(manifest[key])) throw new Error(`atelier.manifest.json ${key} must be an array`)
|
|
149
|
+
for (const [index, value] of manifest[key].entries()) addManifestPath(paths, value, `${key}[${index}]`)
|
|
150
|
+
}
|
|
151
|
+
for (const rel of paths) {
|
|
152
|
+
const resolved = resolveWorkspacePath({ workspaceRoot: root, workspaceRootReal: rootReal, rel, requireFile: true })
|
|
153
|
+
if (!resolved.ok) throw new Error(`atelier.manifest.json enrolled path is unavailable: ${rel}`)
|
|
154
|
+
const realRel = path.relative(rootReal, resolved.real).replaceAll('\\', '/')
|
|
155
|
+
if (realRel !== rel) throw new Error(`atelier.manifest.json enrolled path must not be a symlink: ${rel}`)
|
|
156
|
+
}
|
|
157
|
+
return Object.freeze({ manifest, manifestPath: manifestReal, paths })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function publishedStaticPathVerdict({ resolved, publication } = {}) {
|
|
161
|
+
if (!resolved?.ok) return resolved
|
|
162
|
+
const realRel = path.relative(resolved.workspaceRootReal, resolved.real).replaceAll('\\', '/')
|
|
163
|
+
const segments = realRel.split('/')
|
|
164
|
+
if (segments.some((segment) => segment.startsWith('.')) || deniedStaticStatePath(realRel)) {
|
|
165
|
+
return { ok: false, status: 403, error: 'hidden or local state path is not published', path: resolved.path }
|
|
166
|
+
}
|
|
167
|
+
if (realRel === 'atelier.manifest.json' || secretShapedPath(realRel)) {
|
|
168
|
+
return { ok: false, status: 403, error: 'secret-shaped path is not published', path: resolved.path }
|
|
169
|
+
}
|
|
170
|
+
if (!PUBLISHED_STATIC_EXTENSIONS.has(path.extname(realRel).toLowerCase())) {
|
|
171
|
+
return { ok: false, status: 403, error: 'static file type is not published', path: resolved.path }
|
|
172
|
+
}
|
|
173
|
+
if (!publication?.paths?.has(realRel)) {
|
|
174
|
+
return { ok: false, status: 404, error: 'path is not enrolled by atelier.manifest.json', path: resolved.path }
|
|
175
|
+
}
|
|
176
|
+
return resolved
|
|
93
177
|
}
|
|
94
178
|
|
|
95
179
|
export function pathContainedBy(root, candidate) {
|
|
@@ -191,6 +275,7 @@ export function resolveWorkspacePath({
|
|
|
191
275
|
real,
|
|
192
276
|
stat,
|
|
193
277
|
workspaceContained: true,
|
|
278
|
+
workspaceRootReal,
|
|
194
279
|
}
|
|
195
280
|
}
|
|
196
281
|
|
package/src/server/server.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export async function runServerCommand(argv = process.argv.slice(2)) {
|
|
|
17
17
|
const sidecar = createAtelierSidecarServer({
|
|
18
18
|
workspaceRoot: project.outputRoot,
|
|
19
19
|
stateDir: project.outputRoot,
|
|
20
|
+
reviewProject: argv.includes('--review') ? project : null,
|
|
20
21
|
// argv > PORT env > default. Supervisors that assign a free port (preview
|
|
21
22
|
// panes, dev harnesses) pass it via PORT, and an Atelier that ignores that
|
|
22
23
|
// fights whatever already holds the canonical port instead of coexisting.
|
|
@@ -26,11 +27,12 @@ export async function runServerCommand(argv = process.argv.slice(2)) {
|
|
|
26
27
|
if (args.smoke) {
|
|
27
28
|
const address = await sidecar.listen()
|
|
28
29
|
const base = `http://127.0.0.1:${address.port}`
|
|
30
|
+
const headers = { Origin: base, 'Sec-Fetch-Site': 'same-origin' }
|
|
29
31
|
// @atelier-egress-local-computed
|
|
30
|
-
const health = await fetch(`${base}/api/health
|
|
32
|
+
const health = await fetch(`${base}/api/health`, { headers }).then((res) => res.json())
|
|
31
33
|
if (!health.ok) throw new Error('health check failed')
|
|
32
34
|
// @atelier-egress-local-computed
|
|
33
|
-
const page = await fetch(`${base}
|
|
35
|
+
const page = await fetch(`${base}/`, { headers }).then((res) => res.text())
|
|
34
36
|
if (!page.includes('<meta name="mnstry:atelier"')) throw new Error('projection smoke failed')
|
|
35
37
|
await sidecar.close()
|
|
36
38
|
console.log('[atelier:browser:smoke] local projection and health endpoint passed')
|