@typedstandards/verify-core 0.13.0 → 0.14.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 +31 -0
- package/README.md +8 -0
- package/dist/checks.d.ts +1 -0
- package/dist/checks.js +14 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/strict-json.d.ts +24 -0
- package/dist/strict-json.js +109 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,37 @@ references are to `npstorey/civic-ai-tools-website` (#119 is the offline-crypto
|
|
|
6
6
|
hardening arc; #116 is the standalone-verifier arc this package was extracted
|
|
7
7
|
in).
|
|
8
8
|
|
|
9
|
+
## 0.14.0 — 2026-10-16
|
|
10
|
+
|
|
11
|
+
The W5 release (civic-ai-tools#260, shipped in #146 and #147). **A minor bump**: new exports
|
|
12
|
+
(`CONTENT_LAYOUT`; `parseStrictJson` and `RepeatedMemberNameError`), and a type URI check #12 newly
|
|
13
|
+
reads `ok`.
|
|
14
|
+
|
|
15
|
+
- **`content/layout/v1` is registered (hub ADR-0034; spec §8.14).** New export `CONTENT_LAYOUT`
|
|
16
|
+
(`'content/layout/v1'`). `KNOWN_TYPE_URIS` holds eighteen entries: `content/analysis/v1`, then
|
|
17
|
+
`content/layout/v1`, then the sixteen attestation sub-types in the §8.12.1 table order, unchanged.
|
|
18
|
+
Check #12 reads `ok` for a layout node, where it read `unknown_type`. No payload check is added: a
|
|
19
|
+
layout node's envelope verifies as any node's (§8.14.5).
|
|
20
|
+
- **`parseStrictJson` and `RepeatedMemberNameError` (new exports; typedstandards#145,
|
|
21
|
+
specification §8.2 and §9.2 check #1).** `parseStrictJson(text: string | Uint8Array)`
|
|
22
|
+
refuses a JSON text that repeats a member name within an object, which is not I-JSON
|
|
23
|
+
(RFC 7493), the input RFC 8785 JCS requires. It throws `RepeatedMemberNameError`
|
|
24
|
+
(`name` `'RepeatedMemberNameError'`), carrying `memberName` (unescaped) and
|
|
25
|
+
`pointer`, the JSON Pointer (RFC 6901) of the object that holds it. Names compare
|
|
26
|
+
after unescaping; the same name in two different objects is not a repeat. A text with
|
|
27
|
+
no repeated name returns exactly what `JSON.parse` returns, and a text that is not
|
|
28
|
+
JSON throws what `JSON.parse` throws, unchanged. Bytes are decoded as UTF-8 with
|
|
29
|
+
`fatal`. The walk keeps its own stack, so nesting the platform parser accepts does
|
|
30
|
+
not overflow it. No dependency is added.
|
|
31
|
+
- **`verifyRecord` is unchanged:** it takes a parsed package, so the refusal belongs to
|
|
32
|
+
whoever reads the text. `JSON.parse` keeps the last occurrence of a repeated name, and
|
|
33
|
+
the envelope hash is computed over the parsed object, so a caller that parses a
|
|
34
|
+
package with it accepts a text the specification refuses.
|
|
35
|
+
- **A negative vector**, `src/__fixtures__/repeated-member-name.bundle.json` (not
|
|
36
|
+
published): the site's minted interop bundle with a second, earlier `summary` member in
|
|
37
|
+
its package, which under `JSON.parse` gives the signed package's envelope and content
|
|
38
|
+
hashes. Its provenance is in `src/__fixtures__/README.md`.
|
|
39
|
+
|
|
9
40
|
## 0.13.0 — 2026-09-27
|
|
10
41
|
|
|
11
42
|
The core-minors release (typedstandards#113, shipped in #114, #115 and #118). **A minor bump** —
|
package/README.md
CHANGED
|
@@ -64,6 +64,14 @@ const result = await verifyRecord(input, {
|
|
|
64
64
|
// contentHash, typeResolution, signerIdentity, lifecycle, ...
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
> **Parse the package's text with `parseStrictJson`.** A record package's JSON text is
|
|
68
|
+
> I-JSON (RFC 7493), so a verifier refuses a text that repeats a member name within an
|
|
69
|
+
> object, before any hash is computed (spec §8.2, §9.2 check #1). `JSON.parse` keeps the
|
|
70
|
+
> last occurrence and drops the others, and `verifyRecord` takes the parsed object, so it
|
|
71
|
+
> cannot see a repeat. `parseStrictJson(text)` (a string, or bytes decoded as UTF-8)
|
|
72
|
+
> throws `RepeatedMemberNameError`, with `memberName` and `pointer` (the JSON Pointer of
|
|
73
|
+
> the object), on a repeated name, and otherwise returns what `JSON.parse` returns.
|
|
74
|
+
|
|
67
75
|
> **`verifyEvidence` is the same function.** The 2026-08-19 vocabulary settlement
|
|
68
76
|
> renamed the entry point to `verifyRecord` (spec Appendix J) — "evidence" is
|
|
69
77
|
> retired from the artifact/infrastructure brand role and kept only for the
|
package/dist/checks.d.ts
CHANGED
|
@@ -102,6 +102,7 @@ export interface ContentProfileCheck {
|
|
|
102
102
|
* integrity failure: both labels are signature-covered.
|
|
103
103
|
*/
|
|
104
104
|
export declare function checkContentProfile(pkg: Record<string, unknown>): ContentProfileCheck;
|
|
105
|
+
export declare const CONTENT_LAYOUT = "content/layout/v1";
|
|
105
106
|
export declare const KNOWN_TYPE_URIS: readonly string[];
|
|
106
107
|
export declare const TYPE_RESOLUTION_STATUSES: readonly ["ok", "implicit", "unknown_type"];
|
|
107
108
|
export type TypeResolutionStatus = (typeof TYPE_RESOLUTION_STATUSES)[number];
|
package/dist/checks.js
CHANGED
|
@@ -244,15 +244,22 @@ export function checkContentProfile(pkg) {
|
|
|
244
244
|
return { status: 'ok', ...base };
|
|
245
245
|
}
|
|
246
246
|
// --- Typed-standards envelope checks (spec §9.2 checks #12, #14, #15) ---
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
247
|
+
// The `content/layout/v1` sub-type (spec §8.14, hub ADR-0034): a standalone
|
|
248
|
+
// assertion of how a host's page arranges a node's parts. Registered for check
|
|
249
|
+
// #12; verify-core runs no payload check on it (§8.14.5).
|
|
250
|
+
export const CONTENT_LAYOUT = 'content/layout/v1';
|
|
251
|
+
// v0.1 ratified node type URIs. Recognized so check #12 resolves them; only
|
|
252
|
+
// `content/analysis/v1` (+ the lifecycle sub-types) are operationalized today,
|
|
253
|
+
// but the full ratified set is registered so conformant packages don't render
|
|
254
|
+
// as `unknown_type`. The two `content/*` sub-types come first:
|
|
255
|
+
// `content/analysis/v1`, then `content/layout/v1` (§8.14). The sixteen
|
|
256
|
+
// attestation sub-types follow in the order of the §8.12.1 table;
|
|
257
|
+
// `type-resolution.test.ts` holds that table's copy. Exported and frozen so a
|
|
258
|
+
// check outside this package can compare the registered set with the
|
|
259
|
+
// specification's list without being able to change it.
|
|
254
260
|
export const KNOWN_TYPE_URIS = Object.freeze([
|
|
255
261
|
'content/analysis/v1',
|
|
262
|
+
CONTENT_LAYOUT,
|
|
256
263
|
'attestation/withdraws/v1',
|
|
257
264
|
'attestation/reinstates/v1',
|
|
258
265
|
'attestation/supersedes/v1',
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** A JSON text repeats a member name within one object, so it is not I-JSON (RFC 7493). */
|
|
2
|
+
export declare class RepeatedMemberNameError extends Error {
|
|
3
|
+
/** The repeated member name, unescaped. */
|
|
4
|
+
readonly memberName: string;
|
|
5
|
+
/** The JSON Pointer (RFC 6901) of the object that holds it: `""` for the top-level value. */
|
|
6
|
+
readonly pointer: string;
|
|
7
|
+
constructor(memberName: string, pointer: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse a record's JSON text strictly: refuse a text that repeats a member name
|
|
11
|
+
* within an object (I-JSON, RFC 7493; specification §8.2), before any hash is
|
|
12
|
+
* computed over what it holds.
|
|
13
|
+
*
|
|
14
|
+
* - Bytes are decoded as UTF-8 with `fatal`, so malformed UTF-8 throws the
|
|
15
|
+
* decoder's `TypeError` rather than being replaced. A leading byte order mark is
|
|
16
|
+
* removed, as the decoder does by default.
|
|
17
|
+
* - A text that is not JSON throws what `JSON.parse` throws, unchanged.
|
|
18
|
+
* - A text that repeats a member name within one object throws
|
|
19
|
+
* `RepeatedMemberNameError`, naming the member and the JSON Pointer of the
|
|
20
|
+
* object. Names compare after unescaping, so `"a"` repeats `"a"`; the same
|
|
21
|
+
* name in two different objects is not a repeat.
|
|
22
|
+
* - Otherwise it returns `JSON.parse`'s value.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseStrictJson(text: string | Uint8Array): unknown;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The strict parse of a record's JSON text (typedstandards#145; specification §8.2,
|
|
2
|
+
// §9.2 check #1; hub ADR-0035).
|
|
3
|
+
//
|
|
4
|
+
// A record package's JSON text is I-JSON (RFC 7493), the input RFC 8785 JCS
|
|
5
|
+
// requires, and I-JSON allows no member name to repeat within an object. The
|
|
6
|
+
// platform's `JSON.parse` keeps the last occurrence of a repeated name and drops
|
|
7
|
+
// the earlier ones without a word, and the envelope hash is computed over the
|
|
8
|
+
// parsed object, so a text with a repeated name hashes as the text without it while
|
|
9
|
+
// a reader that keeps another occurrence sees other content. `parseStrictJson`
|
|
10
|
+
// refuses such a text, and otherwise returns exactly what `JSON.parse` returns.
|
|
11
|
+
//
|
|
12
|
+
// Pure: no I/O, no dependency, browser-safe. The walk keeps its own stack, so any
|
|
13
|
+
// nesting the platform parser accepts is walked without recursion.
|
|
14
|
+
/** A JSON text repeats a member name within one object, so it is not I-JSON (RFC 7493). */
|
|
15
|
+
export class RepeatedMemberNameError extends Error {
|
|
16
|
+
/** The repeated member name, unescaped. */
|
|
17
|
+
memberName;
|
|
18
|
+
/** The JSON Pointer (RFC 6901) of the object that holds it: `""` for the top-level value. */
|
|
19
|
+
pointer;
|
|
20
|
+
constructor(memberName, pointer) {
|
|
21
|
+
super(`JSON text is not I-JSON (RFC 7493): the member name ${JSON.stringify(memberName)} is repeated in the object at JSON Pointer ${JSON.stringify(pointer)}`);
|
|
22
|
+
this.name = 'RepeatedMemberNameError';
|
|
23
|
+
this.memberName = memberName;
|
|
24
|
+
this.pointer = pointer;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const QUOTE = 0x22;
|
|
28
|
+
const BACKSLASH = 0x5c;
|
|
29
|
+
const OPEN_OBJECT = 0x7b;
|
|
30
|
+
const CLOSE_OBJECT = 0x7d;
|
|
31
|
+
const OPEN_ARRAY = 0x5b;
|
|
32
|
+
const CLOSE_ARRAY = 0x5d;
|
|
33
|
+
const COMMA = 0x2c;
|
|
34
|
+
const escapeSegment = (s) => s.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
35
|
+
/** The index of the quote that closes the string opening at `start`, in text known to be JSON. */
|
|
36
|
+
function stringEnd(text, start) {
|
|
37
|
+
let i = start + 1;
|
|
38
|
+
for (;;) {
|
|
39
|
+
const c = text.charCodeAt(i);
|
|
40
|
+
if (c === BACKSLASH)
|
|
41
|
+
i += 2;
|
|
42
|
+
else if (c === QUOTE)
|
|
43
|
+
return i;
|
|
44
|
+
else
|
|
45
|
+
i += 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Throw `RepeatedMemberNameError` at the first repeated name, in text `JSON.parse` has accepted. */
|
|
49
|
+
function refuseRepeatedNames(text) {
|
|
50
|
+
const stack = [];
|
|
51
|
+
const segments = [];
|
|
52
|
+
const n = text.length;
|
|
53
|
+
for (let i = 0; i < n; i++) {
|
|
54
|
+
const c = text.charCodeAt(i);
|
|
55
|
+
if (c === QUOTE) {
|
|
56
|
+
const end = stringEnd(text, i);
|
|
57
|
+
const top = stack[stack.length - 1];
|
|
58
|
+
if (top?.names && top.expectName) {
|
|
59
|
+
const raw = text.slice(i + 1, end);
|
|
60
|
+
const name = raw.includes('\\') ? JSON.parse(text.slice(i, end + 1)) : raw;
|
|
61
|
+
if (top.names.has(name))
|
|
62
|
+
throw new RepeatedMemberNameError(name, segments.map((s) => `/${escapeSegment(s)}`).join(''));
|
|
63
|
+
top.names.add(name);
|
|
64
|
+
top.segment = name;
|
|
65
|
+
top.expectName = false;
|
|
66
|
+
}
|
|
67
|
+
i = end;
|
|
68
|
+
}
|
|
69
|
+
else if (c === OPEN_OBJECT || c === OPEN_ARRAY) {
|
|
70
|
+
const parent = stack[stack.length - 1];
|
|
71
|
+
if (parent)
|
|
72
|
+
segments.push(parent.names ? parent.segment : String(parent.index));
|
|
73
|
+
stack.push({ names: c === OPEN_OBJECT ? new Set() : null, expectName: c === OPEN_OBJECT, segment: '', index: 0 });
|
|
74
|
+
}
|
|
75
|
+
else if (c === CLOSE_OBJECT || c === CLOSE_ARRAY) {
|
|
76
|
+
stack.pop();
|
|
77
|
+
if (stack.length > 0)
|
|
78
|
+
segments.pop();
|
|
79
|
+
}
|
|
80
|
+
else if (c === COMMA) {
|
|
81
|
+
const top = stack[stack.length - 1];
|
|
82
|
+
if (top.names)
|
|
83
|
+
top.expectName = true;
|
|
84
|
+
else
|
|
85
|
+
top.index += 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Parse a record's JSON text strictly: refuse a text that repeats a member name
|
|
91
|
+
* within an object (I-JSON, RFC 7493; specification §8.2), before any hash is
|
|
92
|
+
* computed over what it holds.
|
|
93
|
+
*
|
|
94
|
+
* - Bytes are decoded as UTF-8 with `fatal`, so malformed UTF-8 throws the
|
|
95
|
+
* decoder's `TypeError` rather than being replaced. A leading byte order mark is
|
|
96
|
+
* removed, as the decoder does by default.
|
|
97
|
+
* - A text that is not JSON throws what `JSON.parse` throws, unchanged.
|
|
98
|
+
* - A text that repeats a member name within one object throws
|
|
99
|
+
* `RepeatedMemberNameError`, naming the member and the JSON Pointer of the
|
|
100
|
+
* object. Names compare after unescaping, so `"a"` repeats `"a"`; the same
|
|
101
|
+
* name in two different objects is not a repeat.
|
|
102
|
+
* - Otherwise it returns `JSON.parse`'s value.
|
|
103
|
+
*/
|
|
104
|
+
export function parseStrictJson(text) {
|
|
105
|
+
const source = typeof text === 'string' ? text : new TextDecoder('utf-8', { fatal: true }).decode(text);
|
|
106
|
+
const value = JSON.parse(source);
|
|
107
|
+
refuseRepeatedNames(source);
|
|
108
|
+
return value;
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typedstandards/verify-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Portable, browser-safe verification core for Typed Standards record packages — the §9.2 check suite, single-sourced so it verifies identically on a server and in the browser.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nathan Storey",
|