@forgesworn/fold-kit 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/credential.d.ts +5 -0
- package/dist/credential.js +4 -0
- package/dist/epoch.d.ts +96 -5
- package/dist/epoch.js +152 -24
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/member-epoch.d.ts +36 -2
- package/dist/member-epoch.js +62 -15
- package/dist/seal.d.ts +77 -0
- package/dist/seal.js +128 -0
- package/llms.txt +2 -0
- package/package.json +3 -1
- package/vectors/member-epoch-vectors.json +64 -0
- package/vectors/seal-vectors.json +629 -0
package/README.md
CHANGED
|
@@ -154,6 +154,20 @@ the rest).
|
|
|
154
154
|
`encodeMemberEpochRequest`, `decodeMemberEpochRequest`,
|
|
155
155
|
`encodeMemberEpochGrant`, `decodeMemberEpochGrant`, `readRekeyEvidence`,
|
|
156
156
|
`deriveMemberEpochRequestKey`. Vectors: `vectors/member-epoch-vectors.json`
|
|
157
|
+
- The known-members gate (#207, `docs/member-epoch-catch-up.md`): once a
|
|
158
|
+
room has removed anybody, both desks grant only to participants the room
|
|
159
|
+
knows (`known`), and send anybody else to approval (`onUnknown`; the
|
|
160
|
+
authority answers `refused: 'unknown'`, which `requestRoomEpoch` waits
|
|
161
|
+
through). `encodeRekeyEvent({ members })` and `hostRoomEpoch({ members })`
|
|
162
|
+
carry the authority's member list (`readMemberList` reads one)
|
|
163
|
+
- Seal keys (`docs/seal-key.md`): a credential may name a seal key
|
|
164
|
+
(`createDeviceCredential({ seal })`, `generateSealKey`), and rekeys and
|
|
165
|
+
both grants are sealed to the newest one the sender holds for a device
|
|
166
|
+
instead of its device key, so a copied device heals once its credential
|
|
167
|
+
lapses and the room rekeys. Recipients as `{ device, credential }`;
|
|
168
|
+
`sealSks` on the readers; `credentialFor` on both desks;
|
|
169
|
+
`credentialSeal`, `sealTarget`, `sealTo`, `openSealed`, `newerCredential`,
|
|
170
|
+
`sealCredential`. Vectors: `vectors/seal-vectors.json`
|
|
157
171
|
- `canonicalAdmins`, `signAdmins`, `verifyAdmins` - the authority's signed
|
|
158
172
|
admin list
|
|
159
173
|
- `canonicalChannels`, `signChannels`, `verifyChannels`, `CHANNEL_NAME`,
|
package/dist/credential.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface CreateCredentialOptions {
|
|
|
25
25
|
expiresAt: number;
|
|
26
26
|
/** Injectable clock, in unix seconds. Defaults to the real one. */
|
|
27
27
|
now?: () => number;
|
|
28
|
+
/** The device's seal key for this credential: an x-only public key,
|
|
29
|
+
* minted fresh for each credential, to which rekeys and epoch grants are
|
|
30
|
+
* then sealed instead of the device key. See `seal.ts`. Omitted, the
|
|
31
|
+
* credential is byte-identical to one minted before seal keys. */
|
|
32
|
+
seal?: string;
|
|
28
33
|
}
|
|
29
34
|
/** The longest a person-scoped credential may run. A phone that leaves the house is better at seven days. */
|
|
30
35
|
export declare const PERSON_CREDENTIAL_MAX_SECONDS: number;
|
package/dist/credential.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { KINDS } from './kinds.js';
|
|
2
2
|
import { verifyEventUncached } from './verify.js';
|
|
3
3
|
import { hexEquals, normaliseHex } from './hex.js';
|
|
4
|
+
import { isSealPubkey, SEAL_TAG } from './seal.js';
|
|
4
5
|
/** The longest a person-scoped credential may run. A phone that leaves the house is better at seven days. */
|
|
5
6
|
export const PERSON_CREDENTIAL_MAX_SECONDS = 30 * 24 * 60 * 60;
|
|
6
7
|
/**
|
|
@@ -43,6 +44,8 @@ export async function createDeviceCredential(opts) {
|
|
|
43
44
|
throw new Error('a room credential needs a room');
|
|
44
45
|
if (person && opts.expiresAt - now > PERSON_CREDENTIAL_MAX_SECONDS)
|
|
45
46
|
throw new Error('a person credential may not run more than 30 days');
|
|
47
|
+
if (opts.seal !== undefined && !isSealPubkey(opts.seal))
|
|
48
|
+
throw new Error('a seal key must be an x-only public key on the curve');
|
|
46
49
|
const unsigned = {
|
|
47
50
|
kind: KINDS.CREDENTIAL,
|
|
48
51
|
created_at: now,
|
|
@@ -52,6 +55,7 @@ export async function createDeviceCredential(opts) {
|
|
|
52
55
|
['expiration', String(opts.expiresAt)],
|
|
53
56
|
...(person ? [['scope', 'person']] : []),
|
|
54
57
|
...(opts.label !== undefined ? [['label', opts.label]] : []),
|
|
58
|
+
...(opts.seal !== undefined ? [[SEAL_TAG, opts.seal.toLowerCase()]] : []),
|
|
55
59
|
],
|
|
56
60
|
content: '',
|
|
57
61
|
};
|
package/dist/epoch.d.ts
CHANGED
|
@@ -73,6 +73,10 @@ export interface EpochKeys {
|
|
|
73
73
|
/** The cipher key. The room key in epoch 0. */
|
|
74
74
|
key: Uint8Array;
|
|
75
75
|
}
|
|
76
|
+
/** A participant list as a body carries it: lower-case, deduplicated and
|
|
77
|
+
* sorted, or undefined when the field is absent or is not a list of keys.
|
|
78
|
+
* Undefined is the safe reading, since it makes nobody known. */
|
|
79
|
+
export declare function readMemberList(raw: unknown): string[] | undefined;
|
|
76
80
|
/** A fresh secret for the next epoch. */
|
|
77
81
|
export declare function generateEpochSecret(): Uint8Array;
|
|
78
82
|
/**
|
|
@@ -94,8 +98,11 @@ export interface EncodeRekeyOptions {
|
|
|
94
98
|
/** The epoch being entered. Must be `current.epoch + 1`. */
|
|
95
99
|
next: RoomEpoch;
|
|
96
100
|
/** Devices to seal the new secret for. Everybody still in the room,
|
|
97
|
-
* except the removed participants' devices.
|
|
98
|
-
|
|
101
|
+
* except the removed participants' devices. A recipient given with its
|
|
102
|
+
* newest credential is sealed to that credential's seal key, when it
|
|
103
|
+
* names one; a bare device, to the device key (see `seal.ts`). The
|
|
104
|
+
* `keys` map is keyed by device either way. */
|
|
105
|
+
recipients: ReadonlyArray<string | RekeyRecipient>;
|
|
99
106
|
/** Participants removed at this step. Empty on a rekey that only turns
|
|
100
107
|
* the key over. */
|
|
101
108
|
removed: string[];
|
|
@@ -108,11 +115,22 @@ export interface EncodeRekeyOptions {
|
|
|
108
115
|
* bring a device that missed this rekey up to date (see
|
|
109
116
|
* `member-epoch.ts`). Omitted, the event is byte-identical to before. */
|
|
110
117
|
commit?: boolean;
|
|
118
|
+
/** Every participant the authority knows to be in the room, not only the
|
|
119
|
+
* ones present now: a member who is offline as the room rekeys is still
|
|
120
|
+
* a member. Removed participants are dropped. Omitted, the event is
|
|
121
|
+
* byte-identical to before. */
|
|
122
|
+
members?: string[];
|
|
111
123
|
now: number;
|
|
112
124
|
/** A conference room's end, in unix seconds: the event carries it as a
|
|
113
125
|
* NIP-40 expiration (see `withExpiration`). Omit for a room with no end. */
|
|
114
126
|
expiresAt?: number;
|
|
115
127
|
}
|
|
128
|
+
/** A rekey recipient: a device, and the newest credential the sender holds
|
|
129
|
+
* for it. */
|
|
130
|
+
export interface RekeyRecipient {
|
|
131
|
+
device: string;
|
|
132
|
+
credential?: DeviceCredential;
|
|
133
|
+
}
|
|
116
134
|
/** Announce the next epoch. See the module comment for what this is. */
|
|
117
135
|
export declare function encodeRekeyEvent(opts: EncodeRekeyOptions): Event;
|
|
118
136
|
/** What a rekey says, once read. */
|
|
@@ -125,6 +143,9 @@ export interface RekeyNotice {
|
|
|
125
143
|
* device that was removed, or that was not in the room when the
|
|
126
144
|
* authority rekeyed. */
|
|
127
145
|
secret?: Uint8Array;
|
|
146
|
+
/** The participants the authority listed as in the room. Absent from a
|
|
147
|
+
* rekey written without the list. */
|
|
148
|
+
members?: string[];
|
|
128
149
|
/** True for a current-state grant, whose removed list is cumulative.
|
|
129
150
|
* It synchronises this session; it does not describe a new removal. */
|
|
130
151
|
catchUp?: true;
|
|
@@ -148,6 +169,9 @@ export interface DecodeRekeyOptions extends PeekRekeyOptions {
|
|
|
148
169
|
current: EpochKeys;
|
|
149
170
|
/** This device's key, to open its own copy. */
|
|
150
171
|
deviceSk: Uint8Array;
|
|
172
|
+
/** The seal key secrets this device holds, newest first, tried before
|
|
173
|
+
* the device key (see `seal.ts`). */
|
|
174
|
+
sealSks?: readonly Uint8Array[];
|
|
151
175
|
}
|
|
152
176
|
/**
|
|
153
177
|
* Read a rekey. Null for anything that does not check out, and for a rekey
|
|
@@ -225,8 +249,14 @@ export interface EpochRequest {
|
|
|
225
249
|
* cannot prove which participant it speaks for in this room, or from one
|
|
226
250
|
* that cannot prove it was admitted to the room at all. */
|
|
227
251
|
export declare function decodeEpochRequest(event: Event, opts: DecodeEpochRequestOptions): EpochRequest | null;
|
|
228
|
-
/**
|
|
229
|
-
|
|
252
|
+
/** How often a desk reports the same unknown participant while they keep
|
|
253
|
+
* asking: often enough that a missed "let them in?" comes back, rarely
|
|
254
|
+
* enough that it is not a nag. */
|
|
255
|
+
export declare const REPORT_UNKNOWN_EVERY_SECONDS = 60;
|
|
256
|
+
/** Why the authority would not hand an epoch over. `unknown` is not final:
|
|
257
|
+
* the room has removed somebody, and does not yet know this participant,
|
|
258
|
+
* so it waits for a member to let them in (#207). */
|
|
259
|
+
export type EpochRefusal = 'removed' | 'closed' | 'unknown';
|
|
230
260
|
export interface EncodeEpochGrantOptions {
|
|
231
261
|
roomId: string;
|
|
232
262
|
authoritySk: Uint8Array;
|
|
@@ -236,7 +266,13 @@ export interface EncodeEpochGrantOptions {
|
|
|
236
266
|
/** The current epoch. Omitted when refusing. */
|
|
237
267
|
epoch?: RoomEpoch;
|
|
238
268
|
removed?: string[];
|
|
269
|
+
/** The participants the authority knows, so the requester's own member
|
|
270
|
+
* desk knows them too. Omitted, the body is as before. */
|
|
271
|
+
members?: string[];
|
|
239
272
|
refused?: EpochRefusal;
|
|
273
|
+
/** The newest credential the authority holds for the device: the grant
|
|
274
|
+
* is sealed to its seal key when it names one (see `seal.ts`). */
|
|
275
|
+
credential?: DeviceCredential;
|
|
240
276
|
/** A conference room's end, in unix seconds: the event carries it as a
|
|
241
277
|
* NIP-40 expiration (see `withExpiration`). Omit for a room with no end. */
|
|
242
278
|
expiresAt?: number;
|
|
@@ -248,6 +284,9 @@ export interface DecodeEpochGrantOptions {
|
|
|
248
284
|
roomId: string;
|
|
249
285
|
authority: string;
|
|
250
286
|
deviceSk: Uint8Array;
|
|
287
|
+
/** The seal key secrets this device holds: see
|
|
288
|
+
* `DecodeRekeyOptions.sealSks`. */
|
|
289
|
+
sealSks?: readonly Uint8Array[];
|
|
251
290
|
request: string;
|
|
252
291
|
now: number;
|
|
253
292
|
maxAgeSeconds?: number;
|
|
@@ -261,6 +300,8 @@ export type EpochGrant = {
|
|
|
261
300
|
* the epochs it carried between the requester's and `epoch`. See
|
|
262
301
|
* `MemberEpochGrant.passed`. The authority's own answer has none. */
|
|
263
302
|
passed?: RoomEpoch[];
|
|
303
|
+
/** The participants the room knows, when the answer carried them. */
|
|
304
|
+
members?: string[];
|
|
264
305
|
refused?: undefined;
|
|
265
306
|
} | {
|
|
266
307
|
refused: EpochRefusal;
|
|
@@ -281,6 +322,33 @@ export interface HostRoomEpochOptions {
|
|
|
281
322
|
removed: () => ReadonlySet<string>;
|
|
282
323
|
/** True once the room has been closed: every request is refused. */
|
|
283
324
|
closed?: () => boolean;
|
|
325
|
+
/**
|
|
326
|
+
* Whether the room knows this participant: on the authority's member
|
|
327
|
+
* list, in the room now, or let in. Consulted only once somebody has been
|
|
328
|
+
* removed, and then everybody else is answered `unknown` (#207). Without
|
|
329
|
+
* it, after a removal, nobody is known. Asked on every request.
|
|
330
|
+
*/
|
|
331
|
+
known?: (participant: string) => boolean;
|
|
332
|
+
/** The participants to tell a granted device about, so its own member
|
|
333
|
+
* desk knows them too. Asked on every grant. */
|
|
334
|
+
members?: () => readonly string[];
|
|
335
|
+
/**
|
|
336
|
+
* The newest credential the authority has seen for a device, from the
|
|
337
|
+
* roster. An answer is sealed to the newer of this and the one the
|
|
338
|
+
* request carries, so a thief replaying a device's older, still-live
|
|
339
|
+
* credential is answered under the seal key the device minted since.
|
|
340
|
+
* Keep it monotone: a roster entry carrying an older credential than one
|
|
341
|
+
* already seen must not replace it, or the thief can publish one.
|
|
342
|
+
*/
|
|
343
|
+
credentialFor?: (device: string) => DeviceCredential | undefined;
|
|
344
|
+
/** Somebody the room does not know asked, after a removal: what an app
|
|
345
|
+
* turns into "let them in?". Called once per participant, and again
|
|
346
|
+
* every `reportUnknownEvery` seconds while they keep asking. Letting them in
|
|
347
|
+
* is making `known` say yes; the requester's next ask is then granted. */
|
|
348
|
+
onUnknown?: (request: EpochRequest) => void;
|
|
349
|
+
/** Seconds before a participant still unknown and still asking is
|
|
350
|
+
* reported again. Default 60. */
|
|
351
|
+
reportUnknownEvery?: number;
|
|
284
352
|
policy?: RoomPolicy;
|
|
285
353
|
legacyParticipants?: ReadonlySet<string>;
|
|
286
354
|
now?: () => number;
|
|
@@ -289,6 +357,15 @@ export interface HostRoomEpochOptions {
|
|
|
289
357
|
/** A conference room's end: every grant carries it as an expiration. */
|
|
290
358
|
expiresAt?: number;
|
|
291
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* The credential to seal an answer to: the one the request carries, unless
|
|
362
|
+
* the room has seen a newer one for the same device and participant that
|
|
363
|
+
* still verifies. Shared by both desks.
|
|
364
|
+
*/
|
|
365
|
+
export declare function sealCredential(request: {
|
|
366
|
+
device: string;
|
|
367
|
+
participant: string;
|
|
368
|
+
}, presented: DeviceCredential, credentialFor: ((device: string) => DeviceCredential | undefined) | undefined, roomId: string, now: number): DeviceCredential;
|
|
292
369
|
/**
|
|
293
370
|
* Answer epoch requests for as long as the handle is open. What a keeper
|
|
294
371
|
* runs beside its invitation desk: a member arriving, or returning, after
|
|
@@ -298,6 +375,13 @@ export interface HostRoomEpochOptions {
|
|
|
298
375
|
* *secret*, which opens epoch 0 and nothing after it. A request with no
|
|
299
376
|
* admission proof, or one made under some other key, is not answered at
|
|
300
377
|
* all: a stranger learns nothing, not even that a desk is here.
|
|
378
|
+
*
|
|
379
|
+
* The admission proof is made under the epoch-0 room key, which a removed
|
|
380
|
+
* member still holds, so removal by participant alone is undone by a fresh
|
|
381
|
+
* key (#207). Once anyone has been removed the desk therefore grants only
|
|
382
|
+
* to participants the room knows (`known`), and answers anybody else
|
|
383
|
+
* `unknown` until a member lets them in. A room that has never removed
|
|
384
|
+
* anybody is answered exactly as before.
|
|
301
385
|
*/
|
|
302
386
|
export declare function hostRoomEpoch(opts: HostRoomEpochOptions): {
|
|
303
387
|
close(): void;
|
|
@@ -319,6 +403,12 @@ export interface RequestRoomEpochOptions {
|
|
|
319
403
|
/** Ask the room's current members too (`memberEpochSource`): the first
|
|
320
404
|
* answer that checks out, the authority's or a member's, settles it. */
|
|
321
405
|
members?: MemberEpochSource;
|
|
406
|
+
/** The authority said the room does not know this participant yet. Not
|
|
407
|
+
* final: the ask goes on, and a member letting them in settles it. */
|
|
408
|
+
onUnknown?: () => void;
|
|
409
|
+
/** The seal key secrets this device holds. Asked on every answer,
|
|
410
|
+
* because a credential may renew while the ask is out. */
|
|
411
|
+
sealSks?: () => readonly Uint8Array[];
|
|
322
412
|
}
|
|
323
413
|
/** Thrown when the authority answered, and the answer was no. */
|
|
324
414
|
export declare class EpochRefusedError extends Error {
|
|
@@ -327,7 +417,8 @@ export declare class EpochRefusedError extends Error {
|
|
|
327
417
|
}
|
|
328
418
|
/** Ask the authority where the room is, and wait for the answer. Rejects
|
|
329
419
|
* with `EpochRefusedError` on a refusal, and with a plain error when
|
|
330
|
-
* nobody answers inside the timeout.
|
|
420
|
+
* nobody answers inside the timeout. `unknown` is not final: the ask goes
|
|
421
|
+
* on, and only a timeout after it rejects, with `EpochRefusedError`. */
|
|
331
422
|
export declare function requestRoomEpoch(opts: RequestRoomEpochOptions): Promise<Exclude<EpochGrant, {
|
|
332
423
|
refused: EpochRefusal;
|
|
333
424
|
}>>;
|
package/dist/epoch.js
CHANGED
|
@@ -12,6 +12,7 @@ import { deriveRoom } from './room.js';
|
|
|
12
12
|
import { verifyDeviceCredential } from './credential.js';
|
|
13
13
|
import { evaluateAccess } from './access.js';
|
|
14
14
|
import { verifyEventUncached } from './verify.js';
|
|
15
|
+
import { newerCredential, openSealed, sealTo } from './seal.js';
|
|
15
16
|
import { withExpiration } from './expiration.js';
|
|
16
17
|
import { epochCommitment } from './epoch-commit.js';
|
|
17
18
|
/**
|
|
@@ -80,6 +81,14 @@ function require32(bytes, what) {
|
|
|
80
81
|
if (bytes.length !== 32)
|
|
81
82
|
throw new Error(`${what} must be 32 bytes`);
|
|
82
83
|
}
|
|
84
|
+
/** A participant list as a body carries it: lower-case, deduplicated and
|
|
85
|
+
* sorted, or undefined when the field is absent or is not a list of keys.
|
|
86
|
+
* Undefined is the safe reading, since it makes nobody known. */
|
|
87
|
+
export function readMemberList(raw) {
|
|
88
|
+
if (!Array.isArray(raw) || !raw.every((p) => typeof p === 'string' && HEX64.test(p)))
|
|
89
|
+
return undefined;
|
|
90
|
+
return [...new Set(raw.map(normaliseHex))].sort();
|
|
91
|
+
}
|
|
83
92
|
function requireHex32(value, what) {
|
|
84
93
|
if (!HEX64.test(value))
|
|
85
94
|
throw new Error(`${what} must be 32-byte hex`);
|
|
@@ -119,6 +128,12 @@ export function deriveEpoch(epoch) {
|
|
|
119
128
|
key: hkdf(sha256, epoch.secret, undefined, `${EPOCH_KEY_INFO}/${n}`, 32),
|
|
120
129
|
};
|
|
121
130
|
}
|
|
131
|
+
function memberListOf(members, removed) {
|
|
132
|
+
const out = new Set(members.map((p) => requireHex32(p, 'member participant')));
|
|
133
|
+
for (const p of removed)
|
|
134
|
+
out.delete(normaliseHex(p));
|
|
135
|
+
return [...out].sort();
|
|
136
|
+
}
|
|
122
137
|
/** Announce the next epoch. See the module comment for what this is. */
|
|
123
138
|
export function encodeRekeyEvent(opts) {
|
|
124
139
|
require32(opts.authoritySk, 'authority secret key');
|
|
@@ -132,8 +147,9 @@ export function encodeRekeyEvent(opts) {
|
|
|
132
147
|
const sealed = { v: 1, secret: base64urlnopad.encode(opts.next.secret) };
|
|
133
148
|
const plaintext = JSON.stringify(sealed);
|
|
134
149
|
for (const raw of opts.closed ? [] : opts.recipients) {
|
|
135
|
-
const
|
|
136
|
-
|
|
150
|
+
const recipient = typeof raw === 'string' ? { device: raw } : raw;
|
|
151
|
+
const device = requireHex32(recipient.device, 'recipient device');
|
|
152
|
+
keys[device] = sealTo(plaintext, opts.authoritySk, device, recipient.credential);
|
|
137
153
|
}
|
|
138
154
|
const body = {
|
|
139
155
|
v: 1,
|
|
@@ -142,6 +158,7 @@ export function encodeRekeyEvent(opts) {
|
|
|
142
158
|
...(opts.by !== undefined ? { by: requireHex32(opts.by, 'admin') } : {}),
|
|
143
159
|
...(opts.closed ? { closed: true } : {}),
|
|
144
160
|
...(opts.commit ? { commit: epochCommitment(roomId, epoch, opts.next.secret) } : {}),
|
|
161
|
+
...(opts.members !== undefined ? { members: memberListOf(opts.members, removed) } : {}),
|
|
145
162
|
keys,
|
|
146
163
|
};
|
|
147
164
|
return finalizeEvent({
|
|
@@ -206,14 +223,26 @@ export function decodeRekeyEvent(event, opts) {
|
|
|
206
223
|
};
|
|
207
224
|
if (typeof body.by === 'string' && HEX64.test(body.by))
|
|
208
225
|
notice.by = normaliseHex(body.by);
|
|
226
|
+
const members = readMemberList(body.members);
|
|
227
|
+
if (members)
|
|
228
|
+
notice.members = members;
|
|
209
229
|
const device = getPublicKey(opts.deviceSk);
|
|
210
230
|
const mine = Object.entries(body.keys).find(([to]) => hexEquals(to, device))?.[1];
|
|
211
231
|
if (typeof mine === 'string') {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
// A copy none of this device's keys opens is no copy: sealed to a
|
|
233
|
+
// seal key it has since erased, or one it never had. The rest of the
|
|
234
|
+
// rekey still says who was removed, so it is read without the secret,
|
|
235
|
+
// and the device asks for the epoch.
|
|
236
|
+
try {
|
|
237
|
+
const sealed = JSON.parse(openSealed(mine, event.pubkey, opts.deviceSk, opts.sealSks));
|
|
238
|
+
if (sealed.v === 1 && typeof sealed.secret === 'string') {
|
|
239
|
+
const secret = base64urlnopad.decode(sealed.secret);
|
|
240
|
+
if (secret.length === 32)
|
|
241
|
+
notice.secret = secret;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// No secret, as above.
|
|
217
246
|
}
|
|
218
247
|
}
|
|
219
248
|
return notice;
|
|
@@ -296,6 +325,12 @@ export function encodeEpochRequest(opts) {
|
|
|
296
325
|
* cannot prove which participant it speaks for in this room, or from one
|
|
297
326
|
* that cannot prove it was admitted to the room at all. */
|
|
298
327
|
export function decodeEpochRequest(event, opts) {
|
|
328
|
+
const read = readEpochRequest(event, opts);
|
|
329
|
+
return read && read.request;
|
|
330
|
+
}
|
|
331
|
+
/** `decodeEpochRequest`, keeping the credential the request was made
|
|
332
|
+
* under: what the desk seals its answer by. */
|
|
333
|
+
function readEpochRequest(event, opts) {
|
|
299
334
|
try {
|
|
300
335
|
if (event.kind !== KINDS.EPOCH_REQUEST)
|
|
301
336
|
return null;
|
|
@@ -340,12 +375,16 @@ export function decodeEpochRequest(event, opts) {
|
|
|
340
375
|
if (!evaluateAccess(opts.policy, verdict.participant, proof, opts.now, opts.roomId).admitted)
|
|
341
376
|
return null;
|
|
342
377
|
}
|
|
343
|
-
return { device: verdict.device, participant: verdict.participant, request: event.id };
|
|
378
|
+
return { request: { device: verdict.device, participant: verdict.participant, request: event.id }, credential: body.credential };
|
|
344
379
|
}
|
|
345
380
|
catch {
|
|
346
381
|
return null;
|
|
347
382
|
}
|
|
348
383
|
}
|
|
384
|
+
/** How often a desk reports the same unknown participant while they keep
|
|
385
|
+
* asking: often enough that a missed "let them in?" comes back, rarely
|
|
386
|
+
* enough that it is not a nag. */
|
|
387
|
+
export const REPORT_UNKNOWN_EVERY_SECONDS = 60;
|
|
349
388
|
/** Answer one request: the current epoch sealed to the asking device, or a
|
|
350
389
|
* refusal it alone can read. */
|
|
351
390
|
export function encodeEpochGrant(opts) {
|
|
@@ -366,6 +405,8 @@ export function encodeEpochGrant(opts) {
|
|
|
366
405
|
body.secret = base64urlnopad.encode(opts.epoch.secret);
|
|
367
406
|
}
|
|
368
407
|
body.removed = [...new Set((opts.removed ?? []).map((p) => requireHex32(p, 'removed participant')))].sort();
|
|
408
|
+
if (opts.members !== undefined)
|
|
409
|
+
body.members = memberListOf(opts.members, body.removed);
|
|
369
410
|
}
|
|
370
411
|
return finalizeEvent({
|
|
371
412
|
kind: KINDS.EPOCH_GRANT,
|
|
@@ -374,7 +415,7 @@ export function encodeEpochGrant(opts) {
|
|
|
374
415
|
['d', roomId],
|
|
375
416
|
['p', device],
|
|
376
417
|
], opts.expiresAt),
|
|
377
|
-
content:
|
|
418
|
+
content: sealTo(JSON.stringify(body), opts.authoritySk, device, opts.credential),
|
|
378
419
|
}, opts.authoritySk);
|
|
379
420
|
}
|
|
380
421
|
/** Read the authority's answer to this device's own request. Null for
|
|
@@ -396,29 +437,51 @@ export function decodeEpochGrant(event, opts) {
|
|
|
396
437
|
const addressed = event.tags.find((t) => t[0] === 'p')?.[1];
|
|
397
438
|
if (addressed === undefined || !hexEquals(addressed, device))
|
|
398
439
|
return null;
|
|
399
|
-
const body = JSON.parse(
|
|
440
|
+
const body = JSON.parse(openSealed(event.content, event.pubkey, opts.deviceSk, opts.sealSks));
|
|
400
441
|
if (body.v !== 1 || typeof body.request !== 'string' || !hexEquals(body.request, opts.request))
|
|
401
442
|
return null;
|
|
402
|
-
if (body.refused === 'removed' || body.refused === 'closed')
|
|
443
|
+
if (body.refused === 'removed' || body.refused === 'closed' || body.refused === 'unknown')
|
|
403
444
|
return { refused: body.refused };
|
|
404
445
|
if (!Number.isSafeInteger(body.epoch) || body.epoch < 0 || body.epoch > MAX_EPOCH)
|
|
405
446
|
return null;
|
|
406
447
|
const removed = Array.isArray(body.removed)
|
|
407
448
|
? [...new Set(body.removed.filter((p) => typeof p === 'string' && HEX64.test(p)).map(normaliseHex))].sort()
|
|
408
449
|
: [];
|
|
450
|
+
const members = readMemberList(body.members);
|
|
451
|
+
const listed = members ? { members } : {};
|
|
409
452
|
if (body.epoch === 0)
|
|
410
|
-
return { epoch: { epoch: 0 }, removed };
|
|
453
|
+
return { epoch: { epoch: 0 }, removed, ...listed };
|
|
411
454
|
if (typeof body.secret !== 'string')
|
|
412
455
|
return null;
|
|
413
456
|
const secret = base64urlnopad.decode(body.secret);
|
|
414
457
|
if (secret.length !== 32)
|
|
415
458
|
return null;
|
|
416
|
-
return { epoch: { epoch: body.epoch, secret }, removed };
|
|
459
|
+
return { epoch: { epoch: body.epoch, secret }, removed, ...listed };
|
|
417
460
|
}
|
|
418
461
|
catch {
|
|
419
462
|
return null;
|
|
420
463
|
}
|
|
421
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* The credential to seal an answer to: the one the request carries, unless
|
|
467
|
+
* the room has seen a newer one for the same device and participant that
|
|
468
|
+
* still verifies. Shared by both desks.
|
|
469
|
+
*/
|
|
470
|
+
export function sealCredential(request, presented, credentialFor, roomId, now) {
|
|
471
|
+
let known;
|
|
472
|
+
try {
|
|
473
|
+
known = credentialFor?.(request.device);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
known = undefined;
|
|
477
|
+
}
|
|
478
|
+
if (!known)
|
|
479
|
+
return presented;
|
|
480
|
+
const verdict = verifyDeviceCredential(known, { roomId, now });
|
|
481
|
+
if (!verdict.ok || verdict.device !== normaliseHex(request.device) || verdict.participant !== normaliseHex(request.participant))
|
|
482
|
+
return presented;
|
|
483
|
+
return newerCredential(presented, known);
|
|
484
|
+
}
|
|
422
485
|
/**
|
|
423
486
|
* Answer epoch requests for as long as the handle is open. What a keeper
|
|
424
487
|
* runs beside its invitation desk: a member arriving, or returning, after
|
|
@@ -428,6 +491,13 @@ export function decodeEpochGrant(event, opts) {
|
|
|
428
491
|
* *secret*, which opens epoch 0 and nothing after it. A request with no
|
|
429
492
|
* admission proof, or one made under some other key, is not answered at
|
|
430
493
|
* all: a stranger learns nothing, not even that a desk is here.
|
|
494
|
+
*
|
|
495
|
+
* The admission proof is made under the epoch-0 room key, which a removed
|
|
496
|
+
* member still holds, so removal by participant alone is undone by a fresh
|
|
497
|
+
* key (#207). Once anyone has been removed the desk therefore grants only
|
|
498
|
+
* to participants the room knows (`known`), and answers anybody else
|
|
499
|
+
* `unknown` until a member lets them in. A room that has never removed
|
|
500
|
+
* anybody is answered exactly as before.
|
|
431
501
|
*/
|
|
432
502
|
export function hostRoomEpoch(opts) {
|
|
433
503
|
require32(opts.authoritySk, 'authority secret key');
|
|
@@ -436,11 +506,21 @@ export function hostRoomEpoch(opts) {
|
|
|
436
506
|
const authority = getPublicKey(opts.authoritySk);
|
|
437
507
|
const now = opts.now ?? (() => Math.floor(Date.now() / 1000));
|
|
438
508
|
const answered = new Set();
|
|
509
|
+
/** Requests answered `unknown`, which the requester sends again while it
|
|
510
|
+
* waits: answered again only once they are known. */
|
|
511
|
+
const waiting = new Set();
|
|
512
|
+
/** When each unknown participant was last reported through `onUnknown`,
|
|
513
|
+
* so one still waiting is reported again only after `reportUnknownEvery`. */
|
|
514
|
+
const reported = new Map();
|
|
515
|
+
const bound = (set) => {
|
|
516
|
+
if (set.size > 256)
|
|
517
|
+
set.delete(set.values().next().value);
|
|
518
|
+
};
|
|
439
519
|
let closed = false;
|
|
440
520
|
const unsub = opts.transport.subscribe([{ kinds: [KINDS.EPOCH_REQUEST], '#d': [roomId], '#p': [authority] }], (event) => {
|
|
441
521
|
if (closed)
|
|
442
522
|
return;
|
|
443
|
-
const
|
|
523
|
+
const read = readEpochRequest(event, {
|
|
444
524
|
roomId,
|
|
445
525
|
authoritySk: opts.authoritySk,
|
|
446
526
|
roomKey: opts.roomKey,
|
|
@@ -448,20 +528,40 @@ export function hostRoomEpoch(opts) {
|
|
|
448
528
|
policy: opts.policy,
|
|
449
529
|
legacyParticipants: opts.legacyParticipants,
|
|
450
530
|
});
|
|
451
|
-
if (!
|
|
531
|
+
if (!read || answered.has(read.request.request))
|
|
452
532
|
return;
|
|
453
|
-
|
|
454
|
-
if (answered.size > 256)
|
|
455
|
-
answered.delete(answered.values().next().value);
|
|
533
|
+
const request = read.request;
|
|
456
534
|
let refused;
|
|
457
535
|
if (opts.closed?.())
|
|
458
536
|
refused = 'closed';
|
|
459
537
|
else if (opts.removed().has(request.participant))
|
|
460
538
|
refused = 'removed';
|
|
539
|
+
else if (opts.removed().size > 0 && !opts.known?.(request.participant))
|
|
540
|
+
refused = 'unknown';
|
|
541
|
+
if (refused === 'unknown') {
|
|
542
|
+
if (waiting.has(request.request))
|
|
543
|
+
return;
|
|
544
|
+
waiting.add(request.request);
|
|
545
|
+
bound(waiting);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
waiting.delete(request.request);
|
|
549
|
+
answered.add(request.request);
|
|
550
|
+
bound(answered);
|
|
551
|
+
}
|
|
461
552
|
let grant;
|
|
462
553
|
try {
|
|
463
554
|
grant = refused
|
|
464
|
-
? encodeEpochGrant({
|
|
555
|
+
? encodeEpochGrant({
|
|
556
|
+
roomId,
|
|
557
|
+
authoritySk: opts.authoritySk,
|
|
558
|
+
device: request.device,
|
|
559
|
+
request: request.request,
|
|
560
|
+
now: now(),
|
|
561
|
+
refused,
|
|
562
|
+
credential: sealCredential(request, read.credential, opts.credentialFor, roomId, now()),
|
|
563
|
+
expiresAt: opts.expiresAt,
|
|
564
|
+
})
|
|
465
565
|
: encodeEpochGrant({
|
|
466
566
|
roomId,
|
|
467
567
|
authoritySk: opts.authoritySk,
|
|
@@ -470,6 +570,8 @@ export function hostRoomEpoch(opts) {
|
|
|
470
570
|
now: now(),
|
|
471
571
|
epoch: opts.current(),
|
|
472
572
|
removed: [...opts.removed()],
|
|
573
|
+
...(opts.members ? { members: [...opts.members()] } : {}),
|
|
574
|
+
credential: sealCredential(request, read.credential, opts.credentialFor, roomId, now()),
|
|
473
575
|
expiresAt: opts.expiresAt,
|
|
474
576
|
});
|
|
475
577
|
}
|
|
@@ -477,6 +579,14 @@ export function hostRoomEpoch(opts) {
|
|
|
477
579
|
return;
|
|
478
580
|
}
|
|
479
581
|
opts.transport.publish(grant).catch(() => { });
|
|
582
|
+
if (refused === 'unknown' && now() - (reported.get(request.participant) ?? -Infinity) >= (opts.reportUnknownEvery ?? REPORT_UNKNOWN_EVERY_SECONDS)) {
|
|
583
|
+
reported.set(request.participant, now());
|
|
584
|
+
if (reported.size > 256)
|
|
585
|
+
reported.delete(reported.keys().next().value);
|
|
586
|
+
opts.onUnknown?.(request);
|
|
587
|
+
}
|
|
588
|
+
if (!refused)
|
|
589
|
+
reported.delete(request.participant);
|
|
480
590
|
if (refused)
|
|
481
591
|
opts.onRefused?.(request, refused);
|
|
482
592
|
else
|
|
@@ -495,14 +605,19 @@ export function hostRoomEpoch(opts) {
|
|
|
495
605
|
export class EpochRefusedError extends Error {
|
|
496
606
|
refused;
|
|
497
607
|
constructor(refused) {
|
|
498
|
-
super(refused === 'removed'
|
|
608
|
+
super(refused === 'removed'
|
|
609
|
+
? 'you were removed from this room'
|
|
610
|
+
: refused === 'unknown'
|
|
611
|
+
? 'nobody in this room has let you in yet'
|
|
612
|
+
: 'this room has been closed');
|
|
499
613
|
this.name = 'EpochRefusedError';
|
|
500
614
|
this.refused = refused;
|
|
501
615
|
}
|
|
502
616
|
}
|
|
503
617
|
/** Ask the authority where the room is, and wait for the answer. Rejects
|
|
504
618
|
* with `EpochRefusedError` on a refusal, and with a plain error when
|
|
505
|
-
* nobody answers inside the timeout.
|
|
619
|
+
* nobody answers inside the timeout. `unknown` is not final: the ask goes
|
|
620
|
+
* on, and only a timeout after it rejects, with `EpochRefusedError`. */
|
|
506
621
|
export function requestRoomEpoch(opts) {
|
|
507
622
|
const now = opts.now ?? (() => Math.floor(Date.now() / 1000));
|
|
508
623
|
const roomId = requireHex32(opts.roomId, 'room id');
|
|
@@ -523,6 +638,7 @@ export function requestRoomEpoch(opts) {
|
|
|
523
638
|
let expiry;
|
|
524
639
|
let unsub = () => { };
|
|
525
640
|
let stopMembers = () => { };
|
|
641
|
+
let unknown = false;
|
|
526
642
|
const finish = (settle) => {
|
|
527
643
|
if (settled)
|
|
528
644
|
return;
|
|
@@ -536,10 +652,22 @@ export function requestRoomEpoch(opts) {
|
|
|
536
652
|
settle();
|
|
537
653
|
};
|
|
538
654
|
unsub = opts.transport.subscribe([{ kinds: [KINDS.EPOCH_GRANT], '#d': [roomId], '#p': [device] }], (event) => {
|
|
539
|
-
const grant = decodeEpochGrant(event, {
|
|
655
|
+
const grant = decodeEpochGrant(event, {
|
|
656
|
+
roomId,
|
|
657
|
+
authority: opts.authority,
|
|
658
|
+
deviceSk: opts.deviceSk,
|
|
659
|
+
sealSks: opts.sealSks?.(),
|
|
660
|
+
request: request.id,
|
|
661
|
+
now: now(),
|
|
662
|
+
});
|
|
540
663
|
if (!grant)
|
|
541
664
|
return;
|
|
542
|
-
if (grant.refused)
|
|
665
|
+
if (grant.refused === 'unknown') {
|
|
666
|
+
if (!unknown)
|
|
667
|
+
opts.onUnknown?.();
|
|
668
|
+
unknown = true;
|
|
669
|
+
}
|
|
670
|
+
else if (grant.refused)
|
|
543
671
|
finish(() => reject(new EpochRefusedError(grant.refused)));
|
|
544
672
|
else
|
|
545
673
|
finish(() => resolve(grant));
|
|
@@ -555,7 +683,7 @@ export function requestRoomEpoch(opts) {
|
|
|
555
683
|
if (!settled)
|
|
556
684
|
opts.transport.publish(request).catch(() => { });
|
|
557
685
|
};
|
|
558
|
-
expiry = setTimeout(() => finish(() => reject(new Error('the room has moved to a newer epoch and its keeper is not answering'))), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
686
|
+
expiry = setTimeout(() => finish(() => reject(unknown ? new EpochRefusedError('unknown') : new Error('the room has moved to a newer epoch and its keeper is not answering'))), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
559
687
|
expiry.unref?.();
|
|
560
688
|
retry = setInterval(ask, opts.retryMs ?? DEFAULT_RETRY_MS);
|
|
561
689
|
retry.unref?.();
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type { DeviceCredential, AccessTier, AgentRule, RoomPolicy, KindredProof
|
|
|
7
7
|
export type { RelayTransport } from './transport.js';
|
|
8
8
|
export { createDeviceCredential, verifyDeviceCredential, PERSON_CREDENTIAL_MAX_SECONDS, RestampedCredentialExpiryError, } from './credential.js';
|
|
9
9
|
export type { CreateCredentialOptions, VerifyResult } from './credential.js';
|
|
10
|
+
export { SEAL_TAG, isSealPubkey, generateSealKey, credentialSeal, sealTarget, sealTo, openSealed, newerCredential, } from './seal.js';
|
|
11
|
+
export type { SealKey } from './seal.js';
|
|
10
12
|
export { deriveScoped, SCOPED_LABEL_PATTERN } from './scoped.js';
|
|
11
13
|
export type { ScopedKeys } from './scoped.js';
|
|
12
14
|
export { createSubKeyCertificate, verifySubKeyCertificate, SUB_KEY_CERTIFICATE_SCOPE, } from './sub-cert.js';
|
|
@@ -24,8 +26,8 @@ export { withExpiration, isRoomEnds, requireRoomEnds, MAX_ROOM_ENDS_SECONDS } fr
|
|
|
24
26
|
export { isInvitationRelays, requireInvitationRelays, MAX_INVITATION_RELAYS } from './invitation-relays.js';
|
|
25
27
|
export { MAX_ROOM_LINK_FRAGMENT_LENGTH, parseRoomLink, encodeRoomLink, } from './link.js';
|
|
26
28
|
export type { RoomLink } from './link.js';
|
|
27
|
-
export { EPOCH_ID_INFO, EPOCH_KEY_INFO, MAX_EPOCH, EPOCH_REQUEST_KEY_INFO, generateEpochSecret, deriveEpoch, encodeRekeyEvent, peekRekeyEvent, decodeRekeyEvent, deriveEpochRequestKey, epochRequestAdmission, encodeEpochRequest, decodeEpochRequest, encodeEpochGrant, decodeEpochGrant, hostRoomEpoch, EpochRefusedError, requestRoomEpoch, canonicalAdmins, CHANNEL_NAME, RESERVED_CHANNELS, canonicalChannels, signChannels, verifyChannels, signAdmins, verifyAdmins, EPOCH_LABELS, } from './epoch.js';
|
|
28
|
-
export type { RoomEpoch, EpochKeys, EncodeRekeyOptions, RekeyNotice, PeekRekeyOptions, DecodeRekeyOptions, EpochRequestAdmissionOptions, EncodeEpochRequestOptions, DecodeEpochRequestOptions, EpochRequest, EpochRefusal, EncodeEpochGrantOptions, DecodeEpochGrantOptions, EpochGrant, HostRoomEpochOptions, RequestRoomEpochOptions, SignAdminsOptions, SignChannelsOptions, VerifyChannelsOptions, VerifyAdminsOptions, } from './epoch.js';
|
|
29
|
+
export { EPOCH_ID_INFO, EPOCH_KEY_INFO, MAX_EPOCH, EPOCH_REQUEST_KEY_INFO, generateEpochSecret, deriveEpoch, encodeRekeyEvent, peekRekeyEvent, readMemberList, REPORT_UNKNOWN_EVERY_SECONDS, decodeRekeyEvent, deriveEpochRequestKey, epochRequestAdmission, encodeEpochRequest, decodeEpochRequest, encodeEpochGrant, decodeEpochGrant, hostRoomEpoch, sealCredential, EpochRefusedError, requestRoomEpoch, canonicalAdmins, CHANNEL_NAME, RESERVED_CHANNELS, canonicalChannels, signChannels, verifyChannels, signAdmins, verifyAdmins, EPOCH_LABELS, } from './epoch.js';
|
|
30
|
+
export type { RoomEpoch, EpochKeys, EncodeRekeyOptions, RekeyRecipient, RekeyNotice, PeekRekeyOptions, DecodeRekeyOptions, EpochRequestAdmissionOptions, EncodeEpochRequestOptions, DecodeEpochRequestOptions, EpochRequest, EpochRefusal, EncodeEpochGrantOptions, DecodeEpochGrantOptions, EpochGrant, HostRoomEpochOptions, RequestRoomEpochOptions, SignAdminsOptions, SignChannelsOptions, VerifyChannelsOptions, VerifyAdminsOptions, } from './epoch.js';
|
|
29
31
|
export { EPOCH_COMMIT_PREFIX, epochCommitment, EPOCH_COMMIT_LABELS } from './epoch-commit.js';
|
|
30
32
|
export { MEMBER_EPOCH_KINDS, MEMBER_EPOCH_REQUEST_KEY_INFO, MAX_MEMBER_EPOCH_CHAIN, deriveMemberEpochRequestKey, readRekeyEvidence, encodeMemberEpochRequest, decodeMemberEpochRequest, encodeMemberEpochGrant, decodeMemberEpochGrant, hostMemberEpochDesk, memberEpochSource, requestMemberEpoch, MEMBER_EPOCH_LABELS, } from './member-epoch.js';
|
|
31
33
|
export type { RekeyEvidence, EncodeMemberEpochRequestOptions, DecodeMemberEpochRequestOptions, MemberEpochRequest, EncodeMemberEpochGrantOptions, DecodeMemberEpochGrantOptions, MemberEpochGrant, HostMemberEpochDeskOptions, MemberEpochRequestOptions, MemberEpochSource, } from './member-epoch.js';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { verifyEventUncached, boundedEventVerifier } from './verify.js';
|
|
|
5
5
|
export { localIdentity } from './identity.js';
|
|
6
6
|
export { KINDS } from './kinds.js';
|
|
7
7
|
export { createDeviceCredential, verifyDeviceCredential, PERSON_CREDENTIAL_MAX_SECONDS, RestampedCredentialExpiryError, } from './credential.js';
|
|
8
|
+
export { SEAL_TAG, isSealPubkey, generateSealKey, credentialSeal, sealTarget, sealTo, openSealed, newerCredential, } from './seal.js';
|
|
8
9
|
export { deriveScoped, SCOPED_LABEL_PATTERN } from './scoped.js';
|
|
9
10
|
export { createSubKeyCertificate, verifySubKeyCertificate, SUB_KEY_CERTIFICATE_SCOPE, } from './sub-cert.js';
|
|
10
11
|
export { generateRoomSecret, deriveRoom, encodeJoinUrl, decodeJoinUrl, parseRoomPolicy, ROOM_LABELS, } from './room.js';
|
|
@@ -16,7 +17,7 @@ export { encodePersistentInvitation, decodePersistentInvitation, requestPersiste
|
|
|
16
17
|
export { withExpiration, isRoomEnds, requireRoomEnds, MAX_ROOM_ENDS_SECONDS } from './expiration.js';
|
|
17
18
|
export { isInvitationRelays, requireInvitationRelays, MAX_INVITATION_RELAYS } from './invitation-relays.js';
|
|
18
19
|
export { MAX_ROOM_LINK_FRAGMENT_LENGTH, parseRoomLink, encodeRoomLink, } from './link.js';
|
|
19
|
-
export { EPOCH_ID_INFO, EPOCH_KEY_INFO, MAX_EPOCH, EPOCH_REQUEST_KEY_INFO, generateEpochSecret, deriveEpoch, encodeRekeyEvent, peekRekeyEvent, decodeRekeyEvent, deriveEpochRequestKey, epochRequestAdmission, encodeEpochRequest, decodeEpochRequest, encodeEpochGrant, decodeEpochGrant, hostRoomEpoch, EpochRefusedError, requestRoomEpoch, canonicalAdmins, CHANNEL_NAME, RESERVED_CHANNELS, canonicalChannels, signChannels, verifyChannels, signAdmins, verifyAdmins, EPOCH_LABELS, } from './epoch.js';
|
|
20
|
+
export { EPOCH_ID_INFO, EPOCH_KEY_INFO, MAX_EPOCH, EPOCH_REQUEST_KEY_INFO, generateEpochSecret, deriveEpoch, encodeRekeyEvent, peekRekeyEvent, readMemberList, REPORT_UNKNOWN_EVERY_SECONDS, decodeRekeyEvent, deriveEpochRequestKey, epochRequestAdmission, encodeEpochRequest, decodeEpochRequest, encodeEpochGrant, decodeEpochGrant, hostRoomEpoch, sealCredential, EpochRefusedError, requestRoomEpoch, canonicalAdmins, CHANNEL_NAME, RESERVED_CHANNELS, canonicalChannels, signChannels, verifyChannels, signAdmins, verifyAdmins, EPOCH_LABELS, } from './epoch.js';
|
|
20
21
|
export { EPOCH_COMMIT_PREFIX, epochCommitment, EPOCH_COMMIT_LABELS } from './epoch-commit.js';
|
|
21
22
|
export { MEMBER_EPOCH_KINDS, MEMBER_EPOCH_REQUEST_KEY_INFO, MAX_MEMBER_EPOCH_CHAIN, deriveMemberEpochRequestKey, readRekeyEvidence, encodeMemberEpochRequest, decodeMemberEpochRequest, encodeMemberEpochGrant, decodeMemberEpochGrant, hostMemberEpochDesk, memberEpochSource, requestMemberEpoch, MEMBER_EPOCH_LABELS, } from './member-epoch.js';
|
|
22
23
|
export { deriveChannel, CHANNEL_ID_INFO, CHANNEL_KEY_INFO, MAX_CHANNEL_NAME_LENGTH, CHANNEL_LABELS } from './channel.js';
|