@aooth/user 0.1.85 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atscript-db.d.cts +1 -1
- package/dist/atscript-db.d.mts +1 -1
- package/dist/{federated-identity-store-COXONHvx.d.cts → federated-identity-store-B_jEkl-a.d.cts} +8 -0
- package/dist/{federated-identity-store-COXONHvx.d.mts → federated-identity-store-B_jEkl-a.d.mts} +8 -0
- package/dist/index.cjs +43 -3
- package/dist/index.d.cts +19 -1
- package/dist/index.d.mts +19 -1
- package/dist/index.mjs +43 -3
- package/package.json +1 -1
- package/src/atscript-db/user-credentials.as +4 -0
- package/src/atscript-db/user-credentials.as.d.ts +2 -0
package/dist/atscript-db.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as UserCredentials, i as NewFederatedIdentity, k as UserStoreUpdate, n as FederatedIdentityStore, o as UserStore, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity } from "./federated-identity-store-
|
|
1
|
+
import { D as UserCredentials, i as NewFederatedIdentity, k as UserStoreUpdate, n as FederatedIdentityStore, o as UserStore, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity } from "./federated-identity-store-B_jEkl-a.cjs";
|
|
2
2
|
//#region src/atscript-db/federated-identity-store.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Structural surface of `AtscriptDbTable` covering exactly the methods this
|
package/dist/atscript-db.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as UserCredentials, i as NewFederatedIdentity, k as UserStoreUpdate, n as FederatedIdentityStore, o as UserStore, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity } from "./federated-identity-store-
|
|
1
|
+
import { D as UserCredentials, i as NewFederatedIdentity, k as UserStoreUpdate, n as FederatedIdentityStore, o as UserStore, r as FederatedProfileSnapshot, s as WithCasOptions, t as FederatedIdentity } from "./federated-identity-store-B_jEkl-a.mjs";
|
|
2
2
|
//#region src/atscript-db/federated-identity-store.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Structural surface of `AtscriptDbTable` covering exactly the methods this
|
package/dist/{federated-identity-store-COXONHvx.d.cts → federated-identity-store-B_jEkl-a.d.cts}
RENAMED
|
@@ -65,6 +65,14 @@ interface AccountData {
|
|
|
65
65
|
* the invite has been accepted.
|
|
66
66
|
*/
|
|
67
67
|
pendingInvitation?: boolean;
|
|
68
|
+
/** Id of the user who issued the pending invite (written by the invite flow). */
|
|
69
|
+
invitedBy?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Random marker of the current invite link. A re-invite rotates it, so a
|
|
72
|
+
* link issued before the re-invite answers 410 ("replaced") instead of
|
|
73
|
+
* activating the account. Absent on rows created before 0.2.
|
|
74
|
+
*/
|
|
75
|
+
inviteNonce?: string;
|
|
68
76
|
/**
|
|
69
77
|
* Correspondence address whose inbox the user PROVED ownership of — written
|
|
70
78
|
* at every inbox-proof moment (invite magic-link click, signup / recovery
|
package/dist/{federated-identity-store-COXONHvx.d.mts → federated-identity-store-B_jEkl-a.d.mts}
RENAMED
|
@@ -65,6 +65,14 @@ interface AccountData {
|
|
|
65
65
|
* the invite has been accepted.
|
|
66
66
|
*/
|
|
67
67
|
pendingInvitation?: boolean;
|
|
68
|
+
/** Id of the user who issued the pending invite (written by the invite flow). */
|
|
69
|
+
invitedBy?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Random marker of the current invite link. A re-invite rotates it, so a
|
|
72
|
+
* link issued before the re-invite answers 410 ("replaced") instead of
|
|
73
|
+
* activating the account. Absent on rows created before 0.2.
|
|
74
|
+
*/
|
|
75
|
+
inviteNonce?: string;
|
|
68
76
|
/**
|
|
69
77
|
* Correspondence address whose inbox the user PROVED ownership of — written
|
|
70
78
|
* at every inbox-proof moment (invite magic-link click, signup / recovery
|
package/dist/index.cjs
CHANGED
|
@@ -487,9 +487,14 @@ var UserService = class {
|
|
|
487
487
|
}
|
|
488
488
|
async login(handle, password, lockoutOverride) {
|
|
489
489
|
const user = await this.store.findByHandle(handle);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
490
|
+
try {
|
|
491
|
+
if (!user) throw new require_federated_identity_store.UserAuthError("NOT_FOUND");
|
|
492
|
+
if (!user.account.active) throw new require_federated_identity_store.UserAuthError("INACTIVE");
|
|
493
|
+
await this.ensureNotLockedOrThrow(user.id, user.account);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
await this.burnPasswordHash(password);
|
|
496
|
+
throw err;
|
|
497
|
+
}
|
|
493
498
|
if (await this.hasher.verify(password, user.password.hash)) {
|
|
494
499
|
const now = await this.recordLogin(user.id);
|
|
495
500
|
user.account.lastLogin = now;
|
|
@@ -556,6 +561,26 @@ var UserService = class {
|
|
|
556
561
|
return this.getUser(id);
|
|
557
562
|
}
|
|
558
563
|
/**
|
|
564
|
+
* Accept a pending invitation in ONE compare-and-set write: clears
|
|
565
|
+
* `account.pendingInvitation` only while the row is still pending and its
|
|
566
|
+
* `account.inviteNonce` equals `nonce` (a row without a nonce — written
|
|
567
|
+
* before 0.2 — accepts any). Returns `false` (nothing written) when the
|
|
568
|
+
* invite was already accepted or replaced by a newer one, so two links of
|
|
569
|
+
* the same account can never both complete.
|
|
570
|
+
*/
|
|
571
|
+
async acceptInvitation(id, nonce) {
|
|
572
|
+
let accepted = false;
|
|
573
|
+
await this.store.withCas(id, (user) => {
|
|
574
|
+
accepted = false;
|
|
575
|
+
const account = user.account;
|
|
576
|
+
if (!account.pendingInvitation) return null;
|
|
577
|
+
if (account.inviteNonce !== void 0 && account.inviteNonce !== nonce) return null;
|
|
578
|
+
accepted = true;
|
|
579
|
+
return { set: { account: { pendingInvitation: false } } };
|
|
580
|
+
});
|
|
581
|
+
return accepted;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
559
584
|
* Flip the account active. `opts.verifiedEmail` lets callers that just
|
|
560
585
|
* proved an inbox (invite magic link, signup OTP) record the correspondence
|
|
561
586
|
* address in the same store write — see {@link setVerifiedEmail}.
|
|
@@ -978,6 +1003,21 @@ var UserService = class {
|
|
|
978
1003
|
hasConfirmedMfaMethods(mfa) {
|
|
979
1004
|
return mfa.methods.some((m) => m.confirmed);
|
|
980
1005
|
}
|
|
1006
|
+
/** Lazily minted hash of a random password, with the configured hasher params. */
|
|
1007
|
+
dummyHash;
|
|
1008
|
+
/**
|
|
1009
|
+
* Verify `password` against a throwaway hash — same cost as a real check.
|
|
1010
|
+
* The early exits still skip the failed-attempt counter write a wrong
|
|
1011
|
+
* password costs (one store round trip); `AuthWorkflow`'s `'uniform'`
|
|
1012
|
+
* lockout disclosure spends a comparable shadow-counter write on them.
|
|
1013
|
+
*/
|
|
1014
|
+
async burnPasswordHash(password) {
|
|
1015
|
+
this.dummyHash ??= this.hasher.hash(this.hasher.generatePassword()).catch((err) => {
|
|
1016
|
+
this.dummyHash = void 0;
|
|
1017
|
+
throw err;
|
|
1018
|
+
});
|
|
1019
|
+
await this.hasher.verify(password, await this.dummyHash);
|
|
1020
|
+
}
|
|
981
1021
|
/**
|
|
982
1022
|
* If `account.locked`: auto-unlock when the lock has expired (mutating
|
|
983
1023
|
* `account` in place), or throw `LOCKED` otherwise.
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
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
2
|
//#region src/errors.d.ts
|
|
3
3
|
declare class UserAuthError extends Error {
|
|
4
4
|
readonly type: UserAuthErrorType;
|
|
@@ -178,6 +178,15 @@ declare class UserService<T extends object = object> {
|
|
|
178
178
|
* Used by the invite workflow's `applyProfile` default fallback.
|
|
179
179
|
*/
|
|
180
180
|
update(id: string, patch: Partial<UserCredentials & T>): Promise<UserCredentials & T>;
|
|
181
|
+
/**
|
|
182
|
+
* Accept a pending invitation in ONE compare-and-set write: clears
|
|
183
|
+
* `account.pendingInvitation` only while the row is still pending and its
|
|
184
|
+
* `account.inviteNonce` equals `nonce` (a row without a nonce — written
|
|
185
|
+
* before 0.2 — accepts any). Returns `false` (nothing written) when the
|
|
186
|
+
* invite was already accepted or replaced by a newer one, so two links of
|
|
187
|
+
* the same account can never both complete.
|
|
188
|
+
*/
|
|
189
|
+
acceptInvitation(id: string, nonce: string | undefined): Promise<boolean>;
|
|
181
190
|
/**
|
|
182
191
|
* Flip the account active. `opts.verifiedEmail` lets callers that just
|
|
183
192
|
* proved an inbox (invite magic link, signup OTP) record the correspondence
|
|
@@ -350,6 +359,15 @@ declare class UserService<T extends object = object> {
|
|
|
350
359
|
getCorrespondenceEmail(user: UserCredentials & T): string | undefined | Promise<string | undefined>;
|
|
351
360
|
private applyPasswordChange;
|
|
352
361
|
private hasConfirmedMfaMethods;
|
|
362
|
+
/** Lazily minted hash of a random password, with the configured hasher params. */
|
|
363
|
+
private dummyHash?;
|
|
364
|
+
/**
|
|
365
|
+
* Verify `password` against a throwaway hash — same cost as a real check.
|
|
366
|
+
* The early exits still skip the failed-attempt counter write a wrong
|
|
367
|
+
* password costs (one store round trip); `AuthWorkflow`'s `'uniform'`
|
|
368
|
+
* lockout disclosure spends a comparable shadow-counter write on them.
|
|
369
|
+
*/
|
|
370
|
+
private burnPasswordHash;
|
|
353
371
|
/**
|
|
354
372
|
* If `account.locked`: auto-unlock when the lock has expired (mutating
|
|
355
373
|
* `account` in place), or throw `LOCKED` otherwise.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
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
2
|
//#region src/errors.d.ts
|
|
3
3
|
declare class UserAuthError extends Error {
|
|
4
4
|
readonly type: UserAuthErrorType;
|
|
@@ -178,6 +178,15 @@ declare class UserService<T extends object = object> {
|
|
|
178
178
|
* Used by the invite workflow's `applyProfile` default fallback.
|
|
179
179
|
*/
|
|
180
180
|
update(id: string, patch: Partial<UserCredentials & T>): Promise<UserCredentials & T>;
|
|
181
|
+
/**
|
|
182
|
+
* Accept a pending invitation in ONE compare-and-set write: clears
|
|
183
|
+
* `account.pendingInvitation` only while the row is still pending and its
|
|
184
|
+
* `account.inviteNonce` equals `nonce` (a row without a nonce — written
|
|
185
|
+
* before 0.2 — accepts any). Returns `false` (nothing written) when the
|
|
186
|
+
* invite was already accepted or replaced by a newer one, so two links of
|
|
187
|
+
* the same account can never both complete.
|
|
188
|
+
*/
|
|
189
|
+
acceptInvitation(id: string, nonce: string | undefined): Promise<boolean>;
|
|
181
190
|
/**
|
|
182
191
|
* Flip the account active. `opts.verifiedEmail` lets callers that just
|
|
183
192
|
* proved an inbox (invite magic link, signup OTP) record the correspondence
|
|
@@ -350,6 +359,15 @@ declare class UserService<T extends object = object> {
|
|
|
350
359
|
getCorrespondenceEmail(user: UserCredentials & T): string | undefined | Promise<string | undefined>;
|
|
351
360
|
private applyPasswordChange;
|
|
352
361
|
private hasConfirmedMfaMethods;
|
|
362
|
+
/** Lazily minted hash of a random password, with the configured hasher params. */
|
|
363
|
+
private dummyHash?;
|
|
364
|
+
/**
|
|
365
|
+
* Verify `password` against a throwaway hash — same cost as a real check.
|
|
366
|
+
* The early exits still skip the failed-attempt counter write a wrong
|
|
367
|
+
* password costs (one store round trip); `AuthWorkflow`'s `'uniform'`
|
|
368
|
+
* lockout disclosure spends a comparable shadow-counter write on them.
|
|
369
|
+
*/
|
|
370
|
+
private burnPasswordHash;
|
|
353
371
|
/**
|
|
354
372
|
* If `account.locked`: auto-unlock when the lock has expired (mutating
|
|
355
373
|
* `account` in place), or throw `LOCKED` otherwise.
|
package/dist/index.mjs
CHANGED
|
@@ -486,9 +486,14 @@ var UserService = class {
|
|
|
486
486
|
}
|
|
487
487
|
async login(handle, password, lockoutOverride) {
|
|
488
488
|
const user = await this.store.findByHandle(handle);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
489
|
+
try {
|
|
490
|
+
if (!user) throw new UserAuthError("NOT_FOUND");
|
|
491
|
+
if (!user.account.active) throw new UserAuthError("INACTIVE");
|
|
492
|
+
await this.ensureNotLockedOrThrow(user.id, user.account);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
await this.burnPasswordHash(password);
|
|
495
|
+
throw err;
|
|
496
|
+
}
|
|
492
497
|
if (await this.hasher.verify(password, user.password.hash)) {
|
|
493
498
|
const now = await this.recordLogin(user.id);
|
|
494
499
|
user.account.lastLogin = now;
|
|
@@ -555,6 +560,26 @@ var UserService = class {
|
|
|
555
560
|
return this.getUser(id);
|
|
556
561
|
}
|
|
557
562
|
/**
|
|
563
|
+
* Accept a pending invitation in ONE compare-and-set write: clears
|
|
564
|
+
* `account.pendingInvitation` only while the row is still pending and its
|
|
565
|
+
* `account.inviteNonce` equals `nonce` (a row without a nonce — written
|
|
566
|
+
* before 0.2 — accepts any). Returns `false` (nothing written) when the
|
|
567
|
+
* invite was already accepted or replaced by a newer one, so two links of
|
|
568
|
+
* the same account can never both complete.
|
|
569
|
+
*/
|
|
570
|
+
async acceptInvitation(id, nonce) {
|
|
571
|
+
let accepted = false;
|
|
572
|
+
await this.store.withCas(id, (user) => {
|
|
573
|
+
accepted = false;
|
|
574
|
+
const account = user.account;
|
|
575
|
+
if (!account.pendingInvitation) return null;
|
|
576
|
+
if (account.inviteNonce !== void 0 && account.inviteNonce !== nonce) return null;
|
|
577
|
+
accepted = true;
|
|
578
|
+
return { set: { account: { pendingInvitation: false } } };
|
|
579
|
+
});
|
|
580
|
+
return accepted;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
558
583
|
* Flip the account active. `opts.verifiedEmail` lets callers that just
|
|
559
584
|
* proved an inbox (invite magic link, signup OTP) record the correspondence
|
|
560
585
|
* address in the same store write — see {@link setVerifiedEmail}.
|
|
@@ -977,6 +1002,21 @@ var UserService = class {
|
|
|
977
1002
|
hasConfirmedMfaMethods(mfa) {
|
|
978
1003
|
return mfa.methods.some((m) => m.confirmed);
|
|
979
1004
|
}
|
|
1005
|
+
/** Lazily minted hash of a random password, with the configured hasher params. */
|
|
1006
|
+
dummyHash;
|
|
1007
|
+
/**
|
|
1008
|
+
* Verify `password` against a throwaway hash — same cost as a real check.
|
|
1009
|
+
* The early exits still skip the failed-attempt counter write a wrong
|
|
1010
|
+
* password costs (one store round trip); `AuthWorkflow`'s `'uniform'`
|
|
1011
|
+
* lockout disclosure spends a comparable shadow-counter write on them.
|
|
1012
|
+
*/
|
|
1013
|
+
async burnPasswordHash(password) {
|
|
1014
|
+
this.dummyHash ??= this.hasher.hash(this.hasher.generatePassword()).catch((err) => {
|
|
1015
|
+
this.dummyHash = void 0;
|
|
1016
|
+
throw err;
|
|
1017
|
+
});
|
|
1018
|
+
await this.hasher.verify(password, await this.dummyHash);
|
|
1019
|
+
}
|
|
980
1020
|
/**
|
|
981
1021
|
* If `account.locked`: auto-unlock when the lock has expired (mutating
|
|
982
1022
|
* `account` in place), or throw `LOCKED` otherwise.
|
package/package.json
CHANGED
|
@@ -28,6 +28,10 @@ export interface AoothUserCredentials {
|
|
|
28
28
|
failedLoginAttempts: number
|
|
29
29
|
lastLogin: number.timestamp
|
|
30
30
|
pendingInvitation?: boolean
|
|
31
|
+
// Id of the user who issued the pending invite (set by the invite flow)
|
|
32
|
+
invitedBy?: string
|
|
33
|
+
// Random marker of the CURRENT invite link; a re-invite rotates it, so older links answer 410
|
|
34
|
+
inviteNonce?: string
|
|
31
35
|
// Correspondence address whose inbox the user proved (invite click, OTP, email confirm); auth-owned, not a login handle
|
|
32
36
|
verifiedEmail?: string
|
|
33
37
|
}
|