scxq2-cc 1.0.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/README.md +340 -0
- package/dist/base64.js +83 -0
- package/dist/canon.js +60 -0
- package/dist/cli.mjs +192 -0
- package/dist/engine.js +753 -0
- package/dist/index.d.ts +426 -0
- package/dist/index.js +48 -0
- package/dist/sha.js +71 -0
- package/dist/verify.js +480 -0
- package/dist/wasm-decoder.js +232 -0
- package/package.json +64 -0
- package/src/base64.js +83 -0
- package/src/canon.js +60 -0
- package/src/engine.js +753 -0
- package/src/index.js +48 -0
- package/src/sha.js +71 -0
package/dist/verify.js
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCXQ2 Pack Verifier (v1) — Deterministic Fail-First Ordering (FROZEN)
|
|
3
|
+
*
|
|
4
|
+
* Implements the reference verification algorithm from the SCXQ2 specification.
|
|
5
|
+
* All error codes and ordering are normative and must not change.
|
|
6
|
+
*
|
|
7
|
+
* @module @asx/scxq2-cc/verify
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import crypto from "crypto";
|
|
12
|
+
|
|
13
|
+
/* =============================================================================
|
|
14
|
+
Known Field Sets (Normative)
|
|
15
|
+
============================================================================= */
|
|
16
|
+
|
|
17
|
+
const KNOWN_PACK_FIELDS = new Set([
|
|
18
|
+
"@type", "@version", "mode", "encoding", "created_utc",
|
|
19
|
+
"dict", "blocks", "proof", "pack_sha256_canon"
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const KNOWN_DICT_FIELDS = new Set([
|
|
23
|
+
"@type", "@version", "mode", "encoding",
|
|
24
|
+
"source_sha256_utf8", "dict", "dict_sha256_canon", "ops",
|
|
25
|
+
"created_utc", "max_dict", "min_len", "flags"
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const KNOWN_BLOCK_FIELDS = new Set([
|
|
29
|
+
"@type", "@version", "mode", "encoding",
|
|
30
|
+
"lane_id", "source_sha256_utf8", "dict_sha256_canon",
|
|
31
|
+
"b64", "block_sha256_canon", "edges", "ops",
|
|
32
|
+
"created_utc", "original_bytes_utf8"
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const KNOWN_PROOF_FIELDS = new Set([
|
|
36
|
+
"@type", "@version", "engine", "source_sha256_utf8",
|
|
37
|
+
"dict_sha256_canon", "block_sha256_canon", "roundtrip_sha256_utf8", "ok",
|
|
38
|
+
"created_utc", "steps", "recomputed"
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/* =============================================================================
|
|
42
|
+
Default Policy Profile (Canonical)
|
|
43
|
+
============================================================================= */
|
|
44
|
+
|
|
45
|
+
export const SCXQ2_DEFAULT_POLICY = Object.freeze({
|
|
46
|
+
requireRoundtrip: true,
|
|
47
|
+
requireProof: true,
|
|
48
|
+
maxDictEntries: 65535,
|
|
49
|
+
maxDictEntryUnits: 1048576,
|
|
50
|
+
maxBlocks: 1024,
|
|
51
|
+
maxBlockB64Bytes: 67108864,
|
|
52
|
+
maxOutputUnits: 134217728,
|
|
53
|
+
allowEdges: true,
|
|
54
|
+
allowUnknownPackFields: true,
|
|
55
|
+
allowUnknownBlockFields: true,
|
|
56
|
+
allowedModes: ["SCXQ2-DICT16-B64"],
|
|
57
|
+
allowedEncodings: ["SCXQ2-1"],
|
|
58
|
+
failOnFirstError: true
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/* =============================================================================
|
|
62
|
+
Error Helpers (Normative Shape)
|
|
63
|
+
============================================================================= */
|
|
64
|
+
|
|
65
|
+
function err(code, phase, message, at = {}) {
|
|
66
|
+
return {
|
|
67
|
+
"@type": "scxq2.error",
|
|
68
|
+
"@version": "1.0.0",
|
|
69
|
+
code,
|
|
70
|
+
phase,
|
|
71
|
+
severity: "fatal",
|
|
72
|
+
message,
|
|
73
|
+
at
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function fail(e) {
|
|
78
|
+
return {
|
|
79
|
+
"@type": "scxq2.verify.result",
|
|
80
|
+
"@version": "1.0.0",
|
|
81
|
+
ok: false,
|
|
82
|
+
error: e
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ok(packSha, dictSha, blocks) {
|
|
87
|
+
return {
|
|
88
|
+
"@type": "scxq2.verify.result",
|
|
89
|
+
"@version": "1.0.0",
|
|
90
|
+
ok: true,
|
|
91
|
+
pack_sha256_canon: packSha,
|
|
92
|
+
dict_sha256_canon: dictSha,
|
|
93
|
+
blocks
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* =============================================================================
|
|
98
|
+
Utilities
|
|
99
|
+
============================================================================= */
|
|
100
|
+
|
|
101
|
+
function sha256HexUtf8(s) {
|
|
102
|
+
return crypto.createHash("sha256").update(Buffer.from(s, "utf8")).digest("hex");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sortKeysDeep(v) {
|
|
106
|
+
if (Array.isArray(v)) return v.map(sortKeysDeep);
|
|
107
|
+
if (v && typeof v === "object") {
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const k of Object.keys(v).sort()) out[k] = sortKeysDeep(v[k]);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
return v;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function canon(obj) {
|
|
116
|
+
return JSON.stringify(sortKeysDeep(obj));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function copyMinus(obj, field) {
|
|
120
|
+
const o = { ...obj };
|
|
121
|
+
delete o[field];
|
|
122
|
+
return o;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hasUnknownFields(obj, knownSet) {
|
|
126
|
+
for (const k of Object.keys(obj)) {
|
|
127
|
+
if (!knownSet.has(k)) return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isoUtcLike(s) {
|
|
133
|
+
return typeof s === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/.test(s);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function strictB64Decode(b64) {
|
|
137
|
+
if (typeof b64 !== "string" || b64.length === 0) return null;
|
|
138
|
+
if (!/^[A-Za-z0-9+/=]+$/.test(b64)) return null;
|
|
139
|
+
try {
|
|
140
|
+
return Buffer.from(b64, "base64");
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* =============================================================================
|
|
147
|
+
SCXQ2 Decode (JS Reference Implementation)
|
|
148
|
+
============================================================================= */
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Decodes SCXQ2 byte stream to UTF-16 string.
|
|
152
|
+
*
|
|
153
|
+
* @param {string[]} dictArr - Dictionary array
|
|
154
|
+
* @param {Uint8Array|Buffer} bytes - Encoded bytes
|
|
155
|
+
* @param {Object} limits - Decode limits
|
|
156
|
+
* @returns {{ok: true, value: string}|{ok: false, kind: string, byte_offset: number}}
|
|
157
|
+
*/
|
|
158
|
+
export function scxq2DecodeUtf16(dictArr, bytes, limits = {}) {
|
|
159
|
+
const maxOut = limits?.maxOutputUnits ?? SCXQ2_DEFAULT_POLICY.maxOutputUnits;
|
|
160
|
+
let out = "";
|
|
161
|
+
let outUnits = 0;
|
|
162
|
+
|
|
163
|
+
const m = bytes.length;
|
|
164
|
+
for (let i = 0; i < m; ) {
|
|
165
|
+
const b = bytes[i];
|
|
166
|
+
|
|
167
|
+
// ASCII literal
|
|
168
|
+
if (b <= 0x7f) {
|
|
169
|
+
out += String.fromCharCode(b);
|
|
170
|
+
outUnits += 1;
|
|
171
|
+
i += 1;
|
|
172
|
+
}
|
|
173
|
+
// DICT ref
|
|
174
|
+
else if (b === 0x80) {
|
|
175
|
+
if (i + 2 >= m) {
|
|
176
|
+
return { ok: false, kind: "truncated_sequence", byte_offset: i };
|
|
177
|
+
}
|
|
178
|
+
const j = (bytes[i + 1] << 8) | bytes[i + 2];
|
|
179
|
+
if (j < 0 || j >= dictArr.length) {
|
|
180
|
+
return { ok: false, kind: "dict_index_oob", byte_offset: i, index: j };
|
|
181
|
+
}
|
|
182
|
+
const tok = dictArr[j];
|
|
183
|
+
if (typeof tok !== "string") {
|
|
184
|
+
return { ok: false, kind: "dict_entry_invalid", byte_offset: i, index: j };
|
|
185
|
+
}
|
|
186
|
+
out += tok;
|
|
187
|
+
outUnits += tok.length;
|
|
188
|
+
i += 3;
|
|
189
|
+
}
|
|
190
|
+
// UTF-16 literal
|
|
191
|
+
else if (b === 0x81) {
|
|
192
|
+
if (i + 2 >= m) {
|
|
193
|
+
return { ok: false, kind: "truncated_sequence", byte_offset: i };
|
|
194
|
+
}
|
|
195
|
+
const u = (bytes[i + 1] << 8) | bytes[i + 2];
|
|
196
|
+
out += String.fromCharCode(u);
|
|
197
|
+
outUnits += 1;
|
|
198
|
+
i += 3;
|
|
199
|
+
}
|
|
200
|
+
// Invalid byte
|
|
201
|
+
else {
|
|
202
|
+
return { ok: false, kind: "invalid_byte", byte_offset: i, byte: b };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (outUnits > maxOut) {
|
|
206
|
+
return { ok: false, kind: "output_limit", byte_offset: Math.min(m - 1, i) };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { ok: true, value: out };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function mapDecodeErr(de, lane_id, index) {
|
|
214
|
+
const at = { lane_id: lane_id ?? undefined, index, byte_offset: de.byte_offset };
|
|
215
|
+
switch (de.kind) {
|
|
216
|
+
case "invalid_byte":
|
|
217
|
+
return err("scxq2.error.decode_invalid_byte", "decode", "invalid byte", at);
|
|
218
|
+
case "truncated_sequence":
|
|
219
|
+
return err("scxq2.error.decode_truncated_sequence", "decode", "truncated sequence", at);
|
|
220
|
+
case "dict_index_oob":
|
|
221
|
+
return err("scxq2.error.decode_dict_index_oob", "decode", "dict index out of bounds", at);
|
|
222
|
+
case "dict_entry_invalid":
|
|
223
|
+
return err("scxq2.error.decode_dict_entry_invalid", "decode", "dict entry invalid", at);
|
|
224
|
+
case "output_limit":
|
|
225
|
+
return err("scxq2.error.decode_output_limit", "decode", "output limit exceeded", at);
|
|
226
|
+
default:
|
|
227
|
+
return err("scxq2.error.decode_internal", "decode", "decode internal error", at);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* =============================================================================
|
|
232
|
+
Pack Verifier (Reference Algorithm)
|
|
233
|
+
============================================================================= */
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Verifies an SCXQ2 pack according to the frozen v1 specification.
|
|
237
|
+
* Follows deterministic fail-first ordering.
|
|
238
|
+
*
|
|
239
|
+
* @param {Object} pack - SCXQ2 pack object
|
|
240
|
+
* @param {Object} [opts] - Policy options (merged with SCXQ2_DEFAULT_POLICY)
|
|
241
|
+
* @returns {Object} Verification result
|
|
242
|
+
*/
|
|
243
|
+
export function scxq2PackVerify(pack, opts = {}) {
|
|
244
|
+
const P = { ...SCXQ2_DEFAULT_POLICY, ...(opts || {}) };
|
|
245
|
+
|
|
246
|
+
// =========================================================
|
|
247
|
+
// STEP 1 — PACK STRUCTURE
|
|
248
|
+
// =========================================================
|
|
249
|
+
if (!pack || typeof pack !== "object") {
|
|
250
|
+
return fail(err("scxq2.error.pack_missing", "pack", "pack missing"));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (pack["@type"] !== "scxq2.pack") {
|
|
254
|
+
return fail(err("scxq2.error.pack_type_invalid", "pack", "bad @type", { field: "@type" }));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (pack["@version"] !== "1.0.0") {
|
|
258
|
+
return fail(err("scxq2.error.pack_version_unsupported", "pack", "unsupported @version", { field: "@version" }));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!P.allowedModes.includes(pack["mode"])) {
|
|
262
|
+
return fail(err("scxq2.error.pack_mode_mismatch", "pack", "mode not allowed", { field: "mode" }));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!P.allowedEncodings.includes(pack["encoding"])) {
|
|
266
|
+
return fail(err("scxq2.error.pack_encoding_mismatch", "pack", "encoding not allowed", { field: "encoding" }));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (P.allowUnknownPackFields === false && hasUnknownFields(pack, KNOWN_PACK_FIELDS)) {
|
|
270
|
+
return fail(err("scxq2.error.pack_field_forbidden", "pack", "unknown pack field"));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if ("created_utc" in pack && !isoUtcLike(pack.created_utc)) {
|
|
274
|
+
return fail(err("scxq2.error.pack_created_utc_invalid", "pack", "invalid created_utc", { field: "created_utc" }));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!pack.dict) {
|
|
278
|
+
return fail(err("scxq2.error.dict_missing", "dict", "dict missing", { field: "dict" }));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!Array.isArray(pack.blocks) || pack.blocks.length < 1) {
|
|
282
|
+
return fail(err("scxq2.error.pack_blocks_missing", "pack", "blocks missing/empty", { field: "blocks" }));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (pack.blocks.length > P.maxBlocks) {
|
|
286
|
+
return fail(err("scxq2.error.decode_input_limit", "pack", "too many blocks", { field: "blocks" }));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (P.requireProof && !pack.proof) {
|
|
290
|
+
return fail(err("scxq2.error.pack_proof_missing", "pack", "proof required", { field: "proof" }));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// =========================================================
|
|
294
|
+
// STEP 2 — DICT STRUCTURE
|
|
295
|
+
// =========================================================
|
|
296
|
+
const dict = pack.dict;
|
|
297
|
+
|
|
298
|
+
if (!dict || typeof dict !== "object") {
|
|
299
|
+
return fail(err("scxq2.error.dict_type_invalid", "dict", "dict not object", { field: "dict" }));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (dict["@type"] !== "scxq2.dict") {
|
|
303
|
+
return fail(err("scxq2.error.dict_type_invalid", "dict", "bad dict @type", { field: "dict.@type" }));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (dict["@version"] !== "1.0.0") {
|
|
307
|
+
return fail(err("scxq2.error.dict_version_unsupported", "dict", "unsupported dict @version", { field: "dict.@version" }));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!P.allowedModes.includes(dict.mode)) {
|
|
311
|
+
return fail(err("scxq2.error.pack_mode_mismatch", "dict", "dict mode mismatch", { field: "dict.mode" }));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!P.allowedEncodings.includes(dict.encoding)) {
|
|
315
|
+
return fail(err("scxq2.error.pack_encoding_mismatch", "dict", "dict encoding mismatch", { field: "dict.encoding" }));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!Array.isArray(dict.dict)) {
|
|
319
|
+
return fail(err("scxq2.error.dict_entry_type_invalid", "dict", "dict.dict must be array", { field: "dict.dict" }));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (dict.dict.length > P.maxDictEntries) {
|
|
323
|
+
return fail(err("scxq2.error.dict_size_exceeds_limit", "dict", "dict too large", { field: "dict.dict" }));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (let j = 0; j < dict.dict.length; j++) {
|
|
327
|
+
const e = dict.dict[j];
|
|
328
|
+
if (typeof e !== "string") {
|
|
329
|
+
return fail(err("scxq2.error.dict_entry_type_invalid", "dict", "dict entry not string", { field: "dict.dict", index: j }));
|
|
330
|
+
}
|
|
331
|
+
if (e.length > P.maxDictEntryUnits) {
|
|
332
|
+
return fail(err("scxq2.error.dict_entry_exceeds_limit", "dict", "dict entry too long", { field: "dict.dict", index: j }));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!dict.dict_sha256_canon) {
|
|
337
|
+
return fail(err("scxq2.error.dict_sha_missing", "canon", "dict sha missing", { field: "dict.dict_sha256_canon" }));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// =========================================================
|
|
341
|
+
// STEP 3 — BLOCK STRUCTURE
|
|
342
|
+
// =========================================================
|
|
343
|
+
const dictSha = dict.dict_sha256_canon;
|
|
344
|
+
|
|
345
|
+
for (let k = 0; k < pack.blocks.length; k++) {
|
|
346
|
+
const b = pack.blocks[k];
|
|
347
|
+
|
|
348
|
+
if (!b || typeof b !== "object" || b["@type"] !== "scxq2.block") {
|
|
349
|
+
return fail(err("scxq2.error.block_type_invalid", "block", "bad block @type", { field: "blocks", index: k }));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!P.allowedModes.includes(b.mode)) {
|
|
353
|
+
return fail(err("scxq2.error.block_mode_mismatch", "block", "block mode mismatch", { field: "blocks.mode", index: k }));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (!P.allowedEncodings.includes(b.encoding)) {
|
|
357
|
+
return fail(err("scxq2.error.block_encoding_mismatch", "block", "block encoding mismatch", { field: "blocks.encoding", index: k }));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (typeof b.b64 !== "string" || b.b64.length === 0) {
|
|
361
|
+
return fail(err("scxq2.error.block_b64_missing", "block", "b64 missing", { field: "blocks.b64", index: k }));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!b.dict_sha256_canon) {
|
|
365
|
+
return fail(err("scxq2.error.block_dict_link_missing", "block", "missing dict link", { field: "blocks.dict_sha256_canon", index: k }));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (b.dict_sha256_canon !== dictSha) {
|
|
369
|
+
return fail(err("scxq2.error.block_dict_link_mismatch", "block", "dict link mismatch", { field: "blocks.dict_sha256_canon", index: k }));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!b.block_sha256_canon) {
|
|
373
|
+
return fail(err("scxq2.error.block_sha_missing", "canon", "block sha missing", { field: "blocks.block_sha256_canon", index: k }));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (P.requireRoundtrip && !b.source_sha256_utf8) {
|
|
377
|
+
return fail(err("scxq2.error.block_source_sha_missing", "block", "source sha required", { field: "blocks.source_sha256_utf8", index: k }));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (P.allowUnknownBlockFields === false && hasUnknownFields(b, KNOWN_BLOCK_FIELDS)) {
|
|
381
|
+
return fail(err("scxq2.error.pack_field_forbidden", "block", "unknown block field", { field: "blocks", index: k }));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (P.allowEdges === false && "edges" in b) {
|
|
385
|
+
return fail(err("scxq2.error.policy_disabled_feature", "block", "edges not allowed", { field: "blocks.edges", index: k }));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// =========================================================
|
|
390
|
+
// STEP 4 — CANONICAL HASH VERIFICATION
|
|
391
|
+
// =========================================================
|
|
392
|
+
if (!pack.pack_sha256_canon) {
|
|
393
|
+
return fail(err("scxq2.error.pack_sha_missing", "canon", "pack sha missing", { field: "pack_sha256_canon" }));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
{
|
|
397
|
+
const packSha = sha256HexUtf8(canon(copyMinus(pack, "pack_sha256_canon")));
|
|
398
|
+
if (packSha !== pack.pack_sha256_canon) {
|
|
399
|
+
return fail(err("scxq2.error.pack_sha_mismatch", "canon", "pack sha mismatch", { field: "pack_sha256_canon" }));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
{
|
|
404
|
+
const dictSha2 = sha256HexUtf8(canon(copyMinus(dict, "dict_sha256_canon")));
|
|
405
|
+
if (dictSha2 !== dict.dict_sha256_canon) {
|
|
406
|
+
return fail(err("scxq2.error.dict_sha_mismatch", "canon", "dict sha mismatch", { field: "dict.dict_sha256_canon" }));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
for (let k = 0; k < pack.blocks.length; k++) {
|
|
411
|
+
const b = pack.blocks[k];
|
|
412
|
+
const bSha2 = sha256HexUtf8(canon(copyMinus(b, "block_sha256_canon")));
|
|
413
|
+
if (bSha2 !== b.block_sha256_canon) {
|
|
414
|
+
return fail(err("scxq2.error.block_sha_mismatch", "canon", "block sha mismatch", { field: "blocks.block_sha256_canon", index: k }));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// =========================================================
|
|
419
|
+
// STEP 5 — BASE64 DECODE + DECODE LAW + ROUNDTRIP
|
|
420
|
+
// =========================================================
|
|
421
|
+
for (let k = 0; k < pack.blocks.length; k++) {
|
|
422
|
+
const b = pack.blocks[k];
|
|
423
|
+
const bytes = strictB64Decode(b.b64);
|
|
424
|
+
|
|
425
|
+
if (!bytes) {
|
|
426
|
+
return fail(err("scxq2.error.block_b64_invalid", "block", "invalid base64", { field: "blocks.b64", index: k }));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (bytes.length > P.maxBlockB64Bytes) {
|
|
430
|
+
return fail(err("scxq2.error.decode_input_limit", "decode", "block bytes exceed limit", { field: "blocks.b64", index: k }));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const dec = scxq2DecodeUtf16(dict.dict, bytes, { maxOutputUnits: P.maxOutputUnits });
|
|
434
|
+
if (!dec.ok) {
|
|
435
|
+
return fail(mapDecodeErr(dec, b.lane_id, k));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (P.requireRoundtrip) {
|
|
439
|
+
const outSha = sha256HexUtf8(dec.value);
|
|
440
|
+
if (outSha !== b.source_sha256_utf8) {
|
|
441
|
+
return fail(err("scxq2.error.proof_roundtrip_sha_mismatch", "proof", "roundtrip sha mismatch", { lane_id: b.lane_id, index: k }));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// =========================================================
|
|
447
|
+
// STEP 6 — PROOF OBJECT
|
|
448
|
+
// =========================================================
|
|
449
|
+
if (P.requireProof) {
|
|
450
|
+
const proof = pack.proof;
|
|
451
|
+
|
|
452
|
+
if (!proof || typeof proof !== "object") {
|
|
453
|
+
return fail(err("scxq2.error.proof_type_invalid", "proof", "proof invalid", { field: "proof" }));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (proof["@type"] !== "cc.proof") {
|
|
457
|
+
return fail(err("scxq2.error.proof_type_invalid", "proof", "bad proof @type", { field: "proof.@type" }));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (proof["@version"] !== "1.0.0") {
|
|
461
|
+
return fail(err("scxq2.error.proof_version_unsupported", "proof", "unsupported proof version", { field: "proof.@version" }));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (proof.ok !== true) {
|
|
465
|
+
return fail(err("scxq2.error.proof_ok_false", "proof", "proof ok false", { field: "proof.ok" }));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Witness presence check
|
|
469
|
+
for (const f of ["engine", "source_sha256_utf8", "dict_sha256_canon", "block_sha256_canon", "roundtrip_sha256_utf8"]) {
|
|
470
|
+
if (!proof[f]) {
|
|
471
|
+
return fail(err("scxq2.error.proof_witness_missing", "proof", "missing proof witness", { field: `proof.${f}` }));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// =========================================================
|
|
477
|
+
// SUCCESS
|
|
478
|
+
// =========================================================
|
|
479
|
+
return ok(pack.pack_sha256_canon, dict.dict_sha256_canon, pack.blocks.length);
|
|
480
|
+
}
|