@arbidocs/client 0.3.9

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/dist/index.js ADDED
@@ -0,0 +1,1153 @@
1
+ import createFetchClient from 'openapi-fetch';
2
+ import sodium from 'libsodium-wrappers-sumo';
3
+
4
+ // src/client.ts
5
+
6
+ // src/session.ts
7
+ function createInitialState() {
8
+ return {
9
+ accessToken: null,
10
+ userEmail: null,
11
+ userExtId: null,
12
+ selectedWorkspaceId: null,
13
+ cachedWorkspaceHeaders: {},
14
+ isSsoMode: false,
15
+ isAuth0Authenticated: false,
16
+ auth0AccessToken: null
17
+ };
18
+ }
19
+ function createSessionManager() {
20
+ let state = createInitialState();
21
+ const listeners = /* @__PURE__ */ new Set();
22
+ function notify() {
23
+ const snapshot = { ...state };
24
+ for (const listener of listeners) {
25
+ listener(snapshot);
26
+ }
27
+ }
28
+ return {
29
+ getState() {
30
+ return state;
31
+ },
32
+ setAccessToken(token) {
33
+ state = { ...state, accessToken: token };
34
+ notify();
35
+ },
36
+ setUser(email, extId) {
37
+ state = { ...state, userEmail: email, userExtId: extId ?? state.userExtId };
38
+ notify();
39
+ },
40
+ setSelectedWorkspace(id) {
41
+ state = { ...state, selectedWorkspaceId: id };
42
+ notify();
43
+ },
44
+ setCachedWorkspaceHeader(workspaceId, header) {
45
+ state = {
46
+ ...state,
47
+ cachedWorkspaceHeaders: {
48
+ ...state.cachedWorkspaceHeaders,
49
+ [workspaceId]: header
50
+ }
51
+ };
52
+ notify();
53
+ },
54
+ clearWorkspaceHeaders() {
55
+ state = { ...state, cachedWorkspaceHeaders: {} };
56
+ notify();
57
+ },
58
+ getWorkspaceKeyHeader() {
59
+ const { selectedWorkspaceId, cachedWorkspaceHeaders } = state;
60
+ if (!selectedWorkspaceId) return null;
61
+ return cachedWorkspaceHeaders[selectedWorkspaceId] ?? null;
62
+ },
63
+ setSsoState(opts) {
64
+ state = {
65
+ ...state,
66
+ isSsoMode: opts.isSsoMode ?? state.isSsoMode,
67
+ isAuth0Authenticated: opts.isAuth0Authenticated ?? state.isAuth0Authenticated,
68
+ auth0AccessToken: opts.auth0AccessToken !== void 0 ? opts.auth0AccessToken : state.auth0AccessToken
69
+ };
70
+ notify();
71
+ },
72
+ clear() {
73
+ state = createInitialState();
74
+ notify();
75
+ },
76
+ subscribe(listener) {
77
+ listeners.add(listener);
78
+ return () => {
79
+ listeners.delete(listener);
80
+ };
81
+ }
82
+ };
83
+ }
84
+ function createTokenProvider(session) {
85
+ return {
86
+ getAccessToken: () => session.getState().accessToken
87
+ };
88
+ }
89
+ function createWorkspaceKeyProvider(session) {
90
+ return {
91
+ getWorkspaceKeyHeader: () => session.getWorkspaceKeyHeader()
92
+ };
93
+ }
94
+ function createAuthStateProvider(session) {
95
+ return {
96
+ getUserEmail: () => session.getState().userEmail,
97
+ getSsoState: () => {
98
+ const s = session.getState();
99
+ return {
100
+ isSsoMode: s.isSsoMode,
101
+ isAuth0Authenticated: s.isAuth0Authenticated,
102
+ auth0AccessToken: s.auth0AccessToken
103
+ };
104
+ },
105
+ getSelectedWorkspaceId: () => session.getState().selectedWorkspaceId,
106
+ setAccessToken: (token) => session.setAccessToken(token),
107
+ clearWorkspaceHeaders: () => session.clearWorkspaceHeaders(),
108
+ setCachedWorkspaceHeader: (workspaceId, header) => session.setCachedWorkspaceHeader(workspaceId, header)
109
+ };
110
+ }
111
+ var sodiumReady = null;
112
+ async function initSodium() {
113
+ if (sodiumReady) {
114
+ await sodiumReady;
115
+ return;
116
+ }
117
+ sodiumReady = sodium.ready;
118
+ await sodiumReady;
119
+ }
120
+ function base64ToBytes(base64String) {
121
+ return sodium.from_base64(base64String, sodium.base64_variants.ORIGINAL);
122
+ }
123
+ function bytesToBase64(bytes) {
124
+ return sodium.to_base64(bytes, sodium.base64_variants.ORIGINAL);
125
+ }
126
+ function base64Encode(bytes) {
127
+ const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("");
128
+ return btoa(binString);
129
+ }
130
+ function base64Decode(base64) {
131
+ const binString = atob(base64);
132
+ return Uint8Array.from(binString, (m) => m.codePointAt(0) || 0);
133
+ }
134
+ function generateSalt(username, deploymentDomain) {
135
+ if (!deploymentDomain) {
136
+ throw new Error("Deployment domain must be provided for salt generation");
137
+ }
138
+ const normalized = username.toLowerCase();
139
+ const saltString = `${normalized}|${deploymentDomain}`;
140
+ const hash = sodium.crypto_hash_sha256(sodium.from_string(saltString));
141
+ return hash.slice(0, 16);
142
+ }
143
+ async function generateKeyPairs(username, password, deploymentDomain) {
144
+ await initSodium();
145
+ if (!deploymentDomain) {
146
+ throw new Error("Deployment domain not available. Cannot generate keys.");
147
+ }
148
+ const salt = generateSalt(username, deploymentDomain);
149
+ const seed = sodium.crypto_pwhash(
150
+ 32,
151
+ // 32 bytes for Ed25519 seed
152
+ password,
153
+ salt,
154
+ sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
155
+ sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
156
+ sodium.crypto_pwhash_ALG_ARGON2ID13
157
+ );
158
+ const signingKeyPair = sodium.crypto_sign_seed_keypair(seed);
159
+ const encryptionPublicKey = sodium.crypto_sign_ed25519_pk_to_curve25519(signingKeyPair.publicKey);
160
+ const encryptionPrivateKey = sodium.crypto_sign_ed25519_sk_to_curve25519(
161
+ signingKeyPair.privateKey
162
+ );
163
+ return {
164
+ signing: {
165
+ publicKey: signingKeyPair.publicKey,
166
+ privateKey: signingKeyPair.privateKey,
167
+ publicKeyBase64: sodium.to_base64(signingKeyPair.publicKey, sodium.base64_variants.ORIGINAL)
168
+ },
169
+ encryption: {
170
+ publicKey: encryptionPublicKey,
171
+ privateKey: encryptionPrivateKey,
172
+ publicKeyBase64: sodium.to_base64(encryptionPublicKey, sodium.base64_variants.ORIGINAL)
173
+ }
174
+ };
175
+ }
176
+ function signMessage(message, privateKey) {
177
+ const messageBytes = sodium.from_string(message);
178
+ const signature = sodium.crypto_sign_detached(messageBytes, privateKey);
179
+ return base64Encode(signature);
180
+ }
181
+ async function createWorkspaceKeyHeader(workspaceKey, serverSessionPublicKey) {
182
+ await initSodium();
183
+ const encryptedKey = sealedBoxEncrypt(workspaceKey, serverSessionPublicKey);
184
+ return encryptedKey;
185
+ }
186
+ function sealedBoxDecrypt(encryptedBase64, userEncryptionPrivateKey) {
187
+ const encrypted = base64ToBytes(encryptedBase64);
188
+ const publicKey = sodium.crypto_scalarmult_base(userEncryptionPrivateKey);
189
+ const decrypted = sodium.crypto_box_seal_open(encrypted, publicKey, userEncryptionPrivateKey);
190
+ return decrypted;
191
+ }
192
+ function sealedBoxEncrypt(message, publicKey) {
193
+ const encrypted = sodium.crypto_box_seal(message, publicKey);
194
+ return bytesToBase64(encrypted);
195
+ }
196
+ function derivePublicKey(privateKey) {
197
+ return sodium.crypto_scalarmult_base(privateKey);
198
+ }
199
+ function deriveEncryptionKeypairFromSigning(signingKeyPair) {
200
+ const encryptionPublicKey = sodium.crypto_sign_ed25519_pk_to_curve25519(signingKeyPair.publicKey);
201
+ const encryptionPrivateKey = sodium.crypto_sign_ed25519_sk_to_curve25519(signingKeyPair.secretKey);
202
+ return {
203
+ publicKey: encryptionPublicKey,
204
+ secretKey: encryptionPrivateKey
205
+ };
206
+ }
207
+ async function computeSharedSecret(theirPublicKeyBase64, myPrivateKey) {
208
+ await initSodium();
209
+ const theirPublicKey = base64ToBytes(theirPublicKeyBase64);
210
+ return sodium.crypto_box_beforenm(theirPublicKey, myPrivateKey);
211
+ }
212
+ async function encryptMessageWithSharedSecret(message, sharedSecret) {
213
+ await initSodium();
214
+ const messageBytes = sodium.from_string(message);
215
+ const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
216
+ const ciphertext = sodium.crypto_box_easy_afternm(messageBytes, nonce, sharedSecret);
217
+ const combined = new Uint8Array(nonce.length + ciphertext.length);
218
+ combined.set(nonce, 0);
219
+ combined.set(ciphertext, nonce.length);
220
+ return bytesToBase64(combined);
221
+ }
222
+ async function decryptMessageWithSharedSecret(encryptedBase64, sharedSecret) {
223
+ await initSodium();
224
+ const combined = base64ToBytes(encryptedBase64);
225
+ const nonce = combined.slice(0, sodium.crypto_box_NONCEBYTES);
226
+ const ciphertext = combined.slice(sodium.crypto_box_NONCEBYTES);
227
+ const decrypted = sodium.crypto_box_open_easy_afternm(ciphertext, nonce, sharedSecret);
228
+ return sodium.to_string(decrypted);
229
+ }
230
+ async function encryptMessage(message, recipientPublicKeyBase64, senderPrivateKey) {
231
+ await initSodium();
232
+ const recipientPublicKey = base64ToBytes(recipientPublicKeyBase64);
233
+ const messageBytes = sodium.from_string(message);
234
+ const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
235
+ const ciphertext = sodium.crypto_box_easy(
236
+ messageBytes,
237
+ nonce,
238
+ recipientPublicKey,
239
+ senderPrivateKey
240
+ );
241
+ const combined = new Uint8Array(nonce.length + ciphertext.length);
242
+ combined.set(nonce, 0);
243
+ combined.set(ciphertext, nonce.length);
244
+ return bytesToBase64(combined);
245
+ }
246
+ async function decryptMessage(encryptedBase64, senderPublicKeyBase64, recipientPrivateKey) {
247
+ await initSodium();
248
+ const combined = base64ToBytes(encryptedBase64);
249
+ const senderPublicKey = base64ToBytes(senderPublicKeyBase64);
250
+ const nonce = combined.slice(0, sodium.crypto_box_NONCEBYTES);
251
+ const ciphertext = combined.slice(sodium.crypto_box_NONCEBYTES);
252
+ const decrypted = sodium.crypto_box_open_easy(
253
+ ciphertext,
254
+ nonce,
255
+ senderPublicKey,
256
+ recipientPrivateKey
257
+ );
258
+ return sodium.to_string(decrypted);
259
+ }
260
+
261
+ // src/crypto/keypairs.ts
262
+ async function generateUserKeypairs(username, password, deploymentDomain) {
263
+ if (!deploymentDomain) {
264
+ throw new Error("Deployment domain must be provided. Cannot generate keypairs.");
265
+ }
266
+ const keypairs = await generateKeyPairs(username, password, deploymentDomain);
267
+ const signingKeyPair = {
268
+ publicKey: keypairs.signing.publicKey,
269
+ secretKey: keypairs.signing.privateKey
270
+ };
271
+ const encryptionKeyPair = {
272
+ publicKey: keypairs.encryption.publicKey,
273
+ secretKey: keypairs.encryption.privateKey
274
+ };
275
+ return {
276
+ signingKeyPair,
277
+ encryptionKeyPair
278
+ };
279
+ }
280
+
281
+ // src/crypto/credentials.ts
282
+ async function generateRegistrationCredentials(registrationData, password, deploymentDomain) {
283
+ const { signingKeyPair } = await generateUserKeypairs(
284
+ registrationData.email,
285
+ password,
286
+ deploymentDomain
287
+ );
288
+ const request = {
289
+ ...registrationData,
290
+ signing_key: base64Encode(signingKeyPair.publicKey)
291
+ };
292
+ return { request, signingPrivateKey: signingKeyPair.secretKey };
293
+ }
294
+ async function generateLoginCredentials(loginData, password, deploymentDomain) {
295
+ const { signingKeyPair } = await generateUserKeypairs(loginData.email, password, deploymentDomain);
296
+ const timestamp = Math.floor(Date.now() / 1e3);
297
+ const message = `${loginData.email}|${timestamp}`;
298
+ const signature = signMessage(message, signingKeyPair.secretKey);
299
+ const request = {
300
+ ...loginData,
301
+ signature,
302
+ timestamp
303
+ };
304
+ return { request, signingPrivateKey: signingKeyPair.secretKey };
305
+ }
306
+ function generateLoginCredentialsFromKey(loginData, signingPrivateKey) {
307
+ if (signingPrivateKey.length !== 64) {
308
+ throw new Error("Invalid signing key: must be exactly 64 bytes");
309
+ }
310
+ const timestamp = Math.floor(Date.now() / 1e3);
311
+ const message = `${loginData.email}|${timestamp}`;
312
+ const signature = signMessage(message, signingPrivateKey);
313
+ const request = {
314
+ ...loginData,
315
+ signature,
316
+ timestamp
317
+ };
318
+ return { request, signingPrivateKey };
319
+ }
320
+ async function generatePasswordChangeCredentials(email, currentPassword, newPassword, deploymentDomain) {
321
+ const currentKeypairs = await generateUserKeypairs(email, currentPassword, deploymentDomain);
322
+ const newKeypairs = await generateUserKeypairs(email, newPassword, deploymentDomain);
323
+ const timestamp = Math.floor(Date.now() / 1e3);
324
+ const message = `${email}|${timestamp}`;
325
+ const signature = signMessage(message, currentKeypairs.signingKeyPair.secretKey);
326
+ const request = {
327
+ signature,
328
+ timestamp,
329
+ new_signing_key: base64Encode(newKeypairs.signingKeyPair.publicKey)
330
+ };
331
+ return { request, newSigningPrivateKey: newKeypairs.signingKeyPair.secretKey };
332
+ }
333
+ async function generateRecoveryPasswordChangeCredentials(email, currentSigningKey, newPassword, deploymentDomain) {
334
+ const newKeypairs = await generateUserKeypairs(email, newPassword, deploymentDomain);
335
+ const timestamp = Math.floor(Date.now() / 1e3);
336
+ const message = `${email}|${timestamp}`;
337
+ const signature = signMessage(message, currentSigningKey);
338
+ const request = {
339
+ signature,
340
+ timestamp,
341
+ new_signing_key: base64Encode(newKeypairs.signingKeyPair.publicKey)
342
+ };
343
+ return { request, newSigningPrivateKey: newKeypairs.signingKeyPair.secretKey };
344
+ }
345
+
346
+ // src/middleware/bearer-auth.ts
347
+ function createBearerAuthMiddleware(config) {
348
+ return {
349
+ async onRequest({ request }) {
350
+ const accessToken = config.tokenProvider.getAccessToken();
351
+ if (accessToken) {
352
+ request.headers.set("Authorization", `Bearer ${accessToken}`);
353
+ }
354
+ return request;
355
+ }
356
+ };
357
+ }
358
+
359
+ // src/middleware/workspace-key.ts
360
+ function needsWorkspaceKey(url, urlConfig) {
361
+ if (urlConfig.excludePatterns.some((pattern) => url.includes(pattern))) {
362
+ return false;
363
+ }
364
+ return urlConfig.includePatterns.some((pattern) => url.includes(pattern));
365
+ }
366
+ function createWorkspaceKeyMiddleware(config) {
367
+ return {
368
+ async onRequest({ request }) {
369
+ if (needsWorkspaceKey(request.url, config.urlConfig)) {
370
+ const workspaceHeader = config.workspaceKeyProvider.getWorkspaceKeyHeader();
371
+ if (workspaceHeader) {
372
+ request.headers.set("Workspace-Key", workspaceHeader);
373
+ }
374
+ }
375
+ return request;
376
+ }
377
+ };
378
+ }
379
+
380
+ // src/middleware/auto-relogin.ts
381
+ function createAutoReloginMiddleware(config) {
382
+ return {
383
+ async onResponse({ response, request }) {
384
+ if (response.status === 400) {
385
+ const isSafeMethod = ["GET", "HEAD", "DELETE"].includes(request.method);
386
+ if (isSafeMethod) {
387
+ console.info("[API] 400 error on protected endpoint - triggering instant re-login");
388
+ const newToken = await config.reloginHandler();
389
+ if (newToken) {
390
+ const newRequest = request.clone();
391
+ newRequest.headers.set("Authorization", `Bearer ${newToken}`);
392
+ console.info("[API] Retrying request after re-login");
393
+ return fetch(newRequest);
394
+ }
395
+ }
396
+ }
397
+ if (response.status === 401) {
398
+ const excludePatterns = config.reloginExcludePatterns ?? [];
399
+ const isExcluded = excludePatterns.some((pattern) => request.url.includes(pattern));
400
+ if (!isExcluded) {
401
+ const newToken = await config.reloginHandler();
402
+ if (newToken) {
403
+ try {
404
+ const newRequest = request.clone();
405
+ newRequest.headers.set("Authorization", `Bearer ${newToken}`);
406
+ console.info("[API] Retrying request after re-login:", request.method, request.url);
407
+ return fetch(newRequest);
408
+ } catch (cloneError) {
409
+ console.warn("[API] Could not clone request for retry:", cloneError);
410
+ config.onRetryCloneFailed?.();
411
+ }
412
+ }
413
+ }
414
+ }
415
+ return response;
416
+ }
417
+ };
418
+ }
419
+
420
+ // src/relogin/handler.ts
421
+ var RELOGIN_COOLDOWN_MS = 5e3;
422
+ function createReloginHandler(deps) {
423
+ let reloginPromise = null;
424
+ let lastSuccessToken = null;
425
+ let lastSuccessTime = 0;
426
+ return function instantReLogin() {
427
+ if (reloginPromise) {
428
+ console.info("[API] Re-login already in progress, waiting...");
429
+ return reloginPromise;
430
+ }
431
+ const elapsed = Date.now() - lastSuccessTime;
432
+ if (lastSuccessToken && elapsed < RELOGIN_COOLDOWN_MS) {
433
+ console.info(`[API] Re-login skipped (cooldown, ${elapsed}ms since last success)`);
434
+ return Promise.resolve(lastSuccessToken);
435
+ }
436
+ reloginPromise = (async () => {
437
+ try {
438
+ await deps.crypto.ensureReady();
439
+ const userEmail = deps.authState.getUserEmail();
440
+ if (!userEmail) {
441
+ console.warn("[API] No user email found for instant re-login");
442
+ return null;
443
+ }
444
+ const session = await deps.sessionStorage.getSession();
445
+ if (!session) {
446
+ console.warn("[API] No session found for instant re-login");
447
+ return null;
448
+ }
449
+ const ed25519PublicKey = session.signingPrivateKey.slice(32, 64);
450
+ const encryptionKeyPair = deps.crypto.deriveEncryptionKeypair({
451
+ publicKey: ed25519PublicKey,
452
+ secretKey: session.signingPrivateKey
453
+ });
454
+ const timestamp = Math.floor(Date.now() / 1e3);
455
+ const message = `${userEmail}|${timestamp}`;
456
+ const signature = deps.crypto.signMessage(message, session.signingPrivateKey);
457
+ console.info("[API] Attempting instant re-login");
458
+ const ssoState = deps.authState.getSsoState();
459
+ console.info("[API] Re-login SSO mode:", ssoState.isSsoMode);
460
+ console.info("[API] Auth0 authenticated:", ssoState.isAuth0Authenticated);
461
+ console.info("[API] Persisted Auth0 token available:", !!ssoState.auth0AccessToken);
462
+ const isSsoAccount = ssoState.isSsoMode || ssoState.isAuth0Authenticated || !!ssoState.auth0AccessToken;
463
+ let ssoToken = null;
464
+ if (deps.ssoTokenProvider) {
465
+ console.info("[API] SSO token provider available: true");
466
+ try {
467
+ ssoToken = await deps.ssoTokenProvider.getToken();
468
+ console.info(
469
+ "[API] Got SSO token from provider:",
470
+ ssoToken ? "yes (length: " + ssoToken.length + ")" : "no"
471
+ );
472
+ } catch (error) {
473
+ console.error("[API] Failed to get SSO token from provider:", error);
474
+ }
475
+ } else {
476
+ console.info("[API] SSO token provider available: false");
477
+ }
478
+ if (!ssoToken && ssoState.auth0AccessToken) {
479
+ ssoToken = ssoState.auth0AccessToken;
480
+ console.info(
481
+ "[API] Using persisted SSO token (length:",
482
+ ssoToken.length,
483
+ ") - may be expired"
484
+ );
485
+ }
486
+ if (!ssoToken && isSsoAccount) {
487
+ console.warn("[API] SSO account requires token but none available - aborting re-login");
488
+ return null;
489
+ }
490
+ const loginResult = await deps.loginProvider.login({
491
+ email: userEmail,
492
+ signature,
493
+ timestamp,
494
+ ssoToken: ssoToken ?? void 0
495
+ });
496
+ if (!loginResult) {
497
+ console.warn("[API] Instant re-login failed");
498
+ return null;
499
+ }
500
+ await deps.sessionStorage.saveSession({
501
+ signingPrivateKey: session.signingPrivateKey,
502
+ serverSessionKey: loginResult.sessionKey,
503
+ userExtId: loginResult.userExtId
504
+ });
505
+ await deps.onReloginSuccess?.({
506
+ email: userEmail,
507
+ accessToken: loginResult.accessToken,
508
+ userExtId: loginResult.userExtId,
509
+ serverSessionKey: loginResult.sessionKey
510
+ });
511
+ let activeToken = loginResult.accessToken;
512
+ deps.authState.setAccessToken(activeToken);
513
+ deps.authState.clearWorkspaceHeaders();
514
+ const selectedWorkspaceId = deps.authState.getSelectedWorkspaceId();
515
+ if (selectedWorkspaceId) {
516
+ try {
517
+ const wrappedKey = await deps.workspaceKeyRefreshProvider.getWrappedKey(
518
+ loginResult.accessToken,
519
+ selectedWorkspaceId
520
+ );
521
+ if (wrappedKey) {
522
+ const workspaceKey = deps.crypto.sealedBoxDecrypt(
523
+ wrappedKey,
524
+ encryptionKeyPair.secretKey
525
+ );
526
+ const encryptedWorkspaceKey = await deps.crypto.createWorkspaceKeyHeader(
527
+ workspaceKey,
528
+ loginResult.sessionKey
529
+ );
530
+ deps.authState.setCachedWorkspaceHeader(selectedWorkspaceId, encryptedWorkspaceKey);
531
+ if (deps.workspaceOpenProvider) {
532
+ const workspaceJwt = await deps.workspaceOpenProvider.openWorkspace(
533
+ loginResult.accessToken,
534
+ selectedWorkspaceId,
535
+ encryptedWorkspaceKey
536
+ );
537
+ if (workspaceJwt) {
538
+ activeToken = workspaceJwt;
539
+ deps.authState.setAccessToken(activeToken);
540
+ console.info("[API] Workspace-scoped JWT obtained after re-login");
541
+ }
542
+ }
543
+ }
544
+ } catch (error) {
545
+ console.error("[API] Failed to re-open workspace after re-login:", error);
546
+ }
547
+ }
548
+ console.info("[API] Re-login successful");
549
+ lastSuccessToken = activeToken;
550
+ lastSuccessTime = Date.now();
551
+ return activeToken;
552
+ } catch (error) {
553
+ console.error("[API] Instant re-login error:", error);
554
+ lastSuccessToken = null;
555
+ lastSuccessTime = 0;
556
+ return null;
557
+ } finally {
558
+ reloginPromise = null;
559
+ }
560
+ })();
561
+ return reloginPromise;
562
+ };
563
+ }
564
+
565
+ // src/storage/indexed-db.ts
566
+ var DB_NAME = "arbi-crypto";
567
+ var DB_VERSION = 2;
568
+ var db = null;
569
+ async function initializeDatabase() {
570
+ return new Promise((resolve, reject) => {
571
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
572
+ request.onerror = () => {
573
+ console.error("[STORAGE] Failed to open IndexedDB:", request.error);
574
+ reject(request.error);
575
+ };
576
+ request.onsuccess = () => {
577
+ db = request.result;
578
+ resolve(db);
579
+ };
580
+ request.onupgradeneeded = (event) => {
581
+ const database = event.target.result;
582
+ if (!database.objectStoreNames.contains("session")) {
583
+ database.createObjectStore("session", { keyPath: "id" });
584
+ }
585
+ if (!database.objectStoreNames.contains("wrapping-key")) {
586
+ database.createObjectStore("wrapping-key", { keyPath: "id" });
587
+ }
588
+ };
589
+ });
590
+ }
591
+ async function ensureDatabase() {
592
+ if (!db) {
593
+ await initializeDatabase();
594
+ }
595
+ }
596
+ async function getOrCreateWrappingKey() {
597
+ await ensureDatabase();
598
+ if (!db) {
599
+ throw new Error("Database not initialized");
600
+ }
601
+ return new Promise((resolve, reject) => {
602
+ if (!db) {
603
+ reject(new Error("Database not initialized"));
604
+ return;
605
+ }
606
+ const transaction = db.transaction(["wrapping-key"], "readwrite");
607
+ const store = transaction.objectStore("wrapping-key");
608
+ const request = store.get("master");
609
+ request.onsuccess = () => {
610
+ const result = request.result;
611
+ if (result?.key) {
612
+ resolve(result.key);
613
+ } else {
614
+ crypto.subtle.generateKey(
615
+ {
616
+ name: "AES-GCM",
617
+ length: 256
618
+ },
619
+ false,
620
+ // ❌ NON-EXTRACTABLE - Can't be exported!
621
+ ["encrypt", "decrypt"]
622
+ ).then((key) => {
623
+ if (!db) {
624
+ reject(new Error("Database not initialized"));
625
+ return;
626
+ }
627
+ const putTransaction = db.transaction(["wrapping-key"], "readwrite");
628
+ const putStore = putTransaction.objectStore("wrapping-key");
629
+ const putRequest = putStore.put({
630
+ id: "master",
631
+ key,
632
+ created: Date.now()
633
+ });
634
+ putRequest.onsuccess = () => {
635
+ resolve(key);
636
+ };
637
+ putRequest.onerror = () => {
638
+ reject(new Error("Failed to store wrapping key"));
639
+ };
640
+ }).catch((error) => {
641
+ const errorMsg = error instanceof Error ? error.message : String(error);
642
+ reject(new Error(`Failed to generate wrapping key: ${errorMsg}`));
643
+ });
644
+ }
645
+ };
646
+ request.onerror = () => {
647
+ reject(new Error("Failed to retrieve wrapping key from IndexedDB"));
648
+ };
649
+ });
650
+ }
651
+ async function encryptWithWrappingKey(data) {
652
+ const wrappingKey = await getOrCreateWrappingKey();
653
+ const iv = crypto.getRandomValues(new Uint8Array(12));
654
+ const ciphertext = await crypto.subtle.encrypt(
655
+ {
656
+ name: "AES-GCM",
657
+ iv
658
+ },
659
+ wrappingKey,
660
+ data
661
+ );
662
+ return {
663
+ ciphertext: new Uint8Array(ciphertext),
664
+ iv
665
+ };
666
+ }
667
+ async function decryptWithWrappingKey(ciphertext, iv) {
668
+ const wrappingKey = await getOrCreateWrappingKey();
669
+ const decrypted = await crypto.subtle.decrypt(
670
+ {
671
+ name: "AES-GCM",
672
+ iv
673
+ },
674
+ wrappingKey,
675
+ ciphertext
676
+ );
677
+ return new Uint8Array(decrypted);
678
+ }
679
+ async function saveSession(sessionData) {
680
+ await ensureDatabase();
681
+ if (!db) {
682
+ throw new Error("Database not initialized");
683
+ }
684
+ try {
685
+ const encryptedSigningKey = await encryptWithWrappingKey(sessionData.signingPrivateKey);
686
+ let encryptedServerSessionKey;
687
+ if (sessionData.serverSessionKey) {
688
+ encryptedServerSessionKey = await encryptWithWrappingKey(sessionData.serverSessionKey);
689
+ }
690
+ return new Promise((resolve, reject) => {
691
+ if (!db) {
692
+ reject(new Error("Database not initialized"));
693
+ return;
694
+ }
695
+ const transaction = db.transaction(["session"], "readwrite");
696
+ const store = transaction.objectStore("session");
697
+ const data = {
698
+ id: "current",
699
+ encryptedSigningPrivateKey: encryptedSigningKey.ciphertext,
700
+ signingPrivateKeyIV: encryptedSigningKey.iv,
701
+ encryptedServerSessionKey: encryptedServerSessionKey?.ciphertext,
702
+ serverSessionKeyIV: encryptedServerSessionKey?.iv,
703
+ userExtId: sessionData.userExtId,
704
+ timestamp: Date.now()
705
+ };
706
+ const request = store.put(data);
707
+ request.onsuccess = () => {
708
+ resolve();
709
+ };
710
+ request.onerror = () => {
711
+ console.error("[STORAGE] Failed to save session:", request.error);
712
+ reject(request.error);
713
+ };
714
+ });
715
+ } catch (error) {
716
+ console.error("[STORAGE] Failed to encrypt session data:", error);
717
+ throw error;
718
+ }
719
+ }
720
+ async function getSession() {
721
+ await ensureDatabase();
722
+ if (!db) {
723
+ throw new Error("Database not initialized");
724
+ }
725
+ return new Promise((resolve, reject) => {
726
+ if (!db) {
727
+ reject(new Error("Database not initialized"));
728
+ return;
729
+ }
730
+ const transaction = db.transaction(["session"], "readonly");
731
+ const store = transaction.objectStore("session");
732
+ const request = store.get("current");
733
+ request.onsuccess = async () => {
734
+ const result = request.result;
735
+ if (result) {
736
+ try {
737
+ const signingPrivateKey = await decryptWithWrappingKey(
738
+ result.encryptedSigningPrivateKey,
739
+ result.signingPrivateKeyIV
740
+ );
741
+ let serverSessionKey;
742
+ if (result.encryptedServerSessionKey && result.serverSessionKeyIV) {
743
+ serverSessionKey = await decryptWithWrappingKey(
744
+ result.encryptedServerSessionKey,
745
+ result.serverSessionKeyIV
746
+ );
747
+ }
748
+ resolve({
749
+ signingPrivateKey,
750
+ serverSessionKey,
751
+ userExtId: result.userExtId
752
+ });
753
+ } catch (error) {
754
+ console.error("[STORAGE] Failed to decrypt session data:", error);
755
+ reject(error);
756
+ }
757
+ } else {
758
+ resolve(null);
759
+ }
760
+ };
761
+ request.onerror = () => {
762
+ console.error("[STORAGE] Failed to get session:", request.error);
763
+ reject(request.error);
764
+ };
765
+ });
766
+ }
767
+ async function updateSigningPrivateKey(newSigningPrivateKey) {
768
+ await ensureDatabase();
769
+ try {
770
+ const existingSession = await getSession();
771
+ if (!existingSession) {
772
+ throw new Error("No existing session to update");
773
+ }
774
+ await saveSession({
775
+ signingPrivateKey: newSigningPrivateKey,
776
+ serverSessionKey: existingSession.serverSessionKey,
777
+ userExtId: existingSession.userExtId
778
+ });
779
+ } catch (error) {
780
+ console.error("[STORAGE] Failed to update signing private key:", error);
781
+ throw error;
782
+ }
783
+ }
784
+ async function clearSession() {
785
+ await ensureDatabase();
786
+ if (!db) {
787
+ throw new Error("Database not initialized");
788
+ }
789
+ return new Promise((resolve, reject) => {
790
+ if (!db) {
791
+ reject(new Error("Database not initialized"));
792
+ return;
793
+ }
794
+ const transaction = db.transaction(["session"], "readwrite");
795
+ const store = transaction.objectStore("session");
796
+ const request = store.delete("current");
797
+ request.onsuccess = () => {
798
+ resolve();
799
+ };
800
+ request.onerror = () => {
801
+ console.error("[STORAGE] Failed to clear session:", request.error);
802
+ reject(request.error);
803
+ };
804
+ });
805
+ }
806
+ async function clearWrappingKey() {
807
+ await ensureDatabase();
808
+ if (!db) {
809
+ throw new Error("Database not initialized");
810
+ }
811
+ return new Promise((resolve, reject) => {
812
+ if (!db) {
813
+ reject(new Error("Database not initialized"));
814
+ return;
815
+ }
816
+ const transaction = db.transaction(["wrapping-key"], "readwrite");
817
+ const store = transaction.objectStore("wrapping-key");
818
+ const request = store.delete("master");
819
+ request.onsuccess = () => {
820
+ resolve();
821
+ };
822
+ request.onerror = () => {
823
+ console.error("[STORAGE] Failed to clear wrapping key:", request.error);
824
+ reject(request.error);
825
+ };
826
+ });
827
+ }
828
+ async function clearAllData() {
829
+ await clearSession();
830
+ await clearWrappingKey();
831
+ }
832
+ async function hasSession() {
833
+ const session = await getSession();
834
+ return session !== null;
835
+ }
836
+
837
+ // src/auth/register.ts
838
+ async function register(params, deps) {
839
+ const credentials = await generateRegistrationCredentials(
840
+ {
841
+ email: params.email,
842
+ verification_credential: params.verificationCode,
843
+ given_name: params.firstName,
844
+ family_name: params.lastName,
845
+ picture: params.picture
846
+ },
847
+ params.password,
848
+ deps.deploymentDomain
849
+ );
850
+ const response = await deps.fetchClient.POST("/v1/user/register", {
851
+ body: credentials.request
852
+ });
853
+ if (response.error) {
854
+ throw new Error(
855
+ `Registration failed: ${typeof response.error === "object" && response.error !== null && "detail" in response.error ? response.error.detail : "Unknown error"}`
856
+ );
857
+ }
858
+ return { signingPrivateKey: credentials.signingPrivateKey };
859
+ }
860
+
861
+ // src/auth/login.ts
862
+ async function login(params, deps) {
863
+ const credentials = await generateLoginCredentials(
864
+ {
865
+ email: params.email,
866
+ sso_token: params.ssoToken
867
+ },
868
+ params.password,
869
+ deps.deploymentDomain
870
+ );
871
+ return performLogin(credentials.request, credentials.signingPrivateKey, deps);
872
+ }
873
+ async function loginWithKey(params, deps) {
874
+ const credentials = generateLoginCredentialsFromKey(
875
+ {
876
+ email: params.email,
877
+ sso_token: params.ssoToken
878
+ },
879
+ params.signingPrivateKey
880
+ );
881
+ return performLogin(credentials.request, credentials.signingPrivateKey, deps);
882
+ }
883
+ async function performLogin(request, signingPrivateKey, deps) {
884
+ const response = await deps.fetchClient.POST("/v1/user/login", {
885
+ body: request
886
+ });
887
+ if (response.error || !response.data) {
888
+ throw new Error(
889
+ `Login failed: ${typeof response.error === "object" && response.error !== null && "detail" in response.error ? response.error.detail : "Unknown error"}`
890
+ );
891
+ }
892
+ const data = response.data;
893
+ const serverSessionKey = base64ToBytes(data.session_key);
894
+ await saveSession({
895
+ signingPrivateKey,
896
+ serverSessionKey,
897
+ userExtId: data.user.external_id ?? void 0
898
+ });
899
+ deps.session.setAccessToken(data.access_token);
900
+ deps.session.setUser(request.email, data.user.external_id ?? void 0);
901
+ return {
902
+ accessToken: data.access_token,
903
+ userExtId: data.user.external_id ?? void 0,
904
+ signingPrivateKey,
905
+ serverSessionKey
906
+ };
907
+ }
908
+
909
+ // src/auth/logout.ts
910
+ async function logout(session) {
911
+ session.clear();
912
+ await clearAllData();
913
+ }
914
+
915
+ // src/auth/change-password.ts
916
+ async function changePassword(params, deps) {
917
+ const credentials = await generatePasswordChangeCredentials(
918
+ params.email,
919
+ params.currentPassword,
920
+ params.newPassword,
921
+ deps.deploymentDomain
922
+ );
923
+ const response = await deps.fetchClient.POST("/v1/user/change_password", {
924
+ body: {
925
+ ...credentials.request,
926
+ rewrapped_workspace_keys: params.rewrappedWorkspaceKeys ?? {}
927
+ }
928
+ });
929
+ if (response.error) {
930
+ throw new Error(
931
+ `Password change failed: ${typeof response.error === "object" && response.error !== null && "detail" in response.error ? response.error.detail : "Unknown error"}`
932
+ );
933
+ }
934
+ await updateSigningPrivateKey(credentials.newSigningPrivateKey);
935
+ return { newSigningPrivateKey: credentials.newSigningPrivateKey };
936
+ }
937
+
938
+ // src/client.ts
939
+ var API_PREFIX = "/v1";
940
+ var DEFAULT_WORKSPACE_KEY_URL_CONFIG = {
941
+ excludePatterns: [
942
+ `${API_PREFIX}/user/`,
943
+ `${API_PREFIX}/health/`,
944
+ `${API_PREFIX}/configs/`,
945
+ `${API_PREFIX}/workspace/create`
946
+ ],
947
+ includePatterns: [
948
+ `${API_PREFIX}/workspace/wrk-`,
949
+ `${API_PREFIX}/document/`,
950
+ `${API_PREFIX}/conversation/`,
951
+ `${API_PREFIX}/assistant/`,
952
+ `${API_PREFIX}/tag/`
953
+ ]
954
+ };
955
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set([
956
+ "ECONNREFUSED",
957
+ "ECONNRESET",
958
+ "ETIMEDOUT",
959
+ "UND_ERR_CONNECT_TIMEOUT"
960
+ ]);
961
+ var MAX_RETRIES = 3;
962
+ var BASE_DELAY_MS = 200;
963
+ function isRetryableError(err) {
964
+ if (!(err instanceof Error)) return false;
965
+ const cause = err.cause;
966
+ if (cause?.code && RETRYABLE_CODES.has(cause.code)) return true;
967
+ const msg = err.message || "";
968
+ return RETRYABLE_CODES.has(msg) || msg.includes("ECONNREFUSED") || msg.includes("ECONNRESET");
969
+ }
970
+ function createRetryFetch() {
971
+ return async (input, init) => {
972
+ let lastError;
973
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
974
+ try {
975
+ const fetchInput = input instanceof Request ? input.clone() : input;
976
+ return await globalThis.fetch(fetchInput, init);
977
+ } catch (err) {
978
+ lastError = err;
979
+ if (attempt < MAX_RETRIES && isRetryableError(err)) {
980
+ await new Promise((r) => setTimeout(r, BASE_DELAY_MS * 2 ** attempt));
981
+ continue;
982
+ }
983
+ throw err;
984
+ }
985
+ }
986
+ throw lastError;
987
+ };
988
+ }
989
+ function createArbiClient(options) {
990
+ const {
991
+ baseUrl,
992
+ deploymentDomain,
993
+ workspaceKeyUrlConfig = DEFAULT_WORKSPACE_KEY_URL_CONFIG,
994
+ reloginExcludePatterns = [`${API_PREFIX}/user/login`],
995
+ credentials = "include",
996
+ ssoTokenProvider = null,
997
+ onReloginSuccess
998
+ } = options;
999
+ const retryFetch = createRetryFetch();
1000
+ const session = createSessionManager();
1001
+ const tokenProvider = createTokenProvider(session);
1002
+ const workspaceKeyProvider = createWorkspaceKeyProvider(session);
1003
+ const authState = createAuthStateProvider(session);
1004
+ const cryptoProvider = {
1005
+ ensureReady: initSodium,
1006
+ signMessage,
1007
+ deriveEncryptionKeypair: deriveEncryptionKeypairFromSigning,
1008
+ sealedBoxDecrypt,
1009
+ createWorkspaceKeyHeader,
1010
+ fromBase64: base64ToBytes
1011
+ };
1012
+ const loginProvider = {
1013
+ async login(payload) {
1014
+ const rawFetch = createFetchClient({
1015
+ baseUrl,
1016
+ credentials,
1017
+ fetch: retryFetch
1018
+ });
1019
+ const response = await rawFetch.POST("/v1/user/login", {
1020
+ body: {
1021
+ email: payload.email,
1022
+ signature: payload.signature,
1023
+ timestamp: payload.timestamp,
1024
+ sso_token: payload.ssoToken
1025
+ }
1026
+ });
1027
+ if (response.error || !response.data) {
1028
+ console.warn("[API] Login call failed:", response.error);
1029
+ return null;
1030
+ }
1031
+ const data = response.data;
1032
+ return {
1033
+ accessToken: data.access_token,
1034
+ sessionKey: base64ToBytes(data.session_key),
1035
+ userExtId: data.user.external_id ?? void 0
1036
+ };
1037
+ }
1038
+ };
1039
+ const workspaceKeyRefreshProvider = {
1040
+ async getWrappedKey(accessToken, workspaceId) {
1041
+ const workspacesResponse = await retryFetch(`${baseUrl}${API_PREFIX}/user/workspaces`, {
1042
+ method: "GET",
1043
+ headers: {
1044
+ Authorization: `Bearer ${accessToken}`,
1045
+ "Content-Type": "application/json"
1046
+ },
1047
+ credentials
1048
+ });
1049
+ if (!workspacesResponse.ok) return null;
1050
+ const workspacesData = await workspacesResponse.json();
1051
+ const workspace = workspacesData.find(
1052
+ (w) => w.external_id === workspaceId
1053
+ );
1054
+ return workspace?.wrapped_key ?? null;
1055
+ }
1056
+ };
1057
+ const reloginHandler = createReloginHandler({
1058
+ crypto: cryptoProvider,
1059
+ sessionStorage: { getSession, saveSession },
1060
+ authState,
1061
+ ssoTokenProvider,
1062
+ loginProvider,
1063
+ workspaceKeyRefreshProvider,
1064
+ onReloginSuccess
1065
+ });
1066
+ const fetchClient = createFetchClient({
1067
+ baseUrl,
1068
+ credentials,
1069
+ fetch: retryFetch
1070
+ });
1071
+ fetchClient.use(createBearerAuthMiddleware({ tokenProvider }));
1072
+ fetchClient.use(
1073
+ createWorkspaceKeyMiddleware({
1074
+ workspaceKeyProvider,
1075
+ urlConfig: workspaceKeyUrlConfig
1076
+ })
1077
+ );
1078
+ fetchClient.use(
1079
+ createAutoReloginMiddleware({
1080
+ reloginHandler,
1081
+ reloginExcludePatterns
1082
+ })
1083
+ );
1084
+ const authDeps = {
1085
+ fetchClient,
1086
+ session,
1087
+ deploymentDomain
1088
+ };
1089
+ return {
1090
+ fetch: fetchClient,
1091
+ session,
1092
+ auth: {
1093
+ register: (params) => register(params, authDeps),
1094
+ login: (params) => login(params, authDeps),
1095
+ loginWithKey: (params) => loginWithKey(params, authDeps),
1096
+ logout: () => logout(session),
1097
+ changePassword: (params) => changePassword(params, authDeps),
1098
+ relogin: reloginHandler
1099
+ },
1100
+ crypto: {
1101
+ initSodium,
1102
+ generateUserKeypairs: (email, password) => generateUserKeypairs(email, password, deploymentDomain),
1103
+ signMessage,
1104
+ sealedBoxDecrypt,
1105
+ sealedBoxEncrypt,
1106
+ createWorkspaceKeyHeader,
1107
+ deriveEncryptionKeypairFromSigning,
1108
+ derivePublicKey,
1109
+ base64ToBytes,
1110
+ bytesToBase64,
1111
+ base64Encode,
1112
+ base64Decode,
1113
+ computeSharedSecret,
1114
+ encryptMessage,
1115
+ decryptMessage,
1116
+ encryptMessageWithSharedSecret,
1117
+ decryptMessageWithSharedSecret
1118
+ },
1119
+ storage: {
1120
+ getSession,
1121
+ saveSession
1122
+ }
1123
+ };
1124
+ }
1125
+
1126
+ // src/websocket.ts
1127
+ function buildWebSocketUrl(baseUrl) {
1128
+ const wsScheme = baseUrl.startsWith("https") ? "wss" : "ws";
1129
+ const hostAndPath = baseUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
1130
+ return `${wsScheme}://${hostAndPath}${API_PREFIX}/notifications/ws`;
1131
+ }
1132
+ function createAuthMessage(accessToken) {
1133
+ const msg = { type: "auth", token: accessToken };
1134
+ return JSON.stringify(msg);
1135
+ }
1136
+ function parseServerMessage(data) {
1137
+ try {
1138
+ const parsed = JSON.parse(data);
1139
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.type === "string") {
1140
+ return parsed;
1141
+ }
1142
+ return null;
1143
+ } catch {
1144
+ return null;
1145
+ }
1146
+ }
1147
+ function isMessageType(msg, type) {
1148
+ return msg.type === type;
1149
+ }
1150
+
1151
+ export { API_PREFIX, base64Decode, base64Encode, base64ToBytes, buildWebSocketUrl, bytesToBase64, clearAllData, clearSession, computeSharedSecret, createArbiClient, createAuthMessage, createAutoReloginMiddleware, createBearerAuthMiddleware, createReloginHandler, createSessionManager, createWorkspaceKeyHeader, createWorkspaceKeyMiddleware, decryptMessage, decryptMessageWithSharedSecret, deriveEncryptionKeypairFromSigning, derivePublicKey, encryptMessage, encryptMessageWithSharedSecret, generateKeyPairs, generateLoginCredentials, generateLoginCredentialsFromKey, generatePasswordChangeCredentials, generateRecoveryPasswordChangeCredentials, generateRegistrationCredentials, generateUserKeypairs, getSession, hasSession, initSodium, initializeDatabase, isMessageType, needsWorkspaceKey, parseServerMessage, saveSession, sealedBoxDecrypt, sealedBoxEncrypt, signMessage, updateSigningPrivateKey };
1152
+ //# sourceMappingURL=index.js.map
1153
+ //# sourceMappingURL=index.js.map