@noy-db/on-magic-link 0.1.0-pre.7 → 0.1.0-pre.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +263 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +210 -2
- package/dist/index.d.ts +210 -2
- package/dist/index.js +245 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,214 @@
|
|
|
1
|
-
import { NoydbStore, MagicLinkGrantPayload, UnlockedKeyring, Vault } from '@noy-db/hub';
|
|
1
|
+
import { NoydbStore, createNoydb, Noydb, Role, FactorProof, MagicLinkGrantPayload, UnlockedKeyring, Vault } from '@noy-db/hub';
|
|
2
2
|
export { MAGIC_LINK_GRANTS_COLLECTION, MagicLinkGrantPayload, deriveMagicLinkContentKey } from '@noy-db/hub';
|
|
3
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
|
+
interface AcceptInviteResult {
|
|
120
|
+
readonly db: Noydb;
|
|
121
|
+
readonly payload: InvitePayload;
|
|
122
|
+
}
|
|
123
|
+
declare class InviteExpiredError extends Error {
|
|
124
|
+
readonly expiresAt: string;
|
|
125
|
+
readonly code: "INVITE_EXPIRED";
|
|
126
|
+
constructor(expiresAt: string);
|
|
127
|
+
}
|
|
128
|
+
declare class InviteRevokedError extends Error {
|
|
129
|
+
readonly tokenId: string;
|
|
130
|
+
readonly revokedAt: string;
|
|
131
|
+
readonly code: "INVITE_REVOKED";
|
|
132
|
+
constructor(tokenId: string, revokedAt: string);
|
|
133
|
+
}
|
|
134
|
+
declare class InviteAlreadyAcceptedError extends Error {
|
|
135
|
+
readonly tokenId: string;
|
|
136
|
+
readonly acceptedAt: string;
|
|
137
|
+
readonly code: "INVITE_ALREADY_ACCEPTED";
|
|
138
|
+
constructor(tokenId: string, acceptedAt: string);
|
|
139
|
+
}
|
|
140
|
+
declare class InviteAuditMissingError extends Error {
|
|
141
|
+
readonly tokenId: string;
|
|
142
|
+
readonly code: "INVITE_AUDIT_MISSING";
|
|
143
|
+
constructor(tokenId: string);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Mint an invite for a NEW user. Generates a random temp phrase,
|
|
147
|
+
* calls `db.grant({ userId, passphrase: tempPhrase })`, writes the
|
|
148
|
+
* audit doc, and returns the URL-encodable payload.
|
|
149
|
+
*
|
|
150
|
+
* The recipient claims via `acceptInvite(encoded, { store, newPhrase })`;
|
|
151
|
+
* the rotation inside `acceptInvite` invalidates the temp phrase
|
|
152
|
+
* (single-use by construction).
|
|
153
|
+
*
|
|
154
|
+
* @throws Whatever `db.grant` throws (PrivilegeEscalationError,
|
|
155
|
+
* WeakPassphraseError if the temp phrase fails policy, …)
|
|
156
|
+
*/
|
|
157
|
+
declare function issueInvite(db: Noydb, vault: string, options: IssueInviteOptions): Promise<IssueInviteResult>;
|
|
158
|
+
/**
|
|
159
|
+
* Mint a peer-recovery for an EXISTING user. Generates a random temp
|
|
160
|
+
* phrase, calls `db.recoverUser` (atomic; closes the partial-failure
|
|
161
|
+
* window of compose-from-primitives), writes the audit doc, returns
|
|
162
|
+
* the payload.
|
|
163
|
+
*
|
|
164
|
+
* Owner→owner is allowed (the policy gate `peer-recover-user` carries
|
|
165
|
+
* the freshness factor). The `factors` argument forwards to the gate.
|
|
166
|
+
*/
|
|
167
|
+
declare function issuePeerRecovery(db: Noydb, vault: string, options: IssuePeerRecoveryOptions, factors?: {
|
|
168
|
+
factors?: ReadonlyArray<FactorProof>;
|
|
169
|
+
sharedDevice?: boolean;
|
|
170
|
+
}): Promise<IssueInviteResult>;
|
|
171
|
+
/**
|
|
172
|
+
* Mark an outstanding invite as revoked. After this, `acceptInvite`
|
|
173
|
+
* for the same token rejects with `InviteRevokedError` BEFORE opening
|
|
174
|
+
* any session — closes #32's "revoked-link silent shadow keyring"
|
|
175
|
+
* defense (without the audit doc check, a revoked link could fall
|
|
176
|
+
* through to `createNoydb`'s no-keyring auto-create path and create
|
|
177
|
+
* a fresh empty vault).
|
|
178
|
+
*
|
|
179
|
+
* Idempotent — revoking an already-revoked invite is a no-op.
|
|
180
|
+
*
|
|
181
|
+
* Note: this does NOT delete the granted/recovered keyring; it only
|
|
182
|
+
* marks the invite token as unusable. To fully cancel the invite,
|
|
183
|
+
* call `db.revoke(vault, { userId })` separately. The two are split
|
|
184
|
+
* because peer-recovery doesn't have a "cancel" — the target user
|
|
185
|
+
* already existed.
|
|
186
|
+
*/
|
|
187
|
+
declare function revokeInvite(db: Noydb, vault: string, encodedOrPayload: string | InvitePayload): Promise<void>;
|
|
188
|
+
/**
|
|
189
|
+
* Open the recipient's session under the invite. Validates TTL +
|
|
190
|
+
* revoke + already-accepted, opens `createNoydb` with the temp
|
|
191
|
+
* phrase, immediately rotates to `newPhrase`, marks the audit doc
|
|
192
|
+
* accepted, returns the live `Noydb` instance.
|
|
193
|
+
*
|
|
194
|
+
* Single-use is enforced two ways:
|
|
195
|
+
* 1. The rotation inside this function invalidates the temp phrase.
|
|
196
|
+
* 2. The audit doc's `acceptedAt` field is set on success — a
|
|
197
|
+
* second `acceptInvite` call sees it and throws
|
|
198
|
+
* `InviteAlreadyAcceptedError`.
|
|
199
|
+
*
|
|
200
|
+
* @throws {@link InviteExpiredError} when TTL has passed.
|
|
201
|
+
* @throws {@link InviteRevokedError} when the issuer has revoked.
|
|
202
|
+
* @throws {@link InviteAlreadyAcceptedError} on second call.
|
|
203
|
+
* @throws {@link InviteAuditMissingError} when no audit doc — closes
|
|
204
|
+
* #32's revoked-link-shadow-keyring defense.
|
|
205
|
+
*/
|
|
206
|
+
declare function acceptInvite(encoded: string, options: AcceptInviteOptions): Promise<AcceptInviteResult>;
|
|
207
|
+
/** Encode the payload as a URL-fragment-safe base64url string. */
|
|
208
|
+
declare function encodeInvitePayload(payload: InvitePayload): string;
|
|
209
|
+
/** Decode a base64url string back to an InvitePayload. */
|
|
210
|
+
declare function decodeInvitePayload(encoded: string): InvitePayload;
|
|
211
|
+
|
|
4
212
|
/**
|
|
5
213
|
* **@noy-db/on-magic-link** — one-time-link viewer unlock for noy-db.
|
|
6
214
|
*
|
|
@@ -289,4 +497,4 @@ declare function readMagicLinkGrant(options: {
|
|
|
289
497
|
readonly recordId: string;
|
|
290
498
|
}): Promise<MagicLinkGrantPayload | null>;
|
|
291
499
|
|
|
292
|
-
export { type ClaimMagicLinkDelegationOptions, type ClaimMagicLinkDelegationResult, type ClaimedMagicLinkGrant, type CreateMagicLinkOptions, type IssueMagicLinkDelegationOptions, type IssueMagicLinkDelegationResult, MAGIC_LINK_DEFAULT_TTL_MS, type MagicLinkGrantSpec, type MagicLinkToken, buildMagicLinkKeyring, claimMagicLinkDelegation, createMagicLinkToken, deriveMagicLinkKEK, inspectMagicLinkDelegation, isMagicLinkValid, issueMagicLinkDelegation, readMagicLinkGrant, revokeMagicLinkDelegation };
|
|
500
|
+
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,214 @@
|
|
|
1
|
-
import { NoydbStore, MagicLinkGrantPayload, UnlockedKeyring, Vault } from '@noy-db/hub';
|
|
1
|
+
import { NoydbStore, createNoydb, Noydb, Role, FactorProof, MagicLinkGrantPayload, UnlockedKeyring, Vault } from '@noy-db/hub';
|
|
2
2
|
export { MAGIC_LINK_GRANTS_COLLECTION, MagicLinkGrantPayload, deriveMagicLinkContentKey } from '@noy-db/hub';
|
|
3
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
|
+
interface AcceptInviteResult {
|
|
120
|
+
readonly db: Noydb;
|
|
121
|
+
readonly payload: InvitePayload;
|
|
122
|
+
}
|
|
123
|
+
declare class InviteExpiredError extends Error {
|
|
124
|
+
readonly expiresAt: string;
|
|
125
|
+
readonly code: "INVITE_EXPIRED";
|
|
126
|
+
constructor(expiresAt: string);
|
|
127
|
+
}
|
|
128
|
+
declare class InviteRevokedError extends Error {
|
|
129
|
+
readonly tokenId: string;
|
|
130
|
+
readonly revokedAt: string;
|
|
131
|
+
readonly code: "INVITE_REVOKED";
|
|
132
|
+
constructor(tokenId: string, revokedAt: string);
|
|
133
|
+
}
|
|
134
|
+
declare class InviteAlreadyAcceptedError extends Error {
|
|
135
|
+
readonly tokenId: string;
|
|
136
|
+
readonly acceptedAt: string;
|
|
137
|
+
readonly code: "INVITE_ALREADY_ACCEPTED";
|
|
138
|
+
constructor(tokenId: string, acceptedAt: string);
|
|
139
|
+
}
|
|
140
|
+
declare class InviteAuditMissingError extends Error {
|
|
141
|
+
readonly tokenId: string;
|
|
142
|
+
readonly code: "INVITE_AUDIT_MISSING";
|
|
143
|
+
constructor(tokenId: string);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Mint an invite for a NEW user. Generates a random temp phrase,
|
|
147
|
+
* calls `db.grant({ userId, passphrase: tempPhrase })`, writes the
|
|
148
|
+
* audit doc, and returns the URL-encodable payload.
|
|
149
|
+
*
|
|
150
|
+
* The recipient claims via `acceptInvite(encoded, { store, newPhrase })`;
|
|
151
|
+
* the rotation inside `acceptInvite` invalidates the temp phrase
|
|
152
|
+
* (single-use by construction).
|
|
153
|
+
*
|
|
154
|
+
* @throws Whatever `db.grant` throws (PrivilegeEscalationError,
|
|
155
|
+
* WeakPassphraseError if the temp phrase fails policy, …)
|
|
156
|
+
*/
|
|
157
|
+
declare function issueInvite(db: Noydb, vault: string, options: IssueInviteOptions): Promise<IssueInviteResult>;
|
|
158
|
+
/**
|
|
159
|
+
* Mint a peer-recovery for an EXISTING user. Generates a random temp
|
|
160
|
+
* phrase, calls `db.recoverUser` (atomic; closes the partial-failure
|
|
161
|
+
* window of compose-from-primitives), writes the audit doc, returns
|
|
162
|
+
* the payload.
|
|
163
|
+
*
|
|
164
|
+
* Owner→owner is allowed (the policy gate `peer-recover-user` carries
|
|
165
|
+
* the freshness factor). The `factors` argument forwards to the gate.
|
|
166
|
+
*/
|
|
167
|
+
declare function issuePeerRecovery(db: Noydb, vault: string, options: IssuePeerRecoveryOptions, factors?: {
|
|
168
|
+
factors?: ReadonlyArray<FactorProof>;
|
|
169
|
+
sharedDevice?: boolean;
|
|
170
|
+
}): Promise<IssueInviteResult>;
|
|
171
|
+
/**
|
|
172
|
+
* Mark an outstanding invite as revoked. After this, `acceptInvite`
|
|
173
|
+
* for the same token rejects with `InviteRevokedError` BEFORE opening
|
|
174
|
+
* any session — closes #32's "revoked-link silent shadow keyring"
|
|
175
|
+
* defense (without the audit doc check, a revoked link could fall
|
|
176
|
+
* through to `createNoydb`'s no-keyring auto-create path and create
|
|
177
|
+
* a fresh empty vault).
|
|
178
|
+
*
|
|
179
|
+
* Idempotent — revoking an already-revoked invite is a no-op.
|
|
180
|
+
*
|
|
181
|
+
* Note: this does NOT delete the granted/recovered keyring; it only
|
|
182
|
+
* marks the invite token as unusable. To fully cancel the invite,
|
|
183
|
+
* call `db.revoke(vault, { userId })` separately. The two are split
|
|
184
|
+
* because peer-recovery doesn't have a "cancel" — the target user
|
|
185
|
+
* already existed.
|
|
186
|
+
*/
|
|
187
|
+
declare function revokeInvite(db: Noydb, vault: string, encodedOrPayload: string | InvitePayload): Promise<void>;
|
|
188
|
+
/**
|
|
189
|
+
* Open the recipient's session under the invite. Validates TTL +
|
|
190
|
+
* revoke + already-accepted, opens `createNoydb` with the temp
|
|
191
|
+
* phrase, immediately rotates to `newPhrase`, marks the audit doc
|
|
192
|
+
* accepted, returns the live `Noydb` instance.
|
|
193
|
+
*
|
|
194
|
+
* Single-use is enforced two ways:
|
|
195
|
+
* 1. The rotation inside this function invalidates the temp phrase.
|
|
196
|
+
* 2. The audit doc's `acceptedAt` field is set on success — a
|
|
197
|
+
* second `acceptInvite` call sees it and throws
|
|
198
|
+
* `InviteAlreadyAcceptedError`.
|
|
199
|
+
*
|
|
200
|
+
* @throws {@link InviteExpiredError} when TTL has passed.
|
|
201
|
+
* @throws {@link InviteRevokedError} when the issuer has revoked.
|
|
202
|
+
* @throws {@link InviteAlreadyAcceptedError} on second call.
|
|
203
|
+
* @throws {@link InviteAuditMissingError} when no audit doc — closes
|
|
204
|
+
* #32's revoked-link-shadow-keyring defense.
|
|
205
|
+
*/
|
|
206
|
+
declare function acceptInvite(encoded: string, options: AcceptInviteOptions): Promise<AcceptInviteResult>;
|
|
207
|
+
/** Encode the payload as a URL-fragment-safe base64url string. */
|
|
208
|
+
declare function encodeInvitePayload(payload: InvitePayload): string;
|
|
209
|
+
/** Decode a base64url string back to an InvitePayload. */
|
|
210
|
+
declare function decodeInvitePayload(encoded: string): InvitePayload;
|
|
211
|
+
|
|
4
212
|
/**
|
|
5
213
|
* **@noy-db/on-magic-link** — one-time-link viewer unlock for noy-db.
|
|
6
214
|
*
|
|
@@ -289,4 +497,4 @@ declare function readMagicLinkGrant(options: {
|
|
|
289
497
|
readonly recordId: string;
|
|
290
498
|
}): Promise<MagicLinkGrantPayload | null>;
|
|
291
499
|
|
|
292
|
-
export { type ClaimMagicLinkDelegationOptions, type ClaimMagicLinkDelegationResult, type ClaimedMagicLinkGrant, type CreateMagicLinkOptions, type IssueMagicLinkDelegationOptions, type IssueMagicLinkDelegationResult, MAGIC_LINK_DEFAULT_TTL_MS, type MagicLinkGrantSpec, type MagicLinkToken, buildMagicLinkKeyring, claimMagicLinkDelegation, createMagicLinkToken, deriveMagicLinkKEK, inspectMagicLinkDelegation, isMagicLinkValid, issueMagicLinkDelegation, readMagicLinkGrant, revokeMagicLinkDelegation };
|
|
500
|
+
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 };
|