@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 +81 -28
- package/dist/{chunk-FSU4SK32.js → chunk-IO2MCCG2.js} +35 -40
- package/dist/chunk-IO2MCCG2.js.map +1 -0
- package/dist/{chunk-HOZXDR6Y.js → chunk-L7GXPS74.js} +3 -9
- package/dist/chunk-L7GXPS74.js.map +1 -0
- package/dist/index.cjs +978 -606
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -118
- package/dist/index.d.ts +77 -118
- package/dist/index.js +686 -313
- package/dist/index.js.map +1 -1
- package/dist/org-client-q2u55qod.d.cts +665 -0
- package/dist/org-client-q2u55qod.d.ts +665 -0
- package/dist/{password-hash-HDH6VQCQ.js → password-hash-QRBG6BNJ.js} +2 -2
- package/dist/react.cjs +79 -0
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +17 -1
- package/dist/react.d.ts +17 -1
- package/dist/react.js +79 -0
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +2524 -1672
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +415 -224
- package/dist/server.d.ts +415 -224
- package/dist/server.js +2325 -1484
- package/dist/server.js.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-FSU4SK32.js.map +0 -1
- package/dist/chunk-HOZXDR6Y.js.map +0 -1
- package/dist/org-client-BVTLKcIk.d.cts +0 -355
- package/dist/org-client-BVTLKcIk.d.ts +0 -355
- /package/dist/{password-hash-HDH6VQCQ.js.map → password-hash-QRBG6BNJ.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@korajs/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Offline-first authentication for Kora.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"dist"
|
|
43
43
|
],
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@korajs/core": "0.
|
|
45
|
+
"@korajs/core": "0.5.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"tsup": "^8.0.0",
|
|
52
52
|
"typescript": "^5.7.0",
|
|
53
53
|
"vitest": "^3.0.0",
|
|
54
|
-
"@korajs/store": "0.
|
|
55
|
-
"@korajs/sync": "0.
|
|
56
|
-
"@korajs/server": "0.
|
|
54
|
+
"@korajs/store": "0.5.0",
|
|
55
|
+
"@korajs/sync": "0.5.0",
|
|
56
|
+
"@korajs/server": "0.5.0"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -1 +0,0 @@
|
|
|
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 (\n\t\ttypeof globalThis.crypto === 'undefined' ||\n\t\ttypeof globalThis.crypto.subtle === 'undefined'\n\t) {\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'}\". ` +\n\t\t\t\t'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(\n\t\t\t'Failed to compute SHA-256 thumbprint of the public key JWK.',\n\t\t\t{ cause: 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 (\n\t\ttypeof globalThis.crypto === 'undefined' ||\n\t\ttypeof globalThis.crypto.subtle === 'undefined'\n\t) {\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. ' +\n\t\t\t\t'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:\n\t\t\toptions.authenticatorSelection?.authenticatorAttachment ?? 'platform',\n\t\tresidentKey: options.authenticatorSelection?.residentKey ?? 'preferred',\n\t\tuserVerification:\n\t\t\toptions.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(\n\tattestationObject: Uint8Array,\n): 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(\n\t\t\t'Invalid attestation object: authData is missing or not a byte string.',\n\t\t)\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 =\n\t\t((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\tif (offset >= data.length) {\n\t\tthrow new PasskeyError('CBOR decode error: unexpected end of data.')\n\t}\n\n\tconst initialByte = data[offset] as number\n\tconst majorType = initialByte >> 5\n\tconst additionalInfo = initialByte & 0x1f\n\toffset += 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[offset] as number\n\t\toffset += 1\n\t} else if (additionalInfo === 25) {\n\t\targument = ((data[offset] as number) << 8) | (data[offset + 1] as number)\n\t\toffset += 2\n\t} else if (additionalInfo === 26) {\n\t\targument =\n\t\t\t((data[offset] as number) << 24) |\n\t\t\t((data[offset + 1] as number) << 16) |\n\t\t\t((data[offset + 2] as number) << 8) |\n\t\t\t(data[offset + 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\toffset += 4\n\t} else {\n\t\tthrow new PasskeyError(\n\t\t\t`CBOR decode error: unsupported additional info ${additionalInfo} at byte ${offset - 1}. ` +\n\t\t\t\t'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 }\n\n\t\t// Major type 1: Negative integer (-1 - argument)\n\t\tcase 1:\n\t\t\treturn { value: -1 - argument, offset }\n\n\t\t// Major type 2: Byte string\n\t\tcase 2: {\n\t\t\tconst bytes = data.slice(offset, offset + argument)\n\t\t\treturn { value: bytes, offset: offset + 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(offset, offset + argument)\n\t\t\tconst text = new TextDecoder().decode(textBytes)\n\t\t\treturn { value: text, offset: offset + 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 = offset\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 = offset\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 ${offset - 1}. ` +\n\t\t\t\t\t'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 { encryptData, decryptData } 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(\n\t\t\t\t`Failed to encrypt operation ${fieldName} field.`,\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\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}. ` +\n\t\t\t\t\t'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(\n\t\t\t\t\t`Decrypted ${fieldName} is not a valid record object.`,\n\t\t\t\t\t{ operationId, fieldName },\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. ` +\n\t\t\t\t\t'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 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/provider/built-in/password-hash.ts"],"sourcesContent":["import { randomBytes, pbkdf2, 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(\n\t\tpassword,\n\t\tsalt,\n\t\tPBKDF2_ITERATIONS,\n\t\tKEY_LENGTH,\n\t\tPBKDF2_DIGEST,\n\t)\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,aAAa,QAAQ,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;AAAA,IACxB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACD;AAEA,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":[]}
|
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { KoraError } from '@korajs/core';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Thrown when an authentication operation fails.
|
|
5
|
-
* Includes a machine-readable code and optional context for debugging.
|
|
6
|
-
*/
|
|
7
|
-
declare class AuthError extends KoraError {
|
|
8
|
-
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Possible authentication states for the client.
|
|
12
|
-
* - 'loading': Initial state while restoring tokens from storage
|
|
13
|
-
* - 'authenticated': User is signed in with a valid session
|
|
14
|
-
* - 'unauthenticated': No valid session exists
|
|
15
|
-
*/
|
|
16
|
-
type AuthState = 'loading' | 'authenticated' | 'unauthenticated';
|
|
17
|
-
/**
|
|
18
|
-
* Authenticated user information.
|
|
19
|
-
*/
|
|
20
|
-
interface AuthUser {
|
|
21
|
-
/** Unique user identifier */
|
|
22
|
-
id: string;
|
|
23
|
-
/** User email address */
|
|
24
|
-
email: string;
|
|
25
|
-
/** Display name (may be absent if user did not provide one) */
|
|
26
|
-
name: string | null;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Configuration for the AuthClient.
|
|
30
|
-
*/
|
|
31
|
-
interface AuthClientConfig {
|
|
32
|
-
/** Base URL of the auth server (e.g. 'http://localhost:3001') */
|
|
33
|
-
serverUrl: string;
|
|
34
|
-
/** Storage key prefix for tokens. Defaults to 'kora_auth' */
|
|
35
|
-
storageKey?: string;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Client-side authentication manager for Kora.js.
|
|
39
|
-
*
|
|
40
|
-
* Manages token storage, session restoration, sign-up, sign-in, sign-out,
|
|
41
|
-
* token refresh, and auth state change notifications. Framework-agnostic --
|
|
42
|
-
* works in any JavaScript environment with `fetch` and optionally `localStorage`.
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* ```typescript
|
|
46
|
-
* const auth = new AuthClient({ serverUrl: 'http://localhost:3001' })
|
|
47
|
-
* await auth.initialize()
|
|
48
|
-
*
|
|
49
|
-
* if (!auth.isAuthenticated) {
|
|
50
|
-
* await auth.signIn({ email: 'user@example.com', password: 'secret' })
|
|
51
|
-
* }
|
|
52
|
-
*
|
|
53
|
-
* const unsub = auth.onAuthChange((state) => {
|
|
54
|
-
* console.log('Auth state:', state)
|
|
55
|
-
* })
|
|
56
|
-
* ```
|
|
57
|
-
*/
|
|
58
|
-
declare class AuthClient {
|
|
59
|
-
private readonly serverUrl;
|
|
60
|
-
private readonly storage;
|
|
61
|
-
private readonly listeners;
|
|
62
|
-
private _state;
|
|
63
|
-
private _user;
|
|
64
|
-
private _refreshPromise;
|
|
65
|
-
private _initialized;
|
|
66
|
-
/**
|
|
67
|
-
* Creates a new AuthClient.
|
|
68
|
-
*
|
|
69
|
-
* @param config - Auth client configuration
|
|
70
|
-
*/
|
|
71
|
-
constructor(config: AuthClientConfig);
|
|
72
|
-
/** Current authentication state. */
|
|
73
|
-
get state(): AuthState;
|
|
74
|
-
/** Current authenticated user, or null if not signed in. */
|
|
75
|
-
get currentUser(): AuthUser | null;
|
|
76
|
-
/** Whether the user is currently authenticated. */
|
|
77
|
-
get isAuthenticated(): boolean;
|
|
78
|
-
/**
|
|
79
|
-
* Initialize the auth client by restoring a session from stored tokens.
|
|
80
|
-
*
|
|
81
|
-
* Loads tokens from storage, validates the access token, and attempts a
|
|
82
|
-
* refresh if the access token is expired but a refresh token is available.
|
|
83
|
-
* Safe to call multiple times -- subsequent calls are no-ops once initialized.
|
|
84
|
-
*/
|
|
85
|
-
initialize(): Promise<void>;
|
|
86
|
-
/**
|
|
87
|
-
* Register a new user account.
|
|
88
|
-
*
|
|
89
|
-
* @param params - Sign-up credentials
|
|
90
|
-
* @returns The newly created AuthUser
|
|
91
|
-
* @throws {AuthError} If the request fails or the server returns an error
|
|
92
|
-
*/
|
|
93
|
-
signUp(params: {
|
|
94
|
-
email: string;
|
|
95
|
-
password: string;
|
|
96
|
-
name?: string;
|
|
97
|
-
}): Promise<AuthUser>;
|
|
98
|
-
/**
|
|
99
|
-
* Sign in with email and password.
|
|
100
|
-
*
|
|
101
|
-
* @param params - Sign-in credentials
|
|
102
|
-
* @returns The authenticated AuthUser
|
|
103
|
-
* @throws {AuthError} If the credentials are invalid or the request fails
|
|
104
|
-
*/
|
|
105
|
-
signIn(params: {
|
|
106
|
-
email: string;
|
|
107
|
-
password: string;
|
|
108
|
-
}): Promise<AuthUser>;
|
|
109
|
-
/**
|
|
110
|
-
* Sign out the current user.
|
|
111
|
-
*
|
|
112
|
-
* Clears local tokens and attempts to revoke the refresh token on the server
|
|
113
|
-
* (best-effort — succeeds even if the server is unreachable). This ensures that
|
|
114
|
-
* stolen refresh tokens cannot be used after the user explicitly signs out.
|
|
115
|
-
*/
|
|
116
|
-
signOut(): Promise<void>;
|
|
117
|
-
/**
|
|
118
|
-
* Get a valid access token, automatically refreshing if expired.
|
|
119
|
-
*
|
|
120
|
-
* @returns A valid access token string, or null if the user is not
|
|
121
|
-
* authenticated and refresh is not possible
|
|
122
|
-
*/
|
|
123
|
-
getAccessToken(): Promise<string | null>;
|
|
124
|
-
/**
|
|
125
|
-
* Get a valid token for the sync engine handshake.
|
|
126
|
-
* Alias for {@link getAccessToken}.
|
|
127
|
-
*
|
|
128
|
-
* @returns A valid access token string, or null if unavailable
|
|
129
|
-
*/
|
|
130
|
-
getSyncToken(): Promise<string | null>;
|
|
131
|
-
/**
|
|
132
|
-
* Subscribe to authentication state changes.
|
|
133
|
-
*
|
|
134
|
-
* The callback is invoked whenever the auth state transitions (e.g., from
|
|
135
|
-
* 'unauthenticated' to 'authenticated' on sign-in).
|
|
136
|
-
*
|
|
137
|
-
* @param callback - Function called with the new AuthState on each change
|
|
138
|
-
* @returns An unsubscribe function that removes the listener
|
|
139
|
-
*
|
|
140
|
-
* @example
|
|
141
|
-
* ```typescript
|
|
142
|
-
* const unsub = auth.onAuthChange((state) => {
|
|
143
|
-
* console.log('Auth state changed to:', state)
|
|
144
|
-
* })
|
|
145
|
-
* // Later: unsub()
|
|
146
|
-
* ```
|
|
147
|
-
*/
|
|
148
|
-
onAuthChange(callback: (state: AuthState) => void): () => void;
|
|
149
|
-
/**
|
|
150
|
-
* Update internal state and notify all listeners.
|
|
151
|
-
*/
|
|
152
|
-
private setState;
|
|
153
|
-
/**
|
|
154
|
-
* Restore a session from a valid access token by fetching the user profile.
|
|
155
|
-
* Falls back to extracting the user ID from the token payload if the
|
|
156
|
-
* /auth/me request fails (offline scenario).
|
|
157
|
-
*/
|
|
158
|
-
private restoreSession;
|
|
159
|
-
/**
|
|
160
|
-
* Fetch the current user profile from the server.
|
|
161
|
-
*/
|
|
162
|
-
private fetchUserProfile;
|
|
163
|
-
/**
|
|
164
|
-
* Refresh the access token using a refresh token.
|
|
165
|
-
* De-duplicates concurrent refresh calls so only one network request is made.
|
|
166
|
-
*/
|
|
167
|
-
private refreshAccessToken;
|
|
168
|
-
/**
|
|
169
|
-
* Execute the token refresh network request.
|
|
170
|
-
*/
|
|
171
|
-
private performRefresh;
|
|
172
|
-
/**
|
|
173
|
-
* Make an HTTP request to the auth server.
|
|
174
|
-
*
|
|
175
|
-
* @param path - URL path relative to serverUrl (e.g. '/auth/signin')
|
|
176
|
-
* @param options - Request options
|
|
177
|
-
* @returns Parsed JSON response body
|
|
178
|
-
* @throws {AuthError} On network failure or non-2xx response
|
|
179
|
-
*/
|
|
180
|
-
private request;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Organization info returned by the server.
|
|
185
|
-
*/
|
|
186
|
-
interface ClientOrganization {
|
|
187
|
-
id: string;
|
|
188
|
-
name: string;
|
|
189
|
-
slug: string;
|
|
190
|
-
ownerId: string;
|
|
191
|
-
createdAt: number;
|
|
192
|
-
updatedAt: number;
|
|
193
|
-
metadata: Record<string, unknown>;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Membership info returned by the server.
|
|
197
|
-
*/
|
|
198
|
-
interface ClientMembership {
|
|
199
|
-
id: string;
|
|
200
|
-
orgId: string;
|
|
201
|
-
userId: string;
|
|
202
|
-
role: string;
|
|
203
|
-
invitedBy: string | null;
|
|
204
|
-
joinedAt: number;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Invitation info returned by the server.
|
|
208
|
-
*/
|
|
209
|
-
interface ClientInvitation {
|
|
210
|
-
id: string;
|
|
211
|
-
orgId: string;
|
|
212
|
-
email: string;
|
|
213
|
-
role: string;
|
|
214
|
-
invitedBy: string;
|
|
215
|
-
token: string;
|
|
216
|
-
createdAt: number;
|
|
217
|
-
expiresAt: number;
|
|
218
|
-
status: string;
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Configuration for the OrgClient.
|
|
222
|
-
*/
|
|
223
|
-
interface OrgClientConfig {
|
|
224
|
-
/** Base URL of the auth/org server */
|
|
225
|
-
serverUrl: string;
|
|
226
|
-
/** Function that returns a valid access token for authenticated requests */
|
|
227
|
-
getAccessToken: () => Promise<string | null>;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Thrown when an org operation fails.
|
|
231
|
-
*/
|
|
232
|
-
declare class OrgClientError extends KoraError {
|
|
233
|
-
constructor(message: string, code: string, context?: Record<string, unknown>);
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Client-side organization manager.
|
|
237
|
-
*
|
|
238
|
-
* Handles org CRUD, member management, invitations, and active org switching.
|
|
239
|
-
* Framework-agnostic — works in any JavaScript environment with `fetch`.
|
|
240
|
-
*
|
|
241
|
-
* @example
|
|
242
|
-
* ```typescript
|
|
243
|
-
* const orgClient = new OrgClient({
|
|
244
|
-
* serverUrl: 'http://localhost:3001',
|
|
245
|
-
* getAccessToken: () => authClient.getAccessToken(),
|
|
246
|
-
* })
|
|
247
|
-
*
|
|
248
|
-
* const org = await orgClient.createOrg({ name: 'Acme Inc', slug: 'acme' })
|
|
249
|
-
* orgClient.switchOrg(org.id)
|
|
250
|
-
* ```
|
|
251
|
-
*/
|
|
252
|
-
declare class OrgClient {
|
|
253
|
-
private readonly serverUrl;
|
|
254
|
-
private readonly getAccessToken;
|
|
255
|
-
private readonly listeners;
|
|
256
|
-
private _activeOrgId;
|
|
257
|
-
private _activeOrg;
|
|
258
|
-
private _activeRole;
|
|
259
|
-
constructor(config: OrgClientConfig);
|
|
260
|
-
/** Currently active organization ID */
|
|
261
|
-
get activeOrgId(): string | null;
|
|
262
|
-
/** Currently active organization */
|
|
263
|
-
get activeOrg(): ClientOrganization | null;
|
|
264
|
-
/** Current user's role in the active organization */
|
|
265
|
-
get activeRole(): string | null;
|
|
266
|
-
/**
|
|
267
|
-
* Create a new organization.
|
|
268
|
-
*/
|
|
269
|
-
createOrg(params: {
|
|
270
|
-
name: string;
|
|
271
|
-
slug?: string;
|
|
272
|
-
metadata?: Record<string, unknown>;
|
|
273
|
-
}): Promise<ClientOrganization>;
|
|
274
|
-
/**
|
|
275
|
-
* List all organizations the current user belongs to.
|
|
276
|
-
*/
|
|
277
|
-
listOrgs(): Promise<ClientOrganization[]>;
|
|
278
|
-
/**
|
|
279
|
-
* Get an organization by ID.
|
|
280
|
-
*/
|
|
281
|
-
getOrg(orgId: string): Promise<ClientOrganization>;
|
|
282
|
-
/**
|
|
283
|
-
* Update an organization.
|
|
284
|
-
*/
|
|
285
|
-
updateOrg(orgId: string, params: {
|
|
286
|
-
name?: string;
|
|
287
|
-
slug?: string;
|
|
288
|
-
metadata?: Record<string, unknown>;
|
|
289
|
-
}): Promise<ClientOrganization>;
|
|
290
|
-
/**
|
|
291
|
-
* Delete an organization.
|
|
292
|
-
*/
|
|
293
|
-
deleteOrg(orgId: string): Promise<void>;
|
|
294
|
-
/**
|
|
295
|
-
* Switch the active organization context.
|
|
296
|
-
* Fetches the org details and the user's membership/role.
|
|
297
|
-
*/
|
|
298
|
-
switchOrg(orgId: string): Promise<void>;
|
|
299
|
-
/**
|
|
300
|
-
* Clear the active organization (no org selected).
|
|
301
|
-
*/
|
|
302
|
-
clearActiveOrg(): void;
|
|
303
|
-
/**
|
|
304
|
-
* List members of an organization.
|
|
305
|
-
*/
|
|
306
|
-
listMembers(orgId: string): Promise<ClientMembership[]>;
|
|
307
|
-
/**
|
|
308
|
-
* Remove a member from an organization.
|
|
309
|
-
*/
|
|
310
|
-
removeMember(orgId: string, userId: string): Promise<void>;
|
|
311
|
-
/**
|
|
312
|
-
* Update a member's role.
|
|
313
|
-
*/
|
|
314
|
-
updateMemberRole(orgId: string, userId: string, role: string): Promise<ClientMembership>;
|
|
315
|
-
/**
|
|
316
|
-
* Transfer ownership to another member.
|
|
317
|
-
*/
|
|
318
|
-
transferOwnership(orgId: string, newOwnerId: string): Promise<void>;
|
|
319
|
-
/**
|
|
320
|
-
* Leave an organization (remove yourself).
|
|
321
|
-
*/
|
|
322
|
-
leaveOrg(orgId: string): Promise<void>;
|
|
323
|
-
/**
|
|
324
|
-
* Invite a user to an organization by email.
|
|
325
|
-
*/
|
|
326
|
-
inviteMember(orgId: string, params: {
|
|
327
|
-
email: string;
|
|
328
|
-
role: string;
|
|
329
|
-
}): Promise<ClientInvitation>;
|
|
330
|
-
/**
|
|
331
|
-
* Accept an invitation by token.
|
|
332
|
-
*/
|
|
333
|
-
acceptInvitation(token: string): Promise<ClientMembership>;
|
|
334
|
-
/**
|
|
335
|
-
* List pending invitations for an organization.
|
|
336
|
-
*/
|
|
337
|
-
listInvitations(orgId: string): Promise<ClientInvitation[]>;
|
|
338
|
-
/**
|
|
339
|
-
* Revoke a pending invitation.
|
|
340
|
-
*/
|
|
341
|
-
revokeInvitation(orgId: string, invitationId: string): Promise<void>;
|
|
342
|
-
/**
|
|
343
|
-
* List pending invitations for the current user's email.
|
|
344
|
-
*/
|
|
345
|
-
listMyInvitations(email: string): Promise<ClientInvitation[]>;
|
|
346
|
-
/**
|
|
347
|
-
* Subscribe to active org changes.
|
|
348
|
-
* @returns Unsubscribe function
|
|
349
|
-
*/
|
|
350
|
-
onOrgChange(callback: (orgId: string | null) => void): () => void;
|
|
351
|
-
private notifyListeners;
|
|
352
|
-
private request;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export { AuthClient as A, type ClientInvitation as C, OrgClient as O, type AuthClientConfig as a, AuthError as b, type AuthState as c, type AuthUser as d, type ClientMembership as e, type ClientOrganization as f, type OrgClientConfig as g, OrgClientError as h };
|