@korajs/auth 1.0.0-beta.12 → 1.0.0-beta.14

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 (101) hide show
  1. package/README.md +52 -47
  2. package/dist/{create-org-session-RsDj9cl4.d.cts → create-org-session-ChFdulEM.d.cts} +211 -17
  3. package/dist/{create-org-session-RsDj9cl4.d.ts → create-org-session-ChFdulEM.d.ts} +211 -17
  4. package/dist/index.cjs +645 -150
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +27 -9
  7. package/dist/index.d.ts +27 -9
  8. package/dist/index.js +644 -150
  9. package/dist/index.js.map +1 -1
  10. package/dist/{operation-encryptor-DRmKNWpF.d.cts → operation-encryptor-DDdlb9bm.d.cts} +16 -0
  11. package/dist/{operation-encryptor-DRmKNWpF.d.ts → operation-encryptor-DDdlb9bm.d.ts} +16 -0
  12. package/dist/react.d.cts +2 -2
  13. package/dist/react.d.ts +2 -2
  14. package/dist/server.cjs +2880 -1667
  15. package/dist/server.cjs.map +1 -1
  16. package/dist/server.d.cts +810 -169
  17. package/dist/server.d.ts +810 -169
  18. package/dist/server.js +2848 -1646
  19. package/dist/server.js.map +1 -1
  20. package/dist/svelte.cjs +2 -2
  21. package/dist/svelte.cjs.map +1 -1
  22. package/dist/svelte.d.cts +2 -2
  23. package/dist/svelte.d.ts +2 -2
  24. package/dist/svelte.js +2 -2
  25. package/dist/svelte.js.map +1 -1
  26. package/dist/vue.d.cts +1 -1
  27. package/dist/vue.d.ts +1 -1
  28. package/package.json +7 -7
  29. package/src/admin/admin-api.ts +327 -0
  30. package/src/admin/audit-log.ts +324 -0
  31. package/src/admin/webhooks.ts +576 -0
  32. package/src/bindings/create-auth-session.ts +184 -0
  33. package/src/bindings/create-org-session.ts +130 -0
  34. package/src/client/auth-client.ts +1592 -0
  35. package/src/client/auth-sync.ts +213 -0
  36. package/src/client/device-session.ts +104 -0
  37. package/src/client/org-client.ts +399 -0
  38. package/src/client/quickstart.ts +108 -0
  39. package/src/client/storage.ts +94 -0
  40. package/src/device/device-identity.ts +330 -0
  41. package/src/device/device-store.ts +379 -0
  42. package/src/encryption/auto-lock.ts +170 -0
  43. package/src/encryption/database-encryption.ts +265 -0
  44. package/src/encryption/key-derivation.ts +149 -0
  45. package/src/encryption/operation-encryptor.ts +361 -0
  46. package/src/index.ts +132 -0
  47. package/src/mfa/totp.ts +826 -0
  48. package/src/org/org-routes.ts +758 -0
  49. package/src/org/org-store.ts +490 -0
  50. package/src/org/org-types.ts +230 -0
  51. package/src/passkey/passkey-client.ts +597 -0
  52. package/src/passkey/passkey-server.ts +779 -0
  53. package/src/postgres/ensure-schema.ts +65 -0
  54. package/src/provider/adapter.ts +246 -0
  55. package/src/provider/built-in/auth-routes.ts +1313 -0
  56. package/src/provider/built-in/email-verification.ts +303 -0
  57. package/src/provider/built-in/password-hash.ts +118 -0
  58. package/src/provider/built-in/password-reset.ts +416 -0
  59. package/src/provider/built-in/postgres-user-store.ts +365 -0
  60. package/src/provider/built-in/quickstart-server.ts +760 -0
  61. package/src/provider/built-in/sqlite-user-store.ts +335 -0
  62. package/src/provider/built-in/sync-scopes.ts +85 -0
  63. package/src/provider/built-in/user-store.ts +465 -0
  64. package/src/provider/external/clerk-adapter.ts +157 -0
  65. package/src/provider/external/external-jwt-provider.ts +491 -0
  66. package/src/provider/external/supabase-adapter.ts +163 -0
  67. package/src/provider/oauth/linked-identity-store.ts +108 -0
  68. package/src/provider/oauth/oauth-flow.ts +550 -0
  69. package/src/provider/oauth/oauth-types.ts +184 -0
  70. package/src/provider/oauth/postgres-oauth-store.ts +296 -0
  71. package/src/provider/oauth/sqlite-oauth-store.ts +285 -0
  72. package/src/rbac/rbac-engine.ts +323 -0
  73. package/src/rbac/rbac-types.ts +210 -0
  74. package/src/rbac/scope-resolver.ts +140 -0
  75. package/src/react/AuthProvider.tsx +97 -0
  76. package/src/react/OrgProvider.tsx +41 -0
  77. package/src/react/auth-context.ts +26 -0
  78. package/src/react/hooks.ts +110 -0
  79. package/src/react/org-hooks.ts +214 -0
  80. package/src/react.ts +26 -0
  81. package/src/server.ts +338 -0
  82. package/src/session/session.ts +401 -0
  83. package/src/svelte/auth-context.ts +50 -0
  84. package/src/svelte/org-context.ts +32 -0
  85. package/src/svelte/org-hooks.ts +201 -0
  86. package/src/svelte/use-auth.ts +115 -0
  87. package/src/svelte.ts +25 -0
  88. package/src/tokens/encrypted-token-store.ts +360 -0
  89. package/src/tokens/jwt.ts +236 -0
  90. package/src/tokens/postgres-token-revocation-store.ts +140 -0
  91. package/src/tokens/sqlite-token-revocation-store.ts +121 -0
  92. package/src/tokens/token-manager.ts +821 -0
  93. package/src/tokens/token-store.ts +192 -0
  94. package/src/types.ts +394 -0
  95. package/src/vue/auth-context.ts +10 -0
  96. package/src/vue/auth-provider-types.ts +5 -0
  97. package/src/vue/auth-provider.ts +76 -0
  98. package/src/vue/org-hooks.ts +193 -0
  99. package/src/vue/org-provider.ts +49 -0
  100. package/src/vue/use-auth.ts +139 -0
  101. package/src/vue.ts +10 -0
