@aooth/user 0.2.2 → 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/atscript-db.cjs +4 -2
- package/dist/atscript-db.d.cts +4 -2
- package/dist/atscript-db.d.mts +4 -2
- package/dist/atscript-db.mjs +4 -2
- package/dist/{federated-identity-store-D10-BT9l.d.cts → federated-identity-store-Ds7Ye9qJ.d.cts} +12 -6
- package/dist/{federated-identity-store-D10-BT9l.d.mts → federated-identity-store-Ds7Ye9qJ.d.mts} +12 -6
- package/dist/index.cjs +39 -6
- package/dist/index.d.cts +27 -7
- package/dist/index.d.mts +27 -7
- package/dist/index.mjs +39 -6
- package/package.json +9 -9
package/dist/atscript-db.cjs
CHANGED
|
@@ -132,8 +132,10 @@ var UsersStoreAtscriptDb = class extends require_federated_identity_store.UserSt
|
|
|
132
132
|
throw e;
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
-
async delete(id) {
|
|
136
|
-
|
|
135
|
+
async delete(id, opts) {
|
|
136
|
+
const filter = { id };
|
|
137
|
+
if (opts?.expectedVersion !== void 0) filter.version = opts.expectedVersion;
|
|
138
|
+
return (await this.table.deleteMany(filter)).deletedCount > 0;
|
|
137
139
|
}
|
|
138
140
|
/**
|
|
139
141
|
* Inline retry loop rather than delegating to @atscript/db's
|
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-Ds7Ye9qJ.cjs";
|
|
2
2
|
/**
|
|
3
3
|
* Structural surface of `AtscriptDbTable` covering exactly the methods this
|
|
4
4
|
* adapter calls — kept loose so `@aooth/user` doesn't pull `@atscript/db` types
|
|
@@ -113,7 +113,9 @@ export declare class UsersStoreAtscriptDb<TUserCustom extends object = object> e
|
|
|
113
113
|
findByIdentifier(value: string): Promise<(UserCredentials & TUserCustom) | null>;
|
|
114
114
|
create(data: UserCredentials & TUserCustom): Promise<void>;
|
|
115
115
|
update(id: string, update: UserStoreUpdate): Promise<boolean>;
|
|
116
|
-
delete(id: string
|
|
116
|
+
delete(id: string, opts?: {
|
|
117
|
+
expectedVersion?: number;
|
|
118
|
+
}): Promise<boolean>;
|
|
117
119
|
/**
|
|
118
120
|
* Inline retry loop rather than delegating to @atscript/db's
|
|
119
121
|
* `withOptimisticRetry`: that helper expects the mutator to always return a
|
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-Ds7Ye9qJ.mjs";
|
|
2
2
|
/**
|
|
3
3
|
* Structural surface of `AtscriptDbTable` covering exactly the methods this
|
|
4
4
|
* adapter calls — kept loose so `@aooth/user` doesn't pull `@atscript/db` types
|
|
@@ -113,7 +113,9 @@ export declare class UsersStoreAtscriptDb<TUserCustom extends object = object> e
|
|
|
113
113
|
findByIdentifier(value: string): Promise<(UserCredentials & TUserCustom) | null>;
|
|
114
114
|
create(data: UserCredentials & TUserCustom): Promise<void>;
|
|
115
115
|
update(id: string, update: UserStoreUpdate): Promise<boolean>;
|
|
116
|
-
delete(id: string
|
|
116
|
+
delete(id: string, opts?: {
|
|
117
|
+
expectedVersion?: number;
|
|
118
|
+
}): Promise<boolean>;
|
|
117
119
|
/**
|
|
118
120
|
* Inline retry loop rather than delegating to @atscript/db's
|
|
119
121
|
* `withOptimisticRetry`: that helper expects the mutator to always return a
|
package/dist/atscript-db.mjs
CHANGED
|
@@ -131,8 +131,10 @@ var UsersStoreAtscriptDb = class extends UserStore {
|
|
|
131
131
|
throw e;
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
async delete(id) {
|
|
135
|
-
|
|
134
|
+
async delete(id, opts) {
|
|
135
|
+
const filter = { id };
|
|
136
|
+
if (opts?.expectedVersion !== void 0) filter.version = opts.expectedVersion;
|
|
137
|
+
return (await this.table.deleteMany(filter)).deletedCount > 0;
|
|
136
138
|
}
|
|
137
139
|
/**
|
|
138
140
|
* Inline retry loop rather than delegating to @atscript/db's
|
package/dist/{federated-identity-store-D10-BT9l.d.cts → federated-identity-store-Ds7Ye9qJ.d.cts}
RENAMED
|
@@ -58,10 +58,11 @@ interface AccountData {
|
|
|
58
58
|
lastLogin: number;
|
|
59
59
|
/**
|
|
60
60
|
* True while the user record exists from an admin-issued invite but the
|
|
61
|
-
* invitee has not yet accepted (set password + activate).
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* invitee has not yet accepted (set password + activate). The invite
|
|
62
|
+
* workflow uses it to gate the accept tail and to detect re-invites
|
|
63
|
+
* (`'reuse'` refreshes the pending row and sends a new link);
|
|
64
|
+
* `UserService.revokeInvitation()` deletes only rows that still have it.
|
|
65
|
+
* Absent / `false` once the invite has been accepted.
|
|
65
66
|
*/
|
|
66
67
|
pendingInvitation?: boolean;
|
|
67
68
|
/** Id of the user who issued the pending invite (written by the invite flow). */
|
|
@@ -293,9 +294,14 @@ declare abstract class UserStore<T extends object = object> {
|
|
|
293
294
|
abstract update(id: string, update: UserStoreUpdate): Promise<boolean>;
|
|
294
295
|
/**
|
|
295
296
|
* Hard-delete the row by `id`. Returns `true` when a row was removed, `false`
|
|
296
|
-
* when the id was not found.
|
|
297
|
+
* when the id was not found. With `opts.expectedVersion`, delete only while
|
|
298
|
+
* the row's `version` still equals it (`false` otherwise) — a custom store
|
|
299
|
+
* that ignores it makes `UserService.revokeInvitation()` delete
|
|
300
|
+
* unconditionally after its re-check.
|
|
297
301
|
*/
|
|
298
|
-
abstract delete(id: string
|
|
302
|
+
abstract delete(id: string, opts?: {
|
|
303
|
+
expectedVersion?: number;
|
|
304
|
+
}): Promise<boolean>;
|
|
299
305
|
/**
|
|
300
306
|
* Run a read-modify-write cycle under optimistic concurrency, keyed by `id`.
|
|
301
307
|
* Each attempt re-reads (via `findById`), calls `mutator`, and applies the
|
package/dist/{federated-identity-store-D10-BT9l.d.mts → federated-identity-store-Ds7Ye9qJ.d.mts}
RENAMED
|
@@ -58,10 +58,11 @@ interface AccountData {
|
|
|
58
58
|
lastLogin: number;
|
|
59
59
|
/**
|
|
60
60
|
* True while the user record exists from an admin-issued invite but the
|
|
61
|
-
* invitee has not yet accepted (set password + activate).
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* invitee has not yet accepted (set password + activate). The invite
|
|
62
|
+
* workflow uses it to gate the accept tail and to detect re-invites
|
|
63
|
+
* (`'reuse'` refreshes the pending row and sends a new link);
|
|
64
|
+
* `UserService.revokeInvitation()` deletes only rows that still have it.
|
|
65
|
+
* Absent / `false` once the invite has been accepted.
|
|
65
66
|
*/
|
|
66
67
|
pendingInvitation?: boolean;
|
|
67
68
|
/** Id of the user who issued the pending invite (written by the invite flow). */
|
|
@@ -293,9 +294,14 @@ declare abstract class UserStore<T extends object = object> {
|
|
|
293
294
|
abstract update(id: string, update: UserStoreUpdate): Promise<boolean>;
|
|
294
295
|
/**
|
|
295
296
|
* Hard-delete the row by `id`. Returns `true` when a row was removed, `false`
|
|
296
|
-
* when the id was not found.
|
|
297
|
+
* when the id was not found. With `opts.expectedVersion`, delete only while
|
|
298
|
+
* the row's `version` still equals it (`false` otherwise) — a custom store
|
|
299
|
+
* that ignores it makes `UserService.revokeInvitation()` delete
|
|
300
|
+
* unconditionally after its re-check.
|
|
297
301
|
*/
|
|
298
|
-
abstract delete(id: string
|
|
302
|
+
abstract delete(id: string, opts?: {
|
|
303
|
+
expectedVersion?: number;
|
|
304
|
+
}): Promise<boolean>;
|
|
299
305
|
/**
|
|
300
306
|
* Run a read-modify-write cycle under optimistic concurrency, keyed by `id`.
|
|
301
307
|
* Each attempt re-reads (via `findById`), calls `mutator`, and applies the
|
package/dist/index.cjs
CHANGED
|
@@ -410,8 +410,8 @@ var UserService = class {
|
|
|
410
410
|
}
|
|
411
411
|
/**
|
|
412
412
|
* Creates a user with `account.active: false`. The invite workflow relies
|
|
413
|
-
* on this default (
|
|
414
|
-
*
|
|
413
|
+
* on this default (pending invitees stay inactive until they accept the
|
|
414
|
+
* invite). For setup scripts / seeders / tests that
|
|
415
415
|
* don't go through invite, follow up with `activateAccount(id)` or `login()`
|
|
416
416
|
* will throw `UserAuthError("INACTIVE")` — which the login workflow
|
|
417
417
|
* deliberately re-maps to `"Invalid credentials"` to avoid account
|
|
@@ -540,8 +540,9 @@ var UserService = class {
|
|
|
540
540
|
}
|
|
541
541
|
/**
|
|
542
542
|
* Hard-delete the user row by `id`. Returns nothing on success. Throws
|
|
543
|
-
* `UserAuthError("NOT_FOUND")` when no row matches.
|
|
544
|
-
*
|
|
543
|
+
* `UserAuthError("NOT_FOUND")` when no row matches. To revoke a pending
|
|
544
|
+
* invitation use {@link revokeInvitation}, which never deletes an account
|
|
545
|
+
* whose invite was accepted meanwhile.
|
|
545
546
|
*/
|
|
546
547
|
async deleteUser(id) {
|
|
547
548
|
if (!await this.store.delete(id)) throw new require_federated_identity_store.UserAuthError("NOT_FOUND");
|
|
@@ -550,7 +551,6 @@ var UserService = class {
|
|
|
550
551
|
* Deep-merge `patch` into the user record (top-level fields are shallow-
|
|
551
552
|
* merged; `account` / `mfa` / `password` are merged per their
|
|
552
553
|
* `@db.patch.strategy 'merge'` declaration). Returns the patched record.
|
|
553
|
-
* Used by the invite workflow's `applyProfile` default fallback.
|
|
554
554
|
*/
|
|
555
555
|
async update(id, patch) {
|
|
556
556
|
if (!await this.store.update(id, { set: patch })) throw new require_federated_identity_store.UserAuthError("NOT_FOUND");
|
|
@@ -577,6 +577,36 @@ var UserService = class {
|
|
|
577
577
|
return accepted;
|
|
578
578
|
}
|
|
579
579
|
/**
|
|
580
|
+
* Revoke a pending invitation: delete the invited account, so its invite
|
|
581
|
+
* link stops working (the invite workflow answers it with 410
|
|
582
|
+
* `INVITE_CANCELLED`). Returns `false` and deletes nothing when the account
|
|
583
|
+
* is not pending — the invite was already accepted — or when the row
|
|
584
|
+
* changed while the revoke ran. Throws `UserAuthError("NOT_FOUND")` when no
|
|
585
|
+
* row matches `id`.
|
|
586
|
+
*
|
|
587
|
+
* Safe against a concurrent acceptance or re-invite: one compare-and-set
|
|
588
|
+
* write first rotates `account.inviteNonce` while the row is still pending
|
|
589
|
+
* (an accept racing it fails), and the delete is conditional on the row's
|
|
590
|
+
* `version` still being the one that write produced — any write in between
|
|
591
|
+
* (a re-invite, an acceptance) makes it delete nothing. The bundled stores
|
|
592
|
+
* honour the conditional delete; a custom store whose `delete()` ignores
|
|
593
|
+
* `expectedVersion` leaves a window of one round-trip between the re-check
|
|
594
|
+
* and the delete.
|
|
595
|
+
*/
|
|
596
|
+
async revokeInvitation(id) {
|
|
597
|
+
const nonce = (0, node_crypto.randomBytes)(16).toString("base64url");
|
|
598
|
+
let pending = false;
|
|
599
|
+
await this.store.withCas(id, (user) => {
|
|
600
|
+
pending = user.account.pendingInvitation === true;
|
|
601
|
+
if (!pending) return null;
|
|
602
|
+
return { set: { account: { inviteNonce: nonce } } };
|
|
603
|
+
});
|
|
604
|
+
if (!pending) return false;
|
|
605
|
+
const claimed = await this.store.findById(id);
|
|
606
|
+
if (!claimed?.account.pendingInvitation || claimed.account.inviteNonce !== nonce) return false;
|
|
607
|
+
return this.store.delete(id, { expectedVersion: claimed.version ?? 0 });
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
580
610
|
* Flip the account active. `opts.verifiedEmail` lets callers that just
|
|
581
611
|
* proved an inbox (invite magic link, signup OTP) record the correspondence
|
|
582
612
|
* address in the same store write — see {@link setVerifiedEmail}.
|
|
@@ -1200,7 +1230,10 @@ var UserStoreMemory = class extends require_federated_identity_store.UserStore {
|
|
|
1200
1230
|
user.version = (user.version ?? 0) + 1;
|
|
1201
1231
|
return true;
|
|
1202
1232
|
}
|
|
1203
|
-
async delete(id) {
|
|
1233
|
+
async delete(id, opts) {
|
|
1234
|
+
const user = this.store.get(id);
|
|
1235
|
+
if (!user) return false;
|
|
1236
|
+
if (opts?.expectedVersion !== void 0 && (user.version ?? 0) !== opts.expectedVersion) return false;
|
|
1204
1237
|
return this.store.delete(id);
|
|
1205
1238
|
}
|
|
1206
1239
|
async withCas(id, mutator, opts) {
|
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-Ds7Ye9qJ.cjs";
|
|
2
2
|
export declare class UserAuthError extends Error {
|
|
3
3
|
readonly type: UserAuthErrorType;
|
|
4
4
|
readonly details?: Record<string, unknown> | undefined;
|
|
@@ -106,8 +106,8 @@ export declare class UserService<T extends object = object> {
|
|
|
106
106
|
getUserSnapshot(id: string, scope: object): Promise<UserCredentials & T>;
|
|
107
107
|
/**
|
|
108
108
|
* Creates a user with `account.active: false`. The invite workflow relies
|
|
109
|
-
* on this default (
|
|
110
|
-
*
|
|
109
|
+
* on this default (pending invitees stay inactive until they accept the
|
|
110
|
+
* invite). For setup scripts / seeders / tests that
|
|
111
111
|
* don't go through invite, follow up with `activateAccount(id)` or `login()`
|
|
112
112
|
* will throw `UserAuthError("INACTIVE")` — which the login workflow
|
|
113
113
|
* deliberately re-maps to `"Invalid credentials"` to avoid account
|
|
@@ -160,15 +160,15 @@ export declare class UserService<T extends object = object> {
|
|
|
160
160
|
setPassword(id: string, newPassword: string): Promise<void>;
|
|
161
161
|
/**
|
|
162
162
|
* Hard-delete the user row by `id`. Returns nothing on success. Throws
|
|
163
|
-
* `UserAuthError("NOT_FOUND")` when no row matches.
|
|
164
|
-
*
|
|
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.
|
|
165
166
|
*/
|
|
166
167
|
deleteUser(id: string): Promise<void>;
|
|
167
168
|
/**
|
|
168
169
|
* Deep-merge `patch` into the user record (top-level fields are shallow-
|
|
169
170
|
* merged; `account` / `mfa` / `password` are merged per their
|
|
170
171
|
* `@db.patch.strategy 'merge'` declaration). Returns the patched record.
|
|
171
|
-
* Used by the invite workflow's `applyProfile` default fallback.
|
|
172
172
|
*/
|
|
173
173
|
update(id: string, patch: Partial<UserCredentials & T>): Promise<UserCredentials & T>;
|
|
174
174
|
/**
|
|
@@ -180,6 +180,24 @@ export declare class UserService<T extends object = object> {
|
|
|
180
180
|
* the same account can never both complete.
|
|
181
181
|
*/
|
|
182
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>;
|
|
183
201
|
/**
|
|
184
202
|
* Flip the account active. `opts.verifiedEmail` lets callers that just
|
|
185
203
|
* proved an inbox (invite magic link, signup OTP) record the correspondence
|
|
@@ -415,7 +433,9 @@ export declare class UserStoreMemory<T extends object = object> extends UserStor
|
|
|
415
433
|
*/
|
|
416
434
|
private assertHandlesUnique;
|
|
417
435
|
update(id: string, update: UserStoreUpdate): Promise<boolean>;
|
|
418
|
-
delete(id: string
|
|
436
|
+
delete(id: string, opts?: {
|
|
437
|
+
expectedVersion?: number;
|
|
438
|
+
}): Promise<boolean>;
|
|
419
439
|
withCas(id: string, mutator: (current: UserCredentials & T) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
|
|
420
440
|
}
|
|
421
441
|
interface FederatedIdentityStoreMemoryOptions {
|
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-Ds7Ye9qJ.mjs";
|
|
2
2
|
export declare class UserAuthError extends Error {
|
|
3
3
|
readonly type: UserAuthErrorType;
|
|
4
4
|
readonly details?: Record<string, unknown> | undefined;
|
|
@@ -106,8 +106,8 @@ export declare class UserService<T extends object = object> {
|
|
|
106
106
|
getUserSnapshot(id: string, scope: object): Promise<UserCredentials & T>;
|
|
107
107
|
/**
|
|
108
108
|
* Creates a user with `account.active: false`. The invite workflow relies
|
|
109
|
-
* on this default (
|
|
110
|
-
*
|
|
109
|
+
* on this default (pending invitees stay inactive until they accept the
|
|
110
|
+
* invite). For setup scripts / seeders / tests that
|
|
111
111
|
* don't go through invite, follow up with `activateAccount(id)` or `login()`
|
|
112
112
|
* will throw `UserAuthError("INACTIVE")` — which the login workflow
|
|
113
113
|
* deliberately re-maps to `"Invalid credentials"` to avoid account
|
|
@@ -160,15 +160,15 @@ export declare class UserService<T extends object = object> {
|
|
|
160
160
|
setPassword(id: string, newPassword: string): Promise<void>;
|
|
161
161
|
/**
|
|
162
162
|
* Hard-delete the user row by `id`. Returns nothing on success. Throws
|
|
163
|
-
* `UserAuthError("NOT_FOUND")` when no row matches.
|
|
164
|
-
*
|
|
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.
|
|
165
166
|
*/
|
|
166
167
|
deleteUser(id: string): Promise<void>;
|
|
167
168
|
/**
|
|
168
169
|
* Deep-merge `patch` into the user record (top-level fields are shallow-
|
|
169
170
|
* merged; `account` / `mfa` / `password` are merged per their
|
|
170
171
|
* `@db.patch.strategy 'merge'` declaration). Returns the patched record.
|
|
171
|
-
* Used by the invite workflow's `applyProfile` default fallback.
|
|
172
172
|
*/
|
|
173
173
|
update(id: string, patch: Partial<UserCredentials & T>): Promise<UserCredentials & T>;
|
|
174
174
|
/**
|
|
@@ -180,6 +180,24 @@ export declare class UserService<T extends object = object> {
|
|
|
180
180
|
* the same account can never both complete.
|
|
181
181
|
*/
|
|
182
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>;
|
|
183
201
|
/**
|
|
184
202
|
* Flip the account active. `opts.verifiedEmail` lets callers that just
|
|
185
203
|
* proved an inbox (invite magic link, signup OTP) record the correspondence
|
|
@@ -415,7 +433,9 @@ export declare class UserStoreMemory<T extends object = object> extends UserStor
|
|
|
415
433
|
*/
|
|
416
434
|
private assertHandlesUnique;
|
|
417
435
|
update(id: string, update: UserStoreUpdate): Promise<boolean>;
|
|
418
|
-
delete(id: string
|
|
436
|
+
delete(id: string, opts?: {
|
|
437
|
+
expectedVersion?: number;
|
|
438
|
+
}): Promise<boolean>;
|
|
419
439
|
withCas(id: string, mutator: (current: UserCredentials & T) => UserStoreUpdate | null, opts?: WithCasOptions): Promise<void>;
|
|
420
440
|
}
|
|
421
441
|
interface FederatedIdentityStoreMemoryOptions {
|
package/dist/index.mjs
CHANGED
|
@@ -409,8 +409,8 @@ var UserService = class {
|
|
|
409
409
|
}
|
|
410
410
|
/**
|
|
411
411
|
* Creates a user with `account.active: false`. The invite workflow relies
|
|
412
|
-
* on this default (
|
|
413
|
-
*
|
|
412
|
+
* on this default (pending invitees stay inactive until they accept the
|
|
413
|
+
* invite). For setup scripts / seeders / tests that
|
|
414
414
|
* don't go through invite, follow up with `activateAccount(id)` or `login()`
|
|
415
415
|
* will throw `UserAuthError("INACTIVE")` — which the login workflow
|
|
416
416
|
* deliberately re-maps to `"Invalid credentials"` to avoid account
|
|
@@ -539,8 +539,9 @@ var UserService = class {
|
|
|
539
539
|
}
|
|
540
540
|
/**
|
|
541
541
|
* Hard-delete the user row by `id`. Returns nothing on success. Throws
|
|
542
|
-
* `UserAuthError("NOT_FOUND")` when no row matches.
|
|
543
|
-
*
|
|
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.
|
|
544
545
|
*/
|
|
545
546
|
async deleteUser(id) {
|
|
546
547
|
if (!await this.store.delete(id)) throw new UserAuthError("NOT_FOUND");
|
|
@@ -549,7 +550,6 @@ var UserService = class {
|
|
|
549
550
|
* Deep-merge `patch` into the user record (top-level fields are shallow-
|
|
550
551
|
* merged; `account` / `mfa` / `password` are merged per their
|
|
551
552
|
* `@db.patch.strategy 'merge'` declaration). Returns the patched record.
|
|
552
|
-
* Used by the invite workflow's `applyProfile` default fallback.
|
|
553
553
|
*/
|
|
554
554
|
async update(id, patch) {
|
|
555
555
|
if (!await this.store.update(id, { set: patch })) throw new UserAuthError("NOT_FOUND");
|
|
@@ -576,6 +576,36 @@ var UserService = class {
|
|
|
576
576
|
return accepted;
|
|
577
577
|
}
|
|
578
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
|
+
/**
|
|
579
609
|
* Flip the account active. `opts.verifiedEmail` lets callers that just
|
|
580
610
|
* proved an inbox (invite magic link, signup OTP) record the correspondence
|
|
581
611
|
* address in the same store write — see {@link setVerifiedEmail}.
|
|
@@ -1199,7 +1229,10 @@ var UserStoreMemory = class extends UserStore {
|
|
|
1199
1229
|
user.version = (user.version ?? 0) + 1;
|
|
1200
1230
|
return true;
|
|
1201
1231
|
}
|
|
1202
|
-
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;
|
|
1203
1236
|
return this.store.delete(id);
|
|
1204
1237
|
}
|
|
1205
1238
|
async withCas(id, mutator, opts) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aooth/user",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "User credential primitives for aoothjs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aoothjs",
|
|
@@ -60,18 +60,18 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@atscript/core": "^0.2.
|
|
64
|
-
"@atscript/db": "^0.1.
|
|
65
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
66
|
-
"@atscript/db-sqlite": "^0.1.
|
|
67
|
-
"@atscript/typescript": "^0.2.
|
|
63
|
+
"@atscript/core": "^0.2.4",
|
|
64
|
+
"@atscript/db": "^0.1.168",
|
|
65
|
+
"@atscript/db-sql-tools": "^0.1.168",
|
|
66
|
+
"@atscript/db-sqlite": "^0.1.168",
|
|
67
|
+
"@atscript/typescript": "^0.2.4",
|
|
68
68
|
"@types/better-sqlite3": "^7.6.13",
|
|
69
|
-
"@uniqu/core": "^0.
|
|
69
|
+
"@uniqu/core": "^0.2.0",
|
|
70
70
|
"better-sqlite3": "^12.6.2",
|
|
71
|
-
"unplugin-atscript": "^0.2.
|
|
71
|
+
"unplugin-atscript": "^0.2.4"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
|
-
"@atscript/db": "^0.1.
|
|
74
|
+
"@atscript/db": "^0.1.168"
|
|
75
75
|
},
|
|
76
76
|
"peerDependenciesMeta": {
|
|
77
77
|
"@atscript/db": {
|