owlrunkit 0.13.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/CHANGELOG.md +9 -0
- package/LICENSE +674 -0
- package/NOTICE.md +137 -0
- package/README.md +44 -0
- package/SOURCE.md +10 -0
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_1.md +160 -0
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +459 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +408 -0
- package/integrations/codex/skills/owlcoda-runkit/agents/openai.yaml +4 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/agents-snippet.md +7 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +10 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/coverage-adopt-request.json +26 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/execution-delivery-packet.json +13 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/finalize-request.json +18 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/goal-contract.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/profiles.json +9 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/ready-for-commit-request.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/resource-preflight-request.json +56 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/resume-request.json +10 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/snapshot-request.json +23 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-context.json +15 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verification-receipt.json +11 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/verify-plan-request.json +31 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/visual-smoke-request.json +32 -0
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/worker-lease.json +7 -0
- package/package.json +24 -0
- package/packages/attest/LICENSE +674 -0
- package/packages/attest/README.md +63 -0
- package/packages/attest/cli/owlcoda-attest.mjs +155 -0
- package/packages/attest/consumer-surface-v1.json +255 -0
- package/packages/attest/distribution-manifest-v1.json +104 -0
- package/packages/attest/fixtures/bad/accepted-authority-leak.json +7 -0
- package/packages/attest/fixtures/bad/accepted-failed-receipt.json +7 -0
- package/packages/attest/fixtures/bad/active-context-binding-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/active-source-binding-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/context-fingerprint-mismatch.json +7 -0
- package/packages/attest/fixtures/bad/duplicate-object-key.json +8 -0
- package/packages/attest/fixtures/bad/multiple-active-leaves.json +10 -0
- package/packages/attest/fixtures/bad/orphan-replacement.json +7 -0
- package/packages/attest/fixtures/bad/root-shape-extra-property.json +8 -0
- package/packages/attest/fixtures/bad/source-fingerprint-mismatch.json +7 -0
- package/packages/attest/fixtures/good/accepted-replacement-chain.json +59 -0
- package/packages/attest/fixtures/good/blocked-single-leaf.json +9 -0
- package/packages/attest/fixtures/index.json +90 -0
- package/packages/attest/sbom.spdx.json +33 -0
- package/packages/attest/schemas/attestation-ref-v1.schema.json +44 -0
- package/packages/attest/schemas/attestation-result-v1.schema.json +163 -0
- package/packages/attest/schemas/offline-attestation-bundle-v1.schema.json +30 -0
- package/packages/attest/schemas/public-verification-bundle-v1.schema.json +74 -0
- package/packages/attest/schemas/quick-verification-receipt-v1.schema.json +219 -0
- package/packages/attest/schemas/workspace-snapshot-v1.schema.json +135 -0
- package/packages/attest/src/canonical.mjs +12 -0
- package/packages/attest/src/formal.mjs +762 -0
- package/packages/attest/src/index.mjs +113 -0
- package/packages/attest/src/offline-bundle.mjs +108 -0
- package/packages/attest/src/quick-receipt-contract.mjs +156 -0
- package/packages/attest/src/quick.mjs +274 -0
- package/packages/attest/src/reference-contract.mjs +64 -0
- package/packages/attest/src/reference.mjs +131 -0
- package/packages/attest/src/workspace-snapshot.mjs +171 -0
- package/registry-release-policy-v1.json +34 -0
- package/sbom.spdx.json +39 -0
- package/scripts/runkit-contract/acceptance-evidence.mjs +103 -0
- package/scripts/runkit-contract/core-contract.mjs +392 -0
- package/scripts/runkit-contract/coverage-adoption.mjs +414 -0
- package/scripts/runkit-contract/delivery-create.mjs +252 -0
- package/scripts/runkit-contract/delivery-selection.mjs +31 -0
- package/scripts/runkit-contract/finalize.mjs +790 -0
- package/scripts/runkit-contract/inspect-presentation.mjs +183 -0
- package/scripts/runkit-contract/install-codex-skill.mjs +239 -0
- package/scripts/runkit-contract/lease-lifecycle.mjs +324 -0
- package/scripts/runkit-contract/lifecycle-orchestration.mjs +415 -0
- package/scripts/runkit-contract/offline-store.mjs +248 -0
- package/scripts/runkit-contract/profile-impact.mjs +223 -0
- package/scripts/runkit-contract/provenance-common.mjs +153 -0
- package/scripts/runkit-contract/quick-attest.mjs +4 -0
- package/scripts/runkit-contract/quick-canonical.mjs +41 -0
- package/scripts/runkit-contract/quick-metrics.mjs +86 -0
- package/scripts/runkit-contract/quick-receipt.mjs +139 -0
- package/scripts/runkit-contract/quick-verify.mjs +201 -0
- package/scripts/runkit-contract/quick-workspace-snapshot.mjs +3 -0
- package/scripts/runkit-contract/ready-for-commit.mjs +188 -0
- package/scripts/runkit-contract/receipt-lineage.mjs +332 -0
- package/scripts/runkit-contract/registry-adoption-gate.mjs +117 -0
- package/scripts/runkit-contract/repair-execution.mjs +997 -0
- package/scripts/runkit-contract/resource-preflight.mjs +829 -0
- package/scripts/runkit-contract/resume-execution.mjs +569 -0
- package/scripts/runkit-contract/runkit-cli.mjs +1214 -0
- package/scripts/runkit-contract/schemas/closeout-receipt-v2.schema.json +42 -0
- package/scripts/runkit-contract/schemas/core-artifact-v2.schema.json +23 -0
- package/scripts/runkit-contract/schemas/core-artifact.schema.json +23 -0
- package/scripts/runkit-contract/schemas/coverage-adopt-request-v1.schema.json +66 -0
- package/scripts/runkit-contract/schemas/engine-pin-v2.schema.json +14 -0
- package/scripts/runkit-contract/schemas/engine-pin.schema.json +14 -0
- package/scripts/runkit-contract/schemas/evidence-coverage-index-v1.schema.json +131 -0
- package/scripts/runkit-contract/schemas/finalize-request-v1.schema.json +131 -0
- package/scripts/runkit-contract/schemas/profiles.schema.json +47 -0
- package/scripts/runkit-contract/schemas/project-config-v2.schema.json +13 -0
- package/scripts/runkit-contract/schemas/project-config.schema.json +13 -0
- package/scripts/runkit-contract/schemas/ready-for-commit-v1.schema.json +26 -0
- package/scripts/runkit-contract/schemas/replayable-evidence-v1.schema.json +82 -0
- package/scripts/runkit-contract/schemas/resource-preflight-request-v1.schema.json +178 -0
- package/scripts/runkit-contract/schemas/resource-preflight-v1.schema.json +153 -0
- package/scripts/runkit-contract/schemas/resume-attempt-v1.schema.json +80 -0
- package/scripts/runkit-contract/schemas/resume-request-v1.schema.json +77 -0
- package/scripts/runkit-contract/schemas/snapshot-v1.schema.json +71 -0
- package/scripts/runkit-contract/schemas/verification-context-v1.schema.json +92 -0
- package/scripts/runkit-contract/schemas/verification-plan-v1.schema.json +150 -0
- package/scripts/runkit-contract/schemas/verification-receipt-v2.schema.json +118 -0
- package/scripts/runkit-contract/schemas/verify-plan-request-v1.schema.json +56 -0
- package/scripts/runkit-contract/schemas/visual-smoke-request-v1.schema.json +64 -0
- package/scripts/runkit-contract/schemas/visual-smoke-result-v1.schema.json +68 -0
- package/scripts/runkit-contract/snapshot.mjs +340 -0
- package/scripts/runkit-contract/source-fingerprint.mjs +404 -0
- package/scripts/runkit-contract/templates/coverage-adopt-request.json +26 -0
- package/scripts/runkit-contract/templates/finalize-request.json +18 -0
- package/scripts/runkit-contract/templates/goal-contract.json +11 -0
- package/scripts/runkit-contract/templates/ready-for-commit-request.json +11 -0
- package/scripts/runkit-contract/templates/resource-preflight-request.json +56 -0
- package/scripts/runkit-contract/templates/resume-request.json +10 -0
- package/scripts/runkit-contract/templates/snapshot-request.json +23 -0
- package/scripts/runkit-contract/templates/verify-plan-request.json +31 -0
- package/scripts/runkit-contract/templates/visual-smoke-request.json +32 -0
- package/scripts/runkit-contract/templates/worker-lease.json +7 -0
- package/scripts/runkit-contract/verification-context.mjs +141 -0
- package/scripts/runkit-contract/verification-plan.mjs +608 -0
- package/scripts/runkit-contract/verification-receipt-gate.mjs +566 -0
- package/scripts/runkit-contract/visual-smoke.mjs +410 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
fstatSync,
|
|
6
|
+
lstatSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
export const HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
15
|
+
export const PUBLIC_VERIFIER_LIMITS = Object.freeze({
|
|
16
|
+
maxInputBytes: 1_048_576,
|
|
17
|
+
maxNestingDepth: 64,
|
|
18
|
+
maxJsonNodes: 50_000,
|
|
19
|
+
maxReceipts: 1_024,
|
|
20
|
+
});
|
|
21
|
+
export const FORMAL_ISSUE_CODES = Object.freeze([
|
|
22
|
+
"ACCEPTED_AUTHORITY_GRANTED",
|
|
23
|
+
"ACCEPTED_RECEIPT_NOT_PASSED",
|
|
24
|
+
"ACTIVE_RECEIPT_CONTEXT_BINDING_MISMATCH",
|
|
25
|
+
"ACTIVE_RECEIPT_SOURCE_BINDING_MISMATCH",
|
|
26
|
+
"CANONICAL_JSON_INVALID",
|
|
27
|
+
"CLOSEOUT_ACTIVE_RECEIPT_MISMATCH",
|
|
28
|
+
"CLOSEOUT_CONTEXT_BINDING_MISMATCH",
|
|
29
|
+
"CLOSEOUT_SOURCE_BINDING_MISMATCH",
|
|
30
|
+
"DUPLICATE_OBJECT_KEY",
|
|
31
|
+
"EXTERNAL_AUTHORITY_GRANTED",
|
|
32
|
+
"INPUT_FILE_INVALID",
|
|
33
|
+
"INPUT_JSON_INVALID",
|
|
34
|
+
"INPUT_SIZE_LIMIT_EXCEEDED",
|
|
35
|
+
"JSON_NESTING_LIMIT_EXCEEDED",
|
|
36
|
+
"JSON_NODE_LIMIT_EXCEEDED",
|
|
37
|
+
"PUBLIC_BUNDLE_SHAPE_INVALID",
|
|
38
|
+
"RECEIPT_AUTHORITY_GRANTED",
|
|
39
|
+
"RECEIPT_COUNT_LIMIT_EXCEEDED",
|
|
40
|
+
"RECEIPT_ID_DUPLICATE",
|
|
41
|
+
"RECEIPT_LINEAGE_CYCLE",
|
|
42
|
+
"RECEIPT_LINEAGE_MULTIPLE_ACTIVE_LEAVES",
|
|
43
|
+
"RECEIPT_LINEAGE_NO_ACTIVE_LEAF",
|
|
44
|
+
"RECEIPT_LINEAGE_ORPHAN",
|
|
45
|
+
"SOURCE_FINGERPRINT_MISMATCH",
|
|
46
|
+
"VERIFICATION_CONTEXT_FINGERPRINT_MISMATCH",
|
|
47
|
+
]);
|
|
48
|
+
const FORMAL_ISSUE_CODE_SET = new Set(FORMAL_ISSUE_CODES);
|
|
49
|
+
const AUTHORITY_FIELDS = [
|
|
50
|
+
"authorizationGranted",
|
|
51
|
+
"gitAuthorization",
|
|
52
|
+
"releaseAuthorization",
|
|
53
|
+
"artifactMutationAuthorization",
|
|
54
|
+
"businessActionAuthorization",
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export function isPlainObject(value) {
|
|
58
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const prototype = Object.getPrototypeOf(value);
|
|
62
|
+
return prototype === Object.prototype || prototype === null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const isPlainJsonObject = isPlainObject;
|
|
66
|
+
|
|
67
|
+
export function isSha256Ref(value) {
|
|
68
|
+
return typeof value === "string" && HASH_PATTERN.test(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function sha256Bytes(value) {
|
|
72
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function verifierError(code, message) {
|
|
76
|
+
if (!FORMAL_ISSUE_CODE_SET.has(code)) {
|
|
77
|
+
throw new Error(`undeclared Formal issue code: ${code}`);
|
|
78
|
+
}
|
|
79
|
+
const error = new Error(message);
|
|
80
|
+
error.code = code;
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resolveRegularFilePath(filePath) {
|
|
85
|
+
const metadata = lstatSync(filePath);
|
|
86
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
87
|
+
throw verifierError("INPUT_FILE_INVALID", "input path must be a regular non-symlink file");
|
|
88
|
+
}
|
|
89
|
+
return realpathSync(filePath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function decodeUtf8Strict(bytes) {
|
|
93
|
+
try {
|
|
94
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
95
|
+
} catch {
|
|
96
|
+
throw verifierError("INPUT_JSON_INVALID", "input is not valid UTF-8");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function readFileBytesBounded(filePath) {
|
|
101
|
+
const absolutePath = resolveRegularFilePath(filePath);
|
|
102
|
+
const descriptor = openSync(
|
|
103
|
+
absolutePath,
|
|
104
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
|
|
105
|
+
);
|
|
106
|
+
try {
|
|
107
|
+
const metadata = fstatSync(descriptor);
|
|
108
|
+
if (!metadata.isFile()) {
|
|
109
|
+
throw verifierError("INPUT_FILE_INVALID", "input path must be a regular file");
|
|
110
|
+
}
|
|
111
|
+
if (metadata.size > PUBLIC_VERIFIER_LIMITS.maxInputBytes) {
|
|
112
|
+
throw verifierError("INPUT_SIZE_LIMIT_EXCEEDED", "input file exceeds the input-byte limit");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const buffer = Buffer.allocUnsafe(PUBLIC_VERIFIER_LIMITS.maxInputBytes + 1);
|
|
116
|
+
let offset = 0;
|
|
117
|
+
while (offset < buffer.length) {
|
|
118
|
+
const bytesRead = readSync(descriptor, buffer, offset, buffer.length - offset, null);
|
|
119
|
+
if (bytesRead === 0) break;
|
|
120
|
+
offset += bytesRead;
|
|
121
|
+
}
|
|
122
|
+
if (offset > PUBLIC_VERIFIER_LIMITS.maxInputBytes) {
|
|
123
|
+
throw verifierError("INPUT_SIZE_LIMIT_EXCEEDED", "input file grew beyond the input-byte limit");
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
absolutePath,
|
|
127
|
+
bytes: buffer.subarray(0, offset),
|
|
128
|
+
};
|
|
129
|
+
} finally {
|
|
130
|
+
closeSync(descriptor);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function countNode(budget) {
|
|
135
|
+
budget.nodes += 1;
|
|
136
|
+
if (budget.nodes > PUBLIC_VERIFIER_LIMITS.maxJsonNodes) {
|
|
137
|
+
throw verifierError("JSON_NODE_LIMIT_EXCEEDED", "JSON value exceeds the node limit");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function enterContainer(depth) {
|
|
142
|
+
const nextDepth = depth + 1;
|
|
143
|
+
if (nextDepth > PUBLIC_VERIFIER_LIMITS.maxNestingDepth) {
|
|
144
|
+
throw verifierError("JSON_NESTING_LIMIT_EXCEEDED", "JSON value exceeds the nesting-depth limit");
|
|
145
|
+
}
|
|
146
|
+
return nextDepth;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function canonicalize(value, ancestors, depth, budget) {
|
|
150
|
+
countNode(budget);
|
|
151
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
152
|
+
if (
|
|
153
|
+
typeof value === "string" &&
|
|
154
|
+
Buffer.byteLength(value, "utf8") > PUBLIC_VERIFIER_LIMITS.maxInputBytes
|
|
155
|
+
) {
|
|
156
|
+
throw verifierError("INPUT_SIZE_LIMIT_EXCEEDED", "JSON string exceeds the input-byte limit");
|
|
157
|
+
}
|
|
158
|
+
return JSON.stringify(value);
|
|
159
|
+
}
|
|
160
|
+
if (typeof value === "number") {
|
|
161
|
+
if (!Number.isFinite(value)) {
|
|
162
|
+
throw new TypeError("canonical JSON does not support non-finite numbers");
|
|
163
|
+
}
|
|
164
|
+
return JSON.stringify(value);
|
|
165
|
+
}
|
|
166
|
+
if (typeof value !== "object") {
|
|
167
|
+
throw new TypeError(`canonical JSON does not support ${typeof value}`);
|
|
168
|
+
}
|
|
169
|
+
if (ancestors.has(value)) {
|
|
170
|
+
throw new TypeError("canonical JSON does not support cycles");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const childDepth = enterContainer(depth);
|
|
174
|
+
ancestors.add(value);
|
|
175
|
+
try {
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
const entries = [];
|
|
178
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
179
|
+
if (!Object.hasOwn(value, index)) {
|
|
180
|
+
throw new TypeError("canonical JSON does not support sparse arrays");
|
|
181
|
+
}
|
|
182
|
+
entries.push(canonicalize(value[index], ancestors, childDepth, budget));
|
|
183
|
+
}
|
|
184
|
+
return `[${entries.join(",")}]`;
|
|
185
|
+
}
|
|
186
|
+
if (!isPlainObject(value)) {
|
|
187
|
+
throw new TypeError("canonical JSON supports only plain objects and arrays");
|
|
188
|
+
}
|
|
189
|
+
const members = Object.keys(value)
|
|
190
|
+
.sort()
|
|
191
|
+
.map(
|
|
192
|
+
(key) =>
|
|
193
|
+
`${JSON.stringify(key)}:${canonicalize(value[key], ancestors, childDepth, budget)}`,
|
|
194
|
+
);
|
|
195
|
+
return `{${members.join(",")}}`;
|
|
196
|
+
} finally {
|
|
197
|
+
ancestors.delete(value);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function canonicalJson(value) {
|
|
202
|
+
const result = canonicalize(value, new Set(), 0, { nodes: 0 });
|
|
203
|
+
if (Buffer.byteLength(result, "utf8") > PUBLIC_VERIFIER_LIMITS.maxInputBytes) {
|
|
204
|
+
throw verifierError("INPUT_SIZE_LIMIT_EXCEEDED", "canonical JSON exceeds the input-byte limit");
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function sha256Canonical(value) {
|
|
210
|
+
return sha256Bytes(canonicalJson(value));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
class StrictJsonParser {
|
|
214
|
+
constructor(text) {
|
|
215
|
+
this.text = text;
|
|
216
|
+
this.index = 0;
|
|
217
|
+
this.nodes = 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
parse() {
|
|
221
|
+
const value = this.parseValue(0);
|
|
222
|
+
this.skipWhitespace();
|
|
223
|
+
if (this.index !== this.text.length) {
|
|
224
|
+
this.fail("unexpected trailing content");
|
|
225
|
+
}
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
parseValue(depth) {
|
|
230
|
+
this.skipWhitespace();
|
|
231
|
+
this.nodes += 1;
|
|
232
|
+
if (this.nodes > PUBLIC_VERIFIER_LIMITS.maxJsonNodes) {
|
|
233
|
+
throw verifierError("JSON_NODE_LIMIT_EXCEEDED", "JSON input exceeds the node limit");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const character = this.text[this.index];
|
|
237
|
+
if (character === "{") {
|
|
238
|
+
return this.parseObject(depth);
|
|
239
|
+
}
|
|
240
|
+
if (character === "[") {
|
|
241
|
+
return this.parseArray(depth);
|
|
242
|
+
}
|
|
243
|
+
if (character === '"') {
|
|
244
|
+
return this.parseString();
|
|
245
|
+
}
|
|
246
|
+
if (character === "t") {
|
|
247
|
+
return this.parseLiteral("true", true);
|
|
248
|
+
}
|
|
249
|
+
if (character === "f") {
|
|
250
|
+
return this.parseLiteral("false", false);
|
|
251
|
+
}
|
|
252
|
+
if (character === "n") {
|
|
253
|
+
return this.parseLiteral("null", null);
|
|
254
|
+
}
|
|
255
|
+
if (character === "-" || (character >= "0" && character <= "9")) {
|
|
256
|
+
return this.parseNumber();
|
|
257
|
+
}
|
|
258
|
+
this.fail("expected a JSON value");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
parseObject(depth) {
|
|
262
|
+
const childDepth = this.enter(depth);
|
|
263
|
+
this.index += 1;
|
|
264
|
+
this.skipWhitespace();
|
|
265
|
+
const value = {};
|
|
266
|
+
const keys = new Set();
|
|
267
|
+
if (this.text[this.index] === "}") {
|
|
268
|
+
this.index += 1;
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
while (true) {
|
|
273
|
+
if (this.text[this.index] !== '"') {
|
|
274
|
+
this.fail("expected an object key");
|
|
275
|
+
}
|
|
276
|
+
const key = this.parseString();
|
|
277
|
+
if (keys.has(key)) {
|
|
278
|
+
throw verifierError("DUPLICATE_OBJECT_KEY", `duplicate object key: ${key}`);
|
|
279
|
+
}
|
|
280
|
+
keys.add(key);
|
|
281
|
+
this.skipWhitespace();
|
|
282
|
+
if (this.text[this.index] !== ":") {
|
|
283
|
+
this.fail("expected ':' after an object key");
|
|
284
|
+
}
|
|
285
|
+
this.index += 1;
|
|
286
|
+
Object.defineProperty(value, key, {
|
|
287
|
+
value: this.parseValue(childDepth),
|
|
288
|
+
enumerable: true,
|
|
289
|
+
configurable: true,
|
|
290
|
+
writable: true,
|
|
291
|
+
});
|
|
292
|
+
this.skipWhitespace();
|
|
293
|
+
if (this.text[this.index] === "}") {
|
|
294
|
+
this.index += 1;
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
297
|
+
if (this.text[this.index] !== ",") {
|
|
298
|
+
this.fail("expected ',' or '}' in an object");
|
|
299
|
+
}
|
|
300
|
+
this.index += 1;
|
|
301
|
+
this.skipWhitespace();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
parseArray(depth) {
|
|
306
|
+
const childDepth = this.enter(depth);
|
|
307
|
+
this.index += 1;
|
|
308
|
+
this.skipWhitespace();
|
|
309
|
+
const value = [];
|
|
310
|
+
if (this.text[this.index] === "]") {
|
|
311
|
+
this.index += 1;
|
|
312
|
+
return value;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
while (true) {
|
|
316
|
+
value.push(this.parseValue(childDepth));
|
|
317
|
+
this.skipWhitespace();
|
|
318
|
+
if (this.text[this.index] === "]") {
|
|
319
|
+
this.index += 1;
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
322
|
+
if (this.text[this.index] !== ",") {
|
|
323
|
+
this.fail("expected ',' or ']' in an array");
|
|
324
|
+
}
|
|
325
|
+
this.index += 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
parseString() {
|
|
330
|
+
const start = this.index;
|
|
331
|
+
this.index += 1;
|
|
332
|
+
while (this.index < this.text.length) {
|
|
333
|
+
const character = this.text[this.index];
|
|
334
|
+
if (character === '"') {
|
|
335
|
+
this.index += 1;
|
|
336
|
+
try {
|
|
337
|
+
return JSON.parse(this.text.slice(start, this.index));
|
|
338
|
+
} catch {
|
|
339
|
+
this.fail("invalid JSON string");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (character.charCodeAt(0) < 0x20) {
|
|
343
|
+
this.fail("unescaped control character in JSON string");
|
|
344
|
+
}
|
|
345
|
+
if (character === "\\") {
|
|
346
|
+
this.index += 1;
|
|
347
|
+
const escape = this.text[this.index];
|
|
348
|
+
if (escape === "u") {
|
|
349
|
+
const digits = this.text.slice(this.index + 1, this.index + 5);
|
|
350
|
+
if (!/^[0-9a-fA-F]{4}$/.test(digits)) {
|
|
351
|
+
this.fail("invalid Unicode escape in JSON string");
|
|
352
|
+
}
|
|
353
|
+
this.index += 4;
|
|
354
|
+
} else if (!'"\\/bfnrt'.includes(escape)) {
|
|
355
|
+
this.fail("invalid escape in JSON string");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
this.index += 1;
|
|
359
|
+
}
|
|
360
|
+
this.fail("unterminated JSON string");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
parseLiteral(token, value) {
|
|
364
|
+
if (this.text.slice(this.index, this.index + token.length) !== token) {
|
|
365
|
+
this.fail(`invalid literal: expected ${token}`);
|
|
366
|
+
}
|
|
367
|
+
this.index += token.length;
|
|
368
|
+
return value;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
parseNumber() {
|
|
372
|
+
const match = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(
|
|
373
|
+
this.text.slice(this.index),
|
|
374
|
+
);
|
|
375
|
+
if (!match) {
|
|
376
|
+
this.fail("invalid JSON number");
|
|
377
|
+
}
|
|
378
|
+
this.index += match[0].length;
|
|
379
|
+
const value = Number(match[0]);
|
|
380
|
+
if (!Number.isFinite(value)) {
|
|
381
|
+
this.fail("JSON number is outside the finite number range");
|
|
382
|
+
}
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
enter(depth) {
|
|
387
|
+
const nextDepth = depth + 1;
|
|
388
|
+
if (nextDepth > PUBLIC_VERIFIER_LIMITS.maxNestingDepth) {
|
|
389
|
+
throw verifierError("JSON_NESTING_LIMIT_EXCEEDED", "JSON input exceeds the nesting-depth limit");
|
|
390
|
+
}
|
|
391
|
+
return nextDepth;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
skipWhitespace() {
|
|
395
|
+
while (
|
|
396
|
+
[" ", "\t", "\n", "\r"].includes(this.text[this.index]) &&
|
|
397
|
+
this.index < this.text.length
|
|
398
|
+
) {
|
|
399
|
+
this.index += 1;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
fail(message) {
|
|
404
|
+
throw verifierError("INPUT_JSON_INVALID", `${message} at byte offset ${this.index}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function parseJsonStrict(text) {
|
|
409
|
+
if (typeof text !== "string") {
|
|
410
|
+
throw new TypeError("strict JSON input must be a string");
|
|
411
|
+
}
|
|
412
|
+
if (Buffer.byteLength(text, "utf8") > PUBLIC_VERIFIER_LIMITS.maxInputBytes) {
|
|
413
|
+
throw verifierError("INPUT_SIZE_LIMIT_EXCEEDED", "JSON input exceeds the input-byte limit");
|
|
414
|
+
}
|
|
415
|
+
return new StrictJsonParser(text).parse();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function hasExactKeys(value, requiredKeys, optionalKeys = []) {
|
|
419
|
+
if (!isPlainObject(value)) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
const allowed = new Set([...requiredKeys, ...optionalKeys]);
|
|
423
|
+
const actual = Object.keys(value);
|
|
424
|
+
return requiredKeys.every((key) => Object.hasOwn(value, key))
|
|
425
|
+
&& actual.every((key) => allowed.has(key));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function hasAuthorityShape(value) {
|
|
429
|
+
return AUTHORITY_FIELDS.every((field) => typeof value[field] === "boolean");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function validFingerprintEnvelope(value) {
|
|
433
|
+
return (
|
|
434
|
+
hasExactKeys(value, ["fingerprintAlgorithm", "fingerprint", "payload"]) &&
|
|
435
|
+
value.fingerprintAlgorithm === "sha256-canonical-json-v1" &&
|
|
436
|
+
typeof value.fingerprint === "string" &&
|
|
437
|
+
HASH_PATTERN.test(value.fingerprint) &&
|
|
438
|
+
isPlainObject(value.payload)
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function validReceiptShape(receipt) {
|
|
443
|
+
return (
|
|
444
|
+
hasExactKeys(receipt, [
|
|
445
|
+
"schemaVersion",
|
|
446
|
+
"receiptId",
|
|
447
|
+
"status",
|
|
448
|
+
"replacesReceiptId",
|
|
449
|
+
"sourceFingerprint",
|
|
450
|
+
"verificationContextFingerprint",
|
|
451
|
+
...AUTHORITY_FIELDS,
|
|
452
|
+
]) &&
|
|
453
|
+
receipt.schemaVersion === "OwlCodaRunKitPublicVerificationReceiptV1" &&
|
|
454
|
+
typeof receipt.receiptId === "string" &&
|
|
455
|
+
receipt.receiptId.length > 0 &&
|
|
456
|
+
["passed", "failed", "blocked"].includes(receipt.status) &&
|
|
457
|
+
(receipt.replacesReceiptId === null ||
|
|
458
|
+
(typeof receipt.replacesReceiptId === "string" && receipt.replacesReceiptId.length > 0)) &&
|
|
459
|
+
typeof receipt.sourceFingerprint === "string" &&
|
|
460
|
+
HASH_PATTERN.test(receipt.sourceFingerprint) &&
|
|
461
|
+
typeof receipt.verificationContextFingerprint === "string" &&
|
|
462
|
+
HASH_PATTERN.test(receipt.verificationContextFingerprint) &&
|
|
463
|
+
hasAuthorityShape(receipt)
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function validCloseoutShape(closeout) {
|
|
468
|
+
return (
|
|
469
|
+
hasExactKeys(closeout, [
|
|
470
|
+
"schemaVersion",
|
|
471
|
+
"decision",
|
|
472
|
+
"activeReceiptId",
|
|
473
|
+
"sourceFingerprint",
|
|
474
|
+
"verificationContextFingerprint",
|
|
475
|
+
...AUTHORITY_FIELDS,
|
|
476
|
+
]) &&
|
|
477
|
+
closeout.schemaVersion === "OwlCodaRunKitPublicCloseoutV1" &&
|
|
478
|
+
["accepted", "blocked", "rejected"].includes(closeout.decision) &&
|
|
479
|
+
typeof closeout.activeReceiptId === "string" &&
|
|
480
|
+
closeout.activeReceiptId.length > 0 &&
|
|
481
|
+
typeof closeout.sourceFingerprint === "string" &&
|
|
482
|
+
HASH_PATTERN.test(closeout.sourceFingerprint) &&
|
|
483
|
+
typeof closeout.verificationContextFingerprint === "string" &&
|
|
484
|
+
HASH_PATTERN.test(closeout.verificationContextFingerprint) &&
|
|
485
|
+
hasAuthorityShape(closeout)
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function issue(code, pathValue, message) {
|
|
490
|
+
if (!FORMAL_ISSUE_CODE_SET.has(code)) {
|
|
491
|
+
throw new Error(`undeclared Formal issue code: ${code}`);
|
|
492
|
+
}
|
|
493
|
+
return { code, path: pathValue, message };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function anyAuthorityGranted(value) {
|
|
497
|
+
return AUTHORITY_FIELDS.some((field) => value[field] !== false);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function findLineageCycle(receiptsById) {
|
|
501
|
+
for (const receipt of receiptsById.values()) {
|
|
502
|
+
const visited = new Set();
|
|
503
|
+
let current = receipt;
|
|
504
|
+
while (current && current.replacesReceiptId !== null) {
|
|
505
|
+
if (visited.has(current.receiptId)) {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
visited.add(current.receiptId);
|
|
509
|
+
current = receiptsById.get(current.replacesReceiptId);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function emptyResult(issues) {
|
|
516
|
+
return {
|
|
517
|
+
schemaVersion: "OwlCodaRunKitPublicVerificationResultV1",
|
|
518
|
+
status: "invalid",
|
|
519
|
+
valid: false,
|
|
520
|
+
decision: null,
|
|
521
|
+
activeReceiptId: null,
|
|
522
|
+
activeReceiptStatus: null,
|
|
523
|
+
sourceFingerprint: null,
|
|
524
|
+
verificationContextFingerprint: null,
|
|
525
|
+
authorizationGranted: false,
|
|
526
|
+
gitAuthorization: false,
|
|
527
|
+
releaseAuthorization: false,
|
|
528
|
+
artifactMutationAuthorization: false,
|
|
529
|
+
businessActionAuthorization: false,
|
|
530
|
+
issues,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function verifyBundle(bundle) {
|
|
535
|
+
const issues = [];
|
|
536
|
+
try {
|
|
537
|
+
canonicalJson(bundle);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (
|
|
540
|
+
[
|
|
541
|
+
"INPUT_SIZE_LIMIT_EXCEEDED",
|
|
542
|
+
"JSON_NESTING_LIMIT_EXCEEDED",
|
|
543
|
+
"JSON_NODE_LIMIT_EXCEEDED",
|
|
544
|
+
].includes(error.code)
|
|
545
|
+
) {
|
|
546
|
+
return emptyResult([issue(error.code, "$", error.message)]);
|
|
547
|
+
}
|
|
548
|
+
return emptyResult([issue("PUBLIC_BUNDLE_SHAPE_INVALID", "$", error.message)]);
|
|
549
|
+
}
|
|
550
|
+
if (
|
|
551
|
+
isPlainObject(bundle) &&
|
|
552
|
+
Array.isArray(bundle.receipts) &&
|
|
553
|
+
bundle.receipts.length > PUBLIC_VERIFIER_LIMITS.maxReceipts
|
|
554
|
+
) {
|
|
555
|
+
return emptyResult([
|
|
556
|
+
issue(
|
|
557
|
+
"RECEIPT_COUNT_LIMIT_EXCEEDED",
|
|
558
|
+
"$.receipts",
|
|
559
|
+
`receipt count exceeds ${PUBLIC_VERIFIER_LIMITS.maxReceipts}`,
|
|
560
|
+
),
|
|
561
|
+
]);
|
|
562
|
+
}
|
|
563
|
+
const rootShapeValid =
|
|
564
|
+
hasExactKeys(bundle, ["schemaVersion", "source", "verificationContext", "receipts", "closeout"]) &&
|
|
565
|
+
bundle.schemaVersion === "OwlCodaRunKitPublicVerificationBundleV1" &&
|
|
566
|
+
validFingerprintEnvelope(bundle.source) &&
|
|
567
|
+
validFingerprintEnvelope(bundle.verificationContext) &&
|
|
568
|
+
Array.isArray(bundle.receipts) &&
|
|
569
|
+
bundle.receipts.length > 0 &&
|
|
570
|
+
bundle.receipts.every(validReceiptShape) &&
|
|
571
|
+
validCloseoutShape(bundle.closeout);
|
|
572
|
+
|
|
573
|
+
if (!rootShapeValid) {
|
|
574
|
+
return emptyResult([
|
|
575
|
+
issue(
|
|
576
|
+
"PUBLIC_BUNDLE_SHAPE_INVALID",
|
|
577
|
+
"$",
|
|
578
|
+
"bundle does not match the strict OwlCodaRunKitPublicVerificationBundleV1 shape",
|
|
579
|
+
),
|
|
580
|
+
]);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
try {
|
|
584
|
+
if (sha256Canonical(bundle.source.payload) !== bundle.source.fingerprint) {
|
|
585
|
+
issues.push(
|
|
586
|
+
issue("SOURCE_FINGERPRINT_MISMATCH", "$.source.fingerprint", "source payload fingerprint does not match"),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
if (sha256Canonical(bundle.verificationContext.payload) !== bundle.verificationContext.fingerprint) {
|
|
590
|
+
issues.push(
|
|
591
|
+
issue(
|
|
592
|
+
"VERIFICATION_CONTEXT_FINGERPRINT_MISMATCH",
|
|
593
|
+
"$.verificationContext.fingerprint",
|
|
594
|
+
"verification context payload fingerprint does not match",
|
|
595
|
+
),
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
} catch (error) {
|
|
599
|
+
issues.push(issue(error.code ?? "CANONICAL_JSON_INVALID", "$", error.message));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const receiptsById = new Map();
|
|
603
|
+
for (const receipt of bundle.receipts) {
|
|
604
|
+
if (receiptsById.has(receipt.receiptId)) {
|
|
605
|
+
issues.push(issue("RECEIPT_ID_DUPLICATE", "$.receipts", `duplicate receipt id: ${receipt.receiptId}`));
|
|
606
|
+
} else {
|
|
607
|
+
receiptsById.set(receipt.receiptId, receipt);
|
|
608
|
+
}
|
|
609
|
+
if (receipt.replacesReceiptId !== null && !bundle.receipts.some(
|
|
610
|
+
(candidate) => candidate.receiptId === receipt.replacesReceiptId,
|
|
611
|
+
)) {
|
|
612
|
+
issues.push(
|
|
613
|
+
issue(
|
|
614
|
+
"RECEIPT_LINEAGE_ORPHAN",
|
|
615
|
+
`$.receipts.${receipt.receiptId}.replacesReceiptId`,
|
|
616
|
+
`replacement target is absent: ${receipt.replacesReceiptId}`,
|
|
617
|
+
),
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (anyAuthorityGranted(receipt)) {
|
|
621
|
+
issues.push(
|
|
622
|
+
issue("RECEIPT_AUTHORITY_GRANTED", `$.receipts.${receipt.receiptId}`, "a receipt cannot grant external authority"),
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (findLineageCycle(receiptsById)) {
|
|
628
|
+
issues.push(issue("RECEIPT_LINEAGE_CYCLE", "$.receipts", "receipt replacement lineage contains a cycle"));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const replacedReceiptIds = new Set(
|
|
632
|
+
bundle.receipts
|
|
633
|
+
.map((receipt) => receipt.replacesReceiptId)
|
|
634
|
+
.filter((receiptId) => receiptId !== null),
|
|
635
|
+
);
|
|
636
|
+
const activeLeaves = bundle.receipts.filter((receipt) => !replacedReceiptIds.has(receipt.receiptId));
|
|
637
|
+
if (activeLeaves.length === 0) {
|
|
638
|
+
issues.push(issue("RECEIPT_LINEAGE_NO_ACTIVE_LEAF", "$.receipts", "receipt lineage has no active leaf"));
|
|
639
|
+
} else if (activeLeaves.length > 1) {
|
|
640
|
+
issues.push(
|
|
641
|
+
issue(
|
|
642
|
+
"RECEIPT_LINEAGE_MULTIPLE_ACTIVE_LEAVES",
|
|
643
|
+
"$.receipts",
|
|
644
|
+
`receipt lineage has ${activeLeaves.length} active leaves`,
|
|
645
|
+
),
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const activeReceipt = activeLeaves.length === 1 ? activeLeaves[0] : null;
|
|
650
|
+
if (activeReceipt && bundle.closeout.activeReceiptId !== activeReceipt.receiptId) {
|
|
651
|
+
issues.push(
|
|
652
|
+
issue(
|
|
653
|
+
"CLOSEOUT_ACTIVE_RECEIPT_MISMATCH",
|
|
654
|
+
"$.closeout.activeReceiptId",
|
|
655
|
+
"closeout does not name the unique active receipt",
|
|
656
|
+
),
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (activeReceipt && activeReceipt.sourceFingerprint !== bundle.source.fingerprint) {
|
|
660
|
+
issues.push(
|
|
661
|
+
issue(
|
|
662
|
+
"ACTIVE_RECEIPT_SOURCE_BINDING_MISMATCH",
|
|
663
|
+
`$.receipts.${activeReceipt.receiptId}.sourceFingerprint`,
|
|
664
|
+
"active receipt is not bound to the declared source fingerprint",
|
|
665
|
+
),
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
if (
|
|
669
|
+
activeReceipt &&
|
|
670
|
+
activeReceipt.verificationContextFingerprint !== bundle.verificationContext.fingerprint
|
|
671
|
+
) {
|
|
672
|
+
issues.push(
|
|
673
|
+
issue(
|
|
674
|
+
"ACTIVE_RECEIPT_CONTEXT_BINDING_MISMATCH",
|
|
675
|
+
`$.receipts.${activeReceipt.receiptId}.verificationContextFingerprint`,
|
|
676
|
+
"active receipt is not bound to the declared verification context fingerprint",
|
|
677
|
+
),
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (bundle.closeout.sourceFingerprint !== bundle.source.fingerprint) {
|
|
681
|
+
issues.push(
|
|
682
|
+
issue("CLOSEOUT_SOURCE_BINDING_MISMATCH", "$.closeout.sourceFingerprint", "closeout source binding does not match"),
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
if (bundle.closeout.verificationContextFingerprint !== bundle.verificationContext.fingerprint) {
|
|
686
|
+
issues.push(
|
|
687
|
+
issue(
|
|
688
|
+
"CLOSEOUT_CONTEXT_BINDING_MISMATCH",
|
|
689
|
+
"$.closeout.verificationContextFingerprint",
|
|
690
|
+
"closeout verification context binding does not match",
|
|
691
|
+
),
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (anyAuthorityGranted(bundle.closeout)) {
|
|
696
|
+
issues.push(
|
|
697
|
+
issue(
|
|
698
|
+
bundle.closeout.decision === "accepted" ? "ACCEPTED_AUTHORITY_GRANTED" : "EXTERNAL_AUTHORITY_GRANTED",
|
|
699
|
+
"$.closeout",
|
|
700
|
+
"closeout decisions never grant Git, release, mutation, or business-action authority",
|
|
701
|
+
),
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
if (bundle.closeout.decision === "accepted" && activeReceipt?.status !== "passed") {
|
|
705
|
+
issues.push(
|
|
706
|
+
issue(
|
|
707
|
+
"ACCEPTED_RECEIPT_NOT_PASSED",
|
|
708
|
+
"$.closeout.decision",
|
|
709
|
+
"accepted requires the unique active receipt to have passed",
|
|
710
|
+
),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const valid = issues.length === 0;
|
|
715
|
+
return {
|
|
716
|
+
schemaVersion: "OwlCodaRunKitPublicVerificationResultV1",
|
|
717
|
+
status: valid ? "verified" : "invalid",
|
|
718
|
+
valid,
|
|
719
|
+
decision: bundle.closeout.decision,
|
|
720
|
+
activeReceiptId: activeReceipt?.receiptId ?? null,
|
|
721
|
+
activeReceiptStatus: activeReceipt?.status ?? null,
|
|
722
|
+
sourceFingerprint: bundle.source.fingerprint,
|
|
723
|
+
verificationContextFingerprint: bundle.verificationContext.fingerprint,
|
|
724
|
+
authorizationGranted: false,
|
|
725
|
+
gitAuthorization: false,
|
|
726
|
+
releaseAuthorization: false,
|
|
727
|
+
artifactMutationAuthorization: false,
|
|
728
|
+
businessActionAuthorization: false,
|
|
729
|
+
issues,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function main(argv) {
|
|
734
|
+
if (argv.length !== 2 || argv[0] !== "verify") {
|
|
735
|
+
process.stderr.write("usage: node verifier.mjs verify <bundle.json>\n");
|
|
736
|
+
return 2;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
let bundle;
|
|
740
|
+
try {
|
|
741
|
+
bundle = parseJsonStrict(await readUtf8FileBounded(argv[1]));
|
|
742
|
+
} catch (error) {
|
|
743
|
+
process.stdout.write(
|
|
744
|
+
`${JSON.stringify(emptyResult([issue(error.code ?? "INPUT_JSON_INVALID", "$", error.message)]), null, 2)}\n`,
|
|
745
|
+
);
|
|
746
|
+
return 1;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const result = verifyBundle(bundle);
|
|
750
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
751
|
+
return result.valid ? 0 : 1;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export async function readUtf8FileBounded(filePath) {
|
|
755
|
+
return decodeUtf8Strict(readFileBytesBounded(filePath).bytes);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const isDirectExecution =
|
|
759
|
+
typeof process.argv[1] === "string" && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
760
|
+
if (isDirectExecution) {
|
|
761
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
762
|
+
}
|