@warlock.js/auth 5.29.0 → 5.31.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 (273) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/esm/commands/auth-cleanup-command.d.mts +1 -1
  3. package/esm/commands/auth-cleanup-command.d.mts.map +1 -1
  4. package/esm/commands/auth-cleanup-command.mjs +1 -1
  5. package/esm/commands/auth-cleanup-command.mjs.map +1 -1
  6. package/esm/commands/auth-purge-never-expiring-command.d.mts +1 -1
  7. package/esm/commands/auth-purge-never-expiring-command.d.mts.map +1 -1
  8. package/esm/commands/auth-purge-never-expiring-command.mjs +1 -1
  9. package/esm/commands/auth-purge-never-expiring-command.mjs.map +1 -1
  10. package/esm/commands/jwt-secret-generator-command.d.mts +1 -1
  11. package/esm/commands/jwt-secret-generator-command.d.mts.map +1 -1
  12. package/esm/commands/jwt-secret-generator-command.mjs +1 -1
  13. package/esm/commands/jwt-secret-generator-command.mjs.map +1 -1
  14. package/esm/contracts/auth-contract.d.mts +1 -1
  15. package/esm/contracts/auth-contract.d.mts.map +1 -1
  16. package/esm/contracts/providers.d.mts +1 -1
  17. package/esm/contracts/providers.d.mts.map +1 -1
  18. package/esm/contracts/request-locals.d.mts +1 -1
  19. package/esm/contracts/request-locals.d.mts.map +1 -1
  20. package/esm/contracts/types.d.mts +1 -1
  21. package/esm/contracts/types.d.mts.map +1 -1
  22. package/esm/contracts/types.mjs +1 -1
  23. package/esm/contracts/types.mjs.map +1 -1
  24. package/esm/errors/auth-provider-sdk-missing.error.d.mts +1 -1
  25. package/esm/errors/auth-provider-sdk-missing.error.d.mts.map +1 -1
  26. package/esm/errors/auth-provider-sdk-missing.error.mjs +1 -1
  27. package/esm/errors/auth-provider-sdk-missing.error.mjs.map +1 -1
  28. package/esm/errors/email-not-verified.error.d.mts +1 -1
  29. package/esm/errors/email-not-verified.error.d.mts.map +1 -1
  30. package/esm/errors/email-not-verified.error.mjs +1 -1
  31. package/esm/errors/email-not-verified.error.mjs.map +1 -1
  32. package/esm/errors/invalid-one-time-token.error.d.mts +1 -1
  33. package/esm/errors/invalid-one-time-token.error.d.mts.map +1 -1
  34. package/esm/errors/invalid-one-time-token.error.mjs +1 -1
  35. package/esm/errors/invalid-one-time-token.error.mjs.map +1 -1
  36. package/esm/errors/invalid-passkey.error.d.mts +1 -1
  37. package/esm/errors/invalid-passkey.error.d.mts.map +1 -1
  38. package/esm/errors/invalid-passkey.error.mjs +1 -1
  39. package/esm/errors/invalid-passkey.error.mjs.map +1 -1
  40. package/esm/errors/invalid-provider-callback.error.d.mts +1 -1
  41. package/esm/errors/invalid-provider-callback.error.d.mts.map +1 -1
  42. package/esm/errors/invalid-provider-callback.error.mjs +1 -1
  43. package/esm/errors/invalid-provider-callback.error.mjs.map +1 -1
  44. package/esm/errors/notifications-unavailable.error.d.mts +1 -1
  45. package/esm/errors/notifications-unavailable.error.d.mts.map +1 -1
  46. package/esm/errors/notifications-unavailable.error.mjs +1 -1
  47. package/esm/errors/notifications-unavailable.error.mjs.map +1 -1
  48. package/esm/errors/provider-email-not-verified.error.d.mts +1 -1
  49. package/esm/errors/provider-email-not-verified.error.d.mts.map +1 -1
  50. package/esm/errors/provider-email-not-verified.error.mjs +1 -1
  51. package/esm/errors/provider-email-not-verified.error.mjs.map +1 -1
  52. package/esm/index.d.mts +2 -2
  53. package/esm/index.mjs +2 -2
  54. package/esm/localization/ar.translations.mjs +1 -1
  55. package/esm/localization/ar.translations.mjs.map +1 -1
  56. package/esm/localization/auth-default-translations.d.mts +1 -1
  57. package/esm/localization/auth-default-translations.d.mts.map +1 -1
  58. package/esm/localization/auth-default-translations.mjs +1 -1
  59. package/esm/localization/auth-default-translations.mjs.map +1 -1
  60. package/esm/localization/auth-translations.type.d.mts +1 -1
  61. package/esm/localization/auth-translations.type.d.mts.map +1 -1
  62. package/esm/localization/en.translations.mjs +1 -1
  63. package/esm/localization/en.translations.mjs.map +1 -1
  64. package/esm/localization/init.mjs +1 -1
  65. package/esm/localization/init.mjs.map +1 -1
  66. package/esm/localization/register-auth-translations.d.mts +1 -1
  67. package/esm/localization/register-auth-translations.d.mts.map +1 -1
  68. package/esm/localization/register-auth-translations.mjs +1 -1
  69. package/esm/localization/register-auth-translations.mjs.map +1 -1
  70. package/esm/middleware/auth.middleware.d.mts +1 -1
  71. package/esm/middleware/auth.middleware.d.mts.map +1 -1
  72. package/esm/middleware/auth.middleware.mjs +3 -3
  73. package/esm/middleware/auth.middleware.mjs.map +1 -1
  74. package/esm/middleware/csrf-origin-check.d.mts +1 -1
  75. package/esm/middleware/csrf-origin-check.d.mts.map +1 -1
  76. package/esm/middleware/csrf-origin-check.mjs +1 -1
  77. package/esm/middleware/csrf-origin-check.mjs.map +1 -1
  78. package/esm/middleware/index.d.mts +1 -1
  79. package/esm/middleware/index.mjs +1 -1
  80. package/esm/middleware/localized-login-path.mjs +1 -1
  81. package/esm/middleware/localized-login-path.mjs.map +1 -1
  82. package/esm/middleware/login-throttle.middleware.d.mts +17 -2
  83. package/esm/middleware/login-throttle.middleware.d.mts.map +1 -1
  84. package/esm/middleware/login-throttle.middleware.mjs +84 -6
  85. package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
  86. package/esm/middleware/one-time-token-throttle.middleware.d.mts +1 -1
  87. package/esm/middleware/one-time-token-throttle.middleware.d.mts.map +1 -1
  88. package/esm/middleware/one-time-token-throttle.middleware.mjs +1 -1
  89. package/esm/middleware/one-time-token-throttle.middleware.mjs.map +1 -1
  90. package/esm/middleware/otp-throttle.middleware.d.mts +1 -1
  91. package/esm/middleware/otp-throttle.middleware.d.mts.map +1 -1
  92. package/esm/middleware/otp-throttle.middleware.mjs +1 -1
  93. package/esm/middleware/otp-throttle.middleware.mjs.map +1 -1
  94. package/esm/middleware/require-verified-email.middleware.d.mts +1 -1
  95. package/esm/middleware/require-verified-email.middleware.d.mts.map +1 -1
  96. package/esm/middleware/require-verified-email.middleware.mjs +1 -1
  97. package/esm/middleware/require-verified-email.middleware.mjs.map +1 -1
  98. package/esm/models/access-token/access-token.model.d.mts +3 -35
  99. package/esm/models/access-token/access-token.model.d.mts.map +1 -1
  100. package/esm/models/access-token/access-token.model.mjs +1 -1
  101. package/esm/models/access-token/access-token.model.mjs.map +1 -1
  102. package/esm/models/access-token/family-association.migration.mjs +1 -1
  103. package/esm/models/access-token/family-association.migration.mjs.map +1 -1
  104. package/esm/models/access-token/migration.mjs +1 -1
  105. package/esm/models/access-token/migration.mjs.map +1 -1
  106. package/esm/models/auth-token-family/auth-token-family.model.d.mts +3 -43
  107. package/esm/models/auth-token-family/auth-token-family.model.d.mts.map +1 -1
  108. package/esm/models/auth-token-family/auth-token-family.model.mjs +1 -1
  109. package/esm/models/auth-token-family/auth-token-family.model.mjs.map +1 -1
  110. package/esm/models/auth-token-family/migration.mjs +1 -1
  111. package/esm/models/auth-token-family/migration.mjs.map +1 -1
  112. package/esm/models/auth.model.d.mts +1 -1
  113. package/esm/models/auth.model.d.mts.map +1 -1
  114. package/esm/models/auth.model.mjs +1 -1
  115. package/esm/models/auth.model.mjs.map +1 -1
  116. package/esm/models/index.d.mts +1 -1
  117. package/esm/models/index.d.mts.map +1 -1
  118. package/esm/models/index.mjs +1 -1
  119. package/esm/models/index.mjs.map +1 -1
  120. package/esm/models/one-time-token/migration.mjs +1 -1
  121. package/esm/models/one-time-token/migration.mjs.map +1 -1
  122. package/esm/models/one-time-token/one-time-token.model.d.mts +3 -67
  123. package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -1
  124. package/esm/models/one-time-token/one-time-token.model.mjs +1 -1
  125. package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -1
  126. package/esm/models/passkey-credential/migration.mjs +1 -1
  127. package/esm/models/passkey-credential/migration.mjs.map +1 -1
  128. package/esm/models/passkey-credential/passkey-credential.model.d.mts +3 -47
  129. package/esm/models/passkey-credential/passkey-credential.model.d.mts.map +1 -1
  130. package/esm/models/passkey-credential/passkey-credential.model.mjs +1 -1
  131. package/esm/models/passkey-credential/passkey-credential.model.mjs.map +1 -1
  132. package/esm/models/provider-account/migration.mjs +1 -1
  133. package/esm/models/provider-account/migration.mjs.map +1 -1
  134. package/esm/models/provider-account/provider-account.model.d.mts +3 -35
  135. package/esm/models/provider-account/provider-account.model.d.mts.map +1 -1
  136. package/esm/models/provider-account/provider-account.model.mjs +1 -1
  137. package/esm/models/provider-account/provider-account.model.mjs.map +1 -1
  138. package/esm/models/refresh-token/migration.mjs +1 -1
  139. package/esm/models/refresh-token/migration.mjs.map +1 -1
  140. package/esm/models/refresh-token/refresh-token.model.d.mts +7 -99
  141. package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
  142. package/esm/models/refresh-token/refresh-token.model.mjs +1 -1
  143. package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
  144. package/esm/models/refresh-token/successor-association.migration.mjs +1 -1
  145. package/esm/models/refresh-token/successor-association.migration.mjs.map +1 -1
  146. package/esm/otp/otp.d.mts +1 -1
  147. package/esm/otp/otp.d.mts.map +1 -1
  148. package/esm/otp/otp.mjs +1 -1
  149. package/esm/otp/otp.mjs.map +1 -1
  150. package/esm/passkeys/passkey-authentication.d.mts +1 -1
  151. package/esm/passkeys/passkey-authentication.d.mts.map +1 -1
  152. package/esm/passkeys/passkey-authentication.mjs +1 -1
  153. package/esm/passkeys/passkey-authentication.mjs.map +1 -1
  154. package/esm/passkeys/passkey-challenges.mjs +1 -1
  155. package/esm/passkeys/passkey-challenges.mjs.map +1 -1
  156. package/esm/passkeys/passkey-registration.d.mts +1 -1
  157. package/esm/passkeys/passkey-registration.d.mts.map +1 -1
  158. package/esm/passkeys/passkey-registration.mjs +1 -1
  159. package/esm/passkeys/passkey-registration.mjs.map +1 -1
  160. package/esm/passkeys/simplewebauthn.d.mts +1 -1
  161. package/esm/passkeys/simplewebauthn.d.mts.map +1 -1
  162. package/esm/passkeys/simplewebauthn.mjs +1 -1
  163. package/esm/passkeys/simplewebauthn.mjs.map +1 -1
  164. package/esm/providers/apple-provider.d.mts +1 -1
  165. package/esm/providers/apple-provider.d.mts.map +1 -1
  166. package/esm/providers/apple-provider.mjs +1 -1
  167. package/esm/providers/apple-provider.mjs.map +1 -1
  168. package/esm/providers/discord-provider.d.mts +1 -1
  169. package/esm/providers/discord-provider.d.mts.map +1 -1
  170. package/esm/providers/discord-provider.mjs +1 -1
  171. package/esm/providers/discord-provider.mjs.map +1 -1
  172. package/esm/providers/facebook-provider.d.mts +1 -1
  173. package/esm/providers/facebook-provider.d.mts.map +1 -1
  174. package/esm/providers/facebook-provider.mjs +1 -1
  175. package/esm/providers/facebook-provider.mjs.map +1 -1
  176. package/esm/providers/github-provider.d.mts +1 -1
  177. package/esm/providers/github-provider.d.mts.map +1 -1
  178. package/esm/providers/github-provider.mjs +1 -1
  179. package/esm/providers/github-provider.mjs.map +1 -1
  180. package/esm/providers/google-provider.d.mts +1 -1
  181. package/esm/providers/google-provider.d.mts.map +1 -1
  182. package/esm/providers/google-provider.mjs +1 -1
  183. package/esm/providers/google-provider.mjs.map +1 -1
  184. package/esm/providers/linkedin-provider.d.mts +1 -1
  185. package/esm/providers/linkedin-provider.d.mts.map +1 -1
  186. package/esm/providers/linkedin-provider.mjs +1 -1
  187. package/esm/providers/linkedin-provider.mjs.map +1 -1
  188. package/esm/providers/provider-login.d.mts +1 -1
  189. package/esm/providers/provider-login.d.mts.map +1 -1
  190. package/esm/providers/provider-login.mjs +1 -1
  191. package/esm/providers/provider-login.mjs.map +1 -1
  192. package/esm/providers/provider-state-cookie.mjs +1 -1
  193. package/esm/providers/provider-state-cookie.mjs.map +1 -1
  194. package/esm/providers/resolve-provider-user.mjs +1 -1
  195. package/esm/providers/resolve-provider-user.mjs.map +1 -1
  196. package/esm/providers/x-provider.d.mts +1 -1
  197. package/esm/providers/x-provider.d.mts.map +1 -1
  198. package/esm/providers/x-provider.mjs +1 -1
  199. package/esm/providers/x-provider.mjs.map +1 -1
  200. package/esm/services/auth-config.mjs +1 -1
  201. package/esm/services/auth-config.mjs.map +1 -1
  202. package/esm/services/auth-events.d.mts +1 -1
  203. package/esm/services/auth-events.d.mts.map +1 -1
  204. package/esm/services/auth-events.mjs +1 -1
  205. package/esm/services/auth-events.mjs.map +1 -1
  206. package/esm/services/auth.service.d.mts +1 -1
  207. package/esm/services/auth.service.d.mts.map +1 -1
  208. package/esm/services/auth.service.mjs +1 -1
  209. package/esm/services/auth.service.mjs.map +1 -1
  210. package/esm/services/current-user.d.mts +1 -1
  211. package/esm/services/current-user.d.mts.map +1 -1
  212. package/esm/services/current-user.mjs +1 -1
  213. package/esm/services/current-user.mjs.map +1 -1
  214. package/esm/services/email-verification.d.mts +1 -1
  215. package/esm/services/email-verification.d.mts.map +1 -1
  216. package/esm/services/email-verification.mjs +1 -1
  217. package/esm/services/email-verification.mjs.map +1 -1
  218. package/esm/services/generate-jwt-secret.d.mts +1 -1
  219. package/esm/services/generate-jwt-secret.d.mts.map +1 -1
  220. package/esm/services/generate-jwt-secret.mjs +1 -1
  221. package/esm/services/generate-jwt-secret.mjs.map +1 -1
  222. package/esm/services/jwt.d.mts +1 -1
  223. package/esm/services/jwt.d.mts.map +1 -1
  224. package/esm/services/jwt.mjs +1 -1
  225. package/esm/services/jwt.mjs.map +1 -1
  226. package/esm/services/one-time-tokens.d.mts +1 -1
  227. package/esm/services/one-time-tokens.d.mts.map +1 -1
  228. package/esm/services/one-time-tokens.mjs +1 -1
  229. package/esm/services/one-time-tokens.mjs.map +1 -1
  230. package/esm/services/optional-peer.mjs +1 -1
  231. package/esm/services/optional-peer.mjs.map +1 -1
  232. package/esm/services/password-reset.d.mts +1 -1
  233. package/esm/services/password-reset.d.mts.map +1 -1
  234. package/esm/services/password-reset.mjs +1 -1
  235. package/esm/services/password-reset.mjs.map +1 -1
  236. package/esm/services/resolve-request-user.d.mts +1 -1
  237. package/esm/services/resolve-request-user.d.mts.map +1 -1
  238. package/esm/services/resolve-request-user.mjs +1 -1
  239. package/esm/services/resolve-request-user.mjs.map +1 -1
  240. package/esm/services/token-family-operation.mjs +1 -1
  241. package/esm/services/token-family-operation.mjs.map +1 -1
  242. package/esm/services/token-notifications.mjs +1 -1
  243. package/esm/services/token-notifications.mjs.map +1 -1
  244. package/esm/session/page-session.d.mts +1 -1
  245. package/esm/session/page-session.d.mts.map +1 -1
  246. package/esm/session/page-session.mjs +1 -1
  247. package/esm/session/page-session.mjs.map +1 -1
  248. package/esm/utils/auth-error-codes.d.mts +1 -1
  249. package/esm/utils/auth-error-codes.d.mts.map +1 -1
  250. package/esm/utils/auth-error-codes.mjs +1 -1
  251. package/esm/utils/auth-error-codes.mjs.map +1 -1
  252. package/esm/utils/safe-equal.d.mts +1 -1
  253. package/esm/utils/safe-equal.d.mts.map +1 -1
  254. package/esm/utils/safe-equal.mjs +1 -1
  255. package/esm/utils/safe-equal.mjs.map +1 -1
  256. package/esm/utils/token-expiry.d.mts +1 -1
  257. package/esm/utils/token-expiry.d.mts.map +1 -1
  258. package/esm/utils/token-expiry.mjs +1 -1
  259. package/esm/utils/token-expiry.mjs.map +1 -1
  260. package/llms-full.txt +192 -112
  261. package/package.json +10 -9
  262. package/skills/auth-basics/SKILL.md +10 -13
  263. package/skills/customize-token-storage/SKILL.md +24 -19
  264. package/skills/customize-user-type/SKILL.md +3 -1
  265. package/skills/index.md +4 -1
  266. package/skills/login-with-providers/SKILL.md +54 -29
  267. package/skills/manage-tokens/SKILL.md +2 -2
  268. package/skills/overview/SKILL.md +10 -1
  269. package/skills/protect-routes/SKILL.md +23 -14
  270. package/skills/register-user/SKILL.md +4 -4
  271. package/skills/run-auth-commands/SKILL.md +1 -1
  272. package/skills/throttle-login-attempts/SKILL.md +54 -23
  273. package/skills/verify-email-and-reset-password/SKILL.md +7 -5
