@mnstry/atelier 0.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +383 -0
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +262 -0
- package/SECURITY.md +93 -0
- package/TRADEMARKS.md +88 -0
- package/announcements/2026-08-06-announcements-channel.v1.json +12 -0
- package/announcements/keys/mnstry-announcements.public.v1.json +9 -0
- package/bin/atelier.mjs +2 -0
- package/bin/mnstry-atelier.mjs +3 -0
- package/contracts/analysis-adapter.v1.schema.json +248 -0
- package/contracts/atelier-action-intent.v1.schema.json +143 -0
- package/contracts/atelier-analysis-adapter.v1.schema.json +360 -0
- package/contracts/atelier-attestation.v1.schema.json +239 -0
- package/contracts/atelier-boundary-policy.v1.schema.json +339 -0
- package/contracts/atelier-claim.v1.schema.json +97 -0
- package/contracts/atelier-export.v1.schema.json +1015 -0
- package/contracts/atelier-extension-pack.v1.schema.json +290 -0
- package/contracts/atelier-kit-manifest.v1.schema.json +273 -0
- package/contracts/atelier-lock.v1.schema.json +364 -0
- package/contracts/atelier-migration.v1.schema.json +162 -0
- package/contracts/atelier-project-config.v1.schema.json +352 -0
- package/contracts/atelier-readiness-protocol.v1.schema.json +517 -0
- package/contracts/atelier-readiness-run.v1.schema.json +229 -0
- package/contracts/atelier-readiness.v1.schema.json +457 -0
- package/contracts/compat-baseline.json +10 -0
- package/contracts/git-promote-event.v1.schema.json +145 -0
- package/contracts/knowledge-source-sidecar.v1.schema.json +157 -0
- package/contracts/mnstry-atelier-semantic-profile.v1.json +254 -0
- package/docs/announcements.md +101 -0
- package/docs/atelier-runtime.md +34 -0
- package/docs/attestation.md +201 -0
- package/docs/continuity.md +55 -0
- package/docs/contract-stability.md +111 -0
- package/docs/distributions.md +220 -0
- package/docs/install.md +140 -0
- package/docs/knowledge-graph.md +140 -0
- package/docs/ontology.md +206 -0
- package/docs/release-engineering.md +151 -0
- package/docs/repo-boundary-guard.md +199 -0
- package/docs/tenant-readiness.md +64 -0
- package/docs/upgrade.md +124 -0
- package/fixtures/analysis/invalid/native-graph.v1.json +4 -0
- package/fixtures/analysis/invalid/promoted-claim.v1.json +17 -0
- package/fixtures/analysis/invalid/runtime-import.v1.json +20 -0
- package/fixtures/analysis/valid/output.claims.v1.json +17 -0
- package/fixtures/analysis-adapter/invalid/manifest-hidden-provider.v1.json +24 -0
- package/fixtures/analysis-adapter/invalid/output-frontmatter-mutation.v1.json +21 -0
- package/fixtures/analysis-adapter/invalid/output-native-graph.v1.json +14 -0
- package/fixtures/analysis-adapter/manifest.disabled.v1.json +22 -0
- package/fixtures/analysis-adapter/manifest.enabled-local.v1.json +23 -0
- package/fixtures/analysis-adapter/output.claims.v1.json +21 -0
- package/fixtures/atelier-action-intent/invalid/runtime-mutation-authority.v1.json +16 -0
- package/fixtures/atelier-action-intent/invalid/unknown-top-level-field.v1.json +17 -0
- package/fixtures/atelier-action-intent/invalid/writes-beyond-clipboard.v1.json +16 -0
- package/fixtures/atelier-action-intent/valid/copy-agent-prompt.v1.json +16 -0
- package/fixtures/atelier-action-intent/valid/copy-repo-path.v1.json +22 -0
- package/fixtures/atelier-analysis-adapter/invalid/hidden-model-provider.v1.json +24 -0
- package/fixtures/atelier-analysis-adapter/invalid/inert-with-execute-command.v1.json +24 -0
- package/fixtures/atelier-analysis-adapter/invalid/proposals-with-runtime-import.v1.json +30 -0
- package/fixtures/atelier-analysis-adapter/valid/manifest-inert.v1.json +24 -0
- package/fixtures/atelier-analysis-adapter/valid/proposals-claim-only.v1.json +27 -0
- package/fixtures/atelier-attestation/invalid/bad-digest.v1.json +23 -0
- package/fixtures/atelier-attestation/invalid/conformance-scope.v1.json +23 -0
- package/fixtures/atelier-attestation/invalid/missing-signature.v1.json +22 -0
- package/fixtures/atelier-attestation/invalid/unknown-root-field.v1.json +24 -0
- package/fixtures/atelier-attestation/keys/roundtrip-issuer.public.v1.json +9 -0
- package/fixtures/atelier-attestation/valid/rejected-with-reasons.v1.json +49 -0
- package/fixtures/atelier-attestation/valid/signed-admitted.v1.json +35 -0
- package/fixtures/atelier-attestation/valid/signed-roundtrip.v1.json +29 -0
- package/fixtures/atelier-attestation/valid/unsigned-advisory.v1.json +23 -0
- package/fixtures/atelier-claim/invalid/unknown-provider.v1.json +15 -0
- package/fixtures/atelier-claim/invalid/unknown-top-level-field.v1.json +16 -0
- package/fixtures/atelier-claim/invalid/wrong-schema-const.v1.json +15 -0
- package/fixtures/atelier-claim/valid/adapter-style-proposed-relation.v1.json +19 -0
- package/fixtures/atelier-claim/valid/codex-proposed-relation.v1.json +16 -0
- package/fixtures/atelier-claim/valid/manual-proposed-relation.v1.json +16 -0
- package/fixtures/atelier-claim/valid/readiness-proposed-relation.v1.json +18 -0
- package/fixtures/atelier-export/invalid/dirty-unforced-empty-taint.v1.json +19 -0
- package/fixtures/atelier-export/invalid/object-local-audience-visibility.v1.json +11 -0
- package/fixtures/atelier-export/invalid/open-object-class-owner.v1.json +23 -0
- package/fixtures/atelier-export/invalid/pricing-legacy-amount.v1.json +23 -0
- package/fixtures/atelier-export/invalid/public-disguised-internal-source-ref.v1.json +11 -0
- package/fixtures/atelier-export/invalid/public-nested-private-source-ref.v1.json +17 -0
- package/fixtures/atelier-export/invalid/public-resolved-private-source-ref.v1.json +13 -0
- package/fixtures/atelier-export/invalid/public-resolved-sensitive-source-ref.v1.json +13 -0
- package/fixtures/atelier-export/invalid/public-source-ref-missing-provenance.v1.json +20 -0
- package/fixtures/atelier-export/invalid/public-source-team-kg.json +12 -0
- package/fixtures/atelier-export/invalid/source-node-legacy-visibility.v1.json +21 -0
- package/fixtures/atelier-export/invalid/source-node-unknown-field.v1.json +18 -0
- package/fixtures/atelier-export/invalid/unresolved-public-source-ref.v1.json +11 -0
- package/fixtures/atelier-export/sample-studio-offer.v1.json +397 -0
- package/fixtures/atelier-extension-pack/invalid/bundled-slug-collision.v1.json +22 -0
- package/fixtures/atelier-extension-pack/invalid/escapes-root.v1.json +22 -0
- package/fixtures/atelier-extension-pack/invalid/posture-tampered.v1.json +22 -0
- package/fixtures/atelier-extension-pack/invalid/protocols/autonomous-authority.v1.json +118 -0
- package/fixtures/atelier-extension-pack/invalid/protocols/offer-map.v1.json +109 -0
- package/fixtures/atelier-extension-pack/invalid/redefines-runtime-term.v1.json +19 -0
- package/fixtures/atelier-extension-pack/invalid/wrong-namespace.v1.json +19 -0
- package/fixtures/atelier-extension-pack/valid/protocols/contract-gate.v1.json +109 -0
- package/fixtures/atelier-extension-pack/valid/sample-pack.v1.json +48 -0
- package/fixtures/atelier-kit-manifest/invalid/runtime-copy-without-canonical-role.v1.json +53 -0
- package/fixtures/atelier-kit-manifest/invalid/telemetry-send-path.v1.json +21 -0
- package/fixtures/atelier-kit-manifest/valid/mnstry-atelier.valid.v1.json +76 -0
- package/fixtures/atelier-lock/invalid/absolute-contract-path.v1.json +40 -0
- package/fixtures/atelier-lock/invalid/runtime-mutation-enabled.v1.json +41 -0
- package/fixtures/atelier-lock/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/atelier-lock/valid/extension-pack.v1.json +55 -0
- package/fixtures/atelier-lock/valid/minimal.v1.json +44 -0
- package/fixtures/atelier-migration/invalid/live-apply-mode.v1.json +21 -0
- package/fixtures/atelier-migration/invalid/missing-review-evidence.v1.json +20 -0
- package/fixtures/atelier-migration/invalid/runtime-mutation-enabled.v1.json +21 -0
- package/fixtures/atelier-migration/valid/minimal.v1.json +32 -0
- package/fixtures/atelier-project-config/invalid/absolute-repo-path.v1.json +11 -0
- package/fixtures/atelier-project-config/invalid/ext-not-object.v1.json +12 -0
- package/fixtures/atelier-project-config/invalid/external-kind-read-boundary.v1.json +12 -0
- package/fixtures/atelier-project-config/invalid/missing-repos-and-alignment.v1.json +7 -0
- package/fixtures/atelier-project-config/invalid/unknown-top-level-field.v1.json +14 -0
- package/fixtures/atelier-project-config/valid/alignment-scaffold.v1.json +15 -0
- package/fixtures/atelier-project-config/valid/ext-extension-packs.v1.json +36 -0
- package/fixtures/atelier-project-config/valid/repo-workspace.v1.json +23 -0
- package/fixtures/atelier-project-config/validator-only/ext-member-not-object.v1.json +14 -0
- package/fixtures/atelier-readiness/invalid/dry-run-only-false.v1.json +87 -0
- package/fixtures/atelier-readiness/invalid/support-bundle-send-path.v1.json +87 -0
- package/fixtures/atelier-readiness/invalid/unknown-top-level-field.v1.json +88 -0
- package/fixtures/atelier-readiness/valid/ready-workspace.v1.json +84 -0
- package/fixtures/boundary-policy/invalid/content-rule-exception-blanket-path.v1.json +67 -0
- package/fixtures/boundary-policy/invalid/invalid-actor-private-repo-mapping.v1.json +20 -0
- package/fixtures/boundary-policy/invalid/legacy-warning-missing-warning.v1.json +20 -0
- package/fixtures/boundary-policy/invalid/missing-repo-coverage.v1.json +19 -0
- package/fixtures/boundary-policy/invalid/unknown-key.v1.json +12 -0
- package/fixtures/boundary-policy/valid/content-rule-exception.v1.json +67 -0
- package/fixtures/boundary-policy/valid/legacy-warning.v1.json +33 -0
- package/fixtures/boundary-policy/valid/strict-private-shared.v1.json +40 -0
- package/fixtures/git-promote/invalid/provision-named-revocable.v1.json +29 -0
- package/fixtures/git-promote/private-to-team.v1.json +29 -0
- package/fixtures/knowledge-source-sidecar/invalid/legacy-visibility-field.v1.json +17 -0
- package/fixtures/knowledge-source-sidecar/invalid/missing-audience.v1.json +15 -0
- package/fixtures/knowledge-source-sidecar/invalid/unknown-relation-kind.v1.json +18 -0
- package/fixtures/knowledge-source-sidecar/valid/html-source.v1.json +18 -0
- package/fixtures/knowledge-source-sidecar/valid/pdf-evidence.v1.json +18 -0
- package/fixtures/projects/neutral-project/atelier.project.json +15 -0
- package/fixtures/projects/neutral-project/ops/config/repo-access.v1.json +12 -0
- package/fixtures/projects/neutral-project/project-knowledge.graph.json +69 -0
- package/fixtures/projects/sample-workspace/atelier.project.json +23 -0
- package/fixtures/projects/sample-workspace/boundary-policy.v1.json +59 -0
- package/fixtures/projects/sample-workspace/content/README.md +16 -0
- package/fixtures/projects/sample-workspace/content/source.html +5 -0
- package/fixtures/projects/sample-workspace/content/source.html.kg.json +15 -0
- package/fixtures/projects/sample-workspace/repo-access.v1.json +9 -0
- package/fixtures/projects/source-formats-workspace/atelier.project.json +23 -0
- package/fixtures/projects/source-formats-workspace/content/README.md +17 -0
- package/fixtures/projects/source-formats-workspace/content/data.json +7 -0
- package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/content/metrics.csv +3 -0
- package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml +4 -0
- package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +15 -0
- package/fixtures/projects/source-formats-workspace/repo-access.v1.json +9 -0
- package/fixtures/readiness-protocols/extension-pack/invalid/protocol-missing-path.v1.json +20 -0
- package/fixtures/readiness-protocols/extension-pack/valid/with-protocol.v1.json +24 -0
- package/fixtures/readiness-protocols/protocol/invalid/missing-check-target.v1.json +103 -0
- package/fixtures/readiness-protocols/protocol/invalid/runtime-mutation-enabled.v1.json +107 -0
- package/fixtures/readiness-protocols/protocol/valid/minimal.v1.json +100 -0
- package/fixtures/readiness-protocols/run/invalid/missing-evidence.v1.json +37 -0
- package/fixtures/readiness-protocols/run/invalid/pass-with-blocker.v1.json +22 -0
- package/fixtures/readiness-protocols/run/invalid/runtime-mutation-enabled.v1.json +22 -0
- package/fixtures/readiness-protocols/run/valid/pass.v1.json +44 -0
- package/fixtures/support/invalid/leaky-support.v1.json +13 -0
- package/fixtures/support/valid/minimal-safe.v1.json +20 -0
- package/package.json +98 -0
- package/skills/claude/atelier-open/SKILL.md +68 -0
- package/skills/claude/mnstry-readiness/SKILL.md +41 -0
- package/skills/codex/atelier-open/SKILL.md +68 -0
- package/skills/codex/mnstry-readiness/SKILL.md +41 -0
- package/src/analysis/adapter.mjs +20 -0
- package/src/analysis/analysis-claim-contract.mjs +335 -0
- package/src/attestation/jcs.mjs +64 -0
- package/src/attestation/sign.mjs +305 -0
- package/src/boundary/content-rules.mjs +262 -0
- package/src/boundary/policy.mjs +806 -0
- package/src/check-atelier-export-contract.mjs +116 -0
- package/src/cli/run.mjs +283 -0
- package/src/collaboration/proposals.mjs +262 -0
- package/src/commands/analysis.mjs +8 -0
- package/src/commands/announcements.mjs +308 -0
- package/src/commands/attestation.mjs +198 -0
- package/src/commands/boundary.mjs +25 -0
- package/src/commands/config.mjs +4 -0
- package/src/commands/context.mjs +3 -0
- package/src/commands/distribution.mjs +224 -0
- package/src/commands/egress.mjs +11 -0
- package/src/commands/extension-pack.mjs +209 -0
- package/src/commands/feedback.mjs +3 -0
- package/src/commands/graph.mjs +3 -0
- package/src/commands/init.mjs +130 -0
- package/src/commands/lock.mjs +3 -0
- package/src/commands/project.mjs +3 -0
- package/src/commands/promote.mjs +4 -0
- package/src/commands/readiness.mjs +3 -0
- package/src/commands/server.mjs +6 -0
- package/src/commands/setup.mjs +250 -0
- package/src/commands/support.mjs +3 -0
- package/src/commands/upgrade.mjs +3 -0
- package/src/contracts/corpus.mjs +171 -0
- package/src/egress/check.mjs +45 -0
- package/src/egress/forbidden-egress.mjs +323 -0
- package/src/export/atelier-export-contract.mjs +722 -0
- package/src/extension-packs/loader.mjs +491 -0
- package/src/graph/graph.mjs +370 -0
- package/src/graph/knowledge-graph.mjs +887 -0
- package/src/harness/context-client.mjs +144 -0
- package/src/harness/context.mjs +27 -0
- package/src/index.mjs +88 -0
- package/src/project/config.mjs +557 -0
- package/src/project/file-class.mjs +113 -0
- package/src/project/git-ignore.mjs +31 -0
- package/src/project/path-match.mjs +30 -0
- package/src/project/repo-identity.mjs +176 -0
- package/src/projection/alignment-projection.mjs +169 -0
- package/src/projection/policy.mjs +90 -0
- package/src/projection/project.mjs +152 -0
- package/src/readiness/readiness.mjs +312 -0
- package/src/readiness-protocols/bundled-pack.mjs +766 -0
- package/src/readiness-protocols/runtime.mjs +419 -0
- package/src/server/local-sidecar.mjs +640 -0
- package/src/server/security.mjs +229 -0
- package/src/server/server.mjs +42 -0
- package/src/support/feedback-report.mjs +414 -0
- package/src/support/support-bundle.mjs +226 -0
- package/src/ui/html-primitives.mjs +104 -0
- package/src/upgrade/upgrade.mjs +701 -0
- package/src/validate-atelier-export-dry-run.mjs +75 -0
- package/templates/atelier.project.example.json +23 -0
- package/templates/distribution-workspace/.claude/launch.json +12 -0
- package/templates/distribution-workspace/README.md +86 -0
- package/templates/distribution-workspace/atelier.project.json +36 -0
- package/templates/distribution-workspace/boundary-policy.v1.json +61 -0
- package/templates/distribution-workspace/gitignore +29 -0
- package/templates/distribution-workspace/governance/repo-boundary-ledger.md +5 -0
- package/templates/distribution-workspace/project/README.md +19 -0
- package/templates/distribution-workspace/repo-access.v1.json +9 -0
- package/templates/launch.json +12 -0
- package/templates/private-domain-workspace/.claude/launch.json +12 -0
- package/templates/private-domain-workspace/README.md +50 -0
- package/templates/private-domain-workspace/atelier.project.json +28 -0
- package/templates/private-domain-workspace/boundary-policy.v1.json +54 -0
- package/templates/private-domain-workspace/domain/README.md +19 -0
- package/templates/private-domain-workspace/gitignore +29 -0
- package/templates/private-domain-workspace/governance/repo-boundary-ledger.md +4 -0
- package/templates/private-domain-workspace/repo-access.v1.json +9 -0
- package/templates/shared-project-workspace/.claude/launch.json +12 -0
- package/templates/shared-project-workspace/README.md +51 -0
- package/templates/shared-project-workspace/atelier.project.json +28 -0
- package/templates/shared-project-workspace/boundary-policy.v1.json +61 -0
- package/templates/shared-project-workspace/gitignore +29 -0
- package/templates/shared-project-workspace/governance/repo-boundary-ledger.md +5 -0
- package/templates/shared-project-workspace/project/README.md +19 -0
- package/templates/shared-project-workspace/repo-access.v1.json +9 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// atelier announcements list|verify|show
|
|
3
|
+
//
|
|
4
|
+
// Pull-only inbound channel: MNSTRY announcements are signed JSON documents
|
|
5
|
+
// committed under announcements/ in this repository. Users receive them by
|
|
6
|
+
// running `git pull` — pulling is the consent act — and verify them against
|
|
7
|
+
// the committed MNSTRY announcements public key. This command never fetches
|
|
8
|
+
// anything: it only reads local files. No network access anywhere.
|
|
9
|
+
//
|
|
10
|
+
// The announcement shape (mnstry.announcement@v1) is runtime-defined, not a
|
|
11
|
+
// contracts/ schema: it is validated structurally here in code so the
|
|
12
|
+
// governed contract corpus stays stable.
|
|
13
|
+
//
|
|
14
|
+
// Self-contained subcommand parser (no shared CLI plumbing) so the module can
|
|
15
|
+
// be invoked directly: node src/commands/announcements.mjs <subcommand> ...
|
|
16
|
+
//
|
|
17
|
+
// Trust anchor rule: the public key is ALWAYS the package-committed key
|
|
18
|
+
// (DEFAULT_PUBLIC_KEY) unless the operator explicitly passes --public-key.
|
|
19
|
+
// --dir relocates only where announcement documents are read from; it never
|
|
20
|
+
// relocates the key. A key loaded from inside the tree being verified is not
|
|
21
|
+
// a trust anchor at all — anyone who can write that directory could mint an
|
|
22
|
+
// attacker keypair under the genuine keyId and have forgeries list as
|
|
23
|
+
// verified. Every list run prints the key it verified against for exactly
|
|
24
|
+
// this reason.
|
|
25
|
+
//
|
|
26
|
+
// Exit codes: 0 success (all documents verified), 1 one or more documents
|
|
27
|
+
// failed verification, 2 usage or input error. Log-safety contract: output
|
|
28
|
+
// may name keyId, algorithm, package-relative file paths, and — only after a
|
|
29
|
+
// signature verifies — announcement content. An unverified document's body is
|
|
30
|
+
// never printed, and a key path outside the package is named by basename only
|
|
31
|
+
// so machine layout never reaches a log.
|
|
32
|
+
|
|
33
|
+
import fs from 'node:fs'
|
|
34
|
+
import path from 'node:path'
|
|
35
|
+
import { fileURLToPath } from 'node:url'
|
|
36
|
+
import { safeLabel, verifyDocument } from '../attestation/sign.mjs'
|
|
37
|
+
|
|
38
|
+
const PACKAGE_ROOT = process.env.MNSTRY_ATELIER_PACKAGE_ROOT
|
|
39
|
+
?? fileURLToPath(new URL('../..', import.meta.url))
|
|
40
|
+
const DEFAULT_DIR = path.join(PACKAGE_ROOT, 'announcements')
|
|
41
|
+
const DEFAULT_PUBLIC_KEY = path.join(DEFAULT_DIR, 'keys', 'mnstry-announcements.public.v1.json')
|
|
42
|
+
const ANNOUNCEMENT_SCHEMA = 'mnstry.announcement@v1'
|
|
43
|
+
|
|
44
|
+
const USAGE = `Usage: atelier announcements <subcommand>
|
|
45
|
+
|
|
46
|
+
Subcommands:
|
|
47
|
+
list [--dir DIR] [--public-key FILE]
|
|
48
|
+
List every announcement in the announcements directory (default: the
|
|
49
|
+
package's announcements/), verifying each against the committed MNSTRY
|
|
50
|
+
announcements key. --dir changes only which documents are read; the
|
|
51
|
+
trust anchor stays the committed key unless you pass --public-key.
|
|
52
|
+
Every run prints the key path and keyId it verified against.
|
|
53
|
+
Documents that do not verify are flagged loudly and fail the exit code.
|
|
54
|
+
|
|
55
|
+
verify <file> [--public-key FILE] [--json]
|
|
56
|
+
Verify one announcement document. --public-key defaults to the
|
|
57
|
+
committed MNSTRY announcements key. --json prints { valid, reasons }.
|
|
58
|
+
|
|
59
|
+
show <file> [--public-key FILE]
|
|
60
|
+
Print an announcement's title and body — only if its signature
|
|
61
|
+
verifies. Refuses (exit 1) otherwise; an unverified body is never shown.
|
|
62
|
+
|
|
63
|
+
This channel is pull-only: announcements arrive via git pull and nothing in
|
|
64
|
+
this kit ever contacts a network to look for them.
|
|
65
|
+
|
|
66
|
+
Exit codes: 0 all documents verified, 1 verification failed, 2 usage or
|
|
67
|
+
input error.`
|
|
68
|
+
|
|
69
|
+
function fail(message) {
|
|
70
|
+
console.error(message)
|
|
71
|
+
process.exit(2)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseOptions(argv, spec) {
|
|
75
|
+
const options = { _: [] }
|
|
76
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
77
|
+
const arg = argv[i]
|
|
78
|
+
if (arg === '--help' || arg === '-h') {
|
|
79
|
+
console.log(USAGE)
|
|
80
|
+
process.exit(0)
|
|
81
|
+
}
|
|
82
|
+
if (arg.startsWith('--')) {
|
|
83
|
+
const name = arg.slice(2)
|
|
84
|
+
if (!(name in spec)) fail(`unknown option --${name}\n\n${USAGE}`)
|
|
85
|
+
if (spec[name] === 'flag') {
|
|
86
|
+
options[name] = true
|
|
87
|
+
} else {
|
|
88
|
+
i += 1
|
|
89
|
+
if (argv[i] === undefined) fail(`--${name} requires a value`)
|
|
90
|
+
options[name] = argv[i]
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
options._.push(arg)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return options
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readJsonFile(file, label) {
|
|
100
|
+
let raw
|
|
101
|
+
try {
|
|
102
|
+
raw = fs.readFileSync(file, 'utf8')
|
|
103
|
+
} catch {
|
|
104
|
+
fail(`cannot read ${label} file: ${safeLabel(file, 120)}`)
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(raw)
|
|
108
|
+
} catch {
|
|
109
|
+
fail(`${label} file is not valid JSON: ${file}`)
|
|
110
|
+
}
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function loadPublicKey(keyPath) {
|
|
115
|
+
const doc = readJsonFile(keyPath, 'public key')
|
|
116
|
+
if (!doc || typeof doc !== 'object' || !doc.publicKeyJwk) {
|
|
117
|
+
fail(`public key file has no publicKeyJwk member: ${keyPath}`)
|
|
118
|
+
}
|
|
119
|
+
return doc
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Name a key file for output. Inside the package it is named by its
|
|
123
|
+
// package-relative path (stable across machines and checkouts); anywhere else
|
|
124
|
+
// only the basename is printed, because an absolute path leaks the operator's
|
|
125
|
+
// machine layout into logs and transcripts.
|
|
126
|
+
function describeKeyPath(keyPath) {
|
|
127
|
+
const resolved = path.resolve(keyPath)
|
|
128
|
+
const relative = path.relative(PACKAGE_ROOT, resolved)
|
|
129
|
+
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
|
|
130
|
+
return relative.split(path.sep).join('/')
|
|
131
|
+
}
|
|
132
|
+
return `<outside package>/${path.basename(resolved)}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The key-identity header. Printing which key produced a verdict is what
|
|
136
|
+
// makes a swapped trust anchor visible rather than silent, so it prints on
|
|
137
|
+
// every run — and says so out loud when the key is not the committed default.
|
|
138
|
+
function keyIdentityHeader(keyPath, keyDoc, { explicit }) {
|
|
139
|
+
const keyId = safeLabel(keyDoc.keyId)
|
|
140
|
+
const provenance = explicit
|
|
141
|
+
? 'explicit --public-key, not the committed default key'
|
|
142
|
+
: 'committed default key'
|
|
143
|
+
// The path comes from operator argv, but a filename is an injection sink
|
|
144
|
+
// like any other: escapes can erase this very line, and an over-long
|
|
145
|
+
// basename wraps the header so the true provenance lands off-screen.
|
|
146
|
+
return `key: ${safeLabel(describeKeyPath(keyPath), 120)} (keyId ${keyId}, ${provenance})`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Structural validation of the runtime-defined announcement shape. Returns
|
|
150
|
+
// reason objects in the same { code, message } vocabulary the signature
|
|
151
|
+
// verifier uses, so callers can report one merged list.
|
|
152
|
+
function announcementShapeReasons(doc) {
|
|
153
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
154
|
+
return [{ code: 'announcement.shape', message: 'announcement is not a JSON object' }]
|
|
155
|
+
}
|
|
156
|
+
const reasons = []
|
|
157
|
+
const push = (message) => reasons.push({ code: 'announcement.shape', message })
|
|
158
|
+
if (doc.schema !== ANNOUNCEMENT_SCHEMA) {
|
|
159
|
+
push(`schema must be the constant ${ANNOUNCEMENT_SCHEMA}`)
|
|
160
|
+
}
|
|
161
|
+
if (typeof doc.announcementId !== 'string' || !doc.announcementId.startsWith('announcement:')) {
|
|
162
|
+
push("announcementId must be a string with the 'announcement:' prefix")
|
|
163
|
+
}
|
|
164
|
+
if (typeof doc.publishedAt !== 'string' || !/^\d{4}-\d{2}-\d{2}([T ]|$)/.test(doc.publishedAt)
|
|
165
|
+
|| Number.isNaN(Date.parse(doc.publishedAt))) {
|
|
166
|
+
push('publishedAt must be an ISO date string (YYYY-MM-DD, optionally with a time part)')
|
|
167
|
+
}
|
|
168
|
+
if (typeof doc.title !== 'string' || doc.title.length === 0) {
|
|
169
|
+
push('title must be a non-empty string')
|
|
170
|
+
}
|
|
171
|
+
if (typeof doc.body !== 'string') {
|
|
172
|
+
push('body must be a markdown string')
|
|
173
|
+
}
|
|
174
|
+
if (!('signature' in doc)) {
|
|
175
|
+
push('signature member is required (null only while unsigned; an unsigned announcement never verifies)')
|
|
176
|
+
}
|
|
177
|
+
return reasons
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Verify one parsed announcement document: structural shape first, then the
|
|
181
|
+
// detached signature over the JCS bytes with signature set to null (the same
|
|
182
|
+
// normative rule the attestation flow uses). Never throws on bad documents.
|
|
183
|
+
function verifyAnnouncement(doc, publicKey) {
|
|
184
|
+
const reasons = [...announcementShapeReasons(doc)]
|
|
185
|
+
if (doc && typeof doc === 'object' && !Array.isArray(doc)) {
|
|
186
|
+
reasons.push(...verifyDocument(doc, { publicKey }).reasons)
|
|
187
|
+
}
|
|
188
|
+
return { valid: reasons.length === 0, reasons }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function runVerify(argv) {
|
|
192
|
+
const options = parseOptions(argv, { 'public-key': 'value', json: 'flag' })
|
|
193
|
+
const [file] = options._
|
|
194
|
+
if (!file || options._.length !== 1) fail(`announcements verify requires exactly one announcement file\n\n${USAGE}`)
|
|
195
|
+
const explicitKey = options['public-key'] !== undefined
|
|
196
|
+
const keyPath = explicitKey ? options['public-key'] : DEFAULT_PUBLIC_KEY
|
|
197
|
+
const publicKey = loadPublicKey(keyPath)
|
|
198
|
+
const doc = readJsonFile(file, 'announcement')
|
|
199
|
+
const { valid, reasons } = verifyAnnouncement(doc, publicKey)
|
|
200
|
+
if (options.json) {
|
|
201
|
+
console.log(JSON.stringify({
|
|
202
|
+
valid,
|
|
203
|
+
reasons,
|
|
204
|
+
key: safeLabel(describeKeyPath(keyPath), 120),
|
|
205
|
+
keyId: safeLabel(publicKey.keyId),
|
|
206
|
+
explicitKey,
|
|
207
|
+
}, null, 2))
|
|
208
|
+
} else if (valid) {
|
|
209
|
+
console.log(keyIdentityHeader(keyPath, publicKey, { explicit: explicitKey }))
|
|
210
|
+
console.log(`announcement signature is valid (keyId ${safeLabel(doc.signature.keyId)})`)
|
|
211
|
+
} else {
|
|
212
|
+
console.error(keyIdentityHeader(keyPath, publicKey, { explicit: explicitKey }))
|
|
213
|
+
for (const reason of reasons) console.error(`[${reason.code}] ${reason.message}`)
|
|
214
|
+
}
|
|
215
|
+
process.exit(valid ? 0 : 1)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function runShow(argv) {
|
|
219
|
+
const options = parseOptions(argv, { 'public-key': 'value' })
|
|
220
|
+
const [file] = options._
|
|
221
|
+
if (!file || options._.length !== 1) fail(`announcements show requires exactly one announcement file\n\n${USAGE}`)
|
|
222
|
+
const explicitKey = options['public-key'] !== undefined
|
|
223
|
+
const keyPath = explicitKey ? options['public-key'] : DEFAULT_PUBLIC_KEY
|
|
224
|
+
const publicKey = loadPublicKey(keyPath)
|
|
225
|
+
const doc = readJsonFile(file, 'announcement')
|
|
226
|
+
const { valid, reasons } = verifyAnnouncement(doc, publicKey)
|
|
227
|
+
if (!valid) {
|
|
228
|
+
console.error(keyIdentityHeader(keyPath, publicKey, { explicit: explicitKey }))
|
|
229
|
+
console.error(`refusing to show ${safeLabel(file, 120)}: the announcement does not verify, so its content is untrusted`)
|
|
230
|
+
for (const reason of reasons) console.error(`[${reason.code}] ${reason.message}`)
|
|
231
|
+
process.exit(1)
|
|
232
|
+
}
|
|
233
|
+
// The anchor prints before any attacker-authored content, so a reader sees
|
|
234
|
+
// which key vouched for what they are about to read.
|
|
235
|
+
console.log(keyIdentityHeader(keyPath, publicKey, { explicit: explicitKey }))
|
|
236
|
+
console.log(`${doc.title}`)
|
|
237
|
+
console.log(`${doc.announcementId} — ${doc.publishedAt} (signature valid, keyId ${safeLabel(doc.signature.keyId)})`)
|
|
238
|
+
console.log('')
|
|
239
|
+
console.log(doc.body)
|
|
240
|
+
process.exit(0)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function runList(argv) {
|
|
244
|
+
const options = parseOptions(argv, { dir: 'value', 'public-key': 'value' })
|
|
245
|
+
if (options._.length) fail(`announcements list takes options only\n\n${USAGE}`)
|
|
246
|
+
const dir = options.dir ?? DEFAULT_DIR
|
|
247
|
+
// The trust anchor is the committed key — the same one verify and show
|
|
248
|
+
// default to — never a key found under --dir. Only --public-key moves it,
|
|
249
|
+
// and the header below says so when it does.
|
|
250
|
+
const explicitKey = options['public-key'] !== undefined
|
|
251
|
+
const keyPath = explicitKey ? options['public-key'] : DEFAULT_PUBLIC_KEY
|
|
252
|
+
const publicKey = loadPublicKey(keyPath)
|
|
253
|
+
console.log(keyIdentityHeader(keyPath, publicKey, { explicit: explicitKey }))
|
|
254
|
+
if (options.dir !== undefined) {
|
|
255
|
+
console.log('--dir changes only which documents are read; the trust anchor above is unchanged.')
|
|
256
|
+
}
|
|
257
|
+
let entries
|
|
258
|
+
try {
|
|
259
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
260
|
+
} catch {
|
|
261
|
+
fail(`cannot read announcements directory: ${safeLabel(dir, 120)}`)
|
|
262
|
+
}
|
|
263
|
+
const files = entries
|
|
264
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
265
|
+
.map((entry) => entry.name)
|
|
266
|
+
.sort()
|
|
267
|
+
if (files.length === 0) {
|
|
268
|
+
console.log(`no announcements found in ${dir}`)
|
|
269
|
+
process.exit(0)
|
|
270
|
+
}
|
|
271
|
+
let failures = 0
|
|
272
|
+
for (const name of files) {
|
|
273
|
+
const file = path.join(dir, name)
|
|
274
|
+
let doc = null
|
|
275
|
+
let reasons = null
|
|
276
|
+
try {
|
|
277
|
+
doc = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
278
|
+
} catch {
|
|
279
|
+
reasons = [{ code: 'announcement.unreadable', message: 'file is missing or not valid JSON' }]
|
|
280
|
+
}
|
|
281
|
+
if (!reasons) {
|
|
282
|
+
const result = verifyAnnouncement(doc, publicKey)
|
|
283
|
+
if (!result.valid) reasons = result.reasons
|
|
284
|
+
}
|
|
285
|
+
if (reasons) {
|
|
286
|
+
failures += 1
|
|
287
|
+
console.error(`!! UNVERIFIED ${safeLabel(name, 96)} — do not trust this file's content`)
|
|
288
|
+
for (const reason of reasons) console.error(` [${reason.code}] ${reason.message}`)
|
|
289
|
+
} else {
|
|
290
|
+
console.log(`${doc.announcementId} ${doc.publishedAt} ${doc.title}`)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (failures > 0) {
|
|
294
|
+
console.error(`${failures} of ${files.length} announcement file(s) failed verification`)
|
|
295
|
+
}
|
|
296
|
+
process.exit(failures > 0 ? 1 : 0)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const argv = process.argv.slice(2)
|
|
300
|
+
const subcommand = argv[0]
|
|
301
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
|
|
302
|
+
console.log(USAGE)
|
|
303
|
+
process.exit(0)
|
|
304
|
+
}
|
|
305
|
+
if (subcommand === 'list') runList(argv.slice(1))
|
|
306
|
+
else if (subcommand === 'verify') runVerify(argv.slice(1))
|
|
307
|
+
else if (subcommand === 'show') runShow(argv.slice(1))
|
|
308
|
+
else fail(`unknown announcements subcommand: ${subcommand}\n\n${USAGE}`)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// atelier attestation hash|sign|verify|keygen
|
|
3
|
+
//
|
|
4
|
+
// Self-contained subcommand parser (no shared CLI plumbing) so the module can
|
|
5
|
+
// be invoked directly: node src/commands/attestation.mjs <subcommand> ...
|
|
6
|
+
//
|
|
7
|
+
// Exit codes: 0 success (verify: valid), 1 verify found the attestation
|
|
8
|
+
// invalid, 2 usage or input error. No network access anywhere in this
|
|
9
|
+
// command. Log-safety contract: output may name keyId and algorithm, never
|
|
10
|
+
// any private JWK member and never file contents.
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import {
|
|
15
|
+
generateKeyPair,
|
|
16
|
+
loadSigningKey,
|
|
17
|
+
payloadHashOf,
|
|
18
|
+
signAttestation,
|
|
19
|
+
verifyAttestation,
|
|
20
|
+
verifyPayloadBinding,
|
|
21
|
+
LOCAL_KEY_FILE,
|
|
22
|
+
} from '../attestation/sign.mjs'
|
|
23
|
+
|
|
24
|
+
const USAGE = `Usage: atelier attestation <subcommand>
|
|
25
|
+
|
|
26
|
+
Subcommands:
|
|
27
|
+
hash <payload.json>
|
|
28
|
+
Print the canonical payload hash object (sha-256 over RFC 8785 JCS).
|
|
29
|
+
|
|
30
|
+
sign <attestation.json> [--key FILE] [--out FILE]
|
|
31
|
+
Sign an unsigned attestation (signature must be null) with the local
|
|
32
|
+
signing key file. Writes the signed document to stdout, or to --out.
|
|
33
|
+
|
|
34
|
+
verify <attestation.json> --public-key FILE [--payload FILE] [--json]
|
|
35
|
+
Verify the signature against a public key file, and, when --payload is
|
|
36
|
+
given, the payload binding. --json prints { valid, reasons }.
|
|
37
|
+
|
|
38
|
+
keygen --key-id ID [--algorithm ed25519|es256] [--out FILE]
|
|
39
|
+
Generate a signing key pair. Writes the private key file (default
|
|
40
|
+
${LOCAL_KEY_FILE}, mode 0600, refuses to overwrite)
|
|
41
|
+
and prints only the public key document.
|
|
42
|
+
|
|
43
|
+
Exit codes: 0 success or valid, 1 verify judged the attestation invalid,
|
|
44
|
+
2 usage or input error.`
|
|
45
|
+
|
|
46
|
+
function fail(message) {
|
|
47
|
+
console.error(message)
|
|
48
|
+
process.exit(2)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseOptions(argv, spec) {
|
|
52
|
+
const options = { _: [] }
|
|
53
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
54
|
+
const arg = argv[i]
|
|
55
|
+
if (arg === '--help' || arg === '-h') {
|
|
56
|
+
console.log(USAGE)
|
|
57
|
+
process.exit(0)
|
|
58
|
+
}
|
|
59
|
+
if (arg.startsWith('--')) {
|
|
60
|
+
const name = arg.slice(2)
|
|
61
|
+
if (!(name in spec)) fail(`unknown option --${name}\n\n${USAGE}`)
|
|
62
|
+
if (spec[name] === 'flag') {
|
|
63
|
+
options[name] = true
|
|
64
|
+
} else {
|
|
65
|
+
i += 1
|
|
66
|
+
if (argv[i] === undefined) fail(`--${name} requires a value`)
|
|
67
|
+
options[name] = argv[i]
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
options._.push(arg)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return options
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readJsonFile(file, label) {
|
|
77
|
+
let raw
|
|
78
|
+
try {
|
|
79
|
+
raw = fs.readFileSync(file, 'utf8')
|
|
80
|
+
} catch {
|
|
81
|
+
fail(`cannot read ${label} file: ${file}`)
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(raw)
|
|
85
|
+
} catch {
|
|
86
|
+
fail(`${label} file is not valid JSON: ${file}`)
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function runHash(argv) {
|
|
92
|
+
const options = parseOptions(argv, {})
|
|
93
|
+
const [file] = options._
|
|
94
|
+
if (!file || options._.length !== 1) fail(`attestation hash requires exactly one payload file\n\n${USAGE}`)
|
|
95
|
+
const payload = readJsonFile(file, 'payload')
|
|
96
|
+
try {
|
|
97
|
+
console.log(JSON.stringify(payloadHashOf(payload), null, 2))
|
|
98
|
+
} catch (error) {
|
|
99
|
+
fail(`payload cannot be canonicalized: ${error.message}`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function runSign(argv) {
|
|
104
|
+
const options = parseOptions(argv, { key: 'value', out: 'value' })
|
|
105
|
+
const [file] = options._
|
|
106
|
+
if (!file || options._.length !== 1) fail(`attestation sign requires exactly one attestation file\n\n${USAGE}`)
|
|
107
|
+
const attestation = readJsonFile(file, 'attestation')
|
|
108
|
+
let signed
|
|
109
|
+
try {
|
|
110
|
+
const key = loadSigningKey({ keyPath: options.key ?? null })
|
|
111
|
+
signed = signAttestation({ attestation, key })
|
|
112
|
+
} catch (error) {
|
|
113
|
+
fail(error.message)
|
|
114
|
+
}
|
|
115
|
+
const output = `${JSON.stringify(signed, null, 2)}\n`
|
|
116
|
+
if (options.out) {
|
|
117
|
+
fs.writeFileSync(options.out, output)
|
|
118
|
+
console.log(`signed attestation written to ${options.out}`)
|
|
119
|
+
} else {
|
|
120
|
+
process.stdout.write(output)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function runVerify(argv) {
|
|
125
|
+
const options = parseOptions(argv, { 'public-key': 'value', payload: 'value', json: 'flag' })
|
|
126
|
+
const [file] = options._
|
|
127
|
+
if (!file || options._.length !== 1) fail(`attestation verify requires exactly one attestation file\n\n${USAGE}`)
|
|
128
|
+
if (!options['public-key']) fail(`attestation verify requires --public-key FILE\n\n${USAGE}`)
|
|
129
|
+
const attestation = readJsonFile(file, 'attestation')
|
|
130
|
+
const publicKey = readJsonFile(options['public-key'], 'public key')
|
|
131
|
+
if (!publicKey || typeof publicKey !== 'object' || !publicKey.publicKeyJwk) {
|
|
132
|
+
fail(`public key file has no publicKeyJwk member: ${options['public-key']}`)
|
|
133
|
+
}
|
|
134
|
+
const signature = verifyAttestation({ attestation, publicKey })
|
|
135
|
+
const reasons = [...signature.reasons]
|
|
136
|
+
let payloadBinding = null
|
|
137
|
+
if (options.payload) {
|
|
138
|
+
const payload = readJsonFile(options.payload, 'payload')
|
|
139
|
+
payloadBinding = verifyPayloadBinding({ attestation, payload })
|
|
140
|
+
reasons.push(...payloadBinding.reasons)
|
|
141
|
+
}
|
|
142
|
+
const valid = signature.valid && (payloadBinding === null || payloadBinding.valid)
|
|
143
|
+
if (options.json) {
|
|
144
|
+
console.log(JSON.stringify({ valid, payloadBindingChecked: payloadBinding !== null, reasons }, null, 2))
|
|
145
|
+
} else if (valid) {
|
|
146
|
+
console.log(`attestation signature is valid (keyId ${attestation?.signature?.keyId ?? 'unknown'})`)
|
|
147
|
+
if (payloadBinding === null) console.log('payload binding not checked (pass --payload FILE to check it)')
|
|
148
|
+
else console.log('payload binding is valid')
|
|
149
|
+
} else {
|
|
150
|
+
for (const reason of reasons) console.error(`[${reason.code}] ${reason.message}`)
|
|
151
|
+
}
|
|
152
|
+
process.exit(valid ? 0 : 1)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function runKeygen(argv) {
|
|
156
|
+
const options = parseOptions(argv, { 'key-id': 'value', algorithm: 'value', out: 'value' })
|
|
157
|
+
if (options._.length) fail(`attestation keygen takes options only\n\n${USAGE}`)
|
|
158
|
+
const keyId = options['key-id']
|
|
159
|
+
if (!keyId) fail(`attestation keygen requires --key-id ID\n\n${USAGE}`)
|
|
160
|
+
const algorithm = options.algorithm ?? 'ed25519'
|
|
161
|
+
if (algorithm !== 'ed25519' && algorithm !== 'es256') {
|
|
162
|
+
fail(`--algorithm must be ed25519 or es256, got ${algorithm}`)
|
|
163
|
+
}
|
|
164
|
+
const outPath = path.resolve(options.out ?? LOCAL_KEY_FILE)
|
|
165
|
+
const { privateKeyDoc, publicKeyDoc } = generateKeyPair({ algorithm, keyId })
|
|
166
|
+
// Atomic create-exclusive ('wx' = O_CREAT|O_EXCL, mode 0600): fails on any
|
|
167
|
+
// existing path AND on symlinks — including dangling ones — so a planted
|
|
168
|
+
// link can never redirect the private key to an attacker-chosen path, and
|
|
169
|
+
// there is no check-then-write race window. Error output stays path-only.
|
|
170
|
+
let fd = null
|
|
171
|
+
try {
|
|
172
|
+
fd = fs.openSync(outPath, 'wx', 0o600)
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error?.code === 'EEXIST') {
|
|
175
|
+
fail(`refusing to write signing key file: path already exists or is a symlink: ${outPath}`)
|
|
176
|
+
}
|
|
177
|
+
fail(`cannot create signing key file: ${outPath}`)
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
fs.writeSync(fd, `${JSON.stringify(privateKeyDoc, null, 2)}\n`)
|
|
181
|
+
} finally {
|
|
182
|
+
fs.closeSync(fd)
|
|
183
|
+
}
|
|
184
|
+
console.error(`signing key file written to ${outPath} (mode 0600). Keep it out of version control; ${LOCAL_KEY_FILE} is gitignored by default.`)
|
|
185
|
+
console.log(JSON.stringify(publicKeyDoc, null, 2))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const argv = process.argv.slice(2)
|
|
189
|
+
const subcommand = argv[0]
|
|
190
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
|
|
191
|
+
console.log(USAGE)
|
|
192
|
+
process.exit(0)
|
|
193
|
+
}
|
|
194
|
+
if (subcommand === 'hash') runHash(argv.slice(1))
|
|
195
|
+
else if (subcommand === 'sign') runSign(argv.slice(1))
|
|
196
|
+
else if (subcommand === 'verify') runVerify(argv.slice(1))
|
|
197
|
+
else if (subcommand === 'keygen') runKeygen(argv.slice(1))
|
|
198
|
+
else fail(`unknown attestation subcommand: ${subcommand}\n\n${USAGE}`)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from '../project/config.mjs'
|
|
3
|
+
import {
|
|
4
|
+
runBoundaryAuditCommand,
|
|
5
|
+
runBoundaryCheckCommand,
|
|
6
|
+
runBoundaryInstallHooksCommand,
|
|
7
|
+
runBoundaryPushCheckCommand,
|
|
8
|
+
} from '../boundary/policy.mjs'
|
|
9
|
+
|
|
10
|
+
const args = parseArgs(process.argv.slice(2))
|
|
11
|
+
const subcommand = args._[0] || 'check'
|
|
12
|
+
const rest = (...names) => process.argv.slice(2).filter((arg) => !names.includes(arg))
|
|
13
|
+
|
|
14
|
+
if (subcommand === 'check' || subcommand === 'doctor') {
|
|
15
|
+
runBoundaryCheckCommand(rest('check', 'doctor'))
|
|
16
|
+
} else if (subcommand === 'push-check') {
|
|
17
|
+
runBoundaryPushCheckCommand(rest('push-check'))
|
|
18
|
+
} else if (subcommand === 'audit') {
|
|
19
|
+
runBoundaryAuditCommand(rest('audit'))
|
|
20
|
+
} else if (subcommand === 'install-hooks') {
|
|
21
|
+
runBoundaryInstallHooksCommand(rest('install-hooks'))
|
|
22
|
+
} else {
|
|
23
|
+
console.error(`Unknown boundary command: ${subcommand}`)
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|