@aicent/iqa 1.2.8 → 1.3.2
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 +298 -258
- package/bin/iqa.mjs +59 -59
- package/package.json +9 -2
- package/src/ae128-vectors-v1.json +114 -0
- package/src/conformance.d.mts +29 -29
- package/src/conformance.mjs +181 -181
- package/src/envelope.d.mts +103 -0
- package/src/envelope.mjs +309 -0
- package/src/iqa-uri.d.mts +42 -42
- package/src/iqa-uri.mjs +169 -169
- package/src/vectors.json +26 -26
package/src/envelope.mjs
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// AE-128 -- the fixed 128-byte attestation envelope (layout: draft-02 sec. 7.2).
|
|
2
|
+
//
|
|
3
|
+
// Authority boundaries:
|
|
4
|
+
// Layout .......... draft-02 sec. 7.2 (normative layout)
|
|
5
|
+
// Canonical rule .. draft sec. 3 (HMAC over the full frame, attestation zeroed)
|
|
6
|
+
// Verification .... draft sec. 4 (fail-closed, constant-time compare)
|
|
7
|
+
// Standing codes .. AICENT-009 sec. 11.1.1 closed set (0x00..0x03)
|
|
8
|
+
//
|
|
9
|
+
// Relationship to the RTTP pulse frame: the pulse frame is the QUESTION
|
|
10
|
+
// carrier (rttp side, magic "RTTP", shard at 0x36); AE-128 is the ANSWER
|
|
11
|
+
// carrier (iqa side, magic "IQAE"). The two formats do not parse each other
|
|
12
|
+
// and MUST NOT be conflated. The intent_shard at offset 16 is the same
|
|
13
|
+
// SHA-256[0:16] derivation the pulse frame carries at 0x36 -- one subject,
|
|
14
|
+
// one derived address, two carriers. By design, not a compatibility claim.
|
|
15
|
+
//
|
|
16
|
+
// Zero dependencies. Node 18+. No build step -- the published src/ is what runs.
|
|
17
|
+
|
|
18
|
+
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
19
|
+
|
|
20
|
+
/** Frame size in octets -- fixed; a receiver MUST reject any other length. */
|
|
21
|
+
export const SIZE = 128;
|
|
22
|
+
/** Magic at offset 0: 'IQAE' (IQA Envelope) -- scheme-anchored, four printable ASCII bytes. */
|
|
23
|
+
export const MAGIC = 'IQAE';
|
|
24
|
+
/** The only frame version this module knows. Unknown versions fail closed. */
|
|
25
|
+
export const VERSION = 1;
|
|
26
|
+
/** The only algorithm byte defined in version 1. */
|
|
27
|
+
export const ALGORITHM_HMAC_SHA256 = 0x01;
|
|
28
|
+
|
|
29
|
+
// Fixed offsets (zero-based) -- the whole point of the format.
|
|
30
|
+
export const OFF_MAGIC = 0;
|
|
31
|
+
export const OFF_VERSION = 4;
|
|
32
|
+
export const OFF_ALGORITHM = 5;
|
|
33
|
+
export const OFF_FLAGS = 6; // 2, u16 BE, unassigned in version 1
|
|
34
|
+
export const OFF_LEASE = 8; // 8, u64 BE Unix seconds
|
|
35
|
+
export const OFF_SHARD = 16; // 16, ROUTE_SHARD of the subject
|
|
36
|
+
export const OFF_ATTESTATION = 32; // 32, HMAC over the whole frame, zeroed here
|
|
37
|
+
export const OFF_ORGAN = 64; // 8, US-ASCII organ tag, right-padded 0x00
|
|
38
|
+
export const OFF_NONCE = 72; // 8, u64 BE, unique per verdict
|
|
39
|
+
export const OFF_STANDING = 80; // 1, closed-set standing code (draft v0.3)
|
|
40
|
+
export const OFF_RESERVED = 81; // 15, MUST be all zeros
|
|
41
|
+
export const OFF_EXTENSION = 96; // 32, MUST be all zeros in version 1
|
|
42
|
+
|
|
43
|
+
/** Standing closed set per AICENT-009 sec. 11.1.1 -- wire codes and names. */
|
|
44
|
+
export const STANDING_CODES = Object.freeze({ ghost: 0x00, probation: 0x01, radiant: 0x02, genesis: 0x03 });
|
|
45
|
+
/** The reverse map: wire code -> lowercase name. */
|
|
46
|
+
export const STANDING_NAMES = Object.freeze(Object.fromEntries(
|
|
47
|
+
Object.entries(STANDING_CODES).map(([name, code]) => [code, name])));
|
|
48
|
+
|
|
49
|
+
const ORGAN_LEN = 8;
|
|
50
|
+
const SHARD_LEN = 16;
|
|
51
|
+
const ATTESTATION_LEN = 32;
|
|
52
|
+
const MAGIC_BYTES = Buffer.from(MAGIC, 'ascii');
|
|
53
|
+
|
|
54
|
+
export class EnvelopeError extends Error {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'EnvelopeError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fail = (reason) => new EnvelopeError('AE-128: ' + reason);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* ROUTE_SHARD = SHA-256(ASCII(canonical_authority))[0:16] (I-D sec. 4.4).
|
|
65
|
+
*
|
|
66
|
+
* The same derivation the rttp side performs for its pulse frame (the 16
|
|
67
|
+
* octets at offset 0x36). Pure computation: no registry, no resolver, no
|
|
68
|
+
* network. NOTE: this is NOT deriveRoute() -- that is the 4-octet IQA_ROUTE
|
|
69
|
+
* address fingerprint. Different numbers about different things.
|
|
70
|
+
*/
|
|
71
|
+
export function deriveIntentShard(canonicalAuthority) {
|
|
72
|
+
if (!canonicalAuthority) throw fail('deriveIntentShard: authority is empty');
|
|
73
|
+
try {
|
|
74
|
+
return createHash('sha256').update(canonicalAuthority, 'ascii').digest().subarray(0, SHARD_LEN);
|
|
75
|
+
} catch {
|
|
76
|
+
throw fail('deriveIntentShard: authority must be US-ASCII');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The same 16 octets, as 32 lowercase hex characters. */
|
|
81
|
+
export function intentShardHex(canonicalAuthority) {
|
|
82
|
+
return deriveIntentShard(canonicalAuthority).toString('hex');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveStanding(standing) {
|
|
86
|
+
if (typeof standing === 'number' && Number.isInteger(standing)) {
|
|
87
|
+
if (standing in STANDING_NAMES) return standing;
|
|
88
|
+
throw fail(`standing 0x${standing.toString(16).padStart(2, '0')} is outside the closed set`);
|
|
89
|
+
}
|
|
90
|
+
if (typeof standing === 'string') {
|
|
91
|
+
if (standing in STANDING_CODES) return STANDING_CODES[standing];
|
|
92
|
+
throw fail(`standing ${JSON.stringify(standing)} is outside the closed set`);
|
|
93
|
+
}
|
|
94
|
+
throw fail(`standing must be a code or a name, got ${typeof standing}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveShard(intentShard) {
|
|
98
|
+
let shard;
|
|
99
|
+
if (typeof intentShard === 'string') {
|
|
100
|
+
if (!/^[0-9a-fA-F]+$/.test(intentShard) || intentShard.length % 2 !== 0) {
|
|
101
|
+
throw fail('intent_shard hex is not valid hex');
|
|
102
|
+
}
|
|
103
|
+
shard = Buffer.from(intentShard, 'hex');
|
|
104
|
+
} else if (intentShard instanceof Uint8Array) {
|
|
105
|
+
shard = Buffer.from(intentShard);
|
|
106
|
+
} else {
|
|
107
|
+
throw fail(`intent_shard must be bytes or hex, got ${typeof intentShard}`);
|
|
108
|
+
}
|
|
109
|
+
if (shard.length !== SHARD_LEN) {
|
|
110
|
+
throw fail(`intent_shard must be exactly ${SHARD_LEN} octets, got ${shard.length}`);
|
|
111
|
+
}
|
|
112
|
+
return shard;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveOrganTag(organ) {
|
|
116
|
+
let tag;
|
|
117
|
+
if (typeof organ === 'string') {
|
|
118
|
+
tag = Buffer.from(organ, 'ascii');
|
|
119
|
+
if (tag.length !== organ.length) throw fail('organ tag must be US-ASCII');
|
|
120
|
+
} else if (organ instanceof Uint8Array) {
|
|
121
|
+
tag = Buffer.from(organ);
|
|
122
|
+
} else {
|
|
123
|
+
throw fail(`organ must be a string or bytes, got ${typeof organ}`);
|
|
124
|
+
}
|
|
125
|
+
if (tag.length < 1 || tag.length > ORGAN_LEN) {
|
|
126
|
+
throw fail(`organ tag must be 1..${ORGAN_LEN} octets, got ${tag.length}`);
|
|
127
|
+
}
|
|
128
|
+
for (const b of tag) {
|
|
129
|
+
if (b < 0x20 || b > 0x7e) throw fail('organ tag must be printable US-ASCII');
|
|
130
|
+
}
|
|
131
|
+
return Buffer.concat([tag, Buffer.alloc(ORGAN_LEN - tag.length)]);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveU64(value, label) {
|
|
135
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
136
|
+
throw fail(`${label} must be an integer`);
|
|
137
|
+
}
|
|
138
|
+
if (value < 0 || value > Number.MAX_SAFE_INTEGER) {
|
|
139
|
+
throw fail(`${label} ${value} is outside the safe unsigned range`);
|
|
140
|
+
}
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build one AE-128 frame (draft sec. 2 + sec. 3). Returns 128 bytes.
|
|
146
|
+
*
|
|
147
|
+
* The attestation is HMAC-SHA256 over the ENTIRE frame with the attestation
|
|
148
|
+
* field itself zeroed -- organ tag, nonce, lease and standing are all
|
|
149
|
+
* authenticated: a rewrite of any of them cannot survive verification.
|
|
150
|
+
*/
|
|
151
|
+
export function encode({ key, intent_shard, organ, standing, lease_expiry, nonce, flags = 0 }) {
|
|
152
|
+
if (!(key instanceof Uint8Array) || key.length === 0) {
|
|
153
|
+
throw fail('key must be non-empty bytes (organ key material)');
|
|
154
|
+
}
|
|
155
|
+
if (flags !== 0) {
|
|
156
|
+
throw fail(`flags must be 0x0000 in version 1 (unassigned), got 0x${flags.toString(16).padStart(4, '0')}`);
|
|
157
|
+
}
|
|
158
|
+
const shard = resolveShard(intent_shard);
|
|
159
|
+
const tag = resolveOrganTag(organ);
|
|
160
|
+
const code = resolveStanding(standing);
|
|
161
|
+
const lease = resolveU64(lease_expiry, 'lease_expiry');
|
|
162
|
+
const nonce64 = resolveU64(nonce, 'nonce');
|
|
163
|
+
|
|
164
|
+
const frame = Buffer.alloc(SIZE);
|
|
165
|
+
Buffer.from(MAGIC_BYTES).copy(frame, OFF_MAGIC);
|
|
166
|
+
frame[OFF_VERSION] = VERSION;
|
|
167
|
+
frame[OFF_ALGORITHM] = ALGORITHM_HMAC_SHA256;
|
|
168
|
+
frame.writeUInt16BE(flags, OFF_FLAGS);
|
|
169
|
+
frame.writeBigUInt64BE(BigInt(lease), OFF_LEASE);
|
|
170
|
+
shard.copy(frame, OFF_SHARD);
|
|
171
|
+
tag.copy(frame, OFF_ORGAN);
|
|
172
|
+
frame.writeBigUInt64BE(BigInt(nonce64), OFF_NONCE);
|
|
173
|
+
frame[OFF_STANDING] = code;
|
|
174
|
+
// attestation (32..63), reserved (81..95) and extension (96..127) stay zero
|
|
175
|
+
|
|
176
|
+
const mac = createHmac('sha256', Buffer.from(key)).update(frame).digest();
|
|
177
|
+
mac.copy(frame, OFF_ATTESTATION);
|
|
178
|
+
return frame;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Structural decode -- fail closed on every rule of draft sec. 2/sec. 4.
|
|
183
|
+
* Returns a snake_case field dict (byte-comparable with the Python output).
|
|
184
|
+
* Throws EnvelopeError on any violation; malformed input is never normalised.
|
|
185
|
+
*/
|
|
186
|
+
export function parse(data) {
|
|
187
|
+
if (!(data instanceof Uint8Array)) {
|
|
188
|
+
throw fail(`frame must be bytes, got ${typeof data}`);
|
|
189
|
+
}
|
|
190
|
+
const frame = Buffer.from(data);
|
|
191
|
+
if (frame.length !== SIZE) {
|
|
192
|
+
throw fail(`frame must be exactly ${SIZE} octets, got ${frame.length}`);
|
|
193
|
+
}
|
|
194
|
+
if (!frame.subarray(OFF_MAGIC, OFF_MAGIC + 4).equals(MAGIC_BYTES)) {
|
|
195
|
+
throw fail(`bad magic ${JSON.stringify(frame.subarray(0, 4).toString('latin1'))} (expected 'IQAE')`);
|
|
196
|
+
}
|
|
197
|
+
if (frame[OFF_VERSION] !== VERSION) {
|
|
198
|
+
throw fail(`unknown version 0x${frame[OFF_VERSION].toString(16).padStart(2, '0')}`);
|
|
199
|
+
}
|
|
200
|
+
if (frame[OFF_ALGORITHM] !== ALGORITHM_HMAC_SHA256) {
|
|
201
|
+
throw fail(`unknown algorithm 0x${frame[OFF_ALGORITHM].toString(16).padStart(2, '0')}`);
|
|
202
|
+
}
|
|
203
|
+
const flags = frame.readUInt16BE(OFF_FLAGS);
|
|
204
|
+
if (flags !== 0) {
|
|
205
|
+
throw fail(`flags must be 0x0000 in version 1 (unassigned), got 0x${flags.toString(16).padStart(4, '0')}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const tag = frame.subarray(OFF_ORGAN, OFF_ORGAN + ORGAN_LEN);
|
|
209
|
+
let end = ORGAN_LEN;
|
|
210
|
+
while (end > 0 && tag[end - 1] === 0x00) end -= 1;
|
|
211
|
+
const stripped = tag.subarray(0, end);
|
|
212
|
+
if (stripped.length === 0) {
|
|
213
|
+
throw fail('organ tag must be printable US-ASCII (right-padded with 0x00)');
|
|
214
|
+
}
|
|
215
|
+
for (const b of stripped) {
|
|
216
|
+
if (b < 0x20 || b > 0x7e) throw fail('organ tag must be printable US-ASCII (right-padded with 0x00)');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const code = frame[OFF_STANDING];
|
|
220
|
+
if (!(code in STANDING_NAMES)) {
|
|
221
|
+
throw fail(`standing 0x${code.toString(16).padStart(2, '0')} is outside the closed set`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!frame.subarray(OFF_RESERVED, OFF_EXTENSION).every((b) => b === 0)) {
|
|
225
|
+
throw fail('reserved field (81..95) MUST be all zeros');
|
|
226
|
+
}
|
|
227
|
+
if (!frame.subarray(OFF_EXTENSION, SIZE).every((b) => b === 0)) {
|
|
228
|
+
throw fail('extension field (96..127) MUST be all zeros in version 1');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
version: frame[OFF_VERSION],
|
|
233
|
+
algorithm: frame[OFF_ALGORITHM],
|
|
234
|
+
flags,
|
|
235
|
+
lease_expiry: Number(frame.readBigUInt64BE(OFF_LEASE)),
|
|
236
|
+
intent_shard: frame.subarray(OFF_SHARD, OFF_SHARD + SHARD_LEN),
|
|
237
|
+
attestation: frame.subarray(OFF_ATTESTATION, OFF_ATTESTATION + ATTESTATION_LEN),
|
|
238
|
+
organ: stripped.toString('ascii'),
|
|
239
|
+
nonce: Number(frame.readBigUInt64BE(OFF_NONCE)),
|
|
240
|
+
standing: code,
|
|
241
|
+
standing_name: STANDING_NAMES[code],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Verify one AE-128 frame (draft sec. 4). Constant-time HMAC compare.
|
|
247
|
+
* Returns { ok, reason, frame }: on success frame is the parsed dict; on
|
|
248
|
+
* failure frame is null and reason says which rule rejected it. A failure is
|
|
249
|
+
* a RESULT, never an exception -- the caller decides.
|
|
250
|
+
*/
|
|
251
|
+
export function verify(data, key, { now = null, check_lease = true } = {}) {
|
|
252
|
+
let parsed;
|
|
253
|
+
try {
|
|
254
|
+
parsed = parse(data);
|
|
255
|
+
} catch (exc) {
|
|
256
|
+
return { ok: false, reason: exc.message, frame: null };
|
|
257
|
+
}
|
|
258
|
+
if (check_lease) {
|
|
259
|
+
const at = now === null ? Date.now() / 1000 : now;
|
|
260
|
+
if (typeof at !== 'number') {
|
|
261
|
+
return { ok: false, reason: `now must be a number, got ${typeof at}`, frame: null };
|
|
262
|
+
}
|
|
263
|
+
if (parsed.lease_expiry < at) {
|
|
264
|
+
return { ok: false, reason: `verdict expired (lease_expiry ${parsed.lease_expiry} < now ${Math.floor(at)})`, frame: null };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const scratch = Buffer.from(data);
|
|
268
|
+
scratch.fill(0, OFF_ATTESTATION, OFF_ATTESTATION + ATTESTATION_LEN);
|
|
269
|
+
const expected = createHmac('sha256', Buffer.from(key)).update(scratch).digest();
|
|
270
|
+
const given = Buffer.from(parsed.attestation);
|
|
271
|
+
if (!timingSafeEqual(expected, given)) {
|
|
272
|
+
return { ok: false, reason: 'attestation does not verify (HMAC mismatch)', frame: null };
|
|
273
|
+
}
|
|
274
|
+
return { ok: true, reason: 'ok', frame: parsed };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// The published vector (companion draft v0.3 sec. 6) -- every parameter is
|
|
279
|
+
// documented there, so any implementation MUST reproduce the frame byte for
|
|
280
|
+
// byte from the parameters alone. Generated by ae128-vector-1.py, not typed.
|
|
281
|
+
|
|
282
|
+
/** The merged I-D's own example authority (sec. 4.4). */
|
|
283
|
+
export const VECTOR_1_AUTHORITY = 'f3b2a1c4.pillar.example';
|
|
284
|
+
/** 32-byte US-ASCII test organ key (label retained from v0.1 for diff continuity). */
|
|
285
|
+
export const VECTOR_1_KEY = Buffer.from('PLSF-TEST-ORG-KEY-00012345678901', 'ascii');
|
|
286
|
+
/** 2027-01-01T00:00:00Z. */
|
|
287
|
+
export const VECTOR_1_LEASE = 1798761600;
|
|
288
|
+
export const VECTOR_1_STANDING = 'radiant';
|
|
289
|
+
export const VECTOR_1_ORGAN = 'ORG-TEST';
|
|
290
|
+
export const VECTOR_1_NONCE = 1;
|
|
291
|
+
/** The full 128-octet frame, as lowercase hex. */
|
|
292
|
+
export const VECTOR_1_FRAME_HEX =
|
|
293
|
+
'4951414501010000' + '000000006b36ec80' +
|
|
294
|
+
'557c8154d3780cb78cc5fa1bd72b331c' +
|
|
295
|
+
'4bd14b6ae7b00904986294a326a0f0255847184ff2c9185539d4863efb84596a' +
|
|
296
|
+
'4f52472d544553540000000000000001' +
|
|
297
|
+
'02' + '00'.repeat(47);
|
|
298
|
+
|
|
299
|
+
/** The published vector as bytes -- encode() of the documented parameters. */
|
|
300
|
+
export function vector1Frame() {
|
|
301
|
+
return encode({
|
|
302
|
+
key: VECTOR_1_KEY,
|
|
303
|
+
intent_shard: deriveIntentShard(VECTOR_1_AUTHORITY),
|
|
304
|
+
organ: VECTOR_1_ORGAN,
|
|
305
|
+
standing: VECTOR_1_STANDING,
|
|
306
|
+
lease_expiry: VECTOR_1_LEASE,
|
|
307
|
+
nonce: VECTOR_1_NONCE,
|
|
308
|
+
});
|
|
309
|
+
}
|
package/src/iqa-uri.d.mts
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* iqa --
|
|
3
|
-
* Zero dependencies. Field names are snake_case on purpose: they match the
|
|
4
|
-
* published conformance vectors (and the Python implementation) field for field.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export declare const SCHEME: 'iqa';
|
|
8
|
-
export declare const ORGANS: readonly ['forge', 'tss', 'gateway'];
|
|
9
|
-
export declare const ACTIONS: readonly ['verify', 'audit', 'attest', 'revoke'];
|
|
10
|
-
export declare const STANDINGS: readonly ['ghost', 'probation', 'radiant', 'genesis'];
|
|
11
|
-
export declare const HASH_LENGTHS: readonly [8, 32, 64];
|
|
12
|
-
|
|
13
|
-
export declare class IqaUriError extends Error {}
|
|
14
|
-
|
|
15
|
-
/** True only for the closed four-verb set (
|
|
16
|
-
export declare function isValidAction(value: string): boolean;
|
|
17
|
-
|
|
18
|
-
/**
|
|
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
|
-
*/
|
|
22
|
-
export declare function deriveRoute(authority: string): Uint8Array;
|
|
23
|
-
export declare function deriveRouteHex(authority: string): string;
|
|
24
|
-
|
|
25
|
-
export interface ParsedUri {
|
|
26
|
-
scheme: 'iqa';
|
|
27
|
-
subject: string;
|
|
28
|
-
subject_is_hash: boolean;
|
|
29
|
-
subject_hex: string | null;
|
|
30
|
-
organ: string;
|
|
31
|
-
root: string;
|
|
32
|
-
authority: string;
|
|
33
|
-
action: string | null;
|
|
34
|
-
action_omitted: boolean;
|
|
35
|
-
/**
|
|
36
|
-
action_safe: boolean;
|
|
37
|
-
canonical_uri: string;
|
|
38
|
-
route_hex: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Validate, canonicalise and derive. Throws IqaUriError on any violation. */
|
|
42
|
-
export declare function parse(uri: string): ParsedUri;
|
|
1
|
+
/**
|
|
2
|
+
* iqa -- AICENT-009 sec. 10/sec. 11 codec for JavaScript.
|
|
3
|
+
* Zero dependencies. Field names are snake_case on purpose: they match the
|
|
4
|
+
* published conformance vectors (and the Python implementation) field for field.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export declare const SCHEME: 'iqa';
|
|
8
|
+
export declare const ORGANS: readonly ['forge', 'tss', 'gateway'];
|
|
9
|
+
export declare const ACTIONS: readonly ['verify', 'audit', 'attest', 'revoke'];
|
|
10
|
+
export declare const STANDINGS: readonly ['ghost', 'probation', 'radiant', 'genesis'];
|
|
11
|
+
export declare const HASH_LENGTHS: readonly [8, 32, 64];
|
|
12
|
+
|
|
13
|
+
export declare class IqaUriError extends Error {}
|
|
14
|
+
|
|
15
|
+
/** True only for the closed four-verb set (AICENT-009 sec. 10.1). */
|
|
16
|
+
export declare function isValidAction(value: string): boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
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
|
+
*/
|
|
22
|
+
export declare function deriveRoute(authority: string): Uint8Array;
|
|
23
|
+
export declare function deriveRouteHex(authority: string): string;
|
|
24
|
+
|
|
25
|
+
export interface ParsedUri {
|
|
26
|
+
scheme: 'iqa';
|
|
27
|
+
subject: string;
|
|
28
|
+
subject_is_hash: boolean;
|
|
29
|
+
subject_hex: string | null;
|
|
30
|
+
organ: string;
|
|
31
|
+
root: string;
|
|
32
|
+
authority: string;
|
|
33
|
+
action: string | null;
|
|
34
|
+
action_omitted: boolean;
|
|
35
|
+
/** AICENT-009 sec. 11.3 -- true for omitted and `verify`, false for audit/attest/revoke. */
|
|
36
|
+
action_safe: boolean;
|
|
37
|
+
canonical_uri: string;
|
|
38
|
+
route_hex: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Validate, canonicalise and derive. Throws IqaUriError on any violation. */
|
|
42
|
+
export declare function parse(uri: string): ParsedUri;
|