@@ -1 +1 @@
1
- {"version":3,"file":"refresh-token.model.mjs","names":[],"sources":["../../../../../../../../auth/src/models/refresh-token/refresh-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\nimport { v } from \"@warlock.js/seal\";\nimport type { DeviceInfo } from \"../../contracts/types\";\nimport { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\nimport type { Auth } from \"../auth.model\";\n\n/**\n * Seal schema for the persisted refresh-token record. Exported so an override\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\n * base shape:\n *\n * @example\n * export class AppRefreshToken extends RefreshToken {\n * public static schema = refreshTokenSchema.extend({\n * organization_id: v.string().required(),\n * });\n * }\n */\nexport const refreshTokenSchema = v.object({\n token: v.string().required(),\n user_id: v.scalar().required(),\n user_type: v.string().required(),\n family_id: v.string().required(),\n expires_at: v.date().required(),\n last_used_at: v.date().default(() => new Date()),\n revoked_at: v.date().optional(),\n /** The successor pair a bounded concurrent renewal may reuse. */\n successor_access_token_id: v.string().optional(),\n successor_refresh_token_id: v.string().optional(),\n device_info: v.record(v.any()).optional(),\n});\n\n/**\n * Extra attributes captured when a refresh token is issued.\n */\nexport type RefreshTokenIssueOptions = {\n familyId: string;\n expiresAt: string;\n deviceInfo?: DeviceInfo;\n};\n\n/**\n * Persisted refresh-token record + the data layer for refresh tokens.\n *\n * **Role.** Owns every refresh-token read, write, and lifecycle transition.\n * The auth service drives refresh-token state exclusively through this model's\n * named statics and instance methods, so it never hard-codes a column name —\n * which is what makes a snake/camel mismatch (the historical `userId` bug)\n * structurally impossible and lets an override rename or add columns by\n * extending this class and registering it under `config.auth.refreshToken.model`.\n */\nexport class RefreshToken extends Model {\n public static table = \"refresh_tokens\";\n\n public static schema = refreshTokenSchema;\n\n /**\n * The raw signed refresh JWT is a bearer credential — never serialize it.\n * `hidden` only strips it from `toJSON()` / `JSON.stringify(model)` (e.g. a\n * session list returned via `response.json`); `get(\"token\")`,\n * `findByToken` / `findForUser` and every query still read the column.\n */\n public static hidden = [\"token\"];\n\n /**\n * Permanent regardless of the data source default — Mongo's driver default\n * is `\"trash\"`, which would copy live credential material (the JWT itself)\n * into `refresh_tokensTrash` on every `destroy()` (expiry purge,\n * never-expiring purge). A purged refresh token must not survive anywhere.\n */\n public static deleteStrategy = \"permanent\" as const;\n\n /** Token family this row belongs to (rotation / replay grouping). */\n public get familyId(): string {\n return this.get(\"family_id\");\n }\n\n /**\n * Whether the token's `expires_at` is in the past.\n *\n * **Fails closed as of 4.12.0**: a missing or unparseable `expires_at` now\n * counts as expired. It previously answered `false` for both — \"no expiry\n * recorded ⇒ never expires\" — which handed an unlimited life to precisely the\n * malformed rows, including the `Invalid Date` a pre-4.12.0 unparseable\n * `expiresIn` could write. `expires_at` is `required` in the schema, so a row\n * that cannot say when it dies is malformed, not immortal.\n */\n public get isExpired(): boolean {\n const expiresAt = this.get(\"expires_at\");\n\n if (!isUsableExpiry(expiresAt)) return true;\n\n return new Date().getTime() > new Date(expiresAt).getTime();\n }\n\n /**\n * Whether nothing can ever retire this row — an unusable `expires_at`, or a\n * token carrying no `exp` claim. See {@link isNeverExpiring}.\n */\n public get neverExpires(): boolean {\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\n }\n\n /** Whether the token has been revoked. */\n public get isRevoked(): boolean {\n return !!this.get(\"revoked_at\");\n }\n\n /** Whether the token is still usable (not expired and not revoked). */\n public get isValid(): boolean {\n return !this.isExpired && !this.isRevoked;\n }\n\n /**\n * Unconditionally stamp `revoked_at` on this token.\n */\n public async revoke(): Promise<this> {\n return this.merge({ revoked_at: new Date() }).save();\n }\n\n /**\n * Atomically revoke this token ONLY if it is still active. Resolves to `true`\n * when this call performed the revoke, `false` when a concurrent request had\n * already revoked it — the win/lose signal that powers rotation replay\n * detection. Uses a conditional UPDATE so two concurrent rotations of the\n * same token can never both succeed.\n */\n public async revokeIfActive(): Promise<boolean> {\n const modelClass = this.constructor as typeof RefreshToken;\n\n const revokedCount = await modelClass.atomic(\n { id: this.id, revoked_at: null },\n { $set: { revoked_at: new Date() } },\n );\n\n return revokedCount > 0;\n }\n\n /**\n * Touch `last_used_at` without revoking — the non-rotating refresh path.\n */\n public async markAsUsed(): Promise<void> {\n await this.merge({ last_used_at: new Date() }).save();\n }\n\n /**\n * Persist a freshly-signed refresh token for the user.\n */\n public static issue(user: Auth, token: string, options: RefreshTokenIssueOptions) {\n return this.create({\n token,\n user_id: user.id,\n user_type: user.userType,\n family_id: options.familyId,\n expires_at: options.expiresAt,\n device_info: options.deviceInfo\n ? {\n userAgent: options.deviceInfo.userAgent,\n ip: options.deviceInfo.ip,\n deviceId: options.deviceInfo.deviceId,\n }\n : undefined,\n });\n }\n\n /**\n * Find a refresh-token row by its raw token string.\n */\n public static findByToken(token: string): Promise<RefreshToken | null> {\n return this.first({ token });\n }\n\n /**\n * Find a refresh token scoped to a user — used by logout so a caller can only\n * revoke a token that actually belongs to them.\n */\n public static findForUser(user: Auth, token: string): Promise<RefreshToken | null> {\n return this.first({ token, user_id: user.id });\n }\n\n /** Find any member of a family, including an already-revoked legacy member. */\n public static findInFamily(familyId: string): Promise<RefreshToken | null> {\n return this.first({ family_id: familyId });\n }\n\n /**\n * Every refresh lineage for a user, including rotated rows. Logout uses this\n * to bootstrap durable family records before it starts family revocations.\n */\n public static familiesForUser(user: Auth): Promise<RefreshToken[]> {\n return this.query().where({ user_id: user.id, user_type: user.userType }).get();\n }\n\n /**\n * Delete a specific refresh token belonging to the user.\n */\n public static deleteForUser(user: Auth, token: string) {\n return this.delete({ token, user_id: user.id });\n }\n\n /**\n * Active, unexpired sessions for the user, newest first.\n */\n public static activeFor(user: Auth): Promise<RefreshToken[]> {\n return this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .where(\"expires_at\", \">\", new Date())\n .orderBy(\"created_at\", \"desc\")\n .get();\n }\n\n /**\n * Revoke every still-active refresh token for the user, returning the rows\n * that were revoked so the caller can emit a per-token event for each.\n *\n * Rows are fetched BEFORE they are revoked: a bulk `findAndUpdate` keyed on\n * `revoked_at: null` would re-query the same predicate after the update and\n * match nothing, returning an empty set.\n */\n public static async revokeAllFor(user: Auth): Promise<RefreshToken[]> {\n const tokens = await this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .get();\n\n for (const token of tokens) {\n await token.revoke();\n }\n\n return tokens;\n }\n\n /**\n * Revoke every still-active token in a family (rotation breach containment),\n * returning the revoked rows (fetched before revocation — see `revokeAllFor`).\n */\n public static async revokeFamily(familyId: string): Promise<RefreshToken[]> {\n const tokens = await this.query().where({ family_id: familyId, revoked_at: null }).get();\n\n for (const token of tokens) {\n await token.revoke();\n }\n\n return tokens;\n }\n\n /**\n * Revoke the oldest active tokens so at most `max - 1` remain — making room\n * for the about-to-be-issued one. Bounded by `max`, so the per-row loop is\n * small.\n */\n public static async enforceMax(user: Auth, max: number): Promise<void> {\n const activeTokens = await this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .orderBy(\"created_at\", \"asc\")\n .get();\n\n if (activeTokens.length < max) return;\n\n const tokensToRevoke = activeTokens.slice(0, activeTokens.length - max + 1);\n\n for (const token of tokensToRevoke) {\n await token.revoke();\n }\n }\n\n /**\n * Hard-delete every expired refresh token, returning the deleted rows so the\n * caller can emit a per-token event. Runs from the `auth.cleanup` CLI command\n * (a cold batch path).\n */\n public static async purgeExpired(): Promise<RefreshToken[]> {\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\n\n for (const token of expiredTokens) {\n await token.destroy();\n }\n\n return expiredTokens;\n }\n\n /**\n * Every row that can never retire itself — see {@link neverExpires}. A full\n * scan filtered in memory, for the reason given on\n * {@link AccessToken.findNeverExpiring}: neither an `Invalid Date` nor a\n * missing `exp` claim is expressible as a `where`.\n */\n public static async findNeverExpiring(): Promise<RefreshToken[]> {\n const tokens = await this.query().get();\n\n return tokens.filter((token: RefreshToken) => token.neverExpires);\n }\n\n /**\n * Hard-delete every never-expiring row, returning the rows removed.\n *\n * Deleted rather than `revoked_at`-stamped: a revoked row is still a row this\n * table has to carry, and one of the two shapes being removed is a row whose\n * date column cannot be compared at all — leaving it in place keeps a\n * permanently unpurgeable record. Rotation replay-detection is unaffected;\n * these rows can no longer be presented successfully either way.\n */\n public static async purgeNeverExpiring(): Promise<RefreshToken[]> {\n const tokens = await this.findNeverExpiring();\n\n for (const token of tokens) {\n await token.destroy();\n }\n\n return tokens;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"refresh-token.model.mjs","names":[],"sources":["../../../../../../../../../packages/auth/src/models/refresh-token/refresh-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\nimport { v } from \"@warlock.js/seal\";\nimport type { DeviceInfo } from \"../../contracts/types\";\nimport { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\nimport type { Auth } from \"../auth.model\";\n\n/**\n * Seal schema for the persisted refresh-token record. Exported so an override\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\n * base shape:\n *\n * @example\n * export class AppRefreshToken extends RefreshToken {\n * public static schema = refreshTokenSchema.extend({\n * organization_id: v.string().required(),\n * });\n * }\n */\nexport const refreshTokenSchema = v.object({\n token: v.string().required(),\n user_id: v.scalar().required(),\n user_type: v.string().required(),\n family_id: v.string().required(),\n expires_at: v.date().required(),\n last_used_at: v.date().default(() => new Date()),\n revoked_at: v.date().optional(),\n /** The successor pair a bounded concurrent renewal may reuse. */\n successor_access_token_id: v.string().optional(),\n successor_refresh_token_id: v.string().optional(),\n device_info: v.record(v.any()).optional(),\n});\n\n/**\n * Extra attributes captured when a refresh token is issued.\n */\nexport type RefreshTokenIssueOptions = {\n familyId: string;\n expiresAt: string;\n deviceInfo?: DeviceInfo;\n};\n\n/**\n * Persisted refresh-token record + the data layer for refresh tokens.\n *\n * **Role.** Owns every refresh-token read, write, and lifecycle transition.\n * The auth service drives refresh-token state exclusively through this model's\n * named statics and instance methods, so it never hard-codes a column name —\n * which is what makes a snake/camel mismatch (the historical `userId` bug)\n * structurally impossible and lets an override rename or add columns by\n * extending this class and registering it under `config.auth.refreshToken.model`.\n */\nexport class RefreshToken extends Model {\n public static table = \"refresh_tokens\";\n\n public static schema = refreshTokenSchema;\n\n /**\n * The raw signed refresh JWT is a bearer credential — never serialize it.\n * `hidden` only strips it from `toJSON()` / `JSON.stringify(model)` (e.g. a\n * session list returned via `response.json`); `get(\"token\")`,\n * `findByToken` / `findForUser` and every query still read the column.\n */\n public static hidden = [\"token\"];\n\n /**\n * Permanent regardless of the data source default — Mongo's driver default\n * is `\"trash\"`, which would copy live credential material (the JWT itself)\n * into `refresh_tokensTrash` on every `destroy()` (expiry purge,\n * never-expiring purge). A purged refresh token must not survive anywhere.\n */\n public static deleteStrategy = \"permanent\" as const;\n\n /** Token family this row belongs to (rotation / replay grouping). */\n public get familyId(): string {\n return this.get(\"family_id\");\n }\n\n /**\n * Whether the token's `expires_at` is in the past.\n *\n * **Fails closed as of 4.12.0**: a missing or unparseable `expires_at` now\n * counts as expired. It previously answered `false` for both — \"no expiry\n * recorded ⇒ never expires\" — which handed an unlimited life to precisely the\n * malformed rows, including the `Invalid Date` a pre-4.12.0 unparseable\n * `expiresIn` could write. `expires_at` is `required` in the schema, so a row\n * that cannot say when it dies is malformed, not immortal.\n */\n public get isExpired(): boolean {\n const expiresAt = this.get(\"expires_at\");\n\n if (!isUsableExpiry(expiresAt)) return true;\n\n return new Date().getTime() > new Date(expiresAt).getTime();\n }\n\n /**\n * Whether nothing can ever retire this row — an unusable `expires_at`, or a\n * token carrying no `exp` claim. See {@link isNeverExpiring}.\n */\n public get neverExpires(): boolean {\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\n }\n\n /** Whether the token has been revoked. */\n public get isRevoked(): boolean {\n return !!this.get(\"revoked_at\");\n }\n\n /** Whether the token is still usable (not expired and not revoked). */\n public get isValid(): boolean {\n return !this.isExpired && !this.isRevoked;\n }\n\n /**\n * Unconditionally stamp `revoked_at` on this token.\n */\n public async revoke(): Promise<this> {\n return this.merge({ revoked_at: new Date() }).save();\n }\n\n /**\n * Atomically revoke this token ONLY if it is still active. Resolves to `true`\n * when this call performed the revoke, `false` when a concurrent request had\n * already revoked it — the win/lose signal that powers rotation replay\n * detection. Uses a conditional UPDATE so two concurrent rotations of the\n * same token can never both succeed.\n */\n public async revokeIfActive(): Promise<boolean> {\n const modelClass = this.constructor as typeof RefreshToken;\n\n const revokedCount = await modelClass.atomic(\n { id: this.id, revoked_at: null },\n { $set: { revoked_at: new Date() } },\n );\n\n return revokedCount > 0;\n }\n\n /**\n * Touch `last_used_at` without revoking — the non-rotating refresh path.\n */\n public async markAsUsed(): Promise<void> {\n await this.merge({ last_used_at: new Date() }).save();\n }\n\n /**\n * Persist a freshly-signed refresh token for the user.\n */\n public static issue(user: Auth, token: string, options: RefreshTokenIssueOptions) {\n return this.create({\n token,\n user_id: user.id,\n user_type: user.userType,\n family_id: options.familyId,\n expires_at: options.expiresAt,\n device_info: options.deviceInfo\n ? {\n userAgent: options.deviceInfo.userAgent,\n ip: options.deviceInfo.ip,\n deviceId: options.deviceInfo.deviceId,\n }\n : undefined,\n });\n }\n\n /**\n * Find a refresh-token row by its raw token string.\n */\n public static findByToken(token: string): Promise<RefreshToken | null> {\n return this.first({ token });\n }\n\n /**\n * Find a refresh token scoped to a user — used by logout so a caller can only\n * revoke a token that actually belongs to them.\n */\n public static findForUser(user: Auth, token: string): Promise<RefreshToken | null> {\n return this.first({ token, user_id: user.id });\n }\n\n /** Find any member of a family, including an already-revoked legacy member. */\n public static findInFamily(familyId: string): Promise<RefreshToken | null> {\n return this.first({ family_id: familyId });\n }\n\n /**\n * Every refresh lineage for a user, including rotated rows. Logout uses this\n * to bootstrap durable family records before it starts family revocations.\n */\n public static familiesForUser(user: Auth): Promise<RefreshToken[]> {\n return this.query().where({ user_id: user.id, user_type: user.userType }).get();\n }\n\n /**\n * Delete a specific refresh token belonging to the user.\n */\n public static deleteForUser(user: Auth, token: string) {\n return this.delete({ token, user_id: user.id });\n }\n\n /**\n * Active, unexpired sessions for the user, newest first.\n */\n public static activeFor(user: Auth): Promise<RefreshToken[]> {\n return this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .where(\"expires_at\", \">\", new Date())\n .orderBy(\"created_at\", \"desc\")\n .get();\n }\n\n /**\n * Revoke every still-active refresh token for the user, returning the rows\n * that were revoked so the caller can emit a per-token event for each.\n *\n * Rows are fetched BEFORE they are revoked: a bulk `findAndUpdate` keyed on\n * `revoked_at: null` would re-query the same predicate after the update and\n * match nothing, returning an empty set.\n */\n public static async revokeAllFor(user: Auth): Promise<RefreshToken[]> {\n const tokens = await this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .get();\n\n for (const token of tokens) {\n await token.revoke();\n }\n\n return tokens;\n }\n\n /**\n * Revoke every still-active token in a family (rotation breach containment),\n * returning the revoked rows (fetched before revocation — see `revokeAllFor`).\n */\n public static async revokeFamily(familyId: string): Promise<RefreshToken[]> {\n const tokens = await this.query().where({ family_id: familyId, revoked_at: null }).get();\n\n for (const token of tokens) {\n await token.revoke();\n }\n\n return tokens;\n }\n\n /**\n * Revoke the oldest active tokens so at most `max - 1` remain — making room\n * for the about-to-be-issued one. Bounded by `max`, so the per-row loop is\n * small.\n */\n public static async enforceMax(user: Auth, max: number): Promise<void> {\n const activeTokens = await this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .orderBy(\"created_at\", \"asc\")\n .get();\n\n if (activeTokens.length < max) return;\n\n const tokensToRevoke = activeTokens.slice(0, activeTokens.length - max + 1);\n\n for (const token of tokensToRevoke) {\n await token.revoke();\n }\n }\n\n /**\n * Hard-delete every expired refresh token, returning the deleted rows so the\n * caller can emit a per-token event. Runs from the `auth.cleanup` CLI command\n * (a cold batch path).\n */\n public static async purgeExpired(): Promise<RefreshToken[]> {\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\n\n for (const token of expiredTokens) {\n await token.destroy();\n }\n\n return expiredTokens;\n }\n\n /**\n * Every row that can never retire itself — see {@link neverExpires}. A full\n * scan filtered in memory, for the reason given on\n * {@link AccessToken.findNeverExpiring}: neither an `Invalid Date` nor a\n * missing `exp` claim is expressible as a `where`.\n */\n public static async findNeverExpiring(): Promise<RefreshToken[]> {\n const tokens = await this.query().get();\n\n return tokens.filter((token: RefreshToken) => token.neverExpires);\n }\n\n /**\n * Hard-delete every never-expiring row, returning the rows removed.\n *\n * Deleted rather than `revoked_at`-stamped: a revoked row is still a row this\n * table has to carry, and one of the two shapes being removed is a row whose\n * date column cannot be compared at all — leaving it in place keeps a\n * permanently unpurgeable record. Rotation replay-detection is unaffected;\n * these rows can no longer be presented successfully either way.\n */\n public static async purgeNeverExpiring(): Promise<RefreshToken[]> {\n const tokens = await this.findNeverExpiring();\n\n for (const token of tokens) {\n await token.destroy();\n }\n\n return tokens;\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { RefreshToken } from "./refresh-token.model.mjs";
2
2
  import { migrate } from "@warlock.js/cascade";
3
3
 
4
- //#region ../auth/src/models/refresh-token/successor-association.migration.ts
4
+ //#region ../../packages/auth/src/models/refresh-token/successor-association.migration.ts
5
5
  /** Adds nullable successor links for bounded concurrent refresh coalescing. */
6
6
  const RefreshTokenSuccessorAssociationMigration = migrate(RefreshToken, {
7
7
  name: "refreshTokenSuccessorAssociation",
@@ -1 +1 @@
1
- {"version":3,"file":"successor-association.migration.mjs","names":[],"sources":["../../../../../../../../auth/src/models/refresh-token/successor-association.migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\nimport { RefreshToken } from \"./refresh-token.model\";\n\n/** Adds nullable successor links for bounded concurrent refresh coalescing. */\nexport const RefreshTokenSuccessorAssociationMigration = migrate(RefreshToken, {\n name: \"refreshTokenSuccessorAssociation\",\n up() {\n this.uuid(\"successor_access_token_id\").nullable().index();\n this.uuid(\"successor_refresh_token_id\").nullable().index();\n },\n down() {\n this.dropIndex(\"successor_access_token_id\");\n this.dropIndex(\"successor_refresh_token_id\");\n this.dropColumn(\"successor_access_token_id\");\n this.dropColumn(\"successor_refresh_token_id\");\n },\n});\n"],"mappings":";;;;;AAIA,MAAa,4CAA4C,QAAQ,cAAc;CAC7E,MAAM;CACN,KAAK;EACH,KAAK,KAAK,2BAA2B,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM;EACxD,KAAK,KAAK,4BAA4B,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM;CAC3D;CACA,OAAO;EACL,KAAK,UAAU,2BAA2B;EAC1C,KAAK,UAAU,4BAA4B;EAC3C,KAAK,WAAW,2BAA2B;EAC3C,KAAK,WAAW,4BAA4B;CAC9C;AACF,CAAC"}
1
+ {"version":3,"file":"successor-association.migration.mjs","names":[],"sources":["../../../../../../../../../packages/auth/src/models/refresh-token/successor-association.migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\nimport { RefreshToken } from \"./refresh-token.model\";\n\n/** Adds nullable successor links for bounded concurrent refresh coalescing. */\nexport const RefreshTokenSuccessorAssociationMigration = migrate(RefreshToken, {\n name: \"refreshTokenSuccessorAssociation\",\n up() {\n this.uuid(\"successor_access_token_id\").nullable().index();\n this.uuid(\"successor_refresh_token_id\").nullable().index();\n },\n down() {\n this.dropIndex(\"successor_access_token_id\");\n this.dropIndex(\"successor_refresh_token_id\");\n this.dropColumn(\"successor_access_token_id\");\n this.dropColumn(\"successor_refresh_token_id\");\n },\n});\n"],"mappings":";;;;;AAIA,MAAa,4CAA4C,QAAQ,cAAc;CAC7E,MAAM;CACN,KAAK;EACH,KAAK,KAAK,2BAA2B,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM;EACxD,KAAK,KAAK,4BAA4B,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM;CAC3D;CACA,OAAO;EACL,KAAK,UAAU,2BAA2B;EAC1C,KAAK,UAAU,4BAA4B;EAC3C,KAAK,WAAW,2BAA2B;EAC3C,KAAK,WAAW,4BAA4B;CAC9C;AACF,CAAC"}
package/esm/otp/otp.d.mts CHANGED
@@ -2,7 +2,7 @@ import { Auth } from "../models/auth.model.mjs";
2
2
  import { DeviceInfo, LoginResult } from "../contracts/types.mjs";
3
3
  import { ChildModel } from "@warlock.js/cascade";
4
4
 
5
- //#region ../auth/src/otp/otp.d.ts
5
+ //#region ../../packages/auth/src/otp/otp.d.ts
6
6
  /**
7
7
  * Send a 6-digit login code to the account whose `auth.otp.phoneField`
8
8
  * (default `phone`) equals `phone`, through the notifications channel named by
@@ -1 +1 @@
1
- {"version":3,"file":"otp.d.mts","names":[],"sources":["../../../../../../../auth/src/otp/otp.ts"],"mappings":";;;;;;;AAuFA;;;;;;;;;;;;iBAAsB,UAAA,WAAqB,IAAA,EACzC,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,KAAA,UACA,OAAA;EAAW,OAAA;AAAA,IACV,OAAA;;;;;AAAO;AAkCV;;;;;;;iBAAsB,SAAA,WAAoB,IAAA,EACxC,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,KAAA,UACA,IAAA,UACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,CAAA"}
1
+ {"version":3,"file":"otp.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/otp/otp.ts"],"mappings":";;;;;;;AAuFA;;;;;;;;;;;;iBAAsB,UAAA,WAAqB,IAAA,EACzC,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,KAAA,UACA,OAAA;EAAW,OAAA;AAAA,IACV,OAAA;;;;;AAAO;AAkCV;;;;;;;iBAAsB,SAAA,WAAoB,IAAA,EACxC,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,KAAA,UACA,IAAA,UACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,CAAA"}
package/esm/otp/otp.mjs CHANGED
@@ -6,7 +6,7 @@ import { loadOptionalPeer } from "../services/optional-peer.mjs";
6
6
  import { safeEqual } from "../utils/safe-equal.mjs";
7
7
  import { createHmac, randomBytes, randomInt } from "node:crypto";
8
8
 
9
- //#region ../auth/src/otp/otp.ts
9
+ //#region ../../packages/auth/src/otp/otp.ts
10
10
  const CODE_PATTERN = /^\d{6}$/;
11
11
  /** One error for every rejection: unknown phone, no code, wrong, expired, used, over the cap. */
12
12
  function invalidCode() {
@@ -1 +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"}
1
+ {"version":3,"file":"otp.mjs","names":[],"sources":["../../../../../../../../packages/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"}
@@ -3,7 +3,7 @@ import { DeviceInfo, LoginResult } from "../contracts/types.mjs";
3
3
  import { PasskeyOptionsJSON, PasskeyResponseJSON } from "./simplewebauthn.mjs";
4
4
  import { Request } from "@warlock.js/core";
5
5
 
6
- //#region ../auth/src/passkeys/passkey-authentication.d.ts
6
+ //#region ../../packages/auth/src/passkeys/passkey-authentication.d.ts
7
7
  /**
8
8
  * Authentication options for a passkey login. No user is identified yet
9
9
  * (discoverable credentials), so the stored single-use challenge has no owner;
@@ -1 +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"}
1
+ {"version":3,"file":"passkey-authentication.d.mts","names":[],"sources":["../../../../../../../../packages/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"}
@@ -7,7 +7,7 @@ import { assertPasskeyOrigin, storePasskeyChallenge, takePasskeyChallenge } from
7
7
  import { loadSimpleWebAuthn } from "./simplewebauthn.mjs";
8
8
  import { config } from "@warlock.js/core";
9
9
 
10
- //#region ../auth/src/passkeys/passkey-authentication.ts
10
+ //#region ../../packages/auth/src/passkeys/passkey-authentication.ts
11
11
  /** A counter that did not advance past the stored one (when either is non-zero): a cloned authenticator. */
12
12
  function counterRegressed(stored, presented) {
13
13
  if (presented === void 0) return false;
@@ -1 +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"}
1
+ {"version":3,"file":"passkey-authentication.mjs","names":[],"sources":["../../../../../../../../packages/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"}
@@ -2,7 +2,7 @@ import { authConfig } from "../services/auth-config.mjs";
2
2
  import { InvalidPasskeyError } from "../errors/invalid-passkey.error.mjs";
3
3
  import { hashOneTimeToken, oneTimeTokenModel } from "../services/one-time-tokens.mjs";
4
4
 
5
- //#region ../auth/src/passkeys/passkey-challenges.ts
5
+ //#region ../../packages/auth/src/passkeys/passkey-challenges.ts
6
6
  /**
7
7
  * Store a ceremony challenge server-side: SHA-256 hashed, purpose-bound,
8
8
  * expiring (`auth.passkeys.challengeExpiresIn`, default 5m). A registration
@@ -1 +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"}
1
+ {"version":3,"file":"passkey-challenges.mjs","names":[],"sources":["../../../../../../../../packages/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"}
@@ -3,7 +3,7 @@ import { PasskeyCredential } from "../models/passkey-credential/passkey-credenti
3
3
  import { PasskeyOptionsJSON, PasskeyResponseJSON } from "./simplewebauthn.mjs";
4
4
  import { Request } from "@warlock.js/core";
5
5
 
6
- //#region ../auth/src/passkeys/passkey-registration.d.ts
6
+ //#region ../../packages/auth/src/passkeys/passkey-registration.d.ts
7
7
  /**
8
8
  * Registration options for a LOGGED-IN user adding a passkey. The challenge is
9
9
  * stored single-use and bound to this user; the user's existing credentials
@@ -1 +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"}
1
+ {"version":3,"file":"passkey-registration.d.mts","names":[],"sources":["../../../../../../../../packages/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"}
@@ -5,7 +5,7 @@ import "../models/passkey-credential/index.mjs";
5
5
  import { assertPasskeyOrigin, storePasskeyChallenge, takePasskeyChallenge } from "./passkey-challenges.mjs";
6
6
  import { loadSimpleWebAuthn } from "./simplewebauthn.mjs";
7
7
 
8
- //#region ../auth/src/passkeys/passkey-registration.ts
8
+ //#region ../../packages/auth/src/passkeys/passkey-registration.ts
9
9
  /** Account name the authenticator shows: `auth.passkeys.userName`, else email, else id. */
10
10
  function passkeyUserName(user) {
11
11
  const configured = authConfig.passkeys.settings().userName;
@@ -1 +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"}
1
+ {"version":3,"file":"passkey-registration.mjs","names":[],"sources":["../../../../../../../../packages/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"}
@@ -1,4 +1,4 @@
1
- //#region ../auth/src/passkeys/simplewebauthn.d.ts
1
+ //#region ../../packages/auth/src/passkeys/simplewebauthn.d.ts
2
2
  /** The browser's JSON-serialized credential (`startRegistration` / `startAuthentication` result). */
3
3
  type PasskeyResponseJSON = {
4
4
  id: string;
@@ -1 +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"}
1
+ {"version":3,"file":"simplewebauthn.d.mts","names":[],"sources":["../../../../../../../../packages/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"}
@@ -1,6 +1,6 @@
1
1
  import { loadOptionalPeer } from "../services/optional-peer.mjs";
2
2
 
3
- //#region ../auth/src/passkeys/simplewebauthn.ts
3
+ //#region ../../packages/auth/src/passkeys/simplewebauthn.ts
4
4
  /** The `warlock add` feature that installs `@simplewebauthn/server`. */
5
5
  const PASSKEYS_FEATURE = "auth-passkeys";
6
6
  /** Load `@simplewebauthn/server`, or throw `AuthProviderSdkMissingError` naming `warlock add auth-passkeys`. */
@@ -1 +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"}
1
+ {"version":3,"file":"simplewebauthn.mjs","names":[],"sources":["../../../../../../../../packages/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"}
@@ -1,7 +1,7 @@
1
1
  import { AppleProviderConfig, AuthProvider, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
2
2
  import { JoseModule } from "./google-provider.mjs";
3
3
 
4
- //#region ../auth/src/providers/apple-provider.d.ts
4
+ //#region ../../packages/auth/src/providers/apple-provider.d.ts
5
5
  /** The `warlock add` feature that installs `jose`. */
6
6
  declare const APPLE_AUTH_FEATURE = "auth-apple";
7
7
  type AppleSignJwtBuilder = {
@@ -1 +1 @@
1
- {"version":3,"file":"apple-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/apple-provider.ts"],"mappings":";;;;;cAqBa,kBAAA;AAAA,KAER,mBAAA;EACH,SAAA,GAAY,MAAA,aAAmB,mBAAA;EAC/B,WAAA,QAAmB,mBAAA;EACnB,iBAAA,GAAoB,IAAA,aAAiB,mBAAA;EACrC,WAAA,GAAc,QAAA,aAAqB,mBAAA;EACnC,UAAA,GAAa,OAAA,aAAoB,mBAAA;EACjC,IAAA,GAAO,GAAA,cAAiB,OAAA;AAAA;;;;;;KAQd,eAAA,GAAkB,UAAA;EAC5B,WAAA,GAAc,KAAA,UAAe,GAAA,aAAgB,OAAA;EAC7C,OAAA,OAAc,OAAA,EAAS,MAAA;IACrB,kBAAA,GAAqB,MAAA,EAAQ,MAAA,sBAA4B,mBAAA;EAAA;AAAA;;;;;;;;;;;;;;;;AAX5B;AAQjC;;;;;;;;;;;;cAsCa,aAAA,YAAyB,YAAA;EAAA,iBAKA,QAAA;EAAA,SAJpB,IAAA;EArChB;EAAA,SAuCgB,YAAA;cAEoB,QAAA,EAAU,mBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAqBlB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAlEb;EAAA,QAkGT,YAAA;EAlGgE;EAAA,QAqHhE,YAAA;EAlFH;EAAA,QAyHG,aAAA;AAAA"}
1
+ {"version":3,"file":"apple-provider.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/providers/apple-provider.ts"],"mappings":";;;;;cAqBa,kBAAA;AAAA,KAER,mBAAA;EACH,SAAA,GAAY,MAAA,aAAmB,mBAAA;EAC/B,WAAA,QAAmB,mBAAA;EACnB,iBAAA,GAAoB,IAAA,aAAiB,mBAAA;EACrC,WAAA,GAAc,QAAA,aAAqB,mBAAA;EACnC,UAAA,GAAa,OAAA,aAAoB,mBAAA;EACjC,IAAA,GAAO,GAAA,cAAiB,OAAA;AAAA;;;;;;KAQd,eAAA,GAAkB,UAAA;EAC5B,WAAA,GAAc,KAAA,UAAe,GAAA,aAAgB,OAAA;EAC7C,OAAA,OAAc,OAAA,EAAS,MAAA;IACrB,kBAAA,GAAqB,MAAA,EAAQ,MAAA,sBAA4B,mBAAA;EAAA;AAAA;;;;;;;;;;;;;;;;AAX5B;AAQjC;;;;;;;;;;;;cAsCa,aAAA,YAAyB,YAAA;EAAA,iBAKA,QAAA;EAAA,SAJpB,IAAA;EArChB;EAAA,SAuCgB,YAAA;cAEoB,QAAA,EAAU,mBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAqBlB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAlEb;EAAA,QAkGT,YAAA;EAlGgE;EAAA,QAqHhE,YAAA;EAlFH;EAAA,QAyHG,aAAA;AAAA"}
@@ -2,7 +2,7 @@ import { InvalidProviderCallbackError } from "../errors/invalid-provider-callbac
2
2
  import { loadOptionalPeer } from "../services/optional-peer.mjs";
3
3
  import { pkceChallenge } from "./google-provider.mjs";
4
4
 
5
- //#region ../auth/src/providers/apple-provider.ts
5
+ //#region ../../packages/auth/src/providers/apple-provider.ts
6
6
  const AUTHORIZATION_ENDPOINT = "https://appleid.apple.com/auth/authorize";
7
7
  const TOKEN_ENDPOINT = "https://appleid.apple.com/auth/token";
8
8
  const JWKS_URI = "https://appleid.apple.com/auth/keys";
@@ -1 +1 @@
1
- {"version":3,"file":"apple-provider.mjs","names":[],"sources":["../../../../../../../auth/src/providers/apple-provider.ts"],"sourcesContent":["import type {\n AppleProviderConfig,\n AuthProvider,\n ProviderAuthorizationState,\n ProviderCallbackParams,\n ProviderProfile,\n} from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { loadOptionalPeer } from \"../services/optional-peer\";\nimport type { JoseModule } from \"./google-provider\";\nimport { pkceChallenge } from \"./google-provider\";\n\nconst AUTHORIZATION_ENDPOINT = \"https://appleid.apple.com/auth/authorize\";\nconst TOKEN_ENDPOINT = \"https://appleid.apple.com/auth/token\";\nconst JWKS_URI = \"https://appleid.apple.com/auth/keys\";\nconst ISSUER = \"https://appleid.apple.com\";\nconst DEFAULT_SCOPES = [\"name\", \"email\"];\n/** Apple's own recommendation for a per-request client secret: minutes, not the 6-month maximum it allows. */\nconst CLIENT_SECRET_TTL_SECONDS = 5 * 60;\n\n/** The `warlock add` feature that installs `jose`. */\nexport const APPLE_AUTH_FEATURE = \"auth-apple\";\n\ntype AppleSignJwtBuilder = {\n setIssuer: (issuer: string) => AppleSignJwtBuilder;\n setIssuedAt: () => AppleSignJwtBuilder;\n setExpirationTime: (time: number) => AppleSignJwtBuilder;\n setAudience: (audience: string) => AppleSignJwtBuilder;\n setSubject: (subject: string) => AppleSignJwtBuilder;\n sign: (key: unknown) => Promise<string>;\n};\n\n/**\n * The slice of `jose` the Apple provider uses on top of {@link JoseModule} —\n * `importPKCS8` and `SignJWT` sign the ES256 client-secret JWT Apple requires\n * instead of a static client secret.\n */\nexport type AppleJoseModule = JoseModule & {\n importPKCS8: (pkcs8: string, alg: string) => Promise<unknown>;\n SignJWT: new (payload: Record<string, unknown>) => {\n setProtectedHeader: (header: Record<string, unknown>) => AppleSignJwtBuilder;\n };\n};\n\n/** Apple's key set, created once per process; `jose` caches the fetched JWKS inside it. */\nlet appleKeySet: unknown;\n\n/**\n * Apple (\"Sign in with Apple\") is OpenID Connect over the authorization code\n * flow with PKCE (S256), `state` and `nonce`, the same shape as\n * {@link GoogleProvider} — with two differences that come straight from\n * Apple's own docs (this package has no Apple developer account, so nothing\n * here is proven against a live exchange):\n *\n * 1. Apple has no static client secret. The \"secret\" the token endpoint wants\n * is a fresh ES256 JWT (`iss` = team id, `sub` = client id, `aud` =\n * `https://appleid.apple.com`, signed with the app's private key and the\n * key id as `kid`) — signed once per callback here rather than cached, to\n * avoid ever presenting a stale one.\n * 2. Requesting `name`/`email` (the default scopes) forces\n * `response_mode=form_post`: Apple POSTs `code`, `state` and, on the\n * user's FIRST authorization only, a `user` field — a JSON string with\n * `{ name: { firstName, lastName }, email } ` that never comes back again\n * on a later login. The callback route must therefore accept a POST body,\n * not just a query string, and `request.input` must read that body the\n * same way it already reads a GET query for the other providers.\n *\n * The id_token is verified with `jose` against Apple's JWKS (signature,\n * `iss`, `aud` = the client id, `exp`), then its `nonce` is compared. Apple is\n * also documented to send `email_verified` (and `is_private_email`) as the\n * STRING `\"true\"`/`\"false\"` rather than a JSON boolean on some responses, so\n * both forms are accepted here — treating only the unambiguous `\"false\"` (or\n * `false`) as unverified. A private-relay address (`@privaterelay.appleid.com`)\n * is still a real, working forwarding address and is accepted as any other.\n */\nexport class AppleProvider implements AuthProvider {\n public readonly name = \"apple\";\n /** Apple's default scopes (`name`, `email`) force `response_mode=form_post`. */\n public readonly callbackMode = \"form_post\";\n\n public constructor(private readonly settings: AppleProviderConfig) {}\n\n public authorizationUrl(state: ProviderAuthorizationState): string {\n const scopes = this.settings.scopes ?? DEFAULT_SCOPES;\n const url = new URL(AUTHORIZATION_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", scopes.join(\" \"));\n url.searchParams.set(\"state\", state.state);\n url.searchParams.set(\"nonce\", state.nonce);\n url.searchParams.set(\"code_challenge\", pkceChallenge(state.codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n // Apple requires form_post whenever name/email scopes are requested.\n if (scopes.includes(\"name\") || scopes.includes(\"email\")) {\n url.searchParams.set(\"response_mode\", \"form_post\");\n }\n\n return url.toString();\n }\n\n public async handleCallback({\n query,\n expected,\n }: ProviderCallbackParams): Promise<ProviderProfile> {\n // Load the peer first: a missing SDK fails loudly before any network call.\n const jose = await loadOptionalPeer<AppleJoseModule>(\"jose\", APPLE_AUTH_FEATURE);\n\n const code = query.code;\n\n if (typeof code !== \"string\" || code.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-code\");\n }\n\n const idToken = await this.exchangeCode(jose, code, expected.codeVerifier);\n const claims = await this.verifyIdToken(jose, idToken);\n\n if (typeof claims.nonce !== \"string\" || claims.nonce !== expected.nonce) {\n throw new InvalidProviderCallbackError(\"nonce-mismatch\");\n }\n\n if (typeof claims.sub !== \"string\" || claims.sub.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-subject\");\n }\n\n return {\n provider: this.name,\n providerUserId: claims.sub,\n email: typeof claims.email === \"string\" ? claims.email : undefined,\n emailVerified: isVerifiedFlag(claims.email_verified),\n name: nameFromUserField(query.user),\n raw: claims,\n };\n }\n\n /** Sign a fresh ES256 client-secret JWT — Apple has no static one. */\n private async clientSecret(jose: AppleJoseModule): Promise<string> {\n const key = await jose.importPKCS8(this.settings.privateKey, \"ES256\");\n const now = Math.floor(Date.now() / 1000);\n\n try {\n return await new jose.SignJWT({})\n .setProtectedHeader({ alg: \"ES256\", kid: this.settings.keyId })\n .setIssuer(this.settings.teamId)\n .setIssuedAt()\n .setExpirationTime(now + CLIENT_SECRET_TTL_SECONDS)\n .setAudience(ISSUER)\n .setSubject(this.settings.clientId)\n .sign(key);\n } catch (error) {\n throw new InvalidProviderCallbackError(\"client-secret-signing-failed\", error);\n }\n }\n\n /** Redeem the authorization code (with the PKCE verifier) for an id_token. */\n private async exchangeCode(\n jose: AppleJoseModule,\n code: string,\n codeVerifier: string,\n ): Promise<string> {\n const clientSecret = await this.clientSecret(jose);\n let body: { id_token?: unknown };\n\n try {\n const response = await fetch(TOKEN_ENDPOINT, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n code,\n client_id: this.settings.clientId,\n client_secret: clientSecret,\n redirect_uri: this.settings.redirectUri,\n grant_type: \"authorization_code\",\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Apple token endpoint answered ${response.status}`);\n }\n\n body = (await response.json()) as { id_token?: unknown };\n } catch (error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", error);\n }\n\n if (typeof body.id_token !== \"string\") {\n throw new InvalidProviderCallbackError(\"missing-id-token\");\n }\n\n return body.id_token;\n }\n\n /** Verify signature, issuer, audience and expiry; resolve the claims. */\n private async verifyIdToken(\n jose: AppleJoseModule,\n idToken: string,\n ): Promise<Record<string, unknown>> {\n appleKeySet ??= jose.createRemoteJWKSet(new URL(JWKS_URI));\n\n try {\n const { payload } = await jose.jwtVerify(idToken, appleKeySet, {\n issuer: [ISSUER],\n audience: this.settings.clientId,\n });\n\n return payload;\n } catch (error) {\n throw new InvalidProviderCallbackError(\"invalid-id-token\", error);\n }\n }\n}\n\n/** Apple's `email_verified` arrives as a JSON boolean on some responses, a `\"true\"`/`\"false\"` string on others. */\nfunction isVerifiedFlag(value: unknown): boolean {\n return value === true || value === \"true\";\n}\n\n/**\n * Apple sends the account name only once, on the FIRST authorization, as a\n * `user` form field holding a JSON string: `{\"name\":{\"firstName\",\"lastName\"}}`.\n * Every later login omits it entirely — callers that need the name must save\n * it themselves the first time.\n */\nfunction nameFromUserField(user: unknown): string | undefined {\n if (typeof user !== \"string\" || user.length === 0) return undefined;\n\n try {\n const parsed = JSON.parse(user) as { name?: { firstName?: string; lastName?: string } };\n const first = parsed.name?.firstName;\n const last = parsed.name?.lastName;\n const name = [first, last].filter((part): part is string => Boolean(part)).join(\" \");\n\n return name.length > 0 ? name : undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"apple-provider.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/providers/apple-provider.ts"],"sourcesContent":["import type {\n AppleProviderConfig,\n AuthProvider,\n ProviderAuthorizationState,\n ProviderCallbackParams,\n ProviderProfile,\n} from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { loadOptionalPeer } from \"../services/optional-peer\";\nimport type { JoseModule } from \"./google-provider\";\nimport { pkceChallenge } from \"./google-provider\";\n\nconst AUTHORIZATION_ENDPOINT = \"https://appleid.apple.com/auth/authorize\";\nconst TOKEN_ENDPOINT = \"https://appleid.apple.com/auth/token\";\nconst JWKS_URI = \"https://appleid.apple.com/auth/keys\";\nconst ISSUER = \"https://appleid.apple.com\";\nconst DEFAULT_SCOPES = [\"name\", \"email\"];\n/** Apple's own recommendation for a per-request client secret: minutes, not the 6-month maximum it allows. */\nconst CLIENT_SECRET_TTL_SECONDS = 5 * 60;\n\n/** The `warlock add` feature that installs `jose`. */\nexport const APPLE_AUTH_FEATURE = \"auth-apple\";\n\ntype AppleSignJwtBuilder = {\n setIssuer: (issuer: string) => AppleSignJwtBuilder;\n setIssuedAt: () => AppleSignJwtBuilder;\n setExpirationTime: (time: number) => AppleSignJwtBuilder;\n setAudience: (audience: string) => AppleSignJwtBuilder;\n setSubject: (subject: string) => AppleSignJwtBuilder;\n sign: (key: unknown) => Promise<string>;\n};\n\n/**\n * The slice of `jose` the Apple provider uses on top of {@link JoseModule} —\n * `importPKCS8` and `SignJWT` sign the ES256 client-secret JWT Apple requires\n * instead of a static client secret.\n */\nexport type AppleJoseModule = JoseModule & {\n importPKCS8: (pkcs8: string, alg: string) => Promise<unknown>;\n SignJWT: new (payload: Record<string, unknown>) => {\n setProtectedHeader: (header: Record<string, unknown>) => AppleSignJwtBuilder;\n };\n};\n\n/** Apple's key set, created once per process; `jose` caches the fetched JWKS inside it. */\nlet appleKeySet: unknown;\n\n/**\n * Apple (\"Sign in with Apple\") is OpenID Connect over the authorization code\n * flow with PKCE (S256), `state` and `nonce`, the same shape as\n * {@link GoogleProvider} — with two differences that come straight from\n * Apple's own docs (this package has no Apple developer account, so nothing\n * here is proven against a live exchange):\n *\n * 1. Apple has no static client secret. The \"secret\" the token endpoint wants\n * is a fresh ES256 JWT (`iss` = team id, `sub` = client id, `aud` =\n * `https://appleid.apple.com`, signed with the app's private key and the\n * key id as `kid`) — signed once per callback here rather than cached, to\n * avoid ever presenting a stale one.\n * 2. Requesting `name`/`email` (the default scopes) forces\n * `response_mode=form_post`: Apple POSTs `code`, `state` and, on the\n * user's FIRST authorization only, a `user` field — a JSON string with\n * `{ name: { firstName, lastName }, email } ` that never comes back again\n * on a later login. The callback route must therefore accept a POST body,\n * not just a query string, and `request.input` must read that body the\n * same way it already reads a GET query for the other providers.\n *\n * The id_token is verified with `jose` against Apple's JWKS (signature,\n * `iss`, `aud` = the client id, `exp`), then its `nonce` is compared. Apple is\n * also documented to send `email_verified` (and `is_private_email`) as the\n * STRING `\"true\"`/`\"false\"` rather than a JSON boolean on some responses, so\n * both forms are accepted here — treating only the unambiguous `\"false\"` (or\n * `false`) as unverified. A private-relay address (`@privaterelay.appleid.com`)\n * is still a real, working forwarding address and is accepted as any other.\n */\nexport class AppleProvider implements AuthProvider {\n public readonly name = \"apple\";\n /** Apple's default scopes (`name`, `email`) force `response_mode=form_post`. */\n public readonly callbackMode = \"form_post\";\n\n public constructor(private readonly settings: AppleProviderConfig) {}\n\n public authorizationUrl(state: ProviderAuthorizationState): string {\n const scopes = this.settings.scopes ?? DEFAULT_SCOPES;\n const url = new URL(AUTHORIZATION_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", scopes.join(\" \"));\n url.searchParams.set(\"state\", state.state);\n url.searchParams.set(\"nonce\", state.nonce);\n url.searchParams.set(\"code_challenge\", pkceChallenge(state.codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n // Apple requires form_post whenever name/email scopes are requested.\n if (scopes.includes(\"name\") || scopes.includes(\"email\")) {\n url.searchParams.set(\"response_mode\", \"form_post\");\n }\n\n return url.toString();\n }\n\n public async handleCallback({\n query,\n expected,\n }: ProviderCallbackParams): Promise<ProviderProfile> {\n // Load the peer first: a missing SDK fails loudly before any network call.\n const jose = await loadOptionalPeer<AppleJoseModule>(\"jose\", APPLE_AUTH_FEATURE);\n\n const code = query.code;\n\n if (typeof code !== \"string\" || code.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-code\");\n }\n\n const idToken = await this.exchangeCode(jose, code, expected.codeVerifier);\n const claims = await this.verifyIdToken(jose, idToken);\n\n if (typeof claims.nonce !== \"string\" || claims.nonce !== expected.nonce) {\n throw new InvalidProviderCallbackError(\"nonce-mismatch\");\n }\n\n if (typeof claims.sub !== \"string\" || claims.sub.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-subject\");\n }\n\n return {\n provider: this.name,\n providerUserId: claims.sub,\n email: typeof claims.email === \"string\" ? claims.email : undefined,\n emailVerified: isVerifiedFlag(claims.email_verified),\n name: nameFromUserField(query.user),\n raw: claims,\n };\n }\n\n /** Sign a fresh ES256 client-secret JWT — Apple has no static one. */\n private async clientSecret(jose: AppleJoseModule): Promise<string> {\n const key = await jose.importPKCS8(this.settings.privateKey, \"ES256\");\n const now = Math.floor(Date.now() / 1000);\n\n try {\n return await new jose.SignJWT({})\n .setProtectedHeader({ alg: \"ES256\", kid: this.settings.keyId })\n .setIssuer(this.settings.teamId)\n .setIssuedAt()\n .setExpirationTime(now + CLIENT_SECRET_TTL_SECONDS)\n .setAudience(ISSUER)\n .setSubject(this.settings.clientId)\n .sign(key);\n } catch (error) {\n throw new InvalidProviderCallbackError(\"client-secret-signing-failed\", error);\n }\n }\n\n /** Redeem the authorization code (with the PKCE verifier) for an id_token. */\n private async exchangeCode(\n jose: AppleJoseModule,\n code: string,\n codeVerifier: string,\n ): Promise<string> {\n const clientSecret = await this.clientSecret(jose);\n let body: { id_token?: unknown };\n\n try {\n const response = await fetch(TOKEN_ENDPOINT, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n code,\n client_id: this.settings.clientId,\n client_secret: clientSecret,\n redirect_uri: this.settings.redirectUri,\n grant_type: \"authorization_code\",\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Apple token endpoint answered ${response.status}`);\n }\n\n body = (await response.json()) as { id_token?: unknown };\n } catch (error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", error);\n }\n\n if (typeof body.id_token !== \"string\") {\n throw new InvalidProviderCallbackError(\"missing-id-token\");\n }\n\n return body.id_token;\n }\n\n /** Verify signature, issuer, audience and expiry; resolve the claims. */\n private async verifyIdToken(\n jose: AppleJoseModule,\n idToken: string,\n ): Promise<Record<string, unknown>> {\n appleKeySet ??= jose.createRemoteJWKSet(new URL(JWKS_URI));\n\n try {\n const { payload } = await jose.jwtVerify(idToken, appleKeySet, {\n issuer: [ISSUER],\n audience: this.settings.clientId,\n });\n\n return payload;\n } catch (error) {\n throw new InvalidProviderCallbackError(\"invalid-id-token\", error);\n }\n }\n}\n\n/** Apple's `email_verified` arrives as a JSON boolean on some responses, a `\"true\"`/`\"false\"` string on others. */\nfunction isVerifiedFlag(value: unknown): boolean {\n return value === true || value === \"true\";\n}\n\n/**\n * Apple sends the account name only once, on the FIRST authorization, as a\n * `user` form field holding a JSON string: `{\"name\":{\"firstName\",\"lastName\"}}`.\n * Every later login omits it entirely — callers that need the name must save\n * it themselves the first time.\n */\nfunction nameFromUserField(user: unknown): string | undefined {\n if (typeof user !== \"string\" || user.length === 0) return undefined;\n\n try {\n const parsed = JSON.parse(user) as { name?: { firstName?: string; lastName?: string } };\n const first = parsed.name?.firstName;\n const last = parsed.name?.lastName;\n const name = [first, last].filter((part): part is string => Boolean(part)).join(\" \");\n\n return name.length > 0 ? name : undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { AuthProvider, DiscordProviderConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
2
2
 
3
- //#region ../auth/src/providers/discord-provider.d.ts
3
+ //#region ../../packages/auth/src/providers/discord-provider.d.ts
4
4
  /**
5
5
  * Discord sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
6
6
  * Like GitHub, Discord is not OpenID Connect — there is no `id_token`; the
@@ -1 +1 @@
1
- {"version":3,"file":"discord-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/discord-provider.ts"],"mappings":";;;;;AA8BA;;;;cAAa,eAAA,YAA2B,YAAA;EAAA,iBAGF,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,qBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAclB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAtBE;EAAA,QAkDxB,YAAA;EAAA,QAiCA,SAAA;AAAA"}
1
+ {"version":3,"file":"discord-provider.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/providers/discord-provider.ts"],"mappings":";;;;;AA8BA;;;;cAAa,eAAA,YAA2B,YAAA;EAAA,iBAGF,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,qBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAclB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAtBE;EAAA,QAkDxB,YAAA;EAAA,QAiCA,SAAA;AAAA"}
@@ -1,7 +1,7 @@
1
1
  import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
2
2
  import { pkceChallenge } from "./google-provider.mjs";
3
3
 
4
- //#region ../auth/src/providers/discord-provider.ts
4
+ //#region ../../packages/auth/src/providers/discord-provider.ts
5
5
  const AUTHORIZATION_ENDPOINT = "https://discord.com/oauth2/authorize";
6
6
  const TOKEN_ENDPOINT = "https://discord.com/api/oauth2/token";
7
7
  const USER_ENDPOINT = "https://discord.com/api/users/@me";
@@ -1 +1 @@
1
- {"version":3,"file":"discord-provider.mjs","names":[],"sources":["../../../../../../../auth/src/providers/discord-provider.ts"],"sourcesContent":["import type {\n AuthProvider,\n DiscordProviderConfig,\n ProviderAuthorizationState,\n ProviderCallbackParams,\n ProviderProfile,\n} from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { pkceChallenge } from \"./google-provider\";\n\nconst AUTHORIZATION_ENDPOINT = \"https://discord.com/oauth2/authorize\";\nconst TOKEN_ENDPOINT = \"https://discord.com/api/oauth2/token\";\nconst USER_ENDPOINT = \"https://discord.com/api/users/@me\";\nconst DEFAULT_SCOPES = [\"identify\", \"email\"];\n\ntype DiscordUser = {\n id: string;\n username: string;\n global_name?: string | null;\n avatar?: string | null;\n email?: string | null;\n verified?: boolean;\n};\n\n/**\n * Discord sign-in: plain OAuth 2 authorization code flow with PKCE (S256).\n * Like GitHub, Discord is not OpenID Connect — there is no `id_token`; the\n * access token is redeemed once for `/users/@me` over a direct HTTPS call.\n * No vendor SDK is involved.\n */\nexport class DiscordProvider implements AuthProvider {\n public readonly name = \"discord\";\n\n public constructor(private readonly settings: DiscordProviderConfig) {}\n\n public authorizationUrl(state: ProviderAuthorizationState): string {\n const url = new URL(AUTHORIZATION_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", (this.settings.scopes ?? DEFAULT_SCOPES).join(\" \"));\n url.searchParams.set(\"state\", state.state);\n url.searchParams.set(\"code_challenge\", pkceChallenge(state.codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n return url.toString();\n }\n\n public async handleCallback({\n query,\n expected,\n }: ProviderCallbackParams): Promise<ProviderProfile> {\n const code = query.code;\n\n if (typeof code !== \"string\" || code.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-code\");\n }\n\n const accessToken = await this.exchangeCode(code, expected.codeVerifier);\n const user = await this.fetchUser(accessToken);\n\n if (typeof user.id !== \"string\" || user.id.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-subject\");\n }\n\n return {\n provider: this.name,\n providerUserId: user.id,\n email: user.email ?? undefined,\n emailVerified: user.verified === true,\n name: user.global_name ?? user.username,\n avatar: user.avatar\n ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`\n : undefined,\n raw: user,\n };\n }\n\n /** Redeem the authorization code (with the PKCE verifier) for an access token. */\n private async exchangeCode(code: string, codeVerifier: string): Promise<string> {\n let body: { access_token?: unknown };\n\n try {\n const response = await fetch(TOKEN_ENDPOINT, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n code,\n client_id: this.settings.clientId,\n client_secret: this.settings.clientSecret,\n redirect_uri: this.settings.redirectUri,\n grant_type: \"authorization_code\",\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Discord token endpoint answered ${response.status}`);\n }\n\n body = (await response.json()) as { access_token?: unknown };\n } catch (error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", error);\n }\n\n if (typeof body.access_token !== \"string\" || body.access_token.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-access-token\");\n }\n\n return body.access_token;\n }\n\n private async fetchUser(accessToken: string): Promise<DiscordUser> {\n try {\n const response = await fetch(USER_ENDPOINT, {\n headers: { authorization: `Bearer ${accessToken}` },\n });\n\n if (!response.ok) {\n throw new Error(`Discord user endpoint answered ${response.status}`);\n }\n\n return (await response.json()) as DiscordUser;\n } catch (error) {\n throw new InvalidProviderCallbackError(\"profile-fetch-failed\", error);\n }\n }\n}\n"],"mappings":";;;;AAUA,MAAM,yBAAyB;AAC/B,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;AACtB,MAAM,iBAAiB,CAAC,YAAY,OAAO;;;;;;;AAiB3C,IAAa,kBAAb,MAAqD;CAGnD,AAAO,YAAY,AAAiB,UAAiC;EAAjC;cAFb;CAE+C;CAEtE,AAAO,iBAAiB,OAA2C;EACjE,MAAM,MAAM,IAAI,IAAI,sBAAsB;EAE1C,IAAI,aAAa,IAAI,aAAa,KAAK,SAAS,QAAQ;EACxD,IAAI,aAAa,IAAI,gBAAgB,KAAK,SAAS,WAAW;EAC9D,IAAI,aAAa,IAAI,iBAAiB,MAAM;EAC5C,IAAI,aAAa,IAAI,UAAU,KAAK,SAAS,UAAU,eAAc,CAAE,KAAK,GAAG,CAAC;EAChF,IAAI,aAAa,IAAI,SAAS,MAAM,KAAK;EACzC,IAAI,aAAa,IAAI,kBAAkB,cAAc,MAAM,YAAY,CAAC;EACxE,IAAI,aAAa,IAAI,yBAAyB,MAAM;EAEpD,OAAO,IAAI,SAAS;CACtB;CAEA,MAAa,eAAe,EAC1B,OACA,YACmD;EACnD,MAAM,OAAO,MAAM;EAEnB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,6BAA6B,cAAc;EAGvD,MAAM,cAAc,MAAM,KAAK,aAAa,MAAM,SAAS,YAAY;EACvE,MAAM,OAAO,MAAM,KAAK,UAAU,WAAW;EAE7C,IAAI,OAAO,KAAK,OAAO,YAAY,KAAK,GAAG,WAAW,GACpD,MAAM,IAAI,6BAA6B,iBAAiB;EAG1D,OAAO;GACL,UAAU,KAAK;GACf,gBAAgB,KAAK;GACrB,OAAO,KAAK,SAAS;GACrB,eAAe,KAAK,aAAa;GACjC,MAAM,KAAK,eAAe,KAAK;GAC/B,QAAQ,KAAK,SACT,sCAAsC,KAAK,GAAG,GAAG,KAAK,OAAO,QAC7D;GACJ,KAAK;EACP;CACF;;CAGA,MAAc,aAAa,MAAc,cAAuC;EAC9E,IAAI;EAEJ,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,gBAAgB;IAC3C,QAAQ;IACR,SAAS,EAAE,gBAAgB,oCAAoC;IAC/D,MAAM,IAAI,gBAAgB;KACxB;KACA,WAAW,KAAK,SAAS;KACzB,eAAe,KAAK,SAAS;KAC7B,cAAc,KAAK,SAAS;KAC5B,YAAY;KACZ,eAAe;IACjB,CAAC;GACH,CAAC;GAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,mCAAmC,SAAS,QAAQ;GAGtE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;EAEA,IAAI,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,WAAW,GACxE,MAAM,IAAI,6BAA6B,sBAAsB;EAG/D,OAAO,KAAK;CACd;CAEA,MAAc,UAAU,aAA2C;EACjE,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,eAAe,EAC1C,SAAS,EAAE,eAAe,UAAU,cAAc,EACpD,CAAC;GAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,kCAAkC,SAAS,QAAQ;GAGrE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;CACF;AACF"}
1
+ {"version":3,"file":"discord-provider.mjs","names":[],"sources":["../../../../../../../../packages/auth/src/providers/discord-provider.ts"],"sourcesContent":["import type {\n AuthProvider,\n DiscordProviderConfig,\n ProviderAuthorizationState,\n ProviderCallbackParams,\n ProviderProfile,\n} from \"../contracts/providers\";\nimport { InvalidProviderCallbackError } from \"../errors/invalid-provider-callback.error\";\nimport { pkceChallenge } from \"./google-provider\";\n\nconst AUTHORIZATION_ENDPOINT = \"https://discord.com/oauth2/authorize\";\nconst TOKEN_ENDPOINT = \"https://discord.com/api/oauth2/token\";\nconst USER_ENDPOINT = \"https://discord.com/api/users/@me\";\nconst DEFAULT_SCOPES = [\"identify\", \"email\"];\n\ntype DiscordUser = {\n id: string;\n username: string;\n global_name?: string | null;\n avatar?: string | null;\n email?: string | null;\n verified?: boolean;\n};\n\n/**\n * Discord sign-in: plain OAuth 2 authorization code flow with PKCE (S256).\n * Like GitHub, Discord is not OpenID Connect — there is no `id_token`; the\n * access token is redeemed once for `/users/@me` over a direct HTTPS call.\n * No vendor SDK is involved.\n */\nexport class DiscordProvider implements AuthProvider {\n public readonly name = \"discord\";\n\n public constructor(private readonly settings: DiscordProviderConfig) {}\n\n public authorizationUrl(state: ProviderAuthorizationState): string {\n const url = new URL(AUTHORIZATION_ENDPOINT);\n\n url.searchParams.set(\"client_id\", this.settings.clientId);\n url.searchParams.set(\"redirect_uri\", this.settings.redirectUri);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"scope\", (this.settings.scopes ?? DEFAULT_SCOPES).join(\" \"));\n url.searchParams.set(\"state\", state.state);\n url.searchParams.set(\"code_challenge\", pkceChallenge(state.codeVerifier));\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n\n return url.toString();\n }\n\n public async handleCallback({\n query,\n expected,\n }: ProviderCallbackParams): Promise<ProviderProfile> {\n const code = query.code;\n\n if (typeof code !== \"string\" || code.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-code\");\n }\n\n const accessToken = await this.exchangeCode(code, expected.codeVerifier);\n const user = await this.fetchUser(accessToken);\n\n if (typeof user.id !== \"string\" || user.id.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-subject\");\n }\n\n return {\n provider: this.name,\n providerUserId: user.id,\n email: user.email ?? undefined,\n emailVerified: user.verified === true,\n name: user.global_name ?? user.username,\n avatar: user.avatar\n ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`\n : undefined,\n raw: user,\n };\n }\n\n /** Redeem the authorization code (with the PKCE verifier) for an access token. */\n private async exchangeCode(code: string, codeVerifier: string): Promise<string> {\n let body: { access_token?: unknown };\n\n try {\n const response = await fetch(TOKEN_ENDPOINT, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n code,\n client_id: this.settings.clientId,\n client_secret: this.settings.clientSecret,\n redirect_uri: this.settings.redirectUri,\n grant_type: \"authorization_code\",\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Discord token endpoint answered ${response.status}`);\n }\n\n body = (await response.json()) as { access_token?: unknown };\n } catch (error) {\n throw new InvalidProviderCallbackError(\"code-exchange-failed\", error);\n }\n\n if (typeof body.access_token !== \"string\" || body.access_token.length === 0) {\n throw new InvalidProviderCallbackError(\"missing-access-token\");\n }\n\n return body.access_token;\n }\n\n private async fetchUser(accessToken: string): Promise<DiscordUser> {\n try {\n const response = await fetch(USER_ENDPOINT, {\n headers: { authorization: `Bearer ${accessToken}` },\n });\n\n if (!response.ok) {\n throw new Error(`Discord user endpoint answered ${response.status}`);\n }\n\n return (await response.json()) as DiscordUser;\n } catch (error) {\n throw new InvalidProviderCallbackError(\"profile-fetch-failed\", error);\n }\n }\n}\n"],"mappings":";;;;AAUA,MAAM,yBAAyB;AAC/B,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;AACtB,MAAM,iBAAiB,CAAC,YAAY,OAAO;;;;;;;AAiB3C,IAAa,kBAAb,MAAqD;CAGnD,AAAO,YAAY,AAAiB,UAAiC;EAAjC;cAFb;CAE+C;CAEtE,AAAO,iBAAiB,OAA2C;EACjE,MAAM,MAAM,IAAI,IAAI,sBAAsB;EAE1C,IAAI,aAAa,IAAI,aAAa,KAAK,SAAS,QAAQ;EACxD,IAAI,aAAa,IAAI,gBAAgB,KAAK,SAAS,WAAW;EAC9D,IAAI,aAAa,IAAI,iBAAiB,MAAM;EAC5C,IAAI,aAAa,IAAI,UAAU,KAAK,SAAS,UAAU,eAAc,CAAE,KAAK,GAAG,CAAC;EAChF,IAAI,aAAa,IAAI,SAAS,MAAM,KAAK;EACzC,IAAI,aAAa,IAAI,kBAAkB,cAAc,MAAM,YAAY,CAAC;EACxE,IAAI,aAAa,IAAI,yBAAyB,MAAM;EAEpD,OAAO,IAAI,SAAS;CACtB;CAEA,MAAa,eAAe,EAC1B,OACA,YACmD;EACnD,MAAM,OAAO,MAAM;EAEnB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,6BAA6B,cAAc;EAGvD,MAAM,cAAc,MAAM,KAAK,aAAa,MAAM,SAAS,YAAY;EACvE,MAAM,OAAO,MAAM,KAAK,UAAU,WAAW;EAE7C,IAAI,OAAO,KAAK,OAAO,YAAY,KAAK,GAAG,WAAW,GACpD,MAAM,IAAI,6BAA6B,iBAAiB;EAG1D,OAAO;GACL,UAAU,KAAK;GACf,gBAAgB,KAAK;GACrB,OAAO,KAAK,SAAS;GACrB,eAAe,KAAK,aAAa;GACjC,MAAM,KAAK,eAAe,KAAK;GAC/B,QAAQ,KAAK,SACT,sCAAsC,KAAK,GAAG,GAAG,KAAK,OAAO,QAC7D;GACJ,KAAK;EACP;CACF;;CAGA,MAAc,aAAa,MAAc,cAAuC;EAC9E,IAAI;EAEJ,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,gBAAgB;IAC3C,QAAQ;IACR,SAAS,EAAE,gBAAgB,oCAAoC;IAC/D,MAAM,IAAI,gBAAgB;KACxB;KACA,WAAW,KAAK,SAAS;KACzB,eAAe,KAAK,SAAS;KAC7B,cAAc,KAAK,SAAS;KAC5B,YAAY;KACZ,eAAe;IACjB,CAAC;GACH,CAAC;GAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,mCAAmC,SAAS,QAAQ;GAGtE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;EAEA,IAAI,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,WAAW,GACxE,MAAM,IAAI,6BAA6B,sBAAsB;EAG/D,OAAO,KAAK;CACd;CAEA,MAAc,UAAU,aAA2C;EACjE,IAAI;GACF,MAAM,WAAW,MAAM,MAAM,eAAe,EAC1C,SAAS,EAAE,eAAe,UAAU,cAAc,EACpD,CAAC;GAED,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,MAAM,kCAAkC,SAAS,QAAQ;GAGrE,OAAQ,MAAM,SAAS,KAAK;EAC9B,SAAS,OAAO;GACd,MAAM,IAAI,6BAA6B,wBAAwB,KAAK;EACtE;CACF;AACF"}
@@ -1,6 +1,6 @@
1
1
  import { AuthProvider, FacebookProviderConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile } from "../contracts/providers.mjs";
2
2
 
3
- //#region ../auth/src/providers/facebook-provider.d.ts
3
+ //#region ../../packages/auth/src/providers/facebook-provider.d.ts
4
4
  /**
5
5
  * Facebook sign-in: plain OAuth 2 authorization code flow with PKCE (S256).
6
6
  * Facebook is not OpenID Connect — there is no `id_token`. Unlike every other
@@ -1 +1 @@
1
- {"version":3,"file":"facebook-provider.d.mts","names":[],"sources":["../../../../../../../auth/src/providers/facebook-provider.ts"],"mappings":";;;;;AAuCA;;;;;;;;;;;;;;;cAAa,gBAAA,YAA4B,YAAA;EAAA,iBAGH,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,sBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAclB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAjBZ;EAAA,QA4CV,YAAA;EAAA,QAkCA,SAAA;AAAA"}
1
+ {"version":3,"file":"facebook-provider.d.mts","names":[],"sources":["../../../../../../../../packages/auth/src/providers/facebook-provider.ts"],"mappings":";;;;;AAuCA;;;;;;;;;;;;;;;cAAa,gBAAA,YAA4B,YAAA;EAAA,iBAGH,QAAA;EAAA,SAFpB,IAAA;cAEoB,QAAA,EAAU,sBAAA;EAEvC,gBAAA,CAAiB,KAAA,EAAO,0BAAA;EAclB,cAAA;IACX,KAAA;IACA;EAAA,GACC,sBAAA,GAAyB,OAAA,CAAQ,eAAA;EAjBZ;EAAA,QA4CV,YAAA;EAAA,QAkCA,SAAA;AAAA"}
@@ -1,7 +1,7 @@
1
1
  import { InvalidProviderCallbackError } from "../errors/invalid-provider-callback.error.mjs";
2
2
  import { pkceChallenge } from "./google-provider.mjs";
3
3
 
4
- //#region ../auth/src/providers/facebook-provider.ts
4
+ //#region ../../packages/auth/src/providers/facebook-provider.ts
5
5
  const AUTHORIZATION_ENDPOINT = "https://www.facebook.com/v19.0/dialog/oauth";
6
6
  const TOKEN_ENDPOINT = "https://graph.facebook.com/v19.0/oauth/access_token";
7
7
  const USER_ENDPOINT = "https://graph.facebook.com/v19.0/me";