@aooth/user 0.1.6 → 0.1.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/atscript-db.cjs +108 -22
- package/dist/atscript-db.d.cts +69 -12
- package/dist/atscript-db.d.mts +69 -12
- package/dist/atscript-db.mjs +103 -18
- package/dist/{user-store-BaBmH13V.mjs → federated-identity-store-Cmc7jBrw.mjs} +40 -1
- package/dist/federated-identity-store-DEEed8lA.d.cts +378 -0
- package/dist/federated-identity-store-DEEed8lA.d.mts +378 -0
- package/dist/{user-store-BPZVAboN.cjs → federated-identity-store-oRjhnR5l.cjs} +51 -0
- package/dist/index.cjs +301 -220
- package/dist/index.d.cts +114 -72
- package/dist/index.d.mts +114 -72
- package/dist/index.mjs +268 -189
- package/package.json +23 -9
- package/src/atscript-db/federated-identity.as +44 -0
- package/src/atscript-db/federated-identity.as.d.ts +62 -0
- package/src/atscript-db/user-credentials.as +7 -2
- package/src/atscript-db/user-credentials.as.d.ts +62 -0
- package/dist/user-store-B3EStUfT.d.cts +0 -246
- package/dist/user-store-C1lxahSB.d.mts +0 -246
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as UserServiceConfig, S as
|
|
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-DEEed8lA.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/errors.d.ts
|
|
4
4
|
declare class UserAuthError extends Error {
|
|
@@ -71,6 +71,15 @@ interface ResolvedConfig {
|
|
|
71
71
|
secret: string;
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Orchestrates user credentials over a pluggable {@link UserStore}.
|
|
76
|
+
*
|
|
77
|
+
* Identity model: the stable surrogate **`id`** is the token subject. `getUser`
|
|
78
|
+
* and every mutation/admin method are keyed by `id` (the value carried in the
|
|
79
|
+
* session and returned by `useAuth().getUserId()`); only `login` (and other
|
|
80
|
+
* handle-driven entry points) take a `username`/`email` login handle, resolved
|
|
81
|
+
* via `UserStore.findByHandle`.
|
|
82
|
+
*/
|
|
74
83
|
declare class UserService<T extends object = object> {
|
|
75
84
|
protected readonly store: UserStore<T>;
|
|
76
85
|
protected readonly config: ResolvedConfig;
|
|
@@ -80,79 +89,88 @@ declare class UserService<T extends object = object> {
|
|
|
80
89
|
* Creates a user with `account.active: false`. The invite workflow relies
|
|
81
90
|
* on this default (see `InviteWorkflow.acceptInvite` — pending invitees stay
|
|
82
91
|
* inactive until they accept). For setup scripts / seeders / tests that
|
|
83
|
-
* don't go through invite, follow up with `activateAccount(
|
|
84
|
-
*
|
|
85
|
-
*
|
|
92
|
+
* don't go through invite, follow up with `activateAccount(id)` or `login()`
|
|
93
|
+
* will throw `UserAuthError("INACTIVE")` — which the login workflow
|
|
94
|
+
* deliberately re-maps to `"Invalid credentials"` to avoid account
|
|
86
95
|
* enumeration, so the failure is silent client-side.
|
|
87
96
|
*
|
|
97
|
+
* A stable `id` is minted here (server-managed surrogate, also the token
|
|
98
|
+
* subject) and returned on the record, so callers can `auth.issue(user.id)`
|
|
99
|
+
* without a re-read. Pass `id` via `extras` to override it.
|
|
100
|
+
*
|
|
88
101
|
* @param extras Optional partial user fields merged AFTER the base
|
|
89
102
|
* `UserCredentials` shape, so callers can populate consumer-specific
|
|
90
103
|
* required fields (e.g. `tenantId`) without subclassing the store.
|
|
91
104
|
* Because the merge is shallow and extras win, overlapping top-level
|
|
92
|
-
* keys (`id`, `account`, `mfa`, ...) replace the defaults
|
|
93
|
-
* pass nested objects with all required sub-fields if you
|
|
94
|
-
* override them.
|
|
105
|
+
* keys (`id`, `email`, `account`, `mfa`, ...) replace the defaults
|
|
106
|
+
* entirely — pass nested objects with all required sub-fields if you
|
|
107
|
+
* intend to override them.
|
|
95
108
|
*/
|
|
96
109
|
createUser(username: string, password?: string, extras?: Partial<T>): Promise<UserCredentials & T>;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
/** Read by the stable `id` (the token subject). */
|
|
111
|
+
getUser(id: string): Promise<UserCredentials & T>;
|
|
112
|
+
/**
|
|
113
|
+
* Deterministic handle resolver — `username` exact, then `email` exact.
|
|
114
|
+
* Returns `null` when nothing matches. Maps a login/recovery handle to a row;
|
|
115
|
+
* `login` uses the same resolution.
|
|
116
|
+
*/
|
|
117
|
+
findByHandle(handle: string): Promise<(UserCredentials & T) | null>;
|
|
118
|
+
/**
|
|
119
|
+
* Permissive lookup — `id`, then `username`, then `email` (ordered, first
|
|
120
|
+
* match). For internal / admin / recovery callers that may hold either an id
|
|
121
|
+
* or a handle. NOT for the login path (use {@link login}/{@link findByHandle}).
|
|
122
|
+
*/
|
|
123
|
+
findByIdentifier(value: string): Promise<(UserCredentials & T) | null>;
|
|
124
|
+
login(handle: string, password: string, lockoutOverride?: Partial<LockoutConfig>): Promise<LoginResult<T>>;
|
|
125
|
+
verifyPassword(id: string, password: string): Promise<boolean>;
|
|
126
|
+
changePassword(id: string, currentPassword: string, newPassword: string, repeatPassword?: string): Promise<void>;
|
|
127
|
+
setPassword(id: string, newPassword: string): Promise<void>;
|
|
102
128
|
/**
|
|
103
|
-
* Hard-delete the user row
|
|
104
|
-
* `UserAuthError("NOT_FOUND")` when no row matches
|
|
105
|
-
*
|
|
129
|
+
* Hard-delete the user row by `id`. Returns nothing on success. Throws
|
|
130
|
+
* `UserAuthError("NOT_FOUND")` when no row matches. Used by the invite
|
|
131
|
+
* workflow's `auth/invite/cancel` to revoke a pending invitation.
|
|
106
132
|
*/
|
|
107
|
-
deleteUser(
|
|
133
|
+
deleteUser(id: string): Promise<void>;
|
|
108
134
|
/**
|
|
109
135
|
* Deep-merge `patch` into the user record (top-level fields are shallow-
|
|
110
136
|
* merged; `account` / `mfa` / `password` are merged per their
|
|
111
137
|
* `@db.patch.strategy 'merge'` declaration). Returns the patched record.
|
|
112
138
|
* Used by the invite workflow's `applyProfile` default fallback.
|
|
113
139
|
*/
|
|
114
|
-
update(
|
|
115
|
-
activateAccount(
|
|
116
|
-
deactivateAccount(
|
|
117
|
-
lockAccount(
|
|
118
|
-
unlockAccount(
|
|
140
|
+
update(id: string, patch: Partial<UserCredentials & T>): Promise<UserCredentials & T>;
|
|
141
|
+
activateAccount(id: string): Promise<void>;
|
|
142
|
+
deactivateAccount(id: string): Promise<void>;
|
|
143
|
+
lockAccount(id: string, reason: string, duration?: number): Promise<void>;
|
|
144
|
+
unlockAccount(id: string): Promise<void>;
|
|
119
145
|
getLockStatus(account: UserCredentials["account"]): LockStatus;
|
|
120
|
-
checkPolicies(password: string, passwordData?: PasswordData): Promise<PolicyCheckResult>;
|
|
121
|
-
getTransferablePolicies(): TransferablePolicy[];
|
|
122
|
-
addMfaMethod(username: string, method: MfaMethod): Promise<void>;
|
|
123
|
-
confirmMfaMethod(username: string, name: string): Promise<void>;
|
|
124
|
-
removeMfaMethod(username: string, name: string): Promise<void>;
|
|
125
|
-
setDefaultMfaMethod(username: string, name: string): Promise<void>;
|
|
126
|
-
setMfaAutoSend(username: string, value: boolean): Promise<void>;
|
|
127
|
-
getAvailableMfaMethods(mfa: MfaData): MfaMethodInfo[];
|
|
128
|
-
/**
|
|
129
|
-
* Generate `count` plaintext backup codes (default 10), persist their
|
|
130
|
-
* hashes (replacing any existing batch), and return the plaintext codes
|
|
131
|
-
* once for the caller to deliver to the user. Plaintext is never
|
|
132
|
-
* recoverable after this call returns.
|
|
133
|
-
*
|
|
134
|
-
* Throws `UserAuthError("NOT_FOUND")` if the user does not exist.
|
|
135
|
-
*/
|
|
136
|
-
generateBackupCodes(username: string, count?: number): Promise<string[]>;
|
|
137
146
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
147
|
+
* Returns `true` when the user's password is older than
|
|
148
|
+
* `config.password.maxAgeMs`. Returns `false` when:
|
|
149
|
+
* - `maxAgeMs` is unset or `0` (expiry disabled)
|
|
150
|
+
* - `password.lastChanged` is `0` / falsy (no recorded change — never
|
|
151
|
+
* expire a user whose timestamp wasn't captured, since that would
|
|
152
|
+
* force-loop on every login)
|
|
144
153
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
154
|
+
* Consulted by `@aooth/auth-moost` `LoginWorkflow`'s `credentials`
|
|
155
|
+
* step to set `ctx.isPasswordExpired` when `guards.passwordExpiry`
|
|
156
|
+
* is true (the default).
|
|
147
157
|
*/
|
|
148
|
-
|
|
158
|
+
isPasswordExpired(user: UserCredentials & T, now?: number): boolean;
|
|
159
|
+
checkPolicies(password: string, passwordData?: PasswordData): Promise<PolicyCheckResult>;
|
|
160
|
+
getTransferablePolicies(): TransferablePolicy[];
|
|
161
|
+
addMfaMethod(id: string, method: MfaMethod): Promise<void>;
|
|
162
|
+
confirmMfaMethod(id: string, name: string): Promise<void>;
|
|
163
|
+
removeMfaMethod(id: string, name: string): Promise<void>;
|
|
164
|
+
setDefaultMfaMethod(id: string, name: string): Promise<void>;
|
|
165
|
+
setMfaAutoSend(id: string, value: boolean): Promise<void>;
|
|
166
|
+
getAvailableMfaMethods(mfa: MfaData): MfaMethodInfo[];
|
|
149
167
|
/**
|
|
150
168
|
* Verify a TOTP code against the user's confirmed `totp` MFA method.
|
|
151
169
|
* Failures bump the same `failedLoginAttempts` counter as `login` so an
|
|
152
170
|
* attacker who knows the password but not the TOTP gets `lockout.threshold`
|
|
153
171
|
* total tries across BOTH factors, not `2 * threshold`.
|
|
154
172
|
*/
|
|
155
|
-
verifyMfa(
|
|
173
|
+
verifyMfa(id: string, code: string, config?: TotpConfig, lockoutOverride?: Partial<LockoutConfig>): Promise<void>;
|
|
156
174
|
/**
|
|
157
175
|
* Verify a TOTP code against an UNCONFIRMED `totp` MFA method during initial
|
|
158
176
|
* enrollment. Differs from `verifyMfa`:
|
|
@@ -164,7 +182,7 @@ declare class UserService<T extends object = object> {
|
|
|
164
182
|
* Throws: NOT_FOUND if user missing; MFA_NOT_CONFIGURED if no unconfirmed
|
|
165
183
|
* totp method; MFA_INVALID on wrong code.
|
|
166
184
|
*/
|
|
167
|
-
verifyTotpSetupCode(
|
|
185
|
+
verifyTotpSetupCode(id: string, code: string, config?: TotpConfig): Promise<void>;
|
|
168
186
|
getPasswordHasher(): PasswordHasher;
|
|
169
187
|
getConfig(): Readonly<ResolvedConfig>;
|
|
170
188
|
/**
|
|
@@ -181,19 +199,19 @@ declare class UserService<T extends object = object> {
|
|
|
181
199
|
* write — the array shape is preserved end-to-end so DB adapters with a
|
|
182
200
|
* merge strategy replace the whole array.
|
|
183
201
|
*/
|
|
184
|
-
addTrustedDevice(
|
|
202
|
+
addTrustedDevice(id: string, record: TrustedDeviceRecord): Promise<void>;
|
|
185
203
|
/**
|
|
186
204
|
* Returns true when the supplied token (a) signs against the user+ip with
|
|
187
205
|
* the configured secret, AND (b) matches a persisted record that is still
|
|
188
206
|
* within its expiry window and whose bound IP (if any) matches.
|
|
189
207
|
*/
|
|
190
|
-
verifyTrustedDevice(
|
|
208
|
+
verifyTrustedDevice(userId: string, token: string, ip?: string): Promise<boolean>;
|
|
191
209
|
/**
|
|
192
210
|
* Remove a specific trust record from the user. No-op when the record is
|
|
193
211
|
* absent — mirrors the legacy `DeviceTrustStore.revoke` semantics.
|
|
194
212
|
*/
|
|
195
|
-
revokeTrustedDevice(
|
|
196
|
-
listTrustedDevices(
|
|
213
|
+
revokeTrustedDevice(id: string, token: string): Promise<void>;
|
|
214
|
+
listTrustedDevices(id: string): Promise<TrustedDeviceRecord[]>;
|
|
197
215
|
private requireDeviceTrustSecret;
|
|
198
216
|
private applyPasswordChange;
|
|
199
217
|
private hasConfirmedMfaMethods;
|
|
@@ -207,20 +225,57 @@ declare class UserService<T extends object = object> {
|
|
|
207
225
|
* and always throw `errorCode` (with `details.lockEnds` when the lockout
|
|
208
226
|
* just tripped). Used by both `login` and `verifyMfa` so the two factors
|
|
209
227
|
* share one counter.
|
|
228
|
+
*
|
|
229
|
+
* `lockoutOverride` lets a caller (e.g. a workflow policy resolver) force a
|
|
230
|
+
* different posture for THIS lock — notably `{ duration: 0 }` to make the
|
|
231
|
+
* lock permanent (admin-/recovery-lift only) instead of timed. Unset fields
|
|
232
|
+
* fall back to `this.config.lockout`.
|
|
210
233
|
*/
|
|
211
234
|
private incrementAndMaybeLock;
|
|
212
235
|
}
|
|
213
236
|
//#endregion
|
|
214
237
|
//#region src/store/memory.d.ts
|
|
215
238
|
declare class UserStoreMemory<T extends object = object> extends UserStore<T> {
|
|
239
|
+
/** Keyed by the stable surrogate `id` (the token subject). */
|
|
216
240
|
private store;
|
|
241
|
+
/**
|
|
242
|
+
* Optional seed. The map is keyed by each record's `id`; a record missing an
|
|
243
|
+
* `id` gets one minted (mirrors the DB `@db.default.uuid`). The seed object's
|
|
244
|
+
* keys are ignored — identity is the record's `id`.
|
|
245
|
+
*/
|
|
217
246
|
constructor(seed?: Record<string, UserCredentials & T>);
|
|
218
|
-
exists(
|
|
219
|
-
|
|
247
|
+
exists(handle: string): Promise<boolean>;
|
|
248
|
+
findById(id: string): Promise<(UserCredentials & T) | null>;
|
|
249
|
+
findByHandle(handle: string): Promise<(UserCredentials & T) | null>;
|
|
250
|
+
findByIdentifier(value: string): Promise<(UserCredentials & T) | null>;
|
|
220
251
|
create(data: UserCredentials & T): Promise<void>;
|
|
221
|
-
update(
|
|
222
|
-
delete(
|
|
223
|
-
withCas(
|
|
252
|
+
update(id: string, update: UserStoreUpdate): Promise<boolean>;
|
|
253
|
+
delete(id: string): Promise<boolean>;
|
|
254
|
+
withCas(id: string, mutator: (current: UserCredentials & T) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/store/federated-identity-store-memory.d.ts
|
|
258
|
+
interface FederatedIdentityStoreMemoryOptions {
|
|
259
|
+
/** Injectable clock for deterministic `linkedAt` / `lastLoginAt`. Defaults to `Date.now`. */
|
|
260
|
+
clock?: () => number;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* In-memory {@link FederatedIdentityStore} — the offline-testable reference
|
|
264
|
+
* impl (RFC IDP.md §9). Keyed by the composite `(provider, subject)`;
|
|
265
|
+
* `structuredClone` on every read/write isolates callers from later mutation
|
|
266
|
+
* (mirrors `UserStoreMemory`).
|
|
267
|
+
*/
|
|
268
|
+
declare class FederatedIdentityStoreMemory extends FederatedIdentityStore {
|
|
269
|
+
/** Keyed by `compositeKey(provider, subject)`. */
|
|
270
|
+
private store;
|
|
271
|
+
private clock;
|
|
272
|
+
constructor(opts?: FederatedIdentityStoreMemoryOptions);
|
|
273
|
+
find(provider: string, subject: string): Promise<FederatedIdentity | null>;
|
|
274
|
+
listForUser(userId: string): Promise<FederatedIdentity[]>;
|
|
275
|
+
link(rec: NewFederatedIdentity): Promise<FederatedIdentity>;
|
|
276
|
+
unlink(provider: string, subject: string): Promise<boolean>;
|
|
277
|
+
touchLogin(provider: string, subject: string, profile?: FederatedProfileSnapshot): Promise<void>;
|
|
278
|
+
deleteAllForUser(userId: string): Promise<number>;
|
|
224
279
|
}
|
|
225
280
|
//#endregion
|
|
226
281
|
//#region src/password/policies.d.ts
|
|
@@ -261,23 +316,10 @@ declare function hashMfaCode(code: string): string;
|
|
|
261
316
|
*/
|
|
262
317
|
declare function verifyMfaCode(submitted: string, expectedHash: string): boolean;
|
|
263
318
|
//#endregion
|
|
264
|
-
//#region src/mfa/backup-codes.d.ts
|
|
265
|
-
/**
|
|
266
|
-
* Generate `count` cryptographically-random backup codes (default 10).
|
|
267
|
-
*
|
|
268
|
-
* Format: 10 characters from the 31-char safe alphabet (uppercase letters +
|
|
269
|
-
* digits, omitting I/O/L/0/1), grouped as `XXXX-XXXX-XX`.
|
|
270
|
-
*
|
|
271
|
-
* Returns plaintext codes for the caller to deliver to the user — these
|
|
272
|
-
* should be hashed via {@link hashMfaCode} before persistence and never
|
|
273
|
-
* shown to the user again.
|
|
274
|
-
*/
|
|
275
|
-
declare function generateBackupCodePlaintext(count?: number): string[];
|
|
276
|
-
//#endregion
|
|
277
319
|
//#region src/utils.d.ts
|
|
278
320
|
declare function maskEmail(email: string): string;
|
|
279
321
|
declare function maskPhone(phone: string): string;
|
|
280
322
|
declare function maskMfaValue(method: MfaMethod): string;
|
|
281
323
|
declare function setAtPath(obj: object, path: string, value: unknown): void;
|
|
282
324
|
//#endregion
|
|
283
|
-
export { type AccountData, type DeepPartial, type LockStatus, type LockoutConfig, type LoginResult, type MfaData, type MfaMethod, type MfaMethodInfo, type PasswordConfig, type PasswordData, PasswordHasher, PasswordPolicy, type PasswordPolicyContext, type PasswordPolicyDef, type PasswordPolicyEvalFn, type PasswordPolicyInstance, type PolicyCheckResult, type TotpConfig, type TransferablePolicy, type TrustedDeviceRecord, UserAuthError, type UserAuthErrorType, type UserCredentials, UserService, type UserServiceConfig, UserStore, UserStoreMemory, type UserStoreUpdate, definePasswordPolicy,
|
|
325
|
+
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, 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 };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as UserServiceConfig, S as
|
|
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-DEEed8lA.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/errors.d.ts
|
|
4
4
|
declare class UserAuthError extends Error {
|
|
@@ -71,6 +71,15 @@ interface ResolvedConfig {
|
|
|
71
71
|
secret: string;
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Orchestrates user credentials over a pluggable {@link UserStore}.
|
|
76
|
+
*
|
|
77
|
+
* Identity model: the stable surrogate **`id`** is the token subject. `getUser`
|
|
78
|
+
* and every mutation/admin method are keyed by `id` (the value carried in the
|
|
79
|
+
* session and returned by `useAuth().getUserId()`); only `login` (and other
|
|
80
|
+
* handle-driven entry points) take a `username`/`email` login handle, resolved
|
|
81
|
+
* via `UserStore.findByHandle`.
|
|
82
|
+
*/
|
|
74
83
|
declare class UserService<T extends object = object> {
|
|
75
84
|
protected readonly store: UserStore<T>;
|
|
76
85
|
protected readonly config: ResolvedConfig;
|
|
@@ -80,79 +89,88 @@ declare class UserService<T extends object = object> {
|
|
|
80
89
|
* Creates a user with `account.active: false`. The invite workflow relies
|
|
81
90
|
* on this default (see `InviteWorkflow.acceptInvite` — pending invitees stay
|
|
82
91
|
* inactive until they accept). For setup scripts / seeders / tests that
|
|
83
|
-
* don't go through invite, follow up with `activateAccount(
|
|
84
|
-
*
|
|
85
|
-
*
|
|
92
|
+
* don't go through invite, follow up with `activateAccount(id)` or `login()`
|
|
93
|
+
* will throw `UserAuthError("INACTIVE")` — which the login workflow
|
|
94
|
+
* deliberately re-maps to `"Invalid credentials"` to avoid account
|
|
86
95
|
* enumeration, so the failure is silent client-side.
|
|
87
96
|
*
|
|
97
|
+
* A stable `id` is minted here (server-managed surrogate, also the token
|
|
98
|
+
* subject) and returned on the record, so callers can `auth.issue(user.id)`
|
|
99
|
+
* without a re-read. Pass `id` via `extras` to override it.
|
|
100
|
+
*
|
|
88
101
|
* @param extras Optional partial user fields merged AFTER the base
|
|
89
102
|
* `UserCredentials` shape, so callers can populate consumer-specific
|
|
90
103
|
* required fields (e.g. `tenantId`) without subclassing the store.
|
|
91
104
|
* Because the merge is shallow and extras win, overlapping top-level
|
|
92
|
-
* keys (`id`, `account`, `mfa`, ...) replace the defaults
|
|
93
|
-
* pass nested objects with all required sub-fields if you
|
|
94
|
-
* override them.
|
|
105
|
+
* keys (`id`, `email`, `account`, `mfa`, ...) replace the defaults
|
|
106
|
+
* entirely — pass nested objects with all required sub-fields if you
|
|
107
|
+
* intend to override them.
|
|
95
108
|
*/
|
|
96
109
|
createUser(username: string, password?: string, extras?: Partial<T>): Promise<UserCredentials & T>;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
/** Read by the stable `id` (the token subject). */
|
|
111
|
+
getUser(id: string): Promise<UserCredentials & T>;
|
|
112
|
+
/**
|
|
113
|
+
* Deterministic handle resolver — `username` exact, then `email` exact.
|
|
114
|
+
* Returns `null` when nothing matches. Maps a login/recovery handle to a row;
|
|
115
|
+
* `login` uses the same resolution.
|
|
116
|
+
*/
|
|
117
|
+
findByHandle(handle: string): Promise<(UserCredentials & T) | null>;
|
|
118
|
+
/**
|
|
119
|
+
* Permissive lookup — `id`, then `username`, then `email` (ordered, first
|
|
120
|
+
* match). For internal / admin / recovery callers that may hold either an id
|
|
121
|
+
* or a handle. NOT for the login path (use {@link login}/{@link findByHandle}).
|
|
122
|
+
*/
|
|
123
|
+
findByIdentifier(value: string): Promise<(UserCredentials & T) | null>;
|
|
124
|
+
login(handle: string, password: string, lockoutOverride?: Partial<LockoutConfig>): Promise<LoginResult<T>>;
|
|
125
|
+
verifyPassword(id: string, password: string): Promise<boolean>;
|
|
126
|
+
changePassword(id: string, currentPassword: string, newPassword: string, repeatPassword?: string): Promise<void>;
|
|
127
|
+
setPassword(id: string, newPassword: string): Promise<void>;
|
|
102
128
|
/**
|
|
103
|
-
* Hard-delete the user row
|
|
104
|
-
* `UserAuthError("NOT_FOUND")` when no row matches
|
|
105
|
-
*
|
|
129
|
+
* Hard-delete the user row by `id`. Returns nothing on success. Throws
|
|
130
|
+
* `UserAuthError("NOT_FOUND")` when no row matches. Used by the invite
|
|
131
|
+
* workflow's `auth/invite/cancel` to revoke a pending invitation.
|
|
106
132
|
*/
|
|
107
|
-
deleteUser(
|
|
133
|
+
deleteUser(id: string): Promise<void>;
|
|
108
134
|
/**
|
|
109
135
|
* Deep-merge `patch` into the user record (top-level fields are shallow-
|
|
110
136
|
* merged; `account` / `mfa` / `password` are merged per their
|
|
111
137
|
* `@db.patch.strategy 'merge'` declaration). Returns the patched record.
|
|
112
138
|
* Used by the invite workflow's `applyProfile` default fallback.
|
|
113
139
|
*/
|
|
114
|
-
update(
|
|
115
|
-
activateAccount(
|
|
116
|
-
deactivateAccount(
|
|
117
|
-
lockAccount(
|
|
118
|
-
unlockAccount(
|
|
140
|
+
update(id: string, patch: Partial<UserCredentials & T>): Promise<UserCredentials & T>;
|
|
141
|
+
activateAccount(id: string): Promise<void>;
|
|
142
|
+
deactivateAccount(id: string): Promise<void>;
|
|
143
|
+
lockAccount(id: string, reason: string, duration?: number): Promise<void>;
|
|
144
|
+
unlockAccount(id: string): Promise<void>;
|
|
119
145
|
getLockStatus(account: UserCredentials["account"]): LockStatus;
|
|
120
|
-
checkPolicies(password: string, passwordData?: PasswordData): Promise<PolicyCheckResult>;
|
|
121
|
-
getTransferablePolicies(): TransferablePolicy[];
|
|
122
|
-
addMfaMethod(username: string, method: MfaMethod): Promise<void>;
|
|
123
|
-
confirmMfaMethod(username: string, name: string): Promise<void>;
|
|
124
|
-
removeMfaMethod(username: string, name: string): Promise<void>;
|
|
125
|
-
setDefaultMfaMethod(username: string, name: string): Promise<void>;
|
|
126
|
-
setMfaAutoSend(username: string, value: boolean): Promise<void>;
|
|
127
|
-
getAvailableMfaMethods(mfa: MfaData): MfaMethodInfo[];
|
|
128
|
-
/**
|
|
129
|
-
* Generate `count` plaintext backup codes (default 10), persist their
|
|
130
|
-
* hashes (replacing any existing batch), and return the plaintext codes
|
|
131
|
-
* once for the caller to deliver to the user. Plaintext is never
|
|
132
|
-
* recoverable after this call returns.
|
|
133
|
-
*
|
|
134
|
-
* Throws `UserAuthError("NOT_FOUND")` if the user does not exist.
|
|
135
|
-
*/
|
|
136
|
-
generateBackupCodes(username: string, count?: number): Promise<string[]>;
|
|
137
146
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
147
|
+
* Returns `true` when the user's password is older than
|
|
148
|
+
* `config.password.maxAgeMs`. Returns `false` when:
|
|
149
|
+
* - `maxAgeMs` is unset or `0` (expiry disabled)
|
|
150
|
+
* - `password.lastChanged` is `0` / falsy (no recorded change — never
|
|
151
|
+
* expire a user whose timestamp wasn't captured, since that would
|
|
152
|
+
* force-loop on every login)
|
|
144
153
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
154
|
+
* Consulted by `@aooth/auth-moost` `LoginWorkflow`'s `credentials`
|
|
155
|
+
* step to set `ctx.isPasswordExpired` when `guards.passwordExpiry`
|
|
156
|
+
* is true (the default).
|
|
147
157
|
*/
|
|
148
|
-
|
|
158
|
+
isPasswordExpired(user: UserCredentials & T, now?: number): boolean;
|
|
159
|
+
checkPolicies(password: string, passwordData?: PasswordData): Promise<PolicyCheckResult>;
|
|
160
|
+
getTransferablePolicies(): TransferablePolicy[];
|
|
161
|
+
addMfaMethod(id: string, method: MfaMethod): Promise<void>;
|
|
162
|
+
confirmMfaMethod(id: string, name: string): Promise<void>;
|
|
163
|
+
removeMfaMethod(id: string, name: string): Promise<void>;
|
|
164
|
+
setDefaultMfaMethod(id: string, name: string): Promise<void>;
|
|
165
|
+
setMfaAutoSend(id: string, value: boolean): Promise<void>;
|
|
166
|
+
getAvailableMfaMethods(mfa: MfaData): MfaMethodInfo[];
|
|
149
167
|
/**
|
|
150
168
|
* Verify a TOTP code against the user's confirmed `totp` MFA method.
|
|
151
169
|
* Failures bump the same `failedLoginAttempts` counter as `login` so an
|
|
152
170
|
* attacker who knows the password but not the TOTP gets `lockout.threshold`
|
|
153
171
|
* total tries across BOTH factors, not `2 * threshold`.
|
|
154
172
|
*/
|
|
155
|
-
verifyMfa(
|
|
173
|
+
verifyMfa(id: string, code: string, config?: TotpConfig, lockoutOverride?: Partial<LockoutConfig>): Promise<void>;
|
|
156
174
|
/**
|
|
157
175
|
* Verify a TOTP code against an UNCONFIRMED `totp` MFA method during initial
|
|
158
176
|
* enrollment. Differs from `verifyMfa`:
|
|
@@ -164,7 +182,7 @@ declare class UserService<T extends object = object> {
|
|
|
164
182
|
* Throws: NOT_FOUND if user missing; MFA_NOT_CONFIGURED if no unconfirmed
|
|
165
183
|
* totp method; MFA_INVALID on wrong code.
|
|
166
184
|
*/
|
|
167
|
-
verifyTotpSetupCode(
|
|
185
|
+
verifyTotpSetupCode(id: string, code: string, config?: TotpConfig): Promise<void>;
|
|
168
186
|
getPasswordHasher(): PasswordHasher;
|
|
169
187
|
getConfig(): Readonly<ResolvedConfig>;
|
|
170
188
|
/**
|
|
@@ -181,19 +199,19 @@ declare class UserService<T extends object = object> {
|
|
|
181
199
|
* write — the array shape is preserved end-to-end so DB adapters with a
|
|
182
200
|
* merge strategy replace the whole array.
|
|
183
201
|
*/
|
|
184
|
-
addTrustedDevice(
|
|
202
|
+
addTrustedDevice(id: string, record: TrustedDeviceRecord): Promise<void>;
|
|
185
203
|
/**
|
|
186
204
|
* Returns true when the supplied token (a) signs against the user+ip with
|
|
187
205
|
* the configured secret, AND (b) matches a persisted record that is still
|
|
188
206
|
* within its expiry window and whose bound IP (if any) matches.
|
|
189
207
|
*/
|
|
190
|
-
verifyTrustedDevice(
|
|
208
|
+
verifyTrustedDevice(userId: string, token: string, ip?: string): Promise<boolean>;
|
|
191
209
|
/**
|
|
192
210
|
* Remove a specific trust record from the user. No-op when the record is
|
|
193
211
|
* absent — mirrors the legacy `DeviceTrustStore.revoke` semantics.
|
|
194
212
|
*/
|
|
195
|
-
revokeTrustedDevice(
|
|
196
|
-
listTrustedDevices(
|
|
213
|
+
revokeTrustedDevice(id: string, token: string): Promise<void>;
|
|
214
|
+
listTrustedDevices(id: string): Promise<TrustedDeviceRecord[]>;
|
|
197
215
|
private requireDeviceTrustSecret;
|
|
198
216
|
private applyPasswordChange;
|
|
199
217
|
private hasConfirmedMfaMethods;
|
|
@@ -207,20 +225,57 @@ declare class UserService<T extends object = object> {
|
|
|
207
225
|
* and always throw `errorCode` (with `details.lockEnds` when the lockout
|
|
208
226
|
* just tripped). Used by both `login` and `verifyMfa` so the two factors
|
|
209
227
|
* share one counter.
|
|
228
|
+
*
|
|
229
|
+
* `lockoutOverride` lets a caller (e.g. a workflow policy resolver) force a
|
|
230
|
+
* different posture for THIS lock — notably `{ duration: 0 }` to make the
|
|
231
|
+
* lock permanent (admin-/recovery-lift only) instead of timed. Unset fields
|
|
232
|
+
* fall back to `this.config.lockout`.
|
|
210
233
|
*/
|
|
211
234
|
private incrementAndMaybeLock;
|
|
212
235
|
}
|
|
213
236
|
//#endregion
|
|
214
237
|
//#region src/store/memory.d.ts
|
|
215
238
|
declare class UserStoreMemory<T extends object = object> extends UserStore<T> {
|
|
239
|
+
/** Keyed by the stable surrogate `id` (the token subject). */
|
|
216
240
|
private store;
|
|
241
|
+
/**
|
|
242
|
+
* Optional seed. The map is keyed by each record's `id`; a record missing an
|
|
243
|
+
* `id` gets one minted (mirrors the DB `@db.default.uuid`). The seed object's
|
|
244
|
+
* keys are ignored — identity is the record's `id`.
|
|
245
|
+
*/
|
|
217
246
|
constructor(seed?: Record<string, UserCredentials & T>);
|
|
218
|
-
exists(
|
|
219
|
-
|
|
247
|
+
exists(handle: string): Promise<boolean>;
|
|
248
|
+
findById(id: string): Promise<(UserCredentials & T) | null>;
|
|
249
|
+
findByHandle(handle: string): Promise<(UserCredentials & T) | null>;
|
|
250
|
+
findByIdentifier(value: string): Promise<(UserCredentials & T) | null>;
|
|
220
251
|
create(data: UserCredentials & T): Promise<void>;
|
|
221
|
-
update(
|
|
222
|
-
delete(
|
|
223
|
-
withCas(
|
|
252
|
+
update(id: string, update: UserStoreUpdate): Promise<boolean>;
|
|
253
|
+
delete(id: string): Promise<boolean>;
|
|
254
|
+
withCas(id: string, mutator: (current: UserCredentials & T) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
//#region src/store/federated-identity-store-memory.d.ts
|
|
258
|
+
interface FederatedIdentityStoreMemoryOptions {
|
|
259
|
+
/** Injectable clock for deterministic `linkedAt` / `lastLoginAt`. Defaults to `Date.now`. */
|
|
260
|
+
clock?: () => number;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* In-memory {@link FederatedIdentityStore} — the offline-testable reference
|
|
264
|
+
* impl (RFC IDP.md §9). Keyed by the composite `(provider, subject)`;
|
|
265
|
+
* `structuredClone` on every read/write isolates callers from later mutation
|
|
266
|
+
* (mirrors `UserStoreMemory`).
|
|
267
|
+
*/
|
|
268
|
+
declare class FederatedIdentityStoreMemory extends FederatedIdentityStore {
|
|
269
|
+
/** Keyed by `compositeKey(provider, subject)`. */
|
|
270
|
+
private store;
|
|
271
|
+
private clock;
|
|
272
|
+
constructor(opts?: FederatedIdentityStoreMemoryOptions);
|
|
273
|
+
find(provider: string, subject: string): Promise<FederatedIdentity | null>;
|
|
274
|
+
listForUser(userId: string): Promise<FederatedIdentity[]>;
|
|
275
|
+
link(rec: NewFederatedIdentity): Promise<FederatedIdentity>;
|
|
276
|
+
unlink(provider: string, subject: string): Promise<boolean>;
|
|
277
|
+
touchLogin(provider: string, subject: string, profile?: FederatedProfileSnapshot): Promise<void>;
|
|
278
|
+
deleteAllForUser(userId: string): Promise<number>;
|
|
224
279
|
}
|
|
225
280
|
//#endregion
|
|
226
281
|
//#region src/password/policies.d.ts
|
|
@@ -261,23 +316,10 @@ declare function hashMfaCode(code: string): string;
|
|
|
261
316
|
*/
|
|
262
317
|
declare function verifyMfaCode(submitted: string, expectedHash: string): boolean;
|
|
263
318
|
//#endregion
|
|
264
|
-
//#region src/mfa/backup-codes.d.ts
|
|
265
|
-
/**
|
|
266
|
-
* Generate `count` cryptographically-random backup codes (default 10).
|
|
267
|
-
*
|
|
268
|
-
* Format: 10 characters from the 31-char safe alphabet (uppercase letters +
|
|
269
|
-
* digits, omitting I/O/L/0/1), grouped as `XXXX-XXXX-XX`.
|
|
270
|
-
*
|
|
271
|
-
* Returns plaintext codes for the caller to deliver to the user — these
|
|
272
|
-
* should be hashed via {@link hashMfaCode} before persistence and never
|
|
273
|
-
* shown to the user again.
|
|
274
|
-
*/
|
|
275
|
-
declare function generateBackupCodePlaintext(count?: number): string[];
|
|
276
|
-
//#endregion
|
|
277
319
|
//#region src/utils.d.ts
|
|
278
320
|
declare function maskEmail(email: string): string;
|
|
279
321
|
declare function maskPhone(phone: string): string;
|
|
280
322
|
declare function maskMfaValue(method: MfaMethod): string;
|
|
281
323
|
declare function setAtPath(obj: object, path: string, value: unknown): void;
|
|
282
324
|
//#endregion
|
|
283
|
-
export { type AccountData, type DeepPartial, type LockStatus, type LockoutConfig, type LoginResult, type MfaData, type MfaMethod, type MfaMethodInfo, type PasswordConfig, type PasswordData, PasswordHasher, PasswordPolicy, type PasswordPolicyContext, type PasswordPolicyDef, type PasswordPolicyEvalFn, type PasswordPolicyInstance, type PolicyCheckResult, type TotpConfig, type TransferablePolicy, type TrustedDeviceRecord, UserAuthError, type UserAuthErrorType, type UserCredentials, UserService, type UserServiceConfig, UserStore, UserStoreMemory, type UserStoreUpdate, definePasswordPolicy,
|
|
325
|
+
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, 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 };
|