@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.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # @korajs/auth
2
+
3
+ Offline-first authentication for Kora.js applications.
4
+
5
+ ## Overview
6
+
7
+ `@korajs/auth` provides a complete authentication system designed for offline-first applications. It includes:
8
+
9
+ - **Client-side auth management** — token storage, session restoration, sign-up/sign-in/sign-out
10
+ - **React hooks** — `useAuth()`, `useCurrentUser()`, `useAuthStatus()` for reactive auth state
11
+ - **Server-side auth routes** — email/password authentication with JWT tokens
12
+ - **Device identity** — ECDSA P-256 key pairs for proof-of-possession
13
+ - **Token management** — access/refresh token lifecycle with rotation
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pnpm add @korajs/auth
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### Client-side (React)
24
+
25
+ ```tsx
26
+ import { AuthClient } from '@korajs/auth'
27
+ import { AuthProvider, useAuth } from '@korajs/auth/react'
28
+
29
+ const authClient = new AuthClient({ serverUrl: 'http://localhost:3001' })
30
+
31
+ function App() {
32
+ return (
33
+ <AuthProvider client={authClient}>
34
+ <MyApp />
35
+ </AuthProvider>
36
+ )
37
+ }
38
+
39
+ function MyApp() {
40
+ const { user, isAuthenticated, isLoading, signIn, signOut, error } = useAuth()
41
+
42
+ if (isLoading) return <div>Loading...</div>
43
+
44
+ if (!isAuthenticated) {
45
+ return (
46
+ <button onClick={() => signIn({ email: 'user@example.com', password: 'password' })}>
47
+ Sign In
48
+ </button>
49
+ )
50
+ }
51
+
52
+ return (
53
+ <div>
54
+ <p>Welcome, {user?.name ?? user?.email}</p>
55
+ <button onClick={() => signOut()}>Sign Out</button>
56
+ </div>
57
+ )
58
+ }
59
+ ```
60
+
61
+ ### Server-side
62
+
63
+ ```typescript
64
+ import { BuiltInAuthRoutes, InMemoryUserStore, TokenManager } from '@korajs/auth/server'
65
+
66
+ const userStore = new InMemoryUserStore()
67
+ const tokenManager = new TokenManager({ secret: process.env.AUTH_SECRET! })
68
+ const authRoutes = new BuiltInAuthRoutes({ userStore, tokenManager })
69
+
70
+ // Wire into your HTTP server:
71
+ app.post('/auth/signup', async (req, res) => {
72
+ const result = await authRoutes.handleSignUp(req.body)
73
+ res.status(result.status).json(result.body)
74
+ })
75
+
76
+ app.post('/auth/signin', async (req, res) => {
77
+ const result = await authRoutes.handleSignIn(req.body)
78
+ res.status(result.status).json(result.body)
79
+ })
80
+
81
+ app.post('/auth/refresh', async (req, res) => {
82
+ const result = await authRoutes.handleRefresh(req.body)
83
+ res.status(result.status).json(result.body)
84
+ })
85
+
86
+ app.get('/auth/me', async (req, res) => {
87
+ const token = req.headers.authorization?.replace('Bearer ', '') ?? ''
88
+ const result = await authRoutes.handleGetMe(token)
89
+ res.status(result.status).json(result.body)
90
+ })
91
+
92
+ // Bridge to Kora sync server:
93
+ const syncServer = new KoraSyncServer({
94
+ store,
95
+ auth: authRoutes.toSyncAuthProvider(),
96
+ })
97
+ ```
98
+
99
+ ## Exports
100
+
101
+ ### `@korajs/auth` (client entry)
102
+
103
+ | Export | Description |
104
+ |--------|-------------|
105
+ | `AuthClient` | Client-side auth manager |
106
+ | `TokenStore` | Client-side token persistence |
107
+ | `generateDeviceKeyPair` | ECDSA P-256 key pair generation |
108
+ | `exportPublicKeyJwk` | Export public key as JWK |
109
+ | `signChallenge` | Sign challenge with device key |
110
+ | `verifyChallenge` | Verify challenge signature |
111
+ | `computePublicKeyThumbprint` | RFC 7638 JWK thumbprint |
112
+
113
+ ### `@korajs/auth/react`
114
+
115
+ | Export | Description |
116
+ |--------|-------------|
117
+ | `AuthProvider` | React context provider |
118
+ | `useAuth` | Full auth hook (user, methods, error) |
119
+ | `useCurrentUser` | Lightweight current user hook |
120
+ | `useAuthStatus` | Auth status for route guards |
121
+
122
+ ### `@korajs/auth/server`
123
+
124
+ | Export | Description |
125
+ |--------|-------------|
126
+ | `BuiltInAuthRoutes` | HTTP route handlers |
127
+ | `TokenManager` | JWT issuing/validation |
128
+ | `InMemoryUserStore` | Dev/test user store |
129
+ | `BuiltInProvider` | Adapter wrapping routes |
130
+ | `hashPassword` / `verifyPassword` | PBKDF2 password hashing |
131
+ | `encodeJwt` / `verifyJwt` | Low-level JWT operations |
132
+
133
+ ## Security
134
+
135
+ - Passwords hashed with PBKDF2-SHA512 (600,000 iterations, 32-byte salt)
136
+ - JWT tokens signed with HMAC-SHA256
137
+ - Constant-time signature comparison (prevents timing attacks)
138
+ - Device keys use ECDSA P-256 with non-extractable private keys
139
+ - Refresh token rotation on each use
140
+ - Access tokens expire in 15 minutes (configurable)
141
+
142
+ ## Architecture
143
+
144
+ ```
145
+ Client Server
146
+ ┌──────────────────┐ ┌──────────────────┐
147
+ │ AuthClient │ │ BuiltInAuthRoutes│
148
+ │ ├─ TokenStore │ ──── HTTP ────> │ ├─ UserStore │
149
+ │ └─ AuthState │ │ ├─ TokenManager │
150
+ │ │ │ └─ PasswordHash │
151
+ │ React Hooks │ │ │
152
+ │ ├─ useAuth │ │ SyncAuthProvider │
153
+ │ ├─ useCurrentUser │ └─ authenticate()│
154
+ │ └─ useAuthStatus └──────────────────┘
155
+ └──────────────────┘
156
+ ```
@@ -0,0 +1,181 @@
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
+ /**
66
+ * Creates a new AuthClient.
67
+ *
68
+ * @param config - Auth client configuration
69
+ */
70
+ constructor(config: AuthClientConfig);
71
+ /** Current authentication state. */
72
+ get state(): AuthState;
73
+ /** Current authenticated user, or null if not signed in. */
74
+ get currentUser(): AuthUser | null;
75
+ /** Whether the user is currently authenticated. */
76
+ get isAuthenticated(): boolean;
77
+ /**
78
+ * Initialize the auth client by restoring a session from stored tokens.
79
+ *
80
+ * Loads tokens from storage, validates the access token, and attempts a
81
+ * refresh if the access token is expired but a refresh token is available.
82
+ * Safe to call multiple times -- subsequent calls are no-ops once initialized.
83
+ */
84
+ initialize(): Promise<void>;
85
+ /**
86
+ * Register a new user account.
87
+ *
88
+ * @param params - Sign-up credentials
89
+ * @returns The newly created AuthUser
90
+ * @throws {AuthError} If the request fails or the server returns an error
91
+ */
92
+ signUp(params: {
93
+ email: string;
94
+ password: string;
95
+ name?: string;
96
+ }): Promise<AuthUser>;
97
+ /**
98
+ * Sign in with email and password.
99
+ *
100
+ * @param params - Sign-in credentials
101
+ * @returns The authenticated AuthUser
102
+ * @throws {AuthError} If the credentials are invalid or the request fails
103
+ */
104
+ signIn(params: {
105
+ email: string;
106
+ password: string;
107
+ }): Promise<AuthUser>;
108
+ /**
109
+ * Sign out the current user.
110
+ *
111
+ * Clears local tokens and state. Does not make a network request to the
112
+ * server -- tokens are simply discarded locally.
113
+ */
114
+ signOut(): Promise<void>;
115
+ /**
116
+ * Get a valid access token, automatically refreshing if expired.
117
+ *
118
+ * @returns A valid access token string, or null if the user is not
119
+ * authenticated and refresh is not possible
120
+ */
121
+ getAccessToken(): Promise<string | null>;
122
+ /**
123
+ * Get a valid token for the sync engine handshake.
124
+ * Alias for {@link getAccessToken}.
125
+ *
126
+ * @returns A valid access token string, or null if unavailable
127
+ */
128
+ getSyncToken(): Promise<string | null>;
129
+ /**
130
+ * Subscribe to authentication state changes.
131
+ *
132
+ * The callback is invoked whenever the auth state transitions (e.g., from
133
+ * 'unauthenticated' to 'authenticated' on sign-in).
134
+ *
135
+ * @param callback - Function called with the new AuthState on each change
136
+ * @returns An unsubscribe function that removes the listener
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * const unsub = auth.onAuthChange((state) => {
141
+ * console.log('Auth state changed to:', state)
142
+ * })
143
+ * // Later: unsub()
144
+ * ```
145
+ */
146
+ onAuthChange(callback: (state: AuthState) => void): () => void;
147
+ /**
148
+ * Update internal state and notify all listeners.
149
+ */
150
+ private setState;
151
+ /**
152
+ * Restore a session from a valid access token by fetching the user profile.
153
+ * Falls back to extracting the user ID from the token payload if the
154
+ * /auth/me request fails (offline scenario).
155
+ */
156
+ private restoreSession;
157
+ /**
158
+ * Fetch the current user profile from the server.
159
+ */
160
+ private fetchUserProfile;
161
+ /**
162
+ * Refresh the access token using a refresh token.
163
+ * De-duplicates concurrent refresh calls so only one network request is made.
164
+ */
165
+ private refreshAccessToken;
166
+ /**
167
+ * Execute the token refresh network request.
168
+ */
169
+ private performRefresh;
170
+ /**
171
+ * Make an HTTP request to the auth server.
172
+ *
173
+ * @param path - URL path relative to serverUrl (e.g. '/auth/signin')
174
+ * @param options - Request options
175
+ * @returns Parsed JSON response body
176
+ * @throws {AuthError} On network failure or non-2xx response
177
+ */
178
+ private request;
179
+ }
180
+
181
+ export { AuthClient as A, type AuthClientConfig as a, AuthError as b, type AuthState as c, type AuthUser as d };
@@ -0,0 +1,181 @@
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
+ /**
66
+ * Creates a new AuthClient.
67
+ *
68
+ * @param config - Auth client configuration
69
+ */
70
+ constructor(config: AuthClientConfig);
71
+ /** Current authentication state. */
72
+ get state(): AuthState;
73
+ /** Current authenticated user, or null if not signed in. */
74
+ get currentUser(): AuthUser | null;
75
+ /** Whether the user is currently authenticated. */
76
+ get isAuthenticated(): boolean;
77
+ /**
78
+ * Initialize the auth client by restoring a session from stored tokens.
79
+ *
80
+ * Loads tokens from storage, validates the access token, and attempts a
81
+ * refresh if the access token is expired but a refresh token is available.
82
+ * Safe to call multiple times -- subsequent calls are no-ops once initialized.
83
+ */
84
+ initialize(): Promise<void>;
85
+ /**
86
+ * Register a new user account.
87
+ *
88
+ * @param params - Sign-up credentials
89
+ * @returns The newly created AuthUser
90
+ * @throws {AuthError} If the request fails or the server returns an error
91
+ */
92
+ signUp(params: {
93
+ email: string;
94
+ password: string;
95
+ name?: string;
96
+ }): Promise<AuthUser>;
97
+ /**
98
+ * Sign in with email and password.
99
+ *
100
+ * @param params - Sign-in credentials
101
+ * @returns The authenticated AuthUser
102
+ * @throws {AuthError} If the credentials are invalid or the request fails
103
+ */
104
+ signIn(params: {
105
+ email: string;
106
+ password: string;
107
+ }): Promise<AuthUser>;
108
+ /**
109
+ * Sign out the current user.
110
+ *
111
+ * Clears local tokens and state. Does not make a network request to the
112
+ * server -- tokens are simply discarded locally.
113
+ */
114
+ signOut(): Promise<void>;
115
+ /**
116
+ * Get a valid access token, automatically refreshing if expired.
117
+ *
118
+ * @returns A valid access token string, or null if the user is not
119
+ * authenticated and refresh is not possible
120
+ */
121
+ getAccessToken(): Promise<string | null>;
122
+ /**
123
+ * Get a valid token for the sync engine handshake.
124
+ * Alias for {@link getAccessToken}.
125
+ *
126
+ * @returns A valid access token string, or null if unavailable
127
+ */
128
+ getSyncToken(): Promise<string | null>;
129
+ /**
130
+ * Subscribe to authentication state changes.
131
+ *
132
+ * The callback is invoked whenever the auth state transitions (e.g., from
133
+ * 'unauthenticated' to 'authenticated' on sign-in).
134
+ *
135
+ * @param callback - Function called with the new AuthState on each change
136
+ * @returns An unsubscribe function that removes the listener
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * const unsub = auth.onAuthChange((state) => {
141
+ * console.log('Auth state changed to:', state)
142
+ * })
143
+ * // Later: unsub()
144
+ * ```
145
+ */
146
+ onAuthChange(callback: (state: AuthState) => void): () => void;
147
+ /**
148
+ * Update internal state and notify all listeners.
149
+ */
150
+ private setState;
151
+ /**
152
+ * Restore a session from a valid access token by fetching the user profile.
153
+ * Falls back to extracting the user ID from the token payload if the
154
+ * /auth/me request fails (offline scenario).
155
+ */
156
+ private restoreSession;
157
+ /**
158
+ * Fetch the current user profile from the server.
159
+ */
160
+ private fetchUserProfile;
161
+ /**
162
+ * Refresh the access token using a refresh token.
163
+ * De-duplicates concurrent refresh calls so only one network request is made.
164
+ */
165
+ private refreshAccessToken;
166
+ /**
167
+ * Execute the token refresh network request.
168
+ */
169
+ private performRefresh;
170
+ /**
171
+ * Make an HTTP request to the auth server.
172
+ *
173
+ * @param path - URL path relative to serverUrl (e.g. '/auth/signin')
174
+ * @param options - Request options
175
+ * @returns Parsed JSON response body
176
+ * @throws {AuthError} On network failure or non-2xx response
177
+ */
178
+ private request;
179
+ }
180
+
181
+ export { AuthClient as A, type AuthClientConfig as a, AuthError as b, type AuthState as c, type AuthUser as d };
@@ -0,0 +1,174 @@
1
+ // src/device/device-identity.ts
2
+ import { KoraError } from "@korajs/core";
3
+ var CryptoUnavailableError = class extends KoraError {
4
+ constructor() {
5
+ super(
6
+ "Web Crypto API (crypto.subtle) is not available in this environment. Device identity requires crypto.subtle, which is available in modern browsers and Node.js 20+. If running in SSR, ensure your runtime provides the Web Crypto API.",
7
+ "CRYPTO_UNAVAILABLE"
8
+ );
9
+ this.name = "CryptoUnavailableError";
10
+ }
11
+ };
12
+ var DeviceIdentityError = class extends KoraError {
13
+ constructor(message, context) {
14
+ super(message, "DEVICE_IDENTITY_ERROR", context);
15
+ this.name = "DeviceIdentityError";
16
+ }
17
+ };
18
+ function toBase64Url(buffer) {
19
+ const bytes = new Uint8Array(buffer);
20
+ let binary = "";
21
+ for (let i = 0; i < bytes.length; i++) {
22
+ binary += String.fromCharCode(bytes[i]);
23
+ }
24
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
25
+ }
26
+ function fromBase64Url(str) {
27
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
28
+ const paddingNeeded = (4 - base64.length % 4) % 4;
29
+ base64 += "=".repeat(paddingNeeded);
30
+ const binary = atob(base64);
31
+ const bytes = new Uint8Array(binary.length);
32
+ for (let i = 0; i < binary.length; i++) {
33
+ bytes[i] = binary.charCodeAt(i);
34
+ }
35
+ return bytes;
36
+ }
37
+ function assertCryptoAvailable() {
38
+ if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
39
+ throw new CryptoUnavailableError();
40
+ }
41
+ }
42
+ var ECDSA_ALGORITHM = {
43
+ name: "ECDSA",
44
+ namedCurve: "P-256"
45
+ };
46
+ var ECDSA_SIGN_ALGORITHM = {
47
+ name: "ECDSA",
48
+ hash: { name: "SHA-256" }
49
+ };
50
+ async function generateDeviceKeyPair() {
51
+ assertCryptoAvailable();
52
+ try {
53
+ const keyPair = await globalThis.crypto.subtle.generateKey(
54
+ ECDSA_ALGORITHM,
55
+ // extractable: false makes the private key non-extractable.
56
+ // The public key is always extractable regardless of this flag.
57
+ false,
58
+ ["sign", "verify"]
59
+ );
60
+ return keyPair;
61
+ } catch (cause) {
62
+ throw new DeviceIdentityError(
63
+ "Failed to generate ECDSA P-256 device key pair. Ensure the runtime supports the ECDSA algorithm with the P-256 curve.",
64
+ { cause: cause instanceof Error ? cause.message : String(cause) }
65
+ );
66
+ }
67
+ }
68
+ async function exportPublicKeyJwk(keyPair) {
69
+ assertCryptoAvailable();
70
+ try {
71
+ const jwk = await globalThis.crypto.subtle.exportKey("jwk", keyPair.publicKey);
72
+ return jwk;
73
+ } catch (cause) {
74
+ throw new DeviceIdentityError(
75
+ "Failed to export public key as JWK. The key pair may be invalid or the public key may not support JWK export.",
76
+ { cause: cause instanceof Error ? cause.message : String(cause) }
77
+ );
78
+ }
79
+ }
80
+ async function signChallenge(privateKey, challenge) {
81
+ assertCryptoAvailable();
82
+ try {
83
+ const encoded = new TextEncoder().encode(challenge);
84
+ const signatureBuffer = await globalThis.crypto.subtle.sign(
85
+ ECDSA_SIGN_ALGORITHM,
86
+ privateKey,
87
+ encoded
88
+ );
89
+ return toBase64Url(signatureBuffer);
90
+ } catch (cause) {
91
+ throw new DeviceIdentityError(
92
+ 'Failed to sign challenge. Ensure the key is a valid ECDSA P-256 private key with "sign" usage.',
93
+ { cause: cause instanceof Error ? cause.message : String(cause) }
94
+ );
95
+ }
96
+ }
97
+ async function verifyChallenge(publicKeyJwk, challenge, signature) {
98
+ assertCryptoAvailable();
99
+ try {
100
+ const publicKey = await globalThis.crypto.subtle.importKey(
101
+ "jwk",
102
+ publicKeyJwk,
103
+ ECDSA_ALGORITHM,
104
+ true,
105
+ ["verify"]
106
+ );
107
+ const encoded = new TextEncoder().encode(challenge);
108
+ const signatureBytes = fromBase64Url(signature);
109
+ const isValid = await globalThis.crypto.subtle.verify(
110
+ ECDSA_SIGN_ALGORITHM,
111
+ publicKey,
112
+ signatureBytes,
113
+ encoded
114
+ );
115
+ return isValid;
116
+ } catch (cause) {
117
+ throw new DeviceIdentityError(
118
+ "Failed to verify challenge signature. The public key JWK or signature format may be invalid.",
119
+ {
120
+ cause: cause instanceof Error ? cause.message : String(cause),
121
+ publicKeyKty: publicKeyJwk.kty,
122
+ publicKeyCrv: publicKeyJwk.crv
123
+ }
124
+ );
125
+ }
126
+ }
127
+ async function computePublicKeyThumbprint(publicKeyJwk) {
128
+ assertCryptoAvailable();
129
+ if (publicKeyJwk.kty !== "EC") {
130
+ throw new DeviceIdentityError(
131
+ `Expected JWK key type "EC" but received "${publicKeyJwk.kty ?? "undefined"}". Only ECDSA public keys are supported for device identity.`,
132
+ { kty: publicKeyJwk.kty }
133
+ );
134
+ }
135
+ if (!publicKeyJwk.crv || !publicKeyJwk.x || !publicKeyJwk.y) {
136
+ throw new DeviceIdentityError(
137
+ 'JWK is missing required EC fields. An EC public key JWK must include "crv", "x", and "y" members.',
138
+ {
139
+ hasCrv: Boolean(publicKeyJwk.crv),
140
+ hasX: Boolean(publicKeyJwk.x),
141
+ hasY: Boolean(publicKeyJwk.y)
142
+ }
143
+ );
144
+ }
145
+ const canonicalJson = JSON.stringify({
146
+ crv: publicKeyJwk.crv,
147
+ kty: publicKeyJwk.kty,
148
+ x: publicKeyJwk.x,
149
+ y: publicKeyJwk.y
150
+ });
151
+ try {
152
+ const encoded = new TextEncoder().encode(canonicalJson);
153
+ const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", encoded);
154
+ return toBase64Url(hashBuffer);
155
+ } catch (cause) {
156
+ throw new DeviceIdentityError(
157
+ "Failed to compute SHA-256 thumbprint of the public key JWK.",
158
+ { cause: cause instanceof Error ? cause.message : String(cause) }
159
+ );
160
+ }
161
+ }
162
+
163
+ export {
164
+ CryptoUnavailableError,
165
+ DeviceIdentityError,
166
+ toBase64Url,
167
+ fromBase64Url,
168
+ generateDeviceKeyPair,
169
+ exportPublicKeyJwk,
170
+ signChallenge,
171
+ verifyChallenge,
172
+ computePublicKeyThumbprint
173
+ };
174
+ //# sourceMappingURL=chunk-L554ZDPY.js.map