@warlock.js/auth 5.12.0 → 5.14.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 (169) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +88 -86
  3. package/esm/commands/auth-cleanup-command.mjs +1 -1
  4. package/esm/commands/auth-cleanup-command.mjs.map +1 -1
  5. package/esm/contracts/index.d.mts +2 -1
  6. package/esm/contracts/providers.d.mts +101 -0
  7. package/esm/contracts/providers.d.mts.map +1 -0
  8. package/esm/contracts/types.d.mts +94 -1
  9. package/esm/contracts/types.d.mts.map +1 -1
  10. package/esm/contracts/types.mjs.map +1 -1
  11. package/esm/errors/auth-provider-sdk-missing.error.d.mts +17 -0
  12. package/esm/errors/auth-provider-sdk-missing.error.d.mts.map +1 -0
  13. package/esm/errors/auth-provider-sdk-missing.error.mjs +22 -0
  14. package/esm/errors/auth-provider-sdk-missing.error.mjs.map +1 -0
  15. package/esm/errors/email-not-verified.error.d.mts +16 -0
  16. package/esm/errors/email-not-verified.error.d.mts.map +1 -0
  17. package/esm/errors/email-not-verified.error.mjs +20 -0
  18. package/esm/errors/email-not-verified.error.mjs.map +1 -0
  19. package/esm/errors/index.d.mts +7 -0
  20. package/esm/errors/index.mjs +9 -0
  21. package/esm/errors/invalid-one-time-token.error.d.mts +17 -0
  22. package/esm/errors/invalid-one-time-token.error.d.mts.map +1 -0
  23. package/esm/errors/invalid-one-time-token.error.mjs +21 -0
  24. package/esm/errors/invalid-one-time-token.error.mjs.map +1 -0
  25. package/esm/errors/invalid-passkey.error.d.mts +18 -0
  26. package/esm/errors/invalid-passkey.error.d.mts.map +1 -0
  27. package/esm/errors/invalid-passkey.error.mjs +23 -0
  28. package/esm/errors/invalid-passkey.error.mjs.map +1 -0
  29. package/esm/errors/invalid-provider-callback.error.d.mts +18 -0
  30. package/esm/errors/invalid-provider-callback.error.d.mts.map +1 -0
  31. package/esm/errors/invalid-provider-callback.error.mjs +23 -0
  32. package/esm/errors/invalid-provider-callback.error.mjs.map +1 -0
  33. package/esm/errors/notifications-unavailable.error.d.mts +15 -0
  34. package/esm/errors/notifications-unavailable.error.d.mts.map +1 -0
  35. package/esm/errors/notifications-unavailable.error.mjs +20 -0
  36. package/esm/errors/notifications-unavailable.error.mjs.map +1 -0
  37. package/esm/errors/provider-email-not-verified.error.d.mts +16 -0
  38. package/esm/errors/provider-email-not-verified.error.d.mts.map +1 -0
  39. package/esm/errors/provider-email-not-verified.error.mjs +20 -0
  40. package/esm/errors/provider-email-not-verified.error.mjs.map +1 -0
  41. package/esm/index.d.mts +27 -3
  42. package/esm/index.mjs +27 -1
  43. package/esm/middleware/index.d.mts +4 -1
  44. package/esm/middleware/index.mjs +3 -0
  45. package/esm/middleware/one-time-token-throttle.middleware.d.mts +36 -0
  46. package/esm/middleware/one-time-token-throttle.middleware.d.mts.map +1 -0
  47. package/esm/middleware/one-time-token-throttle.middleware.mjs +53 -0
  48. package/esm/middleware/one-time-token-throttle.middleware.mjs.map +1 -0
  49. package/esm/middleware/otp-throttle.middleware.d.mts +30 -0
  50. package/esm/middleware/otp-throttle.middleware.d.mts.map +1 -0
  51. package/esm/middleware/otp-throttle.middleware.mjs +45 -0
  52. package/esm/middleware/otp-throttle.middleware.mjs.map +1 -0
  53. package/esm/middleware/require-verified-email.middleware.d.mts +18 -0
  54. package/esm/middleware/require-verified-email.middleware.d.mts.map +1 -0
  55. package/esm/middleware/require-verified-email.middleware.mjs +24 -0
  56. package/esm/middleware/require-verified-email.middleware.mjs.map +1 -0
  57. package/esm/models/access-token/access-token.model.d.mts +8 -0
  58. package/esm/models/access-token/access-token.model.d.mts.map +1 -1
  59. package/esm/models/access-token/access-token.model.mjs +3 -0
  60. package/esm/models/access-token/access-token.model.mjs.map +1 -1
  61. package/esm/models/index.d.mts +3 -1
  62. package/esm/models/index.d.mts.map +1 -1
  63. package/esm/models/index.mjs +16 -1
  64. package/esm/models/index.mjs.map +1 -1
  65. package/esm/models/one-time-token/index.d.mts +1 -0
  66. package/esm/models/one-time-token/index.mjs +3 -0
  67. package/esm/models/one-time-token/migration.mjs +31 -0
  68. package/esm/models/one-time-token/migration.mjs.map +1 -0
  69. package/esm/models/one-time-token/one-time-token.model.d.mts +173 -0
  70. package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -0
  71. package/esm/models/one-time-token/one-time-token.model.mjs +151 -0
  72. package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -0
  73. package/esm/models/passkey-credential/index.d.mts +1 -0
  74. package/esm/models/passkey-credential/index.mjs +3 -0
  75. package/esm/models/passkey-credential/migration.mjs +25 -0
  76. package/esm/models/passkey-credential/migration.mjs.map +1 -0
  77. package/esm/models/passkey-credential/passkey-credential.model.d.mts +108 -0
  78. package/esm/models/passkey-credential/passkey-credential.model.d.mts.map +1 -0
  79. package/esm/models/passkey-credential/passkey-credential.model.mjs +70 -0
  80. package/esm/models/passkey-credential/passkey-credential.model.mjs.map +1 -0
  81. package/esm/models/provider-account/index.d.mts +1 -0
  82. package/esm/models/provider-account/index.mjs +3 -0
  83. package/esm/models/provider-account/migration.mjs +25 -0
  84. package/esm/models/provider-account/migration.mjs.map +1 -0
  85. package/esm/models/provider-account/provider-account.model.d.mts +75 -0
  86. package/esm/models/provider-account/provider-account.model.d.mts.map +1 -0
  87. package/esm/models/provider-account/provider-account.model.mjs +55 -0
  88. package/esm/models/provider-account/provider-account.model.mjs.map +1 -0
  89. package/esm/models/refresh-token/refresh-token.model.d.mts +7 -0
  90. package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
  91. package/esm/models/refresh-token/refresh-token.model.mjs +3 -0
  92. package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
  93. package/esm/otp/index.d.mts +1 -0
  94. package/esm/otp/index.mjs +3 -0
  95. package/esm/otp/otp.d.mts +38 -0
  96. package/esm/otp/otp.d.mts.map +1 -0
  97. package/esm/otp/otp.mjs +105 -0
  98. package/esm/otp/otp.mjs.map +1 -0
  99. package/esm/passkeys/index.d.mts +3 -0
  100. package/esm/passkeys/index.mjs +4 -0
  101. package/esm/passkeys/passkey-authentication.d.mts +33 -0
  102. package/esm/passkeys/passkey-authentication.d.mts.map +1 -0
  103. package/esm/passkeys/passkey-authentication.mjs +93 -0
  104. package/esm/passkeys/passkey-authentication.mjs.map +1 -0
  105. package/esm/passkeys/passkey-challenges.mjs +53 -0
  106. package/esm/passkeys/passkey-challenges.mjs.map +1 -0
  107. package/esm/passkeys/passkey-registration.d.mts +29 -0
  108. package/esm/passkeys/passkey-registration.d.mts.map +1 -0
  109. package/esm/passkeys/passkey-registration.mjs +88 -0
  110. package/esm/passkeys/passkey-registration.mjs.map +1 -0
  111. package/esm/passkeys/simplewebauthn.d.mts +21 -0
  112. package/esm/passkeys/simplewebauthn.d.mts.map +1 -0
  113. package/esm/passkeys/simplewebauthn.mjs +13 -0
  114. package/esm/passkeys/simplewebauthn.mjs.map +1 -0
  115. package/esm/providers/google-provider.d.mts +40 -0
  116. package/esm/providers/google-provider.d.mts.map +1 -0
  117. package/esm/providers/google-provider.mjs +105 -0
  118. package/esm/providers/google-provider.mjs.map +1 -0
  119. package/esm/providers/index.d.mts +2 -0
  120. package/esm/providers/index.mjs +4 -0
  121. package/esm/providers/provider-login.d.mts +43 -0
  122. package/esm/providers/provider-login.d.mts.map +1 -0
  123. package/esm/providers/provider-login.mjs +86 -0
  124. package/esm/providers/provider-login.mjs.map +1 -0
  125. package/esm/providers/provider-state-cookie.mjs +63 -0
  126. package/esm/providers/provider-state-cookie.mjs.map +1 -0
  127. package/esm/providers/resolve-provider-user.mjs +45 -0
  128. package/esm/providers/resolve-provider-user.mjs.map +1 -0
  129. package/esm/services/auth-config.mjs +72 -0
  130. package/esm/services/auth-config.mjs.map +1 -1
  131. package/esm/services/auth.service.d.mts +18 -1
  132. package/esm/services/auth.service.d.mts.map +1 -1
  133. package/esm/services/auth.service.mjs +28 -1
  134. package/esm/services/auth.service.mjs.map +1 -1
  135. package/esm/services/email-verification.d.mts +35 -0
  136. package/esm/services/email-verification.d.mts.map +1 -0
  137. package/esm/services/email-verification.mjs +55 -0
  138. package/esm/services/email-verification.mjs.map +1 -0
  139. package/esm/services/index.d.mts +4 -1
  140. package/esm/services/index.mjs +3 -0
  141. package/esm/services/one-time-tokens.d.mts +32 -0
  142. package/esm/services/one-time-tokens.d.mts.map +1 -0
  143. package/esm/services/one-time-tokens.mjs +56 -0
  144. package/esm/services/one-time-tokens.mjs.map +1 -0
  145. package/esm/services/optional-peer.mjs +37 -0
  146. package/esm/services/optional-peer.mjs.map +1 -0
  147. package/esm/services/password-reset.d.mts +34 -0
  148. package/esm/services/password-reset.d.mts.map +1 -0
  149. package/esm/services/password-reset.mjs +81 -0
  150. package/esm/services/password-reset.mjs.map +1 -0
  151. package/esm/services/token-notifications.mjs +87 -0
  152. package/esm/services/token-notifications.mjs.map +1 -0
  153. package/esm/utils/auth-error-codes.d.mts +39 -1
  154. package/esm/utils/auth-error-codes.d.mts.map +1 -1
  155. package/esm/utils/auth-error-codes.mjs +33 -0
  156. package/esm/utils/auth-error-codes.mjs.map +1 -1
  157. package/esm/utils/index.mjs +1 -0
  158. package/esm/utils/safe-equal.d.mts +6 -0
  159. package/esm/utils/safe-equal.d.mts.map +1 -0
  160. package/esm/utils/safe-equal.mjs +13 -0
  161. package/esm/utils/safe-equal.mjs.map +1 -0
  162. package/llms-full.txt +313 -10
  163. package/llms.txt +2 -0
  164. package/package.json +21 -7
  165. package/skills/login-with-providers/SKILL.md +154 -0
  166. package/skills/manage-tokens/SKILL.md +7 -1
  167. package/skills/register-user/SKILL.md +6 -8
  168. package/skills/run-auth-commands/SKILL.md +1 -1
  169. package/skills/verify-email-and-reset-password/SKILL.md +137 -0
