@aooth/user 0.2.1 → 0.2.3

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 CHANGED
@@ -1,14 +1,11 @@
1
- import { C as TotpConfig, D as UserCredentials, E as UserAuthErrorType, O as UserServiceConfig, S as PolicyCheckResult, T as TrustedDeviceRecord, _ as PasswordData, a as pickDefinedProfile, b as PasswordPolicyEvalFn, c as AccountData, d as LockoutConfig, f as LoginResult, g as PasswordConfig, h as MfaMethodInfo, i as NewFederatedIdentity, k as UserStoreUpdate, l as DeepPartial, m as MfaMethod, n as FederatedIdentityStore, o as UserStore, p as MfaData, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity, u as LockStatus, v as PasswordPolicyContext, w as TransferablePolicy, x as PasswordPolicyInstance, y as PasswordPolicyDef } from "./federated-identity-store-B_jEkl-a.cjs";
2
- //#region src/errors.d.ts
3
- declare class UserAuthError extends Error {
1
+ import { C as TotpConfig, D as UserCredentials, E as UserAuthErrorType, O as UserServiceConfig, S as PolicyCheckResult, T as TrustedDeviceRecord, _ as PasswordData, a as pickDefinedProfile, b as PasswordPolicyEvalFn, c as AccountData, d as LockoutConfig, f as LoginResult, g as PasswordConfig, h as MfaMethodInfo, i as NewFederatedIdentity, k as UserStoreUpdate, l as DeepPartial, m as MfaMethod, n as FederatedIdentityStore, o as UserStore, p as MfaData, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity, u as LockStatus, v as PasswordPolicyContext, w as TransferablePolicy, x as PasswordPolicyInstance, y as PasswordPolicyDef } from "./federated-identity-store-Ds7Ye9qJ.cjs";
2
+ export declare class UserAuthError extends Error {
4
3
  readonly type: UserAuthErrorType;
5
4
  readonly details?: Record<string, unknown> | undefined;
6
5
  readonly name = "UserAuthError";
7
6
  constructor(type: UserAuthErrorType, message?: string, details?: Record<string, unknown> | undefined);
8
7
  }
9
- //#endregion
10
- //#region src/password/hasher.d.ts
11
- declare class PasswordHasher {
8
+ export declare class PasswordHasher {
12
9
  private readonly pepper;
13
10
  private readonly N;
14
11
  private readonly r;
@@ -19,9 +16,7 @@ declare class PasswordHasher {
19
16
  verify(password: string, encoded: string): Promise<boolean>;
20
17
  generatePassword(length?: number): string;
21
18
  }
22
- //#endregion
23
- //#region src/password/policy.d.ts
24
- declare function normalizePolicies(policies?: (PasswordPolicyDef | PasswordPolicyInstance)[]): PasswordPolicy[];
19
+ export declare function normalizePolicies(policies?: (PasswordPolicyDef | PasswordPolicyInstance)[]): PasswordPolicy[];
25
20
  /**
26
21
  * Helper that builds a `PasswordPolicyDef` from a real backend function plus
27
22
  * its bound positional arguments. The function runs directly on the server
@@ -43,13 +38,13 @@ declare function normalizePolicies(policies?: (PasswordPolicyDef | PasswordPolic
43
38
  * `undefined` and `transferable` is false; frontend pre-validation skips it
44
39
  * and only the server check enforces the rule.
45
40
  */
46
- declare function definePasswordPolicy<A extends readonly unknown[]>(opts: {
41
+ export declare function definePasswordPolicy<A extends readonly unknown[]>(opts: {
47
42
  rule: (v: string, ...args: A) => boolean | Promise<boolean>;
48
43
  args?: A;
49
44
  description?: string;
50
45
  errorMessage?: string;
51
46
  }): PasswordPolicyDef;
52
- declare class PasswordPolicy implements PasswordPolicyInstance {
47
+ export declare class PasswordPolicy implements PasswordPolicyInstance {
53
48
  readonly rule: PasswordPolicyEvalFn;
54
49
  readonly serialized?: string;
55
50
  readonly description: string;
@@ -58,8 +53,6 @@ declare class PasswordPolicy implements PasswordPolicyInstance {
58
53
  evaluate(password: string, context?: PasswordPolicyContext): boolean | Promise<boolean>;
59
54
  get transferable(): boolean;
60
55
  }
61
- //#endregion
62
- //#region src/user-service.d.ts
63
56
  interface ResolvedConfig {
64
57
  password: Required<Omit<PasswordConfig, "policies">> & {
65
58
  policies: PasswordPolicy[];
@@ -71,7 +64,7 @@ interface ResolvedConfig {
71
64
  };
72
65
  emailField?: string;
73
66
  }
74
- declare const SEEN_DEVICES_DEFAULT_CAP = 5;
67
+ export declare const SEEN_DEVICES_DEFAULT_CAP = 5;
75
68
  /**
76
69
  * Orchestrates user credentials over a pluggable {@link UserStore}.
77
70
  *
@@ -81,7 +74,7 @@ declare const SEEN_DEVICES_DEFAULT_CAP = 5;
81
74
  * handle-driven entry points) take a `username`/`email` login handle, resolved
82
75
  * via `UserStore.findByHandle`.
83
76
  */
84
- declare class UserService<T extends object = object> {
77
+ export declare class UserService<T extends object = object> {
85
78
  protected readonly store: UserStore<T>;
86
79
  protected readonly config: ResolvedConfig;
87
80
  protected readonly hasher: PasswordHasher;
@@ -113,8 +106,8 @@ declare class UserService<T extends object = object> {
113
106
  getUserSnapshot(id: string, scope: object): Promise<UserCredentials & T>;
114
107
  /**
115
108
  * Creates a user with `account.active: false`. The invite workflow relies
116
- * on this default (see `InviteWorkflow.acceptInvite` — pending invitees stay
117
- * inactive until they accept). For setup scripts / seeders / tests that
109
+ * on this default (pending invitees stay inactive until they accept the
110
+ * invite). For setup scripts / seeders / tests that
118
111
  * don't go through invite, follow up with `activateAccount(id)` or `login()`
119
112
  * will throw `UserAuthError("INACTIVE")` — which the login workflow
120
113
  * deliberately re-maps to `"Invalid credentials"` to avoid account
@@ -167,15 +160,15 @@ declare class UserService<T extends object = object> {
167
160
  setPassword(id: string, newPassword: string): Promise<void>;
168
161
  /**
169
162
  * Hard-delete the user row by `id`. Returns nothing on success. Throws
170
- * `UserAuthError("NOT_FOUND")` when no row matches. Used by the invite
171
- * workflow's `auth/invite/cancel` to revoke a pending invitation.
163
+ * `UserAuthError("NOT_FOUND")` when no row matches. To revoke a pending
164
+ * invitation use {@link revokeInvitation}, which never deletes an account
165
+ * whose invite was accepted meanwhile.
172
166
  */
173
167
  deleteUser(id: string): Promise<void>;
174
168
  /**
175
169
  * Deep-merge `patch` into the user record (top-level fields are shallow-
176
170
  * merged; `account` / `mfa` / `password` are merged per their
177
171
  * `@db.patch.strategy 'merge'` declaration). Returns the patched record.
178
- * Used by the invite workflow's `applyProfile` default fallback.
179
172
  */
180
173
  update(id: string, patch: Partial<UserCredentials & T>): Promise<UserCredentials & T>;
181
174
  /**
@@ -187,6 +180,24 @@ declare class UserService<T extends object = object> {
187
180
  * the same account can never both complete.
188
181
  */
189
182
  acceptInvitation(id: string, nonce: string | undefined): Promise<boolean>;
183
+ /**
184
+ * Revoke a pending invitation: delete the invited account, so its invite
185
+ * link stops working (the invite workflow answers it with 410
186
+ * `INVITE_CANCELLED`). Returns `false` and deletes nothing when the account
187
+ * is not pending — the invite was already accepted — or when the row
188
+ * changed while the revoke ran. Throws `UserAuthError("NOT_FOUND")` when no
189
+ * row matches `id`.
190
+ *
191
+ * Safe against a concurrent acceptance or re-invite: one compare-and-set
192
+ * write first rotates `account.inviteNonce` while the row is still pending
193
+ * (an accept racing it fails), and the delete is conditional on the row's
194
+ * `version` still being the one that write produced — any write in between
195
+ * (a re-invite, an acceptance) makes it delete nothing. The bundled stores
196
+ * honour the conditional delete; a custom store whose `delete()` ignores
197
+ * `expectedVersion` leaves a window of one round-trip between the re-check
198
+ * and the delete.
199
+ */
200
+ revokeInvitation(id: string): Promise<boolean>;
190
201
  /**
191
202
  * Flip the account active. `opts.verifiedEmail` lets callers that just
192
203
  * proved an inbox (invite magic link, signup OTP) record the correspondence
@@ -386,9 +397,7 @@ declare class UserService<T extends object = object> {
386
397
  */
387
398
  private incrementAndMaybeLock;
388
399
  }
389
- //#endregion
390
- //#region src/store/memory.d.ts
391
- declare class UserStoreMemory<T extends object = object> extends UserStore<T> {
400
+ export declare class UserStoreMemory<T extends object = object> extends UserStore<T> {
392
401
  /** Keyed by the stable surrogate `id` (the token subject). */
393
402
  private store;
394
403
  /**
@@ -424,11 +433,11 @@ declare class UserStoreMemory<T extends object = object> extends UserStore<T> {
424
433
  */
425
434
  private assertHandlesUnique;
426
435
  update(id: string, update: UserStoreUpdate): Promise<boolean>;
427
- delete(id: string): Promise<boolean>;
436
+ delete(id: string, opts?: {
437
+ expectedVersion?: number;
438
+ }): Promise<boolean>;
428
439
  withCas(id: string, mutator: (current: UserCredentials & T) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
429
440
  }
430
- //#endregion
431
- //#region src/store/federated-identity-store-memory.d.ts
432
441
  interface FederatedIdentityStoreMemoryOptions {
433
442
  /** Injectable clock for deterministic `linkedAt` / `lastLoginAt`. Defaults to `Date.now`. */
434
443
  clock?: () => number;
@@ -439,7 +448,7 @@ interface FederatedIdentityStoreMemoryOptions {
439
448
  * `structuredClone` on every read/write isolates callers from later mutation
440
449
  * (mirrors `UserStoreMemory`).
441
450
  */
442
- declare class FederatedIdentityStoreMemory extends FederatedIdentityStore {
451
+ export declare class FederatedIdentityStoreMemory extends FederatedIdentityStore {
443
452
  /** Keyed by `compositeKey(provider, subject)`. */
444
453
  private store;
445
454
  private clock;
@@ -451,28 +460,22 @@ declare class FederatedIdentityStoreMemory extends FederatedIdentityStore {
451
460
  touchLogin(provider: string, subject: string, profile?: FederatedProfileSnapshot): Promise<void>;
452
461
  deleteAllForUser(userId: string): Promise<number>;
453
462
  }
454
- //#endregion
455
- //#region src/password/policies.d.ts
456
- declare const ppHasMinLength: (min?: number) => PasswordPolicyDef;
457
- declare const ppHasUpperCase: (n?: number) => PasswordPolicyDef;
458
- declare const ppHasLowerCase: (n?: number) => PasswordPolicyDef;
459
- declare const ppHasNumber: (n?: number) => PasswordPolicyDef;
460
- declare const ppHasSpecialChar: (n?: number) => PasswordPolicyDef;
461
- declare const ppMaxRepeatedChars: (maxRepeated?: number) => PasswordPolicyDef;
462
- //#endregion
463
- //#region src/mfa/totp.d.ts
464
- declare function generateTotpSecret(bytes?: number): string;
465
- declare function generateTotpUri(secret: string, issuer: string, account: string, config?: Pick<TotpConfig, "period" | "digits">): string;
466
- declare function generateTotpCode(secret: string, config?: TotpConfig): string;
463
+ export declare const ppHasMinLength: (min?: number) => PasswordPolicyDef;
464
+ export declare const ppHasUpperCase: (n?: number) => PasswordPolicyDef;
465
+ export declare const ppHasLowerCase: (n?: number) => PasswordPolicyDef;
466
+ export declare const ppHasNumber: (n?: number) => PasswordPolicyDef;
467
+ export declare const ppHasSpecialChar: (n?: number) => PasswordPolicyDef;
468
+ export declare const ppMaxRepeatedChars: (maxRepeated?: number) => PasswordPolicyDef;
469
+ export declare function generateTotpSecret(bytes?: number): string;
470
+ export declare function generateTotpUri(secret: string, issuer: string, account: string, config?: Pick<TotpConfig, "period" | "digits">): string;
471
+ export declare function generateTotpCode(secret: string, config?: TotpConfig): string;
467
472
  /**
468
473
  * Returns the matched HOTP counter when `code` is valid within the verification
469
474
  * window, otherwise `null`. Returning the counter (not a bool) lets the caller
470
475
  * persist `lastUsedWindow` and reject same-window replays (RFC 6238 §5.2 SHOULD).
471
476
  */
472
- declare function verifyTotpCode(secret: string, code: string, config?: TotpConfig): number | null;
473
- declare function generateMfaCode(length?: number): string;
474
- //#endregion
475
- //#region src/mfa/codes.d.ts
477
+ export declare function verifyTotpCode(secret: string, code: string, config?: TotpConfig): number | null;
478
+ export declare function generateMfaCode(length?: number): string;
476
479
  /**
477
480
  * SHA-256 hash of an MFA code (e.g. one-time email/SMS code or backup code).
478
481
  *
@@ -480,7 +483,7 @@ declare function generateMfaCode(length?: number): string;
480
483
  * Use {@link verifyMfaCode} to compare a submitted plaintext code against the
481
484
  * stored hash in constant time.
482
485
  */
483
- declare function hashMfaCode(code: string): string;
486
+ export declare function hashMfaCode(code: string): string;
484
487
  /**
485
488
  * Constant-time comparison of a submitted plaintext code against an
486
489
  * expected SHA-256 hex hash (as produced by {@link hashMfaCode}).
@@ -488,12 +491,9 @@ declare function hashMfaCode(code: string): string;
488
491
  * Returns false for malformed/empty expected hashes (timingSafeEqual
489
492
  * requires equal-length, non-empty buffers).
490
493
  */
491
- declare function verifyMfaCode(submitted: string, expectedHash: string): boolean;
492
- //#endregion
493
- //#region src/utils.d.ts
494
- declare function maskEmail(email: string): string;
495
- declare function maskPhone(phone: string): string;
496
- declare function maskMfaValue(method: MfaMethod): string;
497
- declare function setAtPath(obj: object, path: string, value: unknown): void;
498
- //#endregion
499
- export { type AccountData, type DeepPartial, type FederatedIdentity, FederatedIdentityStore, FederatedIdentityStoreMemory, type FederatedIdentityStoreMemoryOptions, type FederatedProfileSnapshot, type LockStatus, type LockoutConfig, type LoginResult, type MfaData, type MfaMethod, type MfaMethodInfo, type NewFederatedIdentity, type PasswordConfig, type PasswordData, PasswordHasher, PasswordPolicy, type PasswordPolicyContext, type PasswordPolicyDef, type PasswordPolicyEvalFn, type PasswordPolicyInstance, type PolicyCheckResult, SEEN_DEVICES_DEFAULT_CAP, type TotpConfig, type TransferablePolicy, type TrustedDeviceRecord, UserAuthError, type UserAuthErrorType, type UserCredentials, UserService, type UserServiceConfig, UserStore, UserStoreMemory, type UserStoreUpdate, definePasswordPolicy, generateMfaCode, generateTotpCode, generateTotpSecret, generateTotpUri, hashMfaCode, maskEmail, maskMfaValue, maskPhone, normalizePolicies, pickDefinedProfile, ppHasLowerCase, ppHasMinLength, ppHasNumber, ppHasSpecialChar, ppHasUpperCase, ppMaxRepeatedChars, setAtPath, verifyMfaCode, verifyTotpCode };
494
+ export declare function verifyMfaCode(submitted: string, expectedHash: string): boolean;
495
+ export declare function maskEmail(email: string): string;
496
+ export declare function maskPhone(phone: string): string;
497
+ export declare function maskMfaValue(method: MfaMethod): string;
498
+ export declare function setAtPath(obj: object, path: string, value: unknown): void;
499
+ export { type AccountData, type DeepPartial, type FederatedIdentity, FederatedIdentityStore, type FederatedIdentityStoreMemoryOptions, type FederatedProfileSnapshot, type LockStatus, type LockoutConfig, type LoginResult, type MfaData, type MfaMethod, type MfaMethodInfo, type NewFederatedIdentity, type PasswordConfig, type PasswordData, type PasswordPolicyContext, type PasswordPolicyDef, type PasswordPolicyEvalFn, type PasswordPolicyInstance, type PolicyCheckResult, type TotpConfig, type TransferablePolicy, type TrustedDeviceRecord, type UserAuthErrorType, type UserCredentials, type UserServiceConfig, UserStore, type UserStoreUpdate, pickDefinedProfile };
package/dist/index.d.mts CHANGED
@@ -1,14 +1,11 @@
1
- import { C as TotpConfig, D as UserCredentials, E as UserAuthErrorType, O as UserServiceConfig, S as PolicyCheckResult, T as TrustedDeviceRecord, _ as PasswordData, a as pickDefinedProfile, b as PasswordPolicyEvalFn, c as AccountData, d as LockoutConfig, f as LoginResult, g as PasswordConfig, h as MfaMethodInfo, i as NewFederatedIdentity, k as UserStoreUpdate, l as DeepPartial, m as MfaMethod, n as FederatedIdentityStore, o as UserStore, p as MfaData, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity, u as LockStatus, v as PasswordPolicyContext, w as TransferablePolicy, x as PasswordPolicyInstance, y as PasswordPolicyDef } from "./federated-identity-store-B_jEkl-a.mjs";
2
- //#region src/errors.d.ts
3
- declare class UserAuthError extends Error {
1
+ import { C as TotpConfig, D as UserCredentials, E as UserAuthErrorType, O as UserServiceConfig, S as PolicyCheckResult, T as TrustedDeviceRecord, _ as PasswordData, a as pickDefinedProfile, b as PasswordPolicyEvalFn, c as AccountData, d as LockoutConfig, f as LoginResult, g as PasswordConfig, h as MfaMethodInfo, i as NewFederatedIdentity, k as UserStoreUpdate, l as DeepPartial, m as MfaMethod, n as FederatedIdentityStore, o as UserStore, p as MfaData, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity, u as LockStatus, v as PasswordPolicyContext, w as TransferablePolicy, x as PasswordPolicyInstance, y as PasswordPolicyDef } from "./federated-identity-store-Ds7Ye9qJ.mjs";
2
+ export declare class UserAuthError extends Error {
4
3
  readonly type: UserAuthErrorType;
5
4
  readonly details?: Record<string, unknown> | undefined;
6
5
  readonly name = "UserAuthError";
7
6
  constructor(type: UserAuthErrorType, message?: string, details?: Record<string, unknown> | undefined);
8
7
  }
9
- //#endregion
10
- //#region src/password/hasher.d.ts
11
- declare class PasswordHasher {
8
+ export declare class PasswordHasher {
12
9
  private readonly pepper;
13
10
  private readonly N;
14
11
  private readonly r;
@@ -19,9 +16,7 @@ declare class PasswordHasher {
19
16
  verify(password: string, encoded: string): Promise<boolean>;
20
17
  generatePassword(length?: number): string;
21
18
  }
22
- //#endregion
23
- //#region src/password/policy.d.ts
24
- declare function normalizePolicies(policies?: (PasswordPolicyDef | PasswordPolicyInstance)[]): PasswordPolicy[];
19
+ export declare function normalizePolicies(policies?: (PasswordPolicyDef | PasswordPolicyInstance)[]): PasswordPolicy[];
25
20
  /**
26
21
  * Helper that builds a `PasswordPolicyDef` from a real backend function plus
27
22
  * its bound positional arguments. The function runs directly on the server
@@ -43,13 +38,13 @@ declare function normalizePolicies(policies?: (PasswordPolicyDef | PasswordPolic
43
38
  * `undefined` and `transferable` is false; frontend pre-validation skips it
44
39
  * and only the server check enforces the rule.
45
40
  */
46
- declare function definePasswordPolicy<A extends readonly unknown[]>(opts: {
41
+ export declare function definePasswordPolicy<A extends readonly unknown[]>(opts: {
47
42
  rule: (v: string, ...args: A) => boolean | Promise<boolean>;
48
43
  args?: A;
49
44
  description?: string;
50
45
  errorMessage?: string;
51
46
  }): PasswordPolicyDef;
52
- declare class PasswordPolicy implements PasswordPolicyInstance {
47
+ export declare class PasswordPolicy implements PasswordPolicyInstance {
53
48
  readonly rule: PasswordPolicyEvalFn;
54
49
  readonly serialized?: string;
55
50
  readonly description: string;
@@ -58,8 +53,6 @@ declare class PasswordPolicy implements PasswordPolicyInstance {
58
53
  evaluate(password: string, context?: PasswordPolicyContext): boolean | Promise<boolean>;
59
54
  get transferable(): boolean;
60
55
  }
61
- //#endregion
62
- //#region src/user-service.d.ts
63
56
  interface ResolvedConfig {
64
57
  password: Required<Omit<PasswordConfig, "policies">> & {
65
58
  policies: PasswordPolicy[];
@@ -71,7 +64,7 @@ interface ResolvedConfig {
71
64
  };
72
65
  emailField?: string;
73
66
  }
74
- declare const SEEN_DEVICES_DEFAULT_CAP = 5;
67
+ export declare const SEEN_DEVICES_DEFAULT_CAP = 5;
75
68
  /**
76
69
  * Orchestrates user credentials over a pluggable {@link UserStore}.
77
70
  *
@@ -81,7 +74,7 @@ declare const SEEN_DEVICES_DEFAULT_CAP = 5;
81
74
  * handle-driven entry points) take a `username`/`email` login handle, resolved
82
75
  * via `UserStore.findByHandle`.
83
76
  */
84
- declare class UserService<T extends object = object> {
77
+ export declare class UserService<T extends object = object> {
85
78
  protected readonly store: UserStore<T>;
86
79
  protected readonly config: ResolvedConfig;
87
80
  protected readonly hasher: PasswordHasher;
@@ -113,8 +106,8 @@ declare class UserService<T extends object = object> {
113
106
  getUserSnapshot(id: string, scope: object): Promise<UserCredentials & T>;
114
107
  /**
115
108
  * Creates a user with `account.active: false`. The invite workflow relies
116
- * on this default (see `InviteWorkflow.acceptInvite` — pending invitees stay
117
- * inactive until they accept). For setup scripts / seeders / tests that
109
+ * on this default (pending invitees stay inactive until they accept the
110
+ * invite). For setup scripts / seeders / tests that
118
111
  * don't go through invite, follow up with `activateAccount(id)` or `login()`
119
112
  * will throw `UserAuthError("INACTIVE")` — which the login workflow
120
113
  * deliberately re-maps to `"Invalid credentials"` to avoid account
@@ -167,15 +160,15 @@ declare class UserService<T extends object = object> {
167
160
  setPassword(id: string, newPassword: string): Promise<void>;
168
161
  /**
169
162
  * Hard-delete the user row by `id`. Returns nothing on success. Throws
170
- * `UserAuthError("NOT_FOUND")` when no row matches. Used by the invite
171
- * workflow's `auth/invite/cancel` to revoke a pending invitation.
163
+ * `UserAuthError("NOT_FOUND")` when no row matches. To revoke a pending
164
+ * invitation use {@link revokeInvitation}, which never deletes an account
165
+ * whose invite was accepted meanwhile.
172
166
  */
173
167
  deleteUser(id: string): Promise<void>;
174
168
  /**
175
169
  * Deep-merge `patch` into the user record (top-level fields are shallow-
176
170
  * merged; `account` / `mfa` / `password` are merged per their
177
171
  * `@db.patch.strategy 'merge'` declaration). Returns the patched record.
178
- * Used by the invite workflow's `applyProfile` default fallback.
179
172
  */
180
173
  update(id: string, patch: Partial<UserCredentials & T>): Promise<UserCredentials & T>;
181
174
  /**
@@ -187,6 +180,24 @@ declare class UserService<T extends object = object> {
187
180
  * the same account can never both complete.
188
181
  */
189
182
  acceptInvitation(id: string, nonce: string | undefined): Promise<boolean>;
183
+ /**
184
+ * Revoke a pending invitation: delete the invited account, so its invite
185
+ * link stops working (the invite workflow answers it with 410
186
+ * `INVITE_CANCELLED`). Returns `false` and deletes nothing when the account
187
+ * is not pending — the invite was already accepted — or when the row
188
+ * changed while the revoke ran. Throws `UserAuthError("NOT_FOUND")` when no
189
+ * row matches `id`.
190
+ *
191
+ * Safe against a concurrent acceptance or re-invite: one compare-and-set
192
+ * write first rotates `account.inviteNonce` while the row is still pending
193
+ * (an accept racing it fails), and the delete is conditional on the row's
194
+ * `version` still being the one that write produced — any write in between
195
+ * (a re-invite, an acceptance) makes it delete nothing. The bundled stores
196
+ * honour the conditional delete; a custom store whose `delete()` ignores
197
+ * `expectedVersion` leaves a window of one round-trip between the re-check
198
+ * and the delete.
199
+ */
200
+ revokeInvitation(id: string): Promise<boolean>;
190
201
  /**
191
202
  * Flip the account active. `opts.verifiedEmail` lets callers that just
192
203
  * proved an inbox (invite magic link, signup OTP) record the correspondence
@@ -386,9 +397,7 @@ declare class UserService<T extends object = object> {
386
397
  */
387
398
  private incrementAndMaybeLock;
388
399
  }
389
- //#endregion
390
- //#region src/store/memory.d.ts
391
- declare class UserStoreMemory<T extends object = object> extends UserStore<T> {
400
+ export declare class UserStoreMemory<T extends object = object> extends UserStore<T> {
392
401
  /** Keyed by the stable surrogate `id` (the token subject). */
393
402
  private store;
394
403
  /**
@@ -424,11 +433,11 @@ declare class UserStoreMemory<T extends object = object> extends UserStore<T> {
424
433
  */
425
434
  private assertHandlesUnique;
426
435
  update(id: string, update: UserStoreUpdate): Promise<boolean>;
427
- delete(id: string): Promise<boolean>;
436
+ delete(id: string, opts?: {
437
+ expectedVersion?: number;
438
+ }): Promise<boolean>;
428
439
  withCas(id: string, mutator: (current: UserCredentials & T) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
429
440
  }
430
- //#endregion
431
- //#region src/store/federated-identity-store-memory.d.ts
432
441
  interface FederatedIdentityStoreMemoryOptions {
433
442
  /** Injectable clock for deterministic `linkedAt` / `lastLoginAt`. Defaults to `Date.now`. */
434
443
  clock?: () => number;
@@ -439,7 +448,7 @@ interface FederatedIdentityStoreMemoryOptions {
439
448
  * `structuredClone` on every read/write isolates callers from later mutation
440
449
  * (mirrors `UserStoreMemory`).
441
450
  */
442
- declare class FederatedIdentityStoreMemory extends FederatedIdentityStore {
451
+ export declare class FederatedIdentityStoreMemory extends FederatedIdentityStore {
443
452
  /** Keyed by `compositeKey(provider, subject)`. */
444
453
  private store;
445
454
  private clock;
@@ -451,28 +460,22 @@ declare class FederatedIdentityStoreMemory extends FederatedIdentityStore {
451
460
  touchLogin(provider: string, subject: string, profile?: FederatedProfileSnapshot): Promise<void>;
452
461
  deleteAllForUser(userId: string): Promise<number>;
453
462
  }
454
- //#endregion
455
- //#region src/password/policies.d.ts
456
- declare const ppHasMinLength: (min?: number) => PasswordPolicyDef;
457
- declare const ppHasUpperCase: (n?: number) => PasswordPolicyDef;
458
- declare const ppHasLowerCase: (n?: number) => PasswordPolicyDef;
459
- declare const ppHasNumber: (n?: number) => PasswordPolicyDef;
460
- declare const ppHasSpecialChar: (n?: number) => PasswordPolicyDef;
461
- declare const ppMaxRepeatedChars: (maxRepeated?: number) => PasswordPolicyDef;
462
- //#endregion
463
- //#region src/mfa/totp.d.ts
464
- declare function generateTotpSecret(bytes?: number): string;
465
- declare function generateTotpUri(secret: string, issuer: string, account: string, config?: Pick<TotpConfig, "period" | "digits">): string;
466
- declare function generateTotpCode(secret: string, config?: TotpConfig): string;
463
+ export declare const ppHasMinLength: (min?: number) => PasswordPolicyDef;
464
+ export declare const ppHasUpperCase: (n?: number) => PasswordPolicyDef;
465
+ export declare const ppHasLowerCase: (n?: number) => PasswordPolicyDef;
466
+ export declare const ppHasNumber: (n?: number) => PasswordPolicyDef;
467
+ export declare const ppHasSpecialChar: (n?: number) => PasswordPolicyDef;
468
+ export declare const ppMaxRepeatedChars: (maxRepeated?: number) => PasswordPolicyDef;
469
+ export declare function generateTotpSecret(bytes?: number): string;
470
+ export declare function generateTotpUri(secret: string, issuer: string, account: string, config?: Pick<TotpConfig, "period" | "digits">): string;
471
+ export declare function generateTotpCode(secret: string, config?: TotpConfig): string;
467
472
  /**
468
473
  * Returns the matched HOTP counter when `code` is valid within the verification
469
474
  * window, otherwise `null`. Returning the counter (not a bool) lets the caller
470
475
  * persist `lastUsedWindow` and reject same-window replays (RFC 6238 §5.2 SHOULD).
471
476
  */
472
- declare function verifyTotpCode(secret: string, code: string, config?: TotpConfig): number | null;
473
- declare function generateMfaCode(length?: number): string;
474
- //#endregion
475
- //#region src/mfa/codes.d.ts
477
+ export declare function verifyTotpCode(secret: string, code: string, config?: TotpConfig): number | null;
478
+ export declare function generateMfaCode(length?: number): string;
476
479
  /**
477
480
  * SHA-256 hash of an MFA code (e.g. one-time email/SMS code or backup code).
478
481
  *
@@ -480,7 +483,7 @@ declare function generateMfaCode(length?: number): string;
480
483
  * Use {@link verifyMfaCode} to compare a submitted plaintext code against the
481
484
  * stored hash in constant time.
482
485
  */
483
- declare function hashMfaCode(code: string): string;
486
+ export declare function hashMfaCode(code: string): string;
484
487
  /**
485
488
  * Constant-time comparison of a submitted plaintext code against an
486
489
  * expected SHA-256 hex hash (as produced by {@link hashMfaCode}).
@@ -488,12 +491,9 @@ declare function hashMfaCode(code: string): string;
488
491
  * Returns false for malformed/empty expected hashes (timingSafeEqual
489
492
  * requires equal-length, non-empty buffers).
490
493
  */
491
- declare function verifyMfaCode(submitted: string, expectedHash: string): boolean;
492
- //#endregion
493
- //#region src/utils.d.ts
494
- declare function maskEmail(email: string): string;
495
- declare function maskPhone(phone: string): string;
496
- declare function maskMfaValue(method: MfaMethod): string;
497
- declare function setAtPath(obj: object, path: string, value: unknown): void;
498
- //#endregion
499
- export { type AccountData, type DeepPartial, type FederatedIdentity, FederatedIdentityStore, FederatedIdentityStoreMemory, type FederatedIdentityStoreMemoryOptions, type FederatedProfileSnapshot, type LockStatus, type LockoutConfig, type LoginResult, type MfaData, type MfaMethod, type MfaMethodInfo, type NewFederatedIdentity, type PasswordConfig, type PasswordData, PasswordHasher, PasswordPolicy, type PasswordPolicyContext, type PasswordPolicyDef, type PasswordPolicyEvalFn, type PasswordPolicyInstance, type PolicyCheckResult, SEEN_DEVICES_DEFAULT_CAP, type TotpConfig, type TransferablePolicy, type TrustedDeviceRecord, UserAuthError, type UserAuthErrorType, type UserCredentials, UserService, type UserServiceConfig, UserStore, UserStoreMemory, type UserStoreUpdate, definePasswordPolicy, generateMfaCode, generateTotpCode, generateTotpSecret, generateTotpUri, hashMfaCode, maskEmail, maskMfaValue, maskPhone, normalizePolicies, pickDefinedProfile, ppHasLowerCase, ppHasMinLength, ppHasNumber, ppHasSpecialChar, ppHasUpperCase, ppMaxRepeatedChars, setAtPath, verifyMfaCode, verifyTotpCode };
494
+ export declare function verifyMfaCode(submitted: string, expectedHash: string): boolean;
495
+ export declare function maskEmail(email: string): string;
496
+ export declare function maskPhone(phone: string): string;
497
+ export declare function maskMfaValue(method: MfaMethod): string;
498
+ export declare function setAtPath(obj: object, path: string, value: unknown): void;
499
+ export { type AccountData, type DeepPartial, type FederatedIdentity, FederatedIdentityStore, type FederatedIdentityStoreMemoryOptions, type FederatedProfileSnapshot, type LockStatus, type LockoutConfig, type LoginResult, type MfaData, type MfaMethod, type MfaMethodInfo, type NewFederatedIdentity, type PasswordConfig, type PasswordData, type PasswordPolicyContext, type PasswordPolicyDef, type PasswordPolicyEvalFn, type PasswordPolicyInstance, type PolicyCheckResult, type TotpConfig, type TransferablePolicy, type TrustedDeviceRecord, type UserAuthErrorType, type UserCredentials, type UserServiceConfig, UserStore, type UserStoreUpdate, pickDefinedProfile };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { a as maskEmail, c as setAtPath, i as incrementAtPath, l as UserStore, n as pickDefinedProfile, o as maskMfaValue, r as deepMerge, s as maskPhone, t as FederatedIdentityStore, u as UserAuthError } from "./federated-identity-store-CRycYlMj.mjs";
1
+ import { a as maskEmail, c as setAtPath, i as incrementAtPath, l as UserStore, n as pickDefinedProfile, o as maskMfaValue, r as deepMerge, s as maskPhone, t as FederatedIdentityStore, u as UserAuthError } from "./federated-identity-store-BZRvw9Pz.mjs";
2
2
  import { createHash, createHmac, randomBytes, randomInt, randomUUID, scrypt, timingSafeEqual } from "node:crypto";
3
- //#region src/base-x/base32.ts
3
+ // src/base-x/base32.ts
4
4
  /**
5
5
  * Partially copied from "thirty-two" library, all credits to Chris Umbel.
6
6
  *
@@ -71,8 +71,7 @@ function decode(input) {
71
71
  }
72
72
  return output.subarray(0, index);
73
73
  }
74
- //#endregion
75
- //#region src/mfa/totp.ts
74
+ // src/mfa/totp.ts
76
75
  function generateTotpSecret(bytes = 20) {
77
76
  return encode(randomBytes(bytes)).toString().replace(/=+$/, "").toUpperCase();
78
77
  }
@@ -126,8 +125,7 @@ function hotpCode(key, counter, digits) {
126
125
  const offset = hmacResult[hmacResult.length - 1] & 15;
127
126
  return (((hmacResult[offset] & 127) << 24 | (hmacResult[offset + 1] & 255) << 16 | (hmacResult[offset + 2] & 255) << 8 | hmacResult[offset + 3] & 255) % 10 ** digits).toString().padStart(digits, "0");
128
127
  }
129
- //#endregion
130
- //#region src/password/hasher.ts
128
+ // src/password/hasher.ts
131
129
  const DEFAULTS = {
132
130
  N: 16384,
133
131
  r: 8,
@@ -137,11 +135,11 @@ const DEFAULTS = {
137
135
  };
138
136
  const PREFIX = "$scrypt$";
139
137
  /** Node's default scrypt memory cap (32 MiB) — kept as the floor. */
140
- const DEFAULT_MAXMEM = 32 * 1024 * 1024;
138
+ const DEFAULT_MAXMEM = 33554432;
141
139
  /** Upper bound for the memory a (possibly stored, untrusted) parameter set may ask for. */
142
- const MAX_MAXMEM = 1024 * 1024 * 1024;
140
+ const MAX_MAXMEM = 1073741824;
143
141
  function scryptAsync(password, salt, keyLength, options) {
144
- const maxmem = Math.min(MAX_MAXMEM, Math.max(DEFAULT_MAXMEM, 256 * options.N * options.r + 1024 * 1024));
142
+ const maxmem = Math.min(MAX_MAXMEM, Math.max(DEFAULT_MAXMEM, 256 * options.N * options.r + 1048576));
145
143
  return new Promise((resolve, reject) => {
146
144
  scrypt(password, salt, keyLength, {
147
145
  ...options,
@@ -211,7 +209,7 @@ var PasswordHasher = class {
211
209
  const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
212
210
  const digits = "0123456789";
213
211
  const special = "!@#$%^&*()-_=+";
214
- const all = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+";
212
+ const all = lower + upper + digits + special;
215
213
  const bytes = randomBytes(minLen);
216
214
  const result = Array.from({ length: minLen });
217
215
  result[0] = lower[bytes[0] % 26];
@@ -227,8 +225,7 @@ var PasswordHasher = class {
227
225
  return result.join("");
228
226
  }
229
227
  };
230
- //#endregion
231
- //#region src/password/policy.ts
228
+ // src/password/policy.ts
232
229
  function normalizePolicies(policies) {
233
230
  return (policies || []).map((p) => p instanceof PasswordPolicy ? p : new PasswordPolicy(p));
234
231
  }
@@ -282,8 +279,7 @@ var PasswordPolicy = class {
282
279
  return this.serialized !== void 0;
283
280
  }
284
281
  };
285
- //#endregion
286
- //#region src/user-service.ts
282
+ // src/user-service.ts
287
283
  function resolveConfig(config) {
288
284
  return {
289
285
  password: {
@@ -413,8 +409,8 @@ var UserService = class {
413
409
  }
414
410
  /**
415
411
  * Creates a user with `account.active: false`. The invite workflow relies
416
- * on this default (see `InviteWorkflow.acceptInvite` — pending invitees stay
417
- * inactive until they accept). For setup scripts / seeders / tests that
412
+ * on this default (pending invitees stay inactive until they accept the
413
+ * invite). For setup scripts / seeders / tests that
418
414
  * don't go through invite, follow up with `activateAccount(id)` or `login()`
419
415
  * will throw `UserAuthError("INACTIVE")` — which the login workflow
420
416
  * deliberately re-maps to `"Invalid credentials"` to avoid account
@@ -543,8 +539,9 @@ var UserService = class {
543
539
  }
544
540
  /**
545
541
  * Hard-delete the user row by `id`. Returns nothing on success. Throws
546
- * `UserAuthError("NOT_FOUND")` when no row matches. Used by the invite
547
- * workflow's `auth/invite/cancel` to revoke a pending invitation.
542
+ * `UserAuthError("NOT_FOUND")` when no row matches. To revoke a pending
543
+ * invitation use {@link revokeInvitation}, which never deletes an account
544
+ * whose invite was accepted meanwhile.
548
545
  */
549
546
  async deleteUser(id) {
550
547
  if (!await this.store.delete(id)) throw new UserAuthError("NOT_FOUND");
@@ -553,7 +550,6 @@ var UserService = class {
553
550
  * Deep-merge `patch` into the user record (top-level fields are shallow-
554
551
  * merged; `account` / `mfa` / `password` are merged per their
555
552
  * `@db.patch.strategy 'merge'` declaration). Returns the patched record.
556
- * Used by the invite workflow's `applyProfile` default fallback.
557
553
  */
558
554
  async update(id, patch) {
559
555
  if (!await this.store.update(id, { set: patch })) throw new UserAuthError("NOT_FOUND");
@@ -580,6 +576,36 @@ var UserService = class {
580
576
  return accepted;
581
577
  }
582
578
  /**
579
+ * Revoke a pending invitation: delete the invited account, so its invite
580
+ * link stops working (the invite workflow answers it with 410
581
+ * `INVITE_CANCELLED`). Returns `false` and deletes nothing when the account
582
+ * is not pending — the invite was already accepted — or when the row
583
+ * changed while the revoke ran. Throws `UserAuthError("NOT_FOUND")` when no
584
+ * row matches `id`.
585
+ *
586
+ * Safe against a concurrent acceptance or re-invite: one compare-and-set
587
+ * write first rotates `account.inviteNonce` while the row is still pending
588
+ * (an accept racing it fails), and the delete is conditional on the row's
589
+ * `version` still being the one that write produced — any write in between
590
+ * (a re-invite, an acceptance) makes it delete nothing. The bundled stores
591
+ * honour the conditional delete; a custom store whose `delete()` ignores
592
+ * `expectedVersion` leaves a window of one round-trip between the re-check
593
+ * and the delete.
594
+ */
595
+ async revokeInvitation(id) {
596
+ const nonce = randomBytes(16).toString("base64url");
597
+ let pending = false;
598
+ await this.store.withCas(id, (user) => {
599
+ pending = user.account.pendingInvitation === true;
600
+ if (!pending) return null;
601
+ return { set: { account: { inviteNonce: nonce } } };
602
+ });
603
+ if (!pending) return false;
604
+ const claimed = await this.store.findById(id);
605
+ if (!claimed?.account.pendingInvitation || claimed.account.inviteNonce !== nonce) return false;
606
+ return this.store.delete(id, { expectedVersion: claimed.version ?? 0 });
607
+ }
608
+ /**
583
609
  * Flip the account active. `opts.verifiedEmail` lets callers that just
584
610
  * proved an inbox (invite magic link, signup OTP) record the correspondence
585
611
  * address in the same store write — see {@link setVerifiedEmail}.
@@ -1107,8 +1133,7 @@ function observeWrites(store, hooks) {
1107
1133
  return fn;
1108
1134
  } });
1109
1135
  }
1110
- //#endregion
1111
- //#region src/store/memory.ts
1136
+ // src/store/memory.ts
1112
1137
  var UserStoreMemory = class extends UserStore {
1113
1138
  /** Keyed by the stable surrogate `id` (the token subject). */
1114
1139
  store = /* @__PURE__ */ new Map();
@@ -1204,7 +1229,10 @@ var UserStoreMemory = class extends UserStore {
1204
1229
  user.version = (user.version ?? 0) + 1;
1205
1230
  return true;
1206
1231
  }
1207
- async delete(id) {
1232
+ async delete(id, opts) {
1233
+ const user = this.store.get(id);
1234
+ if (!user) return false;
1235
+ if (opts?.expectedVersion !== void 0 && (user.version ?? 0) !== opts.expectedVersion) return false;
1208
1236
  return this.store.delete(id);
1209
1237
  }
1210
1238
  async withCas(id, mutator, opts) {
@@ -1222,8 +1250,7 @@ var UserStoreMemory = class extends UserStore {
1222
1250
  throw new UserAuthError("CAS_EXHAUSTED");
1223
1251
  }
1224
1252
  };
1225
- //#endregion
1226
- //#region src/store/federated-identity-store-memory.ts
1253
+ // src/store/federated-identity-store-memory.ts
1227
1254
  /**
1228
1255
  * In-memory {@link FederatedIdentityStore} — the offline-testable reference
1229
1256
  * impl (RFC IDP.md §9). Keyed by the composite `(provider, subject)`;
@@ -1281,8 +1308,7 @@ var FederatedIdentityStoreMemory = class extends FederatedIdentityStore {
1281
1308
  function compositeKey(provider, subject) {
1282
1309
  return `${provider}${subject}`;
1283
1310
  }
1284
- //#endregion
1285
- //#region src/password/policies.ts
1311
+ // src/password/policies.ts
1286
1312
  const ppHasMinLength = (min = 8) => definePasswordPolicy({
1287
1313
  rule: (v, min) => v.length >= min,
1288
1314
  args: [min],
@@ -1319,8 +1345,7 @@ const ppMaxRepeatedChars = (maxRepeated = 2) => definePasswordPolicy({
1319
1345
  description: `No more than ${maxRepeated} consecutive repeated characters`,
1320
1346
  errorMessage: `Password must not have more than ${maxRepeated} consecutive repeated characters`
1321
1347
  });
1322
- //#endregion
1323
- //#region src/mfa/codes.ts
1348
+ // src/mfa/codes.ts
1324
1349
  /**
1325
1350
  * SHA-256 hash of an MFA code (e.g. one-time email/SMS code or backup code).
1326
1351
  *
@@ -1345,5 +1370,4 @@ function verifyMfaCode(submitted, expectedHash) {
1345
1370
  if (a.length !== b.length) return false;
1346
1371
  return timingSafeEqual(a, b);
1347
1372
  }
1348
- //#endregion
1349
1373
  export { FederatedIdentityStore, FederatedIdentityStoreMemory, PasswordHasher, PasswordPolicy, SEEN_DEVICES_DEFAULT_CAP, UserAuthError, UserService, UserStore, UserStoreMemory, definePasswordPolicy, generateMfaCode, generateTotpCode, generateTotpSecret, generateTotpUri, hashMfaCode, maskEmail, maskMfaValue, maskPhone, normalizePolicies, pickDefinedProfile, ppHasLowerCase, ppHasMinLength, ppHasNumber, ppHasSpecialChar, ppHasUpperCase, ppMaxRepeatedChars, setAtPath, verifyMfaCode, verifyTotpCode };