@korajs/auth 0.3.3 → 0.5.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.
package/README.md CHANGED
@@ -17,6 +17,23 @@ Offline-first authentication for Kora.js applications.
17
17
  - **Passkeys (WebAuthn)** -- passwordless authentication with platform authenticators
18
18
  - **Encrypted token storage** -- AES-256-GCM encryption for sensitive environments
19
19
  - **End-to-end encryption** -- encrypt operation data before sync with `OperationEncryptor`
20
+ - **Sync auth binding** -- `createKoraAuthSync()` wires tokens, JWT scopes, and device node ids to `createApp`
21
+
22
+ The client APIs work in browser, Tauri desktop WebView, and mobile JavaScript environments. For desktop apps, run auth routes on your remote sync/auth server and point `AuthClient.serverUrl` at that server. Email/password auth, token refresh, sync authorization, MFA, organizations, and RBAC work across web and desktop clients. Passkeys should be feature-detected because WebAuthn support depends on the operating system WebView.
23
+
24
+ For production desktop and mobile apps, pass a custom token storage adapter backed by the platform credential store and attach a stable device identity:
25
+
26
+ ```typescript
27
+ import { createKoraAuth } from '@korajs/auth'
28
+
29
+ const authClient = createKoraAuth({
30
+ serverUrl: 'https://acme.example.com',
31
+ credentialStore: secureStore,
32
+ deviceKeyStore,
33
+ })
34
+ ```
35
+
36
+ `createKoraAuth()` uses IndexedDB for the device key pair when available. React Native and other runtimes without IndexedDB should pass a platform-backed `deviceKeyStore`.
20
37
 
21
38
  ## Installation
22
39
 
@@ -29,10 +46,10 @@ pnpm add @korajs/auth
29
46
  ### Client-side (React)
30
47
 
31
48
  ```tsx
32
- import { AuthClient } from '@korajs/auth'
49
+ import { createKoraAuth } from '@korajs/auth'
33
50
  import { AuthProvider, useAuth } from '@korajs/auth/react'
34
51
 
35
- const authClient = new AuthClient({ serverUrl: 'http://localhost:3001' })
52
+ const authClient = createKoraAuth({ serverUrl: 'http://localhost:3001' })
36
53
 
37
54
  function App() {
38
55
  return (
@@ -43,15 +60,20 @@ function App() {
43
60
  }
44
61
 
45
62
  function MyApp() {
46
- const { user, isAuthenticated, isLoading, signIn, signOut, error } = useAuth()
63
+ const { user, isAuthenticated, isLoading, signIn, signInWithOAuth, signOut, error } = useAuth()
47
64
 
48
65
  if (isLoading) return <div>Loading...</div>
49
66
 
50
67
  if (!isAuthenticated) {
51
68
  return (
52
- <button onClick={() => signIn({ email: 'user@example.com', password: 'password' })}>
53
- Sign In
54
- </button>
69
+ <>
70
+ <button onClick={() => signIn({ email: 'user@example.com', password: 'password' })}>
71
+ Sign In
72
+ </button>
73
+ <button onClick={() => signInWithOAuth('google')}>
74
+ Sign In with Google
75
+ </button>
76
+ </>
55
77
  )
56
78
  }
57
79
 
@@ -64,41 +86,66 @@ function MyApp() {
64
86
  }
65
87
  ```
66
88
 
67
- ### Server-side
68
-
69
- ```typescript
70
- import { BuiltInAuthRoutes, InMemoryUserStore, TokenManager } from '@korajs/auth/server'
71
-
72
- const userStore = new InMemoryUserStore()
73
- const tokenManager = new TokenManager({ secret: process.env.AUTH_SECRET! })
74
- const authRoutes = new BuiltInAuthRoutes({ userStore, tokenManager })
89
+ ### Sync integration
75
90
 
76
- // Wire into your HTTP server:
77
- app.post('/auth/signup', async (req, res) => {
78
- const result = await authRoutes.handleSignUp(req.body)
79
- res.status(result.status).json(result.body)
91
+ ```tsx
92
+ import { createKoraAuthSync } from '@korajs/auth'
93
+ import { createApp } from 'korajs'
94
+
95
+ const app = createApp({
96
+ schema,
97
+ sync: {
98
+ url: 'ws://localhost:3001/kora-sync',
99
+ authClient: createKoraAuthSync({ authClient, schema }),
100
+ },
80
101
  })
102
+ ```
81
103
 
82
- app.post('/auth/signin', async (req, res) => {
83
- const result = await authRoutes.handleSignIn(req.body)
84
- res.status(result.status).json(result.body)
104
+ ### Server-side
105
+
106
+ ```typescript
107
+ import {
108
+ createKoraAuthServer,
109
+ createSqliteOAuthStores,
110
+ googleProvider,
111
+ } from '@korajs/auth/server'
112
+
113
+ const oauthStores = await createSqliteOAuthStores({
114
+ filename: './auth.db',
85
115
  })
86
116
 
