@openwop/openwop-conformance 2.37.1 → 2.38.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/README.md +3 -3
- package/dist/cli.js +8 -18
- package/dist/lib/certification-bundle-v3.js +46 -18
- package/dist/lib/jcs.js +274 -0
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +247 -33
- package/scenario-majors.json +7 -3
- package/schemas/CORPUS-STAMP.json +29 -29
- package/src/cli.ts +8 -16
- package/src/lib/certification-bundle-v3.ts +42 -17
- package/src/lib/front-mux.ts +36 -1
- package/src/lib/jcs.ts +229 -0
- package/src/lib/llm-cache-key-recipe.ts +10 -20
- package/src/lib/scoped-receiver.ts +2 -2
- package/src/scenarios/jcs-vectors.test.ts +108 -0
- package/src/scenarios/semantic-digest-vectors.test.ts +8 -0
- package/src/scenarios/v2-a2a-operation-map.test.ts +28 -0
- package/src/scenarios/v2-a2ui-v09-surface.test.ts +18 -4
- package/src/scenarios/v2-content-locale-keys.test.ts +10 -1
- package/src/scenarios/v2-mrtr-rounds-ceiling.test.ts +7 -1
- package/src/scenarios/v2-oauth-client-pkce-state-iss.test.ts +7 -1
- package/vectors/jcs-v1.json +294 -0
- package/vectors/semantic-request-digest-v2.json +52 -0
|
@@ -10,30 +10,20 @@
|
|
|
10
10
|
* RFC 0041 §E SECURITY-invariant probe (intra-host reproducibility +
|
|
11
11
|
* non-recipe-field invariance + Phase 4 advertisement alignment).
|
|
12
12
|
*
|
|
13
|
-
* `canonicalize`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* `canonicalize` is RFC 8785 JCS with the RFC 0212 I-JSON refusal set (the
|
|
14
|
+
* suite's one implementation, `./jcs.ts`), and `tools[]` sorts by UTF-16 code
|
|
15
|
+
* units — never `localeCompare`, which orders `get_weather` / `getWeather`
|
|
16
|
+
* differently per locale and so breaks the TS/Python/Go agreement RFC 0150 §C
|
|
17
|
+
* asks for. Keep in sync with `spec/v1/replay.md` §B.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import { createHash } from 'node:crypto';
|
|
20
21
|
import { driver } from './driver.js';
|
|
22
|
+
import { canonicalJSON, codeUnitCompare } from './jcs.js';
|
|
21
23
|
|
|
22
|
-
/** RFC 8785 JCS
|
|
23
|
-
* fields). Sorted keys recursively; no whitespace; preserved array order;
|
|
24
|
-
* strings JSON-encoded verbatim (no NFC normalization — the recipe
|
|
25
|
-
* inputs in our test seam are ASCII). */
|
|
24
|
+
/** RFC 8785 JCS (RFC 0212). Throws `JcsRefusal` on a non-I-JSON value. */
|
|
26
25
|
export function canonicalize(value: unknown): string {
|
|
27
|
-
|
|
28
|
-
if (typeof value === 'boolean' || typeof value === 'number') return JSON.stringify(value);
|
|
29
|
-
if (typeof value === 'string') return JSON.stringify(value);
|
|
30
|
-
if (Array.isArray(value)) return '[' + value.map((v) => canonicalize(v)).join(',') + ']';
|
|
31
|
-
if (typeof value === 'object') {
|
|
32
|
-
const obj = value as Record<string, unknown>;
|
|
33
|
-
const keys = Object.keys(obj).sort();
|
|
34
|
-
return '{' + keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(',') + '}';
|
|
35
|
-
}
|
|
36
|
-
return JSON.stringify(value);
|
|
26
|
+
return canonicalJSON(value);
|
|
37
27
|
}
|
|
38
28
|
|
|
39
29
|
/** @deprecated RETIRED v1 projection (pre RFC 0150 §C). Kept ONLY so a
|
|
@@ -44,7 +34,7 @@ export function canonicalize(value: unknown): string {
|
|
|
44
34
|
export function projectRecipe(raw: Record<string, unknown>): Record<string, unknown> {
|
|
45
35
|
const out: Record<string, unknown> = { provider: raw.provider, model: raw.model, messages: raw.messages };
|
|
46
36
|
if (Array.isArray(raw.tools) && raw.tools.length > 0) {
|
|
47
|
-
out.tools = [...(raw.tools as Array<{ name: string }>)].sort((a, b) => a.name
|
|
37
|
+
out.tools = [...(raw.tools as Array<{ name: string }>)].sort((a, b) => codeUnitCompare(a.name, b.name));
|
|
48
38
|
}
|
|
49
39
|
if (typeof raw.temperature === 'number') out.temperature = raw.temperature;
|
|
50
40
|
if (typeof raw.topP === 'number') out.topP = raw.topP;
|
|
@@ -76,7 +66,7 @@ export const SEMANTIC_REQUEST_RECIPE_V2 = 'openwop-semantic-request-v2';
|
|
|
76
66
|
export function projectSemanticRequestV2(raw: Record<string, unknown>): Record<string, unknown> {
|
|
77
67
|
const request: Record<string, unknown> = { messages: raw.messages };
|
|
78
68
|
if (Array.isArray(raw.tools) && raw.tools.length > 0) {
|
|
79
|
-
request.tools = [...(raw.tools as Array<{ name: string }>)].sort((a, b) => a.name
|
|
69
|
+
request.tools = [...(raw.tools as Array<{ name: string }>)].sort((a, b) => codeUnitCompare(a.name, b.name));
|
|
80
70
|
}
|
|
81
71
|
for (const k of ['temperature', 'topP', 'topK', 'maxOutputTokens', 'seed'] as const) {
|
|
82
72
|
if (typeof raw[k] === 'number') request[k] = raw[k];
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
import { randomBytes } from 'node:crypto';
|
|
51
51
|
import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
52
|
-
import { FRONT_MUX_PREFIX, registerBehindFront, routeFronted, unregisterBehindFront } from './front-mux.js';
|
|
52
|
+
import { FRONT_MUX_PREFIX, registerBehindFront, routeFronted, unregisterBehindFront, withoutFrontPath } from './front-mux.js';
|
|
53
53
|
import { receiverBinding, resolvePublicFront } from './webhook-receiver.js';
|
|
54
54
|
|
|
55
55
|
/** The operator's public front for the suite's webhook receiver. */
|
|
@@ -122,7 +122,7 @@ export async function startScopedReceiver(
|
|
|
122
122
|
// Counted BEFORE routing: `routeFronted` answers an unknown nonce itself and
|
|
123
123
|
// cannot report that it did, and a request that is not this exercise's must
|
|
124
124
|
// still be visible in the failure detail.
|
|
125
|
-
if (!(request.url ?? '/').startsWith(`${FRONT_MUX_PREFIX}${nonce}`)) foreign += 1;
|
|
125
|
+
if (!withoutFrontPath(WEBHOOK_FRONT_ENV, request.url ?? '/').startsWith(`${FRONT_MUX_PREFIX}${nonce}`)) foreign += 1;
|
|
126
126
|
if (routeFronted(WEBHOOK_FRONT_ENV, request, res)) return;
|
|
127
127
|
// Not an `/fx/` path at all: a stranger, or a host that dropped the path it
|
|
128
128
|
// was given. Answered, never recorded — a receiver that counts what it was
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0212 — canonical JSON is RFC 8785 (JCS), and the input MUST be I-JSON.
|
|
3
|
+
*
|
|
4
|
+
* `conformance/vectors/jcs-v1.json` is normative: every signature and digest in
|
|
5
|
+
* the corpus (pack signatures, the certification-bundle attestation,
|
|
6
|
+
* `discovery.sha256`, `witnessSha256`, the RFC 0150 request digest) is over
|
|
7
|
+
* these bytes, and an SDK in another language reproduces the same file. This
|
|
8
|
+
* scenario holds the suite's one implementation (`lib/jcs.ts`) to it.
|
|
9
|
+
*
|
|
10
|
+
* Every leg RECOMPUTES from the vector input; none compares two constants from
|
|
11
|
+
* the file. The sabotages RFC 0212 names, each of which turns a leg red:
|
|
12
|
+
*
|
|
13
|
+
* - a rebuild-object canonicalizer (`JSON.stringify` of an object whose keys
|
|
14
|
+
* were inserted in sorted order) — integer-like keys enumerate first, so
|
|
15
|
+
* `integer-like-keys` fails;
|
|
16
|
+
* - NaN → `null` coercion — the value-boundary refusal leg fails;
|
|
17
|
+
* - a parser that keeps the last duplicate name (`JSON.parse`) — the
|
|
18
|
+
* `duplicate-name` refusal is accepted;
|
|
19
|
+
* - a `localeCompare` comparator — the ordering leg and the
|
|
20
|
+
* `rfc8785-3.2.3-sort` vector fail.
|
|
21
|
+
*
|
|
22
|
+
* Server-free and always-on; the vectors ship with the conformance package.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { describe, it, expect } from 'vitest';
|
|
26
|
+
import { createHash } from 'node:crypto';
|
|
27
|
+
import { readFileSync } from 'node:fs';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { dirname, resolve } from 'node:path';
|
|
30
|
+
import { canonicalJSON, canonicalizeText, codeUnitCompare, JcsRefusal } from '../lib/jcs.js';
|
|
31
|
+
import { witnessDigest, type BundleV3Requirement } from '../lib/certification-bundle-v3.js';
|
|
32
|
+
import { req } from '../lib/requirement-ids.js';
|
|
33
|
+
|
|
34
|
+
interface NumberVector { readonly ieee754: string; readonly canonical: string }
|
|
35
|
+
interface NumberRefusal { readonly ieee754: string; readonly refuse: string }
|
|
36
|
+
interface ObjectVector { readonly id: string; readonly why: string; readonly input: string; readonly canonical: string; readonly sha256: string }
|
|
37
|
+
interface RefusalVector { readonly id: string; readonly why: string; readonly input: string; readonly refuse: string }
|
|
38
|
+
interface Ordering { readonly why: string; readonly ids: readonly string[]; readonly codeUnit: readonly string[] }
|
|
39
|
+
|
|
40
|
+
const VECTORS_PATH = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', 'vectors', 'jcs-v1.json');
|
|
41
|
+
const doc = JSON.parse(readFileSync(VECTORS_PATH, 'utf8')) as {
|
|
42
|
+
numbers: readonly NumberVector[];
|
|
43
|
+
numberRefusals: readonly NumberRefusal[];
|
|
44
|
+
objects: readonly ObjectVector[];
|
|
45
|
+
refusals: readonly RefusalVector[];
|
|
46
|
+
ordering: Ordering;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const SPEC = 'RFC 0212 · spec/v2/core/conformance.md §"Canonical JSON"';
|
|
50
|
+
const sha256 = (s: string): string => createHash('sha256').update(s, 'utf8').digest('hex');
|
|
51
|
+
const double = (hex: string): number => Buffer.from(hex, 'hex').readDoubleBE(0);
|
|
52
|
+
|
|
53
|
+
function refusalOf(fn: () => unknown): JcsRefusal | undefined {
|
|
54
|
+
try { fn(); } catch (e) { if (e instanceof JcsRefusal) return e; throw e; }
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('RFC 0212 — canonical JSON is RFC 8785 JCS over I-JSON', () => {
|
|
59
|
+
it('the vector set is present and non-trivial', () => {
|
|
60
|
+
// An empty or truncated file would make every leg below vacuous.
|
|
61
|
+
expect(doc.numbers.length, req('openwop.it.jcs-vectors.the-vector-set-is-present-and-non-trivial', SPEC, 'the vectors MUST cover RFC 8785 Appendix B numbers, documents and every §B refusal kind')).toBeGreaterThanOrEqual(20);
|
|
62
|
+
expect(doc.objects.length).toBeGreaterThanOrEqual(10);
|
|
63
|
+
expect(new Set(doc.refusals.map((r) => r.refuse))).toEqual(new Set(['duplicate-name', 'lone-surrogate', 'integer-out-of-range', 'not-json', 'non-finite']));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it.each(doc.numbers.map((n) => [n.ieee754, n] as const))('number %s serializes per RFC 8785 Appendix B', (_hex, n) => {
|
|
67
|
+
expect(canonicalJSON(double(n.ieee754)), req('openwop.it.jcs-vectors.number-s-serializes-per-rfc-8785-appendix-b', SPEC, 'numbers MUST serialize as ECMAScript Number::toString (RFC 8785 §3.2.2.3)')).toBe(n.canonical);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it.each(doc.objects.map((o) => [o.id, o] as const))('document %s canonicalizes and hashes', (_id, o) => {
|
|
71
|
+
// The preimage is asserted as well as the hash so a failing implementation
|
|
72
|
+
// can diff WHICH member it got wrong.
|
|
73
|
+
const canonical = canonicalizeText(o.input);
|
|
74
|
+
expect(canonical, req('openwop.it.jcs-vectors.document-s-canonicalizes-and-hashes', SPEC, o.why)).toBe(o.canonical);
|
|
75
|
+
expect(sha256(canonical), req('openwop.it.jcs-vectors.document-s-canonicalizes-and-hashes', SPEC, o.why)).toBe(o.sha256);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it.each(doc.refusals.map((r) => [r.id, r] as const))('text %s is refused, not coerced', (_id, r) => {
|
|
79
|
+
const refusal = refusalOf(() => canonicalizeText(r.input));
|
|
80
|
+
expect(refusal?.kind, req('openwop.it.jcs-vectors.text-s-is-refused-not-coerced', SPEC, `RFC 0212 §B: ${r.why}`)).toBe(r.refuse);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('a non-I-JSON native value is refused at the value boundary', () => {
|
|
84
|
+
const cases: readonly [string, unknown][] = [
|
|
85
|
+
...doc.numberRefusals.map((n) => [`ieee754 ${n.ieee754}`, { n: double(n.ieee754) }] as [string, unknown]),
|
|
86
|
+
['NaN', { n: Number.NaN }],
|
|
87
|
+
['undefined member', { a: undefined }],
|
|
88
|
+
['undefined element', [undefined]],
|
|
89
|
+
['Date', { at: new Date(0) }],
|
|
90
|
+
['bigint', { n: 1n }],
|
|
91
|
+
['lone surrogate', { s: '\ud800' }],
|
|
92
|
+
['lone surrogate in a name', { '\udc00': 1 }],
|
|
93
|
+
];
|
|
94
|
+
for (const [label, value] of cases) {
|
|
95
|
+
expect(refusalOf(() => canonicalJSON(value)), req('openwop.it.jcs-vectors.a-non-i-json-native-value-is-refused-at-the-value-boundary', SPEC, `RFC 0212 §B: ${label} MUST be refused, not serialized`)).toBeInstanceOf(JcsRefusal);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('member names and certification rows sort by UTF-16 code units, not by locale', () => {
|
|
100
|
+
const o = doc.ordering;
|
|
101
|
+
expect([...o.ids].sort(codeUnitCompare), req('openwop.it.jcs-vectors.member-names-and-certification-rows-sort-by-utf-16-code-units-not-by-locale', SPEC, o.why)).toEqual([...o.codeUnit]);
|
|
102
|
+
// The certification digest uses the same comparator (RFC 0212 §C): rows
|
|
103
|
+
// given in vector order digest to the JCS of the code-unit-sorted rows.
|
|
104
|
+
const rows: BundleV3Requirement[] = o.ids.map((id) => ({ id, scenario: 's.test.ts', result: 'executed-pass' }) as BundleV3Requirement);
|
|
105
|
+
const expected = sha256(canonicalJSON([...o.codeUnit].map((id) => ({ id, scenario: 's.test.ts', result: 'executed-pass' }))));
|
|
106
|
+
expect(witnessDigest(rows), req('openwop.it.jcs-vectors.member-names-and-certification-rows-sort-by-utf-16-code-units-not-by-locale', SPEC, 'witnessSha256 rows MUST sort by id in UTF-16 code-unit order; a locale-sensitive comparator MUST NOT be used')).toBe(expected);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -110,6 +110,14 @@ describe('RFC 0150 §C — semantic request digest golden vectors', () => {
|
|
|
110
110
|
expect(digest('tools-sorted-by-name'), req('openwop.it.semantic-digest-vectors.tool-order-is-not-semantic-sorted-and-reversed-agree', 'RFC 0150 §C', 'tool order is not semantic — sorted and reversed agree')).toBe(digest('tools-reversed-same-digest'));
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
it('tool order uses UTF-16 code units, where a locale comparator disagrees', () => {
|
|
114
|
+
// RFC 0212 §D. `alpha`/`zeta` order the same under every comparator, so the
|
|
115
|
+
// pair above cannot catch a `localeCompare` sort; `getWeather`/`get_weather`
|
|
116
|
+
// can — English collation ignores `_` and puts them the other way round.
|
|
117
|
+
expect(digest('tools-code-unit-order'), req('openwop.it.semantic-digest-vectors.tool-order-uses-utf-16-code-units-where-a-locale-comparator-disagrees', 'RFC 0212 §D · RFC 0150 §C', 'tools[] MUST sort by name in UTF-16 code-unit order')).toBe(digest('tools-code-unit-order-reversed'));
|
|
118
|
+
expect(canonicalize(projectSemanticRequestV2(byId.get('tools-code-unit-order')?.input ?? {})), req('openwop.it.semantic-digest-vectors.tool-order-uses-utf-16-code-units-where-a-locale-comparator-disagrees', 'RFC 0212 §D · RFC 0150 §C', 'getWeather (0x57) MUST precede get_weather (0x5F)')).toContain('"tools":[{"name":"getWeather"');
|
|
119
|
+
});
|
|
120
|
+
|
|
113
121
|
it('message order IS semantic — reversing changes the digest', () => {
|
|
114
122
|
expect(
|
|
115
123
|
digest('message-order-is-semantic'),
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* @see spec/v2/interop-map.json a2a.operations / a2a.taskState / a2a.errors
|
|
26
26
|
* @see RFCS/0208-v2-a2a-mcp-operation-mappings.md §C, §D, §E
|
|
27
27
|
* @see RFCS/0199-outbound-oauth-client-and-credential-interrupt.md §D.1 (the credential leg; interop-map.json a2a.taskState override row)
|
|
28
|
+
* @see RFCS/0214-a2a-push-credential-is-a-destination-credential.md (the push-config refusal leg; invariants a2a-push-credential-destination-bound, a2a-push-secrets-not-returned)
|
|
28
29
|
* @see RFCS/0211-a2a-error-details-are-errorinfo.md §A–§E (the error-details legs; the isolation comparator)
|
|
29
30
|
*/
|
|
30
31
|
import { describe, it, expect } from 'vitest';
|
|
@@ -249,6 +250,33 @@ describe('RFC 0208 — v2-a2a-operation-map (host as A2A 1.0 server, gated on a2
|
|
|
249
250
|
await rpc(t.url, 'CancelTask', { id });
|
|
250
251
|
});
|
|
251
252
|
|
|
253
|
+
it('a host not advertising a2a.pushNotifications refuses all four push-config operations with PushNotificationNotSupportedError', async () => {
|
|
254
|
+
const rid = 'openwop.requirement.0214.a2a-push-unadvertised-refused';
|
|
255
|
+
const pushDoc = 'interop-map.json a2a.errors PushNotificationNotSupportedError (serverWhen: push config without a2a.pushNotifications) and the push-config operation rows; A2A v1.0.1 §3.3.4';
|
|
256
|
+
const t = await target(true);
|
|
257
|
+
if (!t.ok) return skip(t, rid);
|
|
258
|
+
const a2a = (await familyAdvertised('a2a'))!;
|
|
259
|
+
if (a2a['pushNotifications'] === true) return softSkip('inapplicable', 'a2a.pushNotifications is advertised — the push-config rows are served here, not refused');
|
|
260
|
+
const first = await startApprovalTask(t.url);
|
|
261
|
+
const id = first.task?.id;
|
|
262
|
+
expect(typeof id, req(rid, DOC, `SendMessage MUST start a task: ${JSON.stringify(first.rpc.error)}`)).toBe('string');
|
|
263
|
+
// Positive control: the task is readable, so a -32001 below could not be masking the refusal under test.
|
|
264
|
+
const read = await rpc(t.url, 'GetTask', { id });
|
|
265
|
+
expect(read.error, req(rid, 'a2a.operations GetTask', `positive control: the task the suite just started MUST be readable (got ${JSON.stringify(read.error)})`)).toBeUndefined();
|
|
266
|
+
const configId = `conf-0214-${randomBytes(6).toString('hex')}`;
|
|
267
|
+
const calls: Array<[string, Record<string, unknown>]> = [
|
|
268
|
+
['CreateTaskPushNotificationConfig', { taskId: id, url: 'https://push.example.com/openwop-conformance' }],
|
|
269
|
+
['GetTaskPushNotificationConfig', { taskId: id, id: configId }],
|
|
270
|
+
['ListTaskPushNotificationConfigs', { taskId: id }],
|
|
271
|
+
['DeleteTaskPushNotificationConfig', { taskId: id, id: configId }],
|
|
272
|
+
];
|
|
273
|
+
for (const [method, params] of calls) {
|
|
274
|
+
const r = await rpc(t.url, method, params);
|
|
275
|
+
expect(r.error?.code, req(rid, pushDoc, `${method} on a host not advertising a2a.pushNotifications MUST be refused PushNotificationNotSupportedError -32003 — not method-not-found -32601 (got ${JSON.stringify(r.error ?? r.result)})`)).toBe(-32003);
|
|
276
|
+
}
|
|
277
|
+
await rpc(t.url, 'CancelTask', { id });
|
|
278
|
+
});
|
|
279
|
+
|
|
252
280
|
it('ListTasks returns only what listRuns returns to the same Subject; tenant and contextId never select', async () => {
|
|
253
281
|
const other = process.env.OPENWOP_TEST_TENANT_B_API_KEY;
|
|
254
282
|
if (!other) return softSkip('blocked', 'OPENWOP_TEST_TENANT_B_API_KEY (a credential bound to a second tenant) is not set — the cross-caller leg cannot run');
|
|
@@ -44,6 +44,7 @@ const CATALOG = 'https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json
|
|
|
44
44
|
const clone = <T>(o: T): T => JSON.parse(JSON.stringify(o)) as T;
|
|
45
45
|
const POSITIVE = JSON.parse(readFileSync(join(FIXTURES_DIR, 'a2ui-v09', 'positive-approve-brief.json'), 'utf8')) as Json;
|
|
46
46
|
const V1_SURFACE: Json = { catalogVersion: '0.9.1', surface: { title: 'Kickoff', components: [{ component: 'heading', text: 'Kickoff', level: 2 }, { component: 'action.button', id: 'go', label: 'Go', action: { target: 'resume' } }] } };
|
|
47
|
+
const V1_LATER: Json = { catalogVersion: '0.9.1', surface: { title: 'Kickoff, later', components: [{ component: 'heading', text: 'Kickoff, later', level: 2 }] } };
|
|
47
48
|
|
|
48
49
|
let seq = 0;
|
|
49
50
|
const nonce = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -217,8 +218,16 @@ describe('RFC 0209 §C.11 — recorded surfaces are returned as recorded (seam-g
|
|
|
217
218
|
const sequence = (r.json as { sequence: number }).sequence;
|
|
218
219
|
const events = await eventsOf(runId);
|
|
219
220
|
expect(carries(events, payload), req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'RFC 0209 §C.11', 'the run\'s poll MUST return the recorded surface payload byte-equal — no surface is regenerated')).toBe(true);
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
// The surface is the last event of the suspended run, so `sequence + 1`
|
|
222
|
+
// names no event and runs.md §Fork REQUIRES 422 fork_point_invalid for it.
|
|
223
|
+
// Record one more envelope on the same surface and fork AT that event:
|
|
224
|
+
// the surface (sequence < fromSeq) is fixed history, the later one re-executes.
|
|
225
|
+
const later = await emit(runId, envelope(2, v2(sid('rec'), [dataMsg()])));
|
|
226
|
+
expect(later.status, req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'RFC 0209 §C.9', `an updateDataModel on the live surface MUST be admitted (it supplies the fork point), got ${code(later)}`)).toBe(201);
|
|
227
|
+
const fromSeq = (later.json as { sequence: number }).sequence;
|
|
228
|
+
expect(fromSeq > sequence, req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'api/seams-v2.yaml emitA2uiSurface', `a later recording names a later sequence (surface ${sequence}, update ${fromSeq})`)).toBe(true);
|
|
229
|
+
const fork = await driver.post(`/runs/${encodeURIComponent(runId)}:fork`, { mode: 'replay', fromSeq });
|
|
230
|
+
expect(fork.status, req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'RFC 0209 §C.11', `POST :fork at fromSeq ${fromSeq} (a recorded event after the surface) MUST answer 201, got ${code(fork)}`)).toBe(201);
|
|
222
231
|
const forkId = (fork.json as { runId: string }).runId;
|
|
223
232
|
expect(carries(await eventsOf(forkId), payload), req('openwop.it.v2-a2ui-v09-surface.recorded-as-recorded', 'RFC 0209 §C.11', 'a fork at a fromSeq after the surface MUST carry it unchanged')).toBe(true);
|
|
224
233
|
await done(forkId);
|
|
@@ -234,8 +243,13 @@ describe('RFC 0209 §C.11 — recorded surfaces are returned as recorded (seam-g
|
|
|
234
243
|
expect(r.status, req('openwop.requirement.0209.legacy-readable', 'RFC 0209 §C.11', `a version-1 surface at floor 1 MUST be admitted, got ${code(r)}`)).toBe(201);
|
|
235
244
|
const sequence = (r.json as { sequence: number }).sequence;
|
|
236
245
|
expect(carries(await eventsOf(runId), V1_SURFACE), req('openwop.requirement.0209.legacy-readable', 'RFC 0209 §C.11', 'the poll MUST return the version-1 surface byte-equal')).toBe(true);
|
|
237
|
-
|
|
238
|
-
|
|
246
|
+
// As in the version-2 leg: fork at a recorded event after the surface, never at `sequence + 1` (no such event; 422 fork_point_invalid).
|
|
247
|
+
const later = await emit(runId, envelope(1, V1_LATER));
|
|
248
|
+
expect(later.status, req('openwop.requirement.0209.legacy-readable', 'RFC 0209 §C.11', `a second version-1 surface at floor 1 MUST be admitted (it supplies the fork point), got ${code(later)}`)).toBe(201);
|
|
249
|
+
const fromSeq = (later.json as { sequence: number }).sequence;
|
|
250
|
+
expect(fromSeq > sequence, req('openwop.requirement.0209.legacy-readable', 'api/seams-v2.yaml emitA2uiSurface', `a later recording names a later sequence (surface ${sequence}, next ${fromSeq})`)).toBe(true);
|
|
251
|
+
const fork = await driver.post(`/runs/${encodeURIComponent(runId)}:fork`, { mode: 'replay', fromSeq });
|
|
252
|
+
expect(fork.status, req('openwop.requirement.0209.legacy-readable', 'RFC 0209 §C.11', `POST :fork at fromSeq ${fromSeq} (a recorded event after the surface) MUST answer 201, got ${code(fork)}`)).toBe(201);
|
|
239
253
|
const forkId = (fork.json as { runId: string }).runId;
|
|
240
254
|
expect(carries(await eventsOf(forkId), V1_SURFACE), req('openwop.requirement.0209.legacy-readable', 'RFC 0209 §C.11', 'a fork at a later fromSeq MUST carry the version-1 surface unchanged')).toBe(true);
|
|
241
255
|
await done(forkId);
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
* `Content-Language` names the tag (case-insensitively, RFC 5646 §2.1.1) and
|
|
22
22
|
* the section carries the overlay.
|
|
23
23
|
*
|
|
24
|
+
* Gate: the family is advertised by the PRESENCE of its record
|
|
25
|
+
* (`familyAdvertised`, RFC 0169 §A.2) — never by a `supported` flag, which the
|
|
26
|
+
* closed v2 `content` record cannot carry. `v2-family-gate-no-supported.test.ts`
|
|
27
|
+
* (a suite self-test) keeps that gate from coming back.
|
|
28
|
+
*
|
|
24
29
|
* Sabotage: a host that 400s the extended-tag write, or that serves only the
|
|
25
30
|
* base locale for it, fails the leg (write status / `Content-Language` /
|
|
26
31
|
* overlay assertion respectively).
|
|
@@ -52,7 +57,11 @@ describe('v2-content-locale-keys (RFC 0206)', () => {
|
|
|
52
57
|
const doc = await v2Discovery().catch(() => null);
|
|
53
58
|
if (doc === null) return softSkip('blocked', 'v2 discovery unreachable');
|
|
54
59
|
const content = await familyAdvertised('content');
|
|
55
|
-
|
|
60
|
+
// RFC 0169 §A.2: at major 2 the record's presence IS the claim. The v2
|
|
61
|
+
// `content` record is closed and has no `supported` field, so a gate on
|
|
62
|
+
// `supported === true` recorded `inapplicable` on every conforming host and
|
|
63
|
+
// this row could never execute (corrected 2026-09-24).
|
|
64
|
+
if (content === null) return softSkip('inapplicable', 'the host does not advertise the content family (no content surface to deliver an extended locale from)');
|
|
56
65
|
const base = typeof content['baseLocale'] === 'string' ? content['baseLocale'] : null;
|
|
57
66
|
const supported = Array.isArray(content['supportedLocales']) ? content['supportedLocales'].filter((x): x is string => typeof x === 'string') : [];
|
|
58
67
|
const keyRe = new RegExp(LOCALE_KEY_PATTERN);
|
|
@@ -85,7 +85,13 @@ describe('RFC 0175 §E.1 — mrtr-rounds-ceiling (gated on mcp + mrtr)', () => {
|
|
|
85
85
|
if (!loopKnown) {
|
|
86
86
|
return softSkip('blocked', `the suite MCP fake server cannot loop — its only MRTR tool (needs_input) completes on the first retry; a \`${LOOP_TOOL}\` fixture tool that re-issues input_required ${rounds} times is required to drive maxRounds + 1`);
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
// The failure message carries the host's code, the fake's count and the
|
|
89
|
+
// body (2.38.0). The 2026-09-24 public v2-reference cut on 2.37.1 recorded
|
|
90
|
+
// `expected 400 to be 422` and nothing else — the same leg passes on every
|
|
91
|
+
// loopback run of that host — so whether the 400 was the host refusing a
|
|
92
|
+
// round the fake answered, or a round the tunnel failed to carry, could not
|
|
93
|
+
// be read from the record.
|
|
94
|
+
expect(res.status, req('openwop.requirement.0175.mrtr-rounds-ceiling.refused', 'interop.md §The MCP round ceiling', `round ${rounds} (maxRounds + 1) MUST be refused with 422 (got ${res.status} ${String(readErrorCode(res.json))}; the fake served ${served.length} tools/call; body ${res.text.slice(0, 400)})`)).toBe(422);
|
|
89
95
|
expect(readErrorCode(res.json), req('openwop.requirement.0175.mrtr-rounds-ceiling.refused', 'errors.json mcp_mrtr_rounds_exceeded', `the refusal MUST carry ${CODE}`)).toBe(CODE);
|
|
90
96
|
expect(
|
|
91
97
|
served.length,
|
|
@@ -118,7 +118,13 @@ describe('RFC 0199 §A — v2-oauth-client-pkce-state-iss (host as OAuth client,
|
|
|
118
118
|
n = d.codeExchanges().length;
|
|
119
119
|
const replay = await userAgentGet(cbA!, key);
|
|
120
120
|
expect([replay.status >= 400, d.codeExchanges().length], req(id, `${DOC} rule 2`, `a replayed (already consumed) state MUST be refused with no token request (got ${replay.status})`)).toEqual([true, n]);
|
|
121
|
-
|
|
121
|
+
// 120 s, not the 30 s default (2.38.0): this leg makes eight sequential
|
|
122
|
+
// round trips — two grants, two consents, four callbacks — and on a public
|
|
123
|
+
// cut every one crosses the operator's tunnel to the AS double. The
|
|
124
|
+
// 2026-09-24 public v2-reference cut on 2.37.1 timed out here at 30 s while
|
|
125
|
+
// the sibling legs (pkce-s256, iss-validated, same-user-callback), which
|
|
126
|
+
// make fewer trips, passed. No assertion or window inside the leg changes.
|
|
127
|
+
}, 120_000);
|
|
122
128
|
|
|
123
129
|
it('a callback authenticated as another Subject makes no token request and stores nothing', async () => {
|
|
124
130
|
const r = await ready();
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": [
|
|
3
|
+
"RFC 0212 — cross-language vectors for canonical JSON (RFC 8785 JCS) with the OpenWOP I-JSON refusal set.",
|
|
4
|
+
"Every signed or hashed preimage in the corpus MUST reproduce `canonical` and `sha256` for each `objects` entry, `canonical` for each `numbers` entry, and MUST refuse each `refusals` entry with a refusal (the `refuse` kind is informative).",
|
|
5
|
+
"Generated by a reference implementation that parses JSON TEXT (so duplicates, integer magnitude and lone surrogates are visible before coercion) and cross-checked against RFC 8785 Appendix B and §3.2.2/§3.2.3."
|
|
6
|
+
],
|
|
7
|
+
"spec": "RFC 8785; RFC 7493 (I-JSON); RFC 0212",
|
|
8
|
+
"hash": "sha256, lowercase hex, over the UTF-8 bytes of `canonical`",
|
|
9
|
+
"numbers": [
|
|
10
|
+
{
|
|
11
|
+
"ieee754": "0000000000000000",
|
|
12
|
+
"canonical": "0"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"ieee754": "8000000000000000",
|
|
16
|
+
"canonical": "0"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"ieee754": "0000000000000001",
|
|
20
|
+
"canonical": "5e-324"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"ieee754": "8000000000000001",
|
|
24
|
+
"canonical": "-5e-324"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"ieee754": "7fefffffffffffff",
|
|
28
|
+
"canonical": "1.7976931348623157e+308"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"ieee754": "ffefffffffffffff",
|
|
32
|
+
"canonical": "-1.7976931348623157e+308"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"ieee754": "4340000000000000",
|
|
36
|
+
"canonical": "9007199254740992"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"ieee754": "c340000000000000",
|
|
40
|
+
"canonical": "-9007199254740992"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"ieee754": "4430000000000000",
|
|
44
|
+
"canonical": "295147905179352830000"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"ieee754": "44b52d02c7e14af5",
|
|
48
|
+
"canonical": "9.999999999999997e+22"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"ieee754": "44b52d02c7e14af6",
|
|
52
|
+
"canonical": "1e+23"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"ieee754": "44b52d02c7e14af7",
|
|
56
|
+
"canonical": "1.0000000000000001e+23"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"ieee754": "444b1ae4d6e2ef4e",
|
|
60
|
+
"canonical": "999999999999999700000"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"ieee754": "444b1ae4d6e2ef4f",
|
|
64
|
+
"canonical": "999999999999999900000"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"ieee754": "444b1ae4d6e2ef50",
|
|
68
|
+
"canonical": "1e+21"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"ieee754": "3eb0c6f7a0b5ed8c",
|
|
72
|
+
"canonical": "9.999999999999997e-7"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"ieee754": "3eb0c6f7a0b5ed8d",
|
|
76
|
+
"canonical": "0.000001"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"ieee754": "41b3de4355555553",
|
|
80
|
+
"canonical": "333333333.3333332"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"ieee754": "41b3de4355555554",
|
|
84
|
+
"canonical": "333333333.33333325"
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"ieee754": "41b3de4355555555",
|
|
88
|
+
"canonical": "333333333.3333333"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"ieee754": "41b3de4355555556",
|
|
92
|
+
"canonical": "333333333.3333334"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"ieee754": "41b3de4355555557",
|
|
96
|
+
"canonical": "333333333.33333343"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"ieee754": "becbf647612f3696",
|
|
100
|
+
"canonical": "-0.0000033333333333333333"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"ieee754": "43143ff3c1cb0959",
|
|
104
|
+
"canonical": "1424953923781206.2"
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
"numberRefusals": [
|
|
108
|
+
{
|
|
109
|
+
"ieee754": "7fffffffffffffff",
|
|
110
|
+
"refuse": "non-finite"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"ieee754": "7ff0000000000000",
|
|
114
|
+
"refuse": "non-finite"
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"objects": [
|
|
118
|
+
{
|
|
119
|
+
"id": "rfc8785-3.2.2",
|
|
120
|
+
"why": "RFC 8785 §3.2.2 worked example: number shortest-round-trip, string escapes, member sort.",
|
|
121
|
+
"input": "{\"numbers\": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001], \"string\": \"\\u20ac$\\u000F\\u000aA'\\u0042\\u0022\\u005c\\\\\\\"\\/\", \"literals\": [null, true, false]}",
|
|
122
|
+
"canonical": "{\"literals\":[null,true,false],\"numbers\":[333333333.3333333,1e+30,4.5,0.002,1e-27],\"string\":\"€$\\u000f\\nA'B\\\"\\\\\\\\\\\"/\"}",
|
|
123
|
+
"sha256": "2d5e01a318d0f0879ab568c4be289c8b1f64ef8921a53c6277d5e069978baacb"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "rfc8785-3.2.3-sort",
|
|
127
|
+
"why": "RFC 8785 §3.2.3: members sorted by UTF-16 code units — CR, \"1\", U+0080, ö, €, 😀 (surrogate pair D83D), דּ (FB33). Locale collation gives a different order.",
|
|
128
|
+
"input": "{\"€\":\"Euro Sign\",\"\\r\":\"Carriage Return\",\"דּ\":\"Hebrew Letter Dalet With Dagesh\",\"1\":\"One\",\"😀\":\"Emoji: Grinning Face\",\"\\u0080\":\"Control\",\"ö\":\"Latin Small Letter O With Diaeresis\"}",
|
|
129
|
+
"canonical": "{\"\\r\":\"Carriage Return\",\"1\":\"One\",\"\":\"Control\",\"ö\":\"Latin Small Letter O With Diaeresis\",\"€\":\"Euro Sign\",\"😀\":\"Emoji: Grinning Face\",\"דּ\":\"Hebrew Letter Dalet With Dagesh\"}",
|
|
130
|
+
"sha256": "5e321556d22018a9656991a9e94f77ec175fa193e52a2429d312f8419ec8b08c"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "integer-like-keys",
|
|
134
|
+
"why": "Keys \"10\" and \"9\" sort by code unit (\"1\" < \"9\"). A canonicalizer that rebuilds a JS object and calls JSON.stringify emits {\"9\":…,\"10\":…} — the sabotage this vector exists for.",
|
|
135
|
+
"input": "{\"9\":\"nine\",\"10\":\"ten\",\"a\":0}",
|
|
136
|
+
"canonical": "{\"10\":\"ten\",\"9\":\"nine\",\"a\":0}",
|
|
137
|
+
"sha256": "ac5e0767681af2d226a2a4df567cee9cd3254a7819a7643f8bddbfa690792dad"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"id": "nested-sort",
|
|
141
|
+
"why": "Sorting applies at every level; arrays keep their order.",
|
|
142
|
+
"input": "{\"b\":[{\"z\":1,\"a\":2},{\"y\":[3,1,2]}],\"a\":{\"d\":null,\"c\":true}}",
|
|
143
|
+
"canonical": "{\"a\":{\"c\":true,\"d\":null},\"b\":[{\"a\":2,\"z\":1},{\"y\":[3,1,2]}]}",
|
|
144
|
+
"sha256": "fc8d1753fb837b89a49b6335715adb82c262e9a6bbfe7f4e7b8993e297951cca"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"id": "control-escapes",
|
|
148
|
+
"why": "U+0000–U+001F: short forms for \\b \\t \\n \\f \\r, lowercase \\u00xx otherwise; U+007F and U+2028 are emitted raw; \"/\" is not escaped.",
|
|
149
|
+
"input": "{\"s\":\"\\u0000\\u0007\\b\\t\\n\\u000b\\f\\r\\u001f\\u007f
/\"}",
|
|
150
|
+
"canonical": "{\"s\":\"\\u0000\\u0007\\b\\t\\n\\u000b\\f\\r\\u001f
/\"}",
|
|
151
|
+
"sha256": "993e6efcefb9f0bc08a2f896edce693cd6e80aa50ed6337ae0921b06a656c1a3"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"id": "non-ascii-raw",
|
|
155
|
+
"why": "Non-ASCII is emitted as raw UTF-8, never \\u-escaped (em dash, as in 625 registry pack strings).",
|
|
156
|
+
"input": "{\"title\":\"a — b\",\"emoji\":\"😀\"}",
|
|
157
|
+
"canonical": "{\"emoji\":\"😀\",\"title\":\"a — b\"}",
|
|
158
|
+
"sha256": "d2a26872e24a775a770515e000f1bb85a0949c08b40621292b7e70037006d02d"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"id": "nfc-composed",
|
|
162
|
+
"why": "U+00E9 (composed). MUST NOT hash equal to the decomposed vector: JCS applies no Unicode normalization.",
|
|
163
|
+
"input": "{\"name\":\"café\"}",
|
|
164
|
+
"canonical": "{\"name\":\"café\"}",
|
|
165
|
+
"sha256": "645fa443126a8954fc6d871912b8fc67bc2ee8feae417efe55546251962ca74d"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"id": "nfd-decomposed",
|
|
169
|
+
"why": "U+0065 U+0301 (decomposed). Pair with nfc-composed.",
|
|
170
|
+
"input": "{\"name\":\"café\"}",
|
|
171
|
+
"canonical": "{\"name\":\"café\"}",
|
|
172
|
+
"sha256": "4b7186ff40a64bfae3cb4bd75ac13b0de5ee693190f20812aaff208a77560ca7"
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"id": "numbers-in-document",
|
|
176
|
+
"why": "Numbers inside a document: -0 → 0, 1.0 → 1, 1e21 → 1e+21, 1e-7 → 1e-7, 0.1+0.2 shortest form, 2^53−1 accepted.",
|
|
177
|
+
"input": "{\"n\":[-0,1.0,1e21,1e-7,0.30000000000000004,9007199254740991,-9007199254740991]}",
|
|
178
|
+
"canonical": "{\"n\":[0,1,1e+21,1e-7,0.30000000000000004,9007199254740991,-9007199254740991]}",
|
|
179
|
+
"sha256": "430e3fefe48227fd5ed7df3ba8b5a6da008c2077858a44bd265d4c7812c498c1"
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"id": "empty-containers",
|
|
183
|
+
"why": "Empty object and array.",
|
|
184
|
+
"input": "{\"a\":{},\"b\":[]}",
|
|
185
|
+
"canonical": "{\"a\":{},\"b\":[]}",
|
|
186
|
+
"sha256": "aeeba1e56a144077f89e26c930dad7cf933e9383cfb8e33ddff618233b29be76"
|
|
187
|
+
}
|
|
188
|
+
],
|
|
189
|
+
"refusals": [
|
|
190
|
+
{
|
|
191
|
+
"id": "duplicate-name",
|
|
192
|
+
"why": "I-JSON §2.3: duplicate member names.",
|
|
193
|
+
"input": "{\"a\":1,\"a\":2}",
|
|
194
|
+
"refuse": "duplicate-name"
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"id": "lone-high-surrogate",
|
|
198
|
+
"why": "I-JSON §2.1: a lone surrogate is not a Unicode scalar value.",
|
|
199
|
+
"input": "{\"s\":\"\\ud800\"}",
|
|
200
|
+
"refuse": "lone-surrogate"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"id": "lone-low-surrogate",
|
|
204
|
+
"why": "I-JSON §2.1.",
|
|
205
|
+
"input": "{\"s\":\"x\\udc00\"}",
|
|
206
|
+
"refuse": "lone-surrogate"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"id": "integer-above-range",
|
|
210
|
+
"why": "Integer literal 2^53+1 exceeds ±(2^53−1) (I-JSON §2.2); a double cannot hold it exactly and a Python verifier would keep it exact — the two canonical forms differ.",
|
|
211
|
+
"input": "{\"n\":9007199254740993}",
|
|
212
|
+
"refuse": "integer-out-of-range"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"id": "integer-below-range",
|
|
216
|
+
"why": "Integer literal −2^53.",
|
|
217
|
+
"input": "{\"n\":-9007199254740992}",
|
|
218
|
+
"refuse": "integer-out-of-range"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"id": "nan-token",
|
|
222
|
+
"why": "NaN is not JSON. A JS canonicalizer given an in-memory NaN MUST refuse, not emit null.",
|
|
223
|
+
"input": "{\"n\":NaN}",
|
|
224
|
+
"refuse": "not-json"
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"id": "infinity-overflow",
|
|
228
|
+
"why": "1e400 parses to Infinity: non-finite.",
|
|
229
|
+
"input": "{\"n\":1e400}",
|
|
230
|
+
"refuse": "non-finite"
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
"ordering": {
|
|
234
|
+
"why": "witnessSha256 rows[] and RFC 0150 tools[] sort by UTF-16 code units of the key. Locale collation disagrees in both listed locales; a verifier using either computes a different digest.",
|
|
235
|
+
"ids": [
|
|
236
|
+
"d",
|
|
237
|
+
"h",
|
|
238
|
+
"ch",
|
|
239
|
+
"i",
|
|
240
|
+
"chain-pin-exact",
|
|
241
|
+
"h2",
|
|
242
|
+
"get_weather",
|
|
243
|
+
"getWeather",
|
|
244
|
+
"A",
|
|
245
|
+
"a",
|
|
246
|
+
"a-b",
|
|
247
|
+
"ab"
|
|
248
|
+
],
|
|
249
|
+
"codeUnit": [
|
|
250
|
+
"A",
|
|
251
|
+
"a",
|
|
252
|
+
"a-b",
|
|
253
|
+
"ab",
|
|
254
|
+
"ch",
|
|
255
|
+
"chain-pin-exact",
|
|
256
|
+
"d",
|
|
257
|
+
"getWeather",
|
|
258
|
+
"get_weather",
|
|
259
|
+
"h",
|
|
260
|
+
"h2",
|
|
261
|
+
"i"
|
|
262
|
+
],
|
|
263
|
+
"localeCs": [
|
|
264
|
+
"a",
|
|
265
|
+
"A",
|
|
266
|
+
"a-b",
|
|
267
|
+
"ab",
|
|
268
|
+
"d",
|
|
269
|
+
"get_weather",
|
|
270
|
+
"getWeather",
|
|
271
|
+
"h",
|
|
272
|
+
"h2",
|
|
273
|
+
"ch",
|
|
274
|
+
"chain-pin-exact",
|
|
275
|
+
"i"
|
|
276
|
+
],
|
|
277
|
+
"localeEn": [
|
|
278
|
+
"a",
|
|
279
|
+
"A",
|
|
280
|
+
"a-b",
|
|
281
|
+
"ab",
|
|
282
|
+
"ch",
|
|
283
|
+
"chain-pin-exact",
|
|
284
|
+
"d",
|
|
285
|
+
"get_weather",
|
|
286
|
+
"getWeather",
|
|
287
|
+
"h",
|
|
288
|
+
"h2",
|
|
289
|
+
"i"
|
|
290
|
+
],
|
|
291
|
+
"differsFromCs": true,
|
|
292
|
+
"differsFromEn": true
|
|
293
|
+
}
|
|
294
|
+
}
|