@noy-db/on-magic-link 0.2.0-pre.3 → 0.2.0-pre.31

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/dist/index.d.cts DELETED
@@ -1,526 +0,0 @@
1
- import { NoydbStore, createNoydb, PassphrasePolicy, Noydb, Role, FactorProof, MagicLinkGrantPayload, UnlockedKeyring, Vault } from '@noy-db/hub';
2
- export { MAGIC_LINK_GRANTS_COLLECTION, MagicLinkGrantPayload, deriveMagicLinkContentKey } from '@noy-db/hub';
3
-
4
- /**
5
- * **Invite + peer-recovery primitives** — issue #32.
6
- *
7
- * Layered on top of `db.grant` (for invite, mints a NEW user) and
8
- * `db.recoverUser` (for peer-recovery, rewraps an EXISTING user under
9
- * a fresh temp passphrase). These flows are SIBLINGS of the existing
10
- * delegation-grant primitives in `./index.ts` — different threat
11
- * model, different on-disk audit shape, no server-held secret.
12
- *
13
- * ## Threat model
14
- *
15
- * The temp passphrase travels in the **URL fragment** — server-blind
16
- * transport (fragments don't traverse TLS proxies, don't appear in
17
- * access logs). Trade-off: any party who sees the URL can claim the
18
- * invite once. Single-use semantics close the window: `acceptInvite`
19
- * rotates the passphrase atomically, marking the audit doc accepted —
20
- * a second `acceptInvite` call rejects.
21
- *
22
- * ## What this module does NOT do
23
- *
24
- * - Send emails / construct HTTPS URLs (that's the application layer)
25
- * - Validate the invite under HTTPS (caller's responsibility)
26
- * - Coordinate with a server-held secret (delegation grants do that;
27
- * invite is server-blind by design)
28
- *
29
- * @see #32 #33 #34
30
- *
31
- * @module
32
- */
33
-
34
- /** Whether the payload mints a NEW user (invite) or rewraps an existing one (peer-recovery). */
35
- type InviteKind = 'invite' | 'peer-recovery';
36
- /**
37
- * Serializable payload encoded into the URL fragment. The temp
38
- * passphrase is the secret; the rest is metadata the recipient
39
- * needs to open the right vault as the right user.
40
- */
41
- interface InvitePayload {
42
- /** ULID identifying this invite — used to look up the audit doc. */
43
- readonly tokenId: string;
44
- readonly vault: string;
45
- readonly userId: string;
46
- readonly displayName?: string;
47
- readonly role?: Role;
48
- readonly kind: InviteKind;
49
- /** Issuer's userId (for forensics; not enforced cryptographically). */
50
- readonly issuer: string;
51
- /** Single-use temporary passphrase — replaced on `acceptInvite`. */
52
- readonly tempPhrase: string;
53
- readonly expiresAt: string;
54
- }
55
- /** Audit doc persisted at `_meta/invite-audit-<tokenId>`. */
56
- interface InviteAuditDoc {
57
- readonly _noydb_invite_audit: 1;
58
- readonly tokenId: string;
59
- readonly kind: InviteKind;
60
- readonly issuer: string;
61
- readonly target: string;
62
- readonly expiresAt: string;
63
- readonly issuedAt: string;
64
- readonly revokedAt?: string;
65
- readonly acceptedAt?: string;
66
- }
67
- interface IssueInviteOptions {
68
- readonly userId: string;
69
- readonly displayName: string;
70
- readonly role: Role;
71
- readonly ttlMs?: number;
72
- /** Override the generated temp phrase (rare; deterministic tests). */
73
- readonly tempPhrase?: string;
74
- }
75
- interface IssuePeerRecoveryOptions {
76
- readonly userId: string;
77
- readonly displayName?: string;
78
- readonly role?: Role;
79
- readonly ttlMs?: number;
80
- readonly tempPhrase?: string;
81
- }
82
- interface IssueInviteResult {
83
- readonly payload: InvitePayload;
84
- /**
85
- * URL-fragment-safe base64url encoding of the JSON payload. Embed
86
- * after a `#` in the application's invite URL. Decoded back at
87
- * accept time via `decodeInvitePayload`.
88
- */
89
- readonly encoded: string;
90
- }
91
- interface AcceptInviteOptions {
92
- /**
93
- * The recipient's NoydbStore. Typically the same shared store the
94
- * issuer used (e.g. a sync-peer pointing at a common backend) — the
95
- * recipient must reach the same `_keyring/<userId>` and
96
- * `_meta/invite-audit-<tokenId>` documents.
97
- */
98
- readonly store: NoydbStore;
99
- /**
100
- * The recipient's chosen new passphrase. `acceptInvite` rotates
101
- * the temp phrase to this value atomically before returning. Must
102
- * pass the vault's phrase strength policy (or the recipient must
103
- * pass `validatePassphrase: false` via the underlying createNoydb
104
- * options — currently exposed via `noydbOptions`).
105
- */
106
- readonly newPhrase: string;
107
- /**
108
- * Reference clock for TTL evaluation. Production callers leave
109
- * this `undefined`; tests pass a fixed date.
110
- */
111
- readonly now?: Date;
112
- /**
113
- * Extra options forwarded to `createNoydb` when the recipient's
114
- * session opens. Useful for `policy`, `validatePassphrase`, or
115
- * `sessionPolicy` tweaks the application layer wants in scope.
116
- */
117
- readonly noydbOptions?: Omit<Parameters<typeof createNoydb>[0], 'store' | 'user' | 'secret'>;
118
- /**
119
- * Forwarded to the inner `keyringRotatePassphrase` call so the
120
- * recipient's `newPhrase` is validated against the same policy the
121
- * vault uses elsewhere. Without this, the rotation step applies the
122
- * default phrase validator (lowercase letters + spaces) regardless
123
- * of any `customValidator` / `pattern` set on the vault — a
124
- * consumer using non-default phrase shapes (e.g. hyphen-separated
125
- * alphanumeric, BIP-39 word-lists, Thai/EN-mixed scripts) hits a
126
- * spurious `WeakPassphraseError`.
127
- *
128
- * `noydbOptions.policy.passphrase` is NOT auto-derived because
129
- * `noydbOptions` flows to the post-rotation `createNoydb`, not the
130
- * rotation itself. Passing the same `PassphrasePolicy` here and
131
- * (via `noydbOptions.policy.passphrase`) keeps both gates aligned.
132
- *
133
- * Added in pre.9 (#53).
134
- */
135
- readonly passphrasePolicy?: PassphrasePolicy;
136
- /**
137
- * Skip phrase strength validation for the recipient's `newPhrase`.
138
- * Forwarded to the inner rotation. Use sparingly — bypasses the
139
- * structural rules that protect against weak phrases.
140
- *
141
- * Added in pre.9 (#53).
142
- */
143
- readonly allowWeakPassphrase?: boolean;
144
- }
145
- interface AcceptInviteResult {
146
- readonly db: Noydb;
147
- readonly payload: InvitePayload;
148
- }
149
- declare class InviteExpiredError extends Error {
150
- readonly expiresAt: string;
151
- readonly code: "INVITE_EXPIRED";
152
- constructor(expiresAt: string);
153
- }
154
- declare class InviteRevokedError extends Error {
155
- readonly tokenId: string;
156
- readonly revokedAt: string;
157
- readonly code: "INVITE_REVOKED";
158
- constructor(tokenId: string, revokedAt: string);
159
- }
160
- declare class InviteAlreadyAcceptedError extends Error {
161
- readonly tokenId: string;
162
- readonly acceptedAt: string;
163
- readonly code: "INVITE_ALREADY_ACCEPTED";
164
- constructor(tokenId: string, acceptedAt: string);
165
- }
166
- declare class InviteAuditMissingError extends Error {
167
- readonly tokenId: string;
168
- readonly code: "INVITE_AUDIT_MISSING";
169
- constructor(tokenId: string);
170
- }
171
- /**
172
- * Mint an invite for a NEW user. Generates a random temp phrase,
173
- * calls `db.grant({ userId, passphrase: tempPhrase })`, writes the
174
- * audit doc, and returns the URL-encodable payload.
175
- *
176
- * The recipient claims via `acceptInvite(encoded, { store, newPhrase })`;
177
- * the rotation inside `acceptInvite` invalidates the temp phrase
178
- * (single-use by construction).
179
- *
180
- * @throws Whatever `db.grant` throws (PrivilegeEscalationError,
181
- * WeakPassphraseError if the temp phrase fails policy, …)
182
- */
183
- declare function issueInvite(db: Noydb, vault: string, options: IssueInviteOptions): Promise<IssueInviteResult>;
184
- /**
185
- * Mint a peer-recovery for an EXISTING user. Generates a random temp
186
- * phrase, calls `db.recoverUser` (atomic; closes the partial-failure
187
- * window of compose-from-primitives), writes the audit doc, returns
188
- * the payload.
189
- *
190
- * Owner→owner is allowed (the policy gate `peer-recover-user` carries
191
- * the freshness factor). The `factors` argument forwards to the gate.
192
- */
193
- declare function issuePeerRecovery(db: Noydb, vault: string, options: IssuePeerRecoveryOptions, factors?: {
194
- factors?: ReadonlyArray<FactorProof>;
195
- sharedDevice?: boolean;
196
- }): Promise<IssueInviteResult>;
197
- /**
198
- * Mark an outstanding invite as revoked. After this, `acceptInvite`
199
- * for the same token rejects with `InviteRevokedError` BEFORE opening
200
- * any session — closes #32's "revoked-link silent shadow keyring"
201
- * defense (without the audit doc check, a revoked link could fall
202
- * through to `createNoydb`'s no-keyring auto-create path and create
203
- * a fresh empty vault).
204
- *
205
- * Idempotent — revoking an already-revoked invite is a no-op.
206
- *
207
- * Note: this does NOT delete the granted/recovered keyring; it only
208
- * marks the invite token as unusable. To fully cancel the invite,
209
- * call `db.revoke(vault, { userId })` separately. The two are split
210
- * because peer-recovery doesn't have a "cancel" — the target user
211
- * already existed.
212
- */
213
- declare function revokeInvite(db: Noydb, vault: string, encodedOrPayload: string | InvitePayload): Promise<void>;
214
- /**
215
- * Open the recipient's session under the invite. Validates TTL +
216
- * revoke + already-accepted, opens `createNoydb` with the temp
217
- * phrase, immediately rotates to `newPhrase`, marks the audit doc
218
- * accepted, returns the live `Noydb` instance.
219
- *
220
- * Single-use is enforced two ways:
221
- * 1. The rotation inside this function invalidates the temp phrase.
222
- * 2. The audit doc's `acceptedAt` field is set on success — a
223
- * second `acceptInvite` call sees it and throws
224
- * `InviteAlreadyAcceptedError`.
225
- *
226
- * @throws {@link InviteExpiredError} when TTL has passed.
227
- * @throws {@link InviteRevokedError} when the issuer has revoked.
228
- * @throws {@link InviteAlreadyAcceptedError} on second call.
229
- * @throws {@link InviteAuditMissingError} when no audit doc — closes
230
- * #32's revoked-link-shadow-keyring defense.
231
- */
232
- declare function acceptInvite(encoded: string, options: AcceptInviteOptions): Promise<AcceptInviteResult>;
233
- /** Encode the payload as a URL-fragment-safe base64url string. */
234
- declare function encodeInvitePayload(payload: InvitePayload): string;
235
- /** Decode a base64url string back to an InvitePayload. */
236
- declare function decodeInvitePayload(encoded: string): InvitePayload;
237
-
238
- /**
239
- * **@noy-db/on-magic-link** — one-time-link viewer unlock for noy-db.
240
- *
241
- * A magic link is a single-use URL that opens a vault in a read-only,
242
- * viewer-scoped session WITHOUT entering a passphrase. The link
243
- * expires after use or after a configurable TTL; the resulting
244
- * session is strictly limited to the `viewer` role.
245
- *
246
- * Part of the `@noy-db/on-*` authentication family. Sibling packages:
247
- * `@noy-db/on-oidc` (federated login), `@noy-db/on-webauthn` (passkey
248
- * / biometric). All follow the same shape: enrol once, produce a
249
- * short-lived token, unwrap a viewer keyring at unlock.
250
- *
251
- * ## Security model
252
- *
253
- * The viewer KEK is derived via:
254
- *
255
- * ```
256
- * HKDF-SHA256(
257
- * ikm = serverSecret,
258
- * salt = sha256(token),
259
- * info = "noydb-magic-link-v1:" + vaultId,
260
- * )
261
- * ```
262
- *
263
- * - `serverSecret` is a server-held secret that the SERVER knows but
264
- * is NOT embedded in the link. If the link is intercepted, the
265
- * attacker cannot derive the KEK without the server secret.
266
- * - `token` is a ULID embedded in the URL. It is single-use at the
267
- * application layer (the server marks it consumed after first use).
268
- * - `vaultId` binds the derived key to a specific vault — a token for
269
- * vault A cannot be used to unlock vault B.
270
- *
271
- * The resulting keyring is ALWAYS viewer-scoped (`role: 'viewer'`).
272
- * The DEKs available to the viewer are only the collections in the
273
- * viewer-specific subset, determined by the admin who created the link.
274
- *
275
- * ## What this package is NOT
276
- *
277
- * This module provides the CRYPTO layer only — it does not:
278
- * - Issue HTTP tokens or send emails (that's the application layer)
279
- * - Mark tokens as consumed (that's the server's responsibility)
280
- * - Store viewer keyrings in the adapter (callers do this via `grant()`)
281
- *
282
- * ## Usage
283
- *
284
- * ```ts
285
- * import {
286
- * createMagicLinkToken,
287
- * deriveMagicLinkKEK,
288
- * isMagicLinkValid,
289
- * buildMagicLinkKeyring,
290
- * } from '@noy-db/on-magic-link'
291
- *
292
- * // SERVER — mint a token + grant the viewer keyring
293
- * const token = createMagicLinkToken('company-a', { ttlMs: 24 * 60 * 60 * 1000 })
294
- * const kek = await deriveMagicLinkKEK(serverSecret, token.token, 'company-a')
295
- * // ... use kek + db.grant(...) to create a viewer keyring entry ...
296
- *
297
- * // Email the link, e.g. https://app.example.com/view?t=<token.token>
298
- *
299
- * // CLIENT — derive the same KEK and unlock
300
- * if (!isMagicLinkValid(token)) throw new Error('expired')
301
- * const sameKek = await deriveMagicLinkKEK(serverSecret, token.token, token.vault)
302
- * const keyring = buildMagicLinkKeyring({ ... })
303
- * ```
304
- *
305
- * @packageDocumentation
306
- */
307
-
308
- /** Default magic-link TTL: 24 hours. */
309
- declare const MAGIC_LINK_DEFAULT_TTL_MS: number;
310
- /**
311
- * The serializable metadata describing a magic link.
312
- * Embed `token` in the link URL as a query parameter or path segment.
313
- */
314
- interface MagicLinkToken {
315
- /** Unique one-time token (ULID). Embed this in the URL. */
316
- readonly token: string;
317
- /** The vault this link unlocks (viewer-only). */
318
- readonly vault: string;
319
- /** ISO timestamp after which the link is invalid. */
320
- readonly expiresAt: string;
321
- /** Role of the resulting session. Always `'viewer'` for magic links. */
322
- readonly role: 'viewer';
323
- }
324
- /** Options for `createMagicLinkToken()`. */
325
- interface CreateMagicLinkOptions {
326
- /** Link lifetime in milliseconds. Default: 24 hours. */
327
- ttlMs?: number;
328
- }
329
- /**
330
- * Derive a viewer KEK from the server secret and the magic-link token.
331
- *
332
- * Both the server (at grant time) and the client (at unlock time) call
333
- * this with the same inputs to get the same key. The key is used to:
334
- * - Server: derive the KEK, call `db.grant()` to create a viewer keyring.
335
- * - Client: derive the KEK, call `db.openVault()` / `loadKeyring()` with
336
- * this KEK directly (bypassing PBKDF2) to unlock the viewer session.
337
- *
338
- * @param serverSecret - Server-held secret (never sent to the client).
339
- * @param token - The ULID from the magic-link URL.
340
- * @param vault - The vault ID this link is for.
341
- */
342
- declare function deriveMagicLinkKEK(serverSecret: string | Uint8Array<ArrayBuffer>, token: string, vault: string): Promise<CryptoKey>;
343
- /**
344
- * Generate a magic-link token (server-side).
345
- *
346
- * Returns a `MagicLinkToken` whose `token` field should be embedded in
347
- * the URL sent to the viewer. The server must store the token metadata
348
- * (or reconstruct it from the URL) so it can:
349
- * 1. Validate that the token has not expired or been used.
350
- * 2. Call `deriveMagicLinkKEK()` to create the viewer keyring.
351
- *
352
- * @param vault - The vault to grant viewer access to.
353
- * @param options - Optional TTL configuration.
354
- */
355
- declare function createMagicLinkToken(vault: string, options?: CreateMagicLinkOptions): MagicLinkToken;
356
- /**
357
- * Validate that a magic-link token is not expired.
358
- * Returns `true` if valid, `false` if expired.
359
- *
360
- * Single-use enforcement (marking a token as consumed after first use)
361
- * is the server's responsibility — this function only checks `expiresAt`.
362
- */
363
- declare function isMagicLinkValid(linkToken: MagicLinkToken): boolean;
364
- /**
365
- * Build a stub `UnlockedKeyring` from the magic-link-derived KEK and
366
- * the viewer's DEK set.
367
- *
368
- * This is a thin wrapper for callers that have already:
369
- * 1. Called `deriveMagicLinkKEK()` to get the viewer KEK.
370
- * 2. Loaded the viewer's keyring from the adapter (which holds the DEKs
371
- * wrapped with the magic-link KEK).
372
- * 3. Unwrapped the DEKs.
373
- *
374
- * The resulting keyring is always viewer-scoped. Callers who want to turn
375
- * it into a session token should call `createSession()` from
376
- * `@noy-db/hub/session`.
377
- */
378
- declare function buildMagicLinkKeyring(opts: {
379
- viewerUserId: string;
380
- displayName: string;
381
- deks: Map<string, CryptoKey>;
382
- kek: CryptoKey;
383
- salt: Uint8Array;
384
- }): UnlockedKeyring;
385
- /**
386
- * Single grant within a batch magic-link issue. The grantor specifies
387
- * the tier + scope; the package handles the wrapping. `record` is
388
- * optional and narrows the grant to a single record id in the
389
- * collection (leave undefined for a whole-collection grant).
390
- */
391
- interface MagicLinkGrantSpec {
392
- readonly toUser: string;
393
- readonly tier: number;
394
- readonly collection?: string;
395
- readonly record?: string;
396
- readonly until: Date | string;
397
- readonly note?: string;
398
- }
399
- interface IssueMagicLinkDelegationOptions {
400
- /**
401
- * Server-held secret that gates access to the grant. Same value must
402
- * be supplied at claim time — the server is the only party that
403
- * knows it, so a leaked URL alone cannot unlock anything.
404
- */
405
- readonly serverSecret: string | Uint8Array<ArrayBuffer>;
406
- /**
407
- * One or more grants to persist under the same magic-link token.
408
- * Single-element arrays cover the common "one collection to one
409
- * user" case; multi-element arrays support scoped cross-collection
410
- * delegations (e.g. client portal: invoices + payments + etax).
411
- */
412
- readonly grants: readonly MagicLinkGrantSpec[];
413
- /**
414
- * Magic-link TTL. Controls `link.expiresAt` (the URL freshness).
415
- * Each grant's own `until` bounds the *delegation* lifetime — the
416
- * claimant only receives DEKs for grants whose `until` is still
417
- * future at claim time. Default 24 h.
418
- */
419
- readonly ttlMs?: number;
420
- /**
421
- * Optional override for the ULID embedded in the URL. Rarely useful
422
- * outside deterministic tests.
423
- */
424
- readonly token?: string;
425
- }
426
- interface IssueMagicLinkDelegationResult {
427
- /** URL-embeddable token metadata. Serialize `link.token` into the link. */
428
- readonly link: MagicLinkToken;
429
- /** One record per grant — mirrors the input array order. */
430
- readonly grants: ReadonlyArray<{
431
- readonly recordId: string;
432
- readonly payload: MagicLinkGrantPayload;
433
- }>;
434
- }
435
- /**
436
- * Issue a magic-link-bound delegation.
437
- *
438
- * Server-side workflow:
439
- *
440
- * ```ts
441
- * import { issueMagicLinkDelegation } from '@noy-db/on-magic-link'
442
- *
443
- * const { link, grants } = await issueMagicLinkDelegation(vault, {
444
- * serverSecret: process.env.MAGIC_LINK_SECRET!,
445
- * grants: [
446
- * { toUser: 'auditor-bob', tier: 1, collection: 'invoices',
447
- * until: new Date(Date.now() + 48*3600e3) },
448
- * ],
449
- * ttlMs: 48 * 3600 * 1000,
450
- * })
451
- *
452
- * // Embed link.token in an email URL. The grantee clicks → loads your
453
- * // client → calls claimMagicLinkDelegation() with the same serverSecret.
454
- * ```
455
- */
456
- declare function issueMagicLinkDelegation(vault: Vault, options: IssueMagicLinkDelegationOptions): Promise<IssueMagicLinkDelegationResult>;
457
- interface ClaimMagicLinkDelegationOptions {
458
- readonly store: NoydbStore;
459
- readonly vault: string;
460
- readonly serverSecret: string | Uint8Array<ArrayBuffer>;
461
- readonly token: string;
462
- /**
463
- * Reference clock used to evaluate grant expiry. Production callers
464
- * leave this `undefined`; tests pass a fixed date.
465
- */
466
- readonly now?: Date;
467
- }
468
- interface ClaimedMagicLinkGrant {
469
- readonly payload: MagicLinkGrantPayload;
470
- /** Tier DEK, ready to insert into a keyring map. */
471
- readonly dek: CryptoKey;
472
- /** True when the grant's `until` has already passed. */
473
- readonly expired: boolean;
474
- }
475
- interface ClaimMagicLinkDelegationResult {
476
- /**
477
- * False when the server secret is wrong, the vault is wrong, or
478
- * every grant is malformed. A `true` with an empty `grants` array
479
- * means the record was deleted (revoked) between issue and claim.
480
- */
481
- readonly valid: boolean;
482
- readonly grants: readonly ClaimedMagicLinkGrant[];
483
- }
484
- /**
485
- * Client-side flow. Derives the same content key + KEK as the grantor,
486
- * loads every grant persisted under the token (primary + batch
487
- * entries), and returns the unwrapped tier DEKs.
488
- *
489
- * The caller decides what to do with them — typically inserting them
490
- * into a freshly-built `UnlockedKeyring` (see `buildMagicLinkKeyring`)
491
- * and opening a viewer session.
492
- */
493
- declare function claimMagicLinkDelegation(options: ClaimMagicLinkDelegationOptions): Promise<ClaimMagicLinkDelegationResult>;
494
- /**
495
- * Read (without unwrapping) the grants under a token — useful for an
496
- * audit UI that shows the grantor what's still live on a link.
497
- */
498
- declare function inspectMagicLinkDelegation(options: {
499
- readonly store: NoydbStore;
500
- readonly vault: string;
501
- readonly serverSecret: string | Uint8Array<ArrayBuffer>;
502
- readonly token: string;
503
- }): Promise<readonly MagicLinkGrantPayload[]>;
504
- /**
505
- * Delete every grant under a token. Idempotent — safe to call on an
506
- * already-revoked or never-existed token. Returns the number of
507
- * records removed.
508
- */
509
- declare function revokeMagicLinkDelegation(options: {
510
- readonly store: NoydbStore;
511
- readonly vault: string;
512
- readonly token: string;
513
- }): Promise<number>;
514
- /**
515
- * Read a single grant by its full record id. Convenience for callers
516
- * that persisted `recordId` during issue and want to resolve just one.
517
- */
518
- declare function readMagicLinkGrant(options: {
519
- readonly store: NoydbStore;
520
- readonly vault: string;
521
- readonly serverSecret: string | Uint8Array<ArrayBuffer>;
522
- readonly token: string;
523
- readonly recordId: string;
524
- }): Promise<MagicLinkGrantPayload | null>;
525
-
526
- export { type AcceptInviteOptions, type AcceptInviteResult, type ClaimMagicLinkDelegationOptions, type ClaimMagicLinkDelegationResult, type ClaimedMagicLinkGrant, type CreateMagicLinkOptions, InviteAlreadyAcceptedError, type InviteAuditDoc, InviteAuditMissingError, InviteExpiredError, type InviteKind, type InvitePayload, InviteRevokedError, type IssueInviteOptions, type IssueInviteResult, type IssueMagicLinkDelegationOptions, type IssueMagicLinkDelegationResult, type IssuePeerRecoveryOptions, MAGIC_LINK_DEFAULT_TTL_MS, type MagicLinkGrantSpec, type MagicLinkToken, acceptInvite, buildMagicLinkKeyring, claimMagicLinkDelegation, createMagicLinkToken, decodeInvitePayload, deriveMagicLinkKEK, encodeInvitePayload, inspectMagicLinkDelegation, isMagicLinkValid, issueInvite, issueMagicLinkDelegation, issuePeerRecovery, readMagicLinkGrant, revokeInvite, revokeMagicLinkDelegation };