@forgesworn/fold-kit 0.9.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,400 @@
1
+ import { hkdf } from '@noble/hashes/hkdf';
2
+ import { sha256 } from '@noble/hashes/sha2';
3
+ import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
4
+ import { base64, base64urlnopad } from '@scure/base';
5
+ import { nip44 } from 'nostr-tools';
6
+ import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
7
+ import { isSealPubkey } from './seal.js';
8
+ import { verifyEventUncached } from './verify.js';
9
+ const ID = 'kithmoot/v4/mls-invitation-id';
10
+ const REQUEST_KEY = 'kithmoot/v4/mls-request-key';
11
+ const ANSWER_KEY = 'kithmoot/v4/mls-answer-key';
12
+ const REQUEST = 'kithmoot/v4/mls-request';
13
+ const ANSWER = 'kithmoot/v4/mls-answer';
14
+ const ACCOUNT = 'kithmoot/v4/mls-joiner-account-proof';
15
+ const BROWSER = 'kithmoot/v4/mls-browser-device-proof';
16
+ const ENGINE = 'kithmoot/v4/mls-engine-device-proof';
17
+ const KEEPER_ACCOUNT = 'kithmoot/v4/mls-keeper-account-proof';
18
+ const KEEPER_DEVICE = 'kithmoot/v4/mls-keeper-device-proof';
19
+ export const MLS_ADMISSION_LABELS = Object.freeze([
20
+ ID, REQUEST_KEY, ANSWER_KEY, REQUEST, ANSWER, ACCOUNT, BROWSER, ENGINE,
21
+ KEEPER_ACCOUNT, KEEPER_DEVICE,
22
+ ]);
23
+ export const MLS_ADMISSION_PLAINTEXT_BYTES = 16_384;
24
+ export const MLS_ADMISSION_CIPHERTEXT_CHARACTERS = 21_936;
25
+ const HEX = /^[0-9a-f]{64}$/;
26
+ const SIG = /^[0-9a-f]{128}$/;
27
+ const DECIMAL = /^(0|[1-9][0-9]*)$/;
28
+ const utf8 = new TextEncoder();
29
+ const text = new TextDecoder('utf-8', { fatal: true });
30
+ const FAILURE = 'invalid MLS admission v4';
31
+ const requests = new WeakMap();
32
+ const answers = new WeakMap();
33
+ function check(ok) {
34
+ if (!ok)
35
+ throw new Error(FAILURE);
36
+ }
37
+ function integer(value) {
38
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
39
+ }
40
+ function hex(value) { return typeof value === 'string' && HEX.test(value); }
41
+ function pubkey(value) { return hex(value) && isSealPubkey(value); }
42
+ function time(value) { check(integer(value)); }
43
+ function digest(value) { return bytesToHex(sha256(utf8.encode(JSON.stringify(value)))); }
44
+ function same(a, b) { return JSON.stringify(a) === JSON.stringify(b); }
45
+ function array(value, length) {
46
+ check(Array.isArray(value) && value.length === length);
47
+ }
48
+ // Every wire-facing decoder accepts bounded strings, never caller event objects
49
+ // or proxies with verification markers/accessors. Scan depth before JSON.parse.
50
+ function parse(raw, max, canonical = true) {
51
+ check(typeof raw === 'string' && raw.length <= max && utf8.encode(raw).length <= max);
52
+ let depth = 0, quoted = false, escaped = false;
53
+ for (const c of raw) {
54
+ if (quoted) {
55
+ if (escaped)
56
+ escaped = false;
57
+ else if (c === '\\')
58
+ escaped = true;
59
+ else if (c === '"')
60
+ quoted = false;
61
+ }
62
+ else if (c === '"')
63
+ quoted = true;
64
+ else if (c === '[' || c === '{')
65
+ check(++depth <= 8);
66
+ else if (c === ']' || c === '}')
67
+ check(--depth >= 0);
68
+ }
69
+ check(depth === 0 && !quoted);
70
+ let value;
71
+ try {
72
+ value = JSON.parse(raw);
73
+ }
74
+ catch {
75
+ throw new Error(FAILURE);
76
+ }
77
+ if (canonical)
78
+ check(JSON.stringify(value) === raw);
79
+ return value;
80
+ }
81
+ function secret(foreign, action) {
82
+ check(foreign instanceof Uint8Array && foreign.length === 32);
83
+ const owned = new Uint8Array(foreign);
84
+ try {
85
+ getPublicKey(owned);
86
+ return action(owned);
87
+ }
88
+ finally {
89
+ owned.fill(0);
90
+ }
91
+ }
92
+ function invitation(fragment) {
93
+ check(typeof fragment === 'string' && fragment.length <= 1024 && /^[A-Za-z0-9_-]+$/.test(fragment));
94
+ const decoded = base64urlnopad.decode(fragment);
95
+ let bearer, transferred = false;
96
+ try {
97
+ check(base64urlnopad.encode(decoded) === fragment);
98
+ const value = parse(text.decode(decoded), 1024);
99
+ array(value, 3);
100
+ check(value[0] === 4 && typeof value[1] === 'string' && pubkey(value[2]));
101
+ bearer = base64urlnopad.decode(value[1]);
102
+ check(bearer.length === 32 && bearer.some(b => b !== 0) && base64urlnopad.encode(bearer) === value[1]);
103
+ const root = value[2];
104
+ const result = { root, bearer, id: bytesToHex(hkdf(sha256, bearer, hexToBytes(root), ID, 32)) };
105
+ transferred = true;
106
+ return result;
107
+ }
108
+ finally {
109
+ decoded.fill(0);
110
+ if (!transferred)
111
+ bearer?.fill(0);
112
+ }
113
+ }
114
+ /** Link fragment only; caller owns CSPRNG generation and secret lifetime. */
115
+ export function encodeMlsInvitationV4(root, bearer) {
116
+ check(pubkey(root) && bearer instanceof Uint8Array && bearer.length === 32 && bearer.some(b => b !== 0));
117
+ return base64urlnopad.encode(utf8.encode(JSON.stringify([4, base64urlnopad.encode(bearer), root])));
118
+ }
119
+ export function readMlsInvitationV4(fragment) {
120
+ const i = invitation(fragment);
121
+ return { root: i.root, bearer: i.bearer, invitationId: i.id };
122
+ }
123
+ function withKey(i, sk, peer, direction, action) {
124
+ const base = nip44.v2.utils.getConversationKey(sk, peer);
125
+ let key;
126
+ try {
127
+ key = hkdf(sha256, base, i.bearer, direction, 32);
128
+ return action(key);
129
+ }
130
+ finally {
131
+ base.fill(0);
132
+ key?.fill(0);
133
+ i.bearer.fill(0);
134
+ }
135
+ }
136
+ function eventFromArray(raw) {
137
+ array(raw, 7);
138
+ const [id, key, created_at, kind, tags, content, sig] = raw;
139
+ check(hex(id) && pubkey(key) && integer(created_at) && integer(kind) && typeof sig === 'string' && SIG.test(sig));
140
+ check(typeof content === 'string' && Array.isArray(tags) && tags.length <= 8);
141
+ for (const tag of tags)
142
+ check(Array.isArray(tag) && tag.length <= 4 && tag.every(s => typeof s === 'string' && s.length <= 256));
143
+ const event = { id, pubkey: key, created_at, kind, tags, content, sig };
144
+ check(verifyEventUncached(event));
145
+ return event;
146
+ }
147
+ function nested(raw) {
148
+ check(JSON.stringify(raw).length <= 4096 && utf8.encode(JSON.stringify(raw)).length <= 4096);
149
+ return eventFromArray(raw);
150
+ }
151
+ function eventArray(e) { return [e.id, e.pubkey, e.created_at, e.kind, e.tags, e.content, e.sig]; }
152
+ function outer(raw) {
153
+ const value = parse(raw, 32768);
154
+ check(typeof value === 'object' && value !== null && !Array.isArray(value));
155
+ const e = value;
156
+ const keys = ['id', 'pubkey', 'created_at', 'kind', 'tags', 'content', 'sig'];
157
+ check(Object.keys(e).length === keys.length && keys.every(k => Object.hasOwn(e, k)));
158
+ check(typeof e.content === 'string' && e.content.length === MLS_ADMISSION_CIPHERTEXT_CHARACTERS && /^[A-Za-z0-9+/]+={0,2}$/.test(e.content));
159
+ check(base64.encode(base64.decode(e.content)) === e.content);
160
+ return eventFromArray(keys.map(k => e[k]));
161
+ }
162
+ function credential(raw, account, device, expiry, now) {
163
+ const e = nested(raw);
164
+ check(e.kind === 20460 && e.pubkey === account && e.content === '' && e.created_at <= now + 30);
165
+ const tags = new Map();
166
+ for (const tag of e.tags) {
167
+ check(tag.length === 2 && ['d', 'scope', 'device', 'expiration', 'label', 'seal'].includes(tag[0]) && !tags.has(tag[0]));
168
+ tags.set(tag[0], tag[1]);
169
+ }
170
+ check(tags.get('d') === account && tags.get('scope') === 'person' && tags.get('device') === device && pubkey(device));
171
+ const expiration = tags.get('expiration');
172
+ check(expiration !== undefined && DECIMAL.test(expiration) && integer(Number(expiration)));
173
+ const end = Number(expiration);
174
+ check(end > now && end >= expiry && end - e.created_at <= 30 * 86400 && end - now <= 30 * 86400);
175
+ check(!tags.has('label') || tags.get('label').length <= 64);
176
+ check(!tags.has('seal') || pubkey(tags.get('seal')));
177
+ return e;
178
+ }
179
+ function checkNow(now) { check(integer(now) && now <= Number.MAX_SAFE_INTEGER - 7 * 86400 - 30); }
180
+ function requestCore(raw, i, now) {
181
+ checkNow(now);
182
+ array(raw, 17);
183
+ check(raw[0] === 4 && raw[1] === REQUEST && raw[2] === i.id && raw[3] === i.root && pubkey(raw[4]));
184
+ for (const index of [5, 6, 12])
185
+ check(hex(raw[index]));
186
+ for (const index of [7, 8, 9, 10])
187
+ check(pubkey(raw[index]));
188
+ check(typeof raw[13] === 'string' && DECIMAL.test(raw[13]) && raw[13].length <= 20 && BigInt(raw[13]) <= 0xffffffffffffffffn);
189
+ time(raw[14]);
190
+ time(raw[15]);
191
+ time(raw[16]);
192
+ check(raw[16] === raw[15] + 90 && now < raw[16] && raw[15] <= now + 30);
193
+ check(raw[14] > raw[16] && raw[14] - raw[15] <= 7 * 86400);
194
+ credential(raw[11], raw[7], raw[9], raw[14], now);
195
+ return raw;
196
+ }
197
+ function proofSpecs(core) {
198
+ const request = core[1] === REQUEST;
199
+ const keys = request ? [core[7], core[8], core[9]] : [core[15], core[16]];
200
+ const labels = request ? [ACCOUNT, BROWSER, ENGINE] : [KEEPER_ACCOUNT, KEEPER_DEVICE];
201
+ return keys.map((key, n) => ({ pubkey: key,
202
+ event: { kind: request ? 20466 : 20467, created_at: core[request ? 15 : 25],
203
+ tags: [['t', labels[n]]], content: JSON.stringify([4, digest(core)]) } }));
204
+ }
205
+ function checkProofs(raw, core) {
206
+ const specs = proofSpecs(core);
207
+ array(raw, specs.length);
208
+ for (let n = 0; n < specs.length; n++) {
209
+ const e = nested(raw[n]), s = specs[n];
210
+ check(e.pubkey === s.pubkey && e.kind === s.event.kind && e.created_at === s.event.created_at && same(e.tags, s.event.tags) && e.content === s.event.content);
211
+ }
212
+ return raw;
213
+ }
214
+ function wrapper(core, proofs) {
215
+ const empty = JSON.stringify([4, core, proofs, '']);
216
+ const length = utf8.encode(empty).length;
217
+ check(length <= MLS_ADMISSION_PLAINTEXT_BYTES);
218
+ const padded = JSON.stringify([4, core, proofs, '0'.repeat(MLS_ADMISSION_PLAINTEXT_BYTES - length)]);
219
+ check(utf8.encode(padded).length === MLS_ADMISSION_PLAINTEXT_BYTES);
220
+ return padded;
221
+ }
222
+ function unwrap(raw) {
223
+ check(utf8.encode(raw).length === MLS_ADMISSION_PLAINTEXT_BYTES);
224
+ const value = parse(raw, MLS_ADMISSION_PLAINTEXT_BYTES);
225
+ array(value, 4);
226
+ check(value[0] === 4 && Array.isArray(value[1]) && Array.isArray(value[2]) && typeof value[3] === 'string' && /^0*$/.test(value[3]));
227
+ check(wrapper(value[1], value[2]) === raw);
228
+ return { core: value[1], proofs: value[2] };
229
+ }
230
+ function requestTags(i, deadline) {
231
+ return [['d', i.id], ['p', i.root], ['expiration', String(deadline)]];
232
+ }
233
+ function requestOuter(e, core, i) {
234
+ check(e.kind === 20466 && e.pubkey === core[4] && e.created_at === core[15] && same(e.tags, requestTags(i, core[16])));
235
+ }
236
+ function retainRequest(handle, now) {
237
+ check(typeof handle === 'object' && handle !== null);
238
+ const value = requests.get(handle);
239
+ check(value);
240
+ const i = { root: value.root, id: value.invitationId };
241
+ requestCore(value.core, i, now);
242
+ requestOuter(value.event, value.core, i);
243
+ return value;
244
+ }
245
+ function answerCore(raw, request, now) {
246
+ checkNow(now);
247
+ check(Array.isArray(raw));
248
+ const q = request.core;
249
+ check(raw[0] === 4 && raw[1] === ANSWER && (raw[2] === 'allow' || raw[2] === 'deny'));
250
+ check(raw[3] === q[2] && raw[4] === q[3] && raw[5] === request.event.id && raw[6] === request.digest && raw[7] === q[5] && raw[8] === q[6]);
251
+ const allow = raw[2] === 'allow', created = allow ? 25 : 9, deadline = allow ? 26 : 10;
252
+ array(raw, allow ? 27 : 11);
253
+ time(raw[created]);
254
+ time(raw[deadline]);
255
+ check(raw[deadline] === q[16] && q[15] <= raw[created] && raw[created] < raw[deadline] && now < raw[deadline] && raw[created] <= now + 30);
256
+ if (allow) {
257
+ for (const [a, b] of [[9, 7], [10, 8], [11, 9], [12, 10], [13, 12], [14, 13]])
258
+ check(raw[a] === q[b]);
259
+ for (const index of [15, 16, 18])
260
+ check(pubkey(raw[index]));
261
+ for (const index of [19, 20, 21, 22])
262
+ check(hex(raw[index]));
263
+ check(raw[19] === q[12] && raw[20] === q[12]);
264
+ time(raw[23]);
265
+ time(raw[24]);
266
+ check(raw[23] > raw[26] && raw[23] <= q[14] && raw[23] <= raw[24] && raw[24] > now);
267
+ credential(raw[17], raw[15], raw[16], raw[23], now);
268
+ }
269
+ return raw;
270
+ }
271
+ function answerTags(request) {
272
+ return [['p', request.event.pubkey], ['e', request.event.id], ['expiration', String(request.core[16])]];
273
+ }
274
+ function answerOuter(e, core, request) {
275
+ check(e.kind === 20467 && e.pubkey === request.core[3] && e.created_at === core[core[2] === 'allow' ? 25 : 9] && same(e.tags, answerTags(request)));
276
+ }
277
+ /** Templates only. Account/device signers remain caller-owned and settle under
278
+ * the application's actual custody. Returned templates never confer authority.
279
+ * core is the exact canonical request array or, with request, Allow array. */
280
+ export function mlsAdmissionProofTemplatesV4(opts) {
281
+ const i = invitation(opts.fragment);
282
+ try {
283
+ const value = parse(opts.core, MLS_ADMISSION_PLAINTEXT_BYTES);
284
+ const core = opts.request ? answerCore(value, retainRequest(opts.request, opts.now), opts.now) : requestCore(value, i, opts.now);
285
+ if (opts.request) {
286
+ const request = retainRequest(opts.request, opts.now);
287
+ check(core[2] === 'allow' && i.root === request.root && i.id === request.invitationId);
288
+ }
289
+ return proofSpecs(core);
290
+ }
291
+ finally {
292
+ i.bearer.fill(0);
293
+ }
294
+ }
295
+ /** proofs is canonical JSON of the three seven-field signed proof arrays. */
296
+ export function encodeMlsAdmissionRequestV4(opts) {
297
+ const i = invitation(opts.fragment);
298
+ try {
299
+ const core = requestCore(parse(opts.core, MLS_ADMISSION_PLAINTEXT_BYTES), i, opts.now);
300
+ const proofs = checkProofs(parse(opts.proofs, MLS_ADMISSION_PLAINTEXT_BYTES), core);
301
+ const body = wrapper(core, proofs);
302
+ return secret(opts.requestSecretKey, sk => {
303
+ check(getPublicKey(sk) === core[4]);
304
+ const content = withKey(i, sk, i.root, REQUEST_KEY, key => nip44.v2.encrypt(body, key));
305
+ const e = finalizeEvent({ kind: 20466, created_at: core[15], tags: requestTags(i, core[16]), content }, sk);
306
+ check(content.length === MLS_ADMISSION_CIPHERTEXT_CHARACTERS);
307
+ return JSON.stringify({ id: e.id, pubkey: e.pubkey, created_at: e.created_at, kind: e.kind, tags: e.tags, content: e.content, sig: e.sig });
308
+ });
309
+ }
310
+ finally {
311
+ i.bearer.fill(0);
312
+ }
313
+ }
314
+ /** Either endpoint may decode its exact request; only this decoder issues the
315
+ * nominal result, after fresh outer/inner checks. Caller secrets are not kept. */
316
+ export function decodeMlsAdmissionRequestV4(opts) {
317
+ const i = invitation(opts.fragment);
318
+ try {
319
+ checkNow(opts.now);
320
+ const e = outer(opts.event);
321
+ check(e.kind === 20466 && (opts.side === 'root' || opts.side === 'joiner'));
322
+ check(integer(e.created_at + 90) && e.created_at <= opts.now + 30 && opts.now < e.created_at + 90 && same(e.tags, requestTags(i, e.created_at + 90)));
323
+ return secret(opts.localSecretKey, sk => {
324
+ check(getPublicKey(sk) === (opts.side === 'root' ? i.root : e.pubkey));
325
+ const body = withKey(i, sk, opts.side === 'root' ? e.pubkey : i.root, REQUEST_KEY, key => nip44.v2.decrypt(e.content, key));
326
+ const { core: raw, proofs } = unwrap(body);
327
+ const core = requestCore(raw, i, opts.now);
328
+ requestOuter(e, core, i);
329
+ checkProofs(proofs, core);
330
+ const handle = Object.freeze(Object.create(null));
331
+ requests.set(handle, { root: i.root, invitationId: i.id, core, digest: digest(core), event: e });
332
+ return handle;
333
+ });
334
+ }
335
+ finally {
336
+ i.bearer.fill(0);
337
+ }
338
+ }
339
+ /** Returned strings are copies of evidence, not a rehydration format. */
340
+ export function readMlsAdmissionRequestV4(handle) {
341
+ const value = requests.get(handle);
342
+ check(value);
343
+ return Object.freeze({ core: JSON.stringify(value.core), digest: value.digest, eventId: value.event.id });
344
+ }
345
+ /** Root correlation and signed keeper consent only. This encoder cannot attest
346
+ * current keeper role; actual vault/box custody and fresh checks stay external. */
347
+ export function encodeMlsAdmissionAnswerV4(opts) {
348
+ const request = retainRequest(opts.request, opts.now), i = invitation(opts.fragment);
349
+ try {
350
+ check(i.root === request.root && i.id === request.invitationId);
351
+ const core = answerCore(parse(opts.core, MLS_ADMISSION_PLAINTEXT_BYTES), request, opts.now);
352
+ const raw = parse(opts.proofs, MLS_ADMISSION_PLAINTEXT_BYTES);
353
+ const proofs = core[2] === 'allow' ? checkProofs(raw, core) : (array(raw, 0), raw);
354
+ const body = wrapper(core, proofs);
355
+ return secret(opts.rootSecretKey, sk => {
356
+ check(getPublicKey(sk) === i.root);
357
+ const content = withKey(i, sk, request.event.pubkey, ANSWER_KEY, key => nip44.v2.encrypt(body, key));
358
+ const e = finalizeEvent({ kind: 20467, created_at: core[core[2] === 'allow' ? 25 : 9], tags: answerTags(request), content }, sk);
359
+ check(content.length === MLS_ADMISSION_CIPHERTEXT_CHARACTERS);
360
+ return JSON.stringify({ id: e.id, pubkey: e.pubkey, created_at: e.created_at, kind: e.kind, tags: e.tags, content: e.content, sig: e.sig });
361
+ });
362
+ }
363
+ finally {
364
+ i.bearer.fill(0);
365
+ }
366
+ }
367
+ export function decodeMlsAdmissionAnswerV4(opts) {
368
+ const request = retainRequest(opts.request, opts.now), i = invitation(opts.fragment);
369
+ try {
370
+ check(i.root === request.root && i.id === request.invitationId);
371
+ const e = outer(opts.event);
372
+ check(e.kind === 20467 && e.pubkey === i.root && same(e.tags, answerTags(request)));
373
+ check(request.core[15] <= e.created_at && e.created_at < request.core[16] && e.created_at <= opts.now + 30);
374
+ return secret(opts.requestSecretKey, sk => {
375
+ check(getPublicKey(sk) === request.event.pubkey);
376
+ const body = withKey(i, sk, i.root, ANSWER_KEY, key => nip44.v2.decrypt(e.content, key));
377
+ const { core: raw, proofs } = unwrap(body);
378
+ const core = answerCore(raw, request, opts.now);
379
+ answerOuter(e, core, request);
380
+ if (core[2] === 'allow')
381
+ checkProofs(proofs, core);
382
+ else
383
+ array(proofs, 0);
384
+ const handle = Object.freeze(Object.create(null));
385
+ answers.set(handle, { core, digest: digest(core), event: e, requestEventId: request.event.id });
386
+ return handle;
387
+ });
388
+ }
389
+ finally {
390
+ i.bearer.fill(0);
391
+ }
392
+ }
393
+ /** Cryptographic evidence only: no usable capability, child or admission. The
394
+ * application must obtain current role witness and consume the operation once. */
395
+ export function readMlsAdmissionAnswerV4(handle) {
396
+ const value = answers.get(handle);
397
+ check(value);
398
+ return Object.freeze({ decision: value.core[2], core: JSON.stringify(value.core), digest: value.digest,
399
+ eventId: value.event.id, requestEventId: value.requestEventId });
400
+ }
@@ -0,0 +1,83 @@
1
+ import { type RoomInvitation } from './invitation.js';
2
+ import { type PersistentRoomAdmission } from './persistent-invitation.js';
3
+ import type { RelayTransport } from './transport.js';
4
+ export declare const MAX_PERSISTENT_INVITATION_EVIDENCE_EVENTS = 64;
5
+ export declare const MAX_PERSISTENT_INVITATION_EVENT_BYTES = 16384;
6
+ export declare const MAX_PERSISTENT_INVITATION_EVIDENCE_BYTES = 262144;
7
+ /** Protected, local observation metadata. EOSE is NOT evidence of relay
8
+ * completeness, current membership or global absence of retirement.
9
+ * `events` are canonical allowlisted Event carriers, not original wire JSON. */
10
+ export interface PersistentInvitationEvidence {
11
+ readonly version: 1;
12
+ readonly invitationId: string;
13
+ readonly inviter: string;
14
+ readonly completion: 'eose';
15
+ readonly eventIds: readonly string[];
16
+ readonly events: readonly string[];
17
+ }
18
+ export interface PersistentInvitationContributors {
19
+ readonly room: readonly string[];
20
+ readonly ends: readonly string[];
21
+ readonly destruct: readonly string[];
22
+ readonly relays: readonly string[];
23
+ }
24
+ export type PersistentInvitationEvidenceOutcome = {
25
+ readonly status: 'admitted';
26
+ /** A fresh owned secret buffer; caller must erase it when finished. */
27
+ readonly admission: PersistentRoomAdmission;
28
+ readonly contributors: PersistentInvitationContributors;
29
+ } | {
30
+ readonly status: 'held';
31
+ readonly reason: 'unavailable' | 'retired' | 'conflicting-rooms' | 'conflicting-relays' | 'wrong-room';
32
+ readonly retirementEventIds: readonly string[];
33
+ };
34
+ export interface PersistentInvitationEvidenceResult {
35
+ readonly evidence: PersistentInvitationEvidence;
36
+ readonly outcome: PersistentInvitationEvidenceOutcome;
37
+ }
38
+ /** One authenticated group event, without an observation/completion claim.
39
+ * This proves its signed fields only, not publication, current membership,
40
+ * latest selection or absence of retirement. */
41
+ export interface PersistentInvitationSource {
42
+ readonly version: 1;
43
+ readonly invitationId: string;
44
+ readonly inviter: string;
45
+ readonly eventId: string;
46
+ readonly createdAt: number;
47
+ /** Exact canonical carrier bytes to retain and reuse for publication. */
48
+ readonly event: string;
49
+ /** Fresh owned decoded fields; caller must erase the secret after use. */
50
+ readonly admission: PersistentRoomAdmission;
51
+ }
52
+ /** Incomplete observations are bounded diagnostic custody only. They are
53
+ * never accepted by revalidatePersistentInvitationEvidence. */
54
+ export declare class PersistentInvitationEvidenceQueryError extends Error {
55
+ readonly observation: Omit<PersistentInvitationEvidence, 'completion'> & {
56
+ readonly completion: 'incomplete';
57
+ };
58
+ constructor(message: string, observation: PersistentInvitationEvidenceQueryError['observation']);
59
+ }
60
+ /** Validate an actual encoder Event or its exact retained canonical carrier.
61
+ * A room-root pin is mandatory. No signing, query, storage or publication is
62
+ * performed. In particular, this cannot manufacture one-event EOSE evidence. */
63
+ export declare function revalidatePersistentInvitationSource(opts: {
64
+ event: unknown;
65
+ invitation: RoomInvitation;
66
+ expectedRoomSecret: Uint8Array;
67
+ }): PersistentInvitationSource;
68
+ /** Reverify all carriers and reproduce selection from protected evidence.
69
+ * Optional expectedRoomSecret binds an existing physical room to its exact
70
+ * root, not merely the invitation's current or historical room label. */
71
+ export declare function revalidatePersistentInvitationEvidence(opts: {
72
+ evidence: unknown;
73
+ invitation: RoomInvitation;
74
+ expectedRoomSecret?: Uint8Array;
75
+ }): PersistentInvitationEvidenceResult;
76
+ /** Additive evidence query. No publication, storage, relay defaults or
77
+ * membership authority. Only a complete bounded query can yield a result. */
78
+ export declare function requestPersistentRoomAdmissionEvidence(opts: {
79
+ transport: RelayTransport;
80
+ invitation: RoomInvitation;
81
+ timeoutMs?: number;
82
+ signal?: AbortSignal;
83
+ }): Promise<PersistentInvitationEvidenceResult>;