@puzzle-ui/auth 0.0.0-stage → 0.5.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.
Files changed (95) hide show
  1. package/README.md +301 -2
  2. package/dist/LICENSE +21 -0
  3. package/dist/auth.lifecycle.manager.cjs +835 -0
  4. package/dist/auth.lifecycle.manager.cjs.map +1 -0
  5. package/dist/auth.lifecycle.manager.d.cts +115 -0
  6. package/dist/auth.lifecycle.manager.d.ts +115 -0
  7. package/dist/auth.lifecycle.manager.js +835 -0
  8. package/dist/auth.lifecycle.manager.js.map +1 -0
  9. package/dist/auth.manager.cjs +454 -0
  10. package/dist/auth.manager.cjs.map +1 -0
  11. package/dist/auth.manager.d.cts +54 -0
  12. package/dist/auth.manager.d.ts +54 -0
  13. package/dist/auth.manager.js +454 -0
  14. package/dist/auth.manager.js.map +1 -0
  15. package/dist/auth.mfa.manager.cjs +1488 -0
  16. package/dist/auth.mfa.manager.cjs.map +1 -0
  17. package/dist/auth.mfa.manager.d.cts +100 -0
  18. package/dist/auth.mfa.manager.d.ts +100 -0
  19. package/dist/auth.mfa.manager.js +1488 -0
  20. package/dist/auth.mfa.manager.js.map +1 -0
  21. package/dist/auth.rate-limit.cjs +78 -0
  22. package/dist/auth.rate-limit.cjs.map +1 -0
  23. package/dist/auth.rate-limit.d.cts +24 -0
  24. package/dist/auth.rate-limit.d.ts +24 -0
  25. package/dist/auth.rate-limit.js +78 -0
  26. package/dist/auth.rate-limit.js.map +1 -0
  27. package/dist/auth.step-up.manager.cjs +269 -0
  28. package/dist/auth.step-up.manager.cjs.map +1 -0
  29. package/dist/auth.step-up.manager.d.cts +74 -0
  30. package/dist/auth.step-up.manager.d.ts +74 -0
  31. package/dist/auth.step-up.manager.js +268 -0
  32. package/dist/auth.step-up.manager.js.map +1 -0
  33. package/dist/auth.storage.cjs +164 -0
  34. package/dist/auth.storage.cjs.map +1 -0
  35. package/dist/auth.storage.d.cts +29 -0
  36. package/dist/auth.storage.d.ts +29 -0
  37. package/dist/auth.storage.js +148 -0
  38. package/dist/auth.storage.js.map +1 -0
  39. package/dist/errors/auth.errors.cjs +101 -0
  40. package/dist/errors/auth.errors.cjs.map +1 -0
  41. package/dist/errors/auth.errors.d.cts +62 -0
  42. package/dist/errors/auth.errors.d.ts +62 -0
  43. package/dist/errors/auth.errors.js +90 -0
  44. package/dist/errors/auth.errors.js.map +1 -0
  45. package/dist/index.cjs +54 -0
  46. package/dist/index.d.cts +21 -0
  47. package/dist/index.d.ts +21 -0
  48. package/dist/index.js +13 -0
  49. package/dist/providers/mfa-provider.types.d.cts +66 -0
  50. package/dist/providers/mfa-provider.types.d.ts +66 -0
  51. package/dist/providers/provider.types.d.cts +34 -0
  52. package/dist/providers/provider.types.d.ts +34 -0
  53. package/dist/providers/simplewebauthn-provider.cjs +185 -0
  54. package/dist/providers/simplewebauthn-provider.cjs.map +1 -0
  55. package/dist/providers/simplewebauthn-provider.d.cts +35 -0
  56. package/dist/providers/simplewebauthn-provider.d.ts +35 -0
  57. package/dist/providers/simplewebauthn-provider.js +185 -0
  58. package/dist/providers/simplewebauthn-provider.js.map +1 -0
  59. package/dist/providers/webcrypto-secret-protector.cjs +118 -0
  60. package/dist/providers/webcrypto-secret-protector.cjs.map +1 -0
  61. package/dist/providers/webcrypto-secret-protector.d.cts +14 -0
  62. package/dist/providers/webcrypto-secret-protector.d.ts +14 -0
  63. package/dist/providers/webcrypto-secret-protector.js +118 -0
  64. package/dist/providers/webcrypto-secret-protector.js.map +1 -0
  65. package/dist/types/auth-lifecycle.types.d.cts +53 -0
  66. package/dist/types/auth-lifecycle.types.d.ts +53 -0
  67. package/dist/types/auth-manager.types.d.cts +106 -0
  68. package/dist/types/auth-manager.types.d.ts +106 -0
  69. package/dist/types/credential.types.d.cts +9 -0
  70. package/dist/types/credential.types.d.ts +9 -0
  71. package/dist/types/mfa.types.d.cts +244 -0
  72. package/dist/types/mfa.types.d.ts +244 -0
  73. package/dist/types/state.types.d.cts +33 -0
  74. package/dist/types/state.types.d.ts +33 -0
  75. package/dist/types/user.types.d.cts +9 -0
  76. package/dist/types/user.types.d.ts +9 -0
  77. package/dist/utils/auth-crypto.utils.cjs +38 -0
  78. package/dist/utils/auth-crypto.utils.cjs.map +1 -0
  79. package/dist/utils/auth-crypto.utils.d.cts +4 -0
  80. package/dist/utils/auth-crypto.utils.d.ts +4 -0
  81. package/dist/utils/auth-crypto.utils.js +35 -0
  82. package/dist/utils/auth-crypto.utils.js.map +1 -0
  83. package/dist/utils/email.utils.cjs +20 -0
  84. package/dist/utils/email.utils.cjs.map +1 -0
  85. package/dist/utils/email.utils.d.cts +2 -0
  86. package/dist/utils/email.utils.d.ts +2 -0
  87. package/dist/utils/email.utils.js +19 -0
  88. package/dist/utils/email.utils.js.map +1 -0
  89. package/dist/utils/totp.utils.cjs +145 -0
  90. package/dist/utils/totp.utils.cjs.map +1 -0
  91. package/dist/utils/totp.utils.d.cts +26 -0
  92. package/dist/utils/totp.utils.d.ts +26 -0
  93. package/dist/utils/totp.utils.js +140 -0
  94. package/dist/utils/totp.utils.js.map +1 -0
  95. package/package.json +45 -4
