@korajs/auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/device/device-identity.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"],"mappings":"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","names":[]}
@@ -0,0 +1,247 @@
1
+ import { KoraError } from '@korajs/core';
2
+
3
+ /**
4
+ * Authenticated user record.
5
+ * Represents the public-facing user identity returned by sign-in and sign-up flows.
6
+ */
7
+ interface AuthUser {
8
+ /** Unique user identifier (UUID v7) */
9
+ id: string;
10
+ /** User's email address, used as the primary login credential */
11
+ email: string;
12
+ /** Display name */
13
+ name: string;
14
+ /** Timestamp of user creation (milliseconds since epoch) */
15
+ createdAt: number;
16
+ /** Timestamp of last profile update (milliseconds since epoch) */
17
+ updatedAt: number;
18
+ }
19
+ /** The three kinds of tokens issued by the auth system */
20
+ type TokenType = 'access' | 'refresh' | 'device_credential';
21
+ /**
22
+ * Base payload present in all JWT tokens.
23
+ * Fields follow standard JWT claim names.
24
+ */
25
+ interface TokenPayload {
26
+ /** Subject: the user ID */
27
+ sub: string;
28
+ /** Device ID that this token was issued to */
29
+ dev: string;
30
+ /** Which kind of token this is */
31
+ type: TokenType;
32
+ /** Issued-at time (seconds since epoch, per JWT spec) */
33
+ iat: number;
34
+ /** Expiration time (seconds since epoch, per JWT spec) */
35
+ exp: number;
36
+ }
37
+ /**
38
+ * Bundle of tokens returned after successful authentication.
39
+ */
40
+ interface AuthTokens {
41
+ /** Short-lived access token */
42
+ accessToken: string;
43
+ /** Long-lived refresh token */
44
+ refreshToken: string;
45
+ /** Optional device credential for offline operation */
46
+ deviceCredential?: string;
47
+ }
48
+ /** Supported auth provider types */
49
+ type AuthProviderType = 'built-in' | 'custom';
50
+ /** Unlock mechanism for encrypted local storage */
51
+ type UnlockMethod = 'biometric' | 'passphrase' | 'both';
52
+ /**
53
+ * Encryption settings for locally stored auth credentials.
54
+ */
55
+ interface AuthEncryptionConfig {
56
+ /** Whether local credential encryption is enabled */
57
+ enabled: boolean;
58
+ /** How the user unlocks encrypted credentials */
59
+ unlock: UnlockMethod;
60
+ /** Milliseconds of inactivity before the app auto-locks. Defaults to 15 minutes. */
61
+ autoLockTimeout?: number;
62
+ }
63
+ /**
64
+ * Custom lifetimes for each token type.
65
+ * All values are in milliseconds.
66
+ */
67
+ interface TokenLifetimeConfig {
68
+ /** Access token lifetime in ms. Default: 15 minutes. */
69
+ access?: number;
70
+ /** Refresh token lifetime in ms. Default: 90 days. */
71
+ refresh?: number;
72
+ /** Device credential lifetime in ms. Default: 90 days. */
73
+ deviceCredential?: number;
74
+ }
75
+ /**
76
+ * Configuration for the Kora auth system.
77
+ * Passed to the auth initializer to control provider, encryption, and token behavior.
78
+ */
79
+ interface AuthConfig {
80
+ /** Which auth provider to use */
81
+ provider: AuthProviderType;
82
+ /** Local encryption settings for stored credentials */
83
+ encryption?: AuthEncryptionConfig;
84
+ /** Maximum time a device can operate offline without checking in (ms). Default: 30 days. */
85
+ maxOfflineDuration?: number;
86
+ /** Custom token lifetimes */
87
+ tokenLifetimes?: TokenLifetimeConfig;
88
+ }
89
+ /** Possible authentication states */
90
+ type AuthStatus = 'authenticated' | 'unauthenticated' | 'locked';
91
+ /**
92
+ * Events emitted by the auth system.
93
+ * These integrate with Kora's event system for DevTools observability.
94
+ */
95
+ type AuthEvent = {
96
+ type: 'auth:signed-in';
97
+ user: AuthUser;
98
+ } | {
99
+ type: 'auth:signed-out';
100
+ } | {
101
+ type: 'auth:locked';
102
+ } | {
103
+ type: 'auth:unlocked';
104
+ user: AuthUser;
105
+ } | {
106
+ type: 'auth:token-refreshed';
107
+ } | {
108
+ type: 'auth:device-revoked';
109
+ deviceId: string;
110
+ } | {
111
+ type: 'auth:permission-changed';
112
+ };
113
+ /** Extract the event type string union from AuthEvent */
114
+ type AuthEventType = AuthEvent['type'];
115
+
116
+ /**
117
+ * Thrown when the Web Crypto API is not available in the current environment.
118
+ * This can happen in older Node.js versions or SSR environments without crypto support.
119
+ */
120
+ declare class CryptoUnavailableError extends KoraError {
121
+ constructor();
122
+ }
123
+ /**
124
+ * Thrown when a device identity operation fails (key generation, signing, verification).
125
+ */
126
+ declare class DeviceIdentityError extends KoraError {
127
+ constructor(message: string, context?: Record<string, unknown>);
128
+ }
129
+ /**
130
+ * Encodes an ArrayBuffer as a base64url string (no padding).
131
+ *
132
+ * @param buffer - The binary data to encode
133
+ * @returns A base64url-encoded string without padding characters
134
+ */
135
+ declare function toBase64Url(buffer: ArrayBuffer): string;
136
+ /**
137
+ * Decodes a base64url string (no padding) into a Uint8Array.
138
+ *
139
+ * @param str - A base64url-encoded string (with or without padding)
140
+ * @returns The decoded binary data as a Uint8Array
141
+ */
142
+ declare function fromBase64Url(str: string): Uint8Array;
143
+ /**
144
+ * Generates an ECDSA P-256 key pair for device identity.
145
+ *
146
+ * The private key is marked as non-extractable, ensuring it cannot be
147
+ * exported from the browser's crypto subsystem. This provides
148
+ * proof-of-possession: only code running on this device can sign with the key.
149
+ *
150
+ * @returns A CryptoKeyPair containing the public and private ECDSA P-256 keys
151
+ * @throws {CryptoUnavailableError} If `crypto.subtle` is not available
152
+ * @throws {DeviceIdentityError} If key generation fails
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * const keyPair = await generateDeviceKeyPair()
157
+ * // keyPair.publicKey can be exported; keyPair.privateKey stays on device
158
+ * ```
159
+ */
160
+ declare function generateDeviceKeyPair(): Promise<CryptoKeyPair>;
161
+ /**
162
+ * Exports the public key from a key pair as a JSON Web Key (JWK).
163
+ *
164
+ * The JWK can be safely transmitted to a server or other devices to identify
165
+ * this device. It contains only the public component of the key pair.
166
+ *
167
+ * @param keyPair - The CryptoKeyPair whose public key should be exported
168
+ * @returns The public key in JWK format
169
+ * @throws {CryptoUnavailableError} If `crypto.subtle` is not available
170
+ * @throws {DeviceIdentityError} If the export operation fails
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * const keyPair = await generateDeviceKeyPair()
175
+ * const jwk = await exportPublicKeyJwk(keyPair)
176
+ * // jwk contains { kty: 'EC', crv: 'P-256', x: '...', y: '...' }
177
+ * ```
178
+ */
179
+ declare function exportPublicKeyJwk(keyPair: CryptoKeyPair): Promise<JsonWebKey>;
180
+ /**
181
+ * Signs a challenge string with the device's private key.
182
+ *
183
+ * Used for proof-of-possession during authentication: the server sends a
184
+ * random challenge, and the device proves it holds the private key by signing it.
185
+ *
186
+ * @param privateKey - The device's private CryptoKey (ECDSA P-256)
187
+ * @param challenge - The challenge string to sign (typically a random nonce from the server)
188
+ * @returns A base64url-encoded ECDSA signature (no padding)
189
+ * @throws {CryptoUnavailableError} If `crypto.subtle` is not available
190
+ * @throws {DeviceIdentityError} If the signing operation fails
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * const keyPair = await generateDeviceKeyPair()
195
+ * const signature = await signChallenge(keyPair.privateKey, 'server-nonce-abc123')
196
+ * // signature is a base64url string like 'MEUCIQDx...'
197
+ * ```
198
+ */
199
+ declare function signChallenge(privateKey: CryptoKey, challenge: string): Promise<string>;
200
+ /**
201
+ * Verifies a challenge signature against a public key.
202
+ *
203
+ * Used server-side (or on any verifying party) to confirm that a device
204
+ * holds the private key corresponding to the given public key.
205
+ *
206
+ * @param publicKeyJwk - The device's public key in JWK format
207
+ * @param challenge - The original challenge string that was signed
208
+ * @param signature - The base64url-encoded signature to verify
209
+ * @returns `true` if the signature is valid, `false` otherwise
210
+ * @throws {CryptoUnavailableError} If `crypto.subtle` is not available
211
+ * @throws {DeviceIdentityError} If the verification operation fails due to an invalid key or format
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * const isValid = await verifyChallenge(publicKeyJwk, 'server-nonce-abc123', signature)
216
+ * if (isValid) {
217
+ * // Device proved possession of the private key
218
+ * }
219
+ * ```
220
+ */
221
+ declare function verifyChallenge(publicKeyJwk: JsonWebKey, challenge: string, signature: string): Promise<boolean>;
222
+ /**
223
+ * Computes a SHA-256 thumbprint of a JWK public key.
224
+ *
225
+ * The thumbprint is computed per RFC 7638: the JWK members required for the key
226
+ * type are serialized in lexicographic order, then hashed with SHA-256. For EC keys
227
+ * (kty: "EC"), the required members are `crv`, `kty`, `x`, and `y`.
228
+ *
229
+ * This thumbprint serves as a compact, stable identifier for the device's public key
230
+ * (used as the `dpk` claim in device credentials).
231
+ *
232
+ * @param publicKeyJwk - The public key in JWK format (must be an EC P-256 key)
233
+ * @returns A base64url-encoded SHA-256 thumbprint (no padding)
234
+ * @throws {CryptoUnavailableError} If `crypto.subtle` is not available
235
+ * @throws {DeviceIdentityError} If the thumbprint computation fails or the JWK is missing required fields
236
+ *
237
+ * @example
238
+ * ```typescript
239
+ * const keyPair = await generateDeviceKeyPair()
240
+ * const jwk = await exportPublicKeyJwk(keyPair)
241
+ * const thumbprint = await computePublicKeyThumbprint(jwk)
242
+ * // thumbprint is a base64url string, e.g., 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs'
243
+ * ```
244
+ */
245
+ declare function computePublicKeyThumbprint(publicKeyJwk: JsonWebKey): Promise<string>;
246
+
247
+ export { type AuthTokens as A, CryptoUnavailableError as C, DeviceIdentityError as D, type TokenPayload as T, type AuthConfig as a, type AuthEvent as b, type AuthEventType as c, type AuthStatus as d, type TokenType as e, computePublicKeyThumbprint as f, exportPublicKeyJwk as g, fromBase64Url as h, generateDeviceKeyPair as i, signChallenge as s, toBase64Url as t, verifyChallenge as v };
@@ -0,0 +1,247 @@
1
+ import { KoraError } from '@korajs/core';
2
+
3
+ /**
4
+ * Authenticated user record.
5
+ * Represents the public-facing user identity returned by sign-in and sign-up flows.
6
+ */
7
+ interface AuthUser {
8
+ /** Unique user identifier (UUID v7) */
9
+ id: string;
10
+ /** User's email address, used as the primary login credential */
11
+ email: string;
12
+ /** Display name */
13
+ name: string;
14
+ /** Timestamp of user creation (milliseconds since epoch) */
15
+ createdAt: number;
16
+ /** Timestamp of last profile update (milliseconds since epoch) */
17
+ updatedAt: number;
18
+ }
19
+ /** The three kinds of tokens issued by the auth system */
20
+ type TokenType = 'access' | 'refresh' | 'device_credential';
21
+ /**
22
+ * Base payload present in all JWT tokens.
23
+ * Fields follow standard JWT claim names.
24
+ */
25
+ interface TokenPayload {
26
+ /** Subject: the user ID */
27
+ sub: string;
28
+ /** Device ID that this token was issued to */
29
+ dev: string;
30
+ /** Which kind of token this is */
31
+ type: TokenType;
32
+ /** Issued-at time (seconds since epoch, per JWT spec) */
33
+ iat: number;
34
+ /** Expiration time (seconds since epoch, per JWT spec) */
35
+ exp: number;
36
+ }
37
+ /**
38
+ * Bundle of tokens returned after successful authentication.
39
+ */
40
+ interface AuthTokens {
41
+ /** Short-lived access token */
42
+ accessToken: string;
43
+ /** Long-lived refresh token */
44
+ refreshToken: string;
45
+ /** Optional device credential for offline operation */
46
+ deviceCredential?: string;
47
+ }
48
+ /** Supported auth provider types */
49
+ type AuthProviderType = 'built-in' | 'custom';
50
+ /** Unlock mechanism for encrypted local storage */
51
+ type UnlockMethod = 'biometric' | 'passphrase' | 'both';
52
+ /**
53
+ * Encryption settings for locally stored auth credentials.
54
+ */
55
+ interface AuthEncryptionConfig {
56
+ /** Whether local credential encryption is enabled */
57
+ enabled: boolean;
58
+ /** How the user unlocks encrypted credentials */
59
+ unlock: UnlockMethod;
60
+ /** Milliseconds of inactivity before the app auto-locks. Defaults to 15 minutes. */
61
+ autoLockTimeout?: number;
62
+ }
63
+ /**
64
+ * Custom lifetimes for each token type.
65
+ * All values are in milliseconds.
66
+ */
67
+ interface TokenLifetimeConfig {
68
+ /** Access token lifetime in ms. Default: 15 minutes. */
69
+ access?: number;
70
+ /** Refresh token lifetime in ms. Default: 90 days. */
71
+ refresh?: number;
72
+ /** Device credential lifetime in ms. Default: 90 days. */
73
+ deviceCredential?: number;
74
+ }
75
+ /**
76
+ * Configuration for the Kora auth system.
77
+ * Passed to the auth initializer to control provider, encryption, and token behavior.
78
+ */
79
+ interface AuthConfig {
80
+ /** Which auth provider to use */
81
+ provider: AuthProviderType;
82
+ /** Local encryption settings for stored credentials */
83
+ encryption?: AuthEncryptionConfig;
84
+ /** Maximum time a device can operate offline without checking in (ms). Default: 30 days. */
85
+ maxOfflineDuration?: number;
86
+ /** Custom token lifetimes */
87
+ tokenLifetimes?: TokenLifetimeConfig;
88
+ }
89
+ /** Possible authentication states */
90
+ type AuthStatus = 'authenticated' | 'unauthenticated' | 'locked';
91
+ /**
92
+ * Events emitted by the auth system.
93
+ * These integrate with Kora's event system for DevTools observability.
94
+ */
95
+ type AuthEvent = {
96
+ type: 'auth:signed-in';
97
+ user: AuthUser;
98
+ } | {
99
+ type: 'auth:signed-out';
100
+ } | {
101
+ type: 'auth:locked';
102
+ } | {
103
+ type: 'auth:unlocked';
104
+ user: AuthUser;
105
+ } | {
106
+ type: 'auth:token-refreshed';
107
+ } | {
108
+ type: 'auth:device-revoked';
109
+ deviceId: string;
110
+ } | {
111
+ type: 'auth:permission-changed';
112
+ };
113
+ /** Extract the event type string union from AuthEvent */
114
+ type AuthEventType = AuthEvent['type'];
115
+
116
+ /**
117
+ * Thrown when the Web Crypto API is not available in the current environment.
118
+ * This can happen in older Node.js versions or SSR environments without crypto support.
119
+ */
120
+ declare class CryptoUnavailableError extends KoraError {
121
+ constructor();
122
+ }
123
+ /**
124
+ * Thrown when a device identity operation fails (key generation, signing, verification).
125
+ */
126
+ declare class DeviceIdentityError extends KoraError {
127
+ constructor(message: string, context?: Record<string, unknown>);
128
+ }
129
+ /**
130
+ * Encodes an ArrayBuffer as a base64url string (no padding).
131
+ *
132
+ * @param buffer - The binary data to encode
133
+ * @returns A base64url-encoded string without padding characters
134
+ */
135
+ declare function toBase64Url(buffer: ArrayBuffer): string;
136
+ /**
137
+ * Decodes a base64url string (no padding) into a Uint8Array.
138
+ *
139
+ * @param str - A base64url-encoded string (with or without padding)
140
+ * @returns The decoded binary data as a Uint8Array
141
+ */
142
+ declare function fromBase64Url(str: string): Uint8Array;
143
+ /**
144
+ * Generates an ECDSA P-256 key pair for device identity.
145
+ *
146
+ * The private key is marked as non-extractable, ensuring it cannot be
147
+ * exported from the browser's crypto subsystem. This provides
148
+ * proof-of-possession: only code running on this device can sign with the key.
149
+ *
150
+ * @returns A CryptoKeyPair containing the public and private ECDSA P-256 keys
151
+ * @throws {CryptoUnavailableError} If `crypto.subtle` is not available
152
+ * @throws {DeviceIdentityError} If key generation fails
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * const keyPair = await generateDeviceKeyPair()
157
+ * // keyPair.publicKey can be exported; keyPair.privateKey stays on device
158
+ * ```
159
+ */
160
+ declare function generateDeviceKeyPair(): Promise<CryptoKeyPair>;
161
+ /**
162
+ * Exports the public key from a key pair as a JSON Web Key (JWK).
163
+ *
164
+ * The JWK can be safely transmitted to a server or other devices to identify
165
+ * this device. It contains only the public component of the key pair.
166
+ *
167
+ * @param keyPair - The CryptoKeyPair whose public key should be exported
168
+ * @returns The public key in JWK format
169
+ * @throws {CryptoUnavailableError} If `crypto.subtle` is not available
170
+ * @throws {DeviceIdentityError} If the export operation fails
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * const keyPair = await generateDeviceKeyPair()
175
+ * const jwk = await exportPublicKeyJwk(keyPair)
176
+ * // jwk contains { kty: 'EC', crv: 'P-256', x: '...', y: '...' }
177
+ * ```
178
+ */
179
+ declare function exportPublicKeyJwk(keyPair: CryptoKeyPair): Promise<JsonWebKey>;
180
+ /**
181
+ * Signs a challenge string with the device's private key.
182
+ *
183
+ * Used for proof-of-possession during authentication: the server sends a
184
+ * random challenge, and the device proves it holds the private key by signing it.
185
+ *
186
+ * @param privateKey - The device's private CryptoKey (ECDSA P-256)
187
+ * @param challenge - The challenge string to sign (typically a random nonce from the server)
188
+ * @returns A base64url-encoded ECDSA signature (no padding)
189
+ * @throws {CryptoUnavailableError} If `crypto.subtle` is not available
190
+ * @throws {DeviceIdentityError} If the signing operation fails
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * const keyPair = await generateDeviceKeyPair()
195
+ * const signature = await signChallenge(keyPair.privateKey, 'server-nonce-abc123')
196
+ * // signature is a base64url string like 'MEUCIQDx...'
197
+ * ```
198
+ */
199
+ declare function signChallenge(privateKey: CryptoKey, challenge: string): Promise<string>;
200
+ /**
201
+ * Verifies a challenge signature against a public key.
202
+ *
203
+ * Used server-side (or on any verifying party) to confirm that a device
204
+ * holds the private key corresponding to the given public key.
205
+ *
206
+ * @param publicKeyJwk - The device's public key in JWK format
207
+ * @param challenge - The original challenge string that was signed
208
+ * @param signature - The base64url-encoded signature to verify
209
+ * @returns `true` if the signature is valid, `false` otherwise
210
+ * @throws {CryptoUnavailableError} If `crypto.subtle` is not available
211
+ * @throws {DeviceIdentityError} If the verification operation fails due to an invalid key or format
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * const isValid = await verifyChallenge(publicKeyJwk, 'server-nonce-abc123', signature)
216
+ * if (isValid) {
217
+ * // Device proved possession of the private key
218
+ * }
219
+ * ```
220
+ */
221
+ declare function verifyChallenge(publicKeyJwk: JsonWebKey, challenge: string, signature: string): Promise<boolean>;
222
+ /**
223
+ * Computes a SHA-256 thumbprint of a JWK public key.
224
+ *
225
+ * The thumbprint is computed per RFC 7638: the JWK members required for the key
226
+ * type are serialized in lexicographic order, then hashed with SHA-256. For EC keys
227
+ * (kty: "EC"), the required members are `crv`, `kty`, `x`, and `y`.
228
+ *
229
+ * This thumbprint serves as a compact, stable identifier for the device's public key
230
+ * (used as the `dpk` claim in device credentials).
231
+ *
232
+ * @param publicKeyJwk - The public key in JWK format (must be an EC P-256 key)
233
+ * @returns A base64url-encoded SHA-256 thumbprint (no padding)
234
+ * @throws {CryptoUnavailableError} If `crypto.subtle` is not available
235
+ * @throws {DeviceIdentityError} If the thumbprint computation fails or the JWK is missing required fields
236
+ *
237
+ * @example
238
+ * ```typescript
239
+ * const keyPair = await generateDeviceKeyPair()
240
+ * const jwk = await exportPublicKeyJwk(keyPair)
241
+ * const thumbprint = await computePublicKeyThumbprint(jwk)
242
+ * // thumbprint is a base64url string, e.g., 'NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs'
243
+ * ```
244
+ */
245
+ declare function computePublicKeyThumbprint(publicKeyJwk: JsonWebKey): Promise<string>;
246
+
247
+ export { type AuthTokens as A, CryptoUnavailableError as C, DeviceIdentityError as D, type TokenPayload as T, type AuthConfig as a, type AuthEvent as b, type AuthEventType as c, type AuthStatus as d, type TokenType as e, computePublicKeyThumbprint as f, exportPublicKeyJwk as g, fromBase64Url as h, generateDeviceKeyPair as i, signChallenge as s, toBase64Url as t, verifyChallenge as v };