87
- app.post('/auth/refresh', async (req, res) => {
88
- const result = await authRoutes.handleRefresh(req.body)
89
- res.status(result.status).json(result.body)
117
+ const auth = createKoraAuthServer({
118
+ jwtSecret: process.env.KORA_AUTH_SECRET!,
119
+ oauth: {
120
+ providers: [
121
+ googleProvider({
122
+ clientId: process.env.GOOGLE_CLIENT_ID!,
123
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
124
+ redirectUri: 'https://app.example.com/auth/oauth/google/callback',
125
+ }),
126
+ ],
127
+ stateStore: oauthStores.stateStore,
128
+ linkedIdentityStore: oauthStores.linkedIdentityStore,
129
+ },
90
130
  })
91
131
 
92
- app.get('/auth/me', async (req, res) => {
93
- const token = req.headers.authorization?.replace('Bearer ', '') ?? ''
94
- const result = await authRoutes.handleGetMe(token)
132
+ // Wire into your HTTP server:
133
+ app.all('/auth/*', async (req, res) => {
134
+ const result = await auth.handleRequest({
135
+ method: req.method,
136
+ path: req.path,
137
+ body: req.body,
138
+ headers: req.headers,
139
+ query: req.query,
140
+ ip: req.ip,
141
+ })
95
142
  res.status(result.status).json(result.body)
96
143
  })
97
144
 
98
145
  // Bridge to Kora sync server:
99
146
  const syncServer = new KoraSyncServer({
100
147
  store,
101
- auth: authRoutes.toSyncAuthProvider(),
148
+ auth: auth.auth,
102
149
  })
103
150
  ```
104
151
 
@@ -108,6 +155,8 @@ const syncServer = new KoraSyncServer({
108
155
 
109
156
  | Export | Description |
110
157
  |--------|-------------|
158
+ | `createKoraAuth` | Quickstart client factory with storage and device identity defaults |
159
+ | `createKoraAuthSync` | Sync auth binding for `createApp({ sync: { authClient } })` |
111
160
  | `AuthClient` | Client-side auth manager (sign-up, sign-in, sign-out, token refresh) |
112
161
  | `OrgClient` | Client-side organization management |
113
162
  | `TokenStore` | Client-side token persistence (localStorage) |
@@ -139,10 +188,14 @@ const syncServer = new KoraSyncServer({
139
188
 
140
189
  | Export | Description |
141
190
  |--------|-------------|
191
+ | `createKoraAuthServer` | Quickstart server factory with auth routes and sync provider |
142
192
  | `BuiltInAuthRoutes` | HTTP route handlers for all auth operations |
143
193
  | `TokenManager` | JWT issuing, validation, refresh rotation, revocation |
144
194
  | `InMemoryUserStore` | Dev/test user store |
145
195
  | `InMemoryTokenRevocationStore` | Dev/test token revocation store |
196
+ | `OAuthManager` / provider helpers | OAuth authorization code flow and provider configs |
197
+ | `InMemoryLinkedIdentityStore` | Dev/test OAuth account-linking store |
198
+ | `createSqliteOAuthStores` / `createPostgresOAuthStores` | Durable OAuth state and linked identity stores |
146
199
  | `SessionManager` / `InMemorySessionStore` | Server-side session management |
147
200
  | `TotpManager` / `InMemoryTotpStore` | TOTP MFA with recovery codes |
148
201
  | `OrgRoutes` / `InMemoryOrgStore` | Organization CRUD, invitations, member management |
@@ -153,10 +153,9 @@ async function computePublicKeyThumbprint(publicKeyJwk) {
153
153
  const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", encoded);
154
154
  return toBase64Url(hashBuffer);
155
155
  } catch (cause) {
156
- throw new DeviceIdentityError(
157
- "Failed to compute SHA-256 thumbprint of the public key JWK.",
158
- { cause: cause instanceof Error ? cause.message : String(cause) }
159
- );
156
+ throw new DeviceIdentityError("Failed to compute SHA-256 thumbprint of the public key JWK.", {
157
+ cause: cause instanceof Error ? cause.message : String(cause)
158
+ });
160
159
  }
161
160
  }
162
161
 
@@ -458,9 +457,7 @@ function extractPublicKeyFromAttestationObject(attestationObject) {
458
457
  const topMap = decoded.value;
459
458
  const authData = topMap.get("authData");
460
459
  if (!(authData instanceof Uint8Array)) {
461
- throw new PasskeyError(
462
- "Invalid attestation object: authData is missing or not a byte string."
463
- );
460
+ throw new PasskeyError("Invalid attestation object: authData is missing or not a byte string.");
464
461
  }
465
462
  let offset = 0;
466
463
  offset += 32;
@@ -482,53 +479,54 @@ function extractPublicKeyFromAttestationObject(attestationObject) {
482
479
  return authData.slice(offset, offset + coseKeyLength);
483
480
  }
484
481
  function decodeCbor(data, offset) {
485
- if (offset >= data.length) {
482
+ let pos = offset;
483
+ if (pos >= data.length) {
486
484
  throw new PasskeyError("CBOR decode error: unexpected end of data.");
487
485
  }
488
- const initialByte = data[offset];
486
+ const initialByte = data[pos];
489
487
  const majorType = initialByte >> 5;
490
488
  const additionalInfo = initialByte & 31;
491
- offset += 1;
489
+ pos += 1;
492
490
  let argument;
493
491
  if (additionalInfo < 24) {
494
492
  argument = additionalInfo;
495
493
  } else if (additionalInfo === 24) {
496
- argument = data[offset];
497
- offset += 1;
494
+ argument = data[pos];
495
+ pos += 1;
498
496
  } else if (additionalInfo === 25) {
499
- argument = data[offset] << 8 | data[offset + 1];
500
- offset += 2;
497
+ argument = data[pos] << 8 | data[pos + 1];
498
+ pos += 2;
501
499
  } else if (additionalInfo === 26) {
502
- argument = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
500
+ argument = data[pos] << 24 | data[pos + 1] << 16 | data[pos + 2] << 8 | data[pos + 3];
503
501
  argument = argument >>> 0;
504
- offset += 4;
502
+ pos += 4;
505
503
  } else {
506
504
  throw new PasskeyError(
507
- `CBOR decode error: unsupported additional info ${additionalInfo} at byte ${offset - 1}. This CBOR decoder only supports definite-length encodings.`
505
+ `CBOR decode error: unsupported additional info ${additionalInfo} at byte ${pos - 1}. This CBOR decoder only supports definite-length encodings.`
508
506
  );
509
507
  }
510
508
  switch (majorType) {
511
509
  // Major type 0: Unsigned integer
512
510
  case 0:
513
- return { value: argument, offset };
511
+ return { value: argument, offset: pos };
514
512
  // Major type 1: Negative integer (-1 - argument)
515
513
  case 1:
516
- return { value: -1 - argument, offset };
514
+ return { value: -1 - argument, offset: pos };
517
515
  // Major type 2: Byte string
518
516
  case 2: {
519
- const bytes = data.slice(offset, offset + argument);
520
- return { value: bytes, offset: offset + argument };
517
+ const bytes = data.slice(pos, pos + argument);
518
+ return { value: bytes, offset: pos + argument };
521
519
  }
522
520
  // Major type 3: Text string (UTF-8)
523
521
  case 3: {
524
- const textBytes = data.slice(offset, offset + argument);
522
+ const textBytes = data.slice(pos, pos + argument);
525
523
  const text = new TextDecoder().decode(textBytes);
526
- return { value: text, offset: offset + argument };
524
+ return { value: text, offset: pos + argument };
527
525
  }
528
526
  // Major type 4: Array
529
527
  case 4: {
530
528
  const arr = [];
531
- let currentOffset = offset;
529
+ let currentOffset = pos;
532
530
  for (let i = 0; i < argument; i++) {
533
531
  const item = decodeCbor(data, currentOffset);
534
532
  arr.push(item.value);
@@ -539,7 +537,7 @@ function decodeCbor(data, offset) {
539
537
  // Major type 5: Map
540
538
  case 5: {
541
539
  const map = /* @__PURE__ */ new Map();
542
- let currentOffset = offset;
540
+ let currentOffset = pos;
543
541
  for (let i = 0; i < argument; i++) {
544
542
  const keyResult = decodeCbor(data, currentOffset);
545
543
  const valResult = decodeCbor(data, keyResult.offset);
@@ -550,7 +548,7 @@ function decodeCbor(data, offset) {
550
548
  }
551
549
  default:
552
550
  throw new PasskeyError(
553
- `CBOR decode error: unsupported major type ${majorType} at byte ${offset - 1}. This CBOR decoder only supports types 0-5 (integers, byte/text strings, arrays, maps).`
551
+ `CBOR decode error: unsupported major type ${majorType} at byte ${pos - 1}. This CBOR decoder only supports types 0-5 (integers, byte/text strings, arrays, maps).`
554
552
  );
555
553
  }
556
554
  }
@@ -694,14 +692,11 @@ var OperationEncryptor = class {
694
692
  if (cause instanceof OperationEncryptionError) {
695
693
  throw cause;
696
694
  }
697
- throw new OperationEncryptionError(
698
- `Failed to encrypt operation ${fieldName} field.`,
699
- {
700
- operationId,
701
- fieldName,
702
- cause: cause instanceof Error ? cause.message : String(cause)
703
- }
704
- );
695
+ throw new OperationEncryptionError(`Failed to encrypt operation ${fieldName} field.`, {
696
+ operationId,
697
+ fieldName,
698
+ cause: cause instanceof Error ? cause.message : String(cause)
699
+ });
705
700
  }
706
701
  }
707
702
  async decryptField(field, operationId, fieldName) {
@@ -725,10 +720,10 @@ var OperationEncryptor = class {
725
720
  const json = new TextDecoder().decode(plaintextBytes);
726
721
  const parsed = JSON.parse(json);
727
722
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
728
- throw new OperationEncryptionError(
729
- `Decrypted ${fieldName} is not a valid record object.`,
730
- { operationId, fieldName }
731
- );
723
+ throw new OperationEncryptionError(`Decrypted ${fieldName} is not a valid record object.`, {
724
+ operationId,
725
+ fieldName
726
+ });
732
727
  }
733
728
  return parsed;
734
729
  } catch (cause) {
@@ -753,7 +748,7 @@ function isEncryptedEnvelope(field) {
753
748
  if (field === null || typeof field !== "object") {
754
749
  return false;
755
750
  }
756
- return field[ENCRYPTED_MARKER] === true && typeof field["ciphertext"] === "string" && typeof field["iv"] === "string" && field["algorithm"] === "AES-256-GCM";
751
+ return field[ENCRYPTED_MARKER] === true && typeof field.ciphertext === "string" && typeof field.iv === "string" && field.algorithm === "AES-256-GCM";
757
752
  }
758
753
 
759
754
  export {
@@ -783,4 +778,4 @@ export {
783
778
  OperationEncryptor,
784
779
  isEncryptedField
785
780
  };
786
- //# sourceMappingURL=chunk-FSU4SK32.js.map
781
+ //# sourceMappingURL=chunk-IO2MCCG2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/device/device-identity.ts","../src/encryption/database-encryption.ts","../src/passkey/passkey-client.ts","../src/encryption/operation-encryptor.ts"],"sourcesContent":["import { KoraError } from '@korajs/core'\n\n// --- Auth-specific errors ---\n\n/**\n * Thrown when the Web Crypto API is not available in the current environment.\n * This can happen in older Node.js versions or SSR environments without crypto support.\n */\nexport class CryptoUnavailableError extends KoraError {\n\tconstructor() {\n\t\tsuper(\n\t\t\t'Web Crypto API (crypto.subtle) is not available in this environment. ' +\n\t\t\t\t'Device identity requires crypto.subtle, which is available in modern browsers and Node.js 20+. ' +\n\t\t\t\t'If running in SSR, ensure your runtime provides the Web Crypto API.',\n\t\t\t'CRYPTO_UNAVAILABLE',\n\t\t)\n\t\tthis.name = 'CryptoUnavailableError'\n\t}\n}\n\n/**\n * Thrown when a device identity operation fails (key generation, signing, verification).\n */\nexport class DeviceIdentityError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'DEVICE_IDENTITY_ERROR', context)\n\t\tthis.name = 'DeviceIdentityError'\n\t}\n}\n\n// --- Encoding helpers ---\n\n/**\n * Encodes an ArrayBuffer as a base64url string (no padding).\n *\n * @param buffer - The binary data to encode\n * @returns A base64url-encoded string without padding characters\n */\nexport function toBase64Url(buffer: ArrayBuffer): string {\n\tconst bytes = new Uint8Array(buffer)\n\tlet binary = ''\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\tbinary += String.fromCharCode(bytes[i] as number)\n\t}\n\t// Standard base64, then convert to base64url (no padding)\n\treturn btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\n/**\n * Decodes a base64url string (no padding) into a Uint8Array.\n *\n * @param str - A base64url-encoded string (with or without padding)\n * @returns The decoded binary data as a Uint8Array\n */\nexport function fromBase64Url(str: string): Uint8Array {\n\t// Convert base64url back to standard base64\n\tlet base64 = str.replace(/-/g, '+').replace(/_/g, '/')\n\t// Add padding if necessary\n\tconst paddingNeeded = (4 - (base64.length % 4)) % 4\n\tbase64 += '='.repeat(paddingNeeded)\n\n\tconst binary = atob(base64)\n\tconst bytes = new Uint8Array(binary.length)\n\tfor (let i = 0; i < binary.length; i++) {\n\t\tbytes[i] = binary.charCodeAt(i)\n\t}\n\treturn bytes\n}\n\n// --- Internal helpers ---\n\n/**\n * Asserts that `crypto.subtle` is available, throwing a clear error if not.\n */\nfunction assertCryptoAvailable(): void {\n\tif (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.subtle === 'undefined') {\n\t\tthrow new CryptoUnavailableError()\n\t}\n}\n\n/** ECDSA algorithm parameters used throughout the module. */\nconst ECDSA_ALGORITHM: EcKeyGenParams = {\n\tname: 'ECDSA',\n\tnamedCurve: 'P-256',\n}\n\n/** Signing algorithm parameters: ECDSA with SHA-256. */\nconst ECDSA_SIGN_ALGORITHM: EcdsaParams = {\n\tname: 'ECDSA',\n\thash: { name: 'SHA-256' },\n}\n\n// --- Public API ---\n\n/**\n * Generates an ECDSA P-256 key pair for device identity.\n *\n * The private key is marked as non-extractable, ensuring it cannot be\n * exported from the browser's crypto subsystem. This provides\n * proof-of-possession: only code running on this device can sign with the key.\n *\n * @returns A CryptoKeyPair containing the public and private ECDSA P-256 keys\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {DeviceIdentityError} If key generation fails\n *\n * @example\n * ```typescript\n * const keyPair = await generateDeviceKeyPair()\n * // keyPair.publicKey can be exported; keyPair.privateKey stays on device\n * ```\n */\nexport async function generateDeviceKeyPair(): Promise<CryptoKeyPair> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst keyPair = await globalThis.crypto.subtle.generateKey(\n\t\t\tECDSA_ALGORITHM,\n\t\t\t// extractable: false makes the private key non-extractable.\n\t\t\t// The public key is always extractable regardless of this flag.\n\t\t\tfalse,\n\t\t\t['sign', 'verify'],\n\t\t)\n\t\treturn keyPair\n\t} catch (cause) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'Failed to generate ECDSA P-256 device key pair. ' +\n\t\t\t\t'Ensure the runtime supports the ECDSA algorithm with the P-256 curve.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n\n/**\n * Exports the public key from a key pair as a JSON Web Key (JWK).\n *\n * The JWK can be safely transmitted to a server or other devices to identify\n * this device. It contains only the public component of the key pair.\n *\n * @param keyPair - The CryptoKeyPair whose public key should be exported\n * @returns The public key in JWK format\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {DeviceIdentityError} If the export operation fails\n *\n * @example\n * ```typescript\n * const keyPair = await generateDeviceKeyPair()\n * const jwk = await exportPublicKeyJwk(keyPair)\n * // jwk contains { kty: 'EC', crv: 'P-256', x: '...', y: '...' }\n * ```\n */\nexport async function exportPublicKeyJwk(keyPair: CryptoKeyPair): Promise<JsonWebKey> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst jwk = await globalThis.crypto.subtle.exportKey('jwk', keyPair.publicKey)\n\t\treturn jwk\n\t} catch (cause) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'Failed to export public key as JWK. ' +\n\t\t\t\t'The key pair may be invalid or the public key may not support JWK export.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n\n/**\n * Signs a challenge string with the device's private key.\n *\n * Used for proof-of-possession during authentication: the server sends a\n * random challenge, and the device proves it holds the private key by signing it.\n *\n * @param privateKey - The device's private CryptoKey (ECDSA P-256)\n * @param challenge - The challenge string to sign (typically a random nonce from the server)\n * @returns A base64url-encoded ECDSA signature (no padding)\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {DeviceIdentityError} If the signing operation fails\n *\n * @example\n * ```typescript\n * const keyPair = await generateDeviceKeyPair()\n * const signature = await signChallenge(keyPair.privateKey, 'server-nonce-abc123')\n * // signature is a base64url string like 'MEUCIQDx...'\n * ```\n */\nexport async function signChallenge(privateKey: CryptoKey, challenge: string): Promise<string> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst encoded = new TextEncoder().encode(challenge)\n\t\tconst signatureBuffer = await globalThis.crypto.subtle.sign(\n\t\t\tECDSA_SIGN_ALGORITHM,\n\t\t\tprivateKey,\n\t\t\tencoded,\n\t\t)\n\t\treturn toBase64Url(signatureBuffer)\n\t} catch (cause) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'Failed to sign challenge. ' +\n\t\t\t\t'Ensure the key is a valid ECDSA P-256 private key with \"sign\" usage.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n\n/**\n * Verifies a challenge signature against a public key.\n *\n * Used server-side (or on any verifying party) to confirm that a device\n * holds the private key corresponding to the given public key.\n *\n * @param publicKeyJwk - The device's public key in JWK format\n * @param challenge - The original challenge string that was signed\n * @param signature - The base64url-encoded signature to verify\n * @returns `true` if the signature is valid, `false` otherwise\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {DeviceIdentityError} If the verification operation fails due to an invalid key or format\n *\n * @example\n * ```typescript\n * const isValid = await verifyChallenge(publicKeyJwk, 'server-nonce-abc123', signature)\n * if (isValid) {\n * // Device proved possession of the private key\n * }\n * ```\n */\nexport async function verifyChallenge(\n\tpublicKeyJwk: JsonWebKey,\n\tchallenge: string,\n\tsignature: string,\n): Promise<boolean> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst publicKey = await globalThis.crypto.subtle.importKey(\n\t\t\t'jwk',\n\t\t\tpublicKeyJwk,\n\t\t\tECDSA_ALGORITHM,\n\t\t\ttrue,\n\t\t\t['verify'],\n\t\t)\n\n\t\tconst encoded = new TextEncoder().encode(challenge)\n\t\tconst signatureBytes = fromBase64Url(signature)\n\n\t\tconst isValid = await globalThis.crypto.subtle.verify(\n\t\t\tECDSA_SIGN_ALGORITHM,\n\t\t\tpublicKey,\n\t\t\tsignatureBytes as unknown as ArrayBuffer,\n\t\t\tencoded,\n\t\t)\n\t\treturn isValid\n\t} catch (cause) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'Failed to verify challenge signature. ' +\n\t\t\t\t'The public key JWK or signature format may be invalid.',\n\t\t\t{\n\t\t\t\tcause: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\tpublicKeyKty: publicKeyJwk.kty,\n\t\t\t\tpublicKeyCrv: publicKeyJwk.crv,\n\t\t\t},\n\t\t)\n\t}\n}\n\n/**\n * Computes a SHA-256 thumbprint of a JWK public key.\n *\n * The thumbprint is computed per RFC 7638: the JWK members required for the key\n * type are serialized in lexicographic order, then hashed with SHA-256. For EC keys\n * (kty: \"EC\"), the required members are `crv`, `kty`, `x`, and `y`.\n *\n * This thumbprint serves as a compact, stable identifier for the device's public key\n * (used as the `dpk` claim in device credentials).\n *\n * @param publicKeyJwk - The public key in JWK format (must be an EC P-256 key)\n * @returns A base64url-encoded SHA-256 thumbprint (no padding)\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {DeviceIdentityError} If the thumbprint computation fails or the JWK is missing required fields\n *\n * @example\n * ```typescript\n * const keyPair = await generateDeviceKeyPair()\n * const jwk = await exportPublicKeyJwk(keyPair)\n * const thumbprint = await computePublicKeyThumbprint(jwk)\n * // thumbprint is a base64url string, e.g., 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs'\n * ```\n */\nexport async function computePublicKeyThumbprint(publicKeyJwk: JsonWebKey): Promise<string> {\n\tassertCryptoAvailable()\n\n\t// RFC 7638 requires specific members in lexicographic order for each key type.\n\t// For EC (kty: \"EC\"), the required members are: crv, kty, x, y.\n\tif (publicKeyJwk.kty !== 'EC') {\n\t\tthrow new DeviceIdentityError(\n\t\t\t`Expected JWK key type \"EC\" but received \"${publicKeyJwk.kty ?? 'undefined'}\". Only ECDSA public keys are supported for device identity.`,\n\t\t\t{ kty: publicKeyJwk.kty },\n\t\t)\n\t}\n\n\tif (!publicKeyJwk.crv || !publicKeyJwk.x || !publicKeyJwk.y) {\n\t\tthrow new DeviceIdentityError(\n\t\t\t'JWK is missing required EC fields. ' +\n\t\t\t\t'An EC public key JWK must include \"crv\", \"x\", and \"y\" members.',\n\t\t\t{\n\t\t\t\thasCrv: Boolean(publicKeyJwk.crv),\n\t\t\t\thasX: Boolean(publicKeyJwk.x),\n\t\t\t\thasY: Boolean(publicKeyJwk.y),\n\t\t\t},\n\t\t)\n\t}\n\n\t// Build the canonical JSON with only the required members in lexicographic order.\n\t// Per RFC 7638, no whitespace, keys in sorted order.\n\tconst canonicalJson = JSON.stringify({\n\t\tcrv: publicKeyJwk.crv,\n\t\tkty: publicKeyJwk.kty,\n\t\tx: publicKeyJwk.x,\n\t\ty: publicKeyJwk.y,\n\t})\n\n\ttry {\n\t\tconst encoded = new TextEncoder().encode(canonicalJson)\n\t\tconst hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', encoded)\n\t\treturn toBase64Url(hashBuffer)\n\t} catch (cause) {\n\t\tthrow new DeviceIdentityError('Failed to compute SHA-256 thumbprint of the public key JWK.', {\n\t\t\tcause: cause instanceof Error ? cause.message : String(cause),\n\t\t})\n\t}\n}\n","import { KoraError } from '@korajs/core'\n\n// --- Encryption-specific errors ---\n\n/**\n * Thrown when an encryption or decryption operation fails.\n * Includes context about what went wrong to aid debugging.\n */\nexport class EncryptionError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'ENCRYPTION_ERROR', context)\n\t\tthis.name = 'EncryptionError'\n\t}\n}\n\n/**\n * Thrown when the Web Crypto API is not available in the current environment.\n * The encryption module requires `crypto.subtle` for AES-256-GCM operations.\n */\nexport class CryptoUnavailableError extends KoraError {\n\tconstructor() {\n\t\tsuper(\n\t\t\t'Web Crypto API (crypto.subtle) is not available in this environment. ' +\n\t\t\t\t'Database encryption requires crypto.subtle, which is available in modern browsers and Node.js 20+. ' +\n\t\t\t\t'If running in SSR, ensure your runtime provides the Web Crypto API.',\n\t\t\t'CRYPTO_UNAVAILABLE',\n\t\t)\n\t\tthis.name = 'CryptoUnavailableError'\n\t}\n}\n\n// --- Internal helpers ---\n\n/** AES-GCM algorithm name, used throughout the module. */\nconst AES_GCM = 'AES-GCM' as const\n\n/** AES-256 key length in bits. */\nconst AES_KEY_LENGTH = 256\n\n/** GCM initialization vector length in bytes (96 bits / 12 bytes is the recommended size). */\nconst IV_LENGTH = 12\n\n/**\n * Asserts that `crypto.subtle` is available, throwing a clear error if not.\n */\nfunction assertCryptoAvailable(): void {\n\tif (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.subtle === 'undefined') {\n\t\tthrow new CryptoUnavailableError()\n\t}\n}\n\n// --- Public API ---\n\n/**\n * Generates a random 256-bit AES-GCM encryption key.\n *\n * The key is extractable so it can be exported for persistence (e.g., encrypted\n * with a passphrase-derived key and stored locally). Use {@link exportKey} to\n * get the raw bytes.\n *\n * @returns A CryptoKey for AES-256-GCM encryption and decryption\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {EncryptionError} If key generation fails\n *\n * @example\n * ```typescript\n * const key = await generateEncryptionKey()\n * // key can be used with encryptData() and decryptData()\n * ```\n */\nexport async function generateEncryptionKey(): Promise<CryptoKey> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst key = await globalThis.crypto.subtle.generateKey(\n\t\t\t{ name: AES_GCM, length: AES_KEY_LENGTH },\n\t\t\t// extractable: true so the key can be exported and persisted\n\t\t\ttrue,\n\t\t\t['encrypt', 'decrypt'],\n\t\t)\n\t\treturn key\n\t} catch (cause) {\n\t\tthrow new EncryptionError(\n\t\t\t'Failed to generate AES-256-GCM encryption key. ' +\n\t\t\t\t'Ensure the runtime supports the AES-GCM algorithm.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n\n/**\n * Encrypts data using AES-256-GCM with a randomly generated IV.\n *\n * Each call generates a fresh 12-byte IV, ensuring that encrypting the same\n * plaintext twice produces different ciphertext. The IV must be stored alongside\n * the ciphertext for decryption.\n *\n * AES-GCM provides both confidentiality and integrity: the ciphertext includes\n * an authentication tag that detects tampering.\n *\n * @param key - An AES-256-GCM CryptoKey (from {@link generateEncryptionKey} or {@link importKey})\n * @param plaintext - The data to encrypt\n * @returns An object containing the ciphertext and the IV used for encryption\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {EncryptionError} If encryption fails\n *\n * @example\n * ```typescript\n * const key = await generateEncryptionKey()\n * const data = new TextEncoder().encode('sensitive data')\n * const { ciphertext, iv } = await encryptData(key, data)\n * // Store ciphertext and iv together; both are needed for decryption\n * ```\n */\nexport async function encryptData(\n\tkey: CryptoKey,\n\tplaintext: Uint8Array,\n): Promise<{ ciphertext: Uint8Array; iv: Uint8Array }> {\n\tassertCryptoAvailable()\n\n\t// Generate a fresh random IV for each encryption operation.\n\t// AES-GCM with a 96-bit IV is the recommended configuration per NIST SP 800-38D.\n\tconst iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_LENGTH))\n\n\ttry {\n\t\tconst ciphertextBuffer = await globalThis.crypto.subtle.encrypt(\n\t\t\t{ name: AES_GCM, iv: iv as unknown as ArrayBuffer },\n\t\t\tkey,\n\t\t\tplaintext as unknown as ArrayBuffer,\n\t\t)\n\t\treturn {\n\t\t\tciphertext: new Uint8Array(ciphertextBuffer),\n\t\t\tiv,\n\t\t}\n\t} catch (cause) {\n\t\tthrow new EncryptionError(\n\t\t\t'Failed to encrypt data with AES-256-GCM. ' +\n\t\t\t\t'Ensure the key is a valid AES-GCM CryptoKey with \"encrypt\" usage.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n\n/**\n * Decrypts AES-256-GCM encrypted data.\n *\n * The IV must be the same one that was used during encryption. AES-GCM\n * authenticates the ciphertext, so any tampering will cause decryption to fail.\n *\n * @param key - The AES-256-GCM CryptoKey used for encryption\n * @param ciphertext - The encrypted data (from {@link encryptData})\n * @param iv - The initialization vector used during encryption (from {@link encryptData})\n * @returns The decrypted plaintext\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {EncryptionError} If decryption fails (wrong key, tampered ciphertext, or wrong IV)\n *\n * @example\n * ```typescript\n * const decrypted = await decryptData(key, ciphertext, iv)\n * const text = new TextDecoder().decode(decrypted)\n * ```\n */\nexport async function decryptData(\n\tkey: CryptoKey,\n\tciphertext: Uint8Array,\n\tiv: Uint8Array,\n): Promise<Uint8Array> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst plaintextBuffer = await globalThis.crypto.subtle.decrypt(\n\t\t\t{ name: AES_GCM, iv: iv as unknown as ArrayBuffer },\n\t\t\tkey,\n\t\t\tciphertext as unknown as ArrayBuffer,\n\t\t)\n\t\treturn new Uint8Array(plaintextBuffer)\n\t} catch (cause) {\n\t\tthrow new EncryptionError(\n\t\t\t'Failed to decrypt data with AES-256-GCM. ' +\n\t\t\t\t'This may indicate a wrong key, tampered ciphertext, or incorrect IV.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n\n/**\n * Exports an AES-256-GCM CryptoKey to its raw byte representation.\n *\n * The raw key is 32 bytes (256 bits). This is useful for persisting the key\n * (e.g., encrypting it with a passphrase-derived key before storing to disk).\n *\n * **Security warning:** Raw key bytes are sensitive material. Never log them,\n * store them in plaintext, or transmit them over the network without encryption.\n *\n * @param key - An extractable AES-256-GCM CryptoKey\n * @returns The raw key bytes (32 bytes for AES-256)\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {EncryptionError} If the key export fails (e.g., key is not extractable)\n *\n * @example\n * ```typescript\n * const key = await generateEncryptionKey()\n * const rawBytes = await exportKey(key)\n * // rawBytes.length === 32\n * ```\n */\nexport async function exportKey(key: CryptoKey): Promise<Uint8Array> {\n\tassertCryptoAvailable()\n\n\ttry {\n\t\tconst rawBuffer = await globalThis.crypto.subtle.exportKey('raw', key)\n\t\treturn new Uint8Array(rawBuffer)\n\t} catch (cause) {\n\t\tthrow new EncryptionError(\n\t\t\t'Failed to export AES-256-GCM key. ' +\n\t\t\t\t'The key may not be extractable. Only keys generated with extractable=true can be exported.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n\n/**\n * Imports raw key bytes into an AES-256-GCM CryptoKey.\n *\n * The input must be exactly 32 bytes (256 bits). The imported key is extractable\n * and supports both encrypt and decrypt operations.\n *\n * @param rawKey - Raw key bytes (must be exactly 32 bytes for AES-256)\n * @returns A CryptoKey for AES-256-GCM operations\n * @throws {CryptoUnavailableError} If `crypto.subtle` is not available\n * @throws {EncryptionError} If the raw key is invalid or import fails\n *\n * @example\n * ```typescript\n * const rawBytes = new Uint8Array(32) // previously exported key bytes\n * const key = await importKey(rawBytes)\n * // key can now be used with encryptData() and decryptData()\n * ```\n */\nexport async function importKey(rawKey: Uint8Array): Promise<CryptoKey> {\n\tassertCryptoAvailable()\n\n\tif (rawKey.length !== 32) {\n\t\tthrow new EncryptionError(\n\t\t\t`Invalid key length: expected 32 bytes (256 bits) for AES-256, but received ${rawKey.length} bytes.`,\n\t\t\t{ actualLength: rawKey.length, expectedLength: 32 },\n\t\t)\n\t}\n\n\ttry {\n\t\tconst key = await globalThis.crypto.subtle.importKey(\n\t\t\t'raw',\n\t\t\trawKey as unknown as ArrayBuffer,\n\t\t\t{ name: AES_GCM, length: AES_KEY_LENGTH },\n\t\t\ttrue,\n\t\t\t['encrypt', 'decrypt'],\n\t\t)\n\t\treturn key\n\t} catch (cause) {\n\t\tthrow new EncryptionError(\n\t\t\t'Failed to import raw key bytes as AES-256-GCM key. ' + 'Ensure the key material is valid.',\n\t\t\t{ cause: cause instanceof Error ? cause.message : String(cause) },\n\t\t)\n\t}\n}\n","import { KoraError } from '@korajs/core'\nimport { fromBase64Url, toBase64Url } from '../device/device-identity'\n\n// ============================================================================\n// Passkey-specific errors\n// ============================================================================\n\n/**\n * Thrown when a passkey operation fails (registration, authentication,\n * or browser API interaction).\n */\nexport class PasskeyError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'PASSKEY_ERROR', context)\n\t\tthis.name = 'PasskeyError'\n\t}\n}\n\n/**\n * Thrown when the browser does not support WebAuthn.\n * Passkey authentication requires a modern browser with the\n * Web Authentication API (navigator.credentials).\n */\nexport class PasskeyUnsupportedError extends KoraError {\n\tconstructor() {\n\t\tsuper(\n\t\t\t'WebAuthn is not supported in this browser. Passkey authentication requires a modern browser with WebAuthn support.',\n\t\t\t'PASSKEY_UNSUPPORTED',\n\t\t)\n\t\tthis.name = 'PasskeyUnsupportedError'\n\t}\n}\n\n// ============================================================================\n// Passkey registration response\n// ============================================================================\n\n/** Response from a passkey registration (credential creation). */\nexport interface PasskeyRegistrationResponse {\n\t/** Base64url-encoded credential ID */\n\tcredentialId: string\n\t/** Base64url-encoded public key (COSE format) */\n\tpublicKey: string\n\t/** Base64url-encoded clientDataJSON */\n\tclientDataJSON: string\n\t/** Base64url-encoded attestation object */\n\tattestationObject: string\n}\n\n// ============================================================================\n// Passkey authentication response\n// ============================================================================\n\n/** Response from a passkey authentication (assertion). */\nexport interface PasskeyAuthenticationResponse {\n\t/** Base64url-encoded credential ID */\n\tcredentialId: string\n\t/** Base64url-encoded authenticator data */\n\tauthenticatorData: string\n\t/** Base64url-encoded clientDataJSON */\n\tclientDataJSON: string\n\t/** Base64url-encoded ECDSA signature */\n\tsignature: string\n\t/** Base64url-encoded user handle (may be null for non-resident keys) */\n\tuserHandle: string | null\n}\n\n// ============================================================================\n// Feature detection\n// ============================================================================\n\n/**\n * Check if WebAuthn/passkeys are supported in the current environment.\n *\n * Returns true if the `navigator.credentials` API is available and supports\n * the `create` and `get` methods required for WebAuthn.\n *\n * @returns `true` if WebAuthn is available, `false` otherwise\n *\n * @example\n * ```typescript\n * if (isPasskeySupported()) {\n * // Show passkey login option\n * }\n * ```\n */\nexport function isPasskeySupported(): boolean {\n\treturn (\n\t\ttypeof globalThis.navigator !== 'undefined' &&\n\t\ttypeof globalThis.navigator.credentials !== 'undefined' &&\n\t\ttypeof globalThis.navigator.credentials.create === 'function' &&\n\t\ttypeof globalThis.navigator.credentials.get === 'function'\n\t)\n}\n\n/**\n * Check if a platform authenticator (biometric) is available.\n *\n * A platform authenticator is built into the device (Touch ID, Face ID,\n * Windows Hello). Returns false if only roaming authenticators (security\n * keys) are available, or if WebAuthn is not supported.\n *\n * @returns `true` if a platform authenticator is available\n *\n * @example\n * ```typescript\n * if (await isPlatformAuthenticatorAvailable()) {\n * // Show \"Sign in with Touch ID\" button\n * }\n * ```\n */\nexport async function isPlatformAuthenticatorAvailable(): Promise<boolean> {\n\tif (!isPasskeySupported()) {\n\t\treturn false\n\t}\n\n\t// PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable is\n\t// the standard way to check for biometric/platform authenticator support.\n\tif (\n\t\ttypeof PublicKeyCredential !== 'undefined' &&\n\t\ttypeof PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function'\n\t) {\n\t\ttry {\n\t\t\treturn await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ============================================================================\n// Registration (credential creation)\n// ============================================================================\n\n/**\n * Create a passkey credential (registration flow).\n *\n * Called during user registration. The server provides a challenge and user info,\n * the browser prompts for biometric verification, and this function returns the\n * credential data to send back to the server for verification.\n *\n * @param options - Registration options from the server\n * @param options.challenge - Base64url-encoded challenge from server\n * @param options.rpId - Relying party ID (your domain, e.g. \"example.com\")\n * @param options.rpName - Relying party display name (e.g. \"My App\")\n * @param options.userId - User ID as base64url-encoded opaque bytes\n * @param options.userName - User's email or username for display\n * @param options.userDisplayName - Human-readable display name\n * @param options.excludeCredentialIds - Credential IDs to exclude (prevents re-registration)\n * @param options.authenticatorSelection - Authenticator selection criteria\n * @returns The credential response to send to the server for verification\n * @throws {PasskeyUnsupportedError} If WebAuthn is not available\n * @throws {PasskeyError} If credential creation fails or is cancelled by the user\n *\n * @example\n * ```typescript\n * const credential = await createPasskeyCredential({\n * challenge: serverOptions.challenge,\n * rpId: 'example.com',\n * rpName: 'My App',\n * userId: serverOptions.userId,\n * userName: 'alice@example.com',\n * userDisplayName: 'Alice',\n * })\n * // Send `credential` to server for verification\n * ```\n */\nexport async function createPasskeyCredential(options: {\n\tchallenge: string\n\trpId: string\n\trpName: string\n\tuserId: string\n\tuserName: string\n\tuserDisplayName: string\n\texcludeCredentialIds?: string[]\n\tauthenticatorSelection?: {\n\t\tauthenticatorAttachment?: 'platform' | 'cross-platform'\n\t\tresidentKey?: 'required' | 'preferred' | 'discouraged'\n\t\tuserVerification?: 'required' | 'preferred' | 'discouraged'\n\t}\n}): Promise<PasskeyRegistrationResponse> {\n\tif (!isPasskeySupported()) {\n\t\tthrow new PasskeyUnsupportedError()\n\t}\n\n\t// Build the excludeCredentials list from base64url credential IDs\n\tconst excludeCredentials: PublicKeyCredentialDescriptor[] = (\n\t\toptions.excludeCredentialIds ?? []\n\t).map((id) => ({\n\t\ttype: 'public-key' as const,\n\t\tid: fromBase64Url(id).buffer as unknown as ArrayBuffer,\n\t}))\n\n\t// Build the authenticator selection criteria with sensible defaults\n\tconst authenticatorSelection: AuthenticatorSelectionCriteria = {\n\t\tauthenticatorAttachment: options.authenticatorSelection?.authenticatorAttachment ?? 'platform',\n\t\tresidentKey: options.authenticatorSelection?.residentKey ?? 'preferred',\n\t\tuserVerification: options.authenticatorSelection?.userVerification ?? 'required',\n\t}\n\n\t// If residentKey is 'required', requireResidentKey must also be true\n\t// for backwards compatibility with older browsers.\n\tif (authenticatorSelection.residentKey === 'required') {\n\t\tauthenticatorSelection.requireResidentKey = true\n\t}\n\n\tconst publicKeyOptions: PublicKeyCredentialCreationOptions = {\n\t\tchallenge: fromBase64Url(options.challenge).buffer as unknown as ArrayBuffer,\n\t\trp: {\n\t\t\tid: options.rpId,\n\t\t\tname: options.rpName,\n\t\t},\n\t\tuser: {\n\t\t\tid: fromBase64Url(options.userId).buffer as unknown as ArrayBuffer,\n\t\t\tname: options.userName,\n\t\t\tdisplayName: options.userDisplayName,\n\t\t},\n\t\tpubKeyCredParams: [\n\t\t\t// ES256 (ECDSA w/ SHA-256) — the most widely supported algorithm\n\t\t\t{ type: 'public-key', alg: -7 },\n\t\t\t// RS256 (RSASSA-PKCS1-v1_5 w/ SHA-256) — fallback for older authenticators\n\t\t\t{ type: 'public-key', alg: -257 },\n\t\t],\n\t\texcludeCredentials,\n\t\tauthenticatorSelection,\n\t\ttimeout: 60000,\n\t\tattestation: 'none',\n\t}\n\n\tlet credential: PublicKeyCredential\n\ttry {\n\t\tconst result = await navigator.credentials.create({\n\t\t\tpublicKey: publicKeyOptions,\n\t\t})\n\n\t\tif (result === null) {\n\t\t\tthrow new PasskeyError(\n\t\t\t\t'Credential creation returned null. The user may have cancelled the operation.',\n\t\t\t\t{ rpId: options.rpId },\n\t\t\t)\n\t\t}\n\n\t\tcredential = result as PublicKeyCredential\n\t} catch (error) {\n\t\tif (error instanceof PasskeyError) {\n\t\t\tthrow error\n\t\t}\n\n\t\t// Map common WebAuthn DOMException names to user-friendly messages\n\t\tconst domError = error as DOMException\n\t\tif (domError.name === 'NotAllowedError') {\n\t\t\tthrow new PasskeyError(\n\t\t\t\t'Passkey creation was cancelled or not allowed. The user may have dismissed the prompt or the operation timed out.',\n\t\t\t\t{ rpId: options.rpId, errorName: domError.name },\n\t\t\t)\n\t\t}\n\t\tif (domError.name === 'InvalidStateError') {\n\t\t\tthrow new PasskeyError(\n\t\t\t\t'A passkey already exists for this user on this authenticator. Use the existing passkey to sign in.',\n\t\t\t\t{ rpId: options.rpId, errorName: domError.name },\n\t\t\t)\n\t\t}\n\n\t\tthrow new PasskeyError(\n\t\t\t`Passkey creation failed: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t{\n\t\t\t\trpId: options.rpId,\n\t\t\t\terrorName: error instanceof Error ? error.name : undefined,\n\t\t\t},\n\t\t)\n\t}\n\n\tconst response = credential.response as AuthenticatorAttestationResponse\n\n\t// Extract the public key from the attestation response.\n\t// getPublicKey() returns the SubjectPublicKeyInfo (SPKI) encoded public key.\n\t// We also need the raw COSE public key from the attestation for server-side storage.\n\t// The attestation object contains the credential public key in COSE format.\n\tconst attestationObject = new Uint8Array(response.attestationObject)\n\tconst clientDataJSON = new Uint8Array(response.clientDataJSON)\n\n\t// Extract the COSE public key from the authenticator data within the attestation object.\n\t// The attestation object is CBOR-encoded and contains authData which includes the\n\t// credential public key in COSE_Key format.\n\tconst publicKeyBytes = extractPublicKeyFromAttestationObject(attestationObject)\n\n\treturn {\n\t\tcredentialId: toBase64Url(credential.rawId),\n\t\tpublicKey: toBase64Url(publicKeyBytes.buffer as unknown as ArrayBuffer),\n\t\tclientDataJSON: toBase64Url(clientDataJSON.buffer as unknown as ArrayBuffer),\n\t\tattestationObject: toBase64Url(attestationObject.buffer as unknown as ArrayBuffer),\n\t}\n}\n\n// ============================================================================\n// Authentication (assertion)\n// ============================================================================\n\n/**\n * Authenticate with a passkey (assertion flow).\n *\n * Called during login. The server provides a challenge, the browser prompts\n * for biometric verification, and this function returns the signed assertion\n * to send back to the server for verification.\n *\n * @param options - Authentication options from the server\n * @param options.challenge - Base64url-encoded challenge from server\n * @param options.rpId - Relying party ID (your domain)\n * @param options.allowCredentialIds - Limit authentication to specific credentials\n * @param options.userVerification - User verification requirement (default: 'preferred')\n * @param options.timeout - Timeout in milliseconds (default: 60000)\n * @returns The assertion response to send to the server for verification\n * @throws {PasskeyUnsupportedError} If WebAuthn is not available\n * @throws {PasskeyError} If authentication fails or is cancelled by the user\n *\n * @example\n * ```typescript\n * const assertion = await authenticateWithPasskey({\n * challenge: serverOptions.challenge,\n * rpId: 'example.com',\n * })\n * // Send `assertion` to server for verification\n * ```\n */\nexport async function authenticateWithPasskey(options: {\n\tchallenge: string\n\trpId: string\n\tallowCredentialIds?: string[]\n\tuserVerification?: 'required' | 'preferred' | 'discouraged'\n\ttimeout?: number\n}): Promise<PasskeyAuthenticationResponse> {\n\tif (!isPasskeySupported()) {\n\t\tthrow new PasskeyUnsupportedError()\n\t}\n\n\t// Build allowCredentials list from base64url credential IDs\n\tconst allowCredentials: PublicKeyCredentialDescriptor[] | undefined =\n\t\toptions.allowCredentialIds?.map((id) => ({\n\t\t\ttype: 'public-key' as const,\n\t\t\tid: fromBase64Url(id).buffer as unknown as ArrayBuffer,\n\t\t}))\n\n\tconst publicKeyOptions: PublicKeyCredentialRequestOptions = {\n\t\tchallenge: fromBase64Url(options.challenge).buffer as unknown as ArrayBuffer,\n\t\trpId: options.rpId,\n\t\tallowCredentials,\n\t\tuserVerification: options.userVerification ?? 'preferred',\n\t\ttimeout: options.timeout ?? 60000,\n\t}\n\n\tlet credential: PublicKeyCredential\n\ttry {\n\t\tconst result = await navigator.credentials.get({\n\t\t\tpublicKey: publicKeyOptions,\n\t\t})\n\n\t\tif (result === null) {\n\t\t\tthrow new PasskeyError(\n\t\t\t\t'Authentication returned null. The user may have cancelled the operation.',\n\t\t\t\t{ rpId: options.rpId },\n\t\t\t)\n\t\t}\n\n\t\tcredential = result as PublicKeyCredential\n\t} catch (error) {\n\t\tif (error instanceof PasskeyError) {\n\t\t\tthrow error\n\t\t}\n\n\t\tconst domError = error as DOMException\n\t\tif (domError.name === 'NotAllowedError') {\n\t\t\tthrow new PasskeyError(\n\t\t\t\t'Passkey authentication was cancelled or not allowed. The user may have dismissed the prompt or the operation timed out.',\n\t\t\t\t{ rpId: options.rpId, errorName: domError.name },\n\t\t\t)\n\t\t}\n\n\t\tthrow new PasskeyError(\n\t\t\t`Passkey authentication failed: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\t{\n\t\t\t\trpId: options.rpId,\n\t\t\t\terrorName: error instanceof Error ? error.name : undefined,\n\t\t\t},\n\t\t)\n\t}\n\n\tconst response = credential.response as AuthenticatorAssertionResponse\n\n\tconst authenticatorData = new Uint8Array(response.authenticatorData)\n\tconst clientDataJSON = new Uint8Array(response.clientDataJSON)\n\tconst signature = new Uint8Array(response.signature)\n\tconst userHandle =\n\t\tresponse.userHandle !== null && response.userHandle.byteLength > 0\n\t\t\t? toBase64Url(response.userHandle)\n\t\t\t: null\n\n\treturn {\n\t\tcredentialId: toBase64Url(credential.rawId),\n\t\tauthenticatorData: toBase64Url(authenticatorData.buffer as unknown as ArrayBuffer),\n\t\tclientDataJSON: toBase64Url(clientDataJSON.buffer as unknown as ArrayBuffer),\n\t\tsignature: toBase64Url(signature.buffer as unknown as ArrayBuffer),\n\t\tuserHandle,\n\t}\n}\n\n// ============================================================================\n// Internal: Extract public key from attestation object\n// ============================================================================\n\n/**\n * Extracts the COSE-encoded public key from a CBOR-encoded attestation object.\n *\n * The attestation object structure (CBOR map):\n * - \"fmt\": attestation format (e.g. \"none\")\n * - \"attStmt\": attestation statement (empty map for \"none\")\n * - \"authData\": authenticator data (byte string)\n *\n * The authenticator data structure:\n * - rpIdHash (32 bytes)\n * - flags (1 byte)\n * - signCount (4 bytes, big-endian)\n * - [if flags.AT set] attestedCredentialData:\n * - aaguid (16 bytes)\n * - credentialIdLength (2 bytes, big-endian)\n * - credentialId (credentialIdLength bytes)\n * - credentialPublicKey (CBOR-encoded COSE_Key, remaining bytes)\n */\nfunction extractPublicKeyFromAttestationObject(attestationObject: Uint8Array): Uint8Array {\n\t// Decode the top-level CBOR map to get authData\n\tconst decoded = decodeCbor(attestationObject, 0)\n\tconst topMap = decoded.value as Map<string, unknown>\n\tconst authData = topMap.get('authData')\n\n\tif (!(authData instanceof Uint8Array)) {\n\t\tthrow new PasskeyError('Invalid attestation object: authData is missing or not a byte string.')\n\t}\n\n\t// Parse authenticator data to find the credential public key\n\tlet offset = 0\n\n\t// rpIdHash: 32 bytes\n\toffset += 32\n\n\t// flags: 1 byte\n\tconst flags = authData[offset] as number\n\toffset += 1\n\n\t// signCount: 4 bytes\n\toffset += 4\n\n\t// Check if attestedCredentialData is present (bit 6 of flags)\n\tconst hasAttestedCredentialData = (flags & 0x40) !== 0\n\tif (!hasAttestedCredentialData) {\n\t\tthrow new PasskeyError(\n\t\t\t'Attestation object does not contain attested credential data. ' +\n\t\t\t\t'The authenticator did not include a public key.',\n\t\t)\n\t}\n\n\t// aaguid: 16 bytes\n\toffset += 16\n\n\t// credentialIdLength: 2 bytes, big-endian\n\tconst credentialIdLength = ((authData[offset] as number) << 8) | (authData[offset + 1] as number)\n\toffset += 2\n\n\t// credentialId: credentialIdLength bytes\n\toffset += credentialIdLength\n\n\t// The remaining bytes in authData from this offset are the CBOR-encoded\n\t// COSE public key. We need to extract exactly those bytes.\n\t// To find the exact length, we decode the CBOR value and use the consumed byte count.\n\tconst coseKeyResult = decodeCbor(authData, offset)\n\tconst coseKeyLength = coseKeyResult.offset - offset\n\n\treturn authData.slice(offset, offset + coseKeyLength)\n}\n\n// ============================================================================\n// Minimal CBOR decoder\n// ============================================================================\n\n/**\n * Minimal CBOR decoder supporting only the types needed for WebAuthn:\n * - Major type 0: Unsigned integer\n * - Major type 1: Negative integer\n * - Major type 2: Byte string\n * - Major type 3: Text string\n * - Major type 4: Array\n * - Major type 5: Map\n *\n * This decoder handles the subset of CBOR used in WebAuthn attestation\n * objects and COSE keys. It does not support tags, floats, or indefinite-length\n * encodings, which are not used in the WebAuthn specification.\n */\n\ninterface CborDecodeResult {\n\tvalue: unknown\n\t/** Byte offset after the decoded value (used for sequential decoding) */\n\toffset: number\n}\n\nfunction decodeCbor(data: Uint8Array, offset: number): CborDecodeResult {\n\tlet pos = offset\n\n\tif (pos >= data.length) {\n\t\tthrow new PasskeyError('CBOR decode error: unexpected end of data.')\n\t}\n\n\tconst initialByte = data[pos] as number\n\tconst majorType = initialByte >> 5\n\tconst additionalInfo = initialByte & 0x1f\n\tpos += 1\n\n\t// Decode the argument (length or value) based on additionalInfo\n\tlet argument: number\n\tif (additionalInfo < 24) {\n\t\targument = additionalInfo\n\t} else if (additionalInfo === 24) {\n\t\targument = data[pos] as number\n\t\tpos += 1\n\t} else if (additionalInfo === 25) {\n\t\targument = ((data[pos] as number) << 8) | (data[pos + 1] as number)\n\t\tpos += 2\n\t} else if (additionalInfo === 26) {\n\t\targument =\n\t\t\t((data[pos] as number) << 24) |\n\t\t\t((data[pos + 1] as number) << 16) |\n\t\t\t((data[pos + 2] as number) << 8) |\n\t\t\t(data[pos + 3] as number)\n\t\t// Handle unsigned 32-bit properly (bitwise ops produce signed 32-bit in JS)\n\t\targument = argument >>> 0\n\t\tpos += 4\n\t} else {\n\t\tthrow new PasskeyError(\n\t\t\t`CBOR decode error: unsupported additional info ${additionalInfo} at byte ${pos - 1}. This CBOR decoder only supports definite-length encodings.`,\n\t\t)\n\t}\n\n\tswitch (majorType) {\n\t\t// Major type 0: Unsigned integer\n\t\tcase 0:\n\t\t\treturn { value: argument, offset: pos }\n\n\t\t// Major type 1: Negative integer (-1 - argument)\n\t\tcase 1:\n\t\t\treturn { value: -1 - argument, offset: pos }\n\n\t\t// Major type 2: Byte string\n\t\tcase 2: {\n\t\t\tconst bytes = data.slice(pos, pos + argument)\n\t\t\treturn { value: bytes, offset: pos + argument }\n\t\t}\n\n\t\t// Major type 3: Text string (UTF-8)\n\t\tcase 3: {\n\t\t\tconst textBytes = data.slice(pos, pos + argument)\n\t\t\tconst text = new TextDecoder().decode(textBytes)\n\t\t\treturn { value: text, offset: pos + argument }\n\t\t}\n\n\t\t// Major type 4: Array\n\t\tcase 4: {\n\t\t\tconst arr: unknown[] = []\n\t\t\tlet currentOffset = pos\n\t\t\tfor (let i = 0; i < argument; i++) {\n\t\t\t\tconst item = decodeCbor(data, currentOffset)\n\t\t\t\tarr.push(item.value)\n\t\t\t\tcurrentOffset = item.offset\n\t\t\t}\n\t\t\treturn { value: arr, offset: currentOffset }\n\t\t}\n\n\t\t// Major type 5: Map\n\t\tcase 5: {\n\t\t\tconst map = new Map<string | number, unknown>()\n\t\t\tlet currentOffset = pos\n\t\t\tfor (let i = 0; i < argument; i++) {\n\t\t\t\tconst keyResult = decodeCbor(data, currentOffset)\n\t\t\t\tconst valResult = decodeCbor(data, keyResult.offset)\n\t\t\t\tmap.set(keyResult.value as string | number, valResult.value)\n\t\t\t\tcurrentOffset = valResult.offset\n\t\t\t}\n\t\t\treturn { value: map, offset: currentOffset }\n\t\t}\n\n\t\tdefault:\n\t\t\tthrow new PasskeyError(\n\t\t\t\t`CBOR decode error: unsupported major type ${majorType} at byte ${pos - 1}. This CBOR decoder only supports types 0-5 (integers, byte/text strings, arrays, maps).`,\n\t\t\t)\n\t}\n}\n\n// Export the CBOR decoder for testing purposes (used by passkey-server too)\nexport { decodeCbor, type CborDecodeResult }\n","import { KoraError } from '@korajs/core'\nimport type { Operation } from '@korajs/core'\nimport { decryptData, encryptData } from './database-encryption'\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/** Marker field indicating that an operation's data has been encrypted. */\nconst ENCRYPTED_MARKER = '__kora_encrypted' as const\n\n/** Current encryption envelope version for forward compatibility. */\nconst ENCRYPTION_VERSION = 1\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * The envelope structure stored in an operation's `data` or `previousData`\n * field when encrypted. The original field contents are replaced with this\n * envelope, which the server relays opaquely.\n */\ninterface EncryptedFieldEnvelope {\n\t/** Marker flag — always true. Used to detect encrypted fields. */\n\t[ENCRYPTED_MARKER]: true\n\t/** Base64url-encoded AES-256-GCM ciphertext */\n\tciphertext: string\n\t/** Base64url-encoded 12-byte initialization vector */\n\tiv: string\n\t/** Algorithm identifier for forward compatibility */\n\talgorithm: 'AES-256-GCM'\n\t/** Envelope version for schema evolution */\n\tversion: number\n}\n\n/**\n * Configuration for the operation encryptor.\n */\nexport interface OperationEncryptorConfig {\n\t/**\n\t * The AES-256-GCM CryptoKey used to encrypt and decrypt operation data.\n\t *\n\t * This can be:\n\t * - A key derived from a user passphrase via `deriveEncryptionKey`\n\t * - A randomly generated key from `generateEncryptionKey`\n\t * - A key imported from raw bytes via `importKey`\n\t *\n\t * All devices that need to read each other's operations must share\n\t * the same encryption key (or keys, if key rotation is implemented).\n\t */\n\tkey: CryptoKey\n}\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Thrown when operation encryption or decryption fails.\n */\nexport class OperationEncryptionError extends KoraError {\n\tconstructor(message: string, context?: Record<string, unknown>) {\n\t\tsuper(message, 'OPERATION_ENCRYPTION_ERROR', context)\n\t\tthis.name = 'OperationEncryptionError'\n\t}\n}\n\n// ============================================================================\n// Base64url encoding helpers\n// ============================================================================\n\nfunction toBase64Url(bytes: Uint8Array): string {\n\tlet binary = ''\n\tfor (let i = 0; i < bytes.length; i++) {\n\t\tbinary += String.fromCharCode(bytes[i] as number)\n\t}\n\treturn btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction fromBase64Url(str: string): Uint8Array {\n\tlet base64 = str.replace(/-/g, '+').replace(/_/g, '/')\n\tconst paddingNeeded = (4 - (base64.length % 4)) % 4\n\tbase64 += '='.repeat(paddingNeeded)\n\n\tconst binary = atob(base64)\n\tconst bytes = new Uint8Array(binary.length)\n\tfor (let i = 0; i < binary.length; i++) {\n\t\tbytes[i] = binary.charCodeAt(i)\n\t}\n\treturn bytes\n}\n\n// ============================================================================\n// Implementation\n// ============================================================================\n\n/**\n * Encrypts and decrypts the `data` and `previousData` fields of Kora operations.\n *\n * This provides end-to-end encryption for sync: the server relays operations\n * without being able to read the user's data. Only metadata needed for sync\n * orchestration (id, nodeId, collection, timestamp, causalDeps, etc.) remains\n * in cleartext so the server can route, deduplicate, and order operations.\n *\n * **How it works:**\n * 1. `encryptOperation` replaces `data` and `previousData` with encrypted envelopes\n * containing base64url-encoded AES-256-GCM ciphertext\n * 2. The encrypted operation is sent through the normal sync pipeline\n * 3. The server stores and relays the operation without modification\n * 4. `decryptOperation` on receiving clients restores the original field values\n *\n * **What stays in cleartext (needed for sync):**\n * - `id` (content-addressed hash — used for deduplication)\n * - `nodeId`, `sequenceNumber` (version vectors)\n * - `timestamp` (causal ordering)\n * - `type`, `collection`, `recordId` (routing and storage)\n * - `causalDeps` (dependency tracking)\n * - `schemaVersion` (migration)\n *\n * **What gets encrypted (user data):**\n * - `data` (record field values for inserts/updates)\n * - `previousData` (previous field values for 3-way merge)\n *\n * @example\n * ```typescript\n * import { OperationEncryptor, generateEncryptionKey } from '@korajs/auth'\n *\n * const key = await generateEncryptionKey()\n * const encryptor = new OperationEncryptor({ key })\n *\n * // Before sending via sync\n * const encrypted = await encryptor.encryptOperation(operation)\n * syncEngine.send(encrypted)\n *\n * // After receiving from sync\n * const decrypted = await encryptor.decryptOperation(receivedOp)\n * store.apply(decrypted)\n * ```\n */\nexport class OperationEncryptor {\n\tprivate readonly key: CryptoKey\n\n\tconstructor(config: OperationEncryptorConfig) {\n\t\tthis.key = config.key\n\t}\n\n\t/**\n\t * Encrypt an operation's data fields.\n\t *\n\t * Returns a new Operation with `data` and `previousData` replaced by\n\t * encrypted envelopes. The original operation is not mutated.\n\t *\n\t * If `data` or `previousData` is null (e.g., delete operations),\n\t * that field remains null — there is nothing to encrypt.\n\t *\n\t * @param operation - The operation to encrypt\n\t * @returns A new operation with encrypted data fields\n\t * @throws {OperationEncryptionError} If encryption fails\n\t */\n\tasync encryptOperation(operation: Operation): Promise<Operation> {\n\t\tconst [encryptedData, encryptedPreviousData] = await Promise.all([\n\t\t\tthis.encryptField(operation.data, operation.id, 'data'),\n\t\t\tthis.encryptField(operation.previousData, operation.id, 'previousData'),\n\t\t])\n\n\t\treturn {\n\t\t\t...operation,\n\t\t\tdata: encryptedData,\n\t\t\tpreviousData: encryptedPreviousData,\n\t\t}\n\t}\n\n\t/**\n\t * Decrypt an operation's data fields.\n\t *\n\t * Returns a new Operation with the original `data` and `previousData`\n\t * restored from their encrypted envelopes. The original operation is\n\t * not mutated.\n\t *\n\t * If a field is null or not encrypted (no marker), it passes through unchanged.\n\t * This enables mixed plaintext/encrypted operations during migration.\n\t *\n\t * @param operation - The operation to decrypt\n\t * @returns A new operation with decrypted data fields\n\t * @throws {OperationEncryptionError} If decryption fails (wrong key, tampered data)\n\t */\n\tasync decryptOperation(operation: Operation): Promise<Operation> {\n\t\tconst [decryptedData, decryptedPreviousData] = await Promise.all([\n\t\t\tthis.decryptField(operation.data, operation.id, 'data'),\n\t\t\tthis.decryptField(operation.previousData, operation.id, 'previousData'),\n\t\t])\n\n\t\treturn {\n\t\t\t...operation,\n\t\t\tdata: decryptedData,\n\t\t\tpreviousData: decryptedPreviousData,\n\t\t}\n\t}\n\n\t/**\n\t * Check if an operation's data fields are encrypted.\n\t *\n\t * Returns true if either `data` or `previousData` contains an encrypted\n\t * envelope marker. Useful for determining whether decryption is needed\n\t * before applying an operation.\n\t *\n\t * @param operation - The operation to check\n\t * @returns true if any data field is encrypted\n\t */\n\tisEncrypted(operation: Operation): boolean {\n\t\treturn isEncryptedEnvelope(operation.data) || isEncryptedEnvelope(operation.previousData)\n\t}\n\n\t/**\n\t * Encrypt a batch of operations.\n\t *\n\t * Convenience method for encrypting multiple operations at once.\n\t * Operations are encrypted in parallel for performance.\n\t *\n\t * @param operations - The operations to encrypt\n\t * @returns New operations with encrypted data fields\n\t */\n\tasync encryptBatch(operations: Operation[]): Promise<Operation[]> {\n\t\treturn Promise.all(operations.map((op) => this.encryptOperation(op)))\n\t}\n\n\t/**\n\t * Decrypt a batch of operations.\n\t *\n\t * Convenience method for decrypting multiple operations at once.\n\t * Operations are decrypted in parallel for performance.\n\t *\n\t * @param operations - The operations to decrypt\n\t * @returns New operations with decrypted data fields\n\t */\n\tasync decryptBatch(operations: Operation[]): Promise<Operation[]> {\n\t\treturn Promise.all(operations.map((op) => this.decryptOperation(op)))\n\t}\n\n\t// --- Private helpers ---\n\n\tprivate async encryptField(\n\t\tfield: Record<string, unknown> | null,\n\t\toperationId: string,\n\t\tfieldName: string,\n\t): Promise<Record<string, unknown> | null> {\n\t\tif (field === null) {\n\t\t\treturn null\n\t\t}\n\n\t\tconst plaintext = new TextEncoder().encode(JSON.stringify(field))\n\n\t\ttry {\n\t\t\tconst { ciphertext, iv } = await encryptData(this.key, plaintext)\n\n\t\t\tconst envelope: EncryptedFieldEnvelope = {\n\t\t\t\t[ENCRYPTED_MARKER]: true,\n\t\t\t\tciphertext: toBase64Url(ciphertext),\n\t\t\t\tiv: toBase64Url(iv),\n\t\t\t\talgorithm: 'AES-256-GCM',\n\t\t\t\tversion: ENCRYPTION_VERSION,\n\t\t\t}\n\n\t\t\treturn envelope as unknown as Record<string, unknown>\n\t\t} catch (cause) {\n\t\t\tif (cause instanceof OperationEncryptionError) {\n\t\t\t\tthrow cause\n\t\t\t}\n\t\t\tthrow new OperationEncryptionError(`Failed to encrypt operation ${fieldName} field.`, {\n\t\t\t\toperationId,\n\t\t\t\tfieldName,\n\t\t\t\tcause: cause instanceof Error ? cause.message : String(cause),\n\t\t\t})\n\t\t}\n\t}\n\n\tprivate async decryptField(\n\t\tfield: Record<string, unknown> | null,\n\t\toperationId: string,\n\t\tfieldName: string,\n\t): Promise<Record<string, unknown> | null> {\n\t\tif (field === null) {\n\t\t\treturn null\n\t\t}\n\n\t\t// Pass through unencrypted fields (backward compatibility)\n\t\tif (!isEncryptedEnvelope(field)) {\n\t\t\treturn field\n\t\t}\n\n\t\tconst envelope = field as unknown as EncryptedFieldEnvelope\n\n\t\tif (envelope.version > ENCRYPTION_VERSION) {\n\t\t\tthrow new OperationEncryptionError(\n\t\t\t\t`Encrypted field uses version ${envelope.version}, but this client only supports version ${ENCRYPTION_VERSION}. Update your @korajs/auth package to decrypt this operation.`,\n\t\t\t\t{ operationId, fieldName, version: envelope.version },\n\t\t\t)\n\t\t}\n\n\t\ttry {\n\t\t\tconst ciphertext = fromBase64Url(envelope.ciphertext)\n\t\t\tconst iv = fromBase64Url(envelope.iv)\n\n\t\t\tconst plaintextBytes = await decryptData(this.key, ciphertext, iv)\n\t\t\tconst json = new TextDecoder().decode(plaintextBytes)\n\t\t\tconst parsed: unknown = JSON.parse(json)\n\n\t\t\tif (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n\t\t\t\tthrow new OperationEncryptionError(`Decrypted ${fieldName} is not a valid record object.`, {\n\t\t\t\t\toperationId,\n\t\t\t\t\tfieldName,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn parsed as Record<string, unknown>\n\t\t} catch (cause) {\n\t\t\tif (cause instanceof OperationEncryptionError) {\n\t\t\t\tthrow cause\n\t\t\t}\n\t\t\tthrow new OperationEncryptionError(\n\t\t\t\t`Failed to decrypt operation ${fieldName} field. This may indicate a wrong encryption key or tampered data.`,\n\t\t\t\t{\n\t\t\t\t\toperationId,\n\t\t\t\t\tfieldName,\n\t\t\t\t\tcause: cause instanceof Error ? cause.message : String(cause),\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t}\n}\n\n// ============================================================================\n// Utility functions\n// ============================================================================\n\n/**\n * Check if a field value is an encrypted envelope.\n *\n * This is a standalone utility function that can be used without constructing\n * an OperationEncryptor instance. Useful for routing logic that needs to\n * detect encrypted operations.\n *\n * @param field - An operation's `data` or `previousData` field\n * @returns true if the field is an encrypted envelope\n */\nexport function isEncryptedField(field: Record<string, unknown> | null): boolean {\n\treturn isEncryptedEnvelope(field)\n}\n\nfunction isEncryptedEnvelope(field: Record<string, unknown> | null): boolean {\n\tif (field === null || typeof field !== 'object') {\n\t\treturn false\n\t}\n\treturn (\n\t\tfield[ENCRYPTED_MARKER] === true &&\n\t\ttypeof field.ciphertext === 'string' &&\n\t\ttypeof field.iv === 'string' &&\n\t\tfield.algorithm === 'AES-256-GCM'\n\t)\n}\n"],"mappings":"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","names":["KoraError","CryptoUnavailableError","assertCryptoAvailable","KoraError","KoraError","KoraError","KoraError","toBase64Url","fromBase64Url"]}
@@ -1,18 +1,12 @@
1
1
  // src/provider/built-in/password-hash.ts
2
- import { randomBytes, pbkdf2, timingSafeEqual } from "crypto";
2
+ import { pbkdf2, randomBytes, timingSafeEqual } from "crypto";
3
3
  var PBKDF2_ITERATIONS = 6e5;
4
4
  var PBKDF2_DIGEST = "sha512";
5
5
  var KEY_LENGTH = 64;
6
6
  var SALT_LENGTH = 32;
7
7
  async function hashPassword(password) {
8
8
  const salt = randomBytes(SALT_LENGTH);
9
- const derivedKey = await pbkdf2Async(
10
- password,
11
- salt,
12
- PBKDF2_ITERATIONS,
13
- KEY_LENGTH,
14
- PBKDF2_DIGEST
15
- );
9
+ const derivedKey = await pbkdf2Async(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST);
16
10
  return {
17
11
  hash: derivedKey.toString("hex"),
18
12
  salt: salt.toString("hex")
@@ -49,4 +43,4 @@ export {
49
43
  hashPassword,
50
44
  verifyPassword
51
45
  };
52
- //# sourceMappingURL=chunk-HOZXDR6Y.js.map
46
+ //# sourceMappingURL=chunk-L7GXPS74.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/provider/built-in/password-hash.ts"],"sourcesContent":["import { pbkdf2, randomBytes, timingSafeEqual } from 'node:crypto'\n\n/** Number of PBKDF2 iterations. 600,000 per OWASP 2023 recommendations for SHA-512. */\nconst PBKDF2_ITERATIONS = 600_000\n\n/** Digest algorithm used by PBKDF2. */\nconst PBKDF2_DIGEST = 'sha512'\n\n/** Length of the derived key in bytes. */\nconst KEY_LENGTH = 64\n\n/** Length of the random salt in bytes. */\nconst SALT_LENGTH = 32\n\ninterface HashResult {\n\t/** Hex-encoded PBKDF2 derived key. */\n\thash: string\n\t/** Hex-encoded random salt used during hashing. */\n\tsalt: string\n}\n\n/**\n * Hashes a password using PBKDF2 with a cryptographically random salt.\n *\n * Uses SHA-512 with 600,000 iterations and a 32-byte random salt,\n * producing a 64-byte derived key. Both the hash and salt are returned\n * as hex-encoded strings for storage.\n *\n * @param password - The plaintext password to hash\n * @returns The hex-encoded hash and salt\n *\n * @example\n * ```typescript\n * const { hash, salt } = await hashPassword('my-secret-password')\n * // Store hash and salt in the database\n * ```\n */\nexport async function hashPassword(password: string): Promise<HashResult> {\n\tconst salt = randomBytes(SALT_LENGTH)\n\n\tconst derivedKey = await pbkdf2Async(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, PBKDF2_DIGEST)\n\n\treturn {\n\t\thash: derivedKey.toString('hex'),\n\t\tsalt: salt.toString('hex'),\n\t}\n}\n\n/**\n * Verifies a plaintext password against a stored hash and salt using\n * timing-safe comparison to prevent timing attacks.\n *\n * Re-derives the key from the password and salt using the same PBKDF2\n * parameters, then compares the result against the stored hash using\n * `crypto.timingSafeEqual` to avoid leaking information through\n * response timing.\n *\n * @param password - The plaintext password to verify\n * @param hash - The hex-encoded hash to compare against\n * @param salt - The hex-encoded salt that was used to produce the hash\n * @returns `true` if the password matches, `false` otherwise\n *\n * @example\n * ```typescript\n * const isValid = await verifyPassword('my-secret-password', storedHash, storedSalt)\n * if (isValid) {\n * // Grant access\n * }\n * ```\n */\nexport async function verifyPassword(\n\tpassword: string,\n\thash: string,\n\tsalt: string,\n): Promise<boolean> {\n\tconst saltBuffer = Buffer.from(salt, 'hex')\n\n\tconst derivedKey = await pbkdf2Async(\n\t\tpassword,\n\t\tsaltBuffer,\n\t\tPBKDF2_ITERATIONS,\n\t\tKEY_LENGTH,\n\t\tPBKDF2_DIGEST,\n\t)\n\n\tconst hashBuffer = Buffer.from(hash, 'hex')\n\n\t// Both buffers must be the same length for timingSafeEqual.\n\t// If the stored hash has an unexpected length, reject rather than throw.\n\tif (derivedKey.length !== hashBuffer.length) {\n\t\treturn false\n\t}\n\n\treturn timingSafeEqual(derivedKey, hashBuffer)\n}\n\n/**\n * Promisified wrapper around Node.js `crypto.pbkdf2`.\n * The callback-based API delegates hashing to libuv's thread pool,\n * avoiding blocking the event loop during the 600,000 iterations.\n */\nfunction pbkdf2Async(\n\tpassword: string,\n\tsalt: Buffer,\n\titerations: number,\n\tkeyLength: number,\n\tdigest: string,\n): Promise<Buffer> {\n\treturn new Promise((resolve, reject) => {\n\t\tpbkdf2(password, salt, iterations, keyLength, digest, (err, derivedKey) => {\n\t\t\tif (err) {\n\t\t\t\treject(err)\n\t\t\t} else {\n\t\t\t\tresolve(derivedKey)\n\t\t\t}\n\t\t})\n\t})\n}\n"],"mappings":";AAAA,SAAS,QAAQ,aAAa,uBAAuB;AAGrD,IAAM,oBAAoB;AAG1B,IAAM,gBAAgB;AAGtB,IAAM,aAAa;AAGnB,IAAM,cAAc;AAyBpB,eAAsB,aAAa,UAAuC;AACzE,QAAM,OAAO,YAAY,WAAW;AAEpC,QAAM,aAAa,MAAM,YAAY,UAAU,MAAM,mBAAmB,YAAY,aAAa;AAEjG,SAAO;AAAA,IACN,MAAM,WAAW,SAAS,KAAK;AAAA,IAC/B,MAAM,KAAK,SAAS,KAAK;AAAA,EAC1B;AACD;AAwBA,eAAsB,eACrB,UACA,MACA,MACmB;AACnB,QAAM,aAAa,OAAO,KAAK,MAAM,KAAK;AAE1C,QAAM,aAAa,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AAEA,QAAM,aAAa,OAAO,KAAK,MAAM,KAAK;AAI1C,MAAI,WAAW,WAAW,WAAW,QAAQ;AAC5C,WAAO;AAAA,EACR;AAEA,SAAO,gBAAgB,YAAY,UAAU;AAC9C;AAOA,SAAS,YACR,UACA,MACA,YACA,WACA,QACkB;AAClB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACvC,WAAO,UAAU,MAAM,YAAY,WAAW,QAAQ,CAAC,KAAK,eAAe;AAC1E,UAAI,KAAK;AACR,eAAO,GAAG;AAAA,MACX,OAAO;AACN,gBAAQ,UAAU;AAAA,MACnB;AAAA,IACD,CAAC;AAAA,EACF,CAAC;AACF;","names":[]}