@@ -0,0 +1,115 @@
1
+ import { type Readable, readable } from 'svelte/store'
2
+ import type { AuthSessionSnapshot } from '../bindings/create-auth-session'
3
+ import type {
4
+ AuthUser,
5
+ LinkedOAuthAccount,
6
+ OAuthAuthorizationOptions,
7
+ OAuthAuthorizationResult,
8
+ OAuthCallbackParams,
9
+ } from '../client/auth-client'
10
+ import { getAuthContext } from './auth-context'
11
+
12
+ export interface UseAuthResult extends AuthSessionSnapshot {
13
+ signUp: (params: {
14
+ email: string
15
+ password: string
16
+ name?: string
17
+ deviceId?: string
18
+ devicePublicKey?: string
19
+ }) => Promise<void>
20
+ signIn: (params: {
21
+ email: string
22
+ password: string
23
+ deviceId?: string
24
+ devicePublicKey?: string
25
+ }) => Promise<void>
26
+ signInWithOAuth: (
27
+ provider: string,
28
+ options?: OAuthAuthorizationOptions,
29
+ ) => Promise<OAuthAuthorizationResult>
30
+ completeOAuthSignIn: (provider: string, params: OAuthCallbackParams) => Promise<void>
31
+ getOAuthAuthorizationUrl: (
32
+ provider: string,
33
+ options?: OAuthAuthorizationOptions,
34
+ ) => Promise<OAuthAuthorizationResult>
35
+ linkOAuth: (provider: string, params: OAuthCallbackParams) => Promise<LinkedOAuthAccount | null>
36
+ listLinkedAccounts: () => Promise<LinkedOAuthAccount[]>
37
+ unlinkOAuth: (provider: string) => Promise<void>
38
+ signOut: () => Promise<void>
39
+ }
40
+
41
+ export function createAuthStore(): Readable<UseAuthResult> {
42
+ const { session } = getAuthContext()
43
+
44
+ return readable<UseAuthResult>(buildResult(session), (set) => {
45
+ const sync = (): void => {
46
+ set(buildResult(session))
47
+ }
48
+ sync()
49
+ return session.subscribe(sync)
50
+ })
51
+ }
52
+
53
+ /** @alias createAuthStore */
54
+ export const useAuth = createAuthStore
55
+
56
+ function buildResult(session: ReturnType<typeof getAuthContext>['session']): UseAuthResult {
57
+ const snapshot = session.getSnapshot()
58
+ return {
59
+ ...snapshot,
60
+ signUp: (params) => session.signUp(params),
61
+ signIn: (params) => session.signIn(params),
62
+ signInWithOAuth: (provider, options) => session.signInWithOAuth(provider, options),
63
+ completeOAuthSignIn: (provider, params) => session.completeOAuthSignIn(provider, params),
64
+ getOAuthAuthorizationUrl: (provider, options) =>
65
+ session.getOAuthAuthorizationUrl(provider, options),
66
+ linkOAuth: (provider, params) => session.linkOAuth(provider, params),
67
+ listLinkedAccounts: () => session.listLinkedAccounts(),
68
+ unlinkOAuth: (provider) => session.unlinkOAuth(provider),
69
+ signOut: () => session.signOut(),
70
+ }
71
+ }
72
+
73
+ export function createCurrentUserStore(): Readable<AuthUser | null> {
74
+ const { session } = getAuthContext()
75
+ return readable<AuthUser | null>(session.getSnapshot().user, (set) => {
76
+ const sync = (): void => {
77
+ set(session.getSnapshot().user)
78
+ }
79
+ sync()
80
+ return session.subscribe(sync)
81
+ })
82
+ }
83
+
84
+ /** @alias createCurrentUserStore */
85
+ export const useCurrentUser = createCurrentUserStore
86
+
87
+ export function createAuthStatusStore(): Readable<{
88
+ state: AuthSessionSnapshot['state']
89
+ isAuthenticated: boolean
90
+ isLoading: boolean
91
+ }> {
92
+ const { session } = getAuthContext()
93
+ return readable(
94
+ {
95
+ state: session.getSnapshot().state,
96
+ isAuthenticated: session.getSnapshot().isAuthenticated,
97
+ isLoading: session.getSnapshot().isLoading,
98
+ },
99
+ (set) => {
100
+ const sync = (): void => {
101
+ const snapshot = session.getSnapshot()
102
+ set({
103
+ state: snapshot.state,
104
+ isAuthenticated: snapshot.isAuthenticated,
105
+ isLoading: snapshot.isLoading,
106
+ })
107
+ }
108
+ sync()
109
+ return session.subscribe(sync)
110
+ },
111
+ )
112
+ }
113
+
114
+ /** @alias createAuthStatusStore */
115
+ export const useAuthStatus = createAuthStatusStore
package/src/svelte.ts ADDED
@@ -0,0 +1,25 @@
1
+ export {
2
+ destroyAuthProvider,
3
+ getAuthContext,
4
+ initAuthProvider,
5
+ } from './svelte/auth-context'
6
+ export type { AuthContextValue } from './svelte/auth-context'
7
+ export {
8
+ createAuthStore,
9
+ createAuthStatusStore,
10
+ createCurrentUserStore,
11
+ useAuth,
12
+ useAuthStatus,
13
+ useCurrentUser,
14
+ } from './svelte/use-auth'
15
+ export type { UseAuthResult } from './svelte/use-auth'
16
+ export { destroyOrgProvider, getOrgContext, initOrgProvider } from './svelte/org-context'
17
+ export type { OrgContextValue } from './svelte/org-context'
18
+ export {
19
+ createPermissionStore,
20
+ useOrg,
21
+ useOrgMembers,
22
+ usePermission,
23
+ checkOrgPermission,
24
+ } from './svelte/org-hooks'
25
+ export type { UseOrgResult, UseOrgMembersResult } from './svelte/org-hooks'
@@ -0,0 +1,360 @@
1
+ import { KoraError } from '@korajs/core'
2
+ import { decryptData, encryptData } from '../encryption/database-encryption'
3
+ import type { AuthTokens } from '../types'
4
+
5
+ // --- Errors ---
6
+
7
+ /**
8
+ * Thrown when encrypted token storage operations fail.
9
+ * Includes context about what went wrong to aid debugging without
10
+ * exposing sensitive key or token material.
11
+ */
12
+ export class EncryptedTokenStoreError extends KoraError {
13
+ constructor(message: string, context?: Record<string, unknown>) {
14
+ super(message, 'ENCRYPTED_TOKEN_STORE_ERROR', context)
15
+ this.name = 'EncryptedTokenStoreError'
16
+ }
17
+ }
18
+
19
+ // --- Encoding helpers ---
20
+
21
+ /**
22
+ * Encodes a Uint8Array as a base64url string (no padding).
23
+ * Implemented locally to avoid coupling to the device-identity module.
24
+ *
25
+ * @param bytes - The binary data to encode
26
+ * @returns A base64url-encoded string without padding characters
27
+ */
28
+ function toBase64Url(bytes: Uint8Array): string {
29
+ let binary = ''
30
+ for (let i = 0; i < bytes.length; i++) {
31
+ binary += String.fromCharCode(bytes[i] as number)
32
+ }
33
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
34
+ }
35
+
36
+ /**
37
+ * Decodes a base64url string (no padding) into a Uint8Array.
38
+ *
39
+ * @param str - A base64url-encoded string (with or without padding)
40
+ * @returns The decoded binary data as a Uint8Array
41
+ */
42
+ function fromBase64Url(str: string): Uint8Array {
43
+ let base64 = str.replace(/-/g, '+').replace(/_/g, '/')
44
+ const paddingNeeded = (4 - (base64.length % 4)) % 4
45
+ base64 += '='.repeat(paddingNeeded)
46
+
47
+ const binary = atob(base64)
48
+ const bytes = new Uint8Array(binary.length)
49
+ for (let i = 0; i < binary.length; i++) {
50
+ bytes[i] = binary.charCodeAt(i)
51
+ }
52
+ return bytes
53
+ }
54
+
55
+ // --- Storage helpers ---
56
+
57
+ /**
58
+ * Minimal storage interface matching the subset of the Web Storage API
59
+ * that EncryptedTokenStore needs. Allows both localStorage and in-memory
60
+ * implementations to be used interchangeably.
61
+ */
62
+ interface SimpleStorage {
63
+ getItem(key: string): string | null
64
+ setItem(key: string, value: string): void
65
+ removeItem(key: string): void
66
+ }
67
+
68
+ /**
69
+ * In-memory storage fallback used when localStorage is unavailable
70
+ * (e.g., in Node.js, SSR environments, or when storage access is denied).
71
+ */
72
+ class MemoryStorage implements SimpleStorage {
73
+ private store = new Map<string, string>()
74
+
75
+ getItem(key: string): string | null {
76
+ return this.store.get(key) ?? null
77
+ }
78
+
79
+ setItem(key: string, value: string): void {
80
+ this.store.set(key, value)
81
+ }
82
+
83
+ removeItem(key: string): void {
84
+ this.store.delete(key)
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Attempts to access localStorage. Returns null if unavailable.
90
+ *
91
+ * localStorage may be unavailable in several scenarios:
92
+ * - Node.js / server-side rendering (no window object)
93
+ * - Private browsing modes with restricted storage
94
+ * - iframe sandboxing without storage access
95
+ * - User has disabled cookies/storage in browser settings
96
+ */
97
+ function tryGetLocalStorage(): SimpleStorage | null {
98
+ try {
99
+ if (typeof globalThis !== 'undefined' && 'localStorage' in globalThis) {
100
+ const storage = globalThis.localStorage as SimpleStorage
101
+ const testKey = '__kora_encrypted_storage_test__'
102
+ storage.setItem(testKey, '1')
103
+ storage.removeItem(testKey)
104
+ return storage
105
+ }
106
+ } catch {
107
+ // localStorage exists but access is denied
108
+ }
109
+ return null
110
+ }
111
+
112
+ // --- Types ---
113
+
114
+ /** Default storage key prefix for encrypted tokens. */
115
+ const DEFAULT_STORAGE_KEY = 'kora_auth_encrypted'
116
+
117
+ /**
118
+ * The serialized format stored in localStorage.
119
+ * Both `iv` and `data` are base64url-encoded strings.
120
+ */
121
+ interface EncryptedPayload {
122
+ /** Base64url-encoded initialization vector (12 bytes for AES-GCM) */
123
+ iv: string
124
+ /** Base64url-encoded AES-256-GCM ciphertext of the JSON-serialized AuthTokens */
125
+ data: string
126
+ }
127
+
128
+ /**
129
+ * Configuration for the encrypted token store.
130
+ */
131
+ export interface EncryptedTokenStoreConfig {
132
+ /**
133
+ * Storage key prefix. Defaults to 'kora_auth_encrypted'.
134
+ * Use different keys if your app runs multiple Kora instances with separate auth.
135
+ */
136
+ storageKey?: string
137
+ /**
138
+ * The AES-256-GCM CryptoKey used to encrypt and decrypt tokens.
139
+ *
140
+ * This can be:
141
+ * - A key derived from a user passphrase via {@link deriveEncryptionKey}
142
+ * - A device-derived key (e.g., from secure hardware or biometric unlock)
143
+ * - A randomly generated key from {@link generateEncryptionKey}
144
+ *
145
+ * The caller is responsible for obtaining the key before constructing the store.
146
+ */
147
+ key: CryptoKey
148
+ }
149
+
150
+ // --- Implementation ---
151
+
152
+ /**
153
+ * Encrypted token store that protects auth tokens at rest.
154
+ *
155
+ * Addresses the security vulnerability of storing tokens in plaintext localStorage,
156
+ * which is accessible to any JavaScript running on the page (XSS attack surface).
157
+ * Tokens are encrypted with AES-256-GCM before being written to storage.
158
+ *
159
+ * The stored format is a JSON string with two base64url-encoded fields:
160
+ * - `iv`: the 12-byte initialization vector (unique per encryption)
161
+ * - `data`: the AES-256-GCM ciphertext of the JSON-serialized tokens
162
+ *
163
+ * AES-GCM provides both confidentiality (tokens are unreadable without the key)
164
+ * and integrity (tampered ciphertext is detected and rejected).
165
+ *
166
+ * @example
167
+ * ```typescript
168
+ * import { deriveEncryptionKey } from '@korajs/auth'
169
+ *
170
+ * // Derive a key from the user's passphrase
171
+ * const { key } = await deriveEncryptionKey('user-passphrase', storedSalt)
172
+ *
173
+ * const store = new EncryptedTokenStore({ key })
174
+ *
175
+ * // After login: encrypt and persist tokens
176
+ * await store.saveTokens({ accessToken: '...', refreshToken: '...' })
177
+ *
178
+ * // Before API calls: decrypt and retrieve
179
+ * const accessToken = await store.getAccessToken()
180
+ *
181
+ * // On logout: remove encrypted data
182
+ * store.clearTokens()
183
+ * ```
184
+ */
185
+ export class EncryptedTokenStore {
186
+ private readonly storageKey: string
187
+ private readonly key: CryptoKey
188
+ private readonly storage: SimpleStorage
189
+
190
+ /**
191
+ * Creates a new EncryptedTokenStore instance.
192
+ *
193
+ * @param config - Configuration including the encryption key and optional storage key
194
+ */
195
+ constructor(config: EncryptedTokenStoreConfig) {
196
+ this.storageKey = config.storageKey ?? DEFAULT_STORAGE_KEY
197
+ this.key = config.key
198
+ this.storage = tryGetLocalStorage() ?? new MemoryStorage()
199
+ }
200
+
201
+ /**
202
+ * Encrypt and save tokens to persistent storage.
203
+ *
204
+ * Serializes the tokens as JSON, encrypts with AES-256-GCM using a fresh
205
+ * random IV, then stores the result as a JSON object containing the
206
+ * base64url-encoded IV and ciphertext.
207
+ *
208
+ * Overwrites any previously stored tokens.
209
+ *
210
+ * @param tokens - The token set to encrypt and store
211
+ * @throws {EncryptedTokenStoreError} If encryption fails
212
+ */
213
+ async saveTokens(tokens: AuthTokens): Promise<void> {
214
+ // Build a clean token object with only the expected fields
215
+ const serialized: AuthTokens = {
216
+ accessToken: tokens.accessToken,
217
+ refreshToken: tokens.refreshToken,
218
+ }
219
+ if (tokens.deviceCredential !== undefined) {
220
+ serialized.deviceCredential = tokens.deviceCredential
221
+ }
222
+
223
+ const plaintext = new TextEncoder().encode(JSON.stringify(serialized))
224
+
225
+ try {
226
+ const { ciphertext, iv } = await encryptData(this.key, plaintext)
227
+
228
+ const payload: EncryptedPayload = {
229
+ iv: toBase64Url(iv),
230
+ data: toBase64Url(ciphertext),
231
+ }
232
+
233
+ this.storage.setItem(this.storageKey, JSON.stringify(payload))
234
+ } catch (cause) {
235
+ // Re-throw EncryptedTokenStoreError as-is
236
+ if (cause instanceof EncryptedTokenStoreError) {
237
+ throw cause
238
+ }
239
+
240
+ throw new EncryptedTokenStoreError(
241
+ 'Failed to encrypt and save auth tokens. ' +
242
+ 'Ensure the encryption key is a valid AES-256-GCM CryptoKey.',
243
+ { cause: cause instanceof Error ? cause.message : String(cause) },
244
+ )
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Load and decrypt tokens from storage.
250
+ *
251
+ * Reads the encrypted payload from localStorage, decodes the base64url IV
252
+ * and ciphertext, decrypts with AES-256-GCM, and parses the resulting JSON.
253
+ *
254
+ * Returns null (without throwing) if:
255
+ * - No tokens have been saved
256
+ * - The stored data is corrupted or not valid JSON
257
+ * - Decryption fails (wrong key, tampered ciphertext, or wrong IV)
258
+ * - The decrypted data does not contain valid token fields
259
+ *
260
+ * This fail-silent design prevents decryption errors from crashing the
261
+ * application. The caller should treat null as "no valid tokens available"
262
+ * and initiate a re-authentication flow.
263
+ *
264
+ * @returns The decrypted {@link AuthTokens}, or null if unavailable or decryption fails
265
+ */
266
+ async loadTokens(): Promise<AuthTokens | null> {
267
+ const raw = this.storage.getItem(this.storageKey)
268
+ if (raw === null) {
269
+ return null
270
+ }
271
+
272
+ try {
273
+ // Parse the stored encrypted payload
274
+ const parsed: unknown = JSON.parse(raw)
275
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
276
+ return null
277
+ }
278
+
279
+ const record = parsed as Record<string, unknown>
280
+ if (typeof record.iv !== 'string' || typeof record.data !== 'string') {
281
+ return null
282
+ }
283
+
284
+ // Decode the base64url-encoded IV and ciphertext
285
+ const iv = fromBase64Url(record.iv)
286
+ const ciphertext = fromBase64Url(record.data)
287
+
288
+ // Decrypt with AES-256-GCM
289
+ const plaintextBytes = await decryptData(this.key, ciphertext, iv)
290
+ const json = new TextDecoder().decode(plaintextBytes)
291
+
292
+ // Parse the decrypted JSON and validate token structure
293
+ const tokenData: unknown = JSON.parse(json)
294
+ if (typeof tokenData !== 'object' || tokenData === null || Array.isArray(tokenData)) {
295
+ return null
296
+ }
297
+
298
+ const tokenRecord = tokenData as Record<string, unknown>
299
+ if (
300
+ typeof tokenRecord.accessToken !== 'string' ||
301
+ typeof tokenRecord.refreshToken !== 'string'
302
+ ) {
303
+ return null
304
+ }
305
+
306
+ const tokens: AuthTokens = {
307
+ accessToken: tokenRecord.accessToken,
308
+ refreshToken: tokenRecord.refreshToken,
309
+ }
310
+
311
+ if (typeof tokenRecord.deviceCredential === 'string') {
312
+ tokens.deviceCredential = tokenRecord.deviceCredential
313
+ }
314
+
315
+ return tokens
316
+ } catch {
317
+ // Decryption failure, corrupted data, or JSON parse error.
318
+ // Return null instead of throwing to allow graceful fallback
319
+ // to re-authentication.
320
+ return null
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Clear all stored encrypted tokens.
326
+ *
327
+ * Removes the encrypted payload from storage. This is a synchronous
328
+ * operation since it only removes the localStorage entry.
329
+ *
330
+ * Call this on logout to ensure no encrypted credential material
331
+ * remains in persistent storage.
332
+ */
333
+ clearTokens(): void {
334
+ this.storage.removeItem(this.storageKey)
335
+ }
336
+
337
+ /**
338
+ * Get the current access token by decrypting stored tokens.
339
+ *
340
+ * Returns the raw token string without validating expiration.
341
+ * The caller is responsible for checking whether the token is
342
+ * still valid and initiating a refresh if needed.
343
+ *
344
+ * @returns The decrypted access token string, or null if no valid tokens are stored
345
+ */
346
+ async getAccessToken(): Promise<string | null> {
347
+ const tokens = await this.loadTokens()
348
+ return tokens?.accessToken ?? null
349
+ }
350
+
351
+ /**
352
+ * Get the current refresh token by decrypting stored tokens.
353
+ *
354
+ * @returns The decrypted refresh token string, or null if no valid tokens are stored
355
+ */
356
+ async getRefreshToken(): Promise<string | null> {
357
+ const tokens = await this.loadTokens()
358
+ return tokens?.refreshToken ?? null
359
+ }
360
+ }