@puzzle-ui/auth 0.0.0-stage → 0.5.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.
Files changed (95) hide show
  1. package/README.md +54 -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 +50 -4
@@ -0,0 +1,185 @@
1
+ import { AuthConfigurationError, AuthValidationError } from "../errors/auth.errors.js";
2
+ import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
3
+ // src/providers/simplewebauthn-provider.ts
4
+ const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
5
+ const isRecord = (value) => typeof value === "object" && value !== null;
6
+ const encodeBase64Url = (input) => {
7
+ let output = "";
8
+ let index = 0;
9
+ while (index + 2 < input.length) {
10
+ const first = input[index++];
11
+ const second = input[index++];
12
+ const third = input[index++];
13
+ output += BASE64URL_ALPHABET.charAt(first >> 2);
14
+ output += BASE64URL_ALPHABET.charAt((first & 3) << 4 | second >> 4);
15
+ output += BASE64URL_ALPHABET.charAt((second & 15) << 2 | third >> 6);
16
+ output += BASE64URL_ALPHABET.charAt(third & 63);
17
+ }
18
+ if (index < input.length) {
19
+ const first = input[index++];
20
+ output += BASE64URL_ALPHABET.charAt(first >> 2);
21
+ if (index < input.length) {
22
+ const second = input[index];
23
+ output += BASE64URL_ALPHABET.charAt((first & 3) << 4 | second >> 4);
24
+ output += BASE64URL_ALPHABET.charAt((second & 15) << 2);
25
+ } else output += BASE64URL_ALPHABET.charAt((first & 3) << 4);
26
+ }
27
+ return output;
28
+ };
29
+ const decodeBase64Url = (value, errorMessage = "The stored passkey public key is invalid.") => {
30
+ if (typeof value !== "string" || value.length === 0 || value.length % 4 === 1 || !/^[A-Za-z0-9_-]+$/.test(value)) throw new AuthValidationError(errorMessage);
31
+ const output = [];
32
+ let buffer = 0;
33
+ let bits = 0;
34
+ for (const character of value) {
35
+ const decoded = BASE64URL_ALPHABET.indexOf(character);
36
+ if (decoded < 0) throw new AuthValidationError(errorMessage);
37
+ buffer = buffer << 6 | decoded;
38
+ bits += 6;
39
+ if (bits >= 8) {
40
+ bits -= 8;
41
+ output.push(buffer >>> bits & 255);
42
+ }
43
+ }
44
+ return new Uint8Array(output);
45
+ };
46
+ const toArrayBuffer = (bytes) => {
47
+ const buffer = new ArrayBuffer(bytes.byteLength);
48
+ new Uint8Array(buffer).set(bytes);
49
+ return buffer;
50
+ };
51
+ const toRegistrationResponse = (value) => {
52
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.rawId !== "string" || value.type !== "public-key" || !isRecord(value.response)) throw new AuthValidationError("The passkey registration response is invalid.");
53
+ return value;
54
+ };
55
+ const toAuthenticationResponse = (value) => {
56
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.rawId !== "string" || value.type !== "public-key" || !isRecord(value.response)) throw new AuthValidationError("The passkey authentication response is invalid.");
57
+ return value;
58
+ };
59
+ /**
60
+ * Derives a stable, opaque WebAuthn user handle from the internal Puzzle user ID.
61
+ *
62
+ * WebAuthn user handles are bytes, not application IDs. Hashing also prevents
63
+ * the internal user ID from being directly exposed in authenticator data.
64
+ */
65
+ const deriveWebAuthnUserId = async (userId) => {
66
+ if (typeof userId !== "string" || userId.trim().length === 0) throw new AuthValidationError("A valid WebAuthn user ID is required.");
67
+ const cryptoApi = globalThis.crypto;
68
+ if (!cryptoApi?.subtle) throw new AuthConfigurationError("This runtime does not provide the required Web Crypto APIs.");
69
+ const input = new TextEncoder().encode(`puzzle-webauthn-user:v1:${userId}`);
70
+ const digest = await cryptoApi.subtle.digest("SHA-256", toArrayBuffer(input));
71
+ return new Uint8Array(digest);
72
+ };
73
+ const validateChallenge = (challenge) => {
74
+ if (typeof challenge !== "string" || challenge.length < 32 || challenge.length > 256 || !/^[A-Za-z0-9_-]+$/.test(challenge)) throw new AuthValidationError("A valid WebAuthn challenge is required.");
75
+ };
76
+ const validateRpId = (rpId) => {
77
+ if (typeof rpId !== "string" || rpId.trim().length === 0 || rpId.length > 253 || rpId.includes("/") || rpId.includes(":")) throw new AuthConfigurationError("The WebAuthn RP ID is invalid.");
78
+ };
79
+ var SimpleWebAuthnProvider = class {
80
+ async generateRegistrationOptions(input) {
81
+ validateChallenge(input.challenge);
82
+ validateRpId(input.rpId);
83
+ if (typeof input.rpName !== "string" || input.rpName.trim().length === 0 || typeof input.userName !== "string" || input.userName.trim().length === 0) throw new AuthValidationError("A WebAuthn RP name and user name are required.");
84
+ const excludeCredentials = input.excludeCredentialIds.map((id) => {
85
+ if (!id || !/^[A-Za-z0-9_-]+$/.test(id)) throw new AuthValidationError("A stored passkey credential ID is invalid.");
86
+ return { id };
87
+ });
88
+ const options = await generateRegistrationOptions({
89
+ rpID: input.rpId,
90
+ rpName: input.rpName.trim(),
91
+ userID: await deriveWebAuthnUserId(input.userId),
92
+ userName: input.userName.trim(),
93
+ userDisplayName: input.userName.trim(),
94
+ challenge: decodeBase64Url(input.challenge, "A valid WebAuthn challenge is required."),
95
+ attestationType: "none",
96
+ authenticatorSelection: {
97
+ residentKey: "required",
98
+ userVerification: "required"
99
+ },
100
+ excludeCredentials
101
+ });
102
+ if (options.challenge !== input.challenge) throw new AuthConfigurationError("SimpleWebAuthn returned an unexpected registration challenge.");
103
+ return options;
104
+ }
105
+ async verifyRegistrationResponse(input) {
106
+ validateChallenge(input.expectedChallenge);
107
+ validateRpId(input.expectedRpId);
108
+ if (typeof input.expectedOrigin !== "string" || input.expectedOrigin.length === 0) throw new AuthConfigurationError("A WebAuthn verification origin is required.");
109
+ const response = toRegistrationResponse(input.response);
110
+ const verification = await verifyRegistrationResponse({
111
+ response,
112
+ expectedChallenge: input.expectedChallenge,
113
+ expectedOrigin: input.expectedOrigin,
114
+ expectedRPID: input.expectedRpId,
115
+ requireUserVerification: true
116
+ });
117
+ if (!verification.verified) return {
118
+ verified: false,
119
+ userVerified: false
120
+ };
121
+ const registration = verification.registrationInfo;
122
+ const credential = registration.credential;
123
+ return {
124
+ verified: true,
125
+ userVerified: registration.userVerified,
126
+ credential: {
127
+ credentialId: credential.id,
128
+ publicKey: encodeBase64Url(credential.publicKey),
129
+ counter: credential.counter,
130
+ transports: credential.transports ?? []
131
+ }
132
+ };
133
+ }
134
+ async generateAuthenticationOptions(input) {
135
+ validateChallenge(input.challenge);
136
+ validateRpId(input.rpId);
137
+ const allowCredentials = input.allowCredentialIds?.map((id) => {
138
+ if (!id || !/^[A-Za-z0-9_-]+$/.test(id)) throw new AuthValidationError("A stored passkey credential ID is invalid.");
139
+ return { id };
140
+ });
141
+ const options = await generateAuthenticationOptions({
142
+ rpID: input.rpId,
143
+ challenge: decodeBase64Url(input.challenge, "A valid WebAuthn challenge is required."),
144
+ userVerification: "required",
145
+ ...allowCredentials ? { allowCredentials } : {}
146
+ });
147
+ if (options.challenge !== input.challenge) throw new AuthConfigurationError("SimpleWebAuthn returned an unexpected authentication challenge.");
148
+ return options;
149
+ }
150
+ async verifyAuthenticationResponse(input) {
151
+ validateChallenge(input.expectedChallenge);
152
+ validateRpId(input.expectedRpId);
153
+ if (typeof input.expectedOrigin !== "string" || input.expectedOrigin.length === 0) throw new AuthConfigurationError("A WebAuthn verification origin is required.");
154
+ const response = toAuthenticationResponse(input.response);
155
+ if (response.id !== input.credential.credentialId) return {
156
+ verified: false,
157
+ userVerified: false,
158
+ credentialId: response.id,
159
+ newCounter: input.credential.counter
160
+ };
161
+ if (!Number.isSafeInteger(input.credential.counter) || input.credential.counter < 0) throw new AuthValidationError("The stored passkey counter is invalid.");
162
+ const credential = {
163
+ id: input.credential.credentialId,
164
+ publicKey: decodeBase64Url(input.credential.publicKey),
165
+ counter: input.credential.counter
166
+ };
167
+ const verification = await verifyAuthenticationResponse({
168
+ response,
169
+ expectedChallenge: input.expectedChallenge,
170
+ expectedOrigin: input.expectedOrigin,
171
+ expectedRPID: input.expectedRpId,
172
+ credential,
173
+ requireUserVerification: true
174
+ });
175
+ return {
176
+ verified: verification.verified,
177
+ userVerified: verification.verified && verification.authenticationInfo.userVerified,
178
+ credentialId: verification.authenticationInfo.credentialID,
179
+ newCounter: verification.authenticationInfo.newCounter
180
+ };
181
+ }
182
+ };
183
+ export { SimpleWebAuthnProvider };
184
+
185
+ //# sourceMappingURL=simplewebauthn-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simplewebauthn-provider.js","names":[],"sources":["../../src/providers/simplewebauthn-provider.ts"],"sourcesContent":["import {\n generateAuthenticationOptions,\n generateRegistrationOptions,\n verifyAuthenticationResponse,\n verifyRegistrationResponse,\n type AuthenticationResponseJSON,\n type RegistrationResponseJSON,\n type WebAuthnCredential,\n} from \"@simplewebauthn/server\";\n\nimport {\n AuthConfigurationError,\n AuthValidationError,\n} from \"../errors/auth.errors\";\n\nimport type {\n WebAuthnAuthenticationVerification,\n WebAuthnOptionsJSON,\n WebAuthnProvider,\n WebAuthnRegistrationVerification,\n} from \"./mfa-provider.types\";\n\nconst BASE64URL_ALPHABET =\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\nconst encodeBase64Url = (input: Uint8Array): string => {\n let output = \"\";\n let index = 0;\n\n while (index + 2 < input.length) {\n const first = input[index++]!;\n const second = input[index++]!;\n const third = input[index++]!;\n\n output += BASE64URL_ALPHABET.charAt(first >> 2);\n output += BASE64URL_ALPHABET.charAt(((first & 3) << 4) | (second >> 4));\n output += BASE64URL_ALPHABET.charAt(((second & 15) << 2) | (third >> 6));\n output += BASE64URL_ALPHABET.charAt(third & 63);\n }\n\n if (index < input.length) {\n const first = input[index++]!;\n\n output += BASE64URL_ALPHABET.charAt(first >> 2);\n\n if (index < input.length) {\n const second = input[index]!;\n\n output += BASE64URL_ALPHABET.charAt(((first & 3) << 4) | (second >> 4));\n output += BASE64URL_ALPHABET.charAt((second & 15) << 2);\n } else {\n output += BASE64URL_ALPHABET.charAt((first & 3) << 4);\n }\n }\n\n return output;\n};\n\nconst decodeBase64Url = (\n value: string,\n errorMessage = \"The stored passkey public key is invalid.\",\n): Uint8Array<ArrayBuffer> => {\n if (\n typeof value !== \"string\" ||\n value.length === 0 ||\n value.length % 4 === 1 ||\n !/^[A-Za-z0-9_-]+$/.test(value)\n ) {\n throw new AuthValidationError(errorMessage);\n }\n\n const output: number[] = [];\n let buffer = 0;\n let bits = 0;\n\n for (const character of value) {\n const decoded = BASE64URL_ALPHABET.indexOf(character);\n\n if (decoded < 0) {\n throw new AuthValidationError(errorMessage);\n }\n\n buffer = (buffer << 6) | decoded;\n bits += 6;\n\n if (bits >= 8) {\n bits -= 8;\n output.push((buffer >>> bits) & 255);\n }\n }\n\n return new Uint8Array(output);\n};\n\nconst toArrayBuffer = (bytes: Uint8Array): ArrayBuffer => {\n const buffer = new ArrayBuffer(bytes.byteLength);\n new Uint8Array(buffer).set(bytes);\n\n return buffer;\n};\n\nconst toRegistrationResponse = (value: unknown): RegistrationResponseJSON => {\n if (\n !isRecord(value) ||\n typeof value.id !== \"string\" ||\n typeof value.rawId !== \"string\" ||\n value.type !== \"public-key\" ||\n !isRecord(value.response)\n ) {\n throw new AuthValidationError(\n \"The passkey registration response is invalid.\",\n );\n }\n\n return value as unknown as RegistrationResponseJSON;\n};\n\nconst toAuthenticationResponse = (\n value: unknown,\n): AuthenticationResponseJSON => {\n if (\n !isRecord(value) ||\n typeof value.id !== \"string\" ||\n typeof value.rawId !== \"string\" ||\n value.type !== \"public-key\" ||\n !isRecord(value.response)\n ) {\n throw new AuthValidationError(\n \"The passkey authentication response is invalid.\",\n );\n }\n\n return value as unknown as AuthenticationResponseJSON;\n};\n\n/**\n * Derives a stable, opaque WebAuthn user handle from the internal Puzzle user ID.\n *\n * WebAuthn user handles are bytes, not application IDs. Hashing also prevents\n * the internal user ID from being directly exposed in authenticator data.\n */\nconst deriveWebAuthnUserId = async (\n userId: string,\n): Promise<Uint8Array<ArrayBuffer>> => {\n if (typeof userId !== \"string\" || userId.trim().length === 0) {\n throw new AuthValidationError(\"A valid WebAuthn user ID is required.\");\n }\n\n const cryptoApi = globalThis.crypto;\n\n if (!cryptoApi?.subtle) {\n throw new AuthConfigurationError(\n \"This runtime does not provide the required Web Crypto APIs.\",\n );\n }\n\n const input = new TextEncoder().encode(`puzzle-webauthn-user:v1:${userId}`);\n\n const digest = await cryptoApi.subtle.digest(\"SHA-256\", toArrayBuffer(input));\n\n return new Uint8Array(digest);\n};\n\nconst validateChallenge = (challenge: string): void => {\n if (\n typeof challenge !== \"string\" ||\n challenge.length < 32 ||\n challenge.length > 256 ||\n !/^[A-Za-z0-9_-]+$/.test(challenge)\n ) {\n throw new AuthValidationError(\"A valid WebAuthn challenge is required.\");\n }\n};\n\nconst validateRpId = (rpId: string): void => {\n if (\n typeof rpId !== \"string\" ||\n rpId.trim().length === 0 ||\n rpId.length > 253 ||\n rpId.includes(\"/\") ||\n rpId.includes(\":\")\n ) {\n throw new AuthConfigurationError(\"The WebAuthn RP ID is invalid.\");\n }\n};\n\nexport class SimpleWebAuthnProvider implements WebAuthnProvider {\n async generateRegistrationOptions(input: {\n readonly challenge: string;\n readonly rpId: string;\n readonly rpName: string;\n readonly userId: string;\n readonly userName: string;\n readonly excludeCredentialIds: readonly string[];\n }): Promise<WebAuthnOptionsJSON> {\n validateChallenge(input.challenge);\n validateRpId(input.rpId);\n\n if (\n typeof input.rpName !== \"string\" ||\n input.rpName.trim().length === 0 ||\n typeof input.userName !== \"string\" ||\n input.userName.trim().length === 0\n ) {\n throw new AuthValidationError(\n \"A WebAuthn RP name and user name are required.\",\n );\n }\n\n const excludeCredentials = input.excludeCredentialIds.map((id) => {\n if (!id || !/^[A-Za-z0-9_-]+$/.test(id)) {\n throw new AuthValidationError(\n \"A stored passkey credential ID is invalid.\",\n );\n }\n\n return { id };\n });\n\n const options = await generateRegistrationOptions({\n rpID: input.rpId,\n rpName: input.rpName.trim(),\n userID: await deriveWebAuthnUserId(input.userId),\n userName: input.userName.trim(),\n userDisplayName: input.userName.trim(),\n challenge: decodeBase64Url(\n input.challenge,\n \"A valid WebAuthn challenge is required.\",\n ),\n attestationType: \"none\",\n authenticatorSelection: {\n residentKey: \"required\",\n userVerification: \"required\",\n },\n excludeCredentials,\n });\n\n if (options.challenge !== input.challenge) {\n throw new AuthConfigurationError(\n \"SimpleWebAuthn returned an unexpected registration challenge.\",\n );\n }\n\n return options as unknown as WebAuthnOptionsJSON;\n }\n\n async verifyRegistrationResponse(input: {\n readonly response: unknown;\n readonly expectedChallenge: string;\n readonly expectedOrigin: string;\n readonly expectedRpId: string;\n }): Promise<WebAuthnRegistrationVerification> {\n validateChallenge(input.expectedChallenge);\n validateRpId(input.expectedRpId);\n\n if (\n typeof input.expectedOrigin !== \"string\" ||\n input.expectedOrigin.length === 0\n ) {\n throw new AuthConfigurationError(\n \"A WebAuthn verification origin is required.\",\n );\n }\n\n const response = toRegistrationResponse(input.response);\n\n const verification = await verifyRegistrationResponse({\n response,\n expectedChallenge: input.expectedChallenge,\n expectedOrigin: input.expectedOrigin,\n expectedRPID: input.expectedRpId,\n requireUserVerification: true,\n });\n\n if (!verification.verified) {\n return {\n verified: false,\n userVerified: false,\n };\n }\n\n const registration = verification.registrationInfo;\n const credential = registration.credential;\n\n return {\n verified: true,\n userVerified: registration.userVerified,\n credential: {\n credentialId: credential.id,\n publicKey: encodeBase64Url(credential.publicKey),\n counter: credential.counter,\n transports: credential.transports ?? [],\n },\n };\n }\n\n async generateAuthenticationOptions(input: {\n readonly challenge: string;\n readonly rpId: string;\n readonly allowCredentialIds?: readonly string[];\n readonly userVerification: \"required\";\n }): Promise<WebAuthnOptionsJSON> {\n validateChallenge(input.challenge);\n validateRpId(input.rpId);\n\n const allowCredentials = input.allowCredentialIds?.map((id) => {\n if (!id || !/^[A-Za-z0-9_-]+$/.test(id)) {\n throw new AuthValidationError(\n \"A stored passkey credential ID is invalid.\",\n );\n }\n\n return { id };\n });\n\n const options = await generateAuthenticationOptions({\n rpID: input.rpId,\n challenge: decodeBase64Url(\n input.challenge,\n \"A valid WebAuthn challenge is required.\",\n ),\n userVerification: \"required\",\n ...(allowCredentials\n ? {\n allowCredentials,\n }\n : {}),\n });\n\n if (options.challenge !== input.challenge) {\n throw new AuthConfigurationError(\n \"SimpleWebAuthn returned an unexpected authentication challenge.\",\n );\n }\n\n return options as unknown as WebAuthnOptionsJSON;\n }\n\n async verifyAuthenticationResponse(input: {\n readonly response: unknown;\n readonly expectedChallenge: string;\n readonly expectedOrigin: string;\n readonly expectedRpId: string;\n readonly credential: {\n readonly credentialId: string;\n readonly publicKey: string;\n readonly counter: number;\n readonly transports?: readonly string[];\n };\n }): Promise<WebAuthnAuthenticationVerification> {\n validateChallenge(input.expectedChallenge);\n validateRpId(input.expectedRpId);\n\n if (\n typeof input.expectedOrigin !== \"string\" ||\n input.expectedOrigin.length === 0\n ) {\n throw new AuthConfigurationError(\n \"A WebAuthn verification origin is required.\",\n );\n }\n\n const response = toAuthenticationResponse(input.response);\n\n if (response.id !== input.credential.credentialId) {\n return {\n verified: false,\n userVerified: false,\n credentialId: response.id,\n newCounter: input.credential.counter,\n };\n }\n\n if (\n !Number.isSafeInteger(input.credential.counter) ||\n input.credential.counter < 0\n ) {\n throw new AuthValidationError(\"The stored passkey counter is invalid.\");\n }\n\n const credential: WebAuthnCredential = {\n id: input.credential.credentialId,\n publicKey: decodeBase64Url(input.credential.publicKey),\n counter: input.credential.counter,\n };\n\n const verification = await verifyAuthenticationResponse({\n response,\n expectedChallenge: input.expectedChallenge,\n expectedOrigin: input.expectedOrigin,\n expectedRPID: input.expectedRpId,\n credential,\n requireUserVerification: true,\n });\n\n return {\n verified: verification.verified,\n userVerified:\n verification.verified && verification.authenticationInfo.userVerified,\n credentialId: verification.authenticationInfo.credentialID,\n newCounter: verification.authenticationInfo.newCounter,\n };\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,118 @@
1
+ const require_auth_errors = require("../errors/auth.errors.cjs");
2
+ // src/providers/webcrypto-secret-protector.ts
3
+ const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
4
+ const ADDITIONAL_DATA_PREFIX = "puzzle-auth:totp:v1:";
5
+ const getCrypto = () => {
6
+ const cryptoApi = globalThis.crypto;
7
+ if (!cryptoApi?.getRandomValues || !cryptoApi.subtle) throw new require_auth_errors.AuthConfigurationError("This runtime does not provide the required Web Crypto APIs.");
8
+ return cryptoApi;
9
+ };
10
+ const encodeBase64Url = (bytes) => {
11
+ let result = "";
12
+ let index = 0;
13
+ while (index + 2 < bytes.length) {
14
+ const first = bytes[index++];
15
+ const second = bytes[index++];
16
+ const third = bytes[index++];
17
+ result += BASE64URL_ALPHABET[first >> 2];
18
+ result += BASE64URL_ALPHABET[(first & 3) << 4 | second >> 4];
19
+ result += BASE64URL_ALPHABET[(second & 15) << 2 | third >> 6];
20
+ result += BASE64URL_ALPHABET[third & 63];
21
+ }
22
+ if (index < bytes.length) {
23
+ const first = bytes[index++];
24
+ result += BASE64URL_ALPHABET[first >> 2];
25
+ if (index < bytes.length) {
26
+ const second = bytes[index];
27
+ result += BASE64URL_ALPHABET[(first & 3) << 4 | second >> 4];
28
+ result += BASE64URL_ALPHABET[(second & 15) << 2];
29
+ } else result += BASE64URL_ALPHABET[(first & 3) << 4];
30
+ }
31
+ return result;
32
+ };
33
+ const decodeBase64Url = (value) => {
34
+ if (typeof value !== "string" || value.length === 0 || !/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) throw new require_auth_errors.AuthConfigurationError("The encrypted authentication secret is malformed.");
35
+ const bytes = [];
36
+ let buffer = 0;
37
+ let bits = 0;
38
+ for (const character of value) {
39
+ const decoded = BASE64URL_ALPHABET.indexOf(character);
40
+ if (decoded < 0) throw new require_auth_errors.AuthConfigurationError("The encrypted authentication secret is malformed.");
41
+ buffer = buffer << 6 | decoded;
42
+ bits += 6;
43
+ if (bits >= 8) {
44
+ bits -= 8;
45
+ bytes.push(buffer >>> bits & 255);
46
+ }
47
+ }
48
+ return new Uint8Array(bytes);
49
+ };
50
+ const toArrayBuffer = (bytes) => bytes.slice().buffer;
51
+ var WebCryptoAuthSecretProtector = class {
52
+ currentKeyId;
53
+ keys;
54
+ importedKeys = /* @__PURE__ */ new Map();
55
+ constructor(options) {
56
+ if (typeof options.currentKeyId !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(options.currentKeyId)) throw new require_auth_errors.AuthConfigurationError("A valid authentication encryption key ID is required.");
57
+ const keys = {};
58
+ for (const [keyId, key] of Object.entries(options.keys)) {
59
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(keyId) || !(key instanceof Uint8Array) || key.byteLength !== 32) throw new require_auth_errors.AuthConfigurationError("Authentication encryption keys must be 32-byte Uint8Array values with valid IDs.");
60
+ keys[keyId] = key.slice();
61
+ }
62
+ if (!keys[options.currentKeyId]) throw new require_auth_errors.AuthConfigurationError("The current authentication encryption key is not configured.");
63
+ this.currentKeyId = options.currentKeyId;
64
+ this.keys = keys;
65
+ }
66
+ getKey(keyId) {
67
+ const existing = this.importedKeys.get(keyId);
68
+ if (existing) return existing;
69
+ const rawKey = this.keys[keyId];
70
+ if (!rawKey) throw new require_auth_errors.AuthConfigurationError(`Authentication encryption key "${keyId}" is unavailable.`);
71
+ const imported = getCrypto().subtle.importKey("raw", toArrayBuffer(rawKey), { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
72
+ this.importedKeys.set(keyId, imported);
73
+ return imported;
74
+ }
75
+ async encrypt(plaintext) {
76
+ if (typeof plaintext !== "string" || plaintext.length === 0) throw new require_auth_errors.AuthConfigurationError("A non-empty authentication secret is required.");
77
+ const cryptoApi = getCrypto();
78
+ const iv = /* @__PURE__ */ new Uint8Array(12);
79
+ cryptoApi.getRandomValues(iv);
80
+ try {
81
+ const key = await this.getKey(this.currentKeyId);
82
+ const encrypted = await cryptoApi.subtle.encrypt({
83
+ name: "AES-GCM",
84
+ iv: toArrayBuffer(iv),
85
+ additionalData: toArrayBuffer(new TextEncoder().encode(`${ADDITIONAL_DATA_PREFIX}${this.currentKeyId}`)),
86
+ tagLength: 128
87
+ }, key, toArrayBuffer(new TextEncoder().encode(plaintext)));
88
+ return {
89
+ ciphertext: encodeBase64Url(new Uint8Array(encrypted)),
90
+ iv: encodeBase64Url(iv),
91
+ keyId: this.currentKeyId
92
+ };
93
+ } catch (error) {
94
+ throw new require_auth_errors.AuthConfigurationError("Could not encrypt the authentication secret.", { cause: error });
95
+ }
96
+ }
97
+ async decrypt(secret) {
98
+ if (!secret || typeof secret.keyId !== "string" || typeof secret.ciphertext !== "string" || typeof secret.iv !== "string") throw new require_auth_errors.AuthConfigurationError("The encrypted authentication secret is incomplete.");
99
+ const iv = decodeBase64Url(secret.iv);
100
+ const ciphertext = decodeBase64Url(secret.ciphertext);
101
+ if (iv.byteLength !== 12 || ciphertext.byteLength < 16) throw new require_auth_errors.AuthConfigurationError("The encrypted authentication secret is malformed.");
102
+ try {
103
+ const key = await this.getKey(secret.keyId);
104
+ const plaintext = await getCrypto().subtle.decrypt({
105
+ name: "AES-GCM",
106
+ iv: toArrayBuffer(iv),
107
+ additionalData: toArrayBuffer(new TextEncoder().encode(`${ADDITIONAL_DATA_PREFIX}${secret.keyId}`)),
108
+ tagLength: 128
109
+ }, key, toArrayBuffer(ciphertext));
110
+ return new TextDecoder("utf-8", { fatal: true }).decode(plaintext);
111
+ } catch (error) {
112
+ throw new require_auth_errors.AuthConfigurationError("Could not decrypt the authentication secret.", { cause: error });
113
+ }
114
+ }
115
+ };
116
+ exports.WebCryptoAuthSecretProtector = WebCryptoAuthSecretProtector;
117
+
118
+ //# sourceMappingURL=webcrypto-secret-protector.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webcrypto-secret-protector.cjs","names":["AuthConfigurationError"],"sources":["../../src/providers/webcrypto-secret-protector.ts"],"sourcesContent":["import { AuthConfigurationError } from \"../errors/auth.errors\";\n\nimport type {\n AuthSecretProtector,\n ProtectedAuthSecret,\n} from \"./mfa-provider.types\";\n\nexport type WebCryptoAuthSecretProtectorOptions = {\n readonly currentKeyId: string;\n readonly keys: Readonly<Record<string, Uint8Array>>;\n};\n\nconst BASE64URL_ALPHABET =\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\nconst ADDITIONAL_DATA_PREFIX = \"puzzle-auth:totp:v1:\";\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\nconst encodeBase64Url = (bytes: Uint8Array): string => {\n let result = \"\";\n let index = 0;\n\n while (index + 2 < bytes.length) {\n const first = bytes[index++]!;\n const second = bytes[index++]!;\n const third = bytes[index++]!;\n\n result += BASE64URL_ALPHABET[first >> 2];\n result += BASE64URL_ALPHABET[((first & 3) << 4) | (second >> 4)];\n result += BASE64URL_ALPHABET[((second & 15) << 2) | (third >> 6)];\n result += BASE64URL_ALPHABET[third & 63];\n }\n\n if (index < bytes.length) {\n const first = bytes[index++]!;\n\n result += BASE64URL_ALPHABET[first >> 2];\n\n if (index < bytes.length) {\n const second = bytes[index]!;\n\n result += BASE64URL_ALPHABET[((first & 3) << 4) | (second >> 4)];\n result += BASE64URL_ALPHABET[(second & 15) << 2];\n } else {\n result += BASE64URL_ALPHABET[(first & 3) << 4];\n }\n }\n\n return result;\n};\n\nconst decodeBase64Url = (value: string): Uint8Array => {\n if (\n typeof value !== \"string\" ||\n value.length === 0 ||\n !/^[A-Za-z0-9_-]+$/.test(value) ||\n value.length % 4 === 1\n ) {\n throw new AuthConfigurationError(\n \"The encrypted authentication secret is malformed.\",\n );\n }\n\n const bytes: number[] = [];\n let buffer = 0;\n let bits = 0;\n\n for (const character of value) {\n const decoded = BASE64URL_ALPHABET.indexOf(character);\n\n if (decoded < 0) {\n throw new AuthConfigurationError(\n \"The encrypted authentication secret is malformed.\",\n );\n }\n\n buffer = (buffer << 6) | decoded;\n bits += 6;\n\n if (bits >= 8) {\n bits -= 8;\n bytes.push((buffer >>> bits) & 255);\n }\n }\n\n return new Uint8Array(bytes);\n};\n\nconst toArrayBuffer = (bytes: Uint8Array): ArrayBuffer =>\n bytes.slice().buffer as ArrayBuffer;\n\nexport class WebCryptoAuthSecretProtector implements AuthSecretProtector {\n private readonly currentKeyId: string;\n private readonly keys: Readonly<Record<string, Uint8Array>>;\n\n private readonly importedKeys = new Map<string, Promise<CryptoKey>>();\n\n constructor(options: WebCryptoAuthSecretProtectorOptions) {\n if (\n typeof options.currentKeyId !== \"string\" ||\n !/^[A-Za-z0-9_-]{1,64}$/.test(options.currentKeyId)\n ) {\n throw new AuthConfigurationError(\n \"A valid authentication encryption key ID is required.\",\n );\n }\n\n const keys: Record<string, Uint8Array> = {};\n\n for (const [keyId, key] of Object.entries(options.keys)) {\n if (\n !/^[A-Za-z0-9_-]{1,64}$/.test(keyId) ||\n !(key instanceof Uint8Array) ||\n key.byteLength !== 32\n ) {\n throw new AuthConfigurationError(\n \"Authentication encryption keys must be 32-byte Uint8Array values with valid IDs.\",\n );\n }\n\n keys[keyId] = key.slice();\n }\n\n if (!keys[options.currentKeyId]) {\n throw new AuthConfigurationError(\n \"The current authentication encryption key is not configured.\",\n );\n }\n\n this.currentKeyId = options.currentKeyId;\n this.keys = keys;\n }\n\n private getKey(keyId: string): Promise<CryptoKey> {\n const existing = this.importedKeys.get(keyId);\n\n if (existing) return existing;\n\n const rawKey = this.keys[keyId];\n\n if (!rawKey) {\n throw new AuthConfigurationError(\n `Authentication encryption key \"${keyId}\" is unavailable.`,\n );\n }\n\n const imported = getCrypto().subtle.importKey(\n \"raw\",\n toArrayBuffer(rawKey),\n { name: \"AES-GCM\" },\n false,\n [\"encrypt\", \"decrypt\"],\n );\n\n this.importedKeys.set(keyId, imported);\n\n return imported;\n }\n\n async encrypt(plaintext: string): Promise<ProtectedAuthSecret> {\n if (typeof plaintext !== \"string\" || plaintext.length === 0) {\n throw new AuthConfigurationError(\n \"A non-empty authentication secret is required.\",\n );\n }\n\n const cryptoApi = getCrypto();\n const iv = new Uint8Array(12);\n\n cryptoApi.getRandomValues(iv);\n\n try {\n const key = await this.getKey(this.currentKeyId);\n\n const encrypted = await cryptoApi.subtle.encrypt(\n {\n name: \"AES-GCM\",\n iv: toArrayBuffer(iv),\n additionalData: toArrayBuffer(\n new TextEncoder().encode(\n `${ADDITIONAL_DATA_PREFIX}${this.currentKeyId}`,\n ),\n ),\n tagLength: 128,\n },\n key,\n toArrayBuffer(new TextEncoder().encode(plaintext)),\n );\n\n return {\n ciphertext: encodeBase64Url(new Uint8Array(encrypted)),\n iv: encodeBase64Url(iv),\n keyId: this.currentKeyId,\n };\n } catch (error) {\n throw new AuthConfigurationError(\n \"Could not encrypt the authentication secret.\",\n { cause: error },\n );\n }\n }\n\n async decrypt(secret: ProtectedAuthSecret): Promise<string> {\n if (\n !secret ||\n typeof secret.keyId !== \"string\" ||\n typeof secret.ciphertext !== \"string\" ||\n typeof secret.iv !== \"string\"\n ) {\n throw new AuthConfigurationError(\n \"The encrypted authentication secret is incomplete.\",\n );\n }\n\n const iv = decodeBase64Url(secret.iv);\n const ciphertext = decodeBase64Url(secret.ciphertext);\n\n if (iv.byteLength !== 12 || ciphertext.byteLength < 16) {\n throw new AuthConfigurationError(\n \"The encrypted authentication secret is malformed.\",\n );\n }\n\n try {\n const key = await this.getKey(secret.keyId);\n\n const plaintext = await getCrypto().subtle.decrypt(\n {\n name: \"AES-GCM\",\n iv: toArrayBuffer(iv),\n additionalData: toArrayBuffer(\n new TextEncoder().encode(\n `${ADDITIONAL_DATA_PREFIX}${secret.keyId}`,\n ),\n ),\n tagLength: 128,\n },\n key,\n toArrayBuffer(ciphertext),\n );\n\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(plaintext);\n } catch (error) {\n throw new AuthConfigurationError(\n \"Could not decrypt the authentication secret.\",\n { cause: error },\n );\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import { AuthSecretProtector, ProtectedAuthSecret } from "./mfa-provider.types.cjs";
2
+ export type WebCryptoAuthSecretProtectorOptions = {
3
+ readonly currentKeyId: string;
4
+ readonly keys: Readonly<Record<string, Uint8Array>>;
5
+ };
6
+ export declare class WebCryptoAuthSecretProtector implements AuthSecretProtector {
7
+ private readonly currentKeyId;
8
+ private readonly keys;
9
+ private readonly importedKeys;
10
+ constructor(options: WebCryptoAuthSecretProtectorOptions);
11
+ private getKey;
12
+ encrypt(plaintext: string): Promise<ProtectedAuthSecret>;
13
+ decrypt(secret: ProtectedAuthSecret): Promise<string>;
14
+ }
@@ -0,0 +1,14 @@
1
+ import { AuthSecretProtector, ProtectedAuthSecret } from "./mfa-provider.types.js";
2
+ export type WebCryptoAuthSecretProtectorOptions = {
3
+ readonly currentKeyId: string;
4
+ readonly keys: Readonly<Record<string, Uint8Array>>;
5
+ };
6
+ export declare class WebCryptoAuthSecretProtector implements AuthSecretProtector {
7
+ private readonly currentKeyId;
8
+ private readonly keys;
9
+ private readonly importedKeys;
10
+ constructor(options: WebCryptoAuthSecretProtectorOptions);
11
+ private getKey;
12
+ encrypt(plaintext: string): Promise<ProtectedAuthSecret>;
13
+ decrypt(secret: ProtectedAuthSecret): Promise<string>;
14
+ }
@@ -0,0 +1,118 @@
1
+ import { AuthConfigurationError } from "../errors/auth.errors.js";
2
+ // src/providers/webcrypto-secret-protector.ts
3
+ const BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
4
+ const ADDITIONAL_DATA_PREFIX = "puzzle-auth:totp:v1:";
5
+ const getCrypto = () => {
6
+ const cryptoApi = globalThis.crypto;
7
+ if (!cryptoApi?.getRandomValues || !cryptoApi.subtle) throw new AuthConfigurationError("This runtime does not provide the required Web Crypto APIs.");
8
+ return cryptoApi;
9
+ };
10
+ const encodeBase64Url = (bytes) => {
11
+ let result = "";
12
+ let index = 0;
13
+ while (index + 2 < bytes.length) {
14
+ const first = bytes[index++];
15
+ const second = bytes[index++];
16
+ const third = bytes[index++];
17
+ result += BASE64URL_ALPHABET[first >> 2];
18
+ result += BASE64URL_ALPHABET[(first & 3) << 4 | second >> 4];
19
+ result += BASE64URL_ALPHABET[(second & 15) << 2 | third >> 6];
20
+ result += BASE64URL_ALPHABET[third & 63];
21
+ }
22
+ if (index < bytes.length) {
23
+ const first = bytes[index++];
24
+ result += BASE64URL_ALPHABET[first >> 2];
25
+ if (index < bytes.length) {
26
+ const second = bytes[index];
27
+ result += BASE64URL_ALPHABET[(first & 3) << 4 | second >> 4];
28
+ result += BASE64URL_ALPHABET[(second & 15) << 2];
29
+ } else result += BASE64URL_ALPHABET[(first & 3) << 4];
30
+ }
31
+ return result;
32
+ };
33
+ const decodeBase64Url = (value) => {
34
+ if (typeof value !== "string" || value.length === 0 || !/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) throw new AuthConfigurationError("The encrypted authentication secret is malformed.");
35
+ const bytes = [];
36
+ let buffer = 0;
37
+ let bits = 0;
38
+ for (const character of value) {
39
+ const decoded = BASE64URL_ALPHABET.indexOf(character);
40
+ if (decoded < 0) throw new AuthConfigurationError("The encrypted authentication secret is malformed.");
41
+ buffer = buffer << 6 | decoded;
42
+ bits += 6;
43
+ if (bits >= 8) {
44
+ bits -= 8;
45
+ bytes.push(buffer >>> bits & 255);
46
+ }
47
+ }
48
+ return new Uint8Array(bytes);
49
+ };
50
+ const toArrayBuffer = (bytes) => bytes.slice().buffer;
51
+ var WebCryptoAuthSecretProtector = class {
52
+ currentKeyId;
53
+ keys;
54
+ importedKeys = /* @__PURE__ */ new Map();
55
+ constructor(options) {
56
+ if (typeof options.currentKeyId !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(options.currentKeyId)) throw new AuthConfigurationError("A valid authentication encryption key ID is required.");
57
+ const keys = {};
58
+ for (const [keyId, key] of Object.entries(options.keys)) {
59
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(keyId) || !(key instanceof Uint8Array) || key.byteLength !== 32) throw new AuthConfigurationError("Authentication encryption keys must be 32-byte Uint8Array values with valid IDs.");
60
+ keys[keyId] = key.slice();
61
+ }
62
+ if (!keys[options.currentKeyId]) throw new AuthConfigurationError("The current authentication encryption key is not configured.");
63
+ this.currentKeyId = options.currentKeyId;
64
+ this.keys = keys;
65
+ }
66
+ getKey(keyId) {
67
+ const existing = this.importedKeys.get(keyId);
68
+ if (existing) return existing;
69
+ const rawKey = this.keys[keyId];
70
+ if (!rawKey) throw new AuthConfigurationError(`Authentication encryption key "${keyId}" is unavailable.`);
71
+ const imported = getCrypto().subtle.importKey("raw", toArrayBuffer(rawKey), { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
72
+ this.importedKeys.set(keyId, imported);
73
+ return imported;
74
+ }
75
+ async encrypt(plaintext) {
76
+ if (typeof plaintext !== "string" || plaintext.length === 0) throw new AuthConfigurationError("A non-empty authentication secret is required.");
77
+ const cryptoApi = getCrypto();
78
+ const iv = /* @__PURE__ */ new Uint8Array(12);
79
+ cryptoApi.getRandomValues(iv);
80
+ try {
81
+ const key = await this.getKey(this.currentKeyId);
82
+ const encrypted = await cryptoApi.subtle.encrypt({
83
+ name: "AES-GCM",
84
+ iv: toArrayBuffer(iv),
85
+ additionalData: toArrayBuffer(new TextEncoder().encode(`${ADDITIONAL_DATA_PREFIX}${this.currentKeyId}`)),
86
+ tagLength: 128
87
+ }, key, toArrayBuffer(new TextEncoder().encode(plaintext)));
88
+ return {
89
+ ciphertext: encodeBase64Url(new Uint8Array(encrypted)),
90
+ iv: encodeBase64Url(iv),
91
+ keyId: this.currentKeyId
92
+ };
93
+ } catch (error) {
94
+ throw new AuthConfigurationError("Could not encrypt the authentication secret.", { cause: error });
95
+ }
96
+ }
97
+ async decrypt(secret) {
98
+ if (!secret || typeof secret.keyId !== "string" || typeof secret.ciphertext !== "string" || typeof secret.iv !== "string") throw new AuthConfigurationError("The encrypted authentication secret is incomplete.");
99
+ const iv = decodeBase64Url(secret.iv);
100
+ const ciphertext = decodeBase64Url(secret.ciphertext);
101
+ if (iv.byteLength !== 12 || ciphertext.byteLength < 16) throw new AuthConfigurationError("The encrypted authentication secret is malformed.");
102
+ try {
103
+ const key = await this.getKey(secret.keyId);
104
+ const plaintext = await getCrypto().subtle.decrypt({
105
+ name: "AES-GCM",
106
+ iv: toArrayBuffer(iv),
107
+ additionalData: toArrayBuffer(new TextEncoder().encode(`${ADDITIONAL_DATA_PREFIX}${secret.keyId}`)),
108
+ tagLength: 128
109
+ }, key, toArrayBuffer(ciphertext));
110
+ return new TextDecoder("utf-8", { fatal: true }).decode(plaintext);
111
+ } catch (error) {
112
+ throw new AuthConfigurationError("Could not decrypt the authentication secret.", { cause: error });
113
+ }
114
+ }
115
+ };
116
+ export { WebCryptoAuthSecretProtector };
117
+
118
+ //# sourceMappingURL=webcrypto-secret-protector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webcrypto-secret-protector.js","names":[],"sources":["../../src/providers/webcrypto-secret-protector.ts"],"sourcesContent":["import { AuthConfigurationError } from \"../errors/auth.errors\";\n\nimport type {\n AuthSecretProtector,\n ProtectedAuthSecret,\n} from \"./mfa-provider.types\";\n\nexport type WebCryptoAuthSecretProtectorOptions = {\n readonly currentKeyId: string;\n readonly keys: Readonly<Record<string, Uint8Array>>;\n};\n\nconst BASE64URL_ALPHABET =\n \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n\nconst ADDITIONAL_DATA_PREFIX = \"puzzle-auth:totp:v1:\";\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\nconst encodeBase64Url = (bytes: Uint8Array): string => {\n let result = \"\";\n let index = 0;\n\n while (index + 2 < bytes.length) {\n const first = bytes[index++]!;\n const second = bytes[index++]!;\n const third = bytes[index++]!;\n\n result += BASE64URL_ALPHABET[first >> 2];\n result += BASE64URL_ALPHABET[((first & 3) << 4) | (second >> 4)];\n result += BASE64URL_ALPHABET[((second & 15) << 2) | (third >> 6)];\n result += BASE64URL_ALPHABET[third & 63];\n }\n\n if (index < bytes.length) {\n const first = bytes[index++]!;\n\n result += BASE64URL_ALPHABET[first >> 2];\n\n if (index < bytes.length) {\n const second = bytes[index]!;\n\n result += BASE64URL_ALPHABET[((first & 3) << 4) | (second >> 4)];\n result += BASE64URL_ALPHABET[(second & 15) << 2];\n } else {\n result += BASE64URL_ALPHABET[(first & 3) << 4];\n }\n }\n\n return result;\n};\n\nconst decodeBase64Url = (value: string): Uint8Array => {\n if (\n typeof value !== \"string\" ||\n value.length === 0 ||\n !/^[A-Za-z0-9_-]+$/.test(value) ||\n value.length % 4 === 1\n ) {\n throw new AuthConfigurationError(\n \"The encrypted authentication secret is malformed.\",\n );\n }\n\n const bytes: number[] = [];\n let buffer = 0;\n let bits = 0;\n\n for (const character of value) {\n const decoded = BASE64URL_ALPHABET.indexOf(character);\n\n if (decoded < 0) {\n throw new AuthConfigurationError(\n \"The encrypted authentication secret is malformed.\",\n );\n }\n\n buffer = (buffer << 6) | decoded;\n bits += 6;\n\n if (bits >= 8) {\n bits -= 8;\n bytes.push((buffer >>> bits) & 255);\n }\n }\n\n return new Uint8Array(bytes);\n};\n\nconst toArrayBuffer = (bytes: Uint8Array): ArrayBuffer =>\n bytes.slice().buffer as ArrayBuffer;\n\nexport class WebCryptoAuthSecretProtector implements AuthSecretProtector {\n private readonly currentKeyId: string;\n private readonly keys: Readonly<Record<string, Uint8Array>>;\n\n private readonly importedKeys = new Map<string, Promise<CryptoKey>>();\n\n constructor(options: WebCryptoAuthSecretProtectorOptions) {\n if (\n typeof options.currentKeyId !== \"string\" ||\n !/^[A-Za-z0-9_-]{1,64}$/.test(options.currentKeyId)\n ) {\n throw new AuthConfigurationError(\n \"A valid authentication encryption key ID is required.\",\n );\n }\n\n const keys: Record<string, Uint8Array> = {};\n\n for (const [keyId, key] of Object.entries(options.keys)) {\n if (\n !/^[A-Za-z0-9_-]{1,64}$/.test(keyId) ||\n !(key instanceof Uint8Array) ||\n key.byteLength !== 32\n ) {\n throw new AuthConfigurationError(\n \"Authentication encryption keys must be 32-byte Uint8Array values with valid IDs.\",\n );\n }\n\n keys[keyId] = key.slice();\n }\n\n if (!keys[options.currentKeyId]) {\n throw new AuthConfigurationError(\n \"The current authentication encryption key is not configured.\",\n );\n }\n\n this.currentKeyId = options.currentKeyId;\n this.keys = keys;\n }\n\n private getKey(keyId: string): Promise<CryptoKey> {\n const existing = this.importedKeys.get(keyId);\n\n if (existing) return existing;\n\n const rawKey = this.keys[keyId];\n\n if (!rawKey) {\n throw new AuthConfigurationError(\n `Authentication encryption key \"${keyId}\" is unavailable.`,\n );\n }\n\n const imported = getCrypto().subtle.importKey(\n \"raw\",\n toArrayBuffer(rawKey),\n { name: \"AES-GCM\" },\n false,\n [\"encrypt\", \"decrypt\"],\n );\n\n this.importedKeys.set(keyId, imported);\n\n return imported;\n }\n\n async encrypt(plaintext: string): Promise<ProtectedAuthSecret> {\n if (typeof plaintext !== \"string\" || plaintext.length === 0) {\n throw new AuthConfigurationError(\n \"A non-empty authentication secret is required.\",\n );\n }\n\n const cryptoApi = getCrypto();\n const iv = new Uint8Array(12);\n\n cryptoApi.getRandomValues(iv);\n\n try {\n const key = await this.getKey(this.currentKeyId);\n\n const encrypted = await cryptoApi.subtle.encrypt(\n {\n name: \"AES-GCM\",\n iv: toArrayBuffer(iv),\n additionalData: toArrayBuffer(\n new TextEncoder().encode(\n `${ADDITIONAL_DATA_PREFIX}${this.currentKeyId}`,\n ),\n ),\n tagLength: 128,\n },\n key,\n toArrayBuffer(new TextEncoder().encode(plaintext)),\n );\n\n return {\n ciphertext: encodeBase64Url(new Uint8Array(encrypted)),\n iv: encodeBase64Url(iv),\n keyId: this.currentKeyId,\n };\n } catch (error) {\n throw new AuthConfigurationError(\n \"Could not encrypt the authentication secret.\",\n { cause: error },\n );\n }\n }\n\n async decrypt(secret: ProtectedAuthSecret): Promise<string> {\n if (\n !secret ||\n typeof secret.keyId !== \"string\" ||\n typeof secret.ciphertext !== \"string\" ||\n typeof secret.iv !== \"string\"\n ) {\n throw new AuthConfigurationError(\n \"The encrypted authentication secret is incomplete.\",\n );\n }\n\n const iv = decodeBase64Url(secret.iv);\n const ciphertext = decodeBase64Url(secret.ciphertext);\n\n if (iv.byteLength !== 12 || ciphertext.byteLength < 16) {\n throw new AuthConfigurationError(\n \"The encrypted authentication secret is malformed.\",\n );\n }\n\n try {\n const key = await this.getKey(secret.keyId);\n\n const plaintext = await getCrypto().subtle.decrypt(\n {\n name: \"AES-GCM\",\n iv: toArrayBuffer(iv),\n additionalData: toArrayBuffer(\n new TextEncoder().encode(\n `${ADDITIONAL_DATA_PREFIX}${secret.keyId}`,\n ),\n ),\n tagLength: 128,\n },\n key,\n toArrayBuffer(ciphertext),\n );\n\n return new TextDecoder(\"utf-8\", { fatal: true }).decode(plaintext);\n } catch (error) {\n throw new AuthConfigurationError(\n \"Could not decrypt the authentication secret.\",\n { cause: error },\n );\n }\n }\n}\n"],"mappings":";;AAYA,MAAM,qBACJ;AAEF,MAAM,yBAAyB;AAE/B,MAAM,kBAA0B;CAC9B,MAAM,YAAY,WAAW;CAE7B,IAAI,CAAC,WAAW,mBAAmB,CAAC,UAAU,QAC5C,MAAM,IAAI,uBACR,6DACF;CAGF,OAAO;AACT;AAEA,MAAM,mBAAmB,UAA8B;CACrD,IAAI,SAAS;CACb,IAAI,QAAQ;CAEZ,OAAO,QAAQ,IAAI,MAAM,QAAQ;EAC/B,MAAM,QAAQ,MAAM;EACpB,MAAM,SAAS,MAAM;EACrB,MAAM,QAAQ,MAAM;EAEpB,UAAU,mBAAmB,SAAS;EACtC,UAAU,oBAAqB,QAAQ,MAAM,IAAM,UAAU;EAC7D,UAAU,oBAAqB,SAAS,OAAO,IAAM,SAAS;EAC9D,UAAU,mBAAmB,QAAQ;CACvC;CAEA,IAAI,QAAQ,MAAM,QAAQ;EACxB,MAAM,QAAQ,MAAM;EAEpB,UAAU,mBAAmB,SAAS;EAEtC,IAAI,QAAQ,MAAM,QAAQ;GACxB,MAAM,SAAS,MAAM;GAErB,UAAU,oBAAqB,QAAQ,MAAM,IAAM,UAAU;GAC7D,UAAU,oBAAoB,SAAS,OAAO;EAChD,OACE,UAAU,oBAAoB,QAAQ,MAAM;CAEhD;CAEA,OAAO;AACT;AAEA,MAAM,mBAAmB,UAA8B;CACrD,IACE,OAAO,UAAU,YACjB,MAAM,WAAW,KACjB,CAAC,mBAAmB,KAAK,KAAK,KAC9B,MAAM,SAAS,MAAM,GAErB,MAAM,IAAI,uBACR,mDACF;CAGF,MAAM,QAAkB,CAAC;CACzB,IAAI,SAAS;CACb,IAAI,OAAO;CAEX,KAAK,MAAM,aAAa,OAAO;EAC7B,MAAM,UAAU,mBAAmB,QAAQ,SAAS;EAEpD,IAAI,UAAU,GACZ,MAAM,IAAI,uBACR,mDACF;EAGF,SAAU,UAAU,IAAK;EACzB,QAAQ;EAER,IAAI,QAAQ,GAAG;GACb,QAAQ;GACR,MAAM,KAAM,WAAW,OAAQ,GAAG;EACpC;CACF;CAEA,OAAO,IAAI,WAAW,KAAK;AAC7B;AAEA,MAAM,iBAAiB,UACrB,MAAM,MAAM,CAAC,CAAC;AAEhB,IAAa,+BAAb,MAAyE;CACvE;CACA;CAEA,+BAAgC,IAAI,IAAgC;CAEpE,YAAY,SAA8C;EACxD,IACE,OAAO,QAAQ,iBAAiB,YAChC,CAAC,wBAAwB,KAAK,QAAQ,YAAY,GAElD,MAAM,IAAI,uBACR,uDACF;EAGF,MAAM,OAAmC,CAAC;EAE1C,KAAK,MAAM,CAAC,OAAO,QAAQ,OAAO,QAAQ,QAAQ,IAAI,GAAG;GACvD,IACE,CAAC,wBAAwB,KAAK,KAAK,KACnC,EAAE,eAAe,eACjB,IAAI,eAAe,IAEnB,MAAM,IAAI,uBACR,kFACF;GAGF,KAAK,SAAS,IAAI,MAAM;EAC1B;EAEA,IAAI,CAAC,KAAK,QAAQ,eAChB,MAAM,IAAI,uBACR,8DACF;EAGF,KAAK,eAAe,QAAQ;EAC5B,KAAK,OAAO;CACd;CAEA,OAAe,OAAmC;EAChD,MAAM,WAAW,KAAK,aAAa,IAAI,KAAK;EAE5C,IAAI,UAAU,OAAO;EAErB,MAAM,SAAS,KAAK,KAAK;EAEzB,IAAI,CAAC,QACH,MAAM,IAAI,uBACR,kCAAkC,MAAM,kBAC1C;EAGF,MAAM,WAAW,UAAU,CAAC,CAAC,OAAO,UAClC,OACA,cAAc,MAAM,GACpB,EAAE,MAAM,UAAU,GAClB,OACA,CAAC,WAAW,SAAS,CACvB;EAEA,KAAK,aAAa,IAAI,OAAO,QAAQ;EAErC,OAAO;CACT;CAEA,MAAM,QAAQ,WAAiD;EAC7D,IAAI,OAAO,cAAc,YAAY,UAAU,WAAW,GACxD,MAAM,IAAI,uBACR,gDACF;EAGF,MAAM,YAAY,UAAU;EAC5B,MAAM,qBAAK,IAAI,WAAW,EAAE;EAE5B,UAAU,gBAAgB,EAAE;EAE5B,IAAI;GACF,MAAM,MAAM,MAAM,KAAK,OAAO,KAAK,YAAY;GAE/C,MAAM,YAAY,MAAM,UAAU,OAAO,QACvC;IACE,MAAM;IACN,IAAI,cAAc,EAAE;IACpB,gBAAgB,cACd,IAAI,YAAY,CAAC,CAAC,OAChB,GAAG,yBAAyB,KAAK,cACnC,CACF;IACA,WAAW;GACb,GACA,KACA,cAAc,IAAI,YAAY,CAAC,CAAC,OAAO,SAAS,CAAC,CACnD;GAEA,OAAO;IACL,YAAY,gBAAgB,IAAI,WAAW,SAAS,CAAC;IACrD,IAAI,gBAAgB,EAAE;IACtB,OAAO,KAAK;GACd;EACF,SAAS,OAAO;GACd,MAAM,IAAI,uBACR,gDACA,EAAE,OAAO,MAAM,CACjB;EACF;CACF;CAEA,MAAM,QAAQ,QAA8C;EAC1D,IACE,CAAC,UACD,OAAO,OAAO,UAAU,YACxB,OAAO,OAAO,eAAe,YAC7B,OAAO,OAAO,OAAO,UAErB,MAAM,IAAI,uBACR,oDACF;EAGF,MAAM,KAAK,gBAAgB,OAAO,EAAE;EACpC,MAAM,aAAa,gBAAgB,OAAO,UAAU;EAEpD,IAAI,GAAG,eAAe,MAAM,WAAW,aAAa,IAClD,MAAM,IAAI,uBACR,mDACF;EAGF,IAAI;GACF,MAAM,MAAM,MAAM,KAAK,OAAO,OAAO,KAAK;GAE1C,MAAM,YAAY,MAAM,UAAU,CAAC,CAAC,OAAO,QACzC;IACE,MAAM;IACN,IAAI,cAAc,EAAE;IACpB,gBAAgB,cACd,IAAI,YAAY,CAAC,CAAC,OAChB,GAAG,yBAAyB,OAAO,OACrC,CACF;IACA,WAAW;GACb,GACA,KACA,cAAc,UAAU,CAC1B;GAEA,OAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,CAAC,CAAC,OAAO,SAAS;EACnE,SAAS,OAAO;GACd,MAAM,IAAI,uBACR,gDACA,EAAE,OAAO,MAAM,CACjB;EACF;CACF;AACF"}
@@ -0,0 +1,53 @@
1
+ import { StorageRecord } from "@puzzle-ui/storage";
2
+ export type AuthActionTokenPurpose = "email-verification" | "password-reset" | "email-change";
3
+ export type AuthActionTokenStorageRecord = StorageRecord & {
4
+ readonly userId: string;
5
+ readonly purpose: AuthActionTokenPurpose;
6
+ readonly tokenHash: string;
7
+ readonly expiresAt: Date;
8
+ readonly consumedAt: Date | null;
9
+ readonly targetEmail: string | null;
10
+ };
11
+ export type AuthAuditAction = "account-registered" | "sign-in" | "sign-in-failed" | "sign-out" | "sign-out-everywhere" | "email-verification-requested" | "email-verified" | "password-reset-requested" | "password-reset-completed" | "password-changed" | "profile-updated" | "email-change-requested" | "email-changed" | "account-deactivated" | "account-deleted" | "mfa-challenge-created" | "mfa-challenge-failed" | "mfa-completed" | "mfa-factor-added" | "mfa-factor-removed" | "mfa-recovery-codes-regenerated" | "passkey-registered" | "passkey-authenticated";
12
+ export type AuthAuditEventStorageRecord = StorageRecord & {
13
+ readonly action: AuthAuditAction;
14
+ readonly userId: string | null;
15
+ readonly sessionId: string | null;
16
+ readonly metadata?: Readonly<Record<string, string | number | boolean | null>>;
17
+ };
18
+ export type AuthRateLimitStorageRecord = StorageRecord & {
19
+ readonly windowStartedAt: Date;
20
+ readonly resetAt: Date;
21
+ readonly windowSeconds: number;
22
+ readonly count: number;
23
+ };
24
+ export type AuthRateLimitRule = {
25
+ readonly limit: number;
26
+ readonly windowSeconds: number;
27
+ };
28
+ export type AuthLifecycleRateLimitAction = "emailVerification" | "passwordReset" | "emailChange";
29
+ export type AuthLifecycleRateLimitPolicy = {
30
+ readonly email?: AuthRateLimitRule;
31
+ readonly ip?: AuthRateLimitRule;
32
+ };
33
+ export type AuthLifecycleRateLimitPolicies = Partial<Record<AuthLifecycleRateLimitAction, AuthLifecycleRateLimitPolicy>>;
34
+ export type AuthLifecycleManagerOptions = {
35
+ readonly rateLimitKeySecret: string;
36
+ readonly usersCollection?: string;
37
+ readonly credentialsCollection?: string;
38
+ readonly actionTokensCollection?: string;
39
+ readonly auditCollection?: string;
40
+ readonly rateLimitsCollection?: string;
41
+ readonly minimumPasswordLength?: number;
42
+ readonly maximumPasswordLength?: number;
43
+ readonly emailVerificationTokenLifetimeSeconds?: number;
44
+ readonly passwordResetTokenLifetimeSeconds?: number;
45
+ readonly emailChangeTokenLifetimeSeconds?: number;
46
+ readonly emailTemplates?: Partial<Record<"emailVerification" | "passwordReset" | "emailChange", string>>;
47
+ readonly emailSubjects?: Partial<Record<"emailVerification" | "passwordReset" | "emailChange", string>>;
48
+ readonly rateLimits?: AuthLifecycleRateLimitPolicies;
49
+ readonly clock?: () => Date;
50
+ readonly requireVerifiedEmail?: boolean;
51
+ readonly onEmailDeliveryError?: (error: unknown) => void;
52
+ readonly onSessionRevocationError?: (error: unknown) => void;
53
+ };
@@ -0,0 +1,53 @@
1
+ import { StorageRecord } from "@puzzle-ui/storage";
2
+ export type AuthActionTokenPurpose = "email-verification" | "password-reset" | "email-change";
3
+ export type AuthActionTokenStorageRecord = StorageRecord & {
4
+ readonly userId: string;
5
+ readonly purpose: AuthActionTokenPurpose;
6
+ readonly tokenHash: string;
7
+ readonly expiresAt: Date;
8
+ readonly consumedAt: Date | null;
9
+ readonly targetEmail: string | null;
10
+ };
11
+ export type AuthAuditAction = "account-registered" | "sign-in" | "sign-in-failed" | "sign-out" | "sign-out-everywhere" | "email-verification-requested" | "email-verified" | "password-reset-requested" | "password-reset-completed" | "password-changed" | "profile-updated" | "email-change-requested" | "email-changed" | "account-deactivated" | "account-deleted" | "mfa-challenge-created" | "mfa-challenge-failed" | "mfa-completed" | "mfa-factor-added" | "mfa-factor-removed" | "mfa-recovery-codes-regenerated" | "passkey-registered" | "passkey-authenticated";
12
+ export type AuthAuditEventStorageRecord = StorageRecord & {
13
+ readonly action: AuthAuditAction;
14
+ readonly userId: string | null;
15
+ readonly sessionId: string | null;
16
+ readonly metadata?: Readonly<Record<string, string | number | boolean | null>>;
17
+ };
18
+ export type AuthRateLimitStorageRecord = StorageRecord & {
19
+ readonly windowStartedAt: Date;
20
+ readonly resetAt: Date;
21
+ readonly windowSeconds: number;
22
+ readonly count: number;
23
+ };
24
+ export type AuthRateLimitRule = {
25
+ readonly limit: number;
26
+ readonly windowSeconds: number;
27
+ };
28
+ export type AuthLifecycleRateLimitAction = "emailVerification" | "passwordReset" | "emailChange";
29
+ export type AuthLifecycleRateLimitPolicy = {
30
+ readonly email?: AuthRateLimitRule;
31
+ readonly ip?: AuthRateLimitRule;
32
+ };
33
+ export type AuthLifecycleRateLimitPolicies = Partial<Record<AuthLifecycleRateLimitAction, AuthLifecycleRateLimitPolicy>>;
34
+ export type AuthLifecycleManagerOptions = {
35
+ readonly rateLimitKeySecret: string;
36
+ readonly usersCollection?: string;
37
+ readonly credentialsCollection?: string;
38
+ readonly actionTokensCollection?: string;
39
+ readonly auditCollection?: string;
40
+ readonly rateLimitsCollection?: string;
41
+ readonly minimumPasswordLength?: number;
42
+ readonly maximumPasswordLength?: number;
43
+ readonly emailVerificationTokenLifetimeSeconds?: number;
44
+ readonly passwordResetTokenLifetimeSeconds?: number;
45
+ readonly emailChangeTokenLifetimeSeconds?: number;
46
+ readonly emailTemplates?: Partial<Record<"emailVerification" | "passwordReset" | "emailChange", string>>;
47
+ readonly emailSubjects?: Partial<Record<"emailVerification" | "passwordReset" | "emailChange", string>>;
48
+ readonly rateLimits?: AuthLifecycleRateLimitPolicies;
49
+ readonly clock?: () => Date;
50
+ readonly requireVerifiedEmail?: boolean;
51
+ readonly onEmailDeliveryError?: (error: unknown) => void;
52
+ readonly onSessionRevocationError?: (error: unknown) => void;
53
+ };