@aicent/iqa 1.2.7 → 1.2.8
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 +72 -61
- package/bin/iqa.mjs +3 -3
- package/examples/quickstart.mjs +1 -1
- package/package.json +2 -2
- package/src/conformance.d.mts +3 -3
- package/src/conformance.mjs +10 -10
- package/src/iqa-uri.d.mts +5 -5
- package/src/iqa-uri.mjs +31 -31
- package/src/vectors.json +25 -25
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @aicent/iqa
|
|
2
2
|
|
|
3
3
|
**IQA for JavaScript: `iqa://` subject-attestation addressing, `IQA_ROUTE`
|
|
4
|
-
derivation, action safety classes
|
|
4
|
+
derivation, action safety classes -- and the published conformance vectors that
|
|
5
5
|
prove an implementation is right.**
|
|
6
6
|
|
|
7
|
-
[RFC-009
|
|
7
|
+
[RFC-009 sec. 10](https://iqa.org/RFC-009/) - sec. 11 - stack v1.2.6 - zero dependencies - no build step
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
## Verify it in one command
|
|
11
|
+
## Verify it in one command -- no trust required
|
|
12
12
|
|
|
13
13
|
```console
|
|
14
14
|
$ npx @aicent/iqa
|
|
@@ -18,22 +18,27 @@ $ npx @aicent/iqa
|
|
|
18
18
|
That is the point of this package. A specification is worth exactly what an
|
|
19
19
|
independent implementation can reproduce from it, so instead of asking you to
|
|
20
20
|
believe a table of numbers, the vectors ship inside the package and replay
|
|
21
|
-
locally
|
|
21
|
+
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.
|
|
22
22
|
|
|
23
|
-
The vectors cover the `iqa://` grammar (RFC-009
|
|
24
|
-
(`organ` and `action`
|
|
25
|
-
is open), the
|
|
26
|
-
the deterministic Ed25519 attestation envelope
|
|
23
|
+
The vectors cover the `iqa://` grammar (RFC-009 sec. 10.2), the **closed sets**
|
|
24
|
+
(`organ` and `action` -- the deliberate difference from `rttp`, whose verb set
|
|
25
|
+
is open), the sec. 11.3 action safety classes, the `IQA_ROUTE` derivation, **and**
|
|
26
|
+
the deterministic Ed25519 attestation envelope -- replayed by both
|
|
27
27
|
implementations.
|
|
28
28
|
|
|
29
|
-
### The other half
|
|
29
|
+
### The other half -- Python, on PyPI
|
|
30
30
|
|
|
31
31
|
```console
|
|
32
32
|
$ pip install iqa-org
|
|
33
33
|
$ python -m iqa.selftest
|
|
34
|
+
[PASS] all 75 checks passed (3 skipped) # default install
|
|
35
|
+
|
|
36
|
+
$ pip install iqa-org[ed25519]
|
|
37
|
+
$ python -m iqa.selftest
|
|
38
|
+
[PASS] all 78 checks passed # with the optional Ed25519 backend
|
|
34
39
|
```
|
|
35
40
|
|
|
36
|
-
Published as **`@aicent/iqa`**
|
|
41
|
+
Published as **`@aicent/iqa`** -- the bare `iqa` name is unregistered on npm but
|
|
37
42
|
blocked by its typosquatting protection, so the JavaScript side lives under
|
|
38
43
|
this project's organization, as `@aicent/rttp` already does. PyPI applies the
|
|
39
44
|
same class of guard: the Python package is therefore **`iqa-org`** (same name
|
|
@@ -62,10 +67,10 @@ import { parse } from '@aicent/iqa';
|
|
|
62
67
|
|
|
63
68
|
const r = parse('iqa://3f9a1b2c.gateway.iqa');
|
|
64
69
|
|
|
65
|
-
r.subject // '3f9a1b2c'
|
|
66
|
-
r.organ // 'gateway'
|
|
67
|
-
r.action // null
|
|
68
|
-
r.action_safe // true
|
|
70
|
+
r.subject // '3f9a1b2c' -- 8-hex routing short form of the AID
|
|
71
|
+
r.organ // 'gateway' -- closed set: forge | tss | gateway
|
|
72
|
+
r.action // null -- omitted = standing read (sec. 11.1)
|
|
73
|
+
r.action_safe // true -- sec. 11.3: omitted and verify are SAFE
|
|
69
74
|
r.canonical_uri // 'iqa://3f9a1b2c.gateway.iqa'
|
|
70
75
|
r.route_hex // '5c378581' <- pure computation, pinned by the vectors
|
|
71
76
|
```
|
|
@@ -80,28 +85,28 @@ $ node examples/quickstart.mjs
|
|
|
80
85
|
Its expected output is the route fingerprint the published conformance vectors
|
|
81
86
|
pin for that address, so the example can be **checked** rather than believed.
|
|
82
87
|
|
|
83
|
-
Addressing is **DNS-free** (RFC-009
|
|
88
|
+
Addressing is **DNS-free** (RFC-009 sec. 12 #9): the route fingerprint is derived
|
|
84
89
|
from the authority by SHA-256, so no registry, resolver or network is
|
|
85
|
-
involved. A URI either yields its fields or throws
|
|
90
|
+
involved. A URI either yields its fields or throws -- there is no fallback, no
|
|
86
91
|
`iqas`, and no rewriting to `https`.
|
|
87
92
|
|
|
88
93
|
---
|
|
89
94
|
|
|
90
|
-
## The two closed sets
|
|
95
|
+
## The two closed sets -- the deliberate difference from `rttp`
|
|
91
96
|
|
|
92
|
-
`rttp` accepts any well-formed action verb. **`iqa` does not.** RFC-009
|
|
97
|
+
`rttp` accepts any well-formed action verb. **`iqa` does not.** RFC-009 sec. 10.1
|
|
93
98
|
marks *both* segments "Closed set", and this package enforces exactly that:
|
|
94
99
|
|
|
95
100
|
```js
|
|
96
|
-
parse('iqa://3f9a1b2c.gateway.iqa/pulse') // throws
|
|
97
|
-
parse('iqa://3f9a1b2c.forgery.iqa') // throws
|
|
101
|
+
parse('iqa://3f9a1b2c.gateway.iqa/pulse') // throws -- 'pulse' is an rttp verb
|
|
102
|
+
parse('iqa://3f9a1b2c.forgery.iqa') // throws -- organ is forge|tss|gateway
|
|
98
103
|
```
|
|
99
104
|
|
|
100
105
|
### The one rule that surprises people
|
|
101
106
|
|
|
102
|
-
**Malformed input is rejected, not normalised.** `iqa
|
|
103
|
-
`iqa://3F9A1B2C
|
|
104
|
-
US-ASCII (RFC-009
|
|
107
|
+
**Malformed input is rejected, not normalised.** `iqa://.../AUDIT`,
|
|
108
|
+
`iqa://3F9A1B2C....` and `IQA://...` all throw: the canonical form is lowercase
|
|
109
|
+
US-ASCII (RFC-009 sec. 10.3), a case variant is not a spelling difference, and
|
|
105
110
|
nothing is ever accepted because a check could not be performed.
|
|
106
111
|
|
|
107
112
|
### One honest quirk of the grammar
|
|
@@ -113,36 +118,36 @@ implementation agrees on it.
|
|
|
113
118
|
|
|
114
119
|
---
|
|
115
120
|
|
|
116
|
-
## Action safety classes (RFC-009
|
|
121
|
+
## Action safety classes (RFC-009 sec. 11.3)
|
|
117
122
|
|
|
118
123
|
| `action` | Class | `action_safe` |
|
|
119
124
|
|:---|:---|:---|
|
|
120
|
-
| *(omitted)* | SAFE
|
|
121
|
-
| `verify` | SAFE
|
|
122
|
-
| `audit` | NOT SAFE
|
|
123
|
-
| `attest` | NOT SAFE
|
|
124
|
-
| `revoke` | NOT SAFE
|
|
125
|
+
| *(omitted)* | SAFE -- standing read | `true` |
|
|
126
|
+
| `verify` | SAFE -- read-only comparison | `true` |
|
|
127
|
+
| `audit` | NOT SAFE -- can trigger Authority Ischemia | `false` |
|
|
128
|
+
| `attest` | NOT SAFE -- state transition | `false` |
|
|
129
|
+
| `revoke` | NOT SAFE -- state transition | `false` |
|
|
125
130
|
|
|
126
|
-
Per
|
|
131
|
+
Per sec. 11.2, dereferencing an `iqa` URI MUST NOT transition any subject's
|
|
127
132
|
standing. Tooling built on this package MUST gate the three NOT-SAFE verbs
|
|
128
|
-
behind an explicit user action
|
|
133
|
+
behind an explicit user action -- parsing one is not requesting one.
|
|
129
134
|
|
|
130
135
|
---
|
|
131
136
|
|
|
132
|
-
## API
|
|
137
|
+
## API -- main entry
|
|
133
138
|
|
|
134
139
|
| Export | What it does |
|
|
135
140
|
|:---|:---|
|
|
136
141
|
| `parse(uri)` | Validate, canonicalise, classify, derive. Throws `IqaUriError`. |
|
|
137
142
|
| `deriveRoute(authority)` | `Uint8Array(4)` = `SHA-256(ASCII(authority))[0:4]`. |
|
|
138
143
|
| `deriveRouteHex(authority)` | The same, as 8 lowercase hex characters. |
|
|
139
|
-
| `isValidAction(value)` |
|
|
144
|
+
| `isValidAction(value)` | sec. 10.1 closed-set test. |
|
|
140
145
|
| `IqaUriError` | The only error type thrown. |
|
|
141
146
|
| `SCHEME`, `ORGANS`, `ACTIONS`, `STANDINGS`, `HASH_LENGTHS` | Constants. |
|
|
142
147
|
|
|
143
|
-
### The attestation envelope
|
|
148
|
+
### The attestation envelope -- `@aicent/iqa/conformance`
|
|
144
149
|
|
|
145
|
-
The deterministic Ed25519 envelope (SPEC/IQA-URI-ATTEST-v1.2.6
|
|
150
|
+
The deterministic Ed25519 envelope (SPEC/IQA-URI-ATTEST-v1.2.6 sec. 5: domain
|
|
146
151
|
prefix `iqa-attest-v1`, `AID = SHA-256(pub)`, `ts`/`nonce` inside the
|
|
147
152
|
signature, 120-second freshness) is part of the shared vector set and is
|
|
148
153
|
replayed by this package's conformance run. `buildSigningInput` is exported
|
|
@@ -174,9 +179,9 @@ In CI:
|
|
|
174
179
|
|
|
175
180
|
| Group | Count | What it proves |
|
|
176
181
|
|:---|---:|:---|
|
|
177
|
-
| `positive_uris` | 8 | Accept, classify and derive
|
|
178
|
-
| `negative_uris` | 18 | Reject
|
|
179
|
-
| `action_safety` | 5 |
|
|
182
|
+
| `positive_uris` | 8 | Accept, classify and derive -- sec. 10.3's own examples, the 256-bit form, the all-zero boundary, the name-subject fall-through |
|
|
183
|
+
| `negative_uris` | 18 | Reject -- uppercase anywhere, `iqas://`, userinfo, port, query, fragment, wrong authority shape, closed-set violations on both segments |
|
|
184
|
+
| `action_safety` | 5 | sec. 11.3 classes as data |
|
|
180
185
|
| `attest_envelope_vector` | 1 | Deterministic Ed25519 envelope, verified through `node:crypto` |
|
|
181
186
|
|
|
182
187
|
`vectors.json` is plain JSON and ships inside the package, also reachable as
|
|
@@ -185,10 +190,10 @@ is what it is for.
|
|
|
185
190
|
|
|
186
191
|
### Why the checker lives in this package, and why that is still honest
|
|
187
192
|
|
|
188
|
-
`src/conformance.mjs` is written from RFC-009
|
|
193
|
+
`src/conformance.mjs` is written from RFC-009 sec. 10 alone and **must never
|
|
189
194
|
import `src/iqa-uri.mjs`**. A conformance suite that reuses the code under
|
|
190
|
-
test only proves the code equals itself. The two files share no code
|
|
191
|
-
a streaming charset scanner, the other a single full-match grammar check
|
|
195
|
+
test only proves the code equals itself. The two files share no code -- one is
|
|
196
|
+
a streaming charset scanner, the other a single full-match grammar check -- so
|
|
192
197
|
the rule is checkable by reading them.
|
|
193
198
|
|
|
194
199
|
The vectors are **generated, not written**. `generated_by` in the JSON names
|
|
@@ -198,20 +203,20 @@ starts agreeing with its own bugs.
|
|
|
198
203
|
|
|
199
204
|
---
|
|
200
205
|
|
|
201
|
-
## Scope
|
|
206
|
+
## Scope -- what this package is not
|
|
202
207
|
|
|
203
208
|
| | |
|
|
204
209
|
|:---|:---|
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
|
|
|
208
|
-
|
|
|
209
|
-
|
|
|
210
|
-
|
|
|
211
|
-
|
|
|
212
|
-
|
|
|
213
|
-
|
|
|
214
|
-
|
|
|
210
|
+
| OK **Validation / canonicalisation** | Real, and specified (RFC-009 sec. 10.2 / sec. 10.3). |
|
|
211
|
+
| OK **Closed-set enforcement** | Real, and specified (sec. 10.1). |
|
|
212
|
+
| OK **IQA_ROUTE derivation** | Real (SPEC sec. 3, decision D1). |
|
|
213
|
+
| OK **Action safety classes** | Real, and specified (sec. 11.3). |
|
|
214
|
+
| OK **Conformance vectors + independent replay** | Published and generated. |
|
|
215
|
+
| NO **Dereferencing / resolver service** | Not here. The codec computes; *answering* a standing read is an Organ's job (RFC-009-C sec. 3). |
|
|
216
|
+
| NO **Staking, tiers, vitality** | Operator economics and telemetry -- RFC-009 narrative, no cryptographic object in this package. |
|
|
217
|
+
| NO **Post-quantum Lattice Guard** | RFC-009 sec. 12 #11 schedules it for v1.4.0; no implementation exists. |
|
|
218
|
+
| NO **Revocation transport** | sec. 11.3 defines the verb; executing it is not the codec's job. |
|
|
219
|
+
| NO **Confidentiality** | Signing is not encryption. |
|
|
215
220
|
|
|
216
221
|
---
|
|
217
222
|
|
|
@@ -219,21 +224,27 @@ starts agreeing with its own bugs.
|
|
|
219
224
|
|
|
220
225
|
| Ecosystem | Name | Status |
|
|
221
226
|
|:---|:---|:---|
|
|
222
|
-
| crates.io | **`iqa-org`** | **published**
|
|
223
|
-
| PyPI | **`iqa-org`** | **published**
|
|
224
|
-
| npm | **`@aicent/iqa`** | the bare `iqa` name is unregistered but **blocked by npm's typosquatting protection** (too similar to existing short names)
|
|
227
|
+
| crates.io | **`iqa-org`** | **published** -- `1.2.8-alpha`. Every published version of this crate is a pre-release, so Cargo will not select it from a plain `iqa-org` requirement: write `cargo add iqa-org@1.2.8-alpha`. |
|
|
228
|
+
| PyPI | **`iqa-org`** | **published** -- the Python reference implementation; `pip install iqa-org` (bare `iqa` blocked by PyPI's typosquatting protection, same class of guard as npm) |
|
|
229
|
+
| npm | **`@aicent/iqa`** | the bare `iqa` name is unregistered but **blocked by npm's typosquatting protection** (too similar to existing short names) -- so this package is published under the organization this project controls, as `@aicent/rttp` already is. The package name itself is still just `iqa`. |
|
|
225
230
|
|
|
226
231
|
---
|
|
227
232
|
|
|
228
233
|
## Specification status
|
|
229
234
|
|
|
230
|
-
* **`iqa` URI scheme**
|
|
231
|
-
Provisional, **under review**. It is **not yet registered**
|
|
232
|
-
2026-09-
|
|
235
|
+
* **`iqa` URI scheme** -- submitted to IANA under RFC 7595, ticket **#1459963**,
|
|
236
|
+
Provisional, **under review**. It is **not yet registered** -- as of
|
|
237
|
+
2026-09-20 the IANA "URI Schemes" registry contains no `iqa` entry. Please
|
|
233
238
|
describe it that way.
|
|
234
|
-
* **
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
* **Internet-Draft (IETF)** -- under IETF review as the Individual Submission
|
|
240
|
+
Internet-Draft `draft-li-iqa-subject-attestation` (revision -00, posted
|
|
241
|
+
2026-09-20, informational):
|
|
242
|
+
https://datatracker.ietf.org/doc/draft-li-iqa-subject-attestation/ .
|
|
243
|
+
An Internet-Draft is a working document -- it is not an IETF standard and
|
|
244
|
+
carries no IETF endorsement.
|
|
245
|
+
* **URI grammar** -- RFC-009 sec. 10.2 (frozen; this package adds no syntax).
|
|
246
|
+
* **Dereference safety** -- RFC-009 sec. 11 (closed sets and safety classes).
|
|
247
|
+
* **Attestation envelope** -- `SPEC/IQA-URI-ATTEST-v1.2.6.md` sec. 5, a **draft**:
|
|
237
248
|
the format is implemented and vector-tested, but it is not yet a numbered
|
|
238
249
|
RFC-009 section.
|
|
239
250
|
|
package/bin/iqa.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// iqa
|
|
2
|
+
// iqa -- replay the published conformance vectors. [PASS] or it is not.
|
|
3
3
|
//
|
|
4
4
|
// npx @aicent/iqa # replay the shipped vectors
|
|
5
5
|
// npx @aicent/iqa --vectors ./new.json # replay a different vector set
|
|
@@ -22,7 +22,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
22
22
|
} else if (args[i] === '--json') {
|
|
23
23
|
json = true;
|
|
24
24
|
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
25
|
-
console.log('@aicent/iqa
|
|
25
|
+
console.log('@aicent/iqa -- replay the published conformance vectors (RFC-009 sec. 10/sec. 11)');
|
|
26
26
|
console.log(' npx @aicent/iqa replay the shipped vectors');
|
|
27
27
|
console.log(' npx @aicent/iqa --vectors ./new.json replay a different vector set');
|
|
28
28
|
console.log(' npx @aicent/iqa --json machine-readable output');
|
|
@@ -51,7 +51,7 @@ if (json) {
|
|
|
51
51
|
vectors: vectorsPath,
|
|
52
52
|
}, null, 2));
|
|
53
53
|
} else {
|
|
54
|
-
console.log(`iqa conformance replay
|
|
54
|
+
console.log(`iqa conformance replay -- spec ${vectors.spec ?? '(unlabeled)'}`);
|
|
55
55
|
for (const f of result.failures) console.log(`[FAIL] ${f}`);
|
|
56
56
|
console.log(`[PASS] all ${result.checked} checks passed`);
|
|
57
57
|
}
|
package/examples/quickstart.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The same three lines as the README quickstart
|
|
1
|
+
// The same three lines as the README quickstart -- runnable, so the example can
|
|
2
2
|
// be CHECKED rather than believed. Its expected output is the route fingerprint
|
|
3
3
|
// the published conformance vectors pin for that address.
|
|
4
4
|
//
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aicent/iqa",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "IQA for JavaScript: iqa:// subject-attestation addressing, IQA_ROUTE derivation, action safety classes
|
|
3
|
+
"version": "1.2.8",
|
|
4
|
+
"description": "IQA for JavaScript: iqa:// subject-attestation addressing, IQA_ROUTE derivation, action safety classes -- and the published conformance vectors that prove an implementation is right.",
|
|
5
5
|
"author": "IQA.ORG Organization",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"homepage": "https://iqa.org/",
|
package/src/conformance.d.mts
CHANGED
|
@@ -13,9 +13,9 @@ export interface ParsedForConformance {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* The independent RFC-009
|
|
17
|
-
* `buildFrame`-style helpers do not exist here: IQA v1.2.6 has no frame
|
|
18
|
-
* the derivation chain is URI
|
|
16
|
+
* The independent RFC-009 sec. 10 implementation used by the checker.
|
|
17
|
+
* `buildFrame`-style helpers do not exist here: IQA v1.2.6 has no frame --
|
|
18
|
+
* the derivation chain is URI -> IQA_ROUTE (4 bytes).
|
|
19
19
|
*/
|
|
20
20
|
export declare function parseUri(uri: string): ParsedForConformance;
|
|
21
21
|
|
package/src/conformance.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
// IQA conformance replay
|
|
1
|
+
// IQA conformance replay -- an INDEPENDENT implementation of RFC-009 sec. 10.
|
|
2
2
|
//
|
|
3
|
-
// This file is written from RFC-009
|
|
3
|
+
// This file is written from RFC-009 sec. 10/sec. 11 alone and MUST NEVER import
|
|
4
4
|
// ./iqa-uri.mjs. A conformance suite that reuses the code under test only
|
|
5
5
|
// proves the code equals itself. The two files share no code, so the rule is
|
|
6
6
|
// checkable by reading them.
|
|
7
7
|
//
|
|
8
8
|
// They ship together for the same reason the Python wheel ships its self-test:
|
|
9
9
|
// so a stranger can verify an installed package offline, with no repository
|
|
10
|
-
// checkout. Zero dependencies
|
|
10
|
+
// checkout. Zero dependencies -- node:crypto and node:fs only.
|
|
11
11
|
|
|
12
12
|
import { createHash, createPublicKey, verify as cryptoVerify } from 'node:crypto';
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
@@ -17,7 +17,7 @@ import { dirname, join } from 'node:path';
|
|
|
17
17
|
const DOMAIN = Buffer.from('iqa-attest-v1\n', 'utf8');
|
|
18
18
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
|
-
// Independent parse
|
|
20
|
+
// Independent parse -- single full-match grammar check, deliberately different
|
|
21
21
|
// in structure from the streaming charset scanner in iqa-uri.mjs.
|
|
22
22
|
// ---------------------------------------------------------------------------
|
|
23
23
|
|
|
@@ -27,9 +27,9 @@ const DOMAIN = Buffer.from('iqa-attest-v1\n', 'utf8');
|
|
|
27
27
|
// so uppercase, '@', ':', '?', '#', whitespace and all foreign bytes fail.
|
|
28
28
|
const GRAMMAR = /^iqa:\/\/([a-z0-9-]+)\.([a-z0-9-]+)\.([a-z0-9-]+)(?:\/([a-z0-9-]+))?$/;
|
|
29
29
|
|
|
30
|
-
const ORGANS = ['forge', 'tss', 'gateway']; //
|
|
31
|
-
const ACTIONS = ['verify', 'audit', 'attest', 'revoke']; //
|
|
32
|
-
const HASH_LENGTHS = [8, 32, 64]; //
|
|
30
|
+
const ORGANS = ['forge', 'tss', 'gateway']; // sec. 10.1 closed set
|
|
31
|
+
const ACTIONS = ['verify', 'audit', 'attest', 'revoke']; // sec. 10.1 closed set
|
|
32
|
+
const HASH_LENGTHS = [8, 32, 64]; // sec. 10.2 hash-subject
|
|
33
33
|
|
|
34
34
|
export function parseUri(uri) {
|
|
35
35
|
const m = GRAMMAR.exec(uri);
|
|
@@ -58,7 +58,7 @@ export function parseUri(uri) {
|
|
|
58
58
|
authority: `${subject}.${organ}.${root}`,
|
|
59
59
|
action,
|
|
60
60
|
action_omitted: action === null,
|
|
61
|
-
action_safe: action === null || action === 'verify', //
|
|
61
|
+
action_safe: action === null || action === 'verify', // sec. 11.3
|
|
62
62
|
canonical_uri: 'iqa://' + `${subject}.${organ}.${root}` + (action ? '/' + action : ''),
|
|
63
63
|
route_hex: createHash('sha256').update(`${subject}.${organ}.${root}`, 'ascii')
|
|
64
64
|
.digest().subarray(0, 4).toString('hex'),
|
|
@@ -66,7 +66,7 @@ export function parseUri(uri) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// ---------------------------------------------------------------------------
|
|
69
|
-
// Envelope
|
|
69
|
+
// Envelope -- canonical JSON + Ed25519 over the domain-separated input.
|
|
70
70
|
// ---------------------------------------------------------------------------
|
|
71
71
|
|
|
72
72
|
function canonicalize(value) {
|
|
@@ -120,7 +120,7 @@ export function loadVectors(path = DEFAULT_VECTORS_PATH) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* Replay a vector set
|
|
123
|
+
* Replay a vector set -> { checked, failures, passed }.
|
|
124
124
|
* The checker accepts any object with the documented groups, so a different
|
|
125
125
|
* vector set (e.g. a candidate next revision) can be replayed the same way.
|
|
126
126
|
*/
|
package/src/iqa-uri.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* iqa
|
|
2
|
+
* iqa -- RFC-009 sec. 10/sec. 11 codec for JavaScript.
|
|
3
3
|
* Zero dependencies. Field names are snake_case on purpose: they match the
|
|
4
4
|
* published conformance vectors (and the Python implementation) field for field.
|
|
5
5
|
*/
|
|
@@ -12,12 +12,12 @@ export declare const HASH_LENGTHS: readonly [8, 32, 64];
|
|
|
12
12
|
|
|
13
13
|
export declare class IqaUriError extends Error {}
|
|
14
14
|
|
|
15
|
-
/** True only for the closed four-verb set (RFC-009
|
|
15
|
+
/** True only for the closed four-verb set (RFC-009 sec. 10.1). */
|
|
16
16
|
export declare function isValidAction(value: string): boolean;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* IQA_ROUTE = SHA-256(ASCII(canonical_authority))[0:4] (SPEC
|
|
20
|
-
* The address fingerprint
|
|
19
|
+
* IQA_ROUTE = SHA-256(ASCII(canonical_authority))[0:4] (SPEC sec. 3).
|
|
20
|
+
* The address fingerprint -- not the subject hash (which hints the AID).
|
|
21
21
|
*/
|
|
22
22
|
export declare function deriveRoute(authority: string): Uint8Array;
|
|
23
23
|
export declare function deriveRouteHex(authority: string): string;
|
|
@@ -32,7 +32,7 @@ export interface ParsedUri {
|
|
|
32
32
|
authority: string;
|
|
33
33
|
action: string | null;
|
|
34
34
|
action_omitted: boolean;
|
|
35
|
-
/** RFC-009
|
|
35
|
+
/** RFC-009 sec. 11.3 -- true for omitted and `verify`, false for audit/attest/revoke. */
|
|
36
36
|
action_safe: boolean;
|
|
37
37
|
canonical_uri: string;
|
|
38
38
|
route_hex: string;
|
package/src/iqa-uri.mjs
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
// iqa:// URI codec
|
|
1
|
+
// iqa:// URI codec -- validate, canonicalise, derive (RFC-009 sec. 10/sec. 11).
|
|
2
2
|
//
|
|
3
3
|
// Authority boundaries:
|
|
4
|
-
// Grammar ......... RFC-009
|
|
5
|
-
// Closed sets .....
|
|
6
|
-
// Case discipline .
|
|
7
|
-
// Deref safety ....
|
|
8
|
-
// Route derivation SPEC/IQA-URI-ATTEST-v1.2.6.md
|
|
4
|
+
// Grammar ......... RFC-009 sec. 10.2 ABNF (sole authority)
|
|
5
|
+
// Closed sets ..... sec. 10.1 (organ and action are CLOSED sets)
|
|
6
|
+
// Case discipline . sec. 10.3 (lowercase US-ASCII only) + sec. 12 #8 (fail closed)
|
|
7
|
+
// Deref safety .... sec. 11.3 (action safety classes)
|
|
8
|
+
// Route derivation SPEC/IQA-URI-ATTEST-v1.2.6.md sec. 3 (IQA_ROUTE)
|
|
9
9
|
//
|
|
10
|
-
// Zero dependencies. Node 18+. No build step
|
|
10
|
+
// Zero dependencies. Node 18+. No build step -- the published src/ is what runs.
|
|
11
11
|
|
|
12
12
|
import { createHash } from 'node:crypto';
|
|
13
13
|
|
|
14
14
|
export const SCHEME = 'iqa';
|
|
15
15
|
|
|
16
|
-
/** RFC-009
|
|
16
|
+
/** RFC-009 sec. 10.1 -- organ is a CLOSED set (RFC-009-A/-B/-C). */
|
|
17
17
|
export const ORGANS = Object.freeze(['forge', 'tss', 'gateway']);
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* RFC-009
|
|
20
|
+
* RFC-009 sec. 10.1 -- action is a CLOSED set. This is the deliberate difference
|
|
21
21
|
* from `rttp`, whose verb set is open: here anything outside the four verbs
|
|
22
22
|
* is rejected.
|
|
23
23
|
*/
|
|
24
24
|
export const ACTIONS = Object.freeze(['verify', 'audit', 'attest', 'revoke']);
|
|
25
25
|
|
|
26
|
-
/** RFC-009-C
|
|
26
|
+
/** RFC-009-C sec. 3 -- standings an Organ may render (closed set, lowercase). */
|
|
27
27
|
export const STANDINGS = Object.freeze(['ghost', 'probation', 'radiant', 'genesis']);
|
|
28
28
|
|
|
29
|
-
/** RFC-009
|
|
29
|
+
/** RFC-009 sec. 10.2 -- hash-subject lengths: routing short form / 128-bit / 256-bit. */
|
|
30
30
|
export const HASH_LENGTHS = Object.freeze([8, 32, 64]);
|
|
31
31
|
|
|
32
|
-
const SAFE = new Set([null, 'verify']); //
|
|
32
|
+
const SAFE = new Set([null, 'verify']); // sec. 11.3: omitted = standing read; verify = read-only
|
|
33
33
|
const LOWHEX = new Set('0123456789abcdef');
|
|
34
34
|
const NAME_CHARS = new Set('abcdefghijklmnopqrstuvwxyz0123456789-');
|
|
35
35
|
const ALLOWED = new Set([...LOWHEX, ...NAME_CHARS, '.', '/']);
|
|
@@ -44,17 +44,17 @@ export class IqaUriError extends Error {
|
|
|
44
44
|
|
|
45
45
|
const fail = (uri, reason) => new IqaUriError(`${JSON.stringify(uri)}: ${reason}`);
|
|
46
46
|
|
|
47
|
-
/**
|
|
47
|
+
/** sec. 10.2 action test -- true only for the closed four-verb set. */
|
|
48
48
|
export function isValidAction(value) {
|
|
49
49
|
return ACTIONS.includes(value);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* IQA_ROUTE = SHA-256(ASCII(canonical_authority))[0:4] (SPEC
|
|
53
|
+
* IQA_ROUTE = SHA-256(ASCII(canonical_authority))[0:4] (SPEC sec. 3, decision D1).
|
|
54
54
|
*
|
|
55
|
-
* Pure computation, DNS-free (RFC-009
|
|
55
|
+
* Pure computation, DNS-free (RFC-009 sec. 12 #9). Mirrors the RTTP ROUTE_SHARD
|
|
56
56
|
* rule so both schemes of the same addressing family derive the same way.
|
|
57
|
-
* NOTE: this is the *address* fingerprint
|
|
57
|
+
* NOTE: this is the *address* fingerprint -- NOT the subject hash, which is a
|
|
58
58
|
* routing hint for the *AID*. Different numbers about different things.
|
|
59
59
|
*/
|
|
60
60
|
export function deriveRoute(authority) {
|
|
@@ -71,7 +71,7 @@ export function deriveRouteHex(authority) {
|
|
|
71
71
|
* Validate, canonicalise and derive. Throws IqaUriError on any violation.
|
|
72
72
|
*
|
|
73
73
|
* Returned fields are snake_case ON PURPOSE so this output and the Python
|
|
74
|
-
* package's output compare directly against the published vectors
|
|
74
|
+
* package's output compare directly against the published vectors -- field
|
|
75
75
|
* for field, byte for byte.
|
|
76
76
|
*/
|
|
77
77
|
export function parse(uri) {
|
|
@@ -80,20 +80,20 @@ export function parse(uri) {
|
|
|
80
80
|
const prefix = SCHEME + '://';
|
|
81
81
|
if (!uri.startsWith(prefix)) {
|
|
82
82
|
throw fail(uri, `scheme must be exactly ${JSON.stringify(prefix)} - no other `
|
|
83
|
-
+ 'scheme, no case variant, no fallback (RFC-009
|
|
83
|
+
+ 'scheme, no case variant, no fallback (RFC-009 sec. 10.3, sec. 12 #8)');
|
|
84
84
|
}
|
|
85
85
|
const rest = uri.slice(prefix.length);
|
|
86
86
|
|
|
87
|
-
// Charset gate: lowercase US-ASCII only (
|
|
87
|
+
// Charset gate: lowercase US-ASCII only (sec. 10.3). One scan rejects uppercase
|
|
88
88
|
// anywhere, userinfo '@', port ':', query '?', fragment '#', whitespace and
|
|
89
89
|
// every other foreign byte.
|
|
90
90
|
for (const ch of rest) {
|
|
91
91
|
if (!ALLOWED.has(ch)) {
|
|
92
92
|
if (FORBIDDEN.has(ch)) {
|
|
93
|
-
throw fail(uri, `${JSON.stringify(ch)} is not defined by the scheme (RFC-009
|
|
93
|
+
throw fail(uri, `${JSON.stringify(ch)} is not defined by the scheme (RFC-009 sec. 10.3)`);
|
|
94
94
|
}
|
|
95
95
|
if (/\s/.test(ch)) {
|
|
96
|
-
throw fail(uri, 'whitespace is not allowed (RFC-009
|
|
96
|
+
throw fail(uri, 'whitespace is not allowed (RFC-009 sec. 10.3 lowercase US-ASCII only)');
|
|
97
97
|
}
|
|
98
98
|
throw fail(uri, `character ${JSON.stringify(ch)} is outside the allowed set `
|
|
99
99
|
+ "(lowercase US-ASCII, digits, '-', '.', '/')");
|
|
@@ -109,10 +109,10 @@ export function parse(uri) {
|
|
|
109
109
|
action = rest.slice(slash + 1);
|
|
110
110
|
if (action === '') {
|
|
111
111
|
throw fail(uri, "trailing '/' with empty action - action is a closed-set "
|
|
112
|
-
+ 'verb, not omissible by empty string (
|
|
112
|
+
+ 'verb, not omissible by empty string (sec. 10.2)');
|
|
113
113
|
}
|
|
114
114
|
if (action.includes('/')) {
|
|
115
|
-
throw fail(uri, "second path segment - path is a single '/<action>' (
|
|
115
|
+
throw fail(uri, "second path segment - path is a single '/<action>' (sec. 10.2)");
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -120,11 +120,11 @@ export function parse(uri) {
|
|
|
120
120
|
const segments = authority.split('.');
|
|
121
121
|
if (segments.length !== 3) {
|
|
122
122
|
throw fail(uri, `authority has ${segments.length} segment(s), must be `
|
|
123
|
-
+ '<subject>.<organ>.<root> (RFC-009
|
|
123
|
+
+ '<subject>.<organ>.<root> (RFC-009 sec. 10.2)');
|
|
124
124
|
}
|
|
125
125
|
const [subject, organ, root] = segments;
|
|
126
126
|
if (subject === '' || root === '') {
|
|
127
|
-
throw fail(uri, 'authority segments must be non-empty (RFC-009
|
|
127
|
+
throw fail(uri, 'authority segments must be non-empty (RFC-009 sec. 10.2)');
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
// Subject: hash form (8/32/64 lowercase hex) or name form. Ordered choice
|
|
@@ -138,16 +138,16 @@ export function parse(uri) {
|
|
|
138
138
|
subjectHex = subject;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
// Organ
|
|
141
|
+
// Organ -- CLOSED set, case-sensitive (sec. 10.1). 'Forge', 'forgery', '' all fail.
|
|
142
142
|
if (!ORGANS.includes(organ)) {
|
|
143
143
|
throw fail(uri, `organ ${JSON.stringify(organ)} is outside the closed set `
|
|
144
|
-
+ `[${ORGANS.join(', ')}] (RFC-009
|
|
144
|
+
+ `[${ORGANS.join(', ')}] (RFC-009 sec. 10.1/sec. 10.3)`);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
// Action
|
|
147
|
+
// Action -- CLOSED set (sec. 10.1). The deliberate difference from rttp.
|
|
148
148
|
if (action !== null && !ACTIONS.includes(action)) {
|
|
149
149
|
throw fail(uri, `action ${JSON.stringify(action)} is outside the closed set `
|
|
150
|
-
+ `[${ACTIONS.join(', ')}] (RFC-009
|
|
150
|
+
+ `[${ACTIONS.join(', ')}] (RFC-009 sec. 10.1)`);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
const canonicalUri = SCHEME + '://' + authority + (action ? '/' + action : '');
|
|
@@ -162,8 +162,8 @@ export function parse(uri) {
|
|
|
162
162
|
authority,
|
|
163
163
|
action,
|
|
164
164
|
action_omitted: action === null,
|
|
165
|
-
action_safe: SAFE.has(action), //
|
|
165
|
+
action_safe: SAFE.has(action), // sec. 11.3
|
|
166
166
|
canonical_uri: canonicalUri,
|
|
167
|
-
route_hex: deriveRouteHex(authority), // SPEC
|
|
167
|
+
route_hex: deriveRouteHex(authority), // SPEC sec. 3
|
|
168
168
|
};
|
|
169
169
|
}
|
package/src/vectors.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"spec": "IQA-URI-ATTEST-v1.2.6",
|
|
3
3
|
"applies_to": [
|
|
4
|
-
"RFC-009
|
|
5
|
-
"RFC-009
|
|
4
|
+
"RFC-009 sec. 10",
|
|
5
|
+
"RFC-009 sec. 11"
|
|
6
6
|
],
|
|
7
7
|
"generated_by": "SPEC/tools/iqa_conformance.py",
|
|
8
8
|
"note": "Generated file - do not edit by hand. Change the cases in iqa_conformance.py instead.",
|
|
@@ -135,77 +135,77 @@
|
|
|
135
135
|
"action_omitted": false,
|
|
136
136
|
"action_safe": true,
|
|
137
137
|
"route_hex": "acee0323",
|
|
138
|
-
"note": "note - 9 hex digits is not a hash length (8/32/64), so the subject parses as a name-subject per the ABNF ordered choice (RFC-009
|
|
138
|
+
"note": "note - 9 hex digits is not a hash length (8/32/64), so the subject parses as a name-subject per the ABNF ordered choice (RFC-009 sec. 10.2)"
|
|
139
139
|
}
|
|
140
140
|
],
|
|
141
141
|
"negative_uris": [
|
|
142
142
|
{
|
|
143
143
|
"uri": "iqa://3f9a1b2c.gateway.iqa/AUDIT",
|
|
144
|
-
"reason": "uppercase action - lowercase only (RFC-009
|
|
144
|
+
"reason": "uppercase action - lowercase only (RFC-009 sec. 10.3)"
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
"uri": "iqa://3F9A1B2C.gateway.iqa/audit",
|
|
148
|
-
"reason": "uppercase hex subject - lowercase only (RFC-009
|
|
148
|
+
"reason": "uppercase hex subject - lowercase only (RFC-009 sec. 10.3)"
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
"uri": "IQA://3f9a1b2c.gateway.iqa/audit",
|
|
152
|
-
"reason": "uppercase scheme prefix - canonical form is lowercase (RFC-009
|
|
152
|
+
"reason": "uppercase scheme prefix - canonical form is lowercase (RFC-009 sec. 10.3)"
|
|
153
153
|
},
|
|
154
154
|
{
|
|
155
155
|
"uri": "iqas://3f9a1b2c.gateway.iqa/audit",
|
|
156
|
-
"reason": "no iqas variant exists and no fallback is defined - fail closed (RFC-009
|
|
156
|
+
"reason": "no iqas variant exists and no fallback is defined - fail closed (RFC-009 sec. 12 #8)"
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"uri": "iqa://subject@iqa.org",
|
|
160
|
-
"reason": "userinfo is not defined (RFC-009
|
|
160
|
+
"reason": "userinfo is not defined (RFC-009 sec. 10.3)"
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
"uri": "iqa://iqa.org/RFC-009/",
|
|
164
|
-
"reason": "no organ, second path segment - this is an https matter (RFC-009
|
|
164
|
+
"reason": "no organ, second path segment - this is an https matter (RFC-009 sec. 10.3)"
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
"uri": "iqa://3f9a1b2c.forgery.iqa",
|
|
168
|
-
"reason": "organ is a closed set (RFC-009
|
|
168
|
+
"reason": "organ is a closed set (RFC-009 sec. 10.1/sec. 10.3)"
|
|
169
169
|
},
|
|
170
170
|
{
|
|
171
171
|
"uri": "iqa://3f9a1b2c.gateway.iqa/pulse",
|
|
172
|
-
"reason": "action is a closed set - 'pulse' is not one of the four verbs (RFC-009
|
|
172
|
+
"reason": "action is a closed set - 'pulse' is not one of the four verbs (RFC-009 sec. 10.1)"
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
175
|
"uri": "iqa://3f9a1b2c.Gateway.iqa/verify",
|
|
176
|
-
"reason": "organ is case-sensitive within the closed set (RFC-009
|
|
176
|
+
"reason": "organ is case-sensitive within the closed set (RFC-009 sec. 10.3)"
|
|
177
177
|
},
|
|
178
178
|
{
|
|
179
179
|
"uri": "iqa://3f9a1b2c.gateway.iqa/audit?x=1",
|
|
180
|
-
"reason": "query is not defined (RFC-009
|
|
180
|
+
"reason": "query is not defined (RFC-009 sec. 10.3)"
|
|
181
181
|
},
|
|
182
182
|
{
|
|
183
183
|
"uri": "iqa://3f9a1b2c.gateway.iqa/audit#f",
|
|
184
|
-
"reason": "fragment is not defined (RFC-009
|
|
184
|
+
"reason": "fragment is not defined (RFC-009 sec. 10.3)"
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
187
|
"uri": "iqa://3f9a1b2c.gateway.iqa:443/audit",
|
|
188
|
-
"reason": "port is not defined (RFC-009
|
|
188
|
+
"reason": "port is not defined (RFC-009 sec. 10.3)"
|
|
189
189
|
},
|
|
190
190
|
{
|
|
191
191
|
"uri": "iqa://3f9a1b2c.gateway.iqa/a/b",
|
|
192
|
-
"reason": "second path segment - path is a single '/<action>' (RFC-009
|
|
192
|
+
"reason": "second path segment - path is a single '/<action>' (RFC-009 sec. 10.2)"
|
|
193
193
|
},
|
|
194
194
|
{
|
|
195
195
|
"uri": "iqa://3f9a1b2c.gateway.iqa/",
|
|
196
|
-
"reason": "trailing '/' with empty action - the closed set has no empty verb (RFC-009
|
|
196
|
+
"reason": "trailing '/' with empty action - the closed set has no empty verb (RFC-009 sec. 10.2)"
|
|
197
197
|
},
|
|
198
198
|
{
|
|
199
199
|
"uri": "iqa://a.b",
|
|
200
|
-
"reason": "authority has two segments, must be <subject>.<organ>.<root> (RFC-009
|
|
200
|
+
"reason": "authority has two segments, must be <subject>.<organ>.<root> (RFC-009 sec. 10.2)"
|
|
201
201
|
},
|
|
202
202
|
{
|
|
203
203
|
"uri": "iqa://a.b.c.d/audit",
|
|
204
|
-
"reason": "authority has four segments, must be <subject>.<organ>.<root> (RFC-009
|
|
204
|
+
"reason": "authority has four segments, must be <subject>.<organ>.<root> (RFC-009 sec. 10.2)"
|
|
205
205
|
},
|
|
206
206
|
{
|
|
207
207
|
"uri": "https://3f9a1b2c.gateway.iqa/audit",
|
|
208
|
-
"reason": "not an iqa scheme - there is no fallback (RFC-009
|
|
208
|
+
"reason": "not an iqa scheme - there is no fallback (RFC-009 sec. 10.3, sec. 12 #8)"
|
|
209
209
|
},
|
|
210
210
|
{
|
|
211
211
|
"uri": "iqa://3f9a1b2c.gateway.iqa/audit ",
|
|
@@ -216,27 +216,27 @@
|
|
|
216
216
|
{
|
|
217
217
|
"action": null,
|
|
218
218
|
"safe": true,
|
|
219
|
-
"why": "omitted - standing read (RFC-009
|
|
219
|
+
"why": "omitted - standing read (RFC-009 sec. 11.1/sec. 11.3)"
|
|
220
220
|
},
|
|
221
221
|
{
|
|
222
222
|
"action": "verify",
|
|
223
223
|
"safe": true,
|
|
224
|
-
"why": "read-only comparison, no state written (RFC-009
|
|
224
|
+
"why": "read-only comparison, no state written (RFC-009 sec. 11.3)"
|
|
225
225
|
},
|
|
226
226
|
{
|
|
227
227
|
"action": "audit",
|
|
228
228
|
"safe": false,
|
|
229
|
-
"why": "a failing measurement triggers Authority Ischemia (RFC-009
|
|
229
|
+
"why": "a failing measurement triggers Authority Ischemia (RFC-009 sec. 11.3/sec. 11.4)"
|
|
230
230
|
},
|
|
231
231
|
{
|
|
232
232
|
"action": "attest",
|
|
233
233
|
"safe": false,
|
|
234
|
-
"why": "state transition - Seal issuance (RFC-009
|
|
234
|
+
"why": "state transition - Seal issuance (RFC-009 sec. 11.3)"
|
|
235
235
|
},
|
|
236
236
|
{
|
|
237
237
|
"action": "revoke",
|
|
238
238
|
"safe": false,
|
|
239
|
-
"why": "state transition - standing withdrawal (RFC-009
|
|
239
|
+
"why": "state transition - standing withdrawal (RFC-009 sec. 11.3)"
|
|
240
240
|
}
|
|
241
241
|
],
|
|
242
242
|
"attest_envelope_vector": {
|