intentdna 1.7.4 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/LICENSE +201 -0
- package/README.md +143 -194
- package/dist/assets/candidates.d.ts +124 -0
- package/dist/assets/candidates.js +1249 -0
- package/dist/assets/catalog.d.ts +101 -0
- package/dist/assets/catalog.js +311 -0
- package/dist/assets/evidence.d.ts +93 -0
- package/dist/assets/evidence.js +391 -0
- package/dist/assets/index.d.ts +4 -0
- package/dist/assets/index.js +4 -0
- package/dist/assets/metadata.d.ts +19 -0
- package/dist/assets/metadata.js +227 -0
- package/dist/cli/commands/assets.d.ts +65 -0
- package/dist/cli/commands/assets.js +2871 -0
- package/dist/cli/commands/auth.d.ts +322 -0
- package/dist/cli/commands/auth.js +1592 -0
- package/dist/cli/commands/cloud-auth-guidance.d.ts +2 -0
- package/dist/cli/commands/cloud-auth-guidance.js +4 -0
- package/dist/cli/commands/cloud.d.ts +1359 -0
- package/dist/cli/commands/cloud.js +8364 -0
- package/dist/cli/commands/evolve.js +18 -2
- package/dist/cli/commands/generate.js +25 -2
- package/dist/cli/commands/guard.d.ts +1 -1
- package/dist/cli/commands/guard.js +1 -1
- package/dist/cli/commands/guide.d.ts +15 -0
- package/dist/cli/commands/guide.js +1747 -0
- package/dist/cli/commands/init.d.ts +12 -0
- package/dist/cli/commands/init.js +95 -60
- package/dist/cli/commands/org.d.ts +16 -0
- package/dist/cli/commands/org.js +2393 -0
- package/dist/cli/commands/release.d.ts +59 -0
- package/dist/cli/commands/release.js +716 -0
- package/dist/cli/commands/report.d.ts +82 -0
- package/dist/cli/commands/report.js +1516 -0
- package/dist/cli/commands/run.d.ts +6 -1
- package/dist/cli/commands/run.js +95 -26
- package/dist/cli/commands/sync.d.ts +8 -1
- package/dist/cli/commands/sync.js +436 -141
- package/dist/cli/commands/templates.d.ts +64 -0
- package/dist/cli/commands/templates.js +2151 -1
- package/dist/cli/commands/workflow.d.ts +26 -0
- package/dist/cli/commands/workflow.js +3053 -0
- package/dist/cli/index.js +719 -42
- package/dist/compiler/cascade.js +4 -9
- package/dist/compiler/compile.js +1 -0
- package/dist/governance/evidence-capture-attribution.d.ts +12 -0
- package/dist/governance/evidence-capture-attribution.js +46 -0
- package/dist/governance/index.d.ts +2 -0
- package/dist/governance/index.js +1 -0
- package/dist/hooks/cli.d.ts +6 -0
- package/dist/hooks/cli.js +168 -12
- package/dist/hooks/enforce.d.ts +1 -0
- package/dist/hooks/protocol.d.ts +1 -1
- package/dist/hooks/protocol.js +1 -1
- package/dist/hooks/state.d.ts +41 -0
- package/dist/hooks/state.js +126 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp/tools-state.js +2 -0
- package/dist/organization/index.d.ts +121 -0
- package/dist/organization/index.js +413 -0
- package/dist/runtime/claude-sync-target.d.ts +49 -0
- package/dist/runtime/claude-sync-target.js +280 -0
- package/dist/runtime/codex-adapter.d.ts +254 -0
- package/dist/runtime/codex-adapter.js +1204 -0
- package/dist/runtime/codex-sync-target.d.ts +33 -0
- package/dist/runtime/codex-sync-target.js +241 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/output-artifact-registry.d.ts +1 -1
- package/dist/runtime/output-artifact-registry.js +2 -0
- package/dist/runtime/settings-adapter.d.ts +1 -1
- package/dist/runtime/settings-adapter.js +1 -1
- package/dist/runtime/skill-adapter.d.ts +5 -1
- package/dist/runtime/skill-adapter.js +120 -34
- package/dist/runtime/sync-artifact-registry.d.ts +99 -0
- package/dist/runtime/sync-artifact-registry.js +194 -0
- package/dist/runtime/sync-target-plan.d.ts +45 -0
- package/dist/runtime/sync-target-plan.js +19 -0
- package/dist/schema/controller-registry.d.ts +8 -1
- package/dist/schema/controller-registry.js +8 -0
- package/dist/schema/types.d.ts +35 -23
- package/dist/schema/types.js +10 -1
- package/dist/schema/validate.js +22 -22
- package/dist/schema/validators/controllers.js +38 -20
- package/dist/schema/workflow-authoring-contract.d.ts +29 -0
- package/dist/schema/workflow-authoring-contract.js +109 -0
- package/dist/schema/yaml-parser.js +18 -0
- package/dist/templates/api-backend.dna.yaml +19 -0
- package/dist/templates/brainstorming-first.dna.yaml +19 -0
- package/dist/templates/code-cleanup.dna.yaml +19 -0
- package/dist/templates/code-review-pipeline.dna.yaml +18 -0
- package/dist/templates/documentation-writer.dna.yaml +19 -0
- package/dist/templates/flutter-behavior-lock.dna.yaml +19 -0
- package/dist/templates/flutter-refactoring-rescue.dna.yaml +19 -0
- package/dist/templates/flutter-rewrite-diagnosis-review.dna.yaml +210 -0
- package/dist/templates/flutter-rewrite-fix-review-loop.dna.yaml +260 -0
- package/dist/templates/flutter-rewrite-new.dna.yaml +506 -0
- package/dist/templates/flutter-rewrite.dna.yaml +20 -0
- package/dist/templates/frontend-quality.dna.yaml +19 -0
- package/dist/templates/full-pipeline.dna.yaml +20 -0
- package/dist/templates/marketplace.d.ts +92 -0
- package/dist/templates/marketplace.js +502 -0
- package/dist/templates/metadata.d.ts +25 -0
- package/dist/templates/metadata.js +136 -3
- package/dist/templates/mobile-dev.dna.yaml +19 -0
- package/dist/templates/multi-agent-handoff-coordination.dna.yaml +156 -0
- package/dist/templates/multi-perspective-review.dna.yaml +19 -0
- package/dist/templates/persistent-executor.dna.yaml +19 -0
- package/dist/templates/qa-loop.dna.yaml +18 -0
- package/dist/templates/release-evidence-gate.dna.yaml +131 -0
- package/dist/templates/requirements-gate.dna.yaml +19 -0
- package/dist/templates/research-orchestration.dna.yaml +19 -0
- package/dist/templates/root-cause-analysis.dna.yaml +19 -0
- package/dist/templates/safe-refactoring.dna.yaml +321 -0
- package/dist/templates/secure-dev.dna.yaml +19 -0
- package/dist/templates/subagent-parallel.dna.yaml +19 -0
- package/dist/templates/systematic-debugging.dna.yaml +19 -0
- package/dist/templates/tdd-strict.dna.yaml +19 -0
- package/dist/templates/ui-visual-regression-qa.dna.yaml +133 -0
- package/dist/templates/workflow-author.dna.yaml +302 -0
- package/dist/templates/workflow-blocked-scope-triage.dna.yaml +242 -0
- package/dist/templates/workflow-completion-audit.dna.yaml +254 -0
- package/dist/templates/workflow-cross-boundary-resource-approval.dna.yaml +249 -0
- package/dist/templates/workflow-evidence-review.dna.yaml +198 -0
- package/dist/templates/workflow-module-inventory.dna.yaml +257 -0
- package/dist/templates/workflow-runtime-smoke-review.dna.yaml +252 -0
- package/dist/templates/workflow-verification-command-routing.dna.yaml +278 -0
- package/dist/workflow/authoring-packet.d.ts +67 -0
- package/dist/workflow/authoring-packet.js +218 -0
- package/dist/workflow/draft.d.ts +159 -0
- package/dist/workflow/draft.js +800 -0
- package/dist/workflow/index.d.ts +3 -0
- package/dist/workflow/index.js +3 -0
- package/dist/workflow/suggestion-review.d.ts +65 -0
- package/dist/workflow/suggestion-review.js +245 -0
- package/package.json +1 -1
- package/spec/README.md +15 -48
- package/spec/codex-adapter-contract.md +176 -0
- package/spec/sync-target-plan.md +158 -0
- package/spec/workflow-capability-ir.md +153 -0
- package/spec/agent-dispatch-fix.md +0 -116
- package/spec/control-plane-convergence-handoff-2026-04-24.md +0 -432
- package/spec/flutter-rewrite-template-optimization.md +0 -301
- package/spec/foundation-hardening.md +0 -178
- package/spec/hooks-infra-harness-hardening.md +0 -741
- package/spec/multi-config.md +0 -172
- package/spec/parallel-isolation.md +0 -268
- package/spec/template-version-namespace-fix.md +0 -653
- package/spec/workflow-pipeline.md +0 -101
|
@@ -0,0 +1,1516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dna report — local evidence package and hidden transport staging.
|
|
3
|
+
*/
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createHash, generateKeyPairSync, createPrivateKey, createPublicKey, randomBytes, sign, verify } from "node:crypto";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { buildReportPackage } from "../../report/report-package.js";
|
|
8
|
+
import { buildAccountEndpoint, isRejectedServiceRoleToken, resolveAuthToken, resolveSupabaseUserJwt } from "./auth.js";
|
|
9
|
+
import { missingCliApiTokenReason } from "./cloud-auth-guidance.js";
|
|
10
|
+
import { linkedSupabaseProjectUrl } from "./cloud.js";
|
|
11
|
+
function defaultReportsDir(projectDir) {
|
|
12
|
+
return join(projectDir, ".dna", "reports");
|
|
13
|
+
}
|
|
14
|
+
function defaultPackagePath(projectDir) {
|
|
15
|
+
return join(defaultReportsDir(projectDir), "latest-report-package.json");
|
|
16
|
+
}
|
|
17
|
+
function defaultKeysDir(projectDir) {
|
|
18
|
+
return join(projectDir, ".dna", "keys");
|
|
19
|
+
}
|
|
20
|
+
function defaultOutboxPath(projectDir, generatedAt) {
|
|
21
|
+
const stamp = generatedAt.replace(/[:.]/g, "-");
|
|
22
|
+
return join(defaultReportsDir(projectDir), "outbox", `report-push-${stamp}.json`);
|
|
23
|
+
}
|
|
24
|
+
function defaultIngestEnvelopePath(projectDir, generatedAt) {
|
|
25
|
+
const stamp = generatedAt.replace(/[:.]/g, "-");
|
|
26
|
+
return join(defaultReportsDir(projectDir), "ingest", `report-ingest-${stamp}.json`);
|
|
27
|
+
}
|
|
28
|
+
function sha256(value) {
|
|
29
|
+
return createHash("sha256").update(value).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
async function writeJson(path, value) {
|
|
32
|
+
await mkdir(dirname(path), { recursive: true });
|
|
33
|
+
await writeFile(path, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
34
|
+
}
|
|
35
|
+
function resolveProjectPath(projectDir, path) {
|
|
36
|
+
return isAbsolute(path) ? path : resolve(projectDir, path);
|
|
37
|
+
}
|
|
38
|
+
function packageSummary(reportPackage, path) {
|
|
39
|
+
return [
|
|
40
|
+
`Report package written: ${path}`,
|
|
41
|
+
`Schema: ${reportPackage.schema_version}`,
|
|
42
|
+
`Runtime decisions: ${reportPackage.runtime_decisions.length}`,
|
|
43
|
+
`Traces: ${reportPackage.traces.length}`,
|
|
44
|
+
`Verifier results: ${reportPackage.verifier_results.length}`,
|
|
45
|
+
`Critical decisions: ${reportPackage.critical_decisions.length}`,
|
|
46
|
+
"Boundary: local report package is evidence projection input, not runtime enforcement.",
|
|
47
|
+
].join("\n") + "\n";
|
|
48
|
+
}
|
|
49
|
+
function ingestBoundaries() {
|
|
50
|
+
return [
|
|
51
|
+
"Signed report ingest envelope is local verifier evidence only; it is not cloud trusted ingestion.",
|
|
52
|
+
"dna report sign and verify perform no network upload and create no backend, auth, storage, or SaaS state.",
|
|
53
|
+
"Receipt status describes local envelope verification, not runtime allow/warn/block/validate authority.",
|
|
54
|
+
"dna-hook and local runtime evidence remain the source for runtime decisions.",
|
|
55
|
+
"Online may project receipts, but browser-local import is not backend persistence.",
|
|
56
|
+
"MCP remains hidden transport and is not a visible product entry.",
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
function packageBoundaries() {
|
|
60
|
+
return [
|
|
61
|
+
"Report package is local runtime evidence output; it is not runtime enforcement.",
|
|
62
|
+
"dna report package performs no network upload and creates no backend, auth, storage, or SaaS state.",
|
|
63
|
+
"Cloud trust requires a signed envelope and trusted ingestion receipt; browser-local package JSON is projection only.",
|
|
64
|
+
"dna-hook and local runtime evidence remain the source for runtime decisions.",
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
function cloudUploadBoundaries() {
|
|
68
|
+
return [
|
|
69
|
+
"dna report upload sends signed report evidence to cloud ingestion only; it does not execute workflows.",
|
|
70
|
+
"Cloud ingestion may verify and persist evidence, but runtime_authority remains false.",
|
|
71
|
+
"dna-hook and local runtime evidence remain the source for allow/warn/block/validate decisions.",
|
|
72
|
+
"CLI API tokens are the user-facing auth path; MCP is not a visible auth route.",
|
|
73
|
+
"Service role keys are server-only and must not be stored in CLI config or browser code.",
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
function buildReportPackageResult(params) {
|
|
77
|
+
return {
|
|
78
|
+
schema_version: "intentdna.report_package_result.v1",
|
|
79
|
+
mode: "intentdna_report_package_result",
|
|
80
|
+
generated_at: params.generatedAt,
|
|
81
|
+
status: params.status,
|
|
82
|
+
path: params.path,
|
|
83
|
+
package: params.reportPackage,
|
|
84
|
+
trusted_cloud_ingestion: false,
|
|
85
|
+
runtime_authority: false,
|
|
86
|
+
transport: {
|
|
87
|
+
network_performed: false,
|
|
88
|
+
hidden_transport: true,
|
|
89
|
+
},
|
|
90
|
+
boundaries: packageBoundaries(),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function cloudReceiptsBoundaries() {
|
|
94
|
+
return [
|
|
95
|
+
"dna report receipts lists cloud receipt projections with a Supabase user JWT; it is not MCP authorization.",
|
|
96
|
+
"Receipt listing is read-only account projection; it does not upload reports, read storage object contents, publish templates, or execute workflows.",
|
|
97
|
+
"Supabase receipt projection is persistence/auth/transport evidence only; runtime_authority remains false.",
|
|
98
|
+
"Report upload continues to use IntentDNA CLI API tokens; Supabase user JWTs are account onboarding/read credentials.",
|
|
99
|
+
"dna-hook and local runtime evidence remain the source for allow/warn/block/validate decisions.",
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
function pushBoundaries() {
|
|
103
|
+
return [
|
|
104
|
+
"dna report push stages local evidence for transport; it does not execute workflows.",
|
|
105
|
+
"No network request is performed by this command in the current static/hidden-transport lane.",
|
|
106
|
+
"Online ingestion, when added, may project evidence summaries but must not become runtime truth.",
|
|
107
|
+
"dna-hook and local runtime evidence remain the source for allow/warn/block/validate decisions.",
|
|
108
|
+
"MCP remains hidden transport and is not a visible product entry.",
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
function buildEnvelope(reportPackage, packagePath, outboxPath, packageJson, generatedAt, opts) {
|
|
112
|
+
return {
|
|
113
|
+
schema_version: "intentdna.report_push_envelope.v1",
|
|
114
|
+
generated_at: generatedAt,
|
|
115
|
+
status: opts.dryRun ? "dry_run" : "staged_for_transport",
|
|
116
|
+
transport: {
|
|
117
|
+
kind: "local_outbox",
|
|
118
|
+
endpoint: opts.endpoint,
|
|
119
|
+
network_performed: false,
|
|
120
|
+
hidden_transport: true,
|
|
121
|
+
},
|
|
122
|
+
source: reportPackage.source,
|
|
123
|
+
package_ref: {
|
|
124
|
+
schema_version: reportPackage.schema_version,
|
|
125
|
+
generated_at: reportPackage.generated_at,
|
|
126
|
+
sha256: sha256(packageJson),
|
|
127
|
+
runtime_decisions: reportPackage.runtime_decisions.length,
|
|
128
|
+
traces: reportPackage.traces.length,
|
|
129
|
+
verifier_results: reportPackage.verifier_results.length,
|
|
130
|
+
critical_decisions: reportPackage.critical_decisions.length,
|
|
131
|
+
},
|
|
132
|
+
files: {
|
|
133
|
+
report_package: packagePath,
|
|
134
|
+
outbox_envelope: outboxPath,
|
|
135
|
+
},
|
|
136
|
+
boundaries: pushBoundaries(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function signedPayloadForEnvelope(envelope) {
|
|
140
|
+
return {
|
|
141
|
+
schema_version: "intentdna.report_ingest_payload.v1",
|
|
142
|
+
generated_at: envelope.generated_at,
|
|
143
|
+
idempotency_key: envelope.idempotency_key,
|
|
144
|
+
freshness: envelope.freshness,
|
|
145
|
+
package_ref: envelope.package_ref,
|
|
146
|
+
identity: envelope.identity,
|
|
147
|
+
visibility: envelope.visibility,
|
|
148
|
+
redaction_policy_version: envelope.redaction_policy_version,
|
|
149
|
+
transport: envelope.transport,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function freshnessWindow(generatedAt) {
|
|
153
|
+
const issuedAt = Date.parse(generatedAt);
|
|
154
|
+
const issuedDate = Number.isNaN(issuedAt) ? new Date() : new Date(issuedAt);
|
|
155
|
+
const expiresAt = new Date(issuedDate.getTime() + 7 * 24 * 60 * 60 * 1000);
|
|
156
|
+
return {
|
|
157
|
+
nonce: randomBytes(16).toString("hex"),
|
|
158
|
+
issued_at: issuedDate.toISOString(),
|
|
159
|
+
expires_at: expiresAt.toISOString(),
|
|
160
|
+
accepted_clock_skew_seconds: 300,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function idempotencyKey(params) {
|
|
164
|
+
return `intentdna-ingest:${sha256(canonicalJson({
|
|
165
|
+
org: params.identity.org,
|
|
166
|
+
project: params.identity.project,
|
|
167
|
+
package_sha256: params.packageHash,
|
|
168
|
+
package_generated_at: params.packageGeneratedAt,
|
|
169
|
+
envelope_generated_at: params.envelopeGeneratedAt,
|
|
170
|
+
}))}`;
|
|
171
|
+
}
|
|
172
|
+
function canonicalJson(value) {
|
|
173
|
+
if (value === null || typeof value !== "object")
|
|
174
|
+
return JSON.stringify(value);
|
|
175
|
+
if (Array.isArray(value))
|
|
176
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
177
|
+
return `{${Object.keys(value)
|
|
178
|
+
.filter((key) => value[key] !== undefined)
|
|
179
|
+
.sort()
|
|
180
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
181
|
+
.join(",")}}`;
|
|
182
|
+
}
|
|
183
|
+
async function readOrCreateSigningKey(projectDir) {
|
|
184
|
+
const keyDir = defaultKeysDir(projectDir);
|
|
185
|
+
const privateKeyPath = join(keyDir, "report-ingest-ed25519-private.pem");
|
|
186
|
+
const publicKeyPath = join(keyDir, "report-ingest-ed25519-public.pem");
|
|
187
|
+
try {
|
|
188
|
+
const [privateKeyPem, publicKeyPem] = await Promise.all([
|
|
189
|
+
readFile(privateKeyPath, "utf-8"),
|
|
190
|
+
readFile(publicKeyPath, "utf-8"),
|
|
191
|
+
]);
|
|
192
|
+
return {
|
|
193
|
+
privateKeyPem,
|
|
194
|
+
publicKeyPem,
|
|
195
|
+
keyId: `local-ed25519:${sha256(publicKeyPem).slice(0, 16)}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
const pair = generateKeyPairSync("ed25519", {
|
|
200
|
+
privateKeyEncoding: { format: "pem", type: "pkcs8" },
|
|
201
|
+
publicKeyEncoding: { format: "pem", type: "spki" },
|
|
202
|
+
});
|
|
203
|
+
await mkdir(keyDir, { recursive: true });
|
|
204
|
+
await Promise.all([
|
|
205
|
+
writeFile(privateKeyPath, pair.privateKey, { encoding: "utf-8", mode: 0o600 }),
|
|
206
|
+
writeFile(publicKeyPath, pair.publicKey, "utf-8"),
|
|
207
|
+
]);
|
|
208
|
+
return {
|
|
209
|
+
privateKeyPem: pair.privateKey,
|
|
210
|
+
publicKeyPem: pair.publicKey,
|
|
211
|
+
keyId: `local-ed25519:${sha256(pair.publicKey).slice(0, 16)}`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function createEphemeralSigningKey() {
|
|
216
|
+
const pair = generateKeyPairSync("ed25519", {
|
|
217
|
+
privateKeyEncoding: { format: "pem", type: "pkcs8" },
|
|
218
|
+
publicKeyEncoding: { format: "pem", type: "spki" },
|
|
219
|
+
});
|
|
220
|
+
return {
|
|
221
|
+
privateKeyPem: pair.privateKey,
|
|
222
|
+
publicKeyPem: pair.publicKey,
|
|
223
|
+
keyId: `ephemeral-ed25519:${sha256(pair.publicKey).slice(0, 16)}`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function buildIngestEnvelope(params) {
|
|
227
|
+
const packageHash = sha256(params.packageJson);
|
|
228
|
+
const identity = {
|
|
229
|
+
org: "local-org",
|
|
230
|
+
project: basename(params.reportPackage.source.project_dir) || "local-project",
|
|
231
|
+
actor: params.opts.actor?.trim() || process.env.USER || "local-actor",
|
|
232
|
+
};
|
|
233
|
+
const payload = {
|
|
234
|
+
schema_version: "intentdna.report_ingest_payload.v1",
|
|
235
|
+
generated_at: params.generatedAt,
|
|
236
|
+
idempotency_key: idempotencyKey({
|
|
237
|
+
identity,
|
|
238
|
+
packageHash,
|
|
239
|
+
packageGeneratedAt: params.reportPackage.generated_at,
|
|
240
|
+
envelopeGeneratedAt: params.generatedAt,
|
|
241
|
+
}),
|
|
242
|
+
freshness: freshnessWindow(params.generatedAt),
|
|
243
|
+
package_ref: {
|
|
244
|
+
schema_version: params.reportPackage.schema_version,
|
|
245
|
+
generated_at: params.reportPackage.generated_at,
|
|
246
|
+
sha256: packageHash,
|
|
247
|
+
path: params.packagePath,
|
|
248
|
+
runtime_decisions: params.reportPackage.runtime_decisions.length,
|
|
249
|
+
traces: params.reportPackage.traces.length,
|
|
250
|
+
verifier_results: params.reportPackage.verifier_results.length,
|
|
251
|
+
critical_decisions: params.reportPackage.critical_decisions.length,
|
|
252
|
+
},
|
|
253
|
+
identity,
|
|
254
|
+
visibility: params.opts.visibility ?? "runtime-local",
|
|
255
|
+
redaction_policy_version: params.opts.redactionPolicyVersion ?? "local-redaction-policy.v1",
|
|
256
|
+
transport: {
|
|
257
|
+
kind: "local_signed_envelope",
|
|
258
|
+
network_performed: false,
|
|
259
|
+
hidden_transport: true,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
const payloadJson = canonicalJson(payload);
|
|
263
|
+
const signature = sign(null, Buffer.from(payloadJson), createPrivateKey(params.key.privateKeyPem)).toString("base64");
|
|
264
|
+
return {
|
|
265
|
+
schema_version: "intentdna.report_ingest_envelope.v1",
|
|
266
|
+
generated_at: payload.generated_at,
|
|
267
|
+
idempotency_key: payload.idempotency_key,
|
|
268
|
+
freshness: payload.freshness,
|
|
269
|
+
package_ref: payload.package_ref,
|
|
270
|
+
identity: payload.identity,
|
|
271
|
+
visibility: payload.visibility,
|
|
272
|
+
redaction_policy_version: payload.redaction_policy_version,
|
|
273
|
+
transport: payload.transport,
|
|
274
|
+
envelope_path: params.envelopePath,
|
|
275
|
+
signing: {
|
|
276
|
+
algorithm: "Ed25519",
|
|
277
|
+
key_id: params.key.keyId,
|
|
278
|
+
public_key_pem: params.key.publicKeyPem,
|
|
279
|
+
signed_payload_sha256: sha256(payloadJson),
|
|
280
|
+
signature,
|
|
281
|
+
},
|
|
282
|
+
files: {
|
|
283
|
+
report_package: params.packagePath,
|
|
284
|
+
envelope: params.envelopePath,
|
|
285
|
+
},
|
|
286
|
+
boundaries: ingestBoundaries(),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function asRecord(value) {
|
|
290
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
291
|
+
}
|
|
292
|
+
function removeSensitiveFields(value) {
|
|
293
|
+
if (Array.isArray(value))
|
|
294
|
+
return value.map((item) => removeSensitiveFields(item));
|
|
295
|
+
if (!value || typeof value !== "object")
|
|
296
|
+
return value;
|
|
297
|
+
const output = {};
|
|
298
|
+
for (const [key, item] of Object.entries(value)) {
|
|
299
|
+
const normalized = key.toLowerCase();
|
|
300
|
+
if (normalized === "raw_token"
|
|
301
|
+
|| normalized === "token_hash"
|
|
302
|
+
|| normalized === "hash"
|
|
303
|
+
|| normalized === "supabase_user_jwt"
|
|
304
|
+
|| normalized === "user_jwt"
|
|
305
|
+
|| normalized === "service_role_key"
|
|
306
|
+
|| normalized === "secret_key"
|
|
307
|
+
|| normalized === "token_pepper") {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
output[key] = removeSensitiveFields(item);
|
|
311
|
+
}
|
|
312
|
+
return output;
|
|
313
|
+
}
|
|
314
|
+
function sanitizeRecord(value) {
|
|
315
|
+
const sanitized = removeSensitiveFields(value);
|
|
316
|
+
return sanitized && typeof sanitized === "object" && !Array.isArray(sanitized)
|
|
317
|
+
? sanitized
|
|
318
|
+
: undefined;
|
|
319
|
+
}
|
|
320
|
+
function sanitizeCloudReceipt(value) {
|
|
321
|
+
const sanitized = sanitizeRecord(value);
|
|
322
|
+
const packageRef = asRecord(sanitized?.package_ref);
|
|
323
|
+
if (!sanitized || !packageRef || typeof packageRef.path !== "string")
|
|
324
|
+
return sanitized;
|
|
325
|
+
return {
|
|
326
|
+
...sanitized,
|
|
327
|
+
package_ref: {
|
|
328
|
+
...packageRef,
|
|
329
|
+
path: "redacted:local-report-package-path",
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function isPackageSchema(value) {
|
|
334
|
+
return value === "intentdna.report_package.v1";
|
|
335
|
+
}
|
|
336
|
+
function parseIngestEnvelope(value) {
|
|
337
|
+
const record = asRecord(value);
|
|
338
|
+
if (!record || record.schema_version !== "intentdna.report_ingest_envelope.v1")
|
|
339
|
+
return undefined;
|
|
340
|
+
const packageRef = asRecord(record.package_ref);
|
|
341
|
+
const identity = asRecord(record.identity);
|
|
342
|
+
const transport = asRecord(record.transport);
|
|
343
|
+
const signing = asRecord(record.signing);
|
|
344
|
+
const files = asRecord(record.files);
|
|
345
|
+
const freshness = asRecord(record.freshness);
|
|
346
|
+
if (!packageRef || !identity || !transport || !signing || !files)
|
|
347
|
+
return undefined;
|
|
348
|
+
if (!isPackageSchema(packageRef.schema_version))
|
|
349
|
+
return undefined;
|
|
350
|
+
if (typeof packageRef.generated_at !== "string" || typeof packageRef.sha256 !== "string" || typeof packageRef.path !== "string")
|
|
351
|
+
return undefined;
|
|
352
|
+
if (typeof packageRef.runtime_decisions !== "number" || typeof packageRef.traces !== "number" || typeof packageRef.verifier_results !== "number" || typeof packageRef.critical_decisions !== "number")
|
|
353
|
+
return undefined;
|
|
354
|
+
if (typeof identity.actor !== "string")
|
|
355
|
+
return undefined;
|
|
356
|
+
if (record.visibility !== "runtime-local" && record.visibility !== "personal" && record.visibility !== "internal")
|
|
357
|
+
return undefined;
|
|
358
|
+
if (typeof record.generated_at !== "string" || typeof record.redaction_policy_version !== "string")
|
|
359
|
+
return undefined;
|
|
360
|
+
if (typeof record.idempotency_key !== "string")
|
|
361
|
+
return undefined;
|
|
362
|
+
if (!freshness
|
|
363
|
+
|| typeof freshness.nonce !== "string"
|
|
364
|
+
|| typeof freshness.issued_at !== "string"
|
|
365
|
+
|| typeof freshness.expires_at !== "string"
|
|
366
|
+
|| typeof freshness.accepted_clock_skew_seconds !== "number")
|
|
367
|
+
return undefined;
|
|
368
|
+
if (transport.kind !== "local_signed_envelope" || transport.network_performed !== false || transport.hidden_transport !== true)
|
|
369
|
+
return undefined;
|
|
370
|
+
if (signing.algorithm !== "Ed25519" || typeof signing.key_id !== "string" || typeof signing.public_key_pem !== "string" || typeof signing.signed_payload_sha256 !== "string" || typeof signing.signature !== "string")
|
|
371
|
+
return undefined;
|
|
372
|
+
if (typeof files.report_package !== "string")
|
|
373
|
+
return undefined;
|
|
374
|
+
return {
|
|
375
|
+
schema_version: "intentdna.report_ingest_envelope.v1",
|
|
376
|
+
generated_at: record.generated_at,
|
|
377
|
+
idempotency_key: record.idempotency_key,
|
|
378
|
+
freshness: {
|
|
379
|
+
nonce: freshness.nonce,
|
|
380
|
+
issued_at: freshness.issued_at,
|
|
381
|
+
expires_at: freshness.expires_at,
|
|
382
|
+
accepted_clock_skew_seconds: freshness.accepted_clock_skew_seconds,
|
|
383
|
+
},
|
|
384
|
+
package_ref: {
|
|
385
|
+
schema_version: packageRef.schema_version,
|
|
386
|
+
generated_at: packageRef.generated_at,
|
|
387
|
+
sha256: packageRef.sha256,
|
|
388
|
+
path: packageRef.path,
|
|
389
|
+
runtime_decisions: packageRef.runtime_decisions,
|
|
390
|
+
traces: packageRef.traces,
|
|
391
|
+
verifier_results: packageRef.verifier_results,
|
|
392
|
+
critical_decisions: packageRef.critical_decisions,
|
|
393
|
+
},
|
|
394
|
+
identity: {
|
|
395
|
+
org: typeof identity.org === "string" ? identity.org : undefined,
|
|
396
|
+
project: typeof identity.project === "string" ? identity.project : undefined,
|
|
397
|
+
actor: identity.actor,
|
|
398
|
+
},
|
|
399
|
+
visibility: record.visibility,
|
|
400
|
+
redaction_policy_version: record.redaction_policy_version,
|
|
401
|
+
transport: {
|
|
402
|
+
kind: "local_signed_envelope",
|
|
403
|
+
network_performed: false,
|
|
404
|
+
hidden_transport: true,
|
|
405
|
+
},
|
|
406
|
+
envelope_path: typeof record.envelope_path === "string"
|
|
407
|
+
? record.envelope_path
|
|
408
|
+
: typeof files.envelope === "string" ? files.envelope : undefined,
|
|
409
|
+
signing: {
|
|
410
|
+
algorithm: "Ed25519",
|
|
411
|
+
key_id: signing.key_id,
|
|
412
|
+
public_key_pem: signing.public_key_pem,
|
|
413
|
+
signed_payload_sha256: signing.signed_payload_sha256,
|
|
414
|
+
signature: signing.signature,
|
|
415
|
+
},
|
|
416
|
+
files: {
|
|
417
|
+
report_package: files.report_package,
|
|
418
|
+
envelope: typeof files.envelope === "string" ? files.envelope : undefined,
|
|
419
|
+
},
|
|
420
|
+
boundaries: Array.isArray(record.boundaries) ? record.boundaries.filter((item) => typeof item === "string") : ingestBoundaries(),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function invalidIngestEnvelopeReasons(value) {
|
|
424
|
+
const record = asRecord(value);
|
|
425
|
+
if (!record)
|
|
426
|
+
return ["rejected: envelope must be a JSON object"];
|
|
427
|
+
if (record.schema_version !== "intentdna.report_ingest_envelope.v1") {
|
|
428
|
+
return typeof record.schema_version === "string"
|
|
429
|
+
? [`rejected: unsupported envelope schema ${record.schema_version}; expected intentdna.report_ingest_envelope.v1`]
|
|
430
|
+
: ["rejected: envelope schema_version is missing or not a string; expected intentdna.report_ingest_envelope.v1"];
|
|
431
|
+
}
|
|
432
|
+
const reasons = [];
|
|
433
|
+
const packageRef = asRecord(record.package_ref);
|
|
434
|
+
const identity = asRecord(record.identity);
|
|
435
|
+
const transport = asRecord(record.transport);
|
|
436
|
+
const signing = asRecord(record.signing);
|
|
437
|
+
const files = asRecord(record.files);
|
|
438
|
+
const freshness = asRecord(record.freshness);
|
|
439
|
+
if (!packageRef)
|
|
440
|
+
reasons.push("rejected: envelope package_ref must be an object");
|
|
441
|
+
if (!identity)
|
|
442
|
+
reasons.push("rejected: envelope identity must be an object");
|
|
443
|
+
if (!transport)
|
|
444
|
+
reasons.push("rejected: envelope transport must be an object");
|
|
445
|
+
if (!signing)
|
|
446
|
+
reasons.push("rejected: envelope signing must be an object");
|
|
447
|
+
if (!files)
|
|
448
|
+
reasons.push("rejected: envelope files must be an object");
|
|
449
|
+
if (packageRef) {
|
|
450
|
+
if (!isPackageSchema(packageRef.schema_version)) {
|
|
451
|
+
reasons.push(typeof packageRef.schema_version === "string"
|
|
452
|
+
? `rejected: unsupported report package schema ${packageRef.schema_version}; expected intentdna.report_package.v1`
|
|
453
|
+
: "rejected: envelope package_ref.schema_version is missing or not supported");
|
|
454
|
+
}
|
|
455
|
+
if (typeof packageRef.generated_at !== "string")
|
|
456
|
+
reasons.push("rejected: envelope package_ref.generated_at must be a string");
|
|
457
|
+
if (typeof packageRef.sha256 !== "string")
|
|
458
|
+
reasons.push("rejected: envelope package_ref.sha256 must be a string");
|
|
459
|
+
if (typeof packageRef.path !== "string")
|
|
460
|
+
reasons.push("rejected: envelope package_ref.path must be a string");
|
|
461
|
+
if (typeof packageRef.runtime_decisions !== "number")
|
|
462
|
+
reasons.push("rejected: envelope package_ref.runtime_decisions must be a number");
|
|
463
|
+
if (typeof packageRef.traces !== "number")
|
|
464
|
+
reasons.push("rejected: envelope package_ref.traces must be a number");
|
|
465
|
+
if (typeof packageRef.verifier_results !== "number")
|
|
466
|
+
reasons.push("rejected: envelope package_ref.verifier_results must be a number");
|
|
467
|
+
if (typeof packageRef.critical_decisions !== "number")
|
|
468
|
+
reasons.push("rejected: envelope package_ref.critical_decisions must be a number");
|
|
469
|
+
}
|
|
470
|
+
if (identity && typeof identity.actor !== "string")
|
|
471
|
+
reasons.push("rejected: envelope identity.actor must be a string");
|
|
472
|
+
if (record.visibility !== "runtime-local" && record.visibility !== "personal" && record.visibility !== "internal") {
|
|
473
|
+
reasons.push("rejected: envelope visibility must be runtime-local, personal, or internal");
|
|
474
|
+
}
|
|
475
|
+
if (typeof record.generated_at !== "string")
|
|
476
|
+
reasons.push("rejected: envelope generated_at must be a string");
|
|
477
|
+
if (typeof record.idempotency_key !== "string")
|
|
478
|
+
reasons.push("rejected: envelope idempotency_key must be a string");
|
|
479
|
+
if (!freshness) {
|
|
480
|
+
reasons.push("rejected: envelope freshness must be an object");
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
if (typeof freshness.nonce !== "string")
|
|
484
|
+
reasons.push("rejected: envelope freshness.nonce must be a string");
|
|
485
|
+
if (typeof freshness.issued_at !== "string")
|
|
486
|
+
reasons.push("rejected: envelope freshness.issued_at must be a string");
|
|
487
|
+
if (typeof freshness.expires_at !== "string")
|
|
488
|
+
reasons.push("rejected: envelope freshness.expires_at must be a string");
|
|
489
|
+
if (typeof freshness.accepted_clock_skew_seconds !== "number")
|
|
490
|
+
reasons.push("rejected: envelope freshness.accepted_clock_skew_seconds must be a number");
|
|
491
|
+
}
|
|
492
|
+
if (typeof record.redaction_policy_version !== "string")
|
|
493
|
+
reasons.push("rejected: envelope redaction_policy_version must be a string");
|
|
494
|
+
if (transport) {
|
|
495
|
+
if (transport.kind !== "local_signed_envelope")
|
|
496
|
+
reasons.push("rejected: envelope transport.kind must be local_signed_envelope");
|
|
497
|
+
if (transport.network_performed !== false)
|
|
498
|
+
reasons.push("rejected: envelope transport.network_performed must be false before trusted upload");
|
|
499
|
+
if (transport.hidden_transport !== true)
|
|
500
|
+
reasons.push("rejected: envelope transport.hidden_transport must be true for local signed envelopes");
|
|
501
|
+
}
|
|
502
|
+
if (signing) {
|
|
503
|
+
if (signing.algorithm !== "Ed25519")
|
|
504
|
+
reasons.push("rejected: envelope signing.algorithm must be Ed25519");
|
|
505
|
+
if (typeof signing.key_id !== "string")
|
|
506
|
+
reasons.push("rejected: envelope signing.key_id must be a string");
|
|
507
|
+
if (typeof signing.public_key_pem !== "string")
|
|
508
|
+
reasons.push("rejected: envelope signing.public_key_pem must be a string");
|
|
509
|
+
if (typeof signing.signed_payload_sha256 !== "string")
|
|
510
|
+
reasons.push("rejected: envelope signing.signed_payload_sha256 must be a string");
|
|
511
|
+
if (typeof signing.signature !== "string")
|
|
512
|
+
reasons.push("rejected: envelope signing.signature must be a string");
|
|
513
|
+
}
|
|
514
|
+
if (files && typeof files.report_package !== "string")
|
|
515
|
+
reasons.push("rejected: envelope files.report_package must be a string");
|
|
516
|
+
return reasons.length > 0
|
|
517
|
+
? reasons
|
|
518
|
+
: ["rejected: envelope shape is invalid for intentdna.report_ingest_envelope.v1"];
|
|
519
|
+
}
|
|
520
|
+
function reportPackageShapeReasons(value) {
|
|
521
|
+
const record = asRecord(value);
|
|
522
|
+
if (!record)
|
|
523
|
+
return ["rejected: report package must be a JSON object"];
|
|
524
|
+
const reasons = [];
|
|
525
|
+
const source = asRecord(record.source);
|
|
526
|
+
if (!isPackageSchema(record.schema_version)) {
|
|
527
|
+
reasons.push(typeof record.schema_version === "string"
|
|
528
|
+
? `rejected: unsupported report package schema ${record.schema_version}; expected intentdna.report_package.v1`
|
|
529
|
+
: "rejected: report package schema_version is missing or not supported; expected intentdna.report_package.v1");
|
|
530
|
+
}
|
|
531
|
+
if (typeof record.generated_at !== "string")
|
|
532
|
+
reasons.push("rejected: report package generated_at must be a string");
|
|
533
|
+
if (!source) {
|
|
534
|
+
reasons.push("rejected: report package source must be an object");
|
|
535
|
+
}
|
|
536
|
+
else if (typeof source.project_dir !== "string") {
|
|
537
|
+
reasons.push("rejected: report package source.project_dir must be a string");
|
|
538
|
+
}
|
|
539
|
+
if (!Array.isArray(record.runtime_decisions))
|
|
540
|
+
reasons.push("rejected: report package runtime_decisions must be an array");
|
|
541
|
+
if (!Array.isArray(record.traces))
|
|
542
|
+
reasons.push("rejected: report package traces must be an array");
|
|
543
|
+
if (!Array.isArray(record.verifier_results))
|
|
544
|
+
reasons.push("rejected: report package verifier_results must be an array");
|
|
545
|
+
if (!Array.isArray(record.critical_decisions))
|
|
546
|
+
reasons.push("rejected: report package critical_decisions must be an array");
|
|
547
|
+
return reasons;
|
|
548
|
+
}
|
|
549
|
+
function validateEnvelopeFreshness(envelope) {
|
|
550
|
+
const reasons = [];
|
|
551
|
+
const issuedAt = Date.parse(envelope.freshness.issued_at);
|
|
552
|
+
const expiresAt = Date.parse(envelope.freshness.expires_at);
|
|
553
|
+
const skewMs = Math.max(0, envelope.freshness.accepted_clock_skew_seconds) * 1000;
|
|
554
|
+
if (Number.isNaN(issuedAt))
|
|
555
|
+
reasons.push("rejected: envelope freshness.issued_at must be an ISO timestamp");
|
|
556
|
+
if (Number.isNaN(expiresAt)) {
|
|
557
|
+
reasons.push("rejected: envelope freshness.expires_at must be an ISO timestamp");
|
|
558
|
+
}
|
|
559
|
+
else if (expiresAt + skewMs < Date.now()) {
|
|
560
|
+
reasons.push("rejected: envelope freshness window has expired");
|
|
561
|
+
}
|
|
562
|
+
return reasons;
|
|
563
|
+
}
|
|
564
|
+
async function readReportPackageForSign(opts, projectDir, packagePath) {
|
|
565
|
+
if (opts.action === "sign" && opts.packagePath) {
|
|
566
|
+
let packageJson;
|
|
567
|
+
try {
|
|
568
|
+
packageJson = await readFile(packagePath, "utf-8");
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
return { ok: false, reasons: ["rejected: report package file is missing"] };
|
|
572
|
+
}
|
|
573
|
+
let parsed;
|
|
574
|
+
try {
|
|
575
|
+
parsed = JSON.parse(packageJson);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
return { ok: false, reasons: ["rejected: report package is not valid JSON"] };
|
|
579
|
+
}
|
|
580
|
+
const reasons = reportPackageShapeReasons(parsed);
|
|
581
|
+
if (reasons.length > 0)
|
|
582
|
+
return { ok: false, reasons };
|
|
583
|
+
return { ok: true, reportPackage: parsed, packageJson };
|
|
584
|
+
}
|
|
585
|
+
const reportPackage = await buildReportPackage({
|
|
586
|
+
projectDir,
|
|
587
|
+
days: opts.days,
|
|
588
|
+
sessionId: opts.sessionId,
|
|
589
|
+
dnaId: opts.dnaId,
|
|
590
|
+
includeMarkerPreview: opts.includeMarkerPreview,
|
|
591
|
+
});
|
|
592
|
+
return { ok: true, reportPackage, packageJson: JSON.stringify(reportPackage, null, 2) + "\n" };
|
|
593
|
+
}
|
|
594
|
+
function buildReportSignRejection(params) {
|
|
595
|
+
return {
|
|
596
|
+
schema_version: "intentdna.report_sign_result.v1",
|
|
597
|
+
generated_at: params.generatedAt,
|
|
598
|
+
status: "rejected",
|
|
599
|
+
package_path: params.packagePath,
|
|
600
|
+
cloud_upload_allowed: false,
|
|
601
|
+
next_command: "dna report package --json",
|
|
602
|
+
runtime_authority: false,
|
|
603
|
+
transport: {
|
|
604
|
+
network_performed: false,
|
|
605
|
+
hidden_transport: true,
|
|
606
|
+
},
|
|
607
|
+
reasons: params.reasons,
|
|
608
|
+
boundaries: ingestBoundaries(),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function buildReceipt(params) {
|
|
612
|
+
const status = params.reasons.some((reason) => reason.startsWith("rejected:"))
|
|
613
|
+
? "rejected"
|
|
614
|
+
: params.packageAvailable ? "verified" : "pending";
|
|
615
|
+
const cloudUploadEligible = status === "verified" && params.packageAvailable;
|
|
616
|
+
return {
|
|
617
|
+
schema_version: "intentdna.report_ingest_receipt.v1",
|
|
618
|
+
generated_at: params.generatedAt,
|
|
619
|
+
status,
|
|
620
|
+
envelope_ref: {
|
|
621
|
+
path: params.envelopePath,
|
|
622
|
+
schema_version: params.envelope?.schema_version,
|
|
623
|
+
sha256: params.envelopeRaw ? sha256(params.envelopeRaw) : undefined,
|
|
624
|
+
key_id: params.envelope?.signing.key_id,
|
|
625
|
+
},
|
|
626
|
+
package_ref: params.envelope?.package_ref,
|
|
627
|
+
identity: params.envelope?.identity,
|
|
628
|
+
visibility: params.envelope?.visibility,
|
|
629
|
+
local_verifier: {
|
|
630
|
+
checked_at: params.generatedAt,
|
|
631
|
+
signature_valid: params.signatureValid,
|
|
632
|
+
package_available: params.packageAvailable,
|
|
633
|
+
package_hash_valid: params.packageHashValid,
|
|
634
|
+
result: status,
|
|
635
|
+
reasons: params.reasons,
|
|
636
|
+
},
|
|
637
|
+
trusted_cloud_ingestion: false,
|
|
638
|
+
runtime_authority: false,
|
|
639
|
+
upload_gate: {
|
|
640
|
+
local_verified: status === "verified",
|
|
641
|
+
package_available: params.packageAvailable,
|
|
642
|
+
cloud_upload_eligible: cloudUploadEligible,
|
|
643
|
+
next_action: cloudUploadEligible
|
|
644
|
+
? "Local evidence verified. Run upload dry-run before any live cloud upload."
|
|
645
|
+
: status === "pending"
|
|
646
|
+
? "Restore the referenced report package or re-sign a fresh package before upload."
|
|
647
|
+
: "Fix the signed envelope or report package, then rerun local verification before upload.",
|
|
648
|
+
next_command: cloudUploadEligible
|
|
649
|
+
? "dna report upload <envelope-path> --dry-run --json"
|
|
650
|
+
: status === "pending"
|
|
651
|
+
? "dna report sign --package .dna/reports/latest-report-package.json --output .dna/reports/ingest/local-envelope.json --json"
|
|
652
|
+
: "dna report verify <envelope-path> --json",
|
|
653
|
+
},
|
|
654
|
+
transport: {
|
|
655
|
+
network_performed: false,
|
|
656
|
+
hidden_transport: true,
|
|
657
|
+
},
|
|
658
|
+
boundaries: ingestBoundaries(),
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function isLocalHttpHost(hostname) {
|
|
662
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
663
|
+
}
|
|
664
|
+
function isSupportedUploadEndpoint(endpoint) {
|
|
665
|
+
if (!endpoint)
|
|
666
|
+
return false;
|
|
667
|
+
let parsed;
|
|
668
|
+
try {
|
|
669
|
+
parsed = new URL(endpoint);
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
const protocolAllowed = parsed.protocol === "https:" || (parsed.protocol === "http:" && isLocalHttpHost(parsed.hostname));
|
|
675
|
+
return protocolAllowed
|
|
676
|
+
&& parsed.pathname === "/functions/v1/ingest-report"
|
|
677
|
+
&& parsed.search === ""
|
|
678
|
+
&& parsed.hash === "";
|
|
679
|
+
}
|
|
680
|
+
function buildUploadEndpoint(opts) {
|
|
681
|
+
const normalize = (value) => {
|
|
682
|
+
const withoutTrailingSlash = value.trim().replace(/\/+$/, "");
|
|
683
|
+
if (withoutTrailingSlash.endsWith("/functions/v1/ingest-report"))
|
|
684
|
+
return withoutTrailingSlash;
|
|
685
|
+
if (withoutTrailingSlash.endsWith("/functions/v1"))
|
|
686
|
+
return `${withoutTrailingSlash}/ingest-report`;
|
|
687
|
+
try {
|
|
688
|
+
const parsed = new URL(withoutTrailingSlash);
|
|
689
|
+
if (parsed.protocol === "http:" && isLocalHttpHost(parsed.hostname) && parsed.pathname === "/") {
|
|
690
|
+
return `${withoutTrailingSlash}/functions/v1/ingest-report`;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
// Leave unsupported endpoint strings unchanged so rejection output preserves the operator input.
|
|
695
|
+
}
|
|
696
|
+
if (/^https:\/\/[^/]+\.supabase\.co$/i.test(withoutTrailingSlash)) {
|
|
697
|
+
return `${withoutTrailingSlash}/functions/v1/ingest-report`;
|
|
698
|
+
}
|
|
699
|
+
return withoutTrailingSlash;
|
|
700
|
+
};
|
|
701
|
+
const explicitEndpoint = opts.endpoint?.trim();
|
|
702
|
+
if (explicitEndpoint) {
|
|
703
|
+
const endpoint = normalize(explicitEndpoint);
|
|
704
|
+
return { input: explicitEndpoint, endpoint, endpointSupported: isSupportedUploadEndpoint(endpoint) };
|
|
705
|
+
}
|
|
706
|
+
const env = opts.env ?? process.env;
|
|
707
|
+
const cloudUrl = env.INTENTDNA_CLOUD_URL?.trim() || env.SUPABASE_URL?.trim() || linkedSupabaseProjectUrl(env);
|
|
708
|
+
if (!cloudUrl)
|
|
709
|
+
return { endpointSupported: false };
|
|
710
|
+
const endpoint = normalize(cloudUrl);
|
|
711
|
+
return { input: cloudUrl, endpoint, endpointSupported: isSupportedUploadEndpoint(endpoint) };
|
|
712
|
+
}
|
|
713
|
+
function localUploadReady(localReceipt) {
|
|
714
|
+
return localReceipt?.status === "verified" && Boolean(localReceipt.package_ref);
|
|
715
|
+
}
|
|
716
|
+
function uploadReadinessAction(params) {
|
|
717
|
+
if (!params.localReady) {
|
|
718
|
+
return {
|
|
719
|
+
nextAction: "Fix the signed envelope or report package, then rerun local verification before upload.",
|
|
720
|
+
nextCommand: "dna report verify <envelope-path> --json",
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
if (!params.tokenPresent) {
|
|
724
|
+
return {
|
|
725
|
+
nextAction: "Issue or store an IntentDNA CLI API token before live upload.",
|
|
726
|
+
nextCommand: "dna auth onboard --project-id <id> --json",
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
if (!params.endpoint) {
|
|
730
|
+
return {
|
|
731
|
+
nextAction: "Set the Supabase project URL or pass the ingest endpoint before live upload.",
|
|
732
|
+
nextCommand: "INTENTDNA_CLOUD_URL=https://<project-ref>.supabase.co dna report upload <envelope-path> --dry-run --json",
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (!params.endpointSupported) {
|
|
736
|
+
return {
|
|
737
|
+
nextAction: "Use a Supabase project URL or an ingest-report Edge Function URL before live upload.",
|
|
738
|
+
nextCommand: "dna report upload <envelope-path> --endpoint https://<project-ref>.supabase.co/functions/v1/ingest-report --dry-run --json",
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
if (params.status === "dry_run") {
|
|
742
|
+
return {
|
|
743
|
+
nextAction: "Dry run is ready. Live cloud ingestion is optional; run upload without --dry-run only after the Supabase functions and secrets are deployed.",
|
|
744
|
+
nextCommand: `dna report upload <envelope-path> --endpoint ${params.endpoint} --json`,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
if (params.networkPerformed && (params.status === "verified" || params.status === "pending")) {
|
|
748
|
+
return {
|
|
749
|
+
nextAction: `Cloud ingestion returned a trusted ${params.status} receipt. Inspect the cloud receipt and audit refs before making release claims.`,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
if (params.networkPerformed) {
|
|
753
|
+
return {
|
|
754
|
+
nextAction: "Inspect the rejected cloud receipt and server audit, then retry after fixing the cloud-side rejection.",
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
return {
|
|
758
|
+
nextAction: "Resolve the rejection reason, then rerun upload dry-run before any live network upload.",
|
|
759
|
+
nextCommand: "dna report upload <envelope-path> --dry-run --json",
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function normalizeReceiptLimit(value) {
|
|
763
|
+
if (!Number.isFinite(value))
|
|
764
|
+
return 25;
|
|
765
|
+
const parsed = Math.floor(Number(value));
|
|
766
|
+
if (parsed <= 0)
|
|
767
|
+
return 25;
|
|
768
|
+
return Math.min(parsed, 100);
|
|
769
|
+
}
|
|
770
|
+
function buildCloudReceiptsRequest(opts) {
|
|
771
|
+
return {
|
|
772
|
+
schema_version: "intentdna.cloud_receipts_list_request.v1",
|
|
773
|
+
organization_id: opts.organizationId?.trim() || undefined,
|
|
774
|
+
project_id: opts.projectId?.trim() || undefined,
|
|
775
|
+
status: opts.status,
|
|
776
|
+
limit: normalizeReceiptLimit(opts.limit),
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function buildCloudReceiptsResult(params) {
|
|
780
|
+
return {
|
|
781
|
+
schema_version: "intentdna.report_cloud_receipts_result.v1",
|
|
782
|
+
generated_at: new Date().toISOString(),
|
|
783
|
+
status: params.status,
|
|
784
|
+
endpoint: params.endpoint,
|
|
785
|
+
request: params.request,
|
|
786
|
+
auth: {
|
|
787
|
+
supabase_user_jwt_present: Boolean(params.jwt.token),
|
|
788
|
+
source: params.jwt.source,
|
|
789
|
+
},
|
|
790
|
+
transport: {
|
|
791
|
+
network_performed: params.networkPerformed ?? false,
|
|
792
|
+
hidden_transport: false,
|
|
793
|
+
},
|
|
794
|
+
runtime_authority: false,
|
|
795
|
+
receipts_projection: sanitizeRecord(params.receiptsProjection),
|
|
796
|
+
missing: params.missing ?? [],
|
|
797
|
+
reasons: params.reasons,
|
|
798
|
+
boundaries: cloudReceiptsBoundaries(),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function buildUploadResult(params) {
|
|
802
|
+
const networkPerformed = params.networkPerformed ?? false;
|
|
803
|
+
const localReady = localUploadReady(params.localReceipt);
|
|
804
|
+
const readinessAction = uploadReadinessAction({
|
|
805
|
+
status: params.status,
|
|
806
|
+
localReady,
|
|
807
|
+
endpoint: params.endpoint,
|
|
808
|
+
endpointSupported: params.endpointSupported,
|
|
809
|
+
tokenPresent: params.tokenPresent,
|
|
810
|
+
networkPerformed,
|
|
811
|
+
});
|
|
812
|
+
const rejectionContext = uploadRejectionContext({
|
|
813
|
+
status: params.status,
|
|
814
|
+
localReady,
|
|
815
|
+
endpoint: params.endpoint,
|
|
816
|
+
endpointSupported: params.endpointSupported,
|
|
817
|
+
tokenPresent: params.tokenPresent,
|
|
818
|
+
networkPerformed,
|
|
819
|
+
cloudHttpOk: params.cloudHttpOk,
|
|
820
|
+
cloudReceipt: params.cloudReceipt,
|
|
821
|
+
nextAction: readinessAction.nextAction,
|
|
822
|
+
nextCommand: readinessAction.nextCommand,
|
|
823
|
+
});
|
|
824
|
+
return {
|
|
825
|
+
schema_version: "intentdna.report_upload_result.v1",
|
|
826
|
+
generated_at: params.generatedAt,
|
|
827
|
+
status: params.status,
|
|
828
|
+
envelope_path: params.envelopePath,
|
|
829
|
+
endpoint_input: params.endpointInput,
|
|
830
|
+
endpoint: params.endpoint,
|
|
831
|
+
endpoint_normalized_to: params.endpointSupported ? params.endpoint : undefined,
|
|
832
|
+
auth: {
|
|
833
|
+
token_present: params.tokenPresent,
|
|
834
|
+
source: params.authSource,
|
|
835
|
+
},
|
|
836
|
+
readiness: {
|
|
837
|
+
local_verifier: params.localReceipt?.status ?? "rejected",
|
|
838
|
+
local_verified: localReady,
|
|
839
|
+
package_available: Boolean(params.localReceipt?.package_ref),
|
|
840
|
+
endpoint_present: Boolean(params.endpoint),
|
|
841
|
+
endpoint_normalized: params.endpointSupported,
|
|
842
|
+
endpoint_supported: params.endpointSupported,
|
|
843
|
+
endpoint_rejection_reason: params.endpoint && !params.endpointSupported
|
|
844
|
+
? "cloud ingest endpoint must be a Supabase project URL or an ingest-report Edge Function URL"
|
|
845
|
+
: undefined,
|
|
846
|
+
auth_token_present: params.tokenPresent,
|
|
847
|
+
auth_source: params.authSource,
|
|
848
|
+
upload_allowed: params.status === "dry_run" && localReady && params.endpointSupported && params.tokenPresent && !networkPerformed,
|
|
849
|
+
network_performed: networkPerformed,
|
|
850
|
+
next_action: readinessAction.nextAction,
|
|
851
|
+
next_command: readinessAction.nextCommand,
|
|
852
|
+
},
|
|
853
|
+
redaction: {
|
|
854
|
+
cloud_storage_projection: localReady ? "redacted_report_package_projection" : "not_prepared",
|
|
855
|
+
original_package_sha256: params.localReceipt?.package_ref?.sha256,
|
|
856
|
+
raw_runtime_payload_persisted: false,
|
|
857
|
+
runtime_authority: false,
|
|
858
|
+
note: localReady
|
|
859
|
+
? "Cloud ingestion verifies the signed package hash, then persists a redacted storage projection."
|
|
860
|
+
: "Cloud storage projection is not prepared until local envelope verification passes.",
|
|
861
|
+
},
|
|
862
|
+
local_receipt: params.localReceipt,
|
|
863
|
+
cloud_receipt: params.cloudReceipt,
|
|
864
|
+
rejection_context: rejectionContext,
|
|
865
|
+
package_ref: params.localReceipt?.package_ref,
|
|
866
|
+
transport: {
|
|
867
|
+
network_performed: networkPerformed,
|
|
868
|
+
hidden_transport: false,
|
|
869
|
+
},
|
|
870
|
+
reasons: params.reasons,
|
|
871
|
+
boundaries: cloudUploadBoundaries(),
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function uploadRejectionContext(params) {
|
|
875
|
+
if (params.status !== "rejected")
|
|
876
|
+
return undefined;
|
|
877
|
+
const cloudReceiptReasons = params.cloudReceipt ? cloudIngestReceiptRejectionReasons(params.cloudReceipt) : [];
|
|
878
|
+
const source = !params.localReady
|
|
879
|
+
? "local_verification"
|
|
880
|
+
: !params.tokenPresent
|
|
881
|
+
? "cli_auth"
|
|
882
|
+
: !params.endpoint || !params.endpointSupported
|
|
883
|
+
? "endpoint"
|
|
884
|
+
: params.networkPerformed && params.cloudHttpOk === false
|
|
885
|
+
? "cloud_http"
|
|
886
|
+
: params.networkPerformed && cloudReceiptReasons.length > 0
|
|
887
|
+
? "cloud_response_schema"
|
|
888
|
+
: params.networkPerformed && params.cloudReceipt
|
|
889
|
+
? "cloud_rejected_receipt"
|
|
890
|
+
: params.networkPerformed
|
|
891
|
+
? "cloud_http"
|
|
892
|
+
: "unknown";
|
|
893
|
+
return {
|
|
894
|
+
source,
|
|
895
|
+
network_performed: params.networkPerformed,
|
|
896
|
+
trusted_cloud_ingestion: asRecord(params.cloudReceipt)?.trusted_cloud_ingestion === true,
|
|
897
|
+
runtime_authority: false,
|
|
898
|
+
next_action: params.nextAction,
|
|
899
|
+
next_command: params.nextCommand,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function cloudIngestReceiptRejectionReasons(value) {
|
|
903
|
+
const record = asRecord(value);
|
|
904
|
+
if (!record)
|
|
905
|
+
return ["rejected: cloud ingestion response must be a JSON object"];
|
|
906
|
+
const reasons = [];
|
|
907
|
+
if (record.schema_version !== "intentdna.report_ingest_receipt.v1") {
|
|
908
|
+
reasons.push(typeof record.schema_version === "string"
|
|
909
|
+
? `rejected: cloud ingestion response schema ${record.schema_version} is unsupported; expected intentdna.report_ingest_receipt.v1`
|
|
910
|
+
: "rejected: cloud ingestion response schema_version is missing; expected intentdna.report_ingest_receipt.v1");
|
|
911
|
+
}
|
|
912
|
+
if (record.trusted_cloud_ingestion !== true) {
|
|
913
|
+
reasons.push("rejected: cloud ingestion receipt must declare trusted_cloud_ingestion=true");
|
|
914
|
+
}
|
|
915
|
+
if (record.runtime_authority !== false) {
|
|
916
|
+
reasons.push("rejected: cloud ingestion receipt must declare runtime_authority=false");
|
|
917
|
+
}
|
|
918
|
+
if (record.status !== "verified" && record.status !== "pending" && record.status !== "rejected") {
|
|
919
|
+
reasons.push("rejected: cloud ingestion receipt status must be verified, pending, or rejected");
|
|
920
|
+
}
|
|
921
|
+
return reasons;
|
|
922
|
+
}
|
|
923
|
+
function localUploadVerificationReasons(localReceipt) {
|
|
924
|
+
if (localReceipt.status === "verified" && localReceipt.package_ref)
|
|
925
|
+
return [];
|
|
926
|
+
return [
|
|
927
|
+
`rejected: local envelope verification returned ${localReceipt.status}; cloud upload requires a verified local envelope and available package`,
|
|
928
|
+
...localReceipt.local_verifier.reasons,
|
|
929
|
+
];
|
|
930
|
+
}
|
|
931
|
+
async function uploadIngestEnvelope(opts, projectDir, envelopePath) {
|
|
932
|
+
const generatedAt = new Date().toISOString();
|
|
933
|
+
const uploadEndpoint = buildUploadEndpoint(opts);
|
|
934
|
+
const endpoint = uploadEndpoint.endpoint;
|
|
935
|
+
const auth = await resolveAuthToken({ env: opts.env, configDir: opts.configDir });
|
|
936
|
+
const localReceipt = await verifyIngestEnvelope(envelopePath);
|
|
937
|
+
const localVerificationReasons = localUploadVerificationReasons(localReceipt);
|
|
938
|
+
if (localVerificationReasons.length > 0) {
|
|
939
|
+
const result = buildUploadResult({
|
|
940
|
+
generatedAt,
|
|
941
|
+
status: "rejected",
|
|
942
|
+
envelopePath,
|
|
943
|
+
endpointInput: uploadEndpoint.input,
|
|
944
|
+
endpoint,
|
|
945
|
+
endpointSupported: uploadEndpoint.endpointSupported,
|
|
946
|
+
tokenPresent: Boolean(auth.token),
|
|
947
|
+
authSource: auth.source,
|
|
948
|
+
localReceipt,
|
|
949
|
+
reasons: localVerificationReasons,
|
|
950
|
+
});
|
|
951
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
952
|
+
return 1;
|
|
953
|
+
}
|
|
954
|
+
if (!auth.token) {
|
|
955
|
+
const result = buildUploadResult({
|
|
956
|
+
generatedAt,
|
|
957
|
+
status: "rejected",
|
|
958
|
+
envelopePath,
|
|
959
|
+
endpointInput: uploadEndpoint.input,
|
|
960
|
+
endpoint,
|
|
961
|
+
endpointSupported: uploadEndpoint.endpointSupported,
|
|
962
|
+
tokenPresent: false,
|
|
963
|
+
authSource: auth.source,
|
|
964
|
+
localReceipt,
|
|
965
|
+
reasons: [missingCliApiTokenReason()],
|
|
966
|
+
});
|
|
967
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
968
|
+
return 2;
|
|
969
|
+
}
|
|
970
|
+
if (isRejectedServiceRoleToken(auth.token, opts.env ?? process.env)) {
|
|
971
|
+
const result = buildUploadResult({
|
|
972
|
+
generatedAt,
|
|
973
|
+
status: "rejected",
|
|
974
|
+
envelopePath,
|
|
975
|
+
endpointInput: uploadEndpoint.input,
|
|
976
|
+
endpoint,
|
|
977
|
+
endpointSupported: uploadEndpoint.endpointSupported,
|
|
978
|
+
tokenPresent: true,
|
|
979
|
+
authSource: auth.source,
|
|
980
|
+
localReceipt,
|
|
981
|
+
reasons: ["rejected: Supabase service role keys are server-only and cannot be used for report upload CLI auth."],
|
|
982
|
+
});
|
|
983
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
984
|
+
return 1;
|
|
985
|
+
}
|
|
986
|
+
if (!endpoint) {
|
|
987
|
+
const result = buildUploadResult({
|
|
988
|
+
generatedAt,
|
|
989
|
+
status: "rejected",
|
|
990
|
+
envelopePath,
|
|
991
|
+
endpointInput: uploadEndpoint.input,
|
|
992
|
+
endpointSupported: uploadEndpoint.endpointSupported,
|
|
993
|
+
tokenPresent: true,
|
|
994
|
+
authSource: auth.source,
|
|
995
|
+
localReceipt,
|
|
996
|
+
reasons: ["rejected: missing cloud ingest endpoint; set INTENTDNA_CLOUD_URL, SUPABASE_URL, or pass --endpoint"],
|
|
997
|
+
});
|
|
998
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
999
|
+
return 2;
|
|
1000
|
+
}
|
|
1001
|
+
if (!uploadEndpoint.endpointSupported) {
|
|
1002
|
+
const result = buildUploadResult({
|
|
1003
|
+
generatedAt,
|
|
1004
|
+
status: "rejected",
|
|
1005
|
+
envelopePath,
|
|
1006
|
+
endpointInput: uploadEndpoint.input,
|
|
1007
|
+
endpoint,
|
|
1008
|
+
endpointSupported: false,
|
|
1009
|
+
tokenPresent: true,
|
|
1010
|
+
authSource: auth.source,
|
|
1011
|
+
localReceipt,
|
|
1012
|
+
reasons: ["rejected: cloud ingest endpoint must be a Supabase project URL or an ingest-report Edge Function URL"],
|
|
1013
|
+
});
|
|
1014
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1015
|
+
return 2;
|
|
1016
|
+
}
|
|
1017
|
+
if (opts.dryRun) {
|
|
1018
|
+
const result = buildUploadResult({
|
|
1019
|
+
generatedAt,
|
|
1020
|
+
status: "dry_run",
|
|
1021
|
+
envelopePath,
|
|
1022
|
+
endpointInput: uploadEndpoint.input,
|
|
1023
|
+
endpoint,
|
|
1024
|
+
endpointSupported: uploadEndpoint.endpointSupported,
|
|
1025
|
+
tokenPresent: true,
|
|
1026
|
+
authSource: auth.source,
|
|
1027
|
+
localReceipt,
|
|
1028
|
+
reasons: ["dry_run: upload request is ready but no network request was performed"],
|
|
1029
|
+
});
|
|
1030
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1031
|
+
return 0;
|
|
1032
|
+
}
|
|
1033
|
+
const packageRef = localReceipt.package_ref;
|
|
1034
|
+
if (!packageRef) {
|
|
1035
|
+
throw new Error("verified local receipt is missing package_ref");
|
|
1036
|
+
}
|
|
1037
|
+
const [envelopeRaw, packageRaw] = await Promise.all([
|
|
1038
|
+
readFile(envelopePath, "utf-8"),
|
|
1039
|
+
readFile(resolveProjectPath(projectDir, packageRef.path), "utf-8"),
|
|
1040
|
+
]);
|
|
1041
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
1042
|
+
const response = await fetchImpl(endpoint, {
|
|
1043
|
+
method: "POST",
|
|
1044
|
+
headers: {
|
|
1045
|
+
"content-type": "application/json",
|
|
1046
|
+
authorization: `Bearer ${auth.token}`,
|
|
1047
|
+
"x-intentdna-cli": "dna report upload",
|
|
1048
|
+
},
|
|
1049
|
+
body: JSON.stringify({
|
|
1050
|
+
schema_version: "intentdna.cloud_ingest_request.v1",
|
|
1051
|
+
envelope: JSON.parse(envelopeRaw),
|
|
1052
|
+
package_json: packageRaw,
|
|
1053
|
+
local_receipt: localReceipt,
|
|
1054
|
+
transport: {
|
|
1055
|
+
kind: "https_edge_function",
|
|
1056
|
+
network_performed: true,
|
|
1057
|
+
hidden_transport: false,
|
|
1058
|
+
},
|
|
1059
|
+
}),
|
|
1060
|
+
});
|
|
1061
|
+
const text = await response.text();
|
|
1062
|
+
let parsed;
|
|
1063
|
+
try {
|
|
1064
|
+
parsed = JSON.parse(text);
|
|
1065
|
+
}
|
|
1066
|
+
catch {
|
|
1067
|
+
parsed = undefined;
|
|
1068
|
+
}
|
|
1069
|
+
if (!parsed) {
|
|
1070
|
+
const result = buildUploadResult({
|
|
1071
|
+
generatedAt,
|
|
1072
|
+
status: "rejected",
|
|
1073
|
+
envelopePath,
|
|
1074
|
+
endpointInput: uploadEndpoint.input,
|
|
1075
|
+
endpoint,
|
|
1076
|
+
endpointSupported: uploadEndpoint.endpointSupported,
|
|
1077
|
+
tokenPresent: true,
|
|
1078
|
+
authSource: auth.source,
|
|
1079
|
+
localReceipt,
|
|
1080
|
+
networkPerformed: true,
|
|
1081
|
+
cloudHttpOk: false,
|
|
1082
|
+
reasons: [`rejected: cloud ingestion endpoint returned HTTP ${response.status}`],
|
|
1083
|
+
});
|
|
1084
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1085
|
+
return 1;
|
|
1086
|
+
}
|
|
1087
|
+
const cloudReceiptReasons = cloudIngestReceiptRejectionReasons(parsed);
|
|
1088
|
+
if (cloudReceiptReasons.length > 0) {
|
|
1089
|
+
const sanitizedReceipt = sanitizeCloudReceipt(parsed);
|
|
1090
|
+
const result = buildUploadResult({
|
|
1091
|
+
generatedAt,
|
|
1092
|
+
status: "rejected",
|
|
1093
|
+
envelopePath,
|
|
1094
|
+
endpointInput: uploadEndpoint.input,
|
|
1095
|
+
endpoint,
|
|
1096
|
+
endpointSupported: uploadEndpoint.endpointSupported,
|
|
1097
|
+
tokenPresent: true,
|
|
1098
|
+
authSource: auth.source,
|
|
1099
|
+
localReceipt,
|
|
1100
|
+
cloudReceipt: sanitizedReceipt,
|
|
1101
|
+
networkPerformed: true,
|
|
1102
|
+
cloudHttpOk: response.ok,
|
|
1103
|
+
reasons: cloudReceiptReasons,
|
|
1104
|
+
});
|
|
1105
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1106
|
+
return 1;
|
|
1107
|
+
}
|
|
1108
|
+
const sanitizedReceipt = sanitizeCloudReceipt(parsed) ?? parsed;
|
|
1109
|
+
const receipt = asRecord(sanitizedReceipt);
|
|
1110
|
+
if (!response.ok) {
|
|
1111
|
+
const cloudReasons = reasonsFromProjection(receipt?.cloud_verifier);
|
|
1112
|
+
const result = buildUploadResult({
|
|
1113
|
+
generatedAt,
|
|
1114
|
+
status: "rejected",
|
|
1115
|
+
envelopePath,
|
|
1116
|
+
endpointInput: uploadEndpoint.input,
|
|
1117
|
+
endpoint,
|
|
1118
|
+
endpointSupported: uploadEndpoint.endpointSupported,
|
|
1119
|
+
tokenPresent: true,
|
|
1120
|
+
authSource: auth.source,
|
|
1121
|
+
localReceipt,
|
|
1122
|
+
cloudReceipt: receipt,
|
|
1123
|
+
networkPerformed: true,
|
|
1124
|
+
cloudHttpOk: false,
|
|
1125
|
+
reasons: [
|
|
1126
|
+
`rejected: cloud ingestion endpoint returned HTTP ${response.status}`,
|
|
1127
|
+
...cloudReasons,
|
|
1128
|
+
],
|
|
1129
|
+
});
|
|
1130
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1131
|
+
return 1;
|
|
1132
|
+
}
|
|
1133
|
+
if (receipt?.status === "rejected") {
|
|
1134
|
+
const cloudReasons = reasonsFromProjection(receipt.cloud_verifier);
|
|
1135
|
+
const result = buildUploadResult({
|
|
1136
|
+
generatedAt,
|
|
1137
|
+
status: "rejected",
|
|
1138
|
+
envelopePath,
|
|
1139
|
+
endpointInput: uploadEndpoint.input,
|
|
1140
|
+
endpoint,
|
|
1141
|
+
endpointSupported: uploadEndpoint.endpointSupported,
|
|
1142
|
+
tokenPresent: true,
|
|
1143
|
+
authSource: auth.source,
|
|
1144
|
+
localReceipt,
|
|
1145
|
+
cloudReceipt: receipt,
|
|
1146
|
+
networkPerformed: true,
|
|
1147
|
+
cloudHttpOk: true,
|
|
1148
|
+
reasons: cloudReasons.length > 0 ? cloudReasons : ["rejected: cloud ingestion returned a rejected receipt"],
|
|
1149
|
+
});
|
|
1150
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1151
|
+
return 1;
|
|
1152
|
+
}
|
|
1153
|
+
const cloudReasons = reasonsFromProjection(receipt?.cloud_verifier);
|
|
1154
|
+
const result = buildUploadResult({
|
|
1155
|
+
generatedAt,
|
|
1156
|
+
status: receipt?.status === "pending" ? "pending" : "verified",
|
|
1157
|
+
envelopePath,
|
|
1158
|
+
endpointInput: uploadEndpoint.input,
|
|
1159
|
+
endpoint,
|
|
1160
|
+
endpointSupported: uploadEndpoint.endpointSupported,
|
|
1161
|
+
tokenPresent: true,
|
|
1162
|
+
authSource: auth.source,
|
|
1163
|
+
localReceipt,
|
|
1164
|
+
cloudReceipt: receipt,
|
|
1165
|
+
networkPerformed: true,
|
|
1166
|
+
cloudHttpOk: true,
|
|
1167
|
+
reasons: cloudReasons.length > 0 ? cloudReasons : [`${receipt?.status ?? "verified"}: cloud ingestion returned a trusted receipt`],
|
|
1168
|
+
});
|
|
1169
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1170
|
+
return 0;
|
|
1171
|
+
}
|
|
1172
|
+
function statusFromCloudReceiptsProjection(value) {
|
|
1173
|
+
const record = asRecord(value);
|
|
1174
|
+
const status = record?.status;
|
|
1175
|
+
return status === "ready" || status === "pending" || status === "rejected" ? status : undefined;
|
|
1176
|
+
}
|
|
1177
|
+
function reasonsFromProjection(value) {
|
|
1178
|
+
const record = asRecord(value);
|
|
1179
|
+
const reasons = record?.reasons;
|
|
1180
|
+
return Array.isArray(reasons) ? reasons.map((reason) => String(reason)) : [];
|
|
1181
|
+
}
|
|
1182
|
+
async function listCloudReceipts(opts) {
|
|
1183
|
+
const env = opts.env ?? process.env;
|
|
1184
|
+
const endpoint = buildAccountEndpoint({
|
|
1185
|
+
subcommand: "report-receipts",
|
|
1186
|
+
endpoint: opts.endpoint,
|
|
1187
|
+
env,
|
|
1188
|
+
}, "list-receipts");
|
|
1189
|
+
const request = buildCloudReceiptsRequest(opts);
|
|
1190
|
+
const jwt = await resolveSupabaseUserJwt({
|
|
1191
|
+
userJwt: opts.userJwt,
|
|
1192
|
+
env,
|
|
1193
|
+
configDir: opts.configDir,
|
|
1194
|
+
});
|
|
1195
|
+
const missing = [
|
|
1196
|
+
!endpoint ? "INTENTDNA_CLOUD_URL or --endpoint <list-receipts-url>" : undefined,
|
|
1197
|
+
!jwt.token
|
|
1198
|
+
? "INTENTDNA_SUPABASE_USER_JWT, --user-jwt <supabase-user-access-token>, or dna auth user-jwt set <token>"
|
|
1199
|
+
: jwt.expired
|
|
1200
|
+
? "fresh Supabase user JWT from dna auth user-jwt set <token>"
|
|
1201
|
+
: undefined,
|
|
1202
|
+
!request.project_id ? "--project-id <supabase-project-uuid>" : undefined,
|
|
1203
|
+
].filter((item) => Boolean(item));
|
|
1204
|
+
if (jwt.token && isRejectedServiceRoleToken(jwt.token, env)) {
|
|
1205
|
+
const result = buildCloudReceiptsResult({
|
|
1206
|
+
status: "rejected",
|
|
1207
|
+
endpoint,
|
|
1208
|
+
request,
|
|
1209
|
+
jwt,
|
|
1210
|
+
missing: ["Supabase user JWT"],
|
|
1211
|
+
reasons: ["rejected: Supabase service role keys are server-only and cannot be used for cloud receipt listing."],
|
|
1212
|
+
});
|
|
1213
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1214
|
+
return 1;
|
|
1215
|
+
}
|
|
1216
|
+
if (opts.dryRun) {
|
|
1217
|
+
const result = buildCloudReceiptsResult({
|
|
1218
|
+
status: "dry_run",
|
|
1219
|
+
endpoint,
|
|
1220
|
+
request,
|
|
1221
|
+
jwt,
|
|
1222
|
+
missing,
|
|
1223
|
+
reasons: missing.length > 0
|
|
1224
|
+
? ["dry_run: cloud receipt listing request shape was built; live listing still needs missing inputs"]
|
|
1225
|
+
: ["dry_run: cloud receipt listing request is ready but no network request was performed"],
|
|
1226
|
+
});
|
|
1227
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1228
|
+
return 0;
|
|
1229
|
+
}
|
|
1230
|
+
if (missing.length > 0) {
|
|
1231
|
+
const result = buildCloudReceiptsResult({
|
|
1232
|
+
status: "rejected",
|
|
1233
|
+
endpoint,
|
|
1234
|
+
request,
|
|
1235
|
+
jwt,
|
|
1236
|
+
missing,
|
|
1237
|
+
reasons: ["rejected: missing inputs for live cloud receipt listing"],
|
|
1238
|
+
});
|
|
1239
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1240
|
+
return 1;
|
|
1241
|
+
}
|
|
1242
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
1243
|
+
const response = await fetchImpl(endpoint, {
|
|
1244
|
+
method: "POST",
|
|
1245
|
+
headers: {
|
|
1246
|
+
"content-type": "application/json",
|
|
1247
|
+
authorization: `Bearer ${jwt.token}`,
|
|
1248
|
+
"x-intentdna-cli": "dna report receipts",
|
|
1249
|
+
},
|
|
1250
|
+
body: JSON.stringify(request),
|
|
1251
|
+
});
|
|
1252
|
+
const text = await response.text();
|
|
1253
|
+
let parsed;
|
|
1254
|
+
try {
|
|
1255
|
+
parsed = JSON.parse(text);
|
|
1256
|
+
}
|
|
1257
|
+
catch {
|
|
1258
|
+
parsed = undefined;
|
|
1259
|
+
}
|
|
1260
|
+
if (!response.ok || !parsed) {
|
|
1261
|
+
const result = buildCloudReceiptsResult({
|
|
1262
|
+
status: "rejected",
|
|
1263
|
+
endpoint,
|
|
1264
|
+
request,
|
|
1265
|
+
jwt,
|
|
1266
|
+
networkPerformed: true,
|
|
1267
|
+
reasons: [`rejected: list-receipts endpoint returned HTTP ${response.status}`],
|
|
1268
|
+
});
|
|
1269
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1270
|
+
return 1;
|
|
1271
|
+
}
|
|
1272
|
+
const projectionStatus = statusFromCloudReceiptsProjection(parsed) ?? "rejected";
|
|
1273
|
+
const result = buildCloudReceiptsResult({
|
|
1274
|
+
status: projectionStatus,
|
|
1275
|
+
endpoint,
|
|
1276
|
+
request,
|
|
1277
|
+
jwt,
|
|
1278
|
+
networkPerformed: true,
|
|
1279
|
+
receiptsProjection: parsed,
|
|
1280
|
+
reasons: reasonsFromProjection(parsed).length > 0
|
|
1281
|
+
? reasonsFromProjection(parsed)
|
|
1282
|
+
: [`${projectionStatus}: cloud receipt projection response returned`],
|
|
1283
|
+
});
|
|
1284
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1285
|
+
return projectionStatus === "ready" || projectionStatus === "pending" ? 0 : 1;
|
|
1286
|
+
}
|
|
1287
|
+
async function verifyIngestEnvelope(envelopePath) {
|
|
1288
|
+
const generatedAt = new Date().toISOString();
|
|
1289
|
+
let raw;
|
|
1290
|
+
try {
|
|
1291
|
+
raw = await readFile(envelopePath, "utf-8");
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
return buildReceipt({
|
|
1295
|
+
envelopePath,
|
|
1296
|
+
signatureValid: false,
|
|
1297
|
+
packageAvailable: false,
|
|
1298
|
+
packageHashValid: "unchecked",
|
|
1299
|
+
reasons: ["rejected: envelope file is missing"],
|
|
1300
|
+
generatedAt,
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
let parsed;
|
|
1304
|
+
try {
|
|
1305
|
+
parsed = JSON.parse(raw);
|
|
1306
|
+
}
|
|
1307
|
+
catch {
|
|
1308
|
+
return buildReceipt({
|
|
1309
|
+
envelopePath,
|
|
1310
|
+
envelopeRaw: raw,
|
|
1311
|
+
signatureValid: false,
|
|
1312
|
+
packageAvailable: false,
|
|
1313
|
+
packageHashValid: "unchecked",
|
|
1314
|
+
reasons: ["rejected: envelope is not valid JSON"],
|
|
1315
|
+
generatedAt,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
const envelope = parseIngestEnvelope(parsed);
|
|
1319
|
+
if (!envelope) {
|
|
1320
|
+
return buildReceipt({
|
|
1321
|
+
envelopePath,
|
|
1322
|
+
envelopeRaw: raw,
|
|
1323
|
+
signatureValid: false,
|
|
1324
|
+
packageAvailable: false,
|
|
1325
|
+
packageHashValid: "unchecked",
|
|
1326
|
+
reasons: invalidIngestEnvelopeReasons(parsed),
|
|
1327
|
+
generatedAt,
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
const payload = signedPayloadForEnvelope(envelope);
|
|
1331
|
+
const payloadJson = canonicalJson(payload);
|
|
1332
|
+
const reasons = [];
|
|
1333
|
+
const payloadHashValid = sha256(payloadJson) === envelope.signing.signed_payload_sha256;
|
|
1334
|
+
let signatureValid = false;
|
|
1335
|
+
try {
|
|
1336
|
+
signatureValid = payloadHashValid && verify(null, Buffer.from(payloadJson), createPublicKey(envelope.signing.public_key_pem), Buffer.from(envelope.signing.signature, "base64"));
|
|
1337
|
+
}
|
|
1338
|
+
catch {
|
|
1339
|
+
signatureValid = false;
|
|
1340
|
+
}
|
|
1341
|
+
if (!payloadHashValid)
|
|
1342
|
+
reasons.push("rejected: signed payload hash does not match envelope fields");
|
|
1343
|
+
if (!signatureValid)
|
|
1344
|
+
reasons.push("rejected: Ed25519 signature is invalid");
|
|
1345
|
+
reasons.push(...validateEnvelopeFreshness(envelope));
|
|
1346
|
+
let packageAvailable = false;
|
|
1347
|
+
let packageHashValid = "unchecked";
|
|
1348
|
+
try {
|
|
1349
|
+
const packageRaw = await readFile(envelope.package_ref.path, "utf-8");
|
|
1350
|
+
packageAvailable = true;
|
|
1351
|
+
packageHashValid = sha256(packageRaw) === envelope.package_ref.sha256;
|
|
1352
|
+
if (!packageHashValid)
|
|
1353
|
+
reasons.push("rejected: report package hash does not match envelope package_ref");
|
|
1354
|
+
}
|
|
1355
|
+
catch {
|
|
1356
|
+
reasons.push("pending: referenced report package file is unavailable");
|
|
1357
|
+
}
|
|
1358
|
+
if (reasons.length === 0)
|
|
1359
|
+
reasons.push("verified: local envelope signature and package hash match");
|
|
1360
|
+
return buildReceipt({
|
|
1361
|
+
envelopePath,
|
|
1362
|
+
envelopeRaw: raw,
|
|
1363
|
+
envelope,
|
|
1364
|
+
signatureValid,
|
|
1365
|
+
packageAvailable,
|
|
1366
|
+
packageHashValid,
|
|
1367
|
+
reasons,
|
|
1368
|
+
generatedAt,
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
export async function runReport(opts) {
|
|
1372
|
+
const projectDir = resolve(opts.projectDir ?? process.cwd());
|
|
1373
|
+
if (opts.action === "receipts") {
|
|
1374
|
+
return listCloudReceipts(opts);
|
|
1375
|
+
}
|
|
1376
|
+
if (opts.action === "upload") {
|
|
1377
|
+
const envelopePath = opts.envelopePath ? resolveProjectPath(projectDir, opts.envelopePath) : "";
|
|
1378
|
+
if (!envelopePath) {
|
|
1379
|
+
process.stderr.write("Usage: dna report upload <envelope-path> [--json] [--endpoint <url>] [--dry-run]\n");
|
|
1380
|
+
return 2;
|
|
1381
|
+
}
|
|
1382
|
+
return uploadIngestEnvelope(opts, projectDir, envelopePath);
|
|
1383
|
+
}
|
|
1384
|
+
if (opts.action === "verify") {
|
|
1385
|
+
const envelopePath = opts.envelopePath ? resolveProjectPath(projectDir, opts.envelopePath) : "";
|
|
1386
|
+
if (!envelopePath) {
|
|
1387
|
+
process.stderr.write("Usage: dna report verify <envelope-path> [--json]\n");
|
|
1388
|
+
return 2;
|
|
1389
|
+
}
|
|
1390
|
+
const receipt = await verifyIngestEnvelope(envelopePath);
|
|
1391
|
+
if (opts.json) {
|
|
1392
|
+
process.stdout.write(JSON.stringify(receipt, null, 2) + "\n");
|
|
1393
|
+
}
|
|
1394
|
+
else {
|
|
1395
|
+
process.stderr.write([
|
|
1396
|
+
`Report ingest receipt: ${receipt.status}`,
|
|
1397
|
+
`Envelope: ${receipt.envelope_ref.path}`,
|
|
1398
|
+
`Package: ${receipt.package_ref?.path ?? "(unavailable)"}`,
|
|
1399
|
+
`Signature valid: ${String(receipt.local_verifier.signature_valid)}`,
|
|
1400
|
+
`Package available: ${String(receipt.local_verifier.package_available)}`,
|
|
1401
|
+
`Package hash valid: ${String(receipt.local_verifier.package_hash_valid)}`,
|
|
1402
|
+
`Cloud upload eligible: ${String(receipt.upload_gate.cloud_upload_eligible)}`,
|
|
1403
|
+
`Next action: ${receipt.upload_gate.next_action}`,
|
|
1404
|
+
`Next command: ${receipt.upload_gate.next_command}`,
|
|
1405
|
+
...receipt.local_verifier.reasons.map((reason) => `Reason: ${reason}`),
|
|
1406
|
+
...receipt.boundaries.map((boundary) => `Boundary: ${boundary}`),
|
|
1407
|
+
].join("\n") + "\n");
|
|
1408
|
+
}
|
|
1409
|
+
return receipt.status === "rejected" ? 1 : 0;
|
|
1410
|
+
}
|
|
1411
|
+
const packagePath = opts.action === "sign" && opts.packagePath
|
|
1412
|
+
? resolveProjectPath(projectDir, opts.packagePath)
|
|
1413
|
+
: opts.action === "sign" && !opts.packagePath
|
|
1414
|
+
? defaultPackagePath(projectDir)
|
|
1415
|
+
: opts.output ? resolveProjectPath(projectDir, opts.output) : defaultPackagePath(projectDir);
|
|
1416
|
+
const packageInput = await readReportPackageForSign(opts, projectDir, packagePath);
|
|
1417
|
+
if (!packageInput.ok) {
|
|
1418
|
+
const generatedAt = new Date().toISOString();
|
|
1419
|
+
const result = buildReportSignRejection({
|
|
1420
|
+
generatedAt,
|
|
1421
|
+
packagePath,
|
|
1422
|
+
reasons: packageInput.reasons,
|
|
1423
|
+
});
|
|
1424
|
+
if (opts.json) {
|
|
1425
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
process.stderr.write([
|
|
1429
|
+
"Report package rejected before signing.",
|
|
1430
|
+
`Report package: ${packagePath}`,
|
|
1431
|
+
"Cloud upload allowed: false",
|
|
1432
|
+
"Next command: dna report package --json",
|
|
1433
|
+
...result.reasons.map((reason) => `Reason: ${reason}`),
|
|
1434
|
+
...result.boundaries.map((boundary) => `Boundary: ${boundary}`),
|
|
1435
|
+
].join("\n") + "\n");
|
|
1436
|
+
}
|
|
1437
|
+
return 1;
|
|
1438
|
+
}
|
|
1439
|
+
const { reportPackage, packageJson } = packageInput;
|
|
1440
|
+
if (opts.action === "package") {
|
|
1441
|
+
if (!opts.dryRun) {
|
|
1442
|
+
await mkdir(dirname(packagePath), { recursive: true });
|
|
1443
|
+
await writeFile(packagePath, packageJson, "utf-8");
|
|
1444
|
+
}
|
|
1445
|
+
if (opts.json) {
|
|
1446
|
+
process.stdout.write(JSON.stringify(buildReportPackageResult({
|
|
1447
|
+
generatedAt: new Date().toISOString(),
|
|
1448
|
+
status: opts.dryRun ? "dry_run" : "written",
|
|
1449
|
+
path: packagePath,
|
|
1450
|
+
reportPackage,
|
|
1451
|
+
}), null, 2) + "\n");
|
|
1452
|
+
}
|
|
1453
|
+
else {
|
|
1454
|
+
process.stderr.write(opts.dryRun ? packageSummary(reportPackage, `${packagePath} (dry-run)`) : packageSummary(reportPackage, packagePath));
|
|
1455
|
+
}
|
|
1456
|
+
return 0;
|
|
1457
|
+
}
|
|
1458
|
+
if (opts.action === "sign") {
|
|
1459
|
+
const generatedAt = new Date().toISOString();
|
|
1460
|
+
const envelopePath = opts.dryRun
|
|
1461
|
+
? undefined
|
|
1462
|
+
: opts.output ? resolveProjectPath(projectDir, opts.output) : defaultIngestEnvelopePath(projectDir, generatedAt);
|
|
1463
|
+
const key = opts.dryRun ? createEphemeralSigningKey() : await readOrCreateSigningKey(projectDir);
|
|
1464
|
+
const envelope = buildIngestEnvelope({
|
|
1465
|
+
reportPackage,
|
|
1466
|
+
packagePath,
|
|
1467
|
+
envelopePath,
|
|
1468
|
+
packageJson,
|
|
1469
|
+
generatedAt,
|
|
1470
|
+
key,
|
|
1471
|
+
opts,
|
|
1472
|
+
});
|
|
1473
|
+
if (!opts.dryRun) {
|
|
1474
|
+
if (!opts.packagePath) {
|
|
1475
|
+
await mkdir(dirname(packagePath), { recursive: true });
|
|
1476
|
+
await writeFile(packagePath, packageJson, "utf-8");
|
|
1477
|
+
}
|
|
1478
|
+
await writeJson(envelopePath, envelope);
|
|
1479
|
+
}
|
|
1480
|
+
if (opts.json) {
|
|
1481
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
|
|
1482
|
+
}
|
|
1483
|
+
else {
|
|
1484
|
+
process.stderr.write([
|
|
1485
|
+
`Report ingest envelope signed: ${envelopePath ?? "(dry-run)"}`,
|
|
1486
|
+
`Report package: ${packagePath}`,
|
|
1487
|
+
`Package hash: ${envelope.package_ref.sha256}`,
|
|
1488
|
+
`Key id: ${envelope.signing.key_id}`,
|
|
1489
|
+
`Network performed: ${String(envelope.transport.network_performed)}`,
|
|
1490
|
+
...envelope.boundaries.map((boundary) => `Boundary: ${boundary}`),
|
|
1491
|
+
].join("\n") + "\n");
|
|
1492
|
+
}
|
|
1493
|
+
return 0;
|
|
1494
|
+
}
|
|
1495
|
+
const generatedAt = new Date().toISOString();
|
|
1496
|
+
const outboxPath = opts.dryRun ? undefined : defaultOutboxPath(projectDir, generatedAt);
|
|
1497
|
+
const envelope = buildEnvelope(reportPackage, packagePath, outboxPath, packageJson, generatedAt, opts);
|
|
1498
|
+
if (!opts.dryRun) {
|
|
1499
|
+
await mkdir(dirname(packagePath), { recursive: true });
|
|
1500
|
+
await writeFile(packagePath, packageJson, "utf-8");
|
|
1501
|
+
await writeJson(outboxPath, envelope);
|
|
1502
|
+
}
|
|
1503
|
+
if (opts.json) {
|
|
1504
|
+
process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
|
|
1505
|
+
}
|
|
1506
|
+
else {
|
|
1507
|
+
process.stderr.write([
|
|
1508
|
+
`Report push staged: ${outboxPath ?? "(dry-run)"}`,
|
|
1509
|
+
`Report package: ${packagePath}`,
|
|
1510
|
+
`Runtime decisions: ${envelope.package_ref.runtime_decisions}`,
|
|
1511
|
+
`Network performed: ${String(envelope.transport.network_performed)}`,
|
|
1512
|
+
...envelope.boundaries.map((boundary) => `Boundary: ${boundary}`),
|
|
1513
|
+
].join("\n") + "\n");
|
|
1514
|
+
}
|
|
1515
|
+
return 0;
|
|
1516
|
+
}
|