@darkauth/client 0.1.1

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/LICENSE ADDED
@@ -0,0 +1 @@
1
+ MIT
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # @DarkAuth/client
2
+
3
+ A TypeScript client library for DarkAuth - providing zero-knowledge authentication and client-side encryption capabilities for web applications.
4
+
5
+ ## Features
6
+
7
+ - **Zero-Knowledge Authentication**: Secure OAuth2/OIDC flow with PKCE and ephemeral key exchange
8
+ - **Client-Side Encryption**: Built-in cryptographic functions for data encryption/decryption
9
+ - **Token Management**: Automatic token storage, validation, and refresh
10
+ - **Data Encryption Keys (DEK)**: Support for deriving and managing data encryption keys
11
+ - **Session Persistence**: Secure session storage with key obfuscation
12
+ - **TypeScript Support**: Full TypeScript definitions included
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @DarkAuth/client
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### Basic Setup
23
+
24
+ ```typescript
25
+ import { setConfig, initiateLogin, handleCallback, getStoredSession } from '@DarkAuth/client';
26
+
27
+ // Configure the client
28
+ setConfig({
29
+ issuer: 'https://auth.example.com',
30
+ clientId: 'your-client-id',
31
+ redirectUri: 'https://app.example.com/callback',
32
+ zk: true // Enable zero-knowledge mode
33
+ });
34
+
35
+ // Start login flow
36
+ await initiateLogin();
37
+
38
+ // Handle OAuth callback (on your callback page)
39
+ const session = await handleCallback();
40
+ if (session) {
41
+ console.log('Logged in!', session.idToken);
42
+ }
43
+
44
+ // Get existing session
45
+ const existingSession = getStoredSession();
46
+ if (existingSession && isTokenValid(existingSession.idToken)) {
47
+ // User is authenticated
48
+ }
49
+ ```
50
+
51
+ ## API Reference
52
+
53
+ ### Configuration
54
+
55
+ #### `setConfig(config: Partial<Config>)`
56
+
57
+ Configure the DarkAuth client with your authentication settings.
58
+
59
+ ```typescript
60
+ setConfig({
61
+ issuer: 'https://auth.example.com', // DarkAuth server URL
62
+ clientId: 'your-client-id', // Your application's client ID
63
+ redirectUri: 'https://app.example.com/callback', // OAuth callback URL
64
+ zk: true // Enable zero-knowledge mode (default: true)
65
+ });
66
+ ```
67
+
68
+ The client also supports environment variables for configuration:
69
+ - `DARKAUTH_ISSUER` or `VITE_DARKAUTH_ISSUER`
70
+ - `DARKAUTH_CLIENT_ID` or `VITE_CLIENT_ID`
71
+ - `VITE_REDIRECT_URI`
72
+
73
+ ### Authentication Functions
74
+
75
+ #### `initiateLogin(): Promise<void>`
76
+
77
+ Starts the OAuth2/OIDC login flow with PKCE. Redirects the user to the DarkAuth authorization server.
78
+
79
+ #### `handleCallback(): Promise<AuthSession | null>`
80
+
81
+ Processes the OAuth callback after successful authentication. Returns an `AuthSession` object containing:
82
+ - `idToken`: JWT ID token
83
+ - `drk`: Derived Root Key for encryption operations
84
+ - `refreshToken?`: Optional refresh token
85
+
86
+ #### `logout(): void`
87
+
88
+ Clears all authentication data from storage.
89
+
90
+ #### `getStoredSession(): AuthSession | null`
91
+
92
+ Retrieves the current session from storage if valid.
93
+
94
+ #### `refreshSession(): Promise<AuthSession | null>`
95
+
96
+ Refreshes the current session using the stored refresh token.
97
+
98
+ ### User Information
99
+
100
+ #### `getCurrentUser(): JwtClaims | null`
101
+
102
+ Returns the parsed JWT claims from the current ID token.
103
+
104
+ #### `parseJwt(token: string): JwtClaims | null`
105
+
106
+ Parses a JWT token and returns its claims.
107
+
108
+ #### `isTokenValid(token: string): boolean`
109
+
110
+ Checks if a JWT token is still valid (not expired).
111
+
112
+ ### Cryptographic Functions
113
+
114
+ The library exports comprehensive cryptographic utilities from `./crypto`:
115
+
116
+ #### Encoding/Decoding
117
+ - `bytesToBase64Url(bytes: Uint8Array): string`
118
+ - `base64UrlToBytes(base64url: string): Uint8Array`
119
+ - `bytesToBase64(bytes: Uint8Array): string`
120
+ - `base64ToBytes(base64: string): Uint8Array`
121
+
122
+ #### Hashing
123
+ - `sha256(bytes: Uint8Array): Promise<Uint8Array>`
124
+
125
+ #### Key Derivation
126
+ - `hkdf(key: Uint8Array, salt: Uint8Array, info: Uint8Array, length?: number): Promise<Uint8Array>`
127
+ - `deriveDek(drk: Uint8Array, noteId: string): Promise<Uint8Array>`
128
+
129
+ #### Encryption/Decryption
130
+ - `aeadEncrypt(key: CryptoKey, plaintext: Uint8Array, additionalData: Uint8Array): Promise<{iv: Uint8Array, ciphertext: Uint8Array}>`
131
+ - `aeadDecrypt(key: CryptoKey, payload: Uint8Array, additionalData: Uint8Array): Promise<Uint8Array>`
132
+ - `encryptNote(drk: Uint8Array, noteId: string, content: string): Promise<string>`
133
+ - `decryptNote(drk: Uint8Array, noteId: string, ciphertextBase64: string, aadObject: Record<string, unknown>): Promise<string>`
134
+
135
+ #### Key Management
136
+ - `wrapPrivateKey(privateKeyJwk: JsonWebKey, drk: Uint8Array): Promise<string>`
137
+ - `unwrapPrivateKey(wrappedKey: string, drk: Uint8Array): Promise<JsonWebKey>`
138
+
139
+ ### Data Encryption Keys (DEK)
140
+
141
+ #### `resolveDek(noteId: string, isOwner: boolean, drk: Uint8Array): Promise<Uint8Array>`
142
+
143
+ Resolves a data encryption key for a specific resource. If the user is the owner, derives the DEK directly. Otherwise, fetches and decrypts the shared DEK.
144
+
145
+ #### `clearKeyCache(): void`
146
+
147
+ Clears the cached encryption keys.
148
+
149
+ ### Hooks System
150
+
151
+ #### `setHooks(hooks: ClientHooks)`
152
+
153
+ Configure hooks for custom data fetching:
154
+
155
+ ```typescript
156
+ setHooks({
157
+ fetchNoteDek: async (noteId: string) => {
158
+ // Fetch encrypted DEK for a shared note
159
+ const response = await fetch(`/api/notes/${noteId}/dek`);
160
+ return response.text();
161
+ },
162
+ fetchWrappedEncPrivateJwk: async () => {
163
+ // Fetch user's wrapped private key
164
+ const response = await fetch('/api/user/private-key');
165
+ return response.text();
166
+ }
167
+ });
168
+ ```
169
+
170
+ ## Types
171
+
172
+ ### `AuthSession`
173
+ ```typescript
174
+ interface AuthSession {
175
+ idToken: string;
176
+ drk: Uint8Array;
177
+ refreshToken?: string;
178
+ }
179
+ ```
180
+
181
+ ### `JwtClaims`
182
+ ```typescript
183
+ interface JwtClaims {
184
+ sub?: string;
185
+ email?: string;
186
+ name?: string;
187
+ exp?: number;
188
+ iat?: number;
189
+ iss?: string;
190
+ }
191
+ ```
192
+
193
+ ### `Config`
194
+ ```typescript
195
+ type Config = {
196
+ issuer: string;
197
+ clientId: string;
198
+ redirectUri: string;
199
+ zk?: boolean;
200
+ }
201
+ ```
202
+
203
+ ### `ClientHooks`
204
+ ```typescript
205
+ type ClientHooks = {
206
+ fetchNoteDek?: (noteId: string) => Promise<string>;
207
+ fetchWrappedEncPrivateJwk?: () => Promise<string>;
208
+ }
209
+ ```
210
+
211
+ ## Security Features
212
+
213
+ - **PKCE (Proof Key for Code Exchange)**: Protects against authorization code interception
214
+ - **Zero-Knowledge Mode**: Ephemeral key exchange for enhanced privacy
215
+ - **Key Obfuscation**: DRK is obfuscated in storage for additional protection
216
+ - **Secure Storage**: Uses sessionStorage for tokens and localStorage for persistent data
217
+ - **AEAD Encryption**: AES-GCM with additional authenticated data for all encryption operations
218
+
219
+ ## Browser Compatibility
220
+
221
+ This library requires a modern browser with support for:
222
+ - Web Crypto API
223
+ - ES2015+ features
224
+ - SessionStorage and LocalStorage
225
+
226
+ ## Development
227
+
228
+ ```bash
229
+ # Install dependencies
230
+ npm install
231
+
232
+ # Build the package
233
+ npm run build
234
+
235
+ # Type checking
236
+ npm run typecheck
237
+
238
+ # Linting and formatting
239
+ npm run lint
240
+ npm run format
241
+ ```
242
+
243
+ ## License
244
+
245
+ MIT
246
+
247
+ ## Contributing
248
+
249
+ Contributions are welcome! Please ensure all code passes linting and type checking before submitting a pull request.
@@ -0,0 +1,19 @@
1
+ export declare function bytesToBase64Url(bytes: Uint8Array): string;
2
+ export declare function base64UrlToBytes(base64url: string): Uint8Array;
3
+ export declare function bytesToBase64(bytes: Uint8Array): string;
4
+ export declare function base64ToBytes(base64: string): Uint8Array;
5
+ export declare function sha256(bytes: Uint8Array): Promise<Uint8Array>;
6
+ export declare function hkdf(key: Uint8Array, salt: Uint8Array, info: Uint8Array, length?: number): Promise<Uint8Array>;
7
+ export declare function aeadEncrypt(key: CryptoKey, plaintext: Uint8Array, additionalData: Uint8Array): Promise<{
8
+ iv: Uint8Array;
9
+ ciphertext: Uint8Array;
10
+ }>;
11
+ export declare function aeadDecrypt(key: CryptoKey, payload: Uint8Array, additionalData: Uint8Array): Promise<Uint8Array>;
12
+ export declare function aeadKey(bytes: Uint8Array): Promise<CryptoKey>;
13
+ export declare function deriveDek(drk: Uint8Array, noteId: string): Promise<Uint8Array>;
14
+ export declare function encryptNote(drk: Uint8Array, noteId: string, content: string): Promise<string>;
15
+ export declare function decryptNote(drk: Uint8Array, noteId: string, ciphertextBase64: string, aadObject: Record<string, unknown>): Promise<string>;
16
+ export declare function encryptNoteWithDek(dek: Uint8Array, noteId: string, content: string): Promise<string>;
17
+ export declare function decryptNoteWithDek(dek: Uint8Array, noteId: string, ciphertextBase64: string, aadObject: Record<string, unknown>): Promise<string>;
18
+ export declare function wrapPrivateKey(privateKeyJwk: JsonWebKey, drk: Uint8Array): Promise<string>;
19
+ export declare function unwrapPrivateKey(wrappedKey: string, drk: Uint8Array): Promise<JsonWebKey>;
package/dist/crypto.js ADDED
@@ -0,0 +1,109 @@
1
+ export function bytesToBase64Url(bytes) {
2
+ let binaryString = "";
3
+ for (const byte of bytes)
4
+ binaryString += String.fromCharCode(byte);
5
+ return btoa(binaryString).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6
+ }
7
+ export function base64UrlToBytes(base64url) {
8
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
9
+ const padding = base64.length % 4 === 2 ? "==" : base64.length % 4 === 3 ? "=" : "";
10
+ return Uint8Array.from(atob(base64 + padding), (c) => c.charCodeAt(0));
11
+ }
12
+ export function bytesToBase64(bytes) {
13
+ let binaryString = "";
14
+ for (const byte of bytes)
15
+ binaryString += String.fromCharCode(byte);
16
+ return btoa(binaryString);
17
+ }
18
+ export function base64ToBytes(base64) {
19
+ return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
20
+ }
21
+ export async function sha256(bytes) {
22
+ const buf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
23
+ const digest = await crypto.subtle.digest("SHA-256", buf);
24
+ return new Uint8Array(digest);
25
+ }
26
+ export async function hkdf(key, salt, info, length = 32) {
27
+ const toAB = (u) => u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
28
+ const importedKey = await crypto.subtle.importKey("raw", toAB(key), "HKDF", false, [
29
+ "deriveBits",
30
+ ]);
31
+ const bits = await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: toAB(salt), info: toAB(info) }, importedKey, length * 8);
32
+ return new Uint8Array(bits);
33
+ }
34
+ export async function aeadEncrypt(key, plaintext, additionalData) {
35
+ const iv = crypto.getRandomValues(new Uint8Array(12));
36
+ const toAB = (u) => u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
37
+ const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: toAB(additionalData) }, key, toAB(plaintext));
38
+ return { iv, ciphertext: new Uint8Array(ciphertext) };
39
+ }
40
+ export async function aeadDecrypt(key, payload, additionalData) {
41
+ const iv = payload.slice(0, 12);
42
+ const ciphertext = payload.slice(12);
43
+ const toAB = (u) => u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
44
+ const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv, additionalData: toAB(additionalData) }, key, toAB(ciphertext));
45
+ return new Uint8Array(plaintext);
46
+ }
47
+ export async function aeadKey(bytes) {
48
+ const toAB = (u) => u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
49
+ return crypto.subtle.importKey("raw", toAB(bytes), { name: "AES-GCM" }, false, [
50
+ "encrypt",
51
+ "decrypt",
52
+ ]);
53
+ }
54
+ export async function deriveDek(drk, noteId) {
55
+ const salt = new TextEncoder().encode("DarkAuth|demo-notes");
56
+ const info = new TextEncoder().encode(`note:${noteId}`);
57
+ return hkdf(drk, salt, info, 32);
58
+ }
59
+ export async function encryptNote(drk, noteId, content) {
60
+ const dek = await deriveDek(drk, noteId);
61
+ return encryptNoteWithDek(dek, noteId, content);
62
+ }
63
+ export async function decryptNote(drk, noteId, ciphertextBase64, aadObject) {
64
+ const dek = await deriveDek(drk, noteId);
65
+ return decryptNoteWithDek(dek, noteId, ciphertextBase64, aadObject);
66
+ }
67
+ export async function encryptNoteWithDek(dek, noteId, content) {
68
+ const key = await aeadKey(dek);
69
+ const plaintext = new TextEncoder().encode(content);
70
+ const aad = new TextEncoder().encode(JSON.stringify({ note_id: noteId }));
71
+ const { iv, ciphertext } = await aeadEncrypt(key, plaintext, aad);
72
+ const payload = new Uint8Array(iv.length + ciphertext.length);
73
+ payload.set(iv, 0);
74
+ payload.set(ciphertext, iv.length);
75
+ return bytesToBase64(payload);
76
+ }
77
+ export async function decryptNoteWithDek(dek, noteId, ciphertextBase64, aadObject) {
78
+ if (noteId === "") {
79
+ // touch param to satisfy TS noUnusedParameters
80
+ }
81
+ const key = await aeadKey(dek);
82
+ const aad = new TextEncoder().encode(JSON.stringify(aadObject));
83
+ const payload = base64ToBytes(ciphertextBase64);
84
+ const plaintext = await aeadDecrypt(key, payload, aad);
85
+ return new TextDecoder().decode(plaintext);
86
+ }
87
+ export async function wrapPrivateKey(privateKeyJwk, drk) {
88
+ const salt = new TextEncoder().encode("DarkAuth|user-keys");
89
+ const info = new TextEncoder().encode("private-key-wrap");
90
+ const wrapKey = await hkdf(drk, salt, info, 32);
91
+ const key = await aeadKey(wrapKey);
92
+ const plaintext = new TextEncoder().encode(JSON.stringify(privateKeyJwk));
93
+ const aad = new TextEncoder().encode("user-private-key");
94
+ const { iv, ciphertext } = await aeadEncrypt(key, plaintext, aad);
95
+ const payload = new Uint8Array(iv.length + ciphertext.length);
96
+ payload.set(iv, 0);
97
+ payload.set(ciphertext, iv.length);
98
+ return bytesToBase64Url(payload);
99
+ }
100
+ export async function unwrapPrivateKey(wrappedKey, drk) {
101
+ const salt = new TextEncoder().encode("DarkAuth|user-keys");
102
+ const info = new TextEncoder().encode("private-key-wrap");
103
+ const wrapKey = await hkdf(drk, salt, info, 32);
104
+ const key = await aeadKey(wrapKey);
105
+ const payload = base64UrlToBytes(wrappedKey);
106
+ const aad = new TextEncoder().encode("user-private-key");
107
+ const plaintext = await aeadDecrypt(key, payload, aad);
108
+ return JSON.parse(new TextDecoder().decode(plaintext));
109
+ }
package/dist/dek.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function resolveDek(noteId: string, isOwner: boolean, drk: Uint8Array): Promise<Uint8Array>;
2
+ export declare function clearKeyCache(): void;
package/dist/dek.js ADDED
@@ -0,0 +1,40 @@
1
+ import { compactDecrypt, importJWK } from "jose";
2
+ import { deriveDek, unwrapPrivateKey } from "./crypto";
3
+ import { getHooks } from "./hooks";
4
+ let cachedPrivKey = null;
5
+ let cachedPrivKeyPromise = null;
6
+ async function getUserEncPrivateKey(drk) {
7
+ if (cachedPrivKey)
8
+ return cachedPrivKey;
9
+ if (cachedPrivKeyPromise)
10
+ return cachedPrivKeyPromise;
11
+ const hooks = getHooks();
12
+ if (!hooks.fetchWrappedEncPrivateJwk)
13
+ throw new Error("fetchWrappedEncPrivateJwk hook not set");
14
+ cachedPrivKeyPromise = (async () => {
15
+ const fetchWrapped = hooks.fetchWrappedEncPrivateJwk;
16
+ if (!fetchWrapped)
17
+ throw new Error("fetchWrappedEncPrivateJwk hook not set");
18
+ const wrapped = await fetchWrapped();
19
+ const jwk = await unwrapPrivateKey(wrapped, drk);
20
+ const key = await importJWK(jwk, "ECDH-ES");
21
+ cachedPrivKey = key;
22
+ return cachedPrivKey;
23
+ })();
24
+ return cachedPrivKeyPromise;
25
+ }
26
+ export async function resolveDek(noteId, isOwner, drk) {
27
+ if (isOwner)
28
+ return deriveDek(drk, noteId);
29
+ const hooks = getHooks();
30
+ if (!hooks.fetchNoteDek)
31
+ throw new Error("fetchNoteDek hook not set");
32
+ const jwe = await hooks.fetchNoteDek(noteId);
33
+ const priv = await getUserEncPrivateKey(drk);
34
+ const { plaintext } = await compactDecrypt(jwe, priv);
35
+ return new Uint8Array(plaintext);
36
+ }
37
+ export function clearKeyCache() {
38
+ cachedPrivKey = null;
39
+ cachedPrivKeyPromise = null;
40
+ }
@@ -0,0 +1,6 @@
1
+ export type ClientHooks = {
2
+ fetchNoteDek?: (noteId: string) => Promise<string>;
3
+ fetchWrappedEncPrivateJwk?: () => Promise<string>;
4
+ };
5
+ export declare function setHooks(next: ClientHooks): void;
6
+ export declare function getHooks(): ClientHooks;
package/dist/hooks.js ADDED
@@ -0,0 +1,7 @@
1
+ let hooks = {};
2
+ export function setHooks(next) {
3
+ hooks = { ...hooks, ...next };
4
+ }
5
+ export function getHooks() {
6
+ return hooks;
7
+ }
@@ -0,0 +1,31 @@
1
+ export * from "./crypto";
2
+ export * from "./dek";
3
+ export { setHooks } from "./hooks";
4
+ type Config = {
5
+ issuer: string;
6
+ clientId: string;
7
+ redirectUri: string;
8
+ zk?: boolean;
9
+ };
10
+ export interface AuthSession {
11
+ idToken: string;
12
+ drk: Uint8Array;
13
+ refreshToken?: string;
14
+ }
15
+ export interface JwtClaims {
16
+ sub?: string;
17
+ email?: string;
18
+ name?: string;
19
+ exp?: number;
20
+ iat?: number;
21
+ iss?: string;
22
+ }
23
+ export declare function setConfig(next: Partial<Config>): void;
24
+ export declare function parseJwt(token: string): JwtClaims | null;
25
+ export declare function isTokenValid(token: string): boolean;
26
+ export declare function initiateLogin(): Promise<void>;
27
+ export declare function handleCallback(): Promise<AuthSession | null>;
28
+ export declare function getStoredSession(): AuthSession | null;
29
+ export declare function refreshSession(): Promise<AuthSession | null>;
30
+ export declare function logout(): void;
31
+ export declare function getCurrentUser(): JwtClaims | null;
package/dist/index.js ADDED
@@ -0,0 +1,233 @@
1
+ import { compactDecrypt } from "jose";
2
+ export * from "./crypto";
3
+ export * from "./dek";
4
+ export { setHooks } from "./hooks";
5
+ function viteEnvGet(key) {
6
+ try {
7
+ const im = import.meta || undefined;
8
+ return im?.env?.[key];
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ }
14
+ let cfg = {
15
+ issuer: (typeof window !== "undefined" && window.__APP_CONFIG__?.issuer) ||
16
+ viteEnvGet("VITE_DARKAUTH_ISSUER") ||
17
+ (typeof process !== "undefined" ? process.env.DARKAUTH_ISSUER : undefined) ||
18
+ "http://localhost:9080",
19
+ clientId: (typeof window !== "undefined" && window.__APP_CONFIG__?.clientId) ||
20
+ viteEnvGet("VITE_CLIENT_ID") ||
21
+ (typeof process !== "undefined" ? process.env.DARKAUTH_CLIENT_ID : undefined) ||
22
+ "app-web",
23
+ redirectUri: (typeof window !== "undefined" && window.__APP_CONFIG__?.redirectUri) ||
24
+ viteEnvGet("VITE_REDIRECT_URI") ||
25
+ (typeof window !== "undefined"
26
+ ? `${window.location.origin}/callback`
27
+ : "http://localhost:5173/callback"),
28
+ zk: true,
29
+ };
30
+ const OBFUSCATION_KEY = "DarkAuth-Storage-Protection-2025";
31
+ export function setConfig(next) {
32
+ cfg = { ...cfg, ...next };
33
+ }
34
+ function bytesToBase64Url(bytes) {
35
+ let s = "";
36
+ for (const b of bytes)
37
+ s += String.fromCharCode(b);
38
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
39
+ }
40
+ function base64UrlToBytes(base64url) {
41
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
42
+ const padding = base64.length % 4 === 2 ? "==" : base64.length % 4 === 3 ? "=" : "";
43
+ return Uint8Array.from(atob(base64 + padding), (c) => c.charCodeAt(0));
44
+ }
45
+ async function sha256(bytes) {
46
+ const buf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
47
+ const d = await crypto.subtle.digest("SHA-256", buf);
48
+ return new Uint8Array(d);
49
+ }
50
+ function parseFragmentParams(hash) {
51
+ const res = {};
52
+ const h = hash.startsWith("#") ? hash.slice(1) : hash;
53
+ for (const part of h.split("&")) {
54
+ const i = part.indexOf("=");
55
+ if (i === -1)
56
+ continue;
57
+ const k = decodeURIComponent(part.slice(0, i));
58
+ const v = decodeURIComponent(part.slice(i + 1));
59
+ res[k] = v;
60
+ }
61
+ return res;
62
+ }
63
+ function obfuscateKey(drk) {
64
+ const obfKey = new TextEncoder().encode(OBFUSCATION_KEY);
65
+ const out = new Uint8Array(drk.length);
66
+ for (let i = 0; i < drk.length; i++) {
67
+ const a = drk[i] ?? 0;
68
+ const b = obfKey[i % obfKey.length] ?? 0;
69
+ out[i] = a ^ b;
70
+ }
71
+ return out;
72
+ }
73
+ function deobfuscateKey(obfuscated) {
74
+ return obfuscateKey(obfuscated);
75
+ }
76
+ export function parseJwt(token) {
77
+ try {
78
+ const parts = token.split(".");
79
+ if (parts.length !== 3)
80
+ return null;
81
+ const mid = parts[1];
82
+ const payload = JSON.parse(atob(mid.replace(/-/g, "+").replace(/_/g, "/")));
83
+ return payload;
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ export function isTokenValid(token) {
90
+ const claims = parseJwt(token);
91
+ if (!claims?.exp)
92
+ return false;
93
+ return claims.exp * 1000 > Date.now() + 5000;
94
+ }
95
+ export async function initiateLogin() {
96
+ const keyPair = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, [
97
+ "deriveKey",
98
+ "deriveBits",
99
+ ]);
100
+ const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
101
+ const privateJwk = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
102
+ sessionStorage.setItem("zk_eph_priv_jwk", JSON.stringify(privateJwk));
103
+ const zkPubParam = bytesToBase64Url(new TextEncoder().encode(JSON.stringify(publicJwk)));
104
+ const state = crypto.randomUUID();
105
+ const verifier = bytesToBase64Url(crypto.getRandomValues(new Uint8Array(32)));
106
+ sessionStorage.setItem("pkce_verifier", verifier);
107
+ const challenge = bytesToBase64Url(await sha256(new TextEncoder().encode(verifier)));
108
+ const authUrl = new URL("/authorize", cfg.issuer);
109
+ authUrl.searchParams.set("client_id", cfg.clientId);
110
+ authUrl.searchParams.set("redirect_uri", cfg.redirectUri);
111
+ authUrl.searchParams.set("response_type", "code");
112
+ authUrl.searchParams.set("scope", "openid profile");
113
+ authUrl.searchParams.set("state", state);
114
+ authUrl.searchParams.set("code_challenge", challenge);
115
+ authUrl.searchParams.set("code_challenge_method", "S256");
116
+ if (cfg.zk !== false)
117
+ authUrl.searchParams.set("zk_pub", zkPubParam);
118
+ location.assign(authUrl.toString());
119
+ }
120
+ export async function handleCallback() {
121
+ if (!location.search.includes("code="))
122
+ return null;
123
+ const params = new URLSearchParams(location.search);
124
+ const code = params.get("code");
125
+ if (!code)
126
+ return null;
127
+ const tokenUrl = new URL("/token", cfg.issuer);
128
+ const verifier = sessionStorage.getItem("pkce_verifier") || "";
129
+ const response = await fetch(tokenUrl.toString(), {
130
+ method: "POST",
131
+ headers: { "content-type": "application/x-www-form-urlencoded" },
132
+ body: new URLSearchParams({
133
+ grant_type: "authorization_code",
134
+ code,
135
+ client_id: cfg.clientId,
136
+ redirect_uri: cfg.redirectUri,
137
+ code_verifier: verifier,
138
+ }),
139
+ });
140
+ if (!response.ok) {
141
+ throw new Error("Token exchange failed");
142
+ }
143
+ const tokenResponse = await response.json();
144
+ const fragmentParams = parseFragmentParams(location.hash || "");
145
+ const drkJwe = fragmentParams.drk_jwe;
146
+ if (!drkJwe || typeof drkJwe !== "string")
147
+ throw new Error("Missing DRK JWE from URL fragment");
148
+ if (tokenResponse.zk_drk_hash) {
149
+ const hash = bytesToBase64Url(await sha256(new TextEncoder().encode(drkJwe)));
150
+ if (tokenResponse.zk_drk_hash !== hash)
151
+ throw new Error("DRK hash mismatch");
152
+ }
153
+ const privateJwkString = sessionStorage.getItem("zk_eph_priv_jwk");
154
+ if (!privateJwkString)
155
+ return null;
156
+ sessionStorage.removeItem("zk_eph_priv_jwk");
157
+ const privateKey = await crypto.subtle.importKey("jwk", JSON.parse(privateJwkString), { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits", "deriveKey"]);
158
+ const { plaintext } = await compactDecrypt(drkJwe, privateKey);
159
+ const drk = new Uint8Array(plaintext);
160
+ const idToken = tokenResponse.id_token;
161
+ const refreshToken = tokenResponse.refresh_token;
162
+ try {
163
+ history.replaceState(null, "", location.origin + location.pathname);
164
+ }
165
+ catch { }
166
+ sessionStorage.setItem("id_token", idToken);
167
+ const obfuscatedDrk = obfuscateKey(drk);
168
+ localStorage.setItem("drk_protected", bytesToBase64Url(obfuscatedDrk));
169
+ if (refreshToken)
170
+ localStorage.setItem("refresh_token", refreshToken);
171
+ return { idToken, drk, refreshToken };
172
+ }
173
+ export function getStoredSession() {
174
+ const idToken = sessionStorage.getItem("id_token");
175
+ const obfuscatedDrkBase64 = localStorage.getItem("drk_protected");
176
+ if (!idToken || !obfuscatedDrkBase64)
177
+ return null;
178
+ if (!isTokenValid(idToken))
179
+ return null;
180
+ try {
181
+ const obfuscatedDrk = base64UrlToBytes(obfuscatedDrkBase64);
182
+ const drk = deobfuscateKey(obfuscatedDrk);
183
+ return { idToken, drk };
184
+ }
185
+ catch {
186
+ localStorage.removeItem("drk_protected");
187
+ return null;
188
+ }
189
+ }
190
+ export async function refreshSession() {
191
+ const refreshToken = localStorage.getItem("refresh_token");
192
+ if (!refreshToken)
193
+ return null;
194
+ const tokenUrl = new URL("/token", cfg.issuer);
195
+ const response = await fetch(tokenUrl.toString(), {
196
+ method: "POST",
197
+ headers: { "content-type": "application/x-www-form-urlencoded" },
198
+ body: new URLSearchParams({
199
+ grant_type: "refresh_token",
200
+ refresh_token: refreshToken,
201
+ client_id: cfg.clientId,
202
+ }),
203
+ });
204
+ if (!response.ok) {
205
+ localStorage.removeItem("refresh_token");
206
+ return null;
207
+ }
208
+ const tokenResponse = await response.json();
209
+ const idToken = tokenResponse.id_token;
210
+ const newRefreshToken = tokenResponse.refresh_token;
211
+ sessionStorage.setItem("id_token", idToken);
212
+ if (newRefreshToken)
213
+ localStorage.setItem("refresh_token", newRefreshToken);
214
+ const obfuscatedDrkBase64 = localStorage.getItem("drk_protected");
215
+ if (!obfuscatedDrkBase64)
216
+ return null;
217
+ const obfuscatedDrk = base64UrlToBytes(obfuscatedDrkBase64);
218
+ const drk = deobfuscateKey(obfuscatedDrk);
219
+ return { idToken, drk, refreshToken: newRefreshToken || refreshToken };
220
+ }
221
+ export function logout() {
222
+ sessionStorage.removeItem("id_token");
223
+ localStorage.removeItem("drk_protected");
224
+ sessionStorage.removeItem("zk_eph_priv_jwk");
225
+ sessionStorage.removeItem("pkce_verifier");
226
+ localStorage.removeItem("refresh_token");
227
+ }
228
+ export function getCurrentUser() {
229
+ const idToken = sessionStorage.getItem("id_token");
230
+ if (!idToken)
231
+ return null;
232
+ return parseJwt(idToken);
233
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@darkauth/client",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "LICENSE",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepack": "npm run build",
16
+ "typecheck": "tsc --noEmit",
17
+ "prepare": "tsc",
18
+ "format": "biome format --write .",
19
+ "lint": "biome lint .",
20
+ "check": "biome check .",
21
+ "check:fix": "biome check --write .",
22
+ "tidy": "biome check --write . && biome lint --write ."
23
+ },
24
+ "dependencies": {
25
+ "jose": "^6.1.3"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^5.9.3",
29
+ "@biomejs/biome": "^2.3.11"
30
+ }
31
+ }