@aicent/rttp 0.1.2 → 1.2.7

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 CHANGED
@@ -1,36 +1,40 @@
1
1
  # @aicent/rttp
2
2
 
3
- **RTTP for JavaScript: `rttp://` intent addressing, `ROUTE_SHARD` derivation —
3
+ **RTTP for JavaScript: `rttp://` intent addressing, `ROUTE_SHARD` derivation --
4
4
  and the published conformance vectors that prove an implementation is right.**
5
5
 
6
- [RFC-002 §10](https://rttp.com/RFC-002/) · v1.2.6 · zero dependencies · no build step
6
+ [RFC-002 sec. 10](https://rttp.com/RFC-002/) - spec v1.2.6 - zero dependencies - no build step
7
7
 
8
8
  ---
9
9
 
10
- ## Verify it in one command — no trust required
10
+ ## Verify it in one command -- no trust required
11
11
 
12
12
  ```console
13
13
  $ npx @aicent/rttp
14
- [PASS] all 25 checks passed (node, independent implementation)
14
+ [PASS] all 35 checks passed (node, independent implementation)
15
15
  ```
16
16
 
17
17
  That is the point of this package. A specification is worth exactly what an
18
18
  independent implementation can reproduce from it, so instead of asking you to
19
19
  believe a table of numbers, the vectors ship inside the package and replay
20
- locally — offline, no account, no network. `[PASS]` or it is not.
20
+ locally -- offline, no account, no network. `[PASS]` or it is not. Every vector file shipped here is listed in `vectors.manifest.json` with its byte count and SHA-256, produced by a generator rather than typed by hand - recompute the hash yourself if you prefer.
21
21
 
22
22
  The JavaScript and Python sides **share no code**. They are separate
23
23
  implementations of the same published vectors, and they agree byte for byte.
24
24
 
25
- ### The other half — Python, on PyPI
25
+ ### The other half -- Python, on PyPI
26
26
 
27
27
  ```console
28
28
  $ pip install rttp
29
29
  $ python -m rttp.selftest
30
- [PASS] all 53 checks passed
30
+ [PASS] all 80 checks passed (3 skipped) # default install
31
+
32
+ $ pip install rttp[ed25519]
33
+ $ python -m rttp.selftest
34
+ [PASS] all 83 checks passed # with the optional Ed25519 backend
31
35
  ```
32
36
 
33
- Published as **`rttp`** — the bare name, because PyPI has no scopes and the name
37
+ Published as **`rttp`** -- the bare name, because PyPI has no scopes and the name
34
38
  was free. That package ships a **byte-identical copy** of this same vector set and
35
39
  replays it offline, exactly as this one does: two registries, two languages, one
36
40
  specification.
@@ -71,38 +75,38 @@ $ node examples/quickstart.mjs
71
75
  459e543b73d86005b72ba77d5756e83c
72
76
  ```
73
77
 
74
- Its expected output is the shard printed above — the value the published
78
+ Its expected output is the shard printed above -- the value the published
75
79
  conformance vectors pin for that address, so the example can be **checked**
76
80
  rather than believed. It is also the value the Python implementation derives,
77
81
  from code that shares nothing with this one.
78
82
 
79
- Addressing is **DNS-free** (RFC-002 §10.5): the routing hash is derived from the
83
+ Addressing is **DNS-free** (RFC-002 sec. 10.5): the routing hash is derived from the
80
84
  authority by SHA-256, so no registry, resolver or network is involved. A URI
81
- either yields a shard or throws — there is no fallback, and no `rttps`.
85
+ either yields a shard or throws -- there is no fallback, and no `rttps`.
82
86
 
83
87
  ---
84
88
 
85
89
  ## The one rule that surprises people
86
90
 
87
- **Malformed input is rejected, not normalised.** `rttp://…/VESSEL` and
88
- `rttp://F3B2A1C4.…/vessel` both throw, because the canonical form is lowercase
89
- US-ASCII (RFC-002 §10.3) and a case variant is not a spelling difference — it is
91
+ **Malformed input is rejected, not normalised.** `rttp://.../VESSEL` and
92
+ `rttp://F3B2A1C4..../vessel` both throw, because the canonical form is lowercase
93
+ US-ASCII (RFC-002 sec. 10.3) and a case variant is not a spelling difference -- it is
90
94
  a *different string*. Admitting both would give one address two spellings, which
91
- is the ambiguity §10.3 exists to remove.
95
+ is the ambiguity sec. 10.3 exists to remove.
92
96
 
93
97
  Nothing here is ever accepted because a check could not be performed: this
94
98
  package fails closed everywhere.
95
99
 
96
100
  ---
97
101
 
98
- ## The browser's registration prefix — accepted, never advertised
102
+ ## The browser's registration prefix -- accepted, never advertised
99
103
 
100
104
  A browser lets a page register a protocol handler only for a scheme it already
101
105
  knows, or for one carrying the browser's own **reserved registration prefix**. So
102
106
  the string a web page hands to a handler is not the bare address.
103
107
 
104
108
  This package **accepts** that form, because refusing a string a browser is about
105
- to deliver would break the handler path — but it is a **tolerance, and the
109
+ to deliver would break the handler path -- but it is a **tolerance, and the
106
110
  specification defines no such form**. The canonical `rttp://` address is what
107
111
  this package reports, and the only form it ever emits: same authority, same path,
108
112
  same **route shard** either way.
@@ -116,7 +120,7 @@ The `scheme` field tells you which form arrived; everything else is identical.
116
120
 
117
121
  ---
118
122
 
119
- ## API — main entry
123
+ ## API -- main entry
120
124
 
121
125
  | Export | What it does |
122
126
  |:---|:---|
@@ -124,14 +128,14 @@ The `scheme` field tells you which form arrived; everything else is identical.
124
128
  | `parseAndDerive(uri)` | Identical to `parse`; the specification's own wording. |
125
129
  | `deriveRouteShard(authority)` | `Uint8Array(16)` = `SHA-256(ASCII(authority))[0:16]`. |
126
130
  | `deriveRouteShardHex(authority)` | The same, as 32 lowercase hex characters. |
127
- | `isValidAction(value)` | §10.2 `action` form test. The verb set is **open**. |
131
+ | `isValidAction(value)` | sec. 10.2 `action` form test. The verb set is **open**. |
128
132
  | `RttpUriError` | The only error type thrown. |
129
133
  | `SCHEME`, `BROWSER_HANDLER_SCHEME`, `ROUTE_SHARD_BYTES`, `HASH_INTENT_LEN`, `ACTION_MAX_LEN`, `SPEC_RELEASE` | Constants. |
130
134
 
131
135
  ### Returned fields
132
136
 
133
137
  Field names are `snake_case` **on purpose**: the published conformance vectors use
134
- those names, so this output and the Python package's output compare directly —
138
+ those names, so this output and the Python package's output compare directly --
135
139
  field for field, byte for byte.
136
140
 
137
141
  | Field | Meaning |
@@ -140,7 +144,7 @@ field for field, byte for byte.
140
144
  | `intent` | 8 lowercase hex digits (32-bit routing hash) or a readable organ token |
141
145
  | `intent_is_hash` / `intent_hash32` | Whether it is a hash-intent, and its value |
142
146
  | `pillar`, `root` | The other two authority labels |
143
- | `action` | Empty string when omitted (RFC-002 §10.4 default operation) |
147
+ | `action` | Empty string when omitted (RFC-002 sec. 10.4 default operation) |
144
148
  | `action_omitted` | The same fact as a boolean |
145
149
  | `authority` | `<intent>.<pillar>.<root>`, canonical |
146
150
  | `canonical_uri` | Always begins `rttp://` |
@@ -148,7 +152,7 @@ field for field, byte for byte.
148
152
 
149
153
  ---
150
154
 
151
- ## The conformance vectors — `@aicent/rttp/conformance`
155
+ ## The conformance vectors -- `@aicent/rttp/conformance`
152
156
 
153
157
  ```js
154
158
  import { loadVectors, runConformance } from '@aicent/rttp/conformance';
@@ -172,32 +176,34 @@ In CI:
172
176
 
173
177
  | Export | What it does |
174
178
  |:---|:---|
175
- | `runConformance(vectors)` | Replay a vector set → `{ checked, failures, passed }` |
179
+ | `runConformance(vectors)` | Replay a vector set -> `{ checked, failures, passed }` |
176
180
  | `loadVectors(path?)` | Parse a vector set; defaults to the shipped one |
177
181
  | `DEFAULT_VECTORS_PATH` | Absolute path of the shipped `vectors.json` |
178
- | `parseUri(uri)` | The independent RFC-002 §10.2 implementation → `{ authority, action, shardHex }` |
179
- | `buildFrame(fields)` | The independent 128-byte frame builder → `Buffer` |
180
- | `ACTION_MAX_LEN` | `16` — the ACTION field's capacity |
182
+ | `parseUri(uri)` | The independent RFC-002 sec. 10.2 implementation -> `{ authority, action, shardHex }` |
183
+ | `buildFrame(fields)` | The independent 128-byte frame builder -> `Buffer` |
184
+ | `ACTION_MAX_LEN` | `16` -- the ACTION field's capacity |
181
185
 
182
186
  `vectors.json` is plain JSON and ships inside the package, also reachable as the
183
187
  subpath export `@aicent/rttp/vectors.json`. Any language can read it; that is
184
188
  what it is for.
185
189
 
186
- ### What the 25 checks are
190
+ ### What the 35 checks are
187
191
 
188
192
  | Group | Count | What it proves |
189
193
  |:---|:---|---:|
190
- | `positive_uris` | 6 | Accept, canonicalise and derive — including a hash-intent and an omitted action. **Five are conformance claims; one is a `tolerance`** — the browser-registration prefix — and the vector set labels it as such |
191
- | `negative_uris` | 15 | **Reject** — uppercase, userinfo, query, fragment, port, multi-segment path, wrong authority shape, trailing slash, foreign scheme, surrounding whitespace |
192
- | `frame_vectors` | 3 | URI → `ROUTE_SHARD` → the 128-byte `PulseHeader128`, bit for bit |
194
+ | `positive_uris` | 6 | Accept, canonicalise and derive -- including a hash-intent and an omitted action. **Five are conformance claims; one is a `tolerance`** -- the browser-registration prefix -- and the vector set labels it as such |
195
+ | `negative_uris` | 15 | **Reject** -- uppercase, userinfo, query, fragment, port, multi-segment path, wrong authority shape, trailing slash, foreign scheme, surrounding whitespace |
196
+ | `frame_vectors` | 3 | URI -> `ROUTE_SHARD` -> the 128-byte `PulseHeader128`, bit for bit |
193
197
  | `compat_vector_spec_rev_0` | 1 | A frame written before the v1.2.6 extension block still verifies |
198
+ | `negative_frames` | 9 | **Reject** -- fail-closed frames: ACTION length/padding/embedded NUL/grammar, unknown SPEC_REV, SPEC_REV=0 with a non-zero extension, non-zero RESERVED, unknown FLAGS bit |
199
+ | `positive_frames` | 1 | Accept -- SPEC_REV=1 with ACTION_LEN=0 is valid and equivalent to an omitted action |
194
200
 
195
201
  Each vector is deterministic: fixed sequence numbers, fixed timestamp, fixed AID.
196
202
  Two implementations either agree byte for byte or they do not.
197
203
 
198
204
  ### Why the checker lives in this package, and why that is still honest
199
205
 
200
- `src/conformance.mjs` is written from RFC-002 §10 alone and **must never import
206
+ `src/conformance.mjs` is written from RFC-002 sec. 10 alone and **must never import
201
207
  `src/rttp-uri.mjs`**. A conformance suite that reuses the code under test only
202
208
  proves the code equals itself. The two files share no code, so the rule is
203
209
  checkable by reading them.
@@ -209,8 +215,8 @@ not a verification step.
209
215
 
210
216
  `buildFrame` **refuses** an action longer than the field can hold rather than
211
217
  truncating it: a silently shortened verb would be a routing decision made by the
212
- codec, and the codec does not get to make that decision. (§10.2 sets no length
213
- limit on `action` — a *parser* must accept a long one; a frame builder cannot.)
218
+ codec, and the codec does not get to make that decision. (sec. 10.2 sets no length
219
+ limit on `action` -- a *parser* must accept a long one; a frame builder cannot.)
214
220
 
215
221
  The vectors are **generated, not written**. `generated_by` in the JSON names the
216
222
  generator, and the file says so itself: change the cases in the generator, not the
@@ -219,19 +225,19 @@ agreeing with its own bugs.
219
225
 
220
226
  ---
221
227
 
222
- ## Scope — what this package is not
228
+ ## Scope -- what this package is not
223
229
 
224
230
  | | |
225
231
  |:---|:---|
226
- | ✅ **Validation** | Real, and specified (RFC-002 §10.2 / §10.3). |
227
- | ✅ **Canonicalisation** | Real, and specified. |
228
- | ✅ **ROUTE_SHARD derivation** | Real, and specified. |
229
- | ✅ **Conformance vectors + independent replay** | Published and generated. |
230
- | ❌ **Framing, as a library API** | `buildFrame` is exported from `./conformance`, where it exists to replay frame vectors — it is not offered as a general codec. |
231
- | ❌ **Transport** | Not here. There is no `send()`. |
232
- | ❌ **Routing service** | Not here. The shard is computed locally; delivering a frame to that hash is an operator's job. |
233
- | ❌ **Sealing (§11)** | Separate artifacts. |
234
- | ❌ **Browsers** | Node only — it uses `node:crypto`. The derivation is trivial to reproduce elsewhere, and the vectors let you prove you did. |
232
+ | OK **Validation** | Real, and specified (RFC-002 sec. 10.2 / sec. 10.3). |
233
+ | OK **Canonicalisation** | Real, and specified. |
234
+ | OK **ROUTE_SHARD derivation** | Real, and specified. |
235
+ | OK **Conformance vectors + independent replay** | Published and generated. |
236
+ | NO **Framing, as a library API** | `buildFrame` is exported from `./conformance`, where it exists to replay frame vectors -- it is not offered as a general codec. |
237
+ | NO **Transport** | Not here. There is no `send()`. |
238
+ | NO **Routing service** | Not here. The shard is computed locally; delivering a frame to that hash is an operator's job. |
239
+ | NO **Sealing (sec. 11)** | Separate artifacts. |
240
+ | NO **Browsers** | Node only -- it uses `node:crypto`. The derivation is trivial to reproduce elsewhere, and the vectors let you prove you did. |
235
241
 
236
242
  ---
237
243
 
@@ -240,18 +246,24 @@ agreeing with its own bugs.
240
246
  | Ecosystem | Name | Status |
241
247
  |:---|:---|:---|
242
248
  | crates.io | `rttp` | held by this project |
243
- | PyPI | `rttp` | **published** — the Python reference implementation; `pip install rttp` |
244
- | npm | **`@aicent/rttp`** | the bare `rttp` name on npm is held by an unrelated, unmaintained 2017 REST helper, and the `rttp` scope cannot be created at all — a personal account already holds that name. This package is therefore published under the organization this project controls; the package name itself is still just `rttp`. |
249
+ | PyPI | `rttp` | **published** -- the Python reference implementation; `pip install rttp` |
250
+ | npm | **`@aicent/rttp`** | the bare `rttp` name on npm is held by an unrelated, unmaintained 2017 REST helper, and the `rttp` scope cannot be created at all -- a personal account already holds that name. This package is therefore published under the organization this project controls; the package name itself is still just `rttp`. |
245
251
 
246
252
  ---
247
253
 
248
254
  ## Specification status
249
255
 
250
- * **`rttp` URI scheme** — submitted to IANA under RFC 7595, ticket **#1459939**,
251
- Provisional, **under review**. It is **not yet registered** — as of 2026-09-17
256
+ * **`rttp` URI scheme** -- submitted to IANA under RFC 7595, ticket **#1459939**,
257
+ Provisional, **under review**. It is **not yet registered** -- as of 2026-09-20
252
258
  the IANA "URI Schemes" registry contains no `rttp` entry. Please describe it
253
259
  that way.
254
- * **Frame layout** — RFC-002 §4.1, extended by `SPEC/RTTP-FRAME-EXT-v1.2.6.md`.
260
+ * **Internet-Draft (IETF)** -- the protocol is under IETF review as the
261
+ Individual Submission Internet-Draft `draft-li-rttp-intent-addressing`
262
+ (revision -01, posted 2026-09-20, informational):
263
+ https://datatracker.ietf.org/doc/draft-li-rttp-intent-addressing/ .
264
+ An Internet-Draft is a working document -- it is not an IETF standard and
265
+ carries no IETF endorsement.
266
+ * **Frame layout** -- RFC-002 sec. 4.1, extended by `SPEC/RTTP-FRAME-EXT-v1.2.6.md`.
255
267
 
256
268
  Where this package and the specification disagree, **the specification wins and
257
269
  this package is wrong.** Please report it.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aicent/rttp",
3
- "version": "0.1.2",
4
- "description": "RTTP for JavaScript: rttp:// intent validation, canonicalisation and ROUTE_SHARD derivation (RFC-002 §10), plus the published conformance vectors and an independent replay of them, including the 128-byte frame. Zero dependencies, no build step.",
3
+ "version": "1.2.7",
4
+ "description": "RTTP for JavaScript: rttp:// intent validation, canonicalisation and ROUTE_SHARD derivation (RFC-002 sec. 10), plus the published conformance vectors and an independent replay of them, including the 128-byte frame. Zero dependencies, no build step.",
5
5
  "keywords": [
6
6
  "rttp",
7
7
  "uri",
@@ -95,7 +95,7 @@ export declare const DEFAULT_VECTORS_PATH: string;
95
95
  /** The ACTION field is 16 bytes. */
96
96
  export declare const ACTION_MAX_LEN: 16;
97
97
 
98
- /** Independent implementation of RFC-002 §10.2. Throws `Error`. */
98
+ /** Independent implementation of RFC-002 sec. 10.2. Throws `Error`. */
99
99
  export declare function parseUri(uri: string): ParsedUri;
100
100
 
101
101
  /** Independent implementation of the 128-byte frame. Throws `Error`. */
@@ -13,7 +13,7 @@
13
13
  * imports nothing from `src/rttp-uri.mjs` and must not: a conformance suite that
14
14
  * reuses the code it is checking only proves that the code equals itself.
15
15
  *
16
- * Authority boundary: URI syntax = RFC-002 §10.2; frame layout = §4.1 as
16
+ * Authority boundary: URI syntax = RFC-002 sec. 10.2; frame layout = sec. 4.1 as
17
17
  * extended by `SPEC/RTTP-FRAME-EXT-v1.2.6.md`.
18
18
  *
19
19
  * Zero dependencies: `node:crypto` and `node:fs`, nothing else.
@@ -31,7 +31,7 @@ import { fileURLToPath } from 'node:url';
31
31
  export const DEFAULT_VECTORS_PATH = fileURLToPath(new URL('./vectors.json', import.meta.url));
32
32
 
33
33
  // ---------------------------------------------------------------------------
34
- // Frame constants (§4.1)
34
+ // Frame constants (sec. 4.1)
35
35
  // ---------------------------------------------------------------------------
36
36
 
37
37
  const MAGIC = 0x52545450;
@@ -42,7 +42,14 @@ const SIZE = 128;
42
42
  const OFF_ACTION_LEN = 0x68;
43
43
  const OFF_ACTION = 0x69;
44
44
 
45
- /** The ACTION field is 16 bytes; ACTION_LEN is one byte. See RFC-002 §10.2. */
45
+ /** Offsets fixed by RTTP-FRAME-EXT-v1.2.6 sec. 2 (extension block at 0x66..0x7F). */
46
+ const OFF_SPEC_REV = 0x66;
47
+ const OFF_FLAGS = 0x67;
48
+ const OFF_RESERVED = 0x79;
49
+ const KNOWN_SPEC_REVS = new Set([0, 1]);
50
+ const FLAG_URI_ANCHORED = 0x01;
51
+
52
+ /** The ACTION field is 16 bytes; ACTION_LEN is one byte. See RFC-002 sec. 10.2. */
46
53
  export const ACTION_MAX_LEN = 16;
47
54
 
48
55
  const TOKEN = /^[a-z0-9-]+$/;
@@ -53,7 +60,7 @@ const HASH_INTENT = /^[0-9a-f]{8}$/;
53
60
  // ---------------------------------------------------------------------------
54
61
 
55
62
  /**
56
- * Parse one `rttp` URI - an implementation written from RFC-002 §10.2 alone,
63
+ * Parse one `rttp` URI - an implementation written from RFC-002 sec. 10.2 alone,
57
64
  * sharing no code with `src/rttp-uri.mjs`.
58
65
  *
59
66
  * @param {string} uri
@@ -159,6 +166,79 @@ export function buildFrame({ sequenceId, ttl, priority, shardHex, aidHex, timest
159
166
  return buf; // 0x79..0x80 RESERVED = 0
160
167
  }
161
168
 
169
+ /**
170
+ * Verify a `PulseHeader128`, mirroring the Python `pulse_header.verify`.
171
+ *
172
+ * Implements the extension-block rules of RTTP-FRAME-EXT-v1.2.6 sec. 2 / sec. 4
173
+ * (R2-R7) and the ACTION grammar of RFC-002 sec. 10.2. Every failure throws: a
174
+ * check that cannot be performed is a failure, never a pass.
175
+ *
176
+ * @param {Buffer} raw 128 bytes
177
+ * @param {{ expectedAidOrigin?: string }} [options]
178
+ * @returns {{ specRev: number, flags: number, uriAnchored: boolean,
179
+ * action: string, actionOmitted: boolean }}
180
+ */
181
+ export function verifyFrame(raw, options = {}) {
182
+ if (!Buffer.isBuffer(raw) || raw.length !== SIZE) {
183
+ throw new Error(`frame must be ${SIZE} bytes`);
184
+ }
185
+ if (raw.readUInt32BE(0x00) !== MAGIC) throw new Error('bad RTTP_MAGIC');
186
+ if (raw.readBigUInt64BE(0x0C) !== VERSION_ID) throw new Error('unsupported VERSION_ID');
187
+
188
+ const specRev = raw[OFF_SPEC_REV];
189
+ if (!KNOWN_SPEC_REVS.has(specRev)) {
190
+ throw new Error(`unknown SPEC_REV: ${specRev} (fail closed)`);
191
+ }
192
+
193
+ const flags = raw[OFF_FLAGS];
194
+ const actionLen = raw[OFF_ACTION_LEN];
195
+
196
+ if (specRev === 0) {
197
+ // R6: a pre-v1.2.6 frame must have an all-zero extension block.
198
+ for (let i = OFF_SPEC_REV; i < SIZE; i += 1) {
199
+ if (raw[i] !== 0) throw new Error('SPEC_REV=0 but extension block is not all-zero');
200
+ }
201
+ } else {
202
+ // R7: unknown FLAGS bits and any RESERVED byte are rejections.
203
+ if ((flags & ~FLAG_URI_ANCHORED) !== 0) {
204
+ throw new Error(`unknown FLAGS bits set: 0x${(flags & ~FLAG_URI_ANCHORED).toString(16).padStart(2, '0')}`);
205
+ }
206
+ for (let i = OFF_RESERVED; i < SIZE; i += 1) {
207
+ if (raw[i] !== 0) throw new Error('RESERVED bytes must be zero in SPEC_REV=1');
208
+ }
209
+ // R4: ACTION_LEN is one byte, the field holds 16.
210
+ if (actionLen > ACTION_MAX_LEN) {
211
+ throw new Error(`ACTION_LEN ${actionLen} > ${ACTION_MAX_LEN}`);
212
+ }
213
+ for (let i = OFF_ACTION + actionLen; i < OFF_ACTION + ACTION_MAX_LEN; i += 1) {
214
+ if (raw[i] !== 0) throw new Error('ACTION padding must be zero');
215
+ }
216
+ }
217
+
218
+ let action = '';
219
+ if (specRev === 1 && actionLen > 0) {
220
+ action = raw.subarray(OFF_ACTION, OFF_ACTION + actionLen).toString('ascii');
221
+ if (!TOKEN.test(action)) {
222
+ throw new Error(`ACTION violates RFC-002 sec. 10.2: ${JSON.stringify(action)}`);
223
+ }
224
+ }
225
+
226
+ if (options.expectedAidOrigin !== undefined) {
227
+ const got = raw.subarray(0x46, 0x66).toString('hex');
228
+ if (got !== options.expectedAidOrigin) {
229
+ throw new Error('AID_ORIGIN does not match the expected origin');
230
+ }
231
+ }
232
+
233
+ return {
234
+ specRev,
235
+ flags,
236
+ uriAnchored: (flags & FLAG_URI_ANCHORED) !== 0,
237
+ action,
238
+ actionOmitted: action === '',
239
+ };
240
+ }
241
+
162
242
  // ---------------------------------------------------------------------------
163
243
  // Vector loading and replay
164
244
  // ---------------------------------------------------------------------------
@@ -219,6 +299,31 @@ export function runConformance(vectors) {
219
299
  if (frame.toString('hex') !== c.header_hex) {
220
300
  failures.push(`${c.uri} :: frame bytes mismatch`);
221
301
  }
302
+ try {
303
+ verifyFrame(frame, { expectedAidOrigin: aidHex });
304
+ } catch (err) {
305
+ failures.push(`${c.uri} :: published frame rejected by verifyFrame: ${err.message}`);
306
+ }
307
+ }
308
+
309
+ for (const c of (vectors.negative_frames || [])) {
310
+ checked += 1;
311
+ try {
312
+ verifyFrame(Buffer.from(c.header_hex, 'hex'), { expectedAidOrigin: aidHex });
313
+ failures.push(`frame ${c.rule} :: ${c.label} :: expected REJECT but accepted`);
314
+ } catch { /* expected */ }
315
+ }
316
+
317
+ for (const c of (vectors.positive_frames || [])) {
318
+ checked += 1;
319
+ try {
320
+ const got = verifyFrame(Buffer.from(c.header_hex, 'hex'), { expectedAidOrigin: aidHex });
321
+ if (got.action !== c.action) {
322
+ failures.push(`frame ${c.rule} :: action ${got.action} != ${c.action}`);
323
+ }
324
+ } catch (err) {
325
+ failures.push(`frame ${c.rule} :: ${c.label} :: expected ACCEPT but rejected: ${err.message}`);
326
+ }
222
327
  }
223
328
 
224
329
  {
@@ -239,6 +344,14 @@ export function runConformance(vectors) {
239
344
  if (frame.toString('hex') !== cv.header_hex) {
240
345
  failures.push('compat vector :: frame bytes mismatch');
241
346
  }
347
+ try {
348
+ const got = verifyFrame(Buffer.from(cv.header_hex, 'hex'), { expectedAidOrigin: aidHex });
349
+ if (got.specRev !== 0 || !got.actionOmitted) {
350
+ failures.push('compat vector :: SPEC_REV=0 semantics mismatch');
351
+ }
352
+ } catch (err) {
353
+ failures.push(`compat vector :: verifyFrame rejected an old frame: ${err.message}`);
354
+ }
242
355
  }
243
356
 
244
357
  return { checked, failures, passed: failures.length === 0 };
@@ -17,7 +17,7 @@ export interface RttpUri {
17
17
  intent_hash32: number | null;
18
18
  pillar: string;
19
19
  root: string;
20
- /** Empty string when omitted (§10.4 default operation). */
20
+ /** Empty string when omitted (sec. 10.4 default operation). */
21
21
  action: string;
22
22
  action_omitted: boolean;
23
23
  /** `<intent>.<pillar>.<root>`, canonical. */
@@ -30,7 +30,7 @@ export interface RttpUri {
30
30
  route_shard_hex: string;
31
31
  }
32
32
 
33
- /** The URI does not conform. Always fail closed (§10.5). */
33
+ /** The URI does not conform. Always fail closed (sec. 10.5). */
34
34
  export declare class RttpUriError extends Error {
35
35
  constructor(message: string);
36
36
  name: 'RttpUriError';
@@ -42,7 +42,7 @@ export declare const SCHEME: 'rttp';
42
42
  /** The browser protocol-handler form - a property of the client, never of the target. */
43
43
  export declare const BROWSER_HANDLER_SCHEME: 'web+rttp';
44
44
 
45
- /** §4.1: ROUTE_SHARD is a u128. */
45
+ /** sec. 4.1: ROUTE_SHARD is a u128. */
46
46
  export declare const ROUTE_SHARD_BYTES: 16;
47
47
 
48
48
  /** Length of a hash-intent, in lowercase hex characters. */
@@ -60,7 +60,7 @@ export declare function parse(uri: string): RttpUri;
60
60
  /** Identical to `parse`. */
61
61
  export declare function parseAndDerive(uri: string): RttpUri;
62
62
 
63
- /** §10.2 `action` syntax test. Judges form only - the verb set is open. */
63
+ /** sec. 10.2 `action` syntax test. Judges form only - the verb set is open. */
64
64
  export declare function isValidAction(value: unknown): boolean;
65
65
 
66
66
  /** `authority` -> ROUTE_SHARD (16 bytes). Throws `RttpUriError`. */
package/src/rttp-uri.mjs CHANGED
@@ -1,37 +1,37 @@
1
1
  /**
2
- * @aicent/rttp - RFC-002 §10 `rttp` URI validation, canonicalisation and
2
+ * @aicent/rttp - RFC-002 sec. 10 `rttp` URI validation, canonicalisation and
3
3
  * ROUTE_SHARD derivation.
4
4
  *
5
- * This module is the reference implementation of the mapping RFC-002 §10.4
5
+ * This module is the reference implementation of the mapping RFC-002 sec. 10.4
6
6
  * promises:
7
7
  * "Dereferencing an `rttp` URI emits one pulse ... carrying `action` as the
8
8
  * intent verb."
9
- * The specification previously had syntax (§10.2 ABNF) but no "URI -> frame
9
+ * The specification previously had syntax (sec. 10.2 ABNF) but no "URI -> frame
10
10
  * field" mapping; this module and `SPEC/RTTP-FRAME-EXT-v1.2.6.md` supply that
11
11
  * segment. It is an independent implementation of the same conformance vectors
12
12
  * as the Python package `rttp` on PyPI - two implementations that share no code
13
13
  * and agree byte for byte is what makes the vectors worth anything.
14
14
  *
15
15
  * Authority (important):
16
- * * URI **syntax** - the sole authority is the RFC-002 §10.2 ABNF. This module
16
+ * * URI **syntax** - the sole authority is the RFC-002 sec. 10.2 ABNF. This module
17
17
  * adds no syntax.
18
18
  * * This module does exactly two things: (1) validate and **canonicalise**
19
- * per §10.2/§10.3; (2) derive ROUTE_SHARD.
19
+ * per sec. 10.2/sec. 10.3; (2) derive ROUTE_SHARD.
20
20
  *
21
21
  * Design constraints (each is a requirement in the specification):
22
- * * §10.1 `intent` is either 8 lowercase hex digits (a 32-bit routing hash)
22
+ * * sec. 10.1 `intent` is either 8 lowercase hex digits (a 32-bit routing hash)
23
23
  * or a readable organ token.
24
- * * §10.2 `action` is an **open set**: 1*( %x61-7A / DIGIT / "-" ).
25
- * * §10.3 The canonical form is **lowercase US-ASCII**; there is no userinfo,
24
+ * * sec. 10.2 `action` is an **open set**: 1*( %x61-7A / DIGIT / "-" ).
25
+ * * sec. 10.3 The canonical form is **lowercase US-ASCII**; there is no userinfo,
26
26
  * port, query or fragment.
27
- * * §10.5 **No DNS**: ROUTE_SHARD is pure computation - no registry, no
27
+ * * sec. 10.5 **No DNS**: ROUTE_SHARD is pure computation - no registry, no
28
28
  * resolver, no network.
29
29
  *
30
30
  * Case is **not** normalised. A case variant is not a spelling difference, it is
31
- * a different string, and it is rejected (§10.3). Nothing here is ever accepted
31
+ * a different string, and it is rejected (sec. 10.3). Nothing here is ever accepted
32
32
  * because a check could not be performed: this module fails closed.
33
33
  *
34
- * ROUTE_SHARD derivation (see spec §5): the first 16 bytes of
34
+ * ROUTE_SHARD derivation (see spec sec. 5): the first 16 bytes of
35
35
  * SHA-256(ASCII(canonical_authority)).
36
36
  *
37
37
  * Zero dependencies - `node:crypto` and nothing else.
@@ -54,22 +54,22 @@ export const SCHEME = 'rttp';
54
54
  * **client**, never of the target: both spellings name the same address, and the
55
55
  * canonical output of this module is always `rttp://`.
56
56
  *
57
- * Accepted for **tolerance only** — the specification defines no such form. A
57
+ * Accepted for **tolerance only** -- the specification defines no such form. A
58
58
  * conforming third-party implementation is not required to reproduce this
59
59
  * tolerance; refusing the string a browser is about to deliver would, however,
60
60
  * break the handler path.
61
61
  */
62
62
  export const BROWSER_HANDLER_SCHEME = 'web+rttp';
63
63
 
64
- /** §4.1: ROUTE_SHARD is a u128. */
64
+ /** sec. 4.1: ROUTE_SHARD is a u128. */
65
65
  export const ROUTE_SHARD_BYTES = 16;
66
66
 
67
- /** §10.2: `lowhex = %x30-39 / %x61-66`; 8 of them is a 32-bit routing hash. */
67
+ /** sec. 10.2: `lowhex = %x30-39 / %x61-66`; 8 of them is a 32-bit routing hash. */
68
68
  export const HASH_INTENT_LEN = 8;
69
69
 
70
70
  /**
71
71
  * The capacity of the frame's ACTION field (`0x69`..`0x78`, one byte of
72
- * ACTION_LEN at `0x68`). **Not** a syntax rule: §10.2 places no length limit on
72
+ * ACTION_LEN at `0x68`). **Not** a syntax rule: sec. 10.2 places no length limit on
73
73
  * `action`, so `parse()` accepts longer values. A frame builder must reject
74
74
  * them, because the field cannot hold more.
75
75
  */
@@ -78,11 +78,11 @@ export const ACTION_MAX_LEN = 16;
78
78
  /** The RTTP specification release this module was written against. */
79
79
  export const SPEC_RELEASE = 'v1.2.6';
80
80
 
81
- // §10.2: name-intent / pillar / root / action = 1*( %x61-7A / DIGIT / "-" )
81
+ // sec. 10.2: name-intent / pillar / root / action = 1*( %x61-7A / DIGIT / "-" )
82
82
  const TOKEN = /^[a-z0-9-]+$/;
83
83
  const LOWHEX = /^[0-9a-f]+$/;
84
84
 
85
- // §10.3: this scheme defines no userinfo / port / query / fragment
85
+ // sec. 10.3: this scheme defines no userinfo / port / query / fragment
86
86
  const FORBIDDEN_CHARS = ['@', '?', '#', '[', ']'];
87
87
 
88
88
  // ---------------------------------------------------------------------------
@@ -90,7 +90,7 @@ const FORBIDDEN_CHARS = ['@', '?', '#', '[', ']'];
90
90
  // ---------------------------------------------------------------------------
91
91
 
92
92
  /**
93
- * The URI does not conform. **Always fail closed** (§10.5: no fallback, and no
93
+ * The URI does not conform. **Always fail closed** (sec. 10.5: no fallback, and no
94
94
  * `rttps`).
95
95
  */
96
96
  export class RttpUriError extends Error {
@@ -105,7 +105,7 @@ export class RttpUriError extends Error {
105
105
  // ---------------------------------------------------------------------------
106
106
 
107
107
  /**
108
- * §10.2 token check, with the case rule of §10.3 kept separate so that an
108
+ * sec. 10.2 token check, with the case rule of sec. 10.3 kept separate so that an
109
109
  * uppercase letter produces a message that says what is actually wrong.
110
110
  *
111
111
  * @param {string} value
@@ -118,10 +118,10 @@ function checkToken(value, what) {
118
118
  for (const ch of value) {
119
119
  if (TOKEN.test(ch)) continue;
120
120
  if (ch !== ch.toLowerCase()) {
121
- // §10.3: the canonical form is lowercase. Uppercase is not a normalisable
121
+ // sec. 10.3: the canonical form is lowercase. Uppercase is not a normalisable
122
122
  // spelling difference - it is illegal input.
123
123
  throw new RttpUriError(
124
- `${what} contains uppercase '${ch}' - lowercase US-ASCII only (RFC-002 §10.3)`);
124
+ `${what} contains uppercase '${ch}' - lowercase US-ASCII only (RFC-002 sec. 10.3)`);
125
125
  }
126
126
  throw new RttpUriError(
127
127
  `${what} contains illegal character ${JSON.stringify(ch)} (allowed: a-z, 0-9, '-')`);
@@ -142,7 +142,7 @@ function checkIntent(value) {
142
142
  }
143
143
 
144
144
  /**
145
- * §10.2 `action` syntax test: 1*( %x61-7A / DIGIT / "-" ).
145
+ * sec. 10.2 `action` syntax test: 1*( %x61-7A / DIGIT / "-" ).
146
146
  *
147
147
  * `action` is an **open set** - this only judges the *form*, never whether the
148
148
  * verb is one this specification knows. Frame builders reuse it so the rule is
@@ -168,10 +168,10 @@ export function isValidAction(value) {
168
168
  *
169
169
  * Properties:
170
170
  * * **Deterministic** - one authority always yields one shard.
171
- * * **Pure computation** - no DNS, no registry, no network (§10.5).
171
+ * * **Pure computation** - no DNS, no registry, no network (sec. 10.5).
172
172
  * * **One-way** - the authority cannot be recovered from the shard.
173
173
  * * **Independent of `action`** - it routes *where*, never *what*, which is
174
- * exactly why `action` must be a separate frame field (see spec §5.3).
174
+ * exactly why `action` must be a separate frame field (see spec sec. 5.3).
175
175
  *
176
176
  * @param {string} canonicalAuthority
177
177
  * @returns {Uint8Array} 16 bytes
@@ -181,7 +181,7 @@ export function deriveRouteShard(canonicalAuthority) {
181
181
  throw new RttpUriError('canonical_authority is empty');
182
182
  }
183
183
  if (canonicalAuthority !== canonicalAuthority.toLowerCase()) {
184
- throw new RttpUriError('canonical_authority must be lowercase (RFC-002 §10.3)');
184
+ throw new RttpUriError('canonical_authority must be lowercase (RFC-002 sec. 10.3)');
185
185
  }
186
186
  const digest = createHash('sha256')
187
187
  .update(Buffer.from(canonicalAuthority, 'ascii'))
@@ -206,7 +206,7 @@ export function deriveRouteShardHex(canonicalAuthority) {
206
206
  *
207
207
  * The validation order deliberately stays "reject before interpreting": any
208
208
  * suspicious form throws immediately, and nothing is ever guessed at.
209
- * (§10.5: user agents that do not implement this scheme fail closed.)
209
+ * (sec. 10.5: user agents that do not implement this scheme fail closed.)
210
210
  *
211
211
  * Field names are `snake_case` on purpose: the published conformance vectors use
212
212
  * those names, so the output of the Python and JavaScript implementations
@@ -241,7 +241,7 @@ export function parse(uri) {
241
241
  throw new RttpUriError('uri must not contain leading/trailing whitespace');
242
242
  }
243
243
 
244
- // --- scheme (§10.6 prefix whitelist) ---
244
+ // --- scheme (sec. 10.6 prefix whitelist) ---
245
245
  let scheme;
246
246
  let rest;
247
247
  if (uri.startsWith(`${BROWSER_HANDLER_SCHEME}://`)) {
@@ -255,7 +255,7 @@ export function parse(uri) {
255
255
  `not an rttp URI: must begin with '${SCHEME}://' or '${BROWSER_HANDLER_SCHEME}://'`);
256
256
  }
257
257
 
258
- // --- §10.3 excluded characters ---
258
+ // --- sec. 10.3 excluded characters ---
259
259
  for (const ch of FORBIDDEN_CHARS) {
260
260
  if (uri.includes(ch)) {
261
261
  throw new RttpUriError(
@@ -273,7 +273,7 @@ export function parse(uri) {
273
273
  throw new RttpUriError("path must be a single segment '/<action>'");
274
274
  }
275
275
  if (slash !== -1 && action === '') {
276
- // §10.2: path = "/" action, action = 1*(...). A trailing slash with an empty
276
+ // sec. 10.2: path = "/" action, action = 1*(...). A trailing slash with an empty
277
277
  // action is a different form from "no path", and the two must not be
278
278
  // conflated.
279
279
  throw new RttpUriError("trailing '/' with empty action: path must be '/<action>'");
package/src/vectors.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "spec": "RTTP-FRAME-EXT-v1.2.6",
3
3
  "applies_to": [
4
- "RFC-002 §4.1",
5
- "RFC-002 §10"
4
+ "RFC-002 sec. 4.1",
5
+ "RFC-002 sec. 10"
6
6
  ],
7
7
  "generated_by": "SPEC/tools/conformance.py",
8
8
  "note": "Generated file - do not edit by hand. Change the cases in conformance.py instead.",
@@ -78,7 +78,7 @@
78
78
  "action_omitted": false,
79
79
  "route_shard_hex": "cdc32d19641c84aca3292de6276de056",
80
80
  "tolerance": true,
81
- "note": "tolerance - the specification defines no such form; accepted because it is the scheme name under which a handler is registered (RFC-002 §10.6)"
81
+ "note": "tolerance - the specification defines no such form; accepted because it is the scheme name under which a handler is registered (RFC-002 sec. 10.6)"
82
82
  },
83
83
  {
84
84
  "uri": "rttp://a-b-c.d-e.f-g/audit-v2",
@@ -98,35 +98,35 @@
98
98
  "negative_uris": [
99
99
  {
100
100
  "uri": "rttp://f3b2a1c4.rttp.aicent/VESSEL",
101
- "reason": "uppercase action - lowercase only (RFC-002 §10.3)"
101
+ "reason": "uppercase action - lowercase only (RFC-002 sec. 10.3)"
102
102
  },
103
103
  {
104
104
  "uri": "rttp://F3B2A1C4.rttp.aicent/vessel",
105
- "reason": "uppercase intent - lowercase only (RFC-002 §10.3)"
105
+ "reason": "uppercase intent - lowercase only (RFC-002 sec. 10.3)"
106
106
  },
107
107
  {
108
108
  "uri": "RTTP://f3b2a1c4.rttp.aicent/vessel",
109
- "reason": "uppercase scheme prefix - canonical form is lowercase (RFC-002 §10.3)"
109
+ "reason": "uppercase scheme prefix - canonical form is lowercase (RFC-002 sec. 10.3)"
110
110
  },
111
111
  {
112
112
  "uri": "WEB+RTTP://f3b2a1c4.rttp.aicent/vessel",
113
- "reason": "uppercase scheme prefix on the handler form (RFC-002 §10.3)"
113
+ "reason": "uppercase scheme prefix on the handler form (RFC-002 sec. 10.3)"
114
114
  },
115
115
  {
116
116
  "uri": "rttp://subject@rttp.aicent/vessel",
117
- "reason": "userinfo is not defined (RFC-002 §10.3)"
117
+ "reason": "userinfo is not defined (RFC-002 sec. 10.3)"
118
118
  },
119
119
  {
120
120
  "uri": "rttp://f3b2a1c4.rttp.aicent/vessel?x=1",
121
- "reason": "query is not defined (RFC-002 §10.3)"
121
+ "reason": "query is not defined (RFC-002 sec. 10.3)"
122
122
  },
123
123
  {
124
124
  "uri": "rttp://f3b2a1c4.rttp.aicent/vessel#f",
125
- "reason": "fragment is not defined (RFC-002 §10.3)"
125
+ "reason": "fragment is not defined (RFC-002 sec. 10.3)"
126
126
  },
127
127
  {
128
128
  "uri": "rttp://f3b2a1c4.rttp.aicent:443/vessel",
129
- "reason": "port is not defined (RFC-002 §10.3)"
129
+ "reason": "port is not defined (RFC-002 sec. 10.3)"
130
130
  },
131
131
  {
132
132
  "uri": "rttp://f3b2a1c4.rttp.aicent/a/b",
@@ -146,7 +146,7 @@
146
146
  },
147
147
  {
148
148
  "uri": "https://f3b2a1c4.rttp.aicent/vessel",
149
- "reason": "not an rttp scheme - there is no fallback (RFC-002 §10.5)"
149
+ "reason": "not an rttp scheme - there is no fallback (RFC-002 sec. 10.5)"
150
150
  },
151
151
  {
152
152
  "uri": "rttp://f3b2a1c4.rttp.aicent/vessel ",
@@ -198,13 +198,103 @@
198
198
  "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000030000000000000000186cc6acd4b0000080ffbf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d0101000000000000000000000000000000000000000000000000"
199
199
  }
200
200
  ],
201
+ "negative_frames": [
202
+ {
203
+ "label": "ACTION_LEN exceeds the 16-byte maximum (17)",
204
+ "rule": "R4",
205
+ "spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 3.2",
206
+ "expected": "reject",
207
+ "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01011176657373656c0000000000000000000000000000000000"
208
+ },
209
+ {
210
+ "label": "ACTION padding is non-zero",
211
+ "rule": "R4",
212
+ "spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 3.2",
213
+ "expected": "reject",
214
+ "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01010676657373656c0078000000000000000000000000000000"
215
+ },
216
+ {
217
+ "label": "ACTION contains an embedded NUL",
218
+ "rule": "R4",
219
+ "spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 3.2",
220
+ "expected": "reject",
221
+ "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01010676657300656c0000000000000000000000000000000000"
222
+ },
223
+ {
224
+ "label": "ACTION violates the sec. 10.2 grammar (uppercase)",
225
+ "rule": "R4",
226
+ "spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 3.2",
227
+ "expected": "reject",
228
+ "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01010656455353454c0000000000000000000000000000000000"
229
+ },
230
+ {
231
+ "label": "SPEC_REV is outside the known set (2)",
232
+ "rule": "R5",
233
+ "spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
234
+ "expected": "reject",
235
+ "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d02010676657373656c0000000000000000000000000000000000"
236
+ },
237
+ {
238
+ "label": "SPEC_REV is outside the known set (255)",
239
+ "rule": "R5",
240
+ "spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
241
+ "expected": "reject",
242
+ "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789dff010676657373656c0000000000000000000000000000000000"
243
+ },
244
+ {
245
+ "label": "SPEC_REV=0 with a non-zero extension block",
246
+ "rule": "R6",
247
+ "spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
248
+ "expected": "reject",
249
+ "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d00010676657373656c0000000000000000000000000000000000"
250
+ },
251
+ {
252
+ "label": "RESERVED is non-zero under SPEC_REV=1",
253
+ "rule": "R7",
254
+ "spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
255
+ "expected": "reject",
256
+ "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01010676657373656c0000000000000000000001000000000000"
257
+ },
258
+ {
259
+ "label": "unknown FLAGS bit set under SPEC_REV=1",
260
+ "rule": "R7",
261
+ "spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
262
+ "expected": "reject",
263
+ "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d01030676657373656c0000000000000000000000000000000000"
264
+ }
265
+ ],
266
+ "positive_frames": [
267
+ {
268
+ "label": "SPEC_REV=1 with ACTION_LEN=0 is valid and equivalent to omission",
269
+ "rule": "R3",
270
+ "spec_ref": "RTTP-FRAME-EXT-v1.2.6 sec. 4",
271
+ "expected": "accept",
272
+ "action": "",
273
+ "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d0101000000000000000000000000000000000000000000000000"
274
+ }
275
+ ],
276
+ "envelope_vector": {
277
+ "spec": "RTTP-SEAL-ENVELOPE-v1.2.6 sec. 7",
278
+ "alg": "ed25519",
279
+ "seed_hex": "e722fed924c7f76b157faab674ea97bb7c3075f55ad7d3c6319418e22b86d611",
280
+ "aid": "3125ee20b2238281f70ca408192f82152647d365219184fa1fe22c08a42c035e",
281
+ "pub": "9c097c66ab4f82136ceddaef56854f740bf87da3eec42a249ba40bbb066f1e6a",
282
+ "ts": 1760000000,
283
+ "nonce": "0011223344556677",
284
+ "payload": {
285
+ "intent": "verify",
286
+ "task_id": "selftest-0001"
287
+ },
288
+ "signing_input_hex": "727474702d7365616c2d76310a7b22616964223a2233313235656532306232323338323831663730636134303831393266383231353236343764333635323139313834666131666532326330386134326330333565222c22616c67223a2265643235353139222c226e6f6e6365223a2230303131323233333434353536363737222c227061796c6f6164223a7b22696e74656e74223a22766572696679222c227461736b5f6964223a2273656c66746573742d30303031227d2c22707562223a2239633039376336366162346638323133366365646461656635363835346637343062663837646133656563343261323439626134306262623036366631653661222c227473223a313736303030303030307d",
289
+ "sig": "8d8e0d458b1defb6fd078343e7e46fc96cbaa05b6ead9e587d324e4c2358145790042f4a5001f3d902e7da670e2ea5700903363dd2825159a25dc262b25cb305"
290
+ },
201
291
  "compat_vector_spec_rev_0": {
202
292
  "uri": "rttp://f3b2a1c4.rttp.aicent/vessel",
203
293
  "spec_rev": 0,
204
294
  "action": "",
205
295
  "action_omitted": true,
206
296
  "uri_anchored": false,
207
- "expected": "verify() MUST accept; action is treated as omitted (RFC-002 §10.4 default operation)",
297
+ "expected": "verify() MUST accept; action is treated as omitted (RFC-002 sec. 10.4 default operation)",
208
298
  "header_hex": "5254545000000000000000000000000000000082000000000000000000000000000000010000000000000000186cc6acd4b00000ff01bf77b78ff6ceafc363226aa5865622185d42ba8b38fe10bfa702bdfe6af536ea16e20e35fbb48d84c3658d80cad2789d0000000000000000000000000000000000000000000000000000"
209
299
  }
210
300
  }