@aooth/user 0.1.1

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.
@@ -0,0 +1,96 @@
1
+ import { randomBytes } from "node:crypto";
2
+ //#region src/errors.ts
3
+ const defaultMessages = {
4
+ NOT_FOUND: "User not found",
5
+ ALREADY_EXISTS: "User already exists",
6
+ LOCKED: "Account is locked",
7
+ INACTIVE: "Account is not active",
8
+ INVALID_CREDENTIALS: "Invalid credentials",
9
+ POLICY_VIOLATION: "Password does not meet policy requirements",
10
+ PASSWORDS_MISMATCH: "Passwords do not match",
11
+ PASSWORD_IN_HISTORY: "Password was recently used",
12
+ MFA_REQUIRED: "Multi-factor authentication is required",
13
+ MFA_INVALID: "Invalid MFA code",
14
+ MFA_NOT_CONFIGURED: "MFA method is not configured"
15
+ };
16
+ var UserAuthError = class extends Error {
17
+ name = "UserAuthError";
18
+ constructor(type, message, details) {
19
+ super(message ?? defaultMessages[type]);
20
+ this.type = type;
21
+ this.details = details;
22
+ }
23
+ };
24
+ //#endregion
25
+ //#region src/utils.ts
26
+ function maskEmail(email) {
27
+ if (!email) return "";
28
+ const at = email.indexOf("@");
29
+ if (at < 0) return mask(email);
30
+ return mask(email.slice(0, at)) + email.slice(at);
31
+ }
32
+ function maskPhone(phone) {
33
+ return mask(phone);
34
+ }
35
+ function maskMfaValue(method) {
36
+ switch (method.name) {
37
+ case "email": return maskEmail(method.value);
38
+ case "sms": return maskPhone(method.value);
39
+ default: return "";
40
+ }
41
+ }
42
+ function mask(s) {
43
+ if (!s) return "";
44
+ if (s.length <= 2) return "***";
45
+ const show = Math.max(1, Math.floor(s.length / 4));
46
+ return s.slice(0, show) + "***" + s.slice(-show);
47
+ }
48
+ const DEFAULT_CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+";
49
+ function generateSecureRandom(length, charset = DEFAULT_CHARSET) {
50
+ const bytes = randomBytes(length);
51
+ const result = Array.from({ length });
52
+ for (let i = 0; i < length; i++) result[i] = charset[bytes[i] % charset.length];
53
+ return result.join("");
54
+ }
55
+ function deepMerge(target, source) {
56
+ const t = target;
57
+ for (const key of Object.keys(source)) {
58
+ const sv = source[key];
59
+ const tv = t[key];
60
+ if (sv !== null && typeof sv === "object" && !Array.isArray(sv) && tv !== null && typeof tv === "object" && !Array.isArray(tv)) deepMerge(tv, sv);
61
+ else t[key] = sv;
62
+ }
63
+ }
64
+ function setAtPath(obj, path, value) {
65
+ const parts = path.split(".");
66
+ let current = obj;
67
+ for (let i = 0; i < parts.length - 1; i++) {
68
+ let next = current[parts[i]];
69
+ if (next === void 0 || next === null || typeof next !== "object") {
70
+ next = {};
71
+ current[parts[i]] = next;
72
+ }
73
+ current = next;
74
+ }
75
+ current[parts[parts.length - 1]] = value;
76
+ }
77
+ function incrementAtPath(obj, path, amount) {
78
+ const parts = path.split(".");
79
+ let current = obj;
80
+ for (let i = 0; i < parts.length - 1; i++) {
81
+ let next = current[parts[i]];
82
+ if (next === void 0 || next === null || typeof next !== "object") {
83
+ next = {};
84
+ current[parts[i]] = next;
85
+ }
86
+ current = next;
87
+ }
88
+ const leaf = parts[parts.length - 1];
89
+ const existing = current[leaf];
90
+ current[leaf] = (typeof existing === "number" ? existing : 0) + amount;
91
+ }
92
+ //#endregion
93
+ //#region src/store/user-store.ts
94
+ var UserStore = class {};
95
+ //#endregion
96
+ export { maskEmail as a, setAtPath as c, incrementAtPath as i, UserAuthError as l, deepMerge as n, maskMfaValue as o, generateSecureRandom as r, maskPhone as s, UserStore as t };
@@ -0,0 +1,149 @@
1
+ let node_crypto = require("node:crypto");
2
+ //#region src/errors.ts
3
+ const defaultMessages = {
4
+ NOT_FOUND: "User not found",
5
+ ALREADY_EXISTS: "User already exists",
6
+ LOCKED: "Account is locked",
7
+ INACTIVE: "Account is not active",
8
+ INVALID_CREDENTIALS: "Invalid credentials",
9
+ POLICY_VIOLATION: "Password does not meet policy requirements",
10
+ PASSWORDS_MISMATCH: "Passwords do not match",
11
+ PASSWORD_IN_HISTORY: "Password was recently used",
12
+ MFA_REQUIRED: "Multi-factor authentication is required",
13
+ MFA_INVALID: "Invalid MFA code",
14
+ MFA_NOT_CONFIGURED: "MFA method is not configured"
15
+ };
16
+ var UserAuthError = class extends Error {
17
+ name = "UserAuthError";
18
+ constructor(type, message, details) {
19
+ super(message ?? defaultMessages[type]);
20
+ this.type = type;
21
+ this.details = details;
22
+ }
23
+ };
24
+ //#endregion
25
+ //#region src/utils.ts
26
+ function maskEmail(email) {
27
+ if (!email) return "";
28
+ const at = email.indexOf("@");
29
+ if (at < 0) return mask(email);
30
+ return mask(email.slice(0, at)) + email.slice(at);
31
+ }
32
+ function maskPhone(phone) {
33
+ return mask(phone);
34
+ }
35
+ function maskMfaValue(method) {
36
+ switch (method.name) {
37
+ case "email": return maskEmail(method.value);
38
+ case "sms": return maskPhone(method.value);
39
+ default: return "";
40
+ }
41
+ }
42
+ function mask(s) {
43
+ if (!s) return "";
44
+ if (s.length <= 2) return "***";
45
+ const show = Math.max(1, Math.floor(s.length / 4));
46
+ return s.slice(0, show) + "***" + s.slice(-show);
47
+ }
48
+ const DEFAULT_CHARSET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+";
49
+ function generateSecureRandom(length, charset = DEFAULT_CHARSET) {
50
+ const bytes = (0, node_crypto.randomBytes)(length);
51
+ const result = Array.from({ length });
52
+ for (let i = 0; i < length; i++) result[i] = charset[bytes[i] % charset.length];
53
+ return result.join("");
54
+ }
55
+ function deepMerge(target, source) {
56
+ const t = target;
57
+ for (const key of Object.keys(source)) {
58
+ const sv = source[key];
59
+ const tv = t[key];
60
+ if (sv !== null && typeof sv === "object" && !Array.isArray(sv) && tv !== null && typeof tv === "object" && !Array.isArray(tv)) deepMerge(tv, sv);
61
+ else t[key] = sv;
62
+ }
63
+ }
64
+ function setAtPath(obj, path, value) {
65
+ const parts = path.split(".");
66
+ let current = obj;
67
+ for (let i = 0; i < parts.length - 1; i++) {
68
+ let next = current[parts[i]];
69
+ if (next === void 0 || next === null || typeof next !== "object") {
70
+ next = {};
71
+ current[parts[i]] = next;
72
+ }
73
+ current = next;
74
+ }
75
+ current[parts[parts.length - 1]] = value;
76
+ }
77
+ function incrementAtPath(obj, path, amount) {
78
+ const parts = path.split(".");
79
+ let current = obj;
80
+ for (let i = 0; i < parts.length - 1; i++) {
81
+ let next = current[parts[i]];
82
+ if (next === void 0 || next === null || typeof next !== "object") {
83
+ next = {};
84
+ current[parts[i]] = next;
85
+ }
86
+ current = next;
87
+ }
88
+ const leaf = parts[parts.length - 1];
89
+ const existing = current[leaf];
90
+ current[leaf] = (typeof existing === "number" ? existing : 0) + amount;
91
+ }
92
+ //#endregion
93
+ //#region src/store/user-store.ts
94
+ var UserStore = class {};
95
+ //#endregion
96
+ Object.defineProperty(exports, "UserAuthError", {
97
+ enumerable: true,
98
+ get: function() {
99
+ return UserAuthError;
100
+ }
101
+ });
102
+ Object.defineProperty(exports, "UserStore", {
103
+ enumerable: true,
104
+ get: function() {
105
+ return UserStore;
106
+ }
107
+ });
108
+ Object.defineProperty(exports, "deepMerge", {
109
+ enumerable: true,
110
+ get: function() {
111
+ return deepMerge;
112
+ }
113
+ });
114
+ Object.defineProperty(exports, "generateSecureRandom", {
115
+ enumerable: true,
116
+ get: function() {
117
+ return generateSecureRandom;
118
+ }
119
+ });
120
+ Object.defineProperty(exports, "incrementAtPath", {
121
+ enumerable: true,
122
+ get: function() {
123
+ return incrementAtPath;
124
+ }
125
+ });
126
+ Object.defineProperty(exports, "maskEmail", {
127
+ enumerable: true,
128
+ get: function() {
129
+ return maskEmail;
130
+ }
131
+ });
132
+ Object.defineProperty(exports, "maskMfaValue", {
133
+ enumerable: true,
134
+ get: function() {
135
+ return maskMfaValue;
136
+ }
137
+ });
138
+ Object.defineProperty(exports, "maskPhone", {
139
+ enumerable: true,
140
+ get: function() {
141
+ return maskPhone;
142
+ }
143
+ });
144
+ Object.defineProperty(exports, "setAtPath", {
145
+ enumerable: true,
146
+ get: function() {
147
+ return setAtPath;
148
+ }
149
+ });
@@ -0,0 +1,187 @@
1
+ //#region src/types.d.ts
2
+ interface UserCredentials {
3
+ id: string;
4
+ username: string;
5
+ password: PasswordData;
6
+ account: AccountData;
7
+ mfa: MfaData;
8
+ /**
9
+ * Hashed backup codes (SHA-256, hex-encoded). Generated via
10
+ * `UserService.generateBackupCodes`. Undefined when the user has not
11
+ * enrolled backup codes; an empty array means all codes were consumed.
12
+ */
13
+ backupCodes?: string[];
14
+ /**
15
+ * Persisted device-trust records ("remember this device, skip MFA next
16
+ * time"). Managed by `UserService.{issue,add,verify,revoke,list}TrustedDevice`.
17
+ * Absent when the user has never opted in.
18
+ */
19
+ trustedDevices?: TrustedDeviceRecord[];
20
+ }
21
+ interface TrustedDeviceRecord {
22
+ /** `<raw>.<sig>` — what we hand back to the consumer and what they round-trip. */
23
+ token: string;
24
+ /** Bound IP — set when `deviceTrust.bindsTo === 'cookie+ip'`. */
25
+ ip?: string;
26
+ issuedAt: number;
27
+ expiresAt: number;
28
+ /** Optional human-readable label (e.g. user-agent summary). */
29
+ name?: string;
30
+ }
31
+ interface PasswordData {
32
+ /** Self-describing scrypt hash: $scrypt$N=...,r=...,p=...,l=...$salt$hash */
33
+ hash: string;
34
+ /** Previous password hashes (self-describing strings) */
35
+ history: string[];
36
+ lastChanged: number;
37
+ /** True when password was system-generated and user hasn't set their own */
38
+ isInitial: boolean;
39
+ }
40
+ interface AccountData {
41
+ active: boolean;
42
+ locked: boolean;
43
+ lockReason: string;
44
+ /** 0 = permanent lock, >0 = timestamp (ms) when lock expires */
45
+ lockEnds: number;
46
+ failedLoginAttempts: number;
47
+ lastLogin: number;
48
+ /**
49
+ * True while the user record exists from an admin-issued invite but the
50
+ * invitee has not yet accepted (set password + activate). Used by
51
+ * `InviteWorkflow` to gate the accept tail, reject duplicate invites, and
52
+ * power `auth.reInvite` / `auth.cancelInvite`. Absent / `false` once the
53
+ * invite has been accepted.
54
+ */
55
+ pendingInvitation?: boolean;
56
+ }
57
+ interface MfaData {
58
+ /** Registered MFA methods */
59
+ methods: MfaMethod[];
60
+ /** Name of the default MFA method */
61
+ defaultMethod: string;
62
+ /** Auto-send MFA challenge on login */
63
+ autoSend: boolean;
64
+ }
65
+ interface MfaMethod {
66
+ /** Method name: 'email', 'sms', 'totp' */
67
+ name: string;
68
+ /** Whether this method has been verified/confirmed */
69
+ confirmed: boolean;
70
+ /** The method's value: email address, phone number, or TOTP secret */
71
+ value: string;
72
+ }
73
+ interface UserServiceConfig {
74
+ password?: PasswordConfig;
75
+ lockout?: LockoutConfig;
76
+ /** Injectable clock for testability. Defaults to Date.now */
77
+ clock?: () => number;
78
+ /**
79
+ * Device-trust config. Required (with a non-empty `secret`) when any
80
+ * `issueTrustedDevice` / `verifyTrustedDevice` API is called; the methods
81
+ * throw clearly when invoked without it.
82
+ */
83
+ deviceTrust?: {
84
+ /** HMAC-SHA256 signing secret for trust-device tokens. */secret: string;
85
+ };
86
+ }
87
+ interface PasswordConfig {
88
+ /** Pepper string prepended to password before hashing */
89
+ pepper?: string;
90
+ /** Number of historical hashes to retain (0 = disabled) */
91
+ historyLength?: number;
92
+ /** scrypt cost parameter N (default 16384) */
93
+ scryptN?: number;
94
+ /** scrypt block size r (default 8) */
95
+ scryptR?: number;
96
+ /** scrypt parallelism p (default 1) */
97
+ scryptP?: number;
98
+ /** Hash output length in bytes (default 64) */
99
+ keyLength?: number;
100
+ /** Password policy rules */
101
+ policies?: (PasswordPolicyDef | PasswordPolicyInstance)[];
102
+ }
103
+ interface LockoutConfig {
104
+ /** Lock after this many failed attempts (0 = disabled) */
105
+ threshold?: number;
106
+ /** Lock duration in ms (0 = permanent) */
107
+ duration?: number;
108
+ }
109
+ interface PasswordPolicyDef {
110
+ rule: string | PasswordPolicyEvalFn;
111
+ description?: string;
112
+ errorMessage?: string;
113
+ }
114
+ type PasswordPolicyEvalFn = (password: string, context?: PasswordPolicyContext) => boolean | Promise<boolean>;
115
+ interface PasswordPolicyContext {
116
+ passwordData?: PasswordData;
117
+ passwordConfig?: PasswordConfig;
118
+ }
119
+ /** Interface satisfied by the PasswordPolicy class (avoids circular import) */
120
+ interface PasswordPolicyInstance extends PasswordPolicyDef {
121
+ evaluate(password: string, context?: PasswordPolicyContext): boolean | Promise<boolean>;
122
+ transferable: boolean;
123
+ }
124
+ type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] };
125
+ interface UserStoreUpdate {
126
+ /** Partial object with fields to set (deep-merged) */
127
+ set?: DeepPartial<UserCredentials>;
128
+ /** Dot-paths to atomically increment: e.g. {'account.failedLoginAttempts': 1} */
129
+ inc?: Record<string, number>;
130
+ }
131
+ type UserAuthErrorType = "NOT_FOUND" | "ALREADY_EXISTS" | "LOCKED" | "INACTIVE" | "INVALID_CREDENTIALS" | "POLICY_VIOLATION" | "PASSWORDS_MISMATCH" | "PASSWORD_IN_HISTORY" | "MFA_REQUIRED" | "MFA_INVALID" | "MFA_NOT_CONFIGURED";
132
+ interface LoginResult<T extends object = object> {
133
+ user: UserCredentials & T;
134
+ /** Whether MFA verification is required before granting full access */
135
+ mfaRequired: boolean;
136
+ }
137
+ interface LockStatus {
138
+ locked: boolean;
139
+ /** True when lock has a non-zero lockEnds that is in the past */
140
+ expired: boolean;
141
+ reason: string;
142
+ lockEnds: number;
143
+ }
144
+ interface PolicyCheckResult {
145
+ passed: boolean;
146
+ policies: {
147
+ description: string;
148
+ passed: boolean;
149
+ }[];
150
+ errors: string[];
151
+ }
152
+ interface TransferablePolicy {
153
+ rule: string;
154
+ description?: string;
155
+ errorMessage?: string;
156
+ }
157
+ interface MfaMethodInfo {
158
+ name: string;
159
+ isDefault: boolean;
160
+ masked: string;
161
+ }
162
+ interface TotpConfig {
163
+ /** Time step in seconds (default 30) */
164
+ period?: number;
165
+ /** Number of digits in the code (default 6) */
166
+ digits?: number;
167
+ /** Verification window — number of steps to check on each side (default 1) */
168
+ window?: number;
169
+ /** Injectable clock for testability */
170
+ clock?: () => number;
171
+ }
172
+ //#endregion
173
+ //#region src/store/user-store.d.ts
174
+ declare abstract class UserStore<T extends object = object> {
175
+ abstract exists(username: string): Promise<boolean>;
176
+ abstract findByUsername(username: string): Promise<(UserCredentials & T) | null>;
177
+ abstract create(data: UserCredentials & T): Promise<void>;
178
+ abstract update(username: string, update: UserStoreUpdate): Promise<boolean>;
179
+ /**
180
+ * Hard-delete the row. Returns `true` when a row was removed, `false` when
181
+ * the username was not found. Used by `UserService.deleteUser` (and in turn
182
+ * by the invite workflow's `auth.cancelInvite` step).
183
+ */
184
+ abstract delete(username: string): Promise<boolean>;
185
+ }
186
+ //#endregion
187
+ export { UserStoreUpdate as C, UserServiceConfig as S, TotpConfig as _, LockoutConfig as a, UserAuthErrorType as b, MfaMethod as c, PasswordData as d, PasswordPolicyContext as f, PolicyCheckResult as g, PasswordPolicyInstance as h, LockStatus as i, MfaMethodInfo as l, PasswordPolicyEvalFn as m, AccountData as n, LoginResult as o, PasswordPolicyDef as p, DeepPartial as r, MfaData as s, UserStore as t, PasswordConfig as u, TransferablePolicy as v, UserCredentials as x, TrustedDeviceRecord as y };
@@ -0,0 +1,187 @@
1
+ //#region src/types.d.ts
2
+ interface UserCredentials {
3
+ id: string;
4
+ username: string;
5
+ password: PasswordData;
6
+ account: AccountData;
7
+ mfa: MfaData;
8
+ /**
9
+ * Hashed backup codes (SHA-256, hex-encoded). Generated via
10
+ * `UserService.generateBackupCodes`. Undefined when the user has not
11
+ * enrolled backup codes; an empty array means all codes were consumed.
12
+ */
13
+ backupCodes?: string[];
14
+ /**
15
+ * Persisted device-trust records ("remember this device, skip MFA next
16
+ * time"). Managed by `UserService.{issue,add,verify,revoke,list}TrustedDevice`.
17
+ * Absent when the user has never opted in.
18
+ */
19
+ trustedDevices?: TrustedDeviceRecord[];
20
+ }
21
+ interface TrustedDeviceRecord {
22
+ /** `<raw>.<sig>` — what we hand back to the consumer and what they round-trip. */
23
+ token: string;
24
+ /** Bound IP — set when `deviceTrust.bindsTo === 'cookie+ip'`. */
25
+ ip?: string;
26
+ issuedAt: number;
27
+ expiresAt: number;
28
+ /** Optional human-readable label (e.g. user-agent summary). */
29
+ name?: string;
30
+ }
31
+ interface PasswordData {
32
+ /** Self-describing scrypt hash: $scrypt$N=...,r=...,p=...,l=...$salt$hash */
33
+ hash: string;
34
+ /** Previous password hashes (self-describing strings) */
35
+ history: string[];
36
+ lastChanged: number;
37
+ /** True when password was system-generated and user hasn't set their own */
38
+ isInitial: boolean;
39
+ }
40
+ interface AccountData {
41
+ active: boolean;
42
+ locked: boolean;
43
+ lockReason: string;
44
+ /** 0 = permanent lock, >0 = timestamp (ms) when lock expires */
45
+ lockEnds: number;
46
+ failedLoginAttempts: number;
47
+ lastLogin: number;
48
+ /**
49
+ * True while the user record exists from an admin-issued invite but the
50
+ * invitee has not yet accepted (set password + activate). Used by
51
+ * `InviteWorkflow` to gate the accept tail, reject duplicate invites, and
52
+ * power `auth.reInvite` / `auth.cancelInvite`. Absent / `false` once the
53
+ * invite has been accepted.
54
+ */
55
+ pendingInvitation?: boolean;
56
+ }
57
+ interface MfaData {
58
+ /** Registered MFA methods */
59
+ methods: MfaMethod[];
60
+ /** Name of the default MFA method */
61
+ defaultMethod: string;
62
+ /** Auto-send MFA challenge on login */
63
+ autoSend: boolean;
64
+ }
65
+ interface MfaMethod {
66
+ /** Method name: 'email', 'sms', 'totp' */
67
+ name: string;
68
+ /** Whether this method has been verified/confirmed */
69
+ confirmed: boolean;
70
+ /** The method's value: email address, phone number, or TOTP secret */
71
+ value: string;
72
+ }
73
+ interface UserServiceConfig {
74
+ password?: PasswordConfig;
75
+ lockout?: LockoutConfig;
76
+ /** Injectable clock for testability. Defaults to Date.now */
77
+ clock?: () => number;
78
+ /**
79
+ * Device-trust config. Required (with a non-empty `secret`) when any
80
+ * `issueTrustedDevice` / `verifyTrustedDevice` API is called; the methods
81
+ * throw clearly when invoked without it.
82
+ */
83
+ deviceTrust?: {
84
+ /** HMAC-SHA256 signing secret for trust-device tokens. */secret: string;
85
+ };
86
+ }
87
+ interface PasswordConfig {
88
+ /** Pepper string prepended to password before hashing */
89
+ pepper?: string;
90
+ /** Number of historical hashes to retain (0 = disabled) */
91
+ historyLength?: number;
92
+ /** scrypt cost parameter N (default 16384) */
93
+ scryptN?: number;
94
+ /** scrypt block size r (default 8) */
95
+ scryptR?: number;
96
+ /** scrypt parallelism p (default 1) */
97
+ scryptP?: number;
98
+ /** Hash output length in bytes (default 64) */
99
+ keyLength?: number;
100
+ /** Password policy rules */
101
+ policies?: (PasswordPolicyDef | PasswordPolicyInstance)[];
102
+ }
103
+ interface LockoutConfig {
104
+ /** Lock after this many failed attempts (0 = disabled) */
105
+ threshold?: number;
106
+ /** Lock duration in ms (0 = permanent) */
107
+ duration?: number;
108
+ }
109
+ interface PasswordPolicyDef {
110
+ rule: string | PasswordPolicyEvalFn;
111
+ description?: string;
112
+ errorMessage?: string;
113
+ }
114
+ type PasswordPolicyEvalFn = (password: string, context?: PasswordPolicyContext) => boolean | Promise<boolean>;
115
+ interface PasswordPolicyContext {
116
+ passwordData?: PasswordData;
117
+ passwordConfig?: PasswordConfig;
118
+ }
119
+ /** Interface satisfied by the PasswordPolicy class (avoids circular import) */
120
+ interface PasswordPolicyInstance extends PasswordPolicyDef {
121
+ evaluate(password: string, context?: PasswordPolicyContext): boolean | Promise<boolean>;
122
+ transferable: boolean;
123
+ }
124
+ type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P] };
125
+ interface UserStoreUpdate {
126
+ /** Partial object with fields to set (deep-merged) */
127
+ set?: DeepPartial<UserCredentials>;
128
+ /** Dot-paths to atomically increment: e.g. {'account.failedLoginAttempts': 1} */
129
+ inc?: Record<string, number>;
130
+ }
131
+ type UserAuthErrorType = "NOT_FOUND" | "ALREADY_EXISTS" | "LOCKED" | "INACTIVE" | "INVALID_CREDENTIALS" | "POLICY_VIOLATION" | "PASSWORDS_MISMATCH" | "PASSWORD_IN_HISTORY" | "MFA_REQUIRED" | "MFA_INVALID" | "MFA_NOT_CONFIGURED";
132
+ interface LoginResult<T extends object = object> {
133
+ user: UserCredentials & T;
134
+ /** Whether MFA verification is required before granting full access */
135
+ mfaRequired: boolean;
136
+ }
137
+ interface LockStatus {
138
+ locked: boolean;
139
+ /** True when lock has a non-zero lockEnds that is in the past */
140
+ expired: boolean;
141
+ reason: string;
142
+ lockEnds: number;
143
+ }
144
+ interface PolicyCheckResult {
145
+ passed: boolean;
146
+ policies: {
147
+ description: string;
148
+ passed: boolean;
149
+ }[];
150
+ errors: string[];
151
+ }
152
+ interface TransferablePolicy {
153
+ rule: string;
154
+ description?: string;
155
+ errorMessage?: string;
156
+ }
157
+ interface MfaMethodInfo {
158
+ name: string;
159
+ isDefault: boolean;
160
+ masked: string;
161
+ }
162
+ interface TotpConfig {
163
+ /** Time step in seconds (default 30) */
164
+ period?: number;
165
+ /** Number of digits in the code (default 6) */
166
+ digits?: number;
167
+ /** Verification window — number of steps to check on each side (default 1) */
168
+ window?: number;
169
+ /** Injectable clock for testability */
170
+ clock?: () => number;
171
+ }
172
+ //#endregion
173
+ //#region src/store/user-store.d.ts
174
+ declare abstract class UserStore<T extends object = object> {
175
+ abstract exists(username: string): Promise<boolean>;
176
+ abstract findByUsername(username: string): Promise<(UserCredentials & T) | null>;
177
+ abstract create(data: UserCredentials & T): Promise<void>;
178
+ abstract update(username: string, update: UserStoreUpdate): Promise<boolean>;
179
+ /**
180
+ * Hard-delete the row. Returns `true` when a row was removed, `false` when
181
+ * the username was not found. Used by `UserService.deleteUser` (and in turn
182
+ * by the invite workflow's `auth.cancelInvite` step).
183
+ */
184
+ abstract delete(username: string): Promise<boolean>;
185
+ }
186
+ //#endregion
187
+ export { UserStoreUpdate as C, UserServiceConfig as S, TotpConfig as _, LockoutConfig as a, UserAuthErrorType as b, MfaMethod as c, PasswordData as d, PasswordPolicyContext as f, PolicyCheckResult as g, PasswordPolicyInstance as h, LockStatus as i, MfaMethodInfo as l, PasswordPolicyEvalFn as m, AccountData as n, LoginResult as o, PasswordPolicyDef as p, DeepPartial as r, MfaData as s, UserStore as t, PasswordConfig as u, TransferablePolicy as v, UserCredentials as x, TrustedDeviceRecord as y };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@aooth/user",
3
+ "version": "0.1.1",
4
+ "description": "User credential primitives for aoothjs",
5
+ "keywords": [
6
+ "aoothjs",
7
+ "auth",
8
+ "authentication",
9
+ "user"
10
+ ],
11
+ "homepage": "https://github.com/moostjs/aoothjs/tree/main/packages/user#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/moostjs/aoothjs/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": "Artem Maltsev",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/moostjs/aoothjs.git",
20
+ "directory": "packages/user"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src/atscript-db/user-credentials.as"
25
+ ],
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "main": "dist/index.mjs",
29
+ "module": "./dist/index.mjs",
30
+ "types": "dist/index.d.mts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.mts",
34
+ "import": "./dist/index.mjs",
35
+ "require": "./dist/index.cjs"
36
+ },
37
+ "./atscript-db": {
38
+ "types": "./dist/atscript-db.d.mts",
39
+ "import": "./dist/atscript-db.mjs",
40
+ "require": "./dist/atscript-db.cjs"
41
+ },
42
+ "./atscript-db/model.as": "./src/atscript-db/user-credentials.as",
43
+ "./package.json": "./package.json"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@prostojs/ftring": "^0.0.3"
50
+ },
51
+ "devDependencies": {
52
+ "@atscript/core": "^0.1.56",
53
+ "@atscript/db": "^0.1.80",
54
+ "@atscript/db-sql-tools": "^0.1.80",
55
+ "@atscript/db-sqlite": "^0.1.80",
56
+ "@atscript/typescript": "^0.1.56",
57
+ "@types/better-sqlite3": "^7.6.13",
58
+ "better-sqlite3": "^12.6.2",
59
+ "unplugin-atscript": "^0.1.56"
60
+ },
61
+ "peerDependencies": {
62
+ "@atscript/db": ">=0.1.79"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@atscript/db": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "scripts": {
70
+ "build": "vp pack",
71
+ "dev": "vp pack --watch",
72
+ "test": "vp test",
73
+ "check": "vp check"
74
+ }
75
+ }