@@ -0,0 +1,244 @@
1
+ import { AuthResult } from "./auth-manager.types.js";
2
+ import { ProtectedAuthSecret } from "../providers/mfa-provider.types.js";
3
+ import { AuthState } from "./state.types.js";
4
+ import { StorageRecord } from "@puzzle-ui/storage";
5
+ export type AuthMfaFactorType = "totp" | "passkey";
6
+ export type AuthMfaFactorStatus = "pending" | "active";
7
+ export type AuthMfaAvailableFactor = "totp" | "passkey" | "recovery-code";
8
+ export type AuthMfaEnrollmentFactor = "totp" | "passkey";
9
+ export type AuthMfaEnrollmentPendingResult = {
10
+ readonly status: "mfa-enrollment-pending";
11
+ readonly challengeId: string;
12
+ /**
13
+ * Server-only temporary credential.
14
+ *
15
+ * Deliver exclusively through a short-lived HttpOnly cookie.
16
+ * Never serialize this token into browser JSON or browser storage.
17
+ */
18
+ readonly challengeToken: string;
19
+ readonly expiresAt: Date;
20
+ readonly availableFactors: readonly AuthMfaEnrollmentFactor[];
21
+ };
22
+ export type AuthMfaEnrollmentCompleted = {
23
+ /**
24
+ * The authenticated result is only created after the first factor
25
+ * has been successfully verified and the enrollment transaction commits.
26
+ */
27
+ readonly auth: AuthResult;
28
+ readonly factor: AuthMfaFactor;
29
+ readonly recoveryCodes: readonly string[];
30
+ };
31
+ export type BeginRequiredMfaEnrollmentInput = {
32
+ readonly challengeToken: string;
33
+ readonly ipAddress: string;
34
+ readonly label?: string;
35
+ };
36
+ export type VerifyRequiredTotpEnrollmentInput = {
37
+ readonly challengeToken: string;
38
+ readonly factorId: string;
39
+ readonly code: string;
40
+ readonly ipAddress: string;
41
+ };
42
+ export type FinishRequiredPasskeyEnrollmentInput = {
43
+ readonly challengeToken: string;
44
+ readonly response: unknown;
45
+ readonly ipAddress: string;
46
+ };
47
+ export type AuthMfaFactor = {
48
+ readonly id: string;
49
+ readonly type: AuthMfaFactorType;
50
+ readonly status: AuthMfaFactorStatus;
51
+ readonly label: string;
52
+ readonly createdAt: Date;
53
+ readonly lastUsedAt: Date | null;
54
+ };
55
+ export type AuthMfaFactorStorageRecord = StorageRecord & {
56
+ readonly userId: string;
57
+ /**
58
+ * Globally unique key.
59
+ * TOTP: totp:<factor-id>
60
+ * Passkey: passkey:<credential-id>
61
+ */
62
+ readonly factorKey: string;
63
+ readonly type: AuthMfaFactorType;
64
+ readonly status: AuthMfaFactorStatus;
65
+ readonly label: string;
66
+ /** Encrypted TOTP secret; never populated for passkeys. */
67
+ readonly secret: ProtectedAuthSecret | null;
68
+ /** WebAuthn credential data; never populated for TOTP. */
69
+ readonly credentialId: string | null;
70
+ readonly publicKey: string | null;
71
+ readonly counter: number;
72
+ readonly transports: readonly string[];
73
+ /** Prevents reuse of an accepted TOTP time step. */
74
+ readonly lastUsedStep: number | null;
75
+ readonly lastUsedAt: Date | null;
76
+ /** Pending enrollments expire automatically at the domain level. */
77
+ readonly expiresAt: Date | null;
78
+ };
79
+ export type AuthMfaChallengePurpose = "sign-in" | "enrollment" | "passkey-registration" | "passkey-sign-in";
80
+ export type AuthMfaChallengeStorageRecord = StorageRecord & {
81
+ readonly purpose: AuthMfaChallengePurpose;
82
+ /** SHA-256 hash of the temporary challenge token. */
83
+ readonly tokenHash: string;
84
+ /** Null for discoverable passkey sign-in. */
85
+ readonly userId: string | null;
86
+ /** Factors accepted by the current MFA challenge. */
87
+ readonly factorIds: readonly string[];
88
+ /** Passkeys allowed for an in-progress WebAuthn ceremony. */
89
+ readonly webAuthnFactorIds: readonly string[];
90
+ /** WebAuthn challenge, bound to this temporary record. */
91
+ readonly webAuthnChallenge: string | null;
92
+ /** Label selected when a passkey registration was initiated. */
93
+ readonly factorLabel: string | null;
94
+ /**
95
+ * Account-state snapshot for password-backed sign-in challenges.
96
+ * A challenge becomes invalid if either record changes.
97
+ */
98
+ readonly userRevision: number | null;
99
+ readonly credentialRevision: number | null;
100
+ readonly attemptCount: number;
101
+ readonly maximumAttempts: number;
102
+ readonly expiresAt: Date;
103
+ readonly consumedAt: Date | null;
104
+ };
105
+ export type AuthRecoveryCodeStorageRecord = StorageRecord & {
106
+ readonly userId: string;
107
+ readonly factorId: string;
108
+ readonly codeHash: string;
109
+ readonly consumedAt: Date | null;
110
+ };
111
+ export type AuthMfaPendingResult = {
112
+ readonly status: "mfa-pending";
113
+ readonly challengeId: string;
114
+ /**
115
+ * Server-only temporary credential.
116
+ *
117
+ * Deliver this through a short-lived HttpOnly cookie.
118
+ * Never serialize it into browser JSON or browser storage.
119
+ */
120
+ readonly challengeToken: string;
121
+ readonly expiresAt: Date;
122
+ readonly availableFactors: readonly AuthMfaAvailableFactor[];
123
+ };
124
+ export type AuthMfaGateway = {
125
+ initialize(): Promise<void>;
126
+ createSignInChallenge(input: {
127
+ readonly userId: string;
128
+ readonly ipAddress: string;
129
+ }): Promise<AuthMfaPendingResult | null>;
130
+ createEnrollmentChallenge(input: {
131
+ readonly userId: string;
132
+ readonly ipAddress: string;
133
+ }): Promise<AuthMfaEnrollmentPendingResult>;
134
+ /**
135
+ * Returns whether the user currently has at least one active MFA factor.
136
+ * Used by AuthManager to enforce policy against existing sessions.
137
+ */
138
+ hasActiveFactor(userId: string): Promise<boolean>;
139
+ getPendingAuthState(token: string): Promise<AuthState | null>;
140
+ revokeChallenges(userId: string): Promise<void>;
141
+ };
142
+ export type AuthMfaTotpOptions = {
143
+ readonly digits: number;
144
+ readonly periodSeconds: number;
145
+ /** Number of adjacent time steps accepted on either side. */
146
+ readonly window: number;
147
+ };
148
+ export type AuthMfaWebAuthnOptions = {
149
+ readonly rpId: string;
150
+ readonly rpName: string;
151
+ /** Exact expected browser origin, including scheme and port. */
152
+ readonly origin: string;
153
+ };
154
+ export type AuthMfaManagerOptions = {
155
+ readonly rateLimitKeySecret: string;
156
+ readonly usersCollection?: string;
157
+ readonly credentialsCollection?: string;
158
+ readonly auditCollection?: string;
159
+ readonly factorsCollection?: string;
160
+ readonly challengesCollection?: string;
161
+ readonly recoveryCodesCollection?: string;
162
+ readonly challengeLifetimeSeconds?: number;
163
+ readonly recoveryCodeCount?: number;
164
+ readonly issuer?: string;
165
+ readonly totp?: Partial<AuthMfaTotpOptions>;
166
+ readonly webAuthn?: AuthMfaWebAuthnOptions;
167
+ /** Keep this aligned with AuthManagerOptions.requireVerifiedEmail. */
168
+ readonly requireVerifiedEmail?: boolean;
169
+ readonly clock?: () => Date;
170
+ };
171
+ export type BeginTotpEnrollmentInput = {
172
+ readonly sessionToken: string;
173
+ readonly currentPassword: string;
174
+ readonly ipAddress: string;
175
+ readonly label?: string;
176
+ };
177
+ export type TotpEnrollmentStarted = {
178
+ readonly factorId: string;
179
+ readonly secret: string;
180
+ readonly otpauthUrl: string;
181
+ readonly expiresAt: Date;
182
+ };
183
+ export type VerifyTotpEnrollmentInput = {
184
+ readonly sessionToken: string;
185
+ readonly factorId: string;
186
+ readonly code: string;
187
+ readonly ipAddress: string;
188
+ };
189
+ export type MfaEnrollmentCompleted = {
190
+ readonly factor: AuthMfaFactor;
191
+ readonly recoveryCodes: readonly string[];
192
+ };
193
+ export type BeginPasskeyRegistrationInput = {
194
+ readonly sessionToken: string;
195
+ readonly currentPassword: string;
196
+ readonly ipAddress: string;
197
+ readonly label?: string;
198
+ };
199
+ export type PasskeyRegistrationStarted = {
200
+ readonly challengeId: string;
201
+ readonly challengeToken: string;
202
+ readonly options: Readonly<Record<string, unknown>>;
203
+ readonly expiresAt: Date;
204
+ };
205
+ export type FinishPasskeyRegistrationInput = {
206
+ readonly sessionToken: string;
207
+ readonly challengeToken: string;
208
+ readonly response: unknown;
209
+ readonly ipAddress: string;
210
+ };
211
+ export type BeginPasskeySignInInput = {
212
+ readonly ipAddress: string;
213
+ };
214
+ export type PasskeySignInStarted = {
215
+ readonly challengeId: string;
216
+ readonly challengeToken: string;
217
+ readonly options: Readonly<Record<string, unknown>>;
218
+ readonly expiresAt: Date;
219
+ };
220
+ export type CompleteMfaSignInInput = {
221
+ readonly challengeToken: string;
222
+ readonly code: string;
223
+ readonly ipAddress: string;
224
+ };
225
+ export type PasskeyMfaStarted = {
226
+ readonly options: Readonly<Record<string, unknown>>;
227
+ readonly expiresAt: Date;
228
+ };
229
+ export type PasskeyMfaCompletedInput = {
230
+ readonly challengeToken: string;
231
+ readonly response: unknown;
232
+ readonly ipAddress: string;
233
+ };
234
+ export type RemoveMfaFactorInput = {
235
+ readonly sessionToken: string;
236
+ readonly factorId: string;
237
+ readonly currentPassword: string;
238
+ readonly ipAddress: string;
239
+ };
240
+ export type RegenerateRecoveryCodesInput = {
241
+ readonly sessionToken: string;
242
+ readonly currentPassword: string;
243
+ readonly ipAddress: string;
244
+ };
@@ -0,0 +1,33 @@
1
+ export type AuthState = {
2
+ readonly status: "unauthenticated";
3
+ } | {
4
+ readonly status: "primary-authenticated";
5
+ readonly userId: string;
6
+ } | {
7
+ readonly status: "mfa-pending";
8
+ readonly userId: string;
9
+ readonly challengeId: string;
10
+ readonly expiresAt: Date;
11
+ } | {
12
+ readonly status: "mfa-enrollment-pending";
13
+ readonly userId: string;
14
+ readonly challengeId: string;
15
+ readonly expiresAt: Date;
16
+ readonly availableFactors: readonly ("totp" | "passkey")[];
17
+ } | {
18
+ /**
19
+ * The session is valid, but the current MFA policy requires an
20
+ * active factor. This state is restricted: it is not authenticated
21
+ * application access.
22
+ *
23
+ * The user may use the enrollment endpoints, which require password
24
+ * confirmation, to add the first required factor.
25
+ */
26
+ readonly status: "mfa-enrollment-required";
27
+ readonly userId: string;
28
+ readonly sessionId: string;
29
+ } | {
30
+ readonly status: "authenticated";
31
+ readonly userId: string;
32
+ readonly sessionId: string;
33
+ };
@@ -0,0 +1,33 @@
1
+ export type AuthState = {
2
+ readonly status: "unauthenticated";
3
+ } | {
4
+ readonly status: "primary-authenticated";
5
+ readonly userId: string;
6
+ } | {
7
+ readonly status: "mfa-pending";
8
+ readonly userId: string;
9
+ readonly challengeId: string;
10
+ readonly expiresAt: Date;
11
+ } | {
12
+ readonly status: "mfa-enrollment-pending";
13
+ readonly userId: string;
14
+ readonly challengeId: string;
15
+ readonly expiresAt: Date;
16
+ readonly availableFactors: readonly ("totp" | "passkey")[];
17
+ } | {
18
+ /**
19
+ * The session is valid, but the current MFA policy requires an
20
+ * active factor. This state is restricted: it is not authenticated
21
+ * application access.
22
+ *
23
+ * The user may use the enrollment endpoints, which require password
24
+ * confirmation, to add the first required factor.
25
+ */
26
+ readonly status: "mfa-enrollment-required";
27
+ readonly userId: string;
28
+ readonly sessionId: string;
29
+ } | {
30
+ readonly status: "authenticated";
31
+ readonly userId: string;
32
+ readonly sessionId: string;
33
+ };
@@ -0,0 +1,9 @@
1
+ export type AuthUser = {
2
+ readonly id: string;
3
+ readonly email: string;
4
+ readonly emailVerifiedAt: Date | null;
5
+ readonly name: string | null;
6
+ readonly image: string | null;
7
+ readonly createdAt: Date;
8
+ readonly updatedAt: Date;
9
+ };
@@ -0,0 +1,9 @@
1
+ export type AuthUser = {
2
+ readonly id: string;
3
+ readonly email: string;
4
+ readonly emailVerifiedAt: Date | null;
5
+ readonly name: string | null;
6
+ readonly image: string | null;
7
+ readonly createdAt: Date;
8
+ readonly updatedAt: Date;
9
+ };
@@ -0,0 +1,38 @@
1
+ const require_auth_errors = require("../errors/auth.errors.cjs");
2
+ // src/utils/auth-crypto.utils.ts
3
+ const getCrypto = () => {
4
+ const cryptoApi = globalThis.crypto;
5
+ if (!cryptoApi?.getRandomValues || !cryptoApi.subtle) throw new require_auth_errors.AuthConfigurationError("This runtime does not provide the required Web Crypto APIs.");
6
+ return cryptoApi;
7
+ };
8
+ const generateAuthId = (prefix) => {
9
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
10
+ getCrypto().getRandomValues(bytes);
11
+ return `${prefix}_${Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
12
+ };
13
+ const createRateLimitKey = async (secret, value) => {
14
+ const cryptoApi = getCrypto();
15
+ const key = await cryptoApi.subtle.importKey("raw", new TextEncoder().encode(secret), {
16
+ name: "HMAC",
17
+ hash: "SHA-256"
18
+ }, false, ["sign"]);
19
+ const signature = await cryptoApi.subtle.sign("HMAC", key, new TextEncoder().encode(value));
20
+ const bytes = new Uint8Array(signature);
21
+ return `hmac-sha256:${Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
22
+ };
23
+ const generateSecureAuthToken = () => {
24
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
25
+ getCrypto().getRandomValues(bytes);
26
+ return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
27
+ };
28
+ const hashAuthToken = async (token) => {
29
+ if (typeof token !== "string" || token.length === 0) throw new require_auth_errors.AuthConfigurationError("A valid token is required.");
30
+ const digest = await getCrypto().subtle.digest("SHA-256", new TextEncoder().encode(token));
31
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
32
+ };
33
+ exports.createRateLimitKey = createRateLimitKey;
34
+ exports.generateAuthId = generateAuthId;
35
+ exports.generateSecureAuthToken = generateSecureAuthToken;
36
+ exports.hashAuthToken = hashAuthToken;
37
+
38
+ //# sourceMappingURL=auth-crypto.utils.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-crypto.utils.cjs","names":["AuthConfigurationError"],"sources":["../../src/utils/auth-crypto.utils.ts"],"sourcesContent":["import { AuthConfigurationError } from \"../errors/auth.errors\";\n\nconst getCrypto = (): Crypto => {\n const cryptoApi = globalThis.crypto;\n\n if (!cryptoApi?.getRandomValues || !cryptoApi.subtle) {\n throw new AuthConfigurationError(\n \"This runtime does not provide the required Web Crypto APIs.\",\n );\n }\n\n return cryptoApi;\n};\n\nexport const generateAuthId = (prefix: string): string => {\n const bytes = new Uint8Array(16);\n\n getCrypto().getRandomValues(bytes);\n\n const value = Array.from(bytes)\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return `${prefix}_${value}`;\n};\n\nexport const createRateLimitKey = async (\n secret: string,\n value: string,\n): Promise<string> => {\n const cryptoApi = getCrypto();\n\n const key = await cryptoApi.subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n {\n name: \"HMAC\",\n hash: \"SHA-256\",\n },\n false,\n [\"sign\"],\n );\n\n const signature = await cryptoApi.subtle.sign(\n \"HMAC\",\n key,\n new TextEncoder().encode(value),\n );\n\n const bytes = new Uint8Array(signature);\n\n const hash = Array.from(bytes)\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return `hmac-sha256:${hash}`;\n};\n\nexport const generateSecureAuthToken = (): string => {\n const bytes = new Uint8Array(32);\n\n getCrypto().getRandomValues(bytes);\n\n return Array.from(bytes)\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n};\n\nexport const hashAuthToken = async (token: string): Promise<string> => {\n if (typeof token !== \"string\" || token.length === 0) {\n throw new AuthConfigurationError(\"A valid token is required.\");\n }\n\n const digest = await getCrypto().subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(token),\n );\n\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n};\n"],"mappings":";;AAEA,MAAM,kBAA0B;CAC9B,MAAM,YAAY,WAAW;CAE7B,IAAI,CAAC,WAAW,mBAAmB,CAAC,UAAU,QAC5C,MAAM,IAAIA,oBAAAA,uBACR,6DACF;CAGF,OAAO;AACT;AAEA,MAAa,kBAAkB,WAA2B;CACxD,MAAM,wBAAQ,IAAI,WAAW,EAAE;CAE/B,UAAU,CAAC,CAAC,gBAAgB,KAAK;CAMjC,OAAO,GAAG,OAAO,GAJH,MAAM,KAAK,KAAK,CAAC,CAC5B,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAEgB;AAC1B;AAEA,MAAa,qBAAqB,OAChC,QACA,UACoB;CACpB,MAAM,YAAY,UAAU;CAE5B,MAAM,MAAM,MAAM,UAAU,OAAO,UACjC,OACA,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAC/B;EACE,MAAM;EACN,MAAM;CACR,GACA,OACA,CAAC,MAAM,CACT;CAEA,MAAM,YAAY,MAAM,UAAU,OAAO,KACvC,QACA,KACA,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,CAChC;CAEA,MAAM,QAAQ,IAAI,WAAW,SAAS;CAMtC,OAAO,eAJM,MAAM,KAAK,KAAK,CAAC,CAC3B,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAEiB;AAC3B;AAEA,MAAa,gCAAwC;CACnD,MAAM,wBAAQ,IAAI,WAAW,EAAE;CAE/B,UAAU,CAAC,CAAC,gBAAgB,KAAK;CAEjC,OAAO,MAAM,KAAK,KAAK,CAAC,CACrB,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAAE;AACZ;AAEA,MAAa,gBAAgB,OAAO,UAAmC;CACrE,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAChD,MAAM,IAAIA,oBAAAA,uBAAuB,4BAA4B;CAG/D,MAAM,SAAS,MAAM,UAAU,CAAC,CAAC,OAAO,OACtC,WACA,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,CAChC;CAEA,OAAO,MAAM,KAAK,IAAI,WAAW,MAAM,CAAC,CAAC,CACtC,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAAE;AACZ"}
@@ -0,0 +1,4 @@
1
+ export declare const generateAuthId: (prefix: string) => string;
2
+ export declare const createRateLimitKey: (secret: string, value: string) => Promise<string>;
3
+ export declare const generateSecureAuthToken: () => string;
4
+ export declare const hashAuthToken: (token: string) => Promise<string>;
@@ -0,0 +1,4 @@
1
+ export declare const generateAuthId: (prefix: string) => string;
2
+ export declare const createRateLimitKey: (secret: string, value: string) => Promise<string>;
3
+ export declare const generateSecureAuthToken: () => string;
4
+ export declare const hashAuthToken: (token: string) => Promise<string>;
@@ -0,0 +1,35 @@
1
+ import { AuthConfigurationError } from "../errors/auth.errors.js";
2
+ // src/utils/auth-crypto.utils.ts
3
+ const getCrypto = () => {
4
+ const cryptoApi = globalThis.crypto;
5
+ if (!cryptoApi?.getRandomValues || !cryptoApi.subtle) throw new AuthConfigurationError("This runtime does not provide the required Web Crypto APIs.");
6
+ return cryptoApi;
7
+ };
8
+ const generateAuthId = (prefix) => {
9
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
10
+ getCrypto().getRandomValues(bytes);
11
+ return `${prefix}_${Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
12
+ };
13
+ const createRateLimitKey = async (secret, value) => {
14
+ const cryptoApi = getCrypto();
15
+ const key = await cryptoApi.subtle.importKey("raw", new TextEncoder().encode(secret), {
16
+ name: "HMAC",
17
+ hash: "SHA-256"
18
+ }, false, ["sign"]);
19
+ const signature = await cryptoApi.subtle.sign("HMAC", key, new TextEncoder().encode(value));
20
+ const bytes = new Uint8Array(signature);
21
+ return `hmac-sha256:${Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
22
+ };
23
+ const generateSecureAuthToken = () => {
24
+ const bytes = /* @__PURE__ */ new Uint8Array(32);
25
+ getCrypto().getRandomValues(bytes);
26
+ return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
27
+ };
28
+ const hashAuthToken = async (token) => {
29
+ if (typeof token !== "string" || token.length === 0) throw new AuthConfigurationError("A valid token is required.");
30
+ const digest = await getCrypto().subtle.digest("SHA-256", new TextEncoder().encode(token));
31
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
32
+ };
33
+ export { createRateLimitKey, generateAuthId, generateSecureAuthToken, hashAuthToken };
34
+
35
+ //# sourceMappingURL=auth-crypto.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-crypto.utils.js","names":[],"sources":["../../src/utils/auth-crypto.utils.ts"],"sourcesContent":["import { AuthConfigurationError } from \"../errors/auth.errors\";\n\nconst getCrypto = (): Crypto => {\n const cryptoApi = globalThis.crypto;\n\n if (!cryptoApi?.getRandomValues || !cryptoApi.subtle) {\n throw new AuthConfigurationError(\n \"This runtime does not provide the required Web Crypto APIs.\",\n );\n }\n\n return cryptoApi;\n};\n\nexport const generateAuthId = (prefix: string): string => {\n const bytes = new Uint8Array(16);\n\n getCrypto().getRandomValues(bytes);\n\n const value = Array.from(bytes)\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return `${prefix}_${value}`;\n};\n\nexport const createRateLimitKey = async (\n secret: string,\n value: string,\n): Promise<string> => {\n const cryptoApi = getCrypto();\n\n const key = await cryptoApi.subtle.importKey(\n \"raw\",\n new TextEncoder().encode(secret),\n {\n name: \"HMAC\",\n hash: \"SHA-256\",\n },\n false,\n [\"sign\"],\n );\n\n const signature = await cryptoApi.subtle.sign(\n \"HMAC\",\n key,\n new TextEncoder().encode(value),\n );\n\n const bytes = new Uint8Array(signature);\n\n const hash = Array.from(bytes)\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n\n return `hmac-sha256:${hash}`;\n};\n\nexport const generateSecureAuthToken = (): string => {\n const bytes = new Uint8Array(32);\n\n getCrypto().getRandomValues(bytes);\n\n return Array.from(bytes)\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n};\n\nexport const hashAuthToken = async (token: string): Promise<string> => {\n if (typeof token !== \"string\" || token.length === 0) {\n throw new AuthConfigurationError(\"A valid token is required.\");\n }\n\n const digest = await getCrypto().subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(token),\n );\n\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\");\n};\n"],"mappings":";;AAEA,MAAM,kBAA0B;CAC9B,MAAM,YAAY,WAAW;CAE7B,IAAI,CAAC,WAAW,mBAAmB,CAAC,UAAU,QAC5C,MAAM,IAAI,uBACR,6DACF;CAGF,OAAO;AACT;AAEA,MAAa,kBAAkB,WAA2B;CACxD,MAAM,wBAAQ,IAAI,WAAW,EAAE;CAE/B,UAAU,CAAC,CAAC,gBAAgB,KAAK;CAMjC,OAAO,GAAG,OAAO,GAJH,MAAM,KAAK,KAAK,CAAC,CAC5B,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAEgB;AAC1B;AAEA,MAAa,qBAAqB,OAChC,QACA,UACoB;CACpB,MAAM,YAAY,UAAU;CAE5B,MAAM,MAAM,MAAM,UAAU,OAAO,UACjC,OACA,IAAI,YAAY,CAAC,CAAC,OAAO,MAAM,GAC/B;EACE,MAAM;EACN,MAAM;CACR,GACA,OACA,CAAC,MAAM,CACT;CAEA,MAAM,YAAY,MAAM,UAAU,OAAO,KACvC,QACA,KACA,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,CAChC;CAEA,MAAM,QAAQ,IAAI,WAAW,SAAS;CAMtC,OAAO,eAJM,MAAM,KAAK,KAAK,CAAC,CAC3B,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAEiB;AAC3B;AAEA,MAAa,gCAAwC;CACnD,MAAM,wBAAQ,IAAI,WAAW,EAAE;CAE/B,UAAU,CAAC,CAAC,gBAAgB,KAAK;CAEjC,OAAO,MAAM,KAAK,KAAK,CAAC,CACrB,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAAE;AACZ;AAEA,MAAa,gBAAgB,OAAO,UAAmC;CACrE,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAChD,MAAM,IAAI,uBAAuB,4BAA4B;CAG/D,MAAM,SAAS,MAAM,UAAU,CAAC,CAAC,OAAO,OACtC,WACA,IAAI,YAAY,CAAC,CAAC,OAAO,KAAK,CAChC;CAEA,OAAO,MAAM,KAAK,IAAI,WAAW,MAAM,CAAC,CAAC,CACtC,KAAK,SAAS,KAAK,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CACjD,KAAK,EAAE;AACZ"}
@@ -0,0 +1,20 @@
1
+ const require_auth_errors = require("../errors/auth.errors.cjs");
2
+ // src/utils/email.utils.ts
3
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4
+ const normalizeEmail = (email) => {
5
+ if (typeof email !== "string") throw new require_auth_errors.AuthValidationError("A valid email address is required.");
6
+ const normalized = email.trim().toLowerCase();
7
+ if (normalized.length === 0 || normalized.length > 254 || !EMAIL_PATTERN.test(normalized)) throw new require_auth_errors.AuthValidationError("A valid email address is required.");
8
+ return normalized;
9
+ };
10
+ const normalizeDisplayName = (name) => {
11
+ if (name === void 0 || name === null) return null;
12
+ if (typeof name !== "string") throw new require_auth_errors.AuthValidationError("Name must be a string.");
13
+ const normalized = name.trim();
14
+ if (normalized.length > 100) throw new require_auth_errors.AuthValidationError("Name must be 100 characters or fewer.");
15
+ return normalized || null;
16
+ };
17
+ exports.normalizeDisplayName = normalizeDisplayName;
18
+ exports.normalizeEmail = normalizeEmail;
19
+
20
+ //# sourceMappingURL=email.utils.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.utils.cjs","names":["AuthValidationError"],"sources":["../../src/utils/email.utils.ts"],"sourcesContent":["import { AuthValidationError } from \"../errors/auth.errors\";\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nexport const normalizeEmail = (email: string): string => {\n if (typeof email !== \"string\") {\n throw new AuthValidationError(\"A valid email address is required.\");\n }\n\n const normalized = email.trim().toLowerCase();\n\n if (\n normalized.length === 0 ||\n normalized.length > 254 ||\n !EMAIL_PATTERN.test(normalized)\n ) {\n throw new AuthValidationError(\"A valid email address is required.\");\n }\n\n return normalized;\n};\n\nexport const normalizeDisplayName = (\n name: string | null | undefined,\n): string | null => {\n if (name === undefined || name === null) {\n return null;\n }\n\n if (typeof name !== \"string\") {\n throw new AuthValidationError(\"Name must be a string.\");\n }\n\n const normalized = name.trim();\n\n if (normalized.length > 100) {\n throw new AuthValidationError(\"Name must be 100 characters or fewer.\");\n }\n\n return normalized || null;\n};\n"],"mappings":";;AAEA,MAAM,gBAAgB;AAEtB,MAAa,kBAAkB,UAA0B;CACvD,IAAI,OAAO,UAAU,UACnB,MAAM,IAAIA,oBAAAA,oBAAoB,oCAAoC;CAGpE,MAAM,aAAa,MAAM,KAAK,CAAC,CAAC,YAAY;CAE5C,IACE,WAAW,WAAW,KACtB,WAAW,SAAS,OACpB,CAAC,cAAc,KAAK,UAAU,GAE9B,MAAM,IAAIA,oBAAAA,oBAAoB,oCAAoC;CAGpE,OAAO;AACT;AAEA,MAAa,wBACX,SACkB;CAClB,IAAI,SAAS,KAAA,KAAa,SAAS,MACjC,OAAO;CAGT,IAAI,OAAO,SAAS,UAClB,MAAM,IAAIA,oBAAAA,oBAAoB,wBAAwB;CAGxD,MAAM,aAAa,KAAK,KAAK;CAE7B,IAAI,WAAW,SAAS,KACtB,MAAM,IAAIA,oBAAAA,oBAAoB,uCAAuC;CAGvE,OAAO,cAAc;AACvB"}
@@ -0,0 +1,2 @@
1
+ export declare const normalizeEmail: (email: string) => string;
2
+ export declare const normalizeDisplayName: (name: string | null | undefined) => string | null;
@@ -0,0 +1,2 @@
1
+ export declare const normalizeEmail: (email: string) => string;
2
+ export declare const normalizeDisplayName: (name: string | null | undefined) => string | null;
@@ -0,0 +1,19 @@
1
+ import { AuthValidationError } from "../errors/auth.errors.js";
2
+ // src/utils/email.utils.ts
3
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4
+ const normalizeEmail = (email) => {
5
+ if (typeof email !== "string") throw new AuthValidationError("A valid email address is required.");
6
+ const normalized = email.trim().toLowerCase();
7
+ if (normalized.length === 0 || normalized.length > 254 || !EMAIL_PATTERN.test(normalized)) throw new AuthValidationError("A valid email address is required.");
8
+ return normalized;
9
+ };
10
+ const normalizeDisplayName = (name) => {
11
+ if (name === void 0 || name === null) return null;
12
+ if (typeof name !== "string") throw new AuthValidationError("Name must be a string.");
13
+ const normalized = name.trim();
14
+ if (normalized.length > 100) throw new AuthValidationError("Name must be 100 characters or fewer.");
15
+ return normalized || null;
16
+ };
17
+ export { normalizeDisplayName, normalizeEmail };
18
+
19
+ //# sourceMappingURL=email.utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.utils.js","names":[],"sources":["../../src/utils/email.utils.ts"],"sourcesContent":["import { AuthValidationError } from \"../errors/auth.errors\";\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\nexport const normalizeEmail = (email: string): string => {\n if (typeof email !== \"string\") {\n throw new AuthValidationError(\"A valid email address is required.\");\n }\n\n const normalized = email.trim().toLowerCase();\n\n if (\n normalized.length === 0 ||\n normalized.length > 254 ||\n !EMAIL_PATTERN.test(normalized)\n ) {\n throw new AuthValidationError(\"A valid email address is required.\");\n }\n\n return normalized;\n};\n\nexport const normalizeDisplayName = (\n name: string | null | undefined,\n): string | null => {\n if (name === undefined || name === null) {\n return null;\n }\n\n if (typeof name !== \"string\") {\n throw new AuthValidationError(\"Name must be a string.\");\n }\n\n const normalized = name.trim();\n\n if (normalized.length > 100) {\n throw new AuthValidationError(\"Name must be 100 characters or fewer.\");\n }\n\n return normalized || null;\n};\n"],"mappings":";;AAEA,MAAM,gBAAgB;AAEtB,MAAa,kBAAkB,UAA0B;CACvD,IAAI,OAAO,UAAU,UACnB,MAAM,IAAI,oBAAoB,oCAAoC;CAGpE,MAAM,aAAa,MAAM,KAAK,CAAC,CAAC,YAAY;CAE5C,IACE,WAAW,WAAW,KACtB,WAAW,SAAS,OACpB,CAAC,cAAc,KAAK,UAAU,GAE9B,MAAM,IAAI,oBAAoB,oCAAoC;CAGpE,OAAO;AACT;AAEA,MAAa,wBACX,SACkB;CAClB,IAAI,SAAS,KAAA,KAAa,SAAS,MACjC,OAAO;CAGT,IAAI,OAAO,SAAS,UAClB,MAAM,IAAI,oBAAoB,wBAAwB;CAGxD,MAAM,aAAa,KAAK,KAAK;CAE7B,IAAI,WAAW,SAAS,KACtB,MAAM,IAAI,oBAAoB,uCAAuC;CAGvE,OAAO,cAAc;AACvB"}
@@ -0,0 +1,145 @@
1
+ const require_auth_errors = require("../errors/auth.errors.cjs");
2
+ // src/utils/totp.utils.ts
3
+ const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
4
+ const getCrypto = () => {
5
+ const cryptoApi = globalThis.crypto;
6
+ if (!cryptoApi?.getRandomValues || !cryptoApi.subtle) throw new require_auth_errors.AuthConfigurationError("This runtime does not provide the required Web Crypto APIs.");
7
+ return cryptoApi;
8
+ };
9
+ const validateTotpOptions = (options) => {
10
+ if (!Number.isInteger(options.digits) || options.digits < 6 || options.digits > 8) throw new require_auth_errors.AuthValidationError("TOTP digits must be between 6 and 8.");
11
+ if (!Number.isInteger(options.periodSeconds) || options.periodSeconds < 15 || options.periodSeconds > 120) throw new require_auth_errors.AuthValidationError("The TOTP period must be between 15 and 120 seconds.");
12
+ };
13
+ const getTimeStep = (timestamp, periodSeconds) => {
14
+ if (!Number.isFinite(timestamp) || timestamp < 0) throw new require_auth_errors.AuthValidationError("A valid TOTP timestamp is required.");
15
+ const step = Math.floor(timestamp / 1e3 / periodSeconds);
16
+ if (!Number.isSafeInteger(step)) throw new require_auth_errors.AuthValidationError("The TOTP timestamp is out of range.");
17
+ return step;
18
+ };
19
+ /**
20
+ * Copies bytes into an ArrayBuffer.
21
+ *
22
+ * Web Crypto APIs accept BufferSource values backed by ArrayBuffer.
23
+ * This avoids ArrayBufferLike incompatibilities with newer TypeScript
24
+ * typed-array definitions.
25
+ */
26
+ const toArrayBuffer = (bytes) => {
27
+ const buffer = new ArrayBuffer(bytes.byteLength);
28
+ const target = new Uint8Array(buffer);
29
+ for (let index = 0; index < bytes.length; index++) target[index] = bytes[index];
30
+ return buffer;
31
+ };
32
+ const constantTimeEqual = (left, right) => {
33
+ if (left.length !== right.length) return false;
34
+ let difference = 0;
35
+ for (let index = 0; index < left.length; index++) difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
36
+ return difference === 0;
37
+ };
38
+ const encodeBase32 = (input) => {
39
+ let output = "";
40
+ let buffer = 0;
41
+ let bits = 0;
42
+ for (const byte of input) {
43
+ buffer = buffer << 8 | byte;
44
+ bits += 8;
45
+ while (bits >= 5) {
46
+ bits -= 5;
47
+ output += BASE32_ALPHABET.charAt(buffer >>> bits & 31);
48
+ }
49
+ }
50
+ if (bits > 0) output += BASE32_ALPHABET.charAt(buffer << 5 - bits & 31);
51
+ return output;
52
+ };
53
+ const decodeBase32 = (input) => {
54
+ if (typeof input !== "string" || input.length === 0) throw new require_auth_errors.AuthValidationError("A valid TOTP secret is required.");
55
+ const normalized = input.toUpperCase().replace(/[\s=-]/g, "");
56
+ if (normalized.length === 0) throw new require_auth_errors.AuthValidationError("The TOTP secret is invalid.");
57
+ const output = [];
58
+ let buffer = 0;
59
+ let bits = 0;
60
+ for (const character of normalized) {
61
+ const value = BASE32_ALPHABET.indexOf(character);
62
+ if (value < 0) throw new require_auth_errors.AuthValidationError("The TOTP secret is invalid.");
63
+ buffer = buffer << 5 | value;
64
+ bits += 5;
65
+ if (bits >= 8) {
66
+ bits -= 8;
67
+ output.push(buffer >>> bits & 255);
68
+ }
69
+ }
70
+ if (bits > 0 && (buffer & (1 << bits) - 1) !== 0) throw new require_auth_errors.AuthValidationError("The TOTP secret is invalid.");
71
+ if (output.length === 0) throw new require_auth_errors.AuthValidationError("The TOTP secret is invalid.");
72
+ return new Uint8Array(output);
73
+ };
74
+ const generateTotpSecret = () => {
75
+ const bytes = /* @__PURE__ */ new Uint8Array(20);
76
+ getCrypto().getRandomValues(bytes);
77
+ return encodeBase32(bytes);
78
+ };
79
+ const generateTotpCode = async (secret, timestamp, options) => {
80
+ validateTotpOptions(options);
81
+ const counter = BigInt(getTimeStep(timestamp, options.periodSeconds));
82
+ const counterBuffer = /* @__PURE__ */ new ArrayBuffer(8);
83
+ const counterView = new DataView(counterBuffer);
84
+ let remaining = counter;
85
+ for (let index = 7; index >= 0; index--) {
86
+ counterView.setUint8(index, Number(remaining & 255n));
87
+ remaining >>= 8n;
88
+ }
89
+ const cryptoApi = getCrypto();
90
+ const secretBytes = decodeBase32(secret);
91
+ const key = await cryptoApi.subtle.importKey("raw", toArrayBuffer(secretBytes), {
92
+ name: "HMAC",
93
+ hash: "SHA-1"
94
+ }, false, ["sign"]);
95
+ const signature = await cryptoApi.subtle.sign("HMAC", key, counterBuffer);
96
+ const hash = new Uint8Array(signature);
97
+ if (hash.length < 20) throw new require_auth_errors.AuthConfigurationError("The runtime returned an invalid HMAC signature.");
98
+ const offset = hash[hash.length - 1] & 15;
99
+ const binary = (hash[offset] & 127) << 24 | hash[offset + 1] << 16 | hash[offset + 2] << 8 | hash[offset + 3];
100
+ const modulus = 10 ** options.digits;
101
+ const value = (binary >>> 0) % modulus;
102
+ return String(value).padStart(options.digits, "0");
103
+ };
104
+ /**
105
+ * Returns the matching time step, or null.
106
+ *
107
+ * The caller must persist the accepted step and reject a step that
108
+ * has already been consumed.
109
+ */
110
+ const verifyTotpCode = async (secret, code, timestamp, options) => {
111
+ validateTotpOptions(options);
112
+ if (!Number.isInteger(options.window) || options.window < 0 || options.window > 2) throw new require_auth_errors.AuthValidationError("The TOTP validation window must be between 0 and 2.");
113
+ if (typeof code !== "string" || !new RegExp(`^\\d{${options.digits}}$`).test(code)) return null;
114
+ const currentStep = getTimeStep(timestamp, options.periodSeconds);
115
+ let matchedStep = null;
116
+ for (let step = currentStep - options.window; step <= currentStep + options.window; step++) {
117
+ if (step < 0) continue;
118
+ const expected = await generateTotpCode(secret, step * options.periodSeconds * 1e3, options);
119
+ if (constantTimeEqual(expected, code)) matchedStep = step;
120
+ }
121
+ return matchedStep;
122
+ };
123
+ const createTotpUri = (input) => {
124
+ validateTotpOptions({
125
+ digits: input.digits,
126
+ periodSeconds: input.periodSeconds
127
+ });
128
+ if (typeof input.issuer !== "string" || input.issuer.trim().length === 0 || typeof input.account !== "string" || input.account.trim().length === 0) throw new require_auth_errors.AuthValidationError("A TOTP issuer and account name are required.");
129
+ decodeBase32(input.secret);
130
+ return `otpauth://totp/${encodeURIComponent(`${input.issuer.trim()}:${input.account.trim()}`)}?${new URLSearchParams({
131
+ secret: input.secret,
132
+ issuer: input.issuer.trim(),
133
+ algorithm: "SHA1",
134
+ digits: String(input.digits),
135
+ period: String(input.periodSeconds)
136
+ }).toString()}`;
137
+ };
138
+ exports.createTotpUri = createTotpUri;
139
+ exports.decodeBase32 = decodeBase32;
140
+ exports.encodeBase32 = encodeBase32;
141
+ exports.generateTotpCode = generateTotpCode;
142
+ exports.generateTotpSecret = generateTotpSecret;
143
+ exports.verifyTotpCode = verifyTotpCode;
144
+
145
+ //# sourceMappingURL=totp.utils.cjs.map