@@ -0,0 +1,105 @@
1
+ import { authConfig } from "../services/auth-config.mjs";
2
+ import { authService } from "../services/auth.service.mjs";
3
+ import { InvalidOneTimeTokenError } from "../errors/invalid-one-time-token.error.mjs";
4
+ import { oneTimeTokenModel } from "../services/one-time-tokens.mjs";
5
+ import { loadOptionalPeer } from "../services/optional-peer.mjs";
6
+ import { safeEqual } from "../utils/safe-equal.mjs";
7
+ import { createHmac, randomBytes, randomInt } from "node:crypto";
8
+
9
+ //#region ../auth/src/otp/otp.ts
10
+ const CODE_PATTERN = /^\d{6}$/;
11
+ /** One error for every rejection: unknown phone, no code, wrong, expired, used, over the cap. */
12
+ function invalidCode() {
13
+ return new InvalidOneTimeTokenError("Invalid or expired code.");
14
+ }
15
+ /**
16
+ * Keyed hash of a code. A plain SHA-256 of a 6-digit code is reversed by
17
+ * trying all million values, so the MAC key (derived from the access-token
18
+ * secret) is what keeps a leaked table useless. `salt` makes each stored
19
+ * value unique.
20
+ */
21
+ function macCode(user, salt, code) {
22
+ return createHmac("sha256", createHmac("sha256", authConfig.accessToken.secret()).update("@warlock.js/auth:otp").digest()).update(`${salt}:${user.userType}:${user.id}:${code}`).digest("base64url");
23
+ }
24
+ /** Stored form: `<salt>.<mac>` — 16 + 1 + 43 chars, inside the 64-char `token_hash` column. */
25
+ function storedCodeHash(user, code) {
26
+ const salt = randomBytes(12).toString("base64url");
27
+ return `${salt}.${macCode(user, salt, code)}`;
28
+ }
29
+ function codeMatches(user, stored, code) {
30
+ const [salt, mac] = stored.split(".");
31
+ return !!salt && !!mac && safeEqual(mac, macCode(user, salt, code));
32
+ }
33
+ /**
34
+ * Resolve how a code is sent: `auth.otp.send`, else the app's notifications
35
+ * channel. Resolved BEFORE the user lookup, so a missing notifications package
36
+ * fails identically for known and unknown phones.
37
+ */
38
+ async function resolveDelivery(channel) {
39
+ const custom = authConfig.otp.send();
40
+ if (custom) return custom;
41
+ const { notify } = await loadOptionalPeer("@warlock.js/notifications", "notifications");
42
+ return (phone, message, channelName) => notify.channel(channelName).send(phone, message);
43
+ }
44
+ /**
45
+ * Send a 6-digit login code to the account whose `auth.otp.phoneField`
46
+ * (default `phone`) equals `phone`, through the notifications channel named by
47
+ * `options.channel` / `auth.otp.channel` (default `"sms"`). Auth ships no
48
+ * SMS/WhatsApp driver — the app registers that channel.
49
+ *
50
+ * The code is stored only as a keyed hash in `one_time_tokens` (purpose
51
+ * `otp`), expires after `auth.otp.expiresIn` (default 5m), and replaces any
52
+ * earlier unused code. **Anti-enumeration:** an unknown phone resolves the same
53
+ * way and sends nothing. Throttle the route with `otpRequestThrottleMiddleware()`.
54
+ *
55
+ * @throws AuthProviderSdkMissingError when `@warlock.js/notifications` is not
56
+ * installed and no `auth.otp.send` is configured.
57
+ */
58
+ async function requestOtp(Model, phone, options = {}) {
59
+ const channel = options.channel ?? authConfig.otp.channel();
60
+ const deliver = await resolveDelivery(channel);
61
+ if (typeof phone !== "string" || phone.length === 0) return;
62
+ const user = await Model.first({ [authConfig.otp.phoneField()]: phone });
63
+ if (!user) return;
64
+ const code = String(randomInt(0, 1e6)).padStart(6, "0");
65
+ const expiresAt = new Date(Date.now() + authConfig.otp.expiresInMs());
66
+ const TokenModel = oneTimeTokenModel();
67
+ await TokenModel.invalidateActiveFor(user, "otp");
68
+ await TokenModel.issue(user, "otp", storedCodeHash(user, code), expiresAt);
69
+ await deliver(phone, {
70
+ body: authConfig.otp.message(code),
71
+ code,
72
+ expiresAt
73
+ }, channel);
74
+ }
75
+ /**
76
+ * Verify a phone login code and log the user in through
77
+ * `authService.completeLogin` — the same outcome as password login.
78
+ *
79
+ * Each check first counts an attempt atomically; once `auth.otp.maxAttempts`
80
+ * (default 5) have been counted the code is invalidated, so it is compared at
81
+ * most that many times even under concurrent guesses. A correct code is
82
+ * consumed single-use. Throttle the route with `otpVerifyThrottleMiddleware()`.
83
+ *
84
+ * @throws InvalidOneTimeTokenError (400, EC008) — the same for an unknown
85
+ * phone, a wrong, expired, used or exhausted code.
86
+ */
87
+ async function verifyOtp(Model, phone, code, deviceInfo) {
88
+ if (typeof phone !== "string" || phone.length === 0) throw invalidCode();
89
+ if (typeof code !== "string" || !CODE_PATTERN.test(code)) throw invalidCode();
90
+ const user = await Model.first({ [authConfig.otp.phoneField()]: phone });
91
+ if (!user) throw invalidCode();
92
+ const row = await oneTimeTokenModel().findActiveFor(user, "otp");
93
+ if (!row || row.isConsumed || row.isExpired) throw invalidCode();
94
+ if (!await row.recordAttempt(authConfig.otp.maxAttempts())) {
95
+ await row.consumeIfActive();
96
+ throw invalidCode();
97
+ }
98
+ if (!codeMatches(user, String(row.get("token_hash") ?? ""), code)) throw invalidCode();
99
+ if (!await row.consumeIfActive()) throw invalidCode();
100
+ return authService.completeLogin(user, deviceInfo);
101
+ }
102
+
103
+ //#endregion
104
+ export { requestOtp, verifyOtp };
105
+ //# sourceMappingURL=otp.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otp.mjs","names":[],"sources":["../../../../../../../auth/src/otp/otp.ts"],"sourcesContent":["import type { ChildModel } from \"@warlock.js/cascade\";\nimport { createHmac, randomBytes, randomInt } from \"node:crypto\";\nimport type { OtpMessage, OtpSender } from \"../contracts/providers\";\nimport type { DeviceInfo, LoginResult } from \"../contracts/types\";\nimport { InvalidOneTimeTokenError } from \"../errors/invalid-one-time-token.error\";\nimport type { Auth } from \"../models/auth.model\";\nimport { authConfig } from \"../services/auth-config\";\nimport { authService } from \"../services/auth.service\";\nimport { oneTimeTokenModel } from \"../services/one-time-tokens\";\nimport { loadOptionalPeer } from \"../services/optional-peer\";\nimport { safeEqual } from \"../utils/safe-equal\";\n\n/** The slice of `@warlock.js/notifications` OTP delivery uses (an optional peer). */\ntype NotifyModule = {\n notify: {\n channel: (name: string) => { send: (to: string, payload: unknown) => Promise<unknown> };\n };\n};\n\nconst CODE_PATTERN = /^\\d{6}$/;\n\n/** One error for every rejection: unknown phone, no code, wrong, expired, used, over the cap. */\nfunction invalidCode(): InvalidOneTimeTokenError {\n return new InvalidOneTimeTokenError(\"Invalid or expired code.\");\n}\n\n/**\n * Keyed hash of a code. A plain SHA-256 of a 6-digit code is reversed by\n * trying all million values, so the MAC key (derived from the access-token\n * secret) is what keeps a leaked table useless. `salt` makes each stored\n * value unique.\n */\nfunction macCode(user: Auth, salt: string, code: string): string {\n const key = createHmac(\"sha256\", authConfig.accessToken.secret())\n .update(\"@warlock.js/auth:otp\")\n .digest();\n\n return createHmac(\"sha256\", key)\n .update(`${salt}:${user.userType}:${user.id}:${code}`)\n .digest(\"base64url\");\n}\n\n/** Stored form: `<salt>.<mac>` — 16 + 1 + 43 chars, inside the 64-char `token_hash` column. */\nfunction storedCodeHash(user: Auth, code: string): string {\n const salt = randomBytes(12).toString(\"base64url\");\n\n return `${salt}.${macCode(user, salt, code)}`;\n}\n\nfunction codeMatches(user: Auth, stored: string, code: string): boolean {\n const [salt, mac] = stored.split(\".\");\n\n return !!salt && !!mac && safeEqual(mac, macCode(user, salt, code));\n}\n\n/**\n * Resolve how a code is sent: `auth.otp.send`, else the app's notifications\n * channel. Resolved BEFORE the user lookup, so a missing notifications package\n * fails identically for known and unknown phones.\n */\nasync function resolveDelivery(channel: string): Promise<OtpSender> {\n const custom = authConfig.otp.send();\n\n if (custom) return custom;\n\n const { notify } = await loadOptionalPeer<NotifyModule>(\n \"@warlock.js/notifications\",\n \"notifications\",\n );\n\n return (phone, message, channelName) => notify.channel(channelName).send(phone, message);\n}\n\n/**\n * Send a 6-digit login code to the account whose `auth.otp.phoneField`\n * (default `phone`) equals `phone`, through the notifications channel named by\n * `options.channel` / `auth.otp.channel` (default `\"sms\"`). Auth ships no\n * SMS/WhatsApp driver — the app registers that channel.\n *\n * The code is stored only as a keyed hash in `one_time_tokens` (purpose\n * `otp`), expires after `auth.otp.expiresIn` (default 5m), and replaces any\n * earlier unused code. **Anti-enumeration:** an unknown phone resolves the same\n * way and sends nothing. Throttle the route with `otpRequestThrottleMiddleware()`.\n *\n * @throws AuthProviderSdkMissingError when `@warlock.js/notifications` is not\n * installed and no `auth.otp.send` is configured.\n */\nexport async function requestOtp<T extends Auth>(\n Model: ChildModel<T>,\n phone: string,\n options: { channel?: string } = {},\n): Promise<void> {\n const channel = options.channel ?? authConfig.otp.channel();\n const deliver = await resolveDelivery(channel);\n\n if (typeof phone !== \"string\" || phone.length === 0) return;\n\n const user = (await Model.first({ [authConfig.otp.phoneField()]: phone })) as T | null;\n\n if (!user) return;\n\n const code = String(randomInt(0, 1_000_000)).padStart(6, \"0\");\n const expiresAt = new Date(Date.now() + authConfig.otp.expiresInMs());\n const TokenModel = oneTimeTokenModel();\n\n await TokenModel.invalidateActiveFor(user, \"otp\");\n await TokenModel.issue(user, \"otp\", storedCodeHash(user, code), expiresAt);\n\n const message: OtpMessage = { body: authConfig.otp.message(code), code, expiresAt };\n\n await deliver(phone, message, channel);\n}\n\n/**\n * Verify a phone login code and log the user in through\n * `authService.completeLogin` — the same outcome as password login.\n *\n * Each check first counts an attempt atomically; once `auth.otp.maxAttempts`\n * (default 5) have been counted the code is invalidated, so it is compared at\n * most that many times even under concurrent guesses. A correct code is\n * consumed single-use. Throttle the route with `otpVerifyThrottleMiddleware()`.\n *\n * @throws InvalidOneTimeTokenError (400, EC008) — the same for an unknown\n * phone, a wrong, expired, used or exhausted code.\n */\nexport async function verifyOtp<T extends Auth>(\n Model: ChildModel<T>,\n phone: string,\n code: string,\n deviceInfo?: DeviceInfo,\n): Promise<LoginResult<T>> {\n if (typeof phone !== \"string\" || phone.length === 0) throw invalidCode();\n if (typeof code !== \"string\" || !CODE_PATTERN.test(code)) throw invalidCode();\n\n const user = (await Model.first({ [authConfig.otp.phoneField()]: phone })) as T | null;\n\n if (!user) throw invalidCode();\n\n const row = await oneTimeTokenModel().findActiveFor(user, \"otp\");\n\n if (!row || row.isConsumed || row.isExpired) throw invalidCode();\n\n if (!(await row.recordAttempt(authConfig.otp.maxAttempts()))) {\n await row.consumeIfActive();\n\n throw invalidCode();\n }\n\n if (!codeMatches(user, String(row.get(\"token_hash\") ?? \"\"), code)) throw invalidCode();\n\n if (!(await row.consumeIfActive())) throw invalidCode();\n\n return authService.completeLogin(user, deviceInfo);\n}\n"],"mappings":";;;;;;;;;AAmBA,MAAM,eAAe;;AAGrB,SAAS,cAAwC;CAC/C,OAAO,IAAI,yBAAyB,0BAA0B;AAChE;;;;;;;AAQA,SAAS,QAAQ,MAAY,MAAc,MAAsB;CAK/D,OAAO,WAAW,UAJN,WAAW,UAAU,WAAW,YAAY,OAAO,CAAC,CAAC,CAC9D,OAAO,sBAAsB,CAAC,CAC9B,OAE2B,CAAC,CAAC,CAC7B,OAAO,GAAG,KAAK,GAAG,KAAK,SAAS,GAAG,KAAK,GAAG,GAAG,MAAM,CAAC,CACrD,OAAO,WAAW;AACvB;;AAGA,SAAS,eAAe,MAAY,MAAsB;CACxD,MAAM,OAAO,YAAY,EAAE,CAAC,CAAC,SAAS,WAAW;CAEjD,OAAO,GAAG,KAAK,GAAG,QAAQ,MAAM,MAAM,IAAI;AAC5C;AAEA,SAAS,YAAY,MAAY,QAAgB,MAAuB;CACtE,MAAM,CAAC,MAAM,OAAO,OAAO,MAAM,GAAG;CAEpC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO,UAAU,KAAK,QAAQ,MAAM,MAAM,IAAI,CAAC;AACpE;;;;;;AAOA,eAAe,gBAAgB,SAAqC;CAClE,MAAM,SAAS,WAAW,IAAI,KAAK;CAEnC,IAAI,QAAQ,OAAO;CAEnB,MAAM,EAAE,WAAW,MAAM,iBACvB,6BACA,eACF;CAEA,QAAQ,OAAO,SAAS,gBAAgB,OAAO,QAAQ,WAAW,CAAC,CAAC,KAAK,OAAO,OAAO;AACzF;;;;;;;;;;;;;;;AAgBA,eAAsB,WACpB,OACA,OACA,UAAgC,CAAC,GAClB;CACf,MAAM,UAAU,QAAQ,WAAW,WAAW,IAAI,QAAQ;CAC1D,MAAM,UAAU,MAAM,gBAAgB,OAAO;CAE7C,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG;CAErD,MAAM,OAAQ,MAAM,MAAM,MAAM,GAAG,WAAW,IAAI,WAAW,IAAI,MAAM,CAAC;CAExE,IAAI,CAAC,MAAM;CAEX,MAAM,OAAO,OAAO,UAAU,GAAG,GAAS,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;CAC5D,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,WAAW,IAAI,YAAY,CAAC;CACpE,MAAM,aAAa,kBAAkB;CAErC,MAAM,WAAW,oBAAoB,MAAM,KAAK;CAChD,MAAM,WAAW,MAAM,MAAM,OAAO,eAAe,MAAM,IAAI,GAAG,SAAS;CAIzE,MAAM,QAAQ,OAAO;EAFS,MAAM,WAAW,IAAI,QAAQ,IAAI;EAAG;EAAM;CAE7C,GAAG,OAAO;AACvC;;;;;;;;;;;;;AAcA,eAAsB,UACpB,OACA,OACA,MACA,YACyB;CACzB,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG,MAAM,YAAY;CACvE,IAAI,OAAO,SAAS,YAAY,CAAC,aAAa,KAAK,IAAI,GAAG,MAAM,YAAY;CAE5E,MAAM,OAAQ,MAAM,MAAM,MAAM,GAAG,WAAW,IAAI,WAAW,IAAI,MAAM,CAAC;CAExE,IAAI,CAAC,MAAM,MAAM,YAAY;CAE7B,MAAM,MAAM,MAAM,kBAAkB,CAAC,CAAC,cAAc,MAAM,KAAK;CAE/D,IAAI,CAAC,OAAO,IAAI,cAAc,IAAI,WAAW,MAAM,YAAY;CAE/D,IAAI,CAAE,MAAM,IAAI,cAAc,WAAW,IAAI,YAAY,CAAC,GAAI;EAC5D,MAAM,IAAI,gBAAgB;EAE1B,MAAM,YAAY;CACpB;CAEA,IAAI,CAAC,YAAY,MAAM,OAAO,IAAI,IAAI,YAAY,KAAK,EAAE,GAAG,IAAI,GAAG,MAAM,YAAY;CAErF,IAAI,CAAE,MAAM,IAAI,gBAAgB,GAAI,MAAM,YAAY;CAEtD,OAAO,YAAY,cAAc,MAAM,UAAU;AACnD"}
@@ -0,0 +1,3 @@
1
+ import { PasskeyOptionsJSON, PasskeyResponseJSON } from "./simplewebauthn.mjs";
2
+ import { generatePasskeyAuthenticationOptions, verifyPasskeyAuthentication } from "./passkey-authentication.mjs";
3
+ import { generatePasskeyRegistrationOptions, verifyPasskeyRegistration } from "./passkey-registration.mjs";
@@ -0,0 +1,4 @@
1
+ import { generatePasskeyAuthenticationOptions, verifyPasskeyAuthentication } from "./passkey-authentication.mjs";
2
+ import { generatePasskeyRegistrationOptions, verifyPasskeyRegistration } from "./passkey-registration.mjs";
3
+
4
+ export { };
@@ -0,0 +1,33 @@
1
+ import { Auth } from "../models/auth.model.mjs";
2
+ import { DeviceInfo, LoginResult } from "../contracts/types.mjs";
3
+ import { PasskeyOptionsJSON, PasskeyResponseJSON } from "./simplewebauthn.mjs";
4
+ import { Request } from "@warlock.js/core";
5
+
6
+ //#region ../auth/src/passkeys/passkey-authentication.d.ts
7
+ /**
8
+ * Authentication options for a passkey login. No user is identified yet
9
+ * (discoverable credentials), so the stored single-use challenge has no owner;
10
+ * the credential the browser returns decides the user.
11
+ *
12
+ * @example
13
+ * // POST /auth/passkeys/login/options
14
+ * response.success(await generatePasskeyAuthenticationOptions());
15
+ */
16
+ declare function generatePasskeyAuthenticationOptions(): Promise<PasskeyOptionsJSON>;
17
+ /**
18
+ * Verify a passkey assertion and log its owner in through
19
+ * `authService.completeLogin` — the same outcome as password login.
20
+ *
21
+ * Checks, in order: the request `Origin` (CSRF), the single-use challenge
22
+ * (consumed even if what follows fails), the credential is registered, the
23
+ * assertion via `@simplewebauthn/server`, then that the signature counter
24
+ * ADVANCED — a counter that did not move past the stored one (when either is
25
+ * non-zero) means a cloned authenticator and is rejected. The new counter is
26
+ * written with a compare-and-set.
27
+ *
28
+ * @throws InvalidPasskeyError (400, EC011) on any rejection.
29
+ */
30
+ declare function verifyPasskeyAuthentication(request: Request, credential: PasskeyResponseJSON, deviceInfo?: DeviceInfo): Promise<LoginResult<Auth>>;
31
+ //#endregion
32
+ export { generatePasskeyAuthenticationOptions, verifyPasskeyAuthentication };
33
+ //# sourceMappingURL=passkey-authentication.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey-authentication.d.mts","names":[],"sources":["../../../../../../../auth/src/passkeys/passkey-authentication.ts"],"mappings":";;;;;;;;AAiDA;;;;AAAwF;AA0BxF;;iBA1BsB,oCAAA,IAAwC,OAAO,CAAC,kBAAA;;;;;;;;;;;;;;iBA0BhD,2BAAA,CACpB,OAAA,EAAS,OAAA,EACT,UAAA,EAAY,mBAAA,EACZ,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,IAAA"}
@@ -0,0 +1,93 @@
1
+ import { authConfig } from "../services/auth-config.mjs";
2
+ import { authService } from "../services/auth.service.mjs";
3
+ import { InvalidPasskeyError } from "../errors/invalid-passkey.error.mjs";
4
+ import { PasskeyCredential } from "../models/passkey-credential/passkey-credential.model.mjs";
5
+ import "../models/passkey-credential/index.mjs";
6
+ import { assertPasskeyOrigin, storePasskeyChallenge, takePasskeyChallenge } from "./passkey-challenges.mjs";
7
+ import { loadSimpleWebAuthn } from "./simplewebauthn.mjs";
8
+ import { config } from "@warlock.js/core";
9
+
10
+ //#region ../auth/src/passkeys/passkey-authentication.ts
11
+ /** A counter that did not advance past the stored one (when either is non-zero): a cloned authenticator. */
12
+ function counterRegressed(stored, presented) {
13
+ if (presented === void 0) return false;
14
+ return (stored > 0 || presented > 0) && !(presented > stored);
15
+ }
16
+ /**
17
+ * The signature counter an assertion CLAIMS (bytes 33–36 of authenticatorData).
18
+ * Unverified — only used to label a rejection the SDK already made.
19
+ */
20
+ function presentedCounter(credential) {
21
+ const encoded = credential.response.authenticatorData;
22
+ if (typeof encoded !== "string") return void 0;
23
+ const authData = Buffer.from(encoded, "base64url");
24
+ return authData.length >= 37 ? authData.readUInt32BE(33) : void 0;
25
+ }
26
+ /**
27
+ * Authentication options for a passkey login. No user is identified yet
28
+ * (discoverable credentials), so the stored single-use challenge has no owner;
29
+ * the credential the browser returns decides the user.
30
+ *
31
+ * @example
32
+ * // POST /auth/passkeys/login/options
33
+ * response.success(await generatePasskeyAuthenticationOptions());
34
+ */
35
+ async function generatePasskeyAuthenticationOptions() {
36
+ const options = await (await loadSimpleWebAuthn()).generateAuthenticationOptions({
37
+ rpID: authConfig.passkeys.settings().rpID,
38
+ userVerification: "preferred"
39
+ });
40
+ await storePasskeyChallenge("passkey-authentication", options.challenge);
41
+ return options;
42
+ }
43
+ /**
44
+ * Verify a passkey assertion and log its owner in through
45
+ * `authService.completeLogin` — the same outcome as password login.
46
+ *
47
+ * Checks, in order: the request `Origin` (CSRF), the single-use challenge
48
+ * (consumed even if what follows fails), the credential is registered, the
49
+ * assertion via `@simplewebauthn/server`, then that the signature counter
50
+ * ADVANCED — a counter that did not move past the stored one (when either is
51
+ * non-zero) means a cloned authenticator and is rejected. The new counter is
52
+ * written with a compare-and-set.
53
+ *
54
+ * @throws InvalidPasskeyError (400, EC011) on any rejection.
55
+ */
56
+ async function verifyPasskeyAuthentication(request, credential, deviceInfo) {
57
+ const webauthn = await loadSimpleWebAuthn();
58
+ const origins = assertPasskeyOrigin(request);
59
+ const { challenge } = await takePasskeyChallenge("passkey-authentication", credential);
60
+ const stored = typeof credential.id === "string" ? await PasskeyCredential.findByCredentialId(credential.id) : null;
61
+ if (!stored) throw new InvalidPasskeyError("unknown-credential");
62
+ let verification;
63
+ try {
64
+ verification = await webauthn.verifyAuthenticationResponse({
65
+ response: credential,
66
+ expectedChallenge: challenge,
67
+ expectedOrigin: origins,
68
+ expectedRPID: authConfig.passkeys.settings().rpID,
69
+ credential: {
70
+ id: stored.credentialId,
71
+ publicKey: new Uint8Array(Buffer.from(String(stored.get("public_key")), "base64url")),
72
+ counter: stored.counter,
73
+ transports: stored.get("transports") ?? void 0
74
+ },
75
+ requireUserVerification: false
76
+ });
77
+ } catch (error) {
78
+ throw new InvalidPasskeyError(counterRegressed(stored.counter, presentedCounter(credential)) ? "counter-regression" : "authentication-verification-failed", error);
79
+ }
80
+ if (!verification.verified) throw new InvalidPasskeyError("authentication-not-verified");
81
+ const previous = stored.counter;
82
+ const next = Number(verification.authenticationInfo.newCounter);
83
+ if (counterRegressed(previous, next)) throw new InvalidPasskeyError("counter-regression");
84
+ if (!await stored.advanceCounter(previous, next)) throw new InvalidPasskeyError("counter-changed-concurrently");
85
+ const UserModel = config.key(`auth.userType.${stored.userType}`);
86
+ const user = UserModel ? await UserModel.find(stored.userId) : null;
87
+ if (!user) throw new InvalidPasskeyError("credential-owner-missing");
88
+ return authService.completeLogin(user, deviceInfo);
89
+ }
90
+
91
+ //#endregion
92
+ export { generatePasskeyAuthenticationOptions, verifyPasskeyAuthentication };
93
+ //# sourceMappingURL=passkey-authentication.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey-authentication.mjs","names":[],"sources":["../../../../../../../auth/src/passkeys/passkey-authentication.ts"],"sourcesContent":["import type { ChildModel } from \"@warlock.js/cascade\";\nimport { config, type Request } from \"@warlock.js/core\";\nimport type { DeviceInfo, LoginResult } from \"../contracts/types\";\nimport { InvalidPasskeyError } from \"../errors/invalid-passkey.error\";\nimport type { Auth } from \"../models/auth.model\";\nimport { PasskeyCredential } from \"../models/passkey-credential\";\nimport { authConfig } from \"../services/auth-config\";\nimport { authService } from \"../services/auth.service\";\nimport {\n assertPasskeyOrigin,\n storePasskeyChallenge,\n takePasskeyChallenge,\n} from \"./passkey-challenges\";\nimport {\n loadSimpleWebAuthn,\n type PasskeyOptionsJSON,\n type PasskeyResponseJSON,\n} from \"./simplewebauthn\";\n\n/** A counter that did not advance past the stored one (when either is non-zero): a cloned authenticator. */\nfunction counterRegressed(stored: number, presented: number | undefined): boolean {\n if (presented === undefined) return false;\n\n return (stored > 0 || presented > 0) && !(presented > stored);\n}\n\n/**\n * The signature counter an assertion CLAIMS (bytes 33–36 of authenticatorData).\n * Unverified — only used to label a rejection the SDK already made.\n */\nfunction presentedCounter(credential: PasskeyResponseJSON): number | undefined {\n const encoded = credential.response.authenticatorData;\n\n if (typeof encoded !== \"string\") return undefined;\n\n const authData = Buffer.from(encoded, \"base64url\");\n\n return authData.length >= 37 ? authData.readUInt32BE(33) : undefined;\n}\n\n/**\n * Authentication options for a passkey login. No user is identified yet\n * (discoverable credentials), so the stored single-use challenge has no owner;\n * the credential the browser returns decides the user.\n *\n * @example\n * // POST /auth/passkeys/login/options\n * response.success(await generatePasskeyAuthenticationOptions());\n */\nexport async function generatePasskeyAuthenticationOptions(): Promise<PasskeyOptionsJSON> {\n const webauthn = await loadSimpleWebAuthn();\n\n const options = await webauthn.generateAuthenticationOptions({\n rpID: authConfig.passkeys.settings().rpID,\n userVerification: \"preferred\",\n });\n\n await storePasskeyChallenge(\"passkey-authentication\", options.challenge);\n\n return options;\n}\n\n/**\n * Verify a passkey assertion and log its owner in through\n * `authService.completeLogin` — the same outcome as password login.\n *\n * Checks, in order: the request `Origin` (CSRF), the single-use challenge\n * (consumed even if what follows fails), the credential is registered, the\n * assertion via `@simplewebauthn/server`, then that the signature counter\n * ADVANCED — a counter that did not move past the stored one (when either is\n * non-zero) means a cloned authenticator and is rejected. The new counter is\n * written with a compare-and-set.\n *\n * @throws InvalidPasskeyError (400, EC011) on any rejection.\n */\nexport async function verifyPasskeyAuthentication(\n request: Request,\n credential: PasskeyResponseJSON,\n deviceInfo?: DeviceInfo,\n): Promise<LoginResult<Auth>> {\n const webauthn = await loadSimpleWebAuthn();\n const origins = assertPasskeyOrigin(request);\n const { challenge } = await takePasskeyChallenge(\"passkey-authentication\", credential);\n\n const stored =\n typeof credential.id === \"string\"\n ? await PasskeyCredential.findByCredentialId(credential.id)\n : null;\n\n if (!stored) {\n throw new InvalidPasskeyError(\"unknown-credential\");\n }\n\n let verification: Awaited<ReturnType<typeof webauthn.verifyAuthenticationResponse>>;\n\n try {\n verification = await webauthn.verifyAuthenticationResponse({\n response: credential,\n expectedChallenge: challenge,\n expectedOrigin: origins,\n expectedRPID: authConfig.passkeys.settings().rpID,\n credential: {\n id: stored.credentialId,\n publicKey: new Uint8Array(Buffer.from(String(stored.get(\"public_key\")), \"base64url\")),\n counter: stored.counter,\n transports: stored.get(\"transports\") ?? undefined,\n },\n requireUserVerification: false,\n });\n } catch (error) {\n // The SDK enforces the counter rule itself and throws a generic Error for\n // it; keep clone detection visible as its own reason in the logs.\n const reason = counterRegressed(stored.counter, presentedCounter(credential))\n ? \"counter-regression\"\n : \"authentication-verification-failed\";\n\n throw new InvalidPasskeyError(reason, error);\n }\n\n if (!verification.verified) {\n throw new InvalidPasskeyError(\"authentication-not-verified\");\n }\n\n const previous = stored.counter;\n const next = Number(verification.authenticationInfo.newCounter);\n\n if (counterRegressed(previous, next)) {\n throw new InvalidPasskeyError(\"counter-regression\");\n }\n\n if (!(await stored.advanceCounter(previous, next))) {\n throw new InvalidPasskeyError(\"counter-changed-concurrently\");\n }\n\n const UserModel = config.key<ChildModel<Auth> | undefined>(`auth.userType.${stored.userType}`);\n const user = UserModel ? ((await UserModel.find(stored.userId)) as Auth | null) : null;\n\n if (!user) {\n throw new InvalidPasskeyError(\"credential-owner-missing\");\n }\n\n return authService.completeLogin(user, deviceInfo);\n}\n"],"mappings":";;;;;;;;;;;AAoBA,SAAS,iBAAiB,QAAgB,WAAwC;CAChF,IAAI,cAAc,QAAW,OAAO;CAEpC,QAAQ,SAAS,KAAK,YAAY,MAAM,EAAE,YAAY;AACxD;;;;;AAMA,SAAS,iBAAiB,YAAqD;CAC7E,MAAM,UAAU,WAAW,SAAS;CAEpC,IAAI,OAAO,YAAY,UAAU,OAAO;CAExC,MAAM,WAAW,OAAO,KAAK,SAAS,WAAW;CAEjD,OAAO,SAAS,UAAU,KAAK,SAAS,aAAa,EAAE,IAAI;AAC7D;;;;;;;;;;AAWA,eAAsB,uCAAoE;CAGxF,MAAM,UAAU,OAAM,MAFC,mBAAmB,EAEZ,CAAC,8BAA8B;EAC3D,MAAM,WAAW,SAAS,SAAS,CAAC,CAAC;EACrC,kBAAkB;CACpB,CAAC;CAED,MAAM,sBAAsB,0BAA0B,QAAQ,SAAS;CAEvE,OAAO;AACT;;;;;;;;;;;;;;AAeA,eAAsB,4BACpB,SACA,YACA,YAC4B;CAC5B,MAAM,WAAW,MAAM,mBAAmB;CAC1C,MAAM,UAAU,oBAAoB,OAAO;CAC3C,MAAM,EAAE,cAAc,MAAM,qBAAqB,0BAA0B,UAAU;CAErF,MAAM,SACJ,OAAO,WAAW,OAAO,WACrB,MAAM,kBAAkB,mBAAmB,WAAW,EAAE,IACxD;CAEN,IAAI,CAAC,QACH,MAAM,IAAI,oBAAoB,oBAAoB;CAGpD,IAAI;CAEJ,IAAI;EACF,eAAe,MAAM,SAAS,6BAA6B;GACzD,UAAU;GACV,mBAAmB;GACnB,gBAAgB;GAChB,cAAc,WAAW,SAAS,SAAS,CAAC,CAAC;GAC7C,YAAY;IACV,IAAI,OAAO;IACX,WAAW,IAAI,WAAW,OAAO,KAAK,OAAO,OAAO,IAAI,YAAY,CAAC,GAAG,WAAW,CAAC;IACpF,SAAS,OAAO;IAChB,YAAY,OAAO,IAAI,YAAY,KAAK;GAC1C;GACA,yBAAyB;EAC3B,CAAC;CACH,SAAS,OAAO;EAOd,MAAM,IAAI,oBAJK,iBAAiB,OAAO,SAAS,iBAAiB,UAAU,CAAC,IACxE,uBACA,sCAEkC,KAAK;CAC7C;CAEA,IAAI,CAAC,aAAa,UAChB,MAAM,IAAI,oBAAoB,6BAA6B;CAG7D,MAAM,WAAW,OAAO;CACxB,MAAM,OAAO,OAAO,aAAa,mBAAmB,UAAU;CAE9D,IAAI,iBAAiB,UAAU,IAAI,GACjC,MAAM,IAAI,oBAAoB,oBAAoB;CAGpD,IAAI,CAAE,MAAM,OAAO,eAAe,UAAU,IAAI,GAC9C,MAAM,IAAI,oBAAoB,8BAA8B;CAG9D,MAAM,YAAY,OAAO,IAAkC,iBAAiB,OAAO,UAAU;CAC7F,MAAM,OAAO,YAAc,MAAM,UAAU,KAAK,OAAO,MAAM,IAAqB;CAElF,IAAI,CAAC,MACH,MAAM,IAAI,oBAAoB,0BAA0B;CAG1D,OAAO,YAAY,cAAc,MAAM,UAAU;AACnD"}
@@ -0,0 +1,53 @@
1
+ import { authConfig } from "../services/auth-config.mjs";
2
+ import { InvalidPasskeyError } from "../errors/invalid-passkey.error.mjs";
3
+ import { hashOneTimeToken, oneTimeTokenModel } from "../services/one-time-tokens.mjs";
4
+
5
+ //#region ../auth/src/passkeys/passkey-challenges.ts
6
+ /**
7
+ * Store a ceremony challenge server-side: SHA-256 hashed, purpose-bound,
8
+ * expiring (`auth.passkeys.challengeExpiresIn`, default 5m). A registration
9
+ * challenge is bound to its user; an authentication challenge has none.
10
+ */
11
+ async function storePasskeyChallenge(ceremony, challenge, user) {
12
+ const expiresAt = new Date(Date.now() + authConfig.passkeys.challengeExpiresInMs());
13
+ await oneTimeTokenModel().issueChallenge(ceremony, hashOneTimeToken(challenge), expiresAt, user);
14
+ }
15
+ /** The challenge the browser signed, read from `clientDataJSON`. */
16
+ function signedChallenge(credential) {
17
+ try {
18
+ const clientData = JSON.parse(Buffer.from(credential.response.clientDataJSON, "base64url").toString("utf8"));
19
+ if (typeof clientData.challenge === "string" && clientData.challenge.length > 0) return clientData.challenge;
20
+ } catch {}
21
+ throw new InvalidPasskeyError("malformed-client-data");
22
+ }
23
+ /**
24
+ * Consume the challenge a credential response signed. It is consumed BEFORE
25
+ * the signature is verified, so a challenge is single-use even when that
26
+ * verification fails. Unknown, other-ceremony, expired and already-used
27
+ * challenges all throw {@link InvalidPasskeyError}.
28
+ */
29
+ async function takePasskeyChallenge(ceremony, credential) {
30
+ const challenge = signedChallenge(credential);
31
+ const row = await oneTimeTokenModel().findByHash(hashOneTimeToken(challenge), ceremony);
32
+ if (!row || row.isConsumed || row.isExpired) throw new InvalidPasskeyError("unknown-or-expired-challenge");
33
+ if (!await row.consumeIfActive()) throw new InvalidPasskeyError("challenge-already-used");
34
+ return {
35
+ challenge,
36
+ row
37
+ };
38
+ }
39
+ /**
40
+ * CSRF rule for a passkey verify: the request's `Origin` header must be one
41
+ * of `auth.passkeys.origin`. The same list is what the signed
42
+ * `clientDataJSON.origin` is checked against.
43
+ */
44
+ function assertPasskeyOrigin(request) {
45
+ const origins = authConfig.passkeys.origins();
46
+ const origin = request.origin;
47
+ if (typeof origin !== "string" || !origins.includes(origin)) throw new InvalidPasskeyError("origin-mismatch");
48
+ return origins;
49
+ }
50
+
51
+ //#endregion
52
+ export { assertPasskeyOrigin, storePasskeyChallenge, takePasskeyChallenge };
53
+ //# sourceMappingURL=passkey-challenges.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey-challenges.mjs","names":[],"sources":["../../../../../../../auth/src/passkeys/passkey-challenges.ts"],"sourcesContent":["import type { Request } from \"@warlock.js/core\";\nimport { InvalidPasskeyError } from \"../errors/invalid-passkey.error\";\nimport type { Auth } from \"../models/auth.model\";\nimport type { OneTimeToken, OneTimeTokenPurpose } from \"../models/one-time-token\";\nimport { authConfig } from \"../services/auth-config\";\nimport { hashOneTimeToken, oneTimeTokenModel } from \"../services/one-time-tokens\";\nimport type { PasskeyResponseJSON } from \"./simplewebauthn\";\n\n/** Which ceremony a challenge belongs to. */\nexport type PasskeyCeremony = Extract<\n OneTimeTokenPurpose,\n \"passkey-registration\" | \"passkey-authentication\"\n>;\n\n/**\n * Store a ceremony challenge server-side: SHA-256 hashed, purpose-bound,\n * expiring (`auth.passkeys.challengeExpiresIn`, default 5m). A registration\n * challenge is bound to its user; an authentication challenge has none.\n */\nexport async function storePasskeyChallenge(\n ceremony: PasskeyCeremony,\n challenge: string,\n user?: Auth,\n): Promise<void> {\n const expiresAt = new Date(Date.now() + authConfig.passkeys.challengeExpiresInMs());\n\n await oneTimeTokenModel().issueChallenge(ceremony, hashOneTimeToken(challenge), expiresAt, user);\n}\n\n/** The challenge the browser signed, read from `clientDataJSON`. */\nfunction signedChallenge(credential: PasskeyResponseJSON): string {\n try {\n const clientData = JSON.parse(\n Buffer.from(credential.response.clientDataJSON, \"base64url\").toString(\"utf8\"),\n ) as { challenge?: unknown };\n\n if (typeof clientData.challenge === \"string\" && clientData.challenge.length > 0) {\n return clientData.challenge;\n }\n } catch {\n // fall through to the rejection\n }\n\n throw new InvalidPasskeyError(\"malformed-client-data\");\n}\n\n/**\n * Consume the challenge a credential response signed. It is consumed BEFORE\n * the signature is verified, so a challenge is single-use even when that\n * verification fails. Unknown, other-ceremony, expired and already-used\n * challenges all throw {@link InvalidPasskeyError}.\n */\nexport async function takePasskeyChallenge(\n ceremony: PasskeyCeremony,\n credential: PasskeyResponseJSON,\n): Promise<{ challenge: string; row: OneTimeToken }> {\n const challenge = signedChallenge(credential);\n const row = await oneTimeTokenModel().findByHash(hashOneTimeToken(challenge), ceremony);\n\n if (!row || row.isConsumed || row.isExpired) {\n throw new InvalidPasskeyError(\"unknown-or-expired-challenge\");\n }\n\n if (!(await row.consumeIfActive())) {\n throw new InvalidPasskeyError(\"challenge-already-used\");\n }\n\n return { challenge, row };\n}\n\n/**\n * CSRF rule for a passkey verify: the request's `Origin` header must be one\n * of `auth.passkeys.origin`. The same list is what the signed\n * `clientDataJSON.origin` is checked against.\n */\nexport function assertPasskeyOrigin(request: Request): string[] {\n const origins = authConfig.passkeys.origins();\n const origin = request.origin;\n\n if (typeof origin !== \"string\" || !origins.includes(origin)) {\n throw new InvalidPasskeyError(\"origin-mismatch\");\n }\n\n return origins;\n}\n"],"mappings":";;;;;;;;;;AAmBA,eAAsB,sBACpB,UACA,WACA,MACe;CACf,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,WAAW,SAAS,qBAAqB,CAAC;CAElF,MAAM,kBAAkB,CAAC,CAAC,eAAe,UAAU,iBAAiB,SAAS,GAAG,WAAW,IAAI;AACjG;;AAGA,SAAS,gBAAgB,YAAyC;CAChE,IAAI;EACF,MAAM,aAAa,KAAK,MACtB,OAAO,KAAK,WAAW,SAAS,gBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAC9E;EAEA,IAAI,OAAO,WAAW,cAAc,YAAY,WAAW,UAAU,SAAS,GAC5E,OAAO,WAAW;CAEtB,QAAQ,CAER;CAEA,MAAM,IAAI,oBAAoB,uBAAuB;AACvD;;;;;;;AAQA,eAAsB,qBACpB,UACA,YACmD;CACnD,MAAM,YAAY,gBAAgB,UAAU;CAC5C,MAAM,MAAM,MAAM,kBAAkB,CAAC,CAAC,WAAW,iBAAiB,SAAS,GAAG,QAAQ;CAEtF,IAAI,CAAC,OAAO,IAAI,cAAc,IAAI,WAChC,MAAM,IAAI,oBAAoB,8BAA8B;CAG9D,IAAI,CAAE,MAAM,IAAI,gBAAgB,GAC9B,MAAM,IAAI,oBAAoB,wBAAwB;CAGxD,OAAO;EAAE;EAAW;CAAI;AAC1B;;;;;;AAOA,SAAgB,oBAAoB,SAA4B;CAC9D,MAAM,UAAU,WAAW,SAAS,QAAQ;CAC5C,MAAM,SAAS,QAAQ;CAEvB,IAAI,OAAO,WAAW,YAAY,CAAC,QAAQ,SAAS,MAAM,GACxD,MAAM,IAAI,oBAAoB,iBAAiB;CAGjD,OAAO;AACT"}
@@ -0,0 +1,29 @@
1
+ import { Auth } from "../models/auth.model.mjs";
2
+ import { PasskeyCredential } from "../models/passkey-credential/passkey-credential.model.mjs";
3
+ import { PasskeyOptionsJSON, PasskeyResponseJSON } from "./simplewebauthn.mjs";
4
+ import { Request } from "@warlock.js/core";
5
+
6
+ //#region ../auth/src/passkeys/passkey-registration.d.ts
7
+ /**
8
+ * Registration options for a LOGGED-IN user adding a passkey. The challenge is
9
+ * stored single-use and bound to this user; the user's existing credentials
10
+ * are excluded so the same authenticator is not registered twice.
11
+ *
12
+ * @example
13
+ * // POST /auth/passkeys/register/options (authMiddleware)
14
+ * response.success(await generatePasskeyRegistrationOptions(request.locals.user));
15
+ */
16
+ declare function generatePasskeyRegistrationOptions(user: Auth): Promise<PasskeyOptionsJSON>;
17
+ /**
18
+ * Verify the browser's registration response and store the credential.
19
+ * Checks, in order: the request `Origin` (CSRF), the single-use challenge
20
+ * (consumed even if what follows fails) belongs to THIS user, then the
21
+ * attestation via `@simplewebauthn/server` against the configured origins and
22
+ * rpID.
23
+ *
24
+ * @throws InvalidPasskeyError (400, EC011) on any rejection.
25
+ */
26
+ declare function verifyPasskeyRegistration(request: Request, user: Auth, credential: PasskeyResponseJSON): Promise<PasskeyCredential>;
27
+ //#endregion
28
+ export { generatePasskeyRegistrationOptions, verifyPasskeyRegistration };
29
+ //# sourceMappingURL=passkey-registration.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey-registration.d.mts","names":[],"sources":["../../../../../../../auth/src/passkeys/passkey-registration.ts"],"mappings":";;;;;;;;;AAoCA;;;;;;iBAAsB,kCAAA,CAAmC,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,kBAAA;;;;;;;AAAkB;AAgChG;;iBAAsB,yBAAA,CACpB,OAAA,EAAS,OAAA,EACT,IAAA,EAAM,IAAA,EACN,UAAA,EAAY,mBAAA,GACX,OAAA,CAAQ,iBAAA"}
@@ -0,0 +1,88 @@
1
+ import { authConfig } from "../services/auth-config.mjs";
2
+ import { InvalidPasskeyError } from "../errors/invalid-passkey.error.mjs";
3
+ import { PasskeyCredential } from "../models/passkey-credential/passkey-credential.model.mjs";
4
+ import "../models/passkey-credential/index.mjs";
5
+ import { assertPasskeyOrigin, storePasskeyChallenge, takePasskeyChallenge } from "./passkey-challenges.mjs";
6
+ import { loadSimpleWebAuthn } from "./simplewebauthn.mjs";
7
+
8
+ //#region ../auth/src/passkeys/passkey-registration.ts
9
+ /** Account name the authenticator shows: `auth.passkeys.userName`, else email, else id. */
10
+ function passkeyUserName(user) {
11
+ const configured = authConfig.passkeys.settings().userName;
12
+ if (configured) return configured(user);
13
+ const email = user.get("email");
14
+ return typeof email === "string" && email ? email : String(user.id);
15
+ }
16
+ /**
17
+ * Registration options for a LOGGED-IN user adding a passkey. The challenge is
18
+ * stored single-use and bound to this user; the user's existing credentials
19
+ * are excluded so the same authenticator is not registered twice.
20
+ *
21
+ * @example
22
+ * // POST /auth/passkeys/register/options (authMiddleware)
23
+ * response.success(await generatePasskeyRegistrationOptions(request.locals.user));
24
+ */
25
+ async function generatePasskeyRegistrationOptions(user) {
26
+ const webauthn = await loadSimpleWebAuthn();
27
+ const settings = authConfig.passkeys.settings();
28
+ const existing = await PasskeyCredential.listFor(user);
29
+ const options = await webauthn.generateRegistrationOptions({
30
+ rpName: settings.rpName,
31
+ rpID: settings.rpID,
32
+ userName: passkeyUserName(user),
33
+ userID: new TextEncoder().encode(`${user.userType}:${user.id}`),
34
+ attestationType: "none",
35
+ excludeCredentials: existing.map((credential) => ({
36
+ id: credential.credentialId,
37
+ transports: credential.get("transports") ?? void 0
38
+ })),
39
+ authenticatorSelection: {
40
+ residentKey: "preferred",
41
+ userVerification: "preferred"
42
+ }
43
+ });
44
+ await storePasskeyChallenge("passkey-registration", options.challenge, user);
45
+ return options;
46
+ }
47
+ /**
48
+ * Verify the browser's registration response and store the credential.
49
+ * Checks, in order: the request `Origin` (CSRF), the single-use challenge
50
+ * (consumed even if what follows fails) belongs to THIS user, then the
51
+ * attestation via `@simplewebauthn/server` against the configured origins and
52
+ * rpID.
53
+ *
54
+ * @throws InvalidPasskeyError (400, EC011) on any rejection.
55
+ */
56
+ async function verifyPasskeyRegistration(request, user, credential) {
57
+ const webauthn = await loadSimpleWebAuthn();
58
+ const origins = assertPasskeyOrigin(request);
59
+ const { challenge, row } = await takePasskeyChallenge("passkey-registration", credential);
60
+ if (String(row.userId) !== String(user.id) || row.userType !== user.userType) throw new InvalidPasskeyError("challenge-issued-to-another-user");
61
+ let verification;
62
+ try {
63
+ verification = await webauthn.verifyRegistrationResponse({
64
+ response: credential,
65
+ expectedChallenge: challenge,
66
+ expectedOrigin: origins,
67
+ expectedRPID: authConfig.passkeys.settings().rpID,
68
+ requireUserVerification: false
69
+ });
70
+ } catch (error) {
71
+ throw new InvalidPasskeyError("registration-verification-failed", error);
72
+ }
73
+ if (!verification.verified || !verification.registrationInfo) throw new InvalidPasskeyError("registration-not-verified");
74
+ const { id, publicKey, counter, transports } = verification.registrationInfo.credential;
75
+ if (await PasskeyCredential.findByCredentialId(id)) throw new InvalidPasskeyError("credential-already-registered");
76
+ return PasskeyCredential.create({
77
+ credential_id: id,
78
+ public_key: Buffer.from(publicKey).toString("base64url"),
79
+ counter,
80
+ transports: transports ?? credential.response.transports ?? [],
81
+ user_id: user.id,
82
+ user_type: user.userType
83
+ });
84
+ }
85
+
86
+ //#endregion
87
+ export { generatePasskeyRegistrationOptions, verifyPasskeyRegistration };
88
+ //# sourceMappingURL=passkey-registration.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey-registration.mjs","names":[],"sources":["../../../../../../../auth/src/passkeys/passkey-registration.ts"],"sourcesContent":["import type { Request } from \"@warlock.js/core\";\nimport { InvalidPasskeyError } from \"../errors/invalid-passkey.error\";\nimport type { Auth } from \"../models/auth.model\";\nimport { PasskeyCredential } from \"../models/passkey-credential\";\nimport { authConfig } from \"../services/auth-config\";\nimport {\n assertPasskeyOrigin,\n storePasskeyChallenge,\n takePasskeyChallenge,\n} from \"./passkey-challenges\";\nimport {\n loadSimpleWebAuthn,\n type PasskeyOptionsJSON,\n type PasskeyResponseJSON,\n} from \"./simplewebauthn\";\n\n/** Account name the authenticator shows: `auth.passkeys.userName`, else email, else id. */\nfunction passkeyUserName(user: Auth): string {\n const configured = authConfig.passkeys.settings().userName;\n\n if (configured) return configured(user);\n\n const email = user.get(\"email\");\n\n return typeof email === \"string\" && email ? email : String(user.id);\n}\n\n/**\n * Registration options for a LOGGED-IN user adding a passkey. The challenge is\n * stored single-use and bound to this user; the user's existing credentials\n * are excluded so the same authenticator is not registered twice.\n *\n * @example\n * // POST /auth/passkeys/register/options (authMiddleware)\n * response.success(await generatePasskeyRegistrationOptions(request.locals.user));\n */\nexport async function generatePasskeyRegistrationOptions(user: Auth): Promise<PasskeyOptionsJSON> {\n const webauthn = await loadSimpleWebAuthn();\n const settings = authConfig.passkeys.settings();\n const existing = await PasskeyCredential.listFor(user);\n\n const options = await webauthn.generateRegistrationOptions({\n rpName: settings.rpName,\n rpID: settings.rpID,\n userName: passkeyUserName(user),\n userID: new TextEncoder().encode(`${user.userType}:${user.id}`),\n attestationType: \"none\",\n excludeCredentials: existing.map((credential) => ({\n id: credential.credentialId,\n transports: credential.get(\"transports\") ?? undefined,\n })),\n authenticatorSelection: { residentKey: \"preferred\", userVerification: \"preferred\" },\n });\n\n await storePasskeyChallenge(\"passkey-registration\", options.challenge, user);\n\n return options;\n}\n\n/**\n * Verify the browser's registration response and store the credential.\n * Checks, in order: the request `Origin` (CSRF), the single-use challenge\n * (consumed even if what follows fails) belongs to THIS user, then the\n * attestation via `@simplewebauthn/server` against the configured origins and\n * rpID.\n *\n * @throws InvalidPasskeyError (400, EC011) on any rejection.\n */\nexport async function verifyPasskeyRegistration(\n request: Request,\n user: Auth,\n credential: PasskeyResponseJSON,\n): Promise<PasskeyCredential> {\n const webauthn = await loadSimpleWebAuthn();\n const origins = assertPasskeyOrigin(request);\n const { challenge, row } = await takePasskeyChallenge(\"passkey-registration\", credential);\n\n if (String(row.userId) !== String(user.id) || row.userType !== user.userType) {\n throw new InvalidPasskeyError(\"challenge-issued-to-another-user\");\n }\n\n let verification: Awaited<ReturnType<typeof webauthn.verifyRegistrationResponse>>;\n\n try {\n verification = await webauthn.verifyRegistrationResponse({\n response: credential,\n expectedChallenge: challenge,\n expectedOrigin: origins,\n expectedRPID: authConfig.passkeys.settings().rpID,\n requireUserVerification: false,\n });\n } catch (error) {\n throw new InvalidPasskeyError(\"registration-verification-failed\", error);\n }\n\n if (!verification.verified || !verification.registrationInfo) {\n throw new InvalidPasskeyError(\"registration-not-verified\");\n }\n\n const { id, publicKey, counter, transports } = verification.registrationInfo.credential;\n\n if (await PasskeyCredential.findByCredentialId(id)) {\n throw new InvalidPasskeyError(\"credential-already-registered\");\n }\n\n return PasskeyCredential.create({\n credential_id: id,\n public_key: Buffer.from(publicKey).toString(\"base64url\"),\n counter,\n transports: transports ?? credential.response.transports ?? [],\n user_id: user.id,\n user_type: user.userType,\n });\n}\n"],"mappings":";;;;;;;;;AAiBA,SAAS,gBAAgB,MAAoB;CAC3C,MAAM,aAAa,WAAW,SAAS,SAAS,CAAC,CAAC;CAElD,IAAI,YAAY,OAAO,WAAW,IAAI;CAEtC,MAAM,QAAQ,KAAK,IAAI,OAAO;CAE9B,OAAO,OAAO,UAAU,YAAY,QAAQ,QAAQ,OAAO,KAAK,EAAE;AACpE;;;;;;;;;;AAWA,eAAsB,mCAAmC,MAAyC;CAChG,MAAM,WAAW,MAAM,mBAAmB;CAC1C,MAAM,WAAW,WAAW,SAAS,SAAS;CAC9C,MAAM,WAAW,MAAM,kBAAkB,QAAQ,IAAI;CAErD,MAAM,UAAU,MAAM,SAAS,4BAA4B;EACzD,QAAQ,SAAS;EACjB,MAAM,SAAS;EACf,UAAU,gBAAgB,IAAI;EAC9B,QAAQ,IAAI,YAAY,CAAC,CAAC,OAAO,GAAG,KAAK,SAAS,GAAG,KAAK,IAAI;EAC9D,iBAAiB;EACjB,oBAAoB,SAAS,KAAK,gBAAgB;GAChD,IAAI,WAAW;GACf,YAAY,WAAW,IAAI,YAAY,KAAK;EAC9C,EAAE;EACF,wBAAwB;GAAE,aAAa;GAAa,kBAAkB;EAAY;CACpF,CAAC;CAED,MAAM,sBAAsB,wBAAwB,QAAQ,WAAW,IAAI;CAE3E,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,0BACpB,SACA,MACA,YAC4B;CAC5B,MAAM,WAAW,MAAM,mBAAmB;CAC1C,MAAM,UAAU,oBAAoB,OAAO;CAC3C,MAAM,EAAE,WAAW,QAAQ,MAAM,qBAAqB,wBAAwB,UAAU;CAExF,IAAI,OAAO,IAAI,MAAM,MAAM,OAAO,KAAK,EAAE,KAAK,IAAI,aAAa,KAAK,UAClE,MAAM,IAAI,oBAAoB,kCAAkC;CAGlE,IAAI;CAEJ,IAAI;EACF,eAAe,MAAM,SAAS,2BAA2B;GACvD,UAAU;GACV,mBAAmB;GACnB,gBAAgB;GAChB,cAAc,WAAW,SAAS,SAAS,CAAC,CAAC;GAC7C,yBAAyB;EAC3B,CAAC;CACH,SAAS,OAAO;EACd,MAAM,IAAI,oBAAoB,oCAAoC,KAAK;CACzE;CAEA,IAAI,CAAC,aAAa,YAAY,CAAC,aAAa,kBAC1C,MAAM,IAAI,oBAAoB,2BAA2B;CAG3D,MAAM,EAAE,IAAI,WAAW,SAAS,eAAe,aAAa,iBAAiB;CAE7E,IAAI,MAAM,kBAAkB,mBAAmB,EAAE,GAC/C,MAAM,IAAI,oBAAoB,+BAA+B;CAG/D,OAAO,kBAAkB,OAAO;EAC9B,eAAe;EACf,YAAY,OAAO,KAAK,SAAS,CAAC,CAAC,SAAS,WAAW;EACvD;EACA,YAAY,cAAc,WAAW,SAAS,cAAc,CAAC;EAC7D,SAAS,KAAK;EACd,WAAW,KAAK;CAClB,CAAC;AACH"}
@@ -0,0 +1,21 @@
1
+ //#region ../auth/src/passkeys/simplewebauthn.d.ts
2
+ /** The browser's JSON-serialized credential (`startRegistration` / `startAuthentication` result). */
3
+ type PasskeyResponseJSON = {
4
+ id: string;
5
+ rawId: string;
6
+ type: string;
7
+ response: {
8
+ clientDataJSON: string;
9
+ transports?: string[];
10
+ [key: string]: unknown;
11
+ };
12
+ [key: string]: unknown;
13
+ };
14
+ /** Options JSON handed to the browser; `challenge` is what auth stores. */
15
+ type PasskeyOptionsJSON = {
16
+ challenge: string;
17
+ [key: string]: unknown;
18
+ };
19
+ //#endregion
20
+ export { PasskeyOptionsJSON, PasskeyResponseJSON };
21
+ //# sourceMappingURL=simplewebauthn.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simplewebauthn.d.mts","names":[],"sources":["../../../../../../../auth/src/passkeys/simplewebauthn.ts"],"mappings":";;KAMY,mBAAA;EACV,EAAA;EACA,KAAA;EACA,IAAA;EACA,QAAA;IACE,cAAA;IACA,UAAA;IAAA,CACC,GAAA;EAAA;EAAA,CAEF,GAAA;AAAA;AAAW;AAAA,KAIF,kBAAA;EACV,SAAA;EAAA,CACC,GAAW;AAAA"}
@@ -0,0 +1,13 @@
1
+ import { loadOptionalPeer } from "../services/optional-peer.mjs";
2
+
3
+ //#region ../auth/src/passkeys/simplewebauthn.ts
4
+ /** The `warlock add` feature that installs `@simplewebauthn/server`. */
5
+ const PASSKEYS_FEATURE = "auth-passkeys";
6
+ /** Load `@simplewebauthn/server`, or throw `AuthProviderSdkMissingError` naming `warlock add auth-passkeys`. */
7
+ function loadSimpleWebAuthn() {
8
+ return loadOptionalPeer("@simplewebauthn/server", PASSKEYS_FEATURE);
9
+ }
10
+
11
+ //#endregion
12
+ export { loadSimpleWebAuthn };
13
+ //# sourceMappingURL=simplewebauthn.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simplewebauthn.mjs","names":[],"sources":["../../../../../../../auth/src/passkeys/simplewebauthn.ts"],"sourcesContent":["import { loadOptionalPeer } from \"../services/optional-peer\";\n\n/** The `warlock add` feature that installs `@simplewebauthn/server`. */\nexport const PASSKEYS_FEATURE = \"auth-passkeys\";\n\n/** The browser's JSON-serialized credential (`startRegistration` / `startAuthentication` result). */\nexport type PasskeyResponseJSON = {\n id: string;\n rawId: string;\n type: string;\n response: {\n clientDataJSON: string;\n transports?: string[];\n [key: string]: unknown;\n };\n [key: string]: unknown;\n};\n\n/** Options JSON handed to the browser; `challenge` is what auth stores. */\nexport type PasskeyOptionsJSON = {\n challenge: string;\n [key: string]: unknown;\n};\n\n/** A stored credential in the shape `@simplewebauthn/server` verifies against. */\nexport type WebAuthnCredential = {\n id: string;\n publicKey: Uint8Array;\n counter: number;\n transports?: string[];\n};\n\n/**\n * The slice of `@simplewebauthn/server` (v13) auth uses — declared locally\n * because the package is an optional peer.\n */\nexport type SimpleWebAuthnServer = {\n generateRegistrationOptions: (options: Record<string, unknown>) => Promise<PasskeyOptionsJSON>;\n verifyRegistrationResponse: (options: Record<string, unknown>) => Promise<{\n verified: boolean;\n registrationInfo?: { credential: WebAuthnCredential };\n }>;\n generateAuthenticationOptions: (options: Record<string, unknown>) => Promise<PasskeyOptionsJSON>;\n verifyAuthenticationResponse: (options: Record<string, unknown>) => Promise<{\n verified: boolean;\n authenticationInfo: { newCounter: number };\n }>;\n};\n\n/** Load `@simplewebauthn/server`, or throw `AuthProviderSdkMissingError` naming `warlock add auth-passkeys`. */\nexport function loadSimpleWebAuthn(): Promise<SimpleWebAuthnServer> {\n return loadOptionalPeer<SimpleWebAuthnServer>(\"@simplewebauthn/server\", PASSKEYS_FEATURE);\n}\n"],"mappings":";;;;AAGA,MAAa,mBAAmB;;AA+ChC,SAAgB,qBAAoD;CAClE,OAAO,iBAAuC,0BAA0B,gBAAgB;AAC1F"}
@@ -0,0 +1,40 @@
1
+ import { AuthProvider, GoogleProviderConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
2
+
3
+ //#region ../auth/src/providers/google-provider.d.ts
4
+ /** The `warlock add` feature that installs `jose`. */
5
+ declare const GOOGLE_AUTH_FEATURE = "auth-google";
6
+ /** The slice of `jose` the Google provider uses — declared locally because `jose` is an optional peer. */
7
+ type JoseModule = {
8
+ createRemoteJWKSet: (url: URL) => unknown;
9
+ jwtVerify: (token: string, key: unknown, options: {
10
+ issuer: string[];
11
+ audience: string;
12
+ }) => Promise<{
13
+ payload: Record<string, unknown>;
14
+ }>;
15
+ };
16
+ /** PKCE S256 code challenge for a verifier. */
17
+ declare function pkceChallenge(codeVerifier: string): string;
18
+ /**
19
+ * Google sign-in: OpenID Connect authorization code flow with PKCE (S256),
20
+ * `state` and `nonce`. The code is exchanged with plain `fetch`; the id_token
21
+ * is verified with `jose` against Google's JWKS (signature, `iss`, `aud` = the
22
+ * client id, `exp`), then its `nonce` is compared. No Google SDK.
23
+ */
24
+ declare class GoogleProvider implements AuthProvider {
25
+ private readonly settings;
26
+ readonly name = "google";
27
+ constructor(settings: GoogleProviderConfig);
28
+ authorizationUrl(state: ProviderAuthorizationState): string;
29
+ handleCallback({
30
+ query,
31
+ expected
32
+ }: ProviderCallbackParams): Promise<ProviderProfile>;
33
+ /** Redeem the authorization code (with the PKCE verifier) for an id_token. */
34
+ private exchangeCode;
35
+ /** Verify signature, issuer, audience and expiry; resolve the claims. */
36
+ private verifyIdToken;
37
+ }
38
+ //#endregion
39
+ export { GOOGLE_AUTH_FEATURE, GoogleProvider, JoseModule, pkceChallenge };
40
+ //# sourceMappingURL=google-provider.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/google-provider.ts"],"mappings":";;;;cAkBa,mBAAA;AAAb;AAAA,KAGY,UAAA;EACV,kBAAA,GAAqB,GAAA,EAAK,GAAA;EAC1B,SAAA,GACE,KAAA,UACA,GAAA,WACA,OAAA;IAAW,MAAA;IAAkB,QAAA;EAAA,MAC1B,OAAA;IAAU,OAAA,EAAS,MAAA;EAAA;AAAA;;iBAOV,aAAA,CAAc,YAAoB;;;;;;;cAUrC,cAAA,YAA0B,YAAA;EAAA,iBAGD,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,oBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAelB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAxCZ;EAAA,QA0EV,YAAA;EA1EgB;EAAA,QA4GhB,aAAA;AAAA"}