@authrim/sveltekit 0.1.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 (266) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +531 -0
  3. package/dist/__tests__/client-events.test.d.ts +2 -0
  4. package/dist/__tests__/client-events.test.d.ts.map +1 -0
  5. package/dist/__tests__/client-events.test.js +225 -0
  6. package/dist/__tests__/providers.test.d.ts +2 -0
  7. package/dist/__tests__/providers.test.d.ts.map +1 -0
  8. package/dist/__tests__/providers.test.js +68 -0
  9. package/dist/__tests__/response.test.d.ts +2 -0
  10. package/dist/__tests__/response.test.d.ts.map +1 -0
  11. package/dist/__tests__/response.test.js +99 -0
  12. package/dist/__tests__/stores.test.d.ts +2 -0
  13. package/dist/__tests__/stores.test.d.ts.map +1 -0
  14. package/dist/__tests__/stores.test.js +91 -0
  15. package/dist/client.d.ts +25 -0
  16. package/dist/client.d.ts.map +1 -0
  17. package/dist/client.js +411 -0
  18. package/dist/components/AuthProvider.svelte +56 -0
  19. package/dist/components/AuthProvider.svelte.d.ts +34 -0
  20. package/dist/components/AuthProvider.svelte.d.ts.map +1 -0
  21. package/dist/components/ProtectedRoute.svelte +71 -0
  22. package/dist/components/ProtectedRoute.svelte.d.ts +38 -0
  23. package/dist/components/ProtectedRoute.svelte.d.ts.map +1 -0
  24. package/dist/components/SignInButton.svelte +93 -0
  25. package/dist/components/SignInButton.svelte.d.ts +43 -0
  26. package/dist/components/SignInButton.svelte.d.ts.map +1 -0
  27. package/dist/components/SignOutButton.svelte +72 -0
  28. package/dist/components/SignOutButton.svelte.d.ts +40 -0
  29. package/dist/components/SignOutButton.svelte.d.ts.map +1 -0
  30. package/dist/components/UserProfile.svelte +71 -0
  31. package/dist/components/UserProfile.svelte.d.ts +51 -0
  32. package/dist/components/UserProfile.svelte.d.ts.map +1 -0
  33. package/dist/components/index.d.ts +6 -0
  34. package/dist/components/index.d.ts.map +1 -0
  35. package/dist/components/index.js +5 -0
  36. package/dist/direct-auth/ciba.d.ts +47 -0
  37. package/dist/direct-auth/ciba.d.ts.map +1 -0
  38. package/dist/direct-auth/ciba.js +77 -0
  39. package/dist/direct-auth/consent.d.ts +85 -0
  40. package/dist/direct-auth/consent.d.ts.map +1 -0
  41. package/dist/direct-auth/consent.js +57 -0
  42. package/dist/direct-auth/device-flow.d.ts +40 -0
  43. package/dist/direct-auth/device-flow.d.ts.map +1 -0
  44. package/dist/direct-auth/device-flow.js +45 -0
  45. package/dist/direct-auth/email-code.d.ts +48 -0
  46. package/dist/direct-auth/email-code.d.ts.map +1 -0
  47. package/dist/direct-auth/email-code.js +265 -0
  48. package/dist/direct-auth/index.d.ts +9 -0
  49. package/dist/direct-auth/index.d.ts.map +1 -0
  50. package/dist/direct-auth/index.js +8 -0
  51. package/dist/direct-auth/login-challenge.d.ts +41 -0
  52. package/dist/direct-auth/login-challenge.d.ts.map +1 -0
  53. package/dist/direct-auth/login-challenge.js +34 -0
  54. package/dist/direct-auth/passkey.d.ts +30 -0
  55. package/dist/direct-auth/passkey.d.ts.map +1 -0
  56. package/dist/direct-auth/passkey.js +392 -0
  57. package/dist/direct-auth/session.d.ts +48 -0
  58. package/dist/direct-auth/session.d.ts.map +1 -0
  59. package/dist/direct-auth/session.js +219 -0
  60. package/dist/direct-auth/social.d.ts +56 -0
  61. package/dist/direct-auth/social.d.ts.map +1 -0
  62. package/dist/direct-auth/social.js +484 -0
  63. package/dist/index.d.ts +17 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +14 -0
  66. package/dist/providers/crypto.d.ts +13 -0
  67. package/dist/providers/crypto.d.ts.map +1 -0
  68. package/dist/providers/crypto.js +27 -0
  69. package/dist/providers/http.d.ts +30 -0
  70. package/dist/providers/http.d.ts.map +1 -0
  71. package/dist/providers/http.js +65 -0
  72. package/dist/providers/index.d.ts +4 -0
  73. package/dist/providers/index.d.ts.map +1 -0
  74. package/dist/providers/index.js +3 -0
  75. package/dist/providers/storage.d.ts +21 -0
  76. package/dist/providers/storage.d.ts.map +1 -0
  77. package/dist/providers/storage.js +83 -0
  78. package/dist/server/handle.d.ts +46 -0
  79. package/dist/server/handle.d.ts.map +1 -0
  80. package/dist/server/handle.js +60 -0
  81. package/dist/server/index.d.ts +4 -0
  82. package/dist/server/index.d.ts.map +1 -0
  83. package/dist/server/index.js +3 -0
  84. package/dist/server/load.d.ts +83 -0
  85. package/dist/server/load.d.ts.map +1 -0
  86. package/dist/server/load.js +86 -0
  87. package/dist/server/session.d.ts +44 -0
  88. package/dist/server/session.d.ts.map +1 -0
  89. package/dist/server/session.js +50 -0
  90. package/dist/stores/auth.d.ts +56 -0
  91. package/dist/stores/auth.d.ts.map +1 -0
  92. package/dist/stores/auth.js +64 -0
  93. package/dist/stores/index.d.ts +2 -0
  94. package/dist/stores/index.d.ts.map +1 -0
  95. package/dist/stores/index.js +1 -0
  96. package/dist/types.d.ts +164 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +4 -0
  99. package/dist/ui/account/LinkAccountButton.svelte +133 -0
  100. package/dist/ui/account/LinkAccountButton.svelte.d.ts +37 -0
  101. package/dist/ui/account/LinkAccountButton.svelte.d.ts.map +1 -0
  102. package/dist/ui/account/LinkedAccountsList.svelte +233 -0
  103. package/dist/ui/account/LinkedAccountsList.svelte.d.ts +32 -0
  104. package/dist/ui/account/LinkedAccountsList.svelte.d.ts.map +1 -0
  105. package/dist/ui/account/UnlinkAccountButton.svelte +179 -0
  106. package/dist/ui/account/UnlinkAccountButton.svelte.d.ts +28 -0
  107. package/dist/ui/account/UnlinkAccountButton.svelte.d.ts.map +1 -0
  108. package/dist/ui/account/index.d.ts +7 -0
  109. package/dist/ui/account/index.d.ts.map +1 -0
  110. package/dist/ui/account/index.js +6 -0
  111. package/dist/ui/context.d.ts +17 -0
  112. package/dist/ui/context.d.ts.map +1 -0
  113. package/dist/ui/context.js +71 -0
  114. package/dist/ui/forms/CIBARequestCard.svelte +315 -0
  115. package/dist/ui/forms/CIBARequestCard.svelte.d.ts +50 -0
  116. package/dist/ui/forms/CIBARequestCard.svelte.d.ts.map +1 -0
  117. package/dist/ui/forms/ClientInfo.svelte +232 -0
  118. package/dist/ui/forms/ClientInfo.svelte.d.ts +35 -0
  119. package/dist/ui/forms/ClientInfo.svelte.d.ts.map +1 -0
  120. package/dist/ui/forms/ConsentScopesList.svelte +109 -0
  121. package/dist/ui/forms/ConsentScopesList.svelte.d.ts +30 -0
  122. package/dist/ui/forms/ConsentScopesList.svelte.d.ts.map +1 -0
  123. package/dist/ui/forms/EmailCodeForm.svelte +224 -0
  124. package/dist/ui/forms/EmailCodeForm.svelte.d.ts +39 -0
  125. package/dist/ui/forms/EmailCodeForm.svelte.d.ts.map +1 -0
  126. package/dist/ui/forms/OrgSelector.svelte +95 -0
  127. package/dist/ui/forms/OrgSelector.svelte.d.ts +37 -0
  128. package/dist/ui/forms/OrgSelector.svelte.d.ts.map +1 -0
  129. package/dist/ui/forms/PasskeyConditionalInput.svelte +173 -0
  130. package/dist/ui/forms/PasskeyConditionalInput.svelte.d.ts +36 -0
  131. package/dist/ui/forms/PasskeyConditionalInput.svelte.d.ts.map +1 -0
  132. package/dist/ui/forms/QRCodeDisplay.svelte +122 -0
  133. package/dist/ui/forms/QRCodeDisplay.svelte.d.ts +27 -0
  134. package/dist/ui/forms/QRCodeDisplay.svelte.d.ts.map +1 -0
  135. package/dist/ui/forms/SocialLoginButtons.svelte +209 -0
  136. package/dist/ui/forms/SocialLoginButtons.svelte.d.ts +33 -0
  137. package/dist/ui/forms/SocialLoginButtons.svelte.d.ts.map +1 -0
  138. package/dist/ui/forms/UserCodeInput.svelte +183 -0
  139. package/dist/ui/forms/UserCodeInput.svelte.d.ts +34 -0
  140. package/dist/ui/forms/UserCodeInput.svelte.d.ts.map +1 -0
  141. package/dist/ui/forms/index.d.ts +13 -0
  142. package/dist/ui/forms/index.d.ts.map +1 -0
  143. package/dist/ui/forms/index.js +12 -0
  144. package/dist/ui/helpers/AuthError.svelte +124 -0
  145. package/dist/ui/helpers/AuthError.svelte.d.ts +26 -0
  146. package/dist/ui/helpers/AuthError.svelte.d.ts.map +1 -0
  147. package/dist/ui/helpers/AuthLoading.svelte +83 -0
  148. package/dist/ui/helpers/AuthLoading.svelte.d.ts +25 -0
  149. package/dist/ui/helpers/AuthLoading.svelte.d.ts.map +1 -0
  150. package/dist/ui/helpers/OTPInput.svelte +214 -0
  151. package/dist/ui/helpers/OTPInput.svelte.d.ts +34 -0
  152. package/dist/ui/helpers/OTPInput.svelte.d.ts.map +1 -0
  153. package/dist/ui/helpers/ResendCodeButton.svelte +140 -0
  154. package/dist/ui/helpers/ResendCodeButton.svelte.d.ts +28 -0
  155. package/dist/ui/helpers/ResendCodeButton.svelte.d.ts.map +1 -0
  156. package/dist/ui/helpers/index.d.ts +8 -0
  157. package/dist/ui/helpers/index.d.ts.map +1 -0
  158. package/dist/ui/helpers/index.js +7 -0
  159. package/dist/ui/index.d.ts +43 -0
  160. package/dist/ui/index.d.ts.map +1 -0
  161. package/dist/ui/index.js +48 -0
  162. package/dist/ui/passkey/PasskeyDeleteButton.svelte +177 -0
  163. package/dist/ui/passkey/PasskeyDeleteButton.svelte.d.ts +26 -0
  164. package/dist/ui/passkey/PasskeyDeleteButton.svelte.d.ts.map +1 -0
  165. package/dist/ui/passkey/PasskeyList.svelte +225 -0
  166. package/dist/ui/passkey/PasskeyList.svelte.d.ts +30 -0
  167. package/dist/ui/passkey/PasskeyList.svelte.d.ts.map +1 -0
  168. package/dist/ui/passkey/PasskeyRegisterButton.svelte +52 -0
  169. package/dist/ui/passkey/PasskeyRegisterButton.svelte.d.ts +38 -0
  170. package/dist/ui/passkey/PasskeyRegisterButton.svelte.d.ts.map +1 -0
  171. package/dist/ui/passkey/index.d.ts +7 -0
  172. package/dist/ui/passkey/index.d.ts.map +1 -0
  173. package/dist/ui/passkey/index.js +6 -0
  174. package/dist/ui/session/SessionExpiryIndicator.svelte +109 -0
  175. package/dist/ui/session/SessionExpiryIndicator.svelte.d.ts +23 -0
  176. package/dist/ui/session/SessionExpiryIndicator.svelte.d.ts.map +1 -0
  177. package/dist/ui/session/SessionList.svelte +231 -0
  178. package/dist/ui/session/SessionList.svelte.d.ts +31 -0
  179. package/dist/ui/session/SessionList.svelte.d.ts.map +1 -0
  180. package/dist/ui/session/SessionRevokeButton.svelte +72 -0
  181. package/dist/ui/session/SessionRevokeButton.svelte.d.ts +26 -0
  182. package/dist/ui/session/SessionRevokeButton.svelte.d.ts.map +1 -0
  183. package/dist/ui/session/index.d.ts +7 -0
  184. package/dist/ui/session/index.d.ts.map +1 -0
  185. package/dist/ui/session/index.js +6 -0
  186. package/dist/ui/shared/Alert.svelte +246 -0
  187. package/dist/ui/shared/Alert.svelte.d.ts +36 -0
  188. package/dist/ui/shared/Alert.svelte.d.ts.map +1 -0
  189. package/dist/ui/shared/Badge.svelte +100 -0
  190. package/dist/ui/shared/Badge.svelte.d.ts +35 -0
  191. package/dist/ui/shared/Badge.svelte.d.ts.map +1 -0
  192. package/dist/ui/shared/Button.svelte +213 -0
  193. package/dist/ui/shared/Button.svelte.d.ts +42 -0
  194. package/dist/ui/shared/Button.svelte.d.ts.map +1 -0
  195. package/dist/ui/shared/Card.svelte +85 -0
  196. package/dist/ui/shared/Card.svelte.d.ts +39 -0
  197. package/dist/ui/shared/Card.svelte.d.ts.map +1 -0
  198. package/dist/ui/shared/CountdownTimer.svelte +150 -0
  199. package/dist/ui/shared/CountdownTimer.svelte.d.ts +30 -0
  200. package/dist/ui/shared/CountdownTimer.svelte.d.ts.map +1 -0
  201. package/dist/ui/shared/Dialog.svelte +240 -0
  202. package/dist/ui/shared/Dialog.svelte.d.ts +39 -0
  203. package/dist/ui/shared/Dialog.svelte.d.ts.map +1 -0
  204. package/dist/ui/shared/Input.svelte +192 -0
  205. package/dist/ui/shared/Input.svelte.d.ts +42 -0
  206. package/dist/ui/shared/Input.svelte.d.ts.map +1 -0
  207. package/dist/ui/shared/LanguageSwitcher.svelte +99 -0
  208. package/dist/ui/shared/LanguageSwitcher.svelte.d.ts +31 -0
  209. package/dist/ui/shared/LanguageSwitcher.svelte.d.ts.map +1 -0
  210. package/dist/ui/shared/Spinner.svelte +75 -0
  211. package/dist/ui/shared/Spinner.svelte.d.ts +24 -0
  212. package/dist/ui/shared/Spinner.svelte.d.ts.map +1 -0
  213. package/dist/ui/shared/index.d.ts +13 -0
  214. package/dist/ui/shared/index.d.ts.map +1 -0
  215. package/dist/ui/shared/index.js +12 -0
  216. package/dist/ui/styles/base.css +168 -0
  217. package/dist/ui/styles/theme.css +279 -0
  218. package/dist/ui/templates/AccountSettingsTemplate.svelte +205 -0
  219. package/dist/ui/templates/AccountSettingsTemplate.svelte.d.ts +49 -0
  220. package/dist/ui/templates/AccountSettingsTemplate.svelte.d.ts.map +1 -0
  221. package/dist/ui/templates/CIBATemplate.svelte +227 -0
  222. package/dist/ui/templates/CIBATemplate.svelte.d.ts +45 -0
  223. package/dist/ui/templates/CIBATemplate.svelte.d.ts.map +1 -0
  224. package/dist/ui/templates/ConsentTemplate.svelte +549 -0
  225. package/dist/ui/templates/ConsentTemplate.svelte.d.ts +76 -0
  226. package/dist/ui/templates/ConsentTemplate.svelte.d.ts.map +1 -0
  227. package/dist/ui/templates/DeviceFlowTemplate.svelte +228 -0
  228. package/dist/ui/templates/DeviceFlowTemplate.svelte.d.ts +47 -0
  229. package/dist/ui/templates/DeviceFlowTemplate.svelte.d.ts.map +1 -0
  230. package/dist/ui/templates/LoginTemplate.svelte +234 -0
  231. package/dist/ui/templates/LoginTemplate.svelte.d.ts +49 -0
  232. package/dist/ui/templates/LoginTemplate.svelte.d.ts.map +1 -0
  233. package/dist/ui/templates/ReauthTemplate.svelte +269 -0
  234. package/dist/ui/templates/ReauthTemplate.svelte.d.ts +54 -0
  235. package/dist/ui/templates/ReauthTemplate.svelte.d.ts.map +1 -0
  236. package/dist/ui/templates/SignUpTemplate.svelte +345 -0
  237. package/dist/ui/templates/SignUpTemplate.svelte.d.ts +53 -0
  238. package/dist/ui/templates/SignUpTemplate.svelte.d.ts.map +1 -0
  239. package/dist/ui/templates/index.d.ts +14 -0
  240. package/dist/ui/templates/index.d.ts.map +1 -0
  241. package/dist/ui/templates/index.js +13 -0
  242. package/dist/ui/types.d.ts +151 -0
  243. package/dist/ui/types.d.ts.map +1 -0
  244. package/dist/ui/types.js +4 -0
  245. package/dist/utils/context.d.ts +12 -0
  246. package/dist/utils/context.d.ts.map +1 -0
  247. package/dist/utils/context.js +26 -0
  248. package/dist/utils/error-mapping.d.ts +29 -0
  249. package/dist/utils/error-mapping.d.ts.map +1 -0
  250. package/dist/utils/error-mapping.js +38 -0
  251. package/dist/utils/index.d.ts +7 -0
  252. package/dist/utils/index.d.ts.map +1 -0
  253. package/dist/utils/index.js +6 -0
  254. package/dist/utils/response.d.ts +21 -0
  255. package/dist/utils/response.d.ts.map +1 -0
  256. package/dist/utils/response.js +84 -0
  257. package/dist/utils/sensitive-data.d.ts +9 -0
  258. package/dist/utils/sensitive-data.d.ts.map +1 -0
  259. package/dist/utils/sensitive-data.js +56 -0
  260. package/dist/utils/ssr.d.ts +38 -0
  261. package/dist/utils/ssr.d.ts.map +1 -0
  262. package/dist/utils/ssr.js +73 -0
  263. package/dist/utils/webauthn-converters.d.ts +9 -0
  264. package/dist/utils/webauthn-converters.d.ts.map +1 -0
  265. package/dist/utils/webauthn-converters.js +75 -0
  266. package/package.json +111 -0
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Passkey Authentication (WebAuthn)
3
+ */
4
+ import { AuthrimError, PKCEHelper, } from '@authrim/core';
5
+ import { getAuthrimCode, mapSeverity } from '../utils/error-mapping.js';
6
+ import { convertToPublicKeyCredentialRequestOptions, convertToPublicKeyCredentialCreationOptions, assertionResponseToJSON, attestationResponseToJSON, } from '../utils/webauthn-converters.js';
7
+ const ENDPOINTS = {
8
+ PASSKEY_LOGIN_START: '/api/v1/auth/direct/passkey/login/start',
9
+ PASSKEY_LOGIN_FINISH: '/api/v1/auth/direct/passkey/login/finish',
10
+ PASSKEY_SIGNUP_START: '/api/v1/auth/direct/passkey/signup/start',
11
+ PASSKEY_SIGNUP_FINISH: '/api/v1/auth/direct/passkey/signup/finish',
12
+ PASSKEY_REGISTER_START: '/api/v1/auth/direct/passkey/register/start',
13
+ PASSKEY_REGISTER_FINISH: '/api/v1/auth/direct/passkey/register/finish',
14
+ };
15
+ export class PasskeyAuthImpl {
16
+ issuer;
17
+ clientId;
18
+ http;
19
+ pkce;
20
+ exchangeToken;
21
+ conditionalAbortController = null;
22
+ constructor(options) {
23
+ this.issuer = options.issuer;
24
+ this.clientId = options.clientId;
25
+ this.http = options.http;
26
+ this.pkce = new PKCEHelper(options.crypto);
27
+ this.exchangeToken = options.exchangeToken;
28
+ }
29
+ isSupported() {
30
+ return (typeof window !== 'undefined' &&
31
+ typeof window.PublicKeyCredential !== 'undefined' &&
32
+ typeof navigator.credentials !== 'undefined');
33
+ }
34
+ async isConditionalUIAvailable() {
35
+ if (!this.isSupported())
36
+ return false;
37
+ try {
38
+ if (typeof PublicKeyCredential.isConditionalMediationAvailable === 'function') {
39
+ return await PublicKeyCredential.isConditionalMediationAvailable();
40
+ }
41
+ return false;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ async login(options) {
48
+ if (!this.isSupported()) {
49
+ return {
50
+ success: false,
51
+ error: {
52
+ error: 'passkey_not_supported',
53
+ error_description: 'WebAuthn is not supported in this browser',
54
+ code: 'AR003003',
55
+ meta: { retryable: false, severity: 'warn' },
56
+ },
57
+ };
58
+ }
59
+ let codeVerifier = '';
60
+ try {
61
+ const pkce = await this.pkce.generatePKCE();
62
+ codeVerifier = pkce.codeVerifier;
63
+ const codeChallenge = pkce.codeChallenge;
64
+ const startRequest = {
65
+ client_id: this.clientId,
66
+ code_challenge: codeChallenge,
67
+ code_challenge_method: 'S256',
68
+ };
69
+ const startResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_LOGIN_START}`, {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify(startRequest),
73
+ });
74
+ if (!startResponse.ok || !startResponse.data) {
75
+ throw new AuthrimError('network_error', 'Failed to start passkey login');
76
+ }
77
+ const { challenge_id, options: webauthnOptions } = startResponse.data;
78
+ const publicKeyOptions = convertToPublicKeyCredentialRequestOptions(webauthnOptions);
79
+ const abortController = new AbortController();
80
+ const abortHandler = () => abortController.abort();
81
+ if (options?.signal) {
82
+ options.signal.addEventListener('abort', abortHandler, { once: true });
83
+ }
84
+ if (options?.conditional || options?.mediation === 'conditional') {
85
+ // Cancel any existing conditional UI request before starting a new one
86
+ // This prevents leaking the old AbortController
87
+ if (this.conditionalAbortController) {
88
+ this.conditionalAbortController.abort();
89
+ }
90
+ this.conditionalAbortController = abortController;
91
+ }
92
+ let credential;
93
+ try {
94
+ credential = (await navigator.credentials.get({
95
+ publicKey: publicKeyOptions,
96
+ mediation: options?.mediation || (options?.conditional ? 'conditional' : 'optional'),
97
+ signal: abortController.signal,
98
+ }));
99
+ }
100
+ catch (error) {
101
+ if (error instanceof Error) {
102
+ if (error.name === 'AbortError' || error.name === 'NotAllowedError') {
103
+ return {
104
+ success: false,
105
+ error: {
106
+ error: 'passkey_cancelled',
107
+ error_description: error.name === 'AbortError'
108
+ ? 'Passkey authentication was cancelled'
109
+ : 'User denied the passkey request',
110
+ code: 'AR003004',
111
+ meta: { retryable: false, severity: 'warn' },
112
+ },
113
+ };
114
+ }
115
+ }
116
+ throw error;
117
+ }
118
+ finally {
119
+ // Cleanup: remove abort handler and clear conditional controller
120
+ if (options?.signal) {
121
+ options.signal.removeEventListener('abort', abortHandler);
122
+ }
123
+ if (options?.conditional) {
124
+ this.conditionalAbortController = null;
125
+ }
126
+ }
127
+ if (!credential) {
128
+ return {
129
+ success: false,
130
+ error: {
131
+ error: 'passkey_not_found',
132
+ error_description: 'No passkey credential found',
133
+ code: 'AR003001',
134
+ meta: { retryable: false, severity: 'warn' },
135
+ },
136
+ };
137
+ }
138
+ const credentialJSON = assertionResponseToJSON(credential);
139
+ const finishRequest = {
140
+ challenge_id,
141
+ credential: credentialJSON,
142
+ code_verifier: codeVerifier,
143
+ };
144
+ const finishResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_LOGIN_FINISH}`, {
145
+ method: 'POST',
146
+ headers: { 'Content-Type': 'application/json' },
147
+ body: JSON.stringify(finishRequest),
148
+ });
149
+ if (!finishResponse.ok || !finishResponse.data) {
150
+ throw new AuthrimError('passkey_verification_failed', 'Failed to verify passkey');
151
+ }
152
+ const { auth_code } = finishResponse.data;
153
+ const result = await this.exchangeToken(auth_code, codeVerifier);
154
+ return {
155
+ success: true,
156
+ session: result.session,
157
+ user: result.user,
158
+ };
159
+ }
160
+ catch (error) {
161
+ if (error instanceof AuthrimError) {
162
+ return {
163
+ success: false,
164
+ error: {
165
+ error: error.code,
166
+ error_description: error.message,
167
+ code: getAuthrimCode(error.code, 'AR003000'),
168
+ meta: {
169
+ retryable: error.meta.retryable,
170
+ severity: mapSeverity(error.meta.severity),
171
+ },
172
+ },
173
+ };
174
+ }
175
+ return {
176
+ success: false,
177
+ error: {
178
+ error: 'passkey_verification_failed',
179
+ error_description: error instanceof Error ? error.message : 'Unknown error',
180
+ code: 'AR003002',
181
+ meta: { retryable: false, severity: 'error' },
182
+ },
183
+ };
184
+ }
185
+ finally {
186
+ // Ensure codeVerifier is cleared regardless of success or failure
187
+ codeVerifier = '';
188
+ }
189
+ }
190
+ async signUp(options) {
191
+ if (!this.isSupported()) {
192
+ return {
193
+ success: false,
194
+ error: {
195
+ error: 'passkey_not_supported',
196
+ error_description: 'WebAuthn is not supported in this browser',
197
+ code: 'AR003003',
198
+ meta: { retryable: false, severity: 'warn' },
199
+ },
200
+ };
201
+ }
202
+ let codeVerifier = '';
203
+ try {
204
+ const pkce = await this.pkce.generatePKCE();
205
+ codeVerifier = pkce.codeVerifier;
206
+ const codeChallenge = pkce.codeChallenge;
207
+ const startRequest = {
208
+ client_id: this.clientId,
209
+ email: options.email,
210
+ display_name: options.displayName,
211
+ code_challenge: codeChallenge,
212
+ code_challenge_method: 'S256',
213
+ authenticator_type: options.authenticatorType,
214
+ resident_key: options.residentKey,
215
+ user_verification: options.userVerification,
216
+ };
217
+ const startResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_SIGNUP_START}`, {
218
+ method: 'POST',
219
+ headers: { 'Content-Type': 'application/json' },
220
+ body: JSON.stringify(startRequest),
221
+ });
222
+ if (!startResponse.ok || !startResponse.data) {
223
+ throw new AuthrimError('network_error', 'Failed to start passkey signup');
224
+ }
225
+ const { challenge_id, options: webauthnOptions } = startResponse.data;
226
+ const publicKeyOptions = convertToPublicKeyCredentialCreationOptions(webauthnOptions);
227
+ const abortController = new AbortController();
228
+ const abortHandler = () => abortController.abort();
229
+ if (options.signal) {
230
+ options.signal.addEventListener('abort', abortHandler, { once: true });
231
+ }
232
+ let credential;
233
+ try {
234
+ credential = (await navigator.credentials.create({
235
+ publicKey: publicKeyOptions,
236
+ signal: abortController.signal,
237
+ }));
238
+ }
239
+ catch (error) {
240
+ if (error instanceof Error) {
241
+ if (error.name === 'AbortError' || error.name === 'NotAllowedError') {
242
+ return {
243
+ success: false,
244
+ error: {
245
+ error: 'passkey_cancelled',
246
+ error_description: 'Passkey registration was cancelled',
247
+ code: 'AR003004',
248
+ meta: { retryable: false, severity: 'warn' },
249
+ },
250
+ };
251
+ }
252
+ }
253
+ throw error;
254
+ }
255
+ finally {
256
+ // Cleanup: remove abort handler
257
+ if (options.signal) {
258
+ options.signal.removeEventListener('abort', abortHandler);
259
+ }
260
+ }
261
+ if (!credential) {
262
+ return {
263
+ success: false,
264
+ error: {
265
+ error: 'passkey_invalid_credential',
266
+ error_description: 'Failed to create passkey credential',
267
+ code: 'AR003005',
268
+ meta: { retryable: false, severity: 'error' },
269
+ },
270
+ };
271
+ }
272
+ const credentialJSON = attestationResponseToJSON(credential);
273
+ const finishRequest = {
274
+ challenge_id,
275
+ credential: credentialJSON,
276
+ code_verifier: codeVerifier,
277
+ };
278
+ const finishResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_SIGNUP_FINISH}`, {
279
+ method: 'POST',
280
+ headers: { 'Content-Type': 'application/json' },
281
+ body: JSON.stringify(finishRequest),
282
+ });
283
+ if (!finishResponse.ok || !finishResponse.data) {
284
+ throw new AuthrimError('passkey_verification_failed', 'Failed to register passkey');
285
+ }
286
+ const { auth_code } = finishResponse.data;
287
+ const result = await this.exchangeToken(auth_code, codeVerifier);
288
+ return {
289
+ success: true,
290
+ session: result.session,
291
+ user: result.user,
292
+ };
293
+ }
294
+ catch (error) {
295
+ if (error instanceof AuthrimError) {
296
+ return {
297
+ success: false,
298
+ error: {
299
+ error: error.code,
300
+ error_description: error.message,
301
+ code: getAuthrimCode(error.code, 'AR003000'),
302
+ meta: {
303
+ retryable: error.meta.retryable,
304
+ severity: mapSeverity(error.meta.severity),
305
+ },
306
+ },
307
+ };
308
+ }
309
+ return {
310
+ success: false,
311
+ error: {
312
+ error: 'passkey_verification_failed',
313
+ error_description: error instanceof Error ? error.message : 'Unknown error',
314
+ code: 'AR003002',
315
+ meta: { retryable: false, severity: 'error' },
316
+ },
317
+ };
318
+ }
319
+ finally {
320
+ // Ensure codeVerifier is cleared regardless of success or failure
321
+ codeVerifier = '';
322
+ }
323
+ }
324
+ async register(options) {
325
+ if (!this.isSupported()) {
326
+ throw new AuthrimError('passkey_not_supported', 'WebAuthn is not supported in this browser');
327
+ }
328
+ const startResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_REGISTER_START}`, {
329
+ method: 'POST',
330
+ headers: { 'Content-Type': 'application/json' },
331
+ body: JSON.stringify({
332
+ client_id: this.clientId,
333
+ display_name: options?.displayName,
334
+ authenticator_type: options?.authenticatorType,
335
+ resident_key: options?.residentKey,
336
+ user_verification: options?.userVerification,
337
+ }),
338
+ });
339
+ if (!startResponse.ok || !startResponse.data) {
340
+ throw new AuthrimError('network_error', 'Failed to start passkey registration');
341
+ }
342
+ const { challenge_id, options: webauthnOptions } = startResponse.data;
343
+ const publicKeyOptions = convertToPublicKeyCredentialCreationOptions(webauthnOptions);
344
+ const abortController = new AbortController();
345
+ const abortHandler = () => abortController.abort();
346
+ if (options?.signal) {
347
+ options.signal.addEventListener('abort', abortHandler, { once: true });
348
+ }
349
+ let credential;
350
+ try {
351
+ credential = (await navigator.credentials.create({
352
+ publicKey: publicKeyOptions,
353
+ signal: abortController.signal,
354
+ }));
355
+ }
356
+ finally {
357
+ // Cleanup: remove abort handler
358
+ if (options?.signal) {
359
+ options.signal.removeEventListener('abort', abortHandler);
360
+ }
361
+ }
362
+ if (!credential) {
363
+ throw new AuthrimError('passkey_invalid_credential', 'Failed to create passkey credential');
364
+ }
365
+ const credentialJSON = attestationResponseToJSON(credential);
366
+ const finishResponse = await this.http.fetch(`${this.issuer}${ENDPOINTS.PASSKEY_REGISTER_FINISH}`, {
367
+ method: 'POST',
368
+ headers: { 'Content-Type': 'application/json' },
369
+ body: JSON.stringify({
370
+ challenge_id,
371
+ credential: credentialJSON,
372
+ }),
373
+ });
374
+ if (!finishResponse.ok || !finishResponse.data) {
375
+ throw new AuthrimError('passkey_verification_failed', 'Failed to register passkey');
376
+ }
377
+ return {
378
+ credentialId: finishResponse.data.credential_id,
379
+ publicKey: finishResponse.data.public_key,
380
+ authenticatorType: finishResponse.data.authenticator_type,
381
+ transports: finishResponse.data.transports,
382
+ createdAt: finishResponse.data.created_at,
383
+ displayName: options?.displayName,
384
+ };
385
+ }
386
+ cancelConditionalUI() {
387
+ if (this.conditionalAbortController) {
388
+ this.conditionalAbortController.abort();
389
+ this.conditionalAbortController = null;
390
+ }
391
+ }
392
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Session Management for Direct Auth
3
+ */
4
+ import { type SessionAuth, type Session, type DirectAuthLogoutOptions, type User } from '@authrim/core';
5
+ import type { BrowserHttpClient } from '../providers/http.js';
6
+ export interface SessionManagerOptions {
7
+ issuer: string;
8
+ clientId: string;
9
+ http: BrowserHttpClient;
10
+ }
11
+ export declare class SessionAuthImpl implements SessionAuth {
12
+ private readonly issuer;
13
+ private readonly clientId;
14
+ private readonly http;
15
+ private readonly storageKey;
16
+ private cachedSession;
17
+ private cachedUser;
18
+ private sessionCacheExpiry;
19
+ private readonly SESSION_CACHE_TTL;
20
+ constructor(options: SessionManagerOptions);
21
+ private getStoredToken;
22
+ private storeToken;
23
+ private removeStoredToken;
24
+ get(): Promise<Session | null>;
25
+ getUser(): Promise<User | null>;
26
+ validate(): Promise<boolean>;
27
+ logout(options?: DirectAuthLogoutOptions): Promise<void>;
28
+ exchangeToken(authCode: string, codeVerifier: string, requestRefreshToken?: boolean): Promise<{
29
+ session?: Session;
30
+ user?: User;
31
+ }>;
32
+ /**
33
+ * Revalidate the current session by clearing cache and fetching fresh data.
34
+ *
35
+ * Note: This does NOT perform OAuth token refresh (grant_type: 'refresh_token').
36
+ * Token refresh is handled automatically by the server when the access token
37
+ * is still valid but needs renewal.
38
+ *
39
+ * For explicit token refresh, use the refresh token flow through the server.
40
+ *
41
+ * @returns Fresh session data or null if not authenticated
42
+ */
43
+ refresh(): Promise<Session | null>;
44
+ isAuthenticated(): Promise<boolean>;
45
+ clearCache(): void;
46
+ getToken(): string | null;
47
+ }
48
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/lib/direct-auth/session.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,uBAAuB,EAG5B,KAAK,IAAI,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAU9D,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,iBAAiB,CAAC;CACzB;AAaD,qBAAa,eAAgB,YAAW,WAAW;IACjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,aAAa,CAAwB;IAC7C,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;gBAE/B,OAAO,EAAE,qBAAqB;IAO1C,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,iBAAiB;IASnB,GAAG,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAyC9B,OAAO,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;IAS/B,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;IAY5B,MAAM,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC;IAqCxD,aAAa,CACjB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,mBAAmB,CAAC,EAAE,OAAO,GAC5B,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,IAAI,CAAA;KAAE,CAAC;IAgE9C;;;;;;;;;;OAUG;IACG,OAAO,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAKlC,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC;IAUzC,UAAU,IAAI,IAAI;IAMlB,QAAQ,IAAI,MAAM,GAAG,IAAI;CAG1B"}
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Session Management for Direct Auth
3
+ */
4
+ import { AuthrimError, } from '@authrim/core';
5
+ const ENDPOINTS = {
6
+ TOKEN: '/api/v1/auth/direct/token',
7
+ SESSION: '/api/v1/auth/direct/session',
8
+ LOGOUT: '/api/v1/auth/direct/logout',
9
+ };
10
+ const STORAGE_KEY_PREFIX = 'authrim_session';
11
+ function getStorageKey(issuer, clientId) {
12
+ const key = `${issuer}:${clientId}`;
13
+ let hash = 0;
14
+ for (let i = 0; i < key.length; i++) {
15
+ const char = key.charCodeAt(i);
16
+ hash = (hash << 5) - hash + char;
17
+ hash = hash & hash;
18
+ }
19
+ return `${STORAGE_KEY_PREFIX}_${Math.abs(hash).toString(36)}`;
20
+ }
21
+ export class SessionAuthImpl {
22
+ issuer;
23
+ clientId;
24
+ http;
25
+ storageKey;
26
+ cachedSession = null;
27
+ cachedUser = null;
28
+ sessionCacheExpiry = 0;
29
+ SESSION_CACHE_TTL = 60000;
30
+ constructor(options) {
31
+ this.issuer = options.issuer;
32
+ this.clientId = options.clientId;
33
+ this.http = options.http;
34
+ this.storageKey = getStorageKey(options.issuer, options.clientId);
35
+ }
36
+ getStoredToken() {
37
+ if (typeof localStorage === 'undefined')
38
+ return null;
39
+ try {
40
+ return localStorage.getItem(this.storageKey);
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ storeToken(token) {
47
+ if (typeof localStorage === 'undefined')
48
+ return;
49
+ try {
50
+ localStorage.setItem(this.storageKey, token);
51
+ }
52
+ catch {
53
+ console.warn('[Authrim] Failed to store token in localStorage');
54
+ }
55
+ }
56
+ removeStoredToken() {
57
+ if (typeof localStorage === 'undefined')
58
+ return;
59
+ try {
60
+ localStorage.removeItem(this.storageKey);
61
+ }
62
+ catch {
63
+ // localStorage not available
64
+ }
65
+ }
66
+ async get() {
67
+ if (this.cachedSession && Date.now() < this.sessionCacheExpiry) {
68
+ return this.cachedSession;
69
+ }
70
+ const token = this.getStoredToken();
71
+ if (!token) {
72
+ this.clearCache();
73
+ return null;
74
+ }
75
+ try {
76
+ const response = await this.http.fetch(`${this.issuer}${ENDPOINTS.SESSION}`, {
77
+ method: 'GET',
78
+ headers: {
79
+ Authorization: `Bearer ${token}`,
80
+ },
81
+ });
82
+ if (!response.ok || !response.data) {
83
+ if (response.status === 401) {
84
+ this.removeStoredToken();
85
+ }
86
+ this.clearCache();
87
+ return null;
88
+ }
89
+ this.cachedSession = response.data.session;
90
+ this.cachedUser = response.data.user;
91
+ this.sessionCacheExpiry = Date.now() + this.SESSION_CACHE_TTL;
92
+ return response.data.session;
93
+ }
94
+ catch {
95
+ this.clearCache();
96
+ return null;
97
+ }
98
+ }
99
+ async getUser() {
100
+ if (this.cachedUser && Date.now() < this.sessionCacheExpiry) {
101
+ return this.cachedUser;
102
+ }
103
+ await this.get();
104
+ return this.cachedUser;
105
+ }
106
+ async validate() {
107
+ try {
108
+ const session = await this.get();
109
+ if (!session)
110
+ return false;
111
+ const expiresAt = new Date(session.expiresAt).getTime();
112
+ return Date.now() < expiresAt;
113
+ }
114
+ catch {
115
+ return false;
116
+ }
117
+ }
118
+ async logout(options) {
119
+ const token = this.getStoredToken();
120
+ if (token) {
121
+ try {
122
+ const requestBody = {
123
+ client_id: this.clientId,
124
+ };
125
+ if (options?.revokeTokens !== undefined) {
126
+ requestBody.revoke_tokens = options.revokeTokens;
127
+ }
128
+ await this.http.fetch(`${this.issuer}${ENDPOINTS.LOGOUT}`, {
129
+ method: 'POST',
130
+ headers: {
131
+ 'Content-Type': 'application/json',
132
+ Authorization: `Bearer ${token}`,
133
+ },
134
+ body: JSON.stringify(requestBody),
135
+ });
136
+ }
137
+ catch (error) {
138
+ console.warn('Logout request failed:', error);
139
+ }
140
+ }
141
+ this.removeStoredToken();
142
+ this.clearCache();
143
+ if (options?.redirectUri && typeof window !== 'undefined') {
144
+ window.location.href = options.redirectUri;
145
+ }
146
+ }
147
+ async exchangeToken(authCode, codeVerifier, requestRefreshToken) {
148
+ const request = {
149
+ grant_type: 'authorization_code',
150
+ code: authCode,
151
+ client_id: this.clientId,
152
+ code_verifier: codeVerifier,
153
+ request_refresh_token: requestRefreshToken,
154
+ };
155
+ const response = await this.http.fetch(`${this.issuer}${ENDPOINTS.TOKEN}`, {
156
+ method: 'POST',
157
+ headers: { 'Content-Type': 'application/json' },
158
+ body: JSON.stringify(request),
159
+ });
160
+ if (!response.ok || !response.data) {
161
+ if (response.status === 400) {
162
+ const errorData = response.data;
163
+ if (errorData?.error === 'invalid_grant') {
164
+ throw new AuthrimError('auth_code_invalid', errorData.error_description || 'Invalid authorization code');
165
+ }
166
+ if (errorData?.error === 'expired_token') {
167
+ throw new AuthrimError('auth_code_expired', errorData.error_description || 'Authorization code has expired');
168
+ }
169
+ }
170
+ throw new AuthrimError('token_error', 'Failed to exchange authorization code for tokens');
171
+ }
172
+ const tokenResponse = response.data;
173
+ if (tokenResponse.access_token) {
174
+ this.storeToken(tokenResponse.access_token);
175
+ }
176
+ if (tokenResponse.session) {
177
+ this.cachedSession = tokenResponse.session;
178
+ this.sessionCacheExpiry = Date.now() + this.SESSION_CACHE_TTL;
179
+ }
180
+ if (tokenResponse.user) {
181
+ this.cachedUser = tokenResponse.user;
182
+ }
183
+ return {
184
+ session: tokenResponse.session,
185
+ user: tokenResponse.user,
186
+ };
187
+ }
188
+ /**
189
+ * Revalidate the current session by clearing cache and fetching fresh data.
190
+ *
191
+ * Note: This does NOT perform OAuth token refresh (grant_type: 'refresh_token').
192
+ * Token refresh is handled automatically by the server when the access token
193
+ * is still valid but needs renewal.
194
+ *
195
+ * For explicit token refresh, use the refresh token flow through the server.
196
+ *
197
+ * @returns Fresh session data or null if not authenticated
198
+ */
199
+ async refresh() {
200
+ this.clearCache();
201
+ return this.get();
202
+ }
203
+ async isAuthenticated() {
204
+ const token = this.getStoredToken();
205
+ if (!token) {
206
+ return false;
207
+ }
208
+ const session = await this.get();
209
+ return session !== null;
210
+ }
211
+ clearCache() {
212
+ this.cachedSession = null;
213
+ this.cachedUser = null;
214
+ this.sessionCacheExpiry = 0;
215
+ }
216
+ getToken() {
217
+ return this.getStoredToken();
218
+ }
219
+ }