@arbidocs/sdk 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/dist/index.js ADDED
@@ -0,0 +1,1107 @@
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 (needsWorkspaceKey(request.url, config.urlConfig)) {
385
+ const hasWorkspaceKey = request.headers.has("Workspace-Key");
386
+ console.debug(
387
+ `[${request.method}] ${response.status}`,
388
+ request.url,
389
+ `Workspace-Key: ${hasWorkspaceKey ? "\u2713" : "\u2717"}`
390
+ );
391
+ }
392
+ if (response.status === 400) {
393
+ const isSafeMethod = ["GET", "HEAD", "DELETE"].includes(request.method);
394
+ if (isSafeMethod) {
395
+ console.info("[API] 400 error on protected endpoint - triggering instant re-login");
396
+ const newToken = await config.reloginHandler();
397
+ if (newToken) {
398
+ const newRequest = request.clone();
399
+ newRequest.headers.set("Authorization", `Bearer ${newToken}`);
400
+ if (needsWorkspaceKey(request.url, config.urlConfig)) {
401
+ const workspaceHeader = config.workspaceKeyProvider.getWorkspaceKeyHeader();
402
+ if (workspaceHeader) {
403
+ newRequest.headers.set("Workspace-Key", workspaceHeader);
404
+ }
405
+ }
406
+ console.info("[API] Retrying request after re-login");
407
+ return fetch(newRequest);
408
+ }
409
+ }
410
+ }
411
+ if (response.status === 401) {
412
+ const excludePatterns = config.reloginExcludePatterns ?? [];
413
+ const isExcluded = excludePatterns.some((pattern) => request.url.includes(pattern));
414
+ if (!isExcluded) {
415
+ const newToken = await config.reloginHandler();
416
+ if (newToken) {
417
+ try {
418
+ const newRequest = request.clone();
419
+ newRequest.headers.set("Authorization", `Bearer ${newToken}`);
420
+ if (needsWorkspaceKey(request.url, config.urlConfig)) {
421
+ const workspaceHeader = config.workspaceKeyProvider.getWorkspaceKeyHeader();
422
+ if (workspaceHeader) {
423
+ newRequest.headers.set("Workspace-Key", workspaceHeader);
424
+ }
425
+ }
426
+ console.info("[API] Retrying request after re-login:", request.method, request.url);
427
+ return fetch(newRequest);
428
+ } catch (cloneError) {
429
+ console.warn("[API] Could not clone request for retry:", cloneError);
430
+ config.onRetryCloneFailed?.();
431
+ }
432
+ }
433
+ }
434
+ }
435
+ return response;
436
+ }
437
+ };
438
+ }
439
+
440
+ // src/relogin/handler.ts
441
+ function createReloginHandler(deps) {
442
+ let reloginPromise = null;
443
+ return function instantReLogin() {
444
+ if (reloginPromise) {
445
+ console.info("[API] Re-login already in progress, waiting...");
446
+ return reloginPromise;
447
+ }
448
+ reloginPromise = (async () => {
449
+ try {
450
+ await deps.crypto.ensureReady();
451
+ const userEmail = deps.authState.getUserEmail();
452
+ if (!userEmail) {
453
+ console.warn("[API] No user email found for instant re-login");
454
+ return null;
455
+ }
456
+ const session = await deps.sessionStorage.getSession();
457
+ if (!session) {
458
+ console.warn("[API] No session found for instant re-login");
459
+ return null;
460
+ }
461
+ const ed25519PublicKey = session.signingPrivateKey.slice(32, 64);
462
+ const encryptionKeyPair = deps.crypto.deriveEncryptionKeypair({
463
+ publicKey: ed25519PublicKey,
464
+ secretKey: session.signingPrivateKey
465
+ });
466
+ const timestamp = Math.floor(Date.now() / 1e3);
467
+ const message = `${userEmail}|${timestamp}`;
468
+ const signature = deps.crypto.signMessage(message, session.signingPrivateKey);
469
+ console.info("[API] Attempting instant re-login");
470
+ const ssoState = deps.authState.getSsoState();
471
+ console.info("[API] Re-login SSO mode:", ssoState.isSsoMode);
472
+ console.info("[API] Auth0 authenticated:", ssoState.isAuth0Authenticated);
473
+ console.info("[API] Persisted Auth0 token available:", !!ssoState.auth0AccessToken);
474
+ const isSsoAccount = ssoState.isSsoMode || ssoState.isAuth0Authenticated || !!ssoState.auth0AccessToken;
475
+ let ssoToken = null;
476
+ if (deps.ssoTokenProvider) {
477
+ console.info("[API] SSO token provider available: true");
478
+ try {
479
+ ssoToken = await deps.ssoTokenProvider.getToken();
480
+ console.info(
481
+ "[API] Got SSO token from provider:",
482
+ ssoToken ? "yes (length: " + ssoToken.length + ")" : "no"
483
+ );
484
+ } catch (error) {
485
+ console.error("[API] Failed to get SSO token from provider:", error);
486
+ }
487
+ } else {
488
+ console.info("[API] SSO token provider available: false");
489
+ }
490
+ if (!ssoToken && ssoState.auth0AccessToken) {
491
+ ssoToken = ssoState.auth0AccessToken;
492
+ console.info(
493
+ "[API] Using persisted SSO token (length:",
494
+ ssoToken.length,
495
+ ") - may be expired"
496
+ );
497
+ }
498
+ if (!ssoToken && isSsoAccount) {
499
+ console.warn("[API] SSO account requires token but none available - aborting re-login");
500
+ return null;
501
+ }
502
+ const loginResult = await deps.loginProvider.login({
503
+ email: userEmail,
504
+ signature,
505
+ timestamp,
506
+ ssoToken: ssoToken ?? void 0
507
+ });
508
+ if (!loginResult) {
509
+ console.warn("[API] Instant re-login failed");
510
+ return null;
511
+ }
512
+ await deps.sessionStorage.saveSession({
513
+ signingPrivateKey: session.signingPrivateKey,
514
+ serverSessionKey: loginResult.sessionKey,
515
+ userExtId: loginResult.userExtId
516
+ });
517
+ deps.onReloginSuccess?.({
518
+ email: userEmail,
519
+ accessToken: loginResult.accessToken,
520
+ userExtId: loginResult.userExtId,
521
+ serverSessionKey: loginResult.sessionKey
522
+ });
523
+ deps.authState.setAccessToken(loginResult.accessToken);
524
+ deps.authState.clearWorkspaceHeaders();
525
+ const selectedWorkspaceId = deps.authState.getSelectedWorkspaceId();
526
+ if (selectedWorkspaceId) {
527
+ try {
528
+ const wrappedKey = await deps.workspaceKeyRefreshProvider.getWrappedKey(
529
+ loginResult.accessToken,
530
+ selectedWorkspaceId
531
+ );
532
+ if (wrappedKey) {
533
+ const workspaceKey = deps.crypto.sealedBoxDecrypt(
534
+ wrappedKey,
535
+ encryptionKeyPair.secretKey
536
+ );
537
+ const workspaceKeyHeader = await deps.crypto.createWorkspaceKeyHeader(
538
+ workspaceKey,
539
+ loginResult.sessionKey
540
+ );
541
+ deps.authState.setCachedWorkspaceHeader(selectedWorkspaceId, workspaceKeyHeader);
542
+ }
543
+ } catch (error) {
544
+ console.error("[API] Failed to regenerate workspace header:", error);
545
+ }
546
+ }
547
+ console.info("[API] Re-login successful");
548
+ return loginResult.accessToken;
549
+ } catch (error) {
550
+ console.error("[API] Instant re-login error:", error);
551
+ return null;
552
+ } finally {
553
+ reloginPromise = null;
554
+ }
555
+ })();
556
+ return reloginPromise;
557
+ };
558
+ }
559
+
560
+ // src/storage/indexed-db.ts
561
+ var DB_NAME = "arbi-crypto";
562
+ var DB_VERSION = 2;
563
+ var db = null;
564
+ async function initializeDatabase() {
565
+ return new Promise((resolve, reject) => {
566
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
567
+ request.onerror = () => {
568
+ console.error("[STORAGE] Failed to open IndexedDB:", request.error);
569
+ reject(request.error);
570
+ };
571
+ request.onsuccess = () => {
572
+ db = request.result;
573
+ resolve(db);
574
+ };
575
+ request.onupgradeneeded = (event) => {
576
+ const database = event.target.result;
577
+ if (!database.objectStoreNames.contains("session")) {
578
+ database.createObjectStore("session", { keyPath: "id" });
579
+ }
580
+ if (!database.objectStoreNames.contains("wrapping-key")) {
581
+ database.createObjectStore("wrapping-key", { keyPath: "id" });
582
+ }
583
+ };
584
+ });
585
+ }
586
+ async function ensureDatabase() {
587
+ if (!db) {
588
+ await initializeDatabase();
589
+ }
590
+ }
591
+ async function getOrCreateWrappingKey() {
592
+ await ensureDatabase();
593
+ if (!db) {
594
+ throw new Error("Database not initialized");
595
+ }
596
+ return new Promise((resolve, reject) => {
597
+ if (!db) {
598
+ reject(new Error("Database not initialized"));
599
+ return;
600
+ }
601
+ const transaction = db.transaction(["wrapping-key"], "readwrite");
602
+ const store = transaction.objectStore("wrapping-key");
603
+ const request = store.get("master");
604
+ request.onsuccess = () => {
605
+ const result = request.result;
606
+ if (result?.key) {
607
+ resolve(result.key);
608
+ } else {
609
+ crypto.subtle.generateKey(
610
+ {
611
+ name: "AES-GCM",
612
+ length: 256
613
+ },
614
+ false,
615
+ // ❌ NON-EXTRACTABLE - Can't be exported!
616
+ ["encrypt", "decrypt"]
617
+ ).then((key) => {
618
+ if (!db) {
619
+ reject(new Error("Database not initialized"));
620
+ return;
621
+ }
622
+ const putTransaction = db.transaction(["wrapping-key"], "readwrite");
623
+ const putStore = putTransaction.objectStore("wrapping-key");
624
+ const putRequest = putStore.put({
625
+ id: "master",
626
+ key,
627
+ created: Date.now()
628
+ });
629
+ putRequest.onsuccess = () => {
630
+ resolve(key);
631
+ };
632
+ putRequest.onerror = () => {
633
+ reject(new Error("Failed to store wrapping key"));
634
+ };
635
+ }).catch((error) => {
636
+ const errorMsg = error instanceof Error ? error.message : String(error);
637
+ reject(new Error(`Failed to generate wrapping key: ${errorMsg}`));
638
+ });
639
+ }
640
+ };
641
+ request.onerror = () => {
642
+ reject(new Error("Failed to retrieve wrapping key from IndexedDB"));
643
+ };
644
+ });
645
+ }
646
+ async function encryptWithWrappingKey(data) {
647
+ const wrappingKey = await getOrCreateWrappingKey();
648
+ const iv = crypto.getRandomValues(new Uint8Array(12));
649
+ const ciphertext = await crypto.subtle.encrypt(
650
+ {
651
+ name: "AES-GCM",
652
+ iv
653
+ },
654
+ wrappingKey,
655
+ data
656
+ );
657
+ return {
658
+ ciphertext: new Uint8Array(ciphertext),
659
+ iv
660
+ };
661
+ }
662
+ async function decryptWithWrappingKey(ciphertext, iv) {
663
+ const wrappingKey = await getOrCreateWrappingKey();
664
+ const decrypted = await crypto.subtle.decrypt(
665
+ {
666
+ name: "AES-GCM",
667
+ iv
668
+ },
669
+ wrappingKey,
670
+ ciphertext
671
+ );
672
+ return new Uint8Array(decrypted);
673
+ }
674
+ async function saveSession(sessionData) {
675
+ await ensureDatabase();
676
+ if (!db) {
677
+ throw new Error("Database not initialized");
678
+ }
679
+ try {
680
+ const encryptedSigningKey = await encryptWithWrappingKey(sessionData.signingPrivateKey);
681
+ let encryptedServerSessionKey;
682
+ if (sessionData.serverSessionKey) {
683
+ encryptedServerSessionKey = await encryptWithWrappingKey(sessionData.serverSessionKey);
684
+ }
685
+ return new Promise((resolve, reject) => {
686
+ if (!db) {
687
+ reject(new Error("Database not initialized"));
688
+ return;
689
+ }
690
+ const transaction = db.transaction(["session"], "readwrite");
691
+ const store = transaction.objectStore("session");
692
+ const data = {
693
+ id: "current",
694
+ encryptedSigningPrivateKey: encryptedSigningKey.ciphertext,
695
+ signingPrivateKeyIV: encryptedSigningKey.iv,
696
+ encryptedServerSessionKey: encryptedServerSessionKey?.ciphertext,
697
+ serverSessionKeyIV: encryptedServerSessionKey?.iv,
698
+ userExtId: sessionData.userExtId,
699
+ timestamp: Date.now()
700
+ };
701
+ const request = store.put(data);
702
+ request.onsuccess = () => {
703
+ resolve();
704
+ };
705
+ request.onerror = () => {
706
+ console.error("[STORAGE] Failed to save session:", request.error);
707
+ reject(request.error);
708
+ };
709
+ });
710
+ } catch (error) {
711
+ console.error("[STORAGE] Failed to encrypt session data:", error);
712
+ throw error;
713
+ }
714
+ }
715
+ async function getSession() {
716
+ await ensureDatabase();
717
+ if (!db) {
718
+ throw new Error("Database not initialized");
719
+ }
720
+ return new Promise((resolve, reject) => {
721
+ if (!db) {
722
+ reject(new Error("Database not initialized"));
723
+ return;
724
+ }
725
+ const transaction = db.transaction(["session"], "readonly");
726
+ const store = transaction.objectStore("session");
727
+ const request = store.get("current");
728
+ request.onsuccess = async () => {
729
+ const result = request.result;
730
+ if (result) {
731
+ try {
732
+ const signingPrivateKey = await decryptWithWrappingKey(
733
+ result.encryptedSigningPrivateKey,
734
+ result.signingPrivateKeyIV
735
+ );
736
+ let serverSessionKey;
737
+ if (result.encryptedServerSessionKey && result.serverSessionKeyIV) {
738
+ serverSessionKey = await decryptWithWrappingKey(
739
+ result.encryptedServerSessionKey,
740
+ result.serverSessionKeyIV
741
+ );
742
+ }
743
+ resolve({
744
+ signingPrivateKey,
745
+ serverSessionKey,
746
+ userExtId: result.userExtId
747
+ });
748
+ } catch (error) {
749
+ console.error("[STORAGE] Failed to decrypt session data:", error);
750
+ reject(error);
751
+ }
752
+ } else {
753
+ resolve(null);
754
+ }
755
+ };
756
+ request.onerror = () => {
757
+ console.error("[STORAGE] Failed to get session:", request.error);
758
+ reject(request.error);
759
+ };
760
+ });
761
+ }
762
+ async function updateSigningPrivateKey(newSigningPrivateKey) {
763
+ await ensureDatabase();
764
+ try {
765
+ const existingSession = await getSession();
766
+ if (!existingSession) {
767
+ throw new Error("No existing session to update");
768
+ }
769
+ await saveSession({
770
+ signingPrivateKey: newSigningPrivateKey,
771
+ serverSessionKey: existingSession.serverSessionKey,
772
+ userExtId: existingSession.userExtId
773
+ });
774
+ } catch (error) {
775
+ console.error("[STORAGE] Failed to update signing private key:", error);
776
+ throw error;
777
+ }
778
+ }
779
+ async function clearSession() {
780
+ await ensureDatabase();
781
+ if (!db) {
782
+ throw new Error("Database not initialized");
783
+ }
784
+ return new Promise((resolve, reject) => {
785
+ if (!db) {
786
+ reject(new Error("Database not initialized"));
787
+ return;
788
+ }
789
+ const transaction = db.transaction(["session"], "readwrite");
790
+ const store = transaction.objectStore("session");
791
+ const request = store.delete("current");
792
+ request.onsuccess = () => {
793
+ resolve();
794
+ };
795
+ request.onerror = () => {
796
+ console.error("[STORAGE] Failed to clear session:", request.error);
797
+ reject(request.error);
798
+ };
799
+ });
800
+ }
801
+ async function clearWrappingKey() {
802
+ await ensureDatabase();
803
+ if (!db) {
804
+ throw new Error("Database not initialized");
805
+ }
806
+ return new Promise((resolve, reject) => {
807
+ if (!db) {
808
+ reject(new Error("Database not initialized"));
809
+ return;
810
+ }
811
+ const transaction = db.transaction(["wrapping-key"], "readwrite");
812
+ const store = transaction.objectStore("wrapping-key");
813
+ const request = store.delete("master");
814
+ request.onsuccess = () => {
815
+ resolve();
816
+ };
817
+ request.onerror = () => {
818
+ console.error("[STORAGE] Failed to clear wrapping key:", request.error);
819
+ reject(request.error);
820
+ };
821
+ });
822
+ }
823
+ async function clearAllData() {
824
+ await clearSession();
825
+ await clearWrappingKey();
826
+ }
827
+ async function hasSession() {
828
+ const session = await getSession();
829
+ return session !== null;
830
+ }
831
+
832
+ // src/auth/register.ts
833
+ async function register(params, deps) {
834
+ const credentials = await generateRegistrationCredentials(
835
+ {
836
+ email: params.email,
837
+ verification_credential: params.verificationCode,
838
+ given_name: params.firstName,
839
+ family_name: params.lastName,
840
+ picture: params.picture
841
+ },
842
+ params.password,
843
+ deps.deploymentDomain
844
+ );
845
+ const response = await deps.fetchClient.POST("/api/user/register", {
846
+ body: credentials.request
847
+ });
848
+ if (response.error) {
849
+ throw new Error(
850
+ `Registration failed: ${typeof response.error === "object" && response.error !== null && "detail" in response.error ? response.error.detail : "Unknown error"}`
851
+ );
852
+ }
853
+ return { signingPrivateKey: credentials.signingPrivateKey };
854
+ }
855
+
856
+ // src/auth/login.ts
857
+ async function login(params, deps) {
858
+ const credentials = await generateLoginCredentials(
859
+ {
860
+ email: params.email,
861
+ sso_token: params.ssoToken
862
+ },
863
+ params.password,
864
+ deps.deploymentDomain
865
+ );
866
+ return performLogin(credentials.request, credentials.signingPrivateKey, deps);
867
+ }
868
+ async function loginWithKey(params, deps) {
869
+ const credentials = generateLoginCredentialsFromKey(
870
+ {
871
+ email: params.email,
872
+ sso_token: params.ssoToken
873
+ },
874
+ params.signingPrivateKey
875
+ );
876
+ return performLogin(credentials.request, credentials.signingPrivateKey, deps);
877
+ }
878
+ async function performLogin(request, signingPrivateKey, deps) {
879
+ const response = await deps.fetchClient.POST("/api/user/login", {
880
+ body: request
881
+ });
882
+ if (response.error || !response.data) {
883
+ throw new Error(
884
+ `Login failed: ${typeof response.error === "object" && response.error !== null && "detail" in response.error ? response.error.detail : "Unknown error"}`
885
+ );
886
+ }
887
+ const data = response.data;
888
+ const serverSessionKey = base64ToBytes(data.session_key);
889
+ await saveSession({
890
+ signingPrivateKey,
891
+ serverSessionKey,
892
+ userExtId: data.user.external_id ?? void 0
893
+ });
894
+ deps.session.setAccessToken(data.access_token);
895
+ deps.session.setUser(request.email, data.user.external_id ?? void 0);
896
+ return {
897
+ accessToken: data.access_token,
898
+ userExtId: data.user.external_id ?? void 0,
899
+ signingPrivateKey,
900
+ serverSessionKey
901
+ };
902
+ }
903
+
904
+ // src/auth/logout.ts
905
+ async function logout(session) {
906
+ session.clear();
907
+ await clearAllData();
908
+ }
909
+
910
+ // src/auth/change-password.ts
911
+ async function changePassword(params, deps) {
912
+ const credentials = await generatePasswordChangeCredentials(
913
+ params.email,
914
+ params.currentPassword,
915
+ params.newPassword,
916
+ deps.deploymentDomain
917
+ );
918
+ const response = await deps.fetchClient.POST("/api/user/change_password", {
919
+ body: {
920
+ ...credentials.request,
921
+ rewrapped_workspace_keys: params.rewrappedWorkspaceKeys ?? {}
922
+ }
923
+ });
924
+ if (response.error) {
925
+ throw new Error(
926
+ `Password change failed: ${typeof response.error === "object" && response.error !== null && "detail" in response.error ? response.error.detail : "Unknown error"}`
927
+ );
928
+ }
929
+ await updateSigningPrivateKey(credentials.newSigningPrivateKey);
930
+ return { newSigningPrivateKey: credentials.newSigningPrivateKey };
931
+ }
932
+
933
+ // src/client.ts
934
+ var DEFAULT_WORKSPACE_KEY_URL_CONFIG = {
935
+ excludePatterns: ["/api/user/", "/api/health/", "/api/configs/", "/api/workspace/create"],
936
+ includePatterns: [
937
+ "/api/workspace/wrk-",
938
+ "/api/document/",
939
+ "/api/conversation/",
940
+ "/api/assistant/",
941
+ "/api/tag/"
942
+ ]
943
+ };
944
+ function createArbiClient(options) {
945
+ const {
946
+ baseUrl,
947
+ deploymentDomain,
948
+ workspaceKeyUrlConfig = DEFAULT_WORKSPACE_KEY_URL_CONFIG,
949
+ reloginExcludePatterns = ["/api/user/login"],
950
+ credentials = "include",
951
+ ssoTokenProvider = null,
952
+ onReloginSuccess
953
+ } = options;
954
+ const session = createSessionManager();
955
+ const tokenProvider = createTokenProvider(session);
956
+ const workspaceKeyProvider = createWorkspaceKeyProvider(session);
957
+ const authState = createAuthStateProvider(session);
958
+ const cryptoProvider = {
959
+ ensureReady: initSodium,
960
+ signMessage,
961
+ deriveEncryptionKeypair: deriveEncryptionKeypairFromSigning,
962
+ sealedBoxDecrypt,
963
+ createWorkspaceKeyHeader,
964
+ fromBase64: base64ToBytes
965
+ };
966
+ const loginProvider = {
967
+ async login(payload) {
968
+ const rawFetch = createFetchClient({
969
+ baseUrl,
970
+ credentials
971
+ });
972
+ const response = await rawFetch.POST("/api/user/login", {
973
+ body: {
974
+ email: payload.email,
975
+ signature: payload.signature,
976
+ timestamp: payload.timestamp,
977
+ sso_token: payload.ssoToken
978
+ }
979
+ });
980
+ if (response.error || !response.data) {
981
+ console.warn("[API] Login call failed:", response.error);
982
+ return null;
983
+ }
984
+ const data = response.data;
985
+ return {
986
+ accessToken: data.access_token,
987
+ sessionKey: base64ToBytes(data.session_key),
988
+ userExtId: data.user.external_id ?? void 0
989
+ };
990
+ }
991
+ };
992
+ const workspaceKeyRefreshProvider = {
993
+ async getWrappedKey(accessToken, workspaceId) {
994
+ const workspacesResponse = await fetch(`${baseUrl}/api/user/workspaces`, {
995
+ method: "GET",
996
+ headers: {
997
+ Authorization: `Bearer ${accessToken}`,
998
+ "Content-Type": "application/json"
999
+ },
1000
+ credentials
1001
+ });
1002
+ if (!workspacesResponse.ok) return null;
1003
+ const workspacesData = await workspacesResponse.json();
1004
+ const workspace = workspacesData.find(
1005
+ (w) => w.external_id === workspaceId
1006
+ );
1007
+ return workspace?.wrapped_key ?? null;
1008
+ }
1009
+ };
1010
+ const reloginHandler = createReloginHandler({
1011
+ crypto: cryptoProvider,
1012
+ sessionStorage: { getSession, saveSession },
1013
+ authState,
1014
+ ssoTokenProvider,
1015
+ loginProvider,
1016
+ workspaceKeyRefreshProvider,
1017
+ onReloginSuccess
1018
+ });
1019
+ const fetchClient = createFetchClient({
1020
+ baseUrl,
1021
+ credentials
1022
+ });
1023
+ fetchClient.use(createBearerAuthMiddleware({ tokenProvider }));
1024
+ fetchClient.use(
1025
+ createWorkspaceKeyMiddleware({
1026
+ workspaceKeyProvider,
1027
+ urlConfig: workspaceKeyUrlConfig
1028
+ })
1029
+ );
1030
+ fetchClient.use(
1031
+ createAutoReloginMiddleware({
1032
+ reloginHandler,
1033
+ workspaceKeyProvider,
1034
+ urlConfig: workspaceKeyUrlConfig,
1035
+ reloginExcludePatterns
1036
+ })
1037
+ );
1038
+ const authDeps = {
1039
+ fetchClient,
1040
+ session,
1041
+ deploymentDomain
1042
+ };
1043
+ return {
1044
+ fetch: fetchClient,
1045
+ session,
1046
+ auth: {
1047
+ register: (params) => register(params, authDeps),
1048
+ login: (params) => login(params, authDeps),
1049
+ loginWithKey: (params) => loginWithKey(params, authDeps),
1050
+ logout: () => logout(session),
1051
+ changePassword: (params) => changePassword(params, authDeps),
1052
+ relogin: reloginHandler
1053
+ },
1054
+ crypto: {
1055
+ initSodium,
1056
+ generateUserKeypairs: (email, password) => generateUserKeypairs(email, password, deploymentDomain),
1057
+ signMessage,
1058
+ sealedBoxDecrypt,
1059
+ sealedBoxEncrypt,
1060
+ createWorkspaceKeyHeader,
1061
+ deriveEncryptionKeypairFromSigning,
1062
+ derivePublicKey,
1063
+ base64ToBytes,
1064
+ bytesToBase64,
1065
+ base64Encode,
1066
+ base64Decode,
1067
+ computeSharedSecret,
1068
+ encryptMessage,
1069
+ decryptMessage,
1070
+ encryptMessageWithSharedSecret,
1071
+ decryptMessageWithSharedSecret
1072
+ },
1073
+ storage: {
1074
+ getSession,
1075
+ saveSession
1076
+ }
1077
+ };
1078
+ }
1079
+
1080
+ // src/websocket.ts
1081
+ function buildWebSocketUrl(baseUrl) {
1082
+ const wsScheme = baseUrl.startsWith("https") ? "wss" : "ws";
1083
+ const hostAndPath = baseUrl.replace(/^https?:\/\//, "").replace(/\/+$/, "");
1084
+ return `${wsScheme}://${hostAndPath}/api/notifications/ws`;
1085
+ }
1086
+ function createAuthMessage(accessToken) {
1087
+ const msg = { type: "auth", token: accessToken };
1088
+ return JSON.stringify(msg);
1089
+ }
1090
+ function parseServerMessage(data) {
1091
+ try {
1092
+ const parsed = JSON.parse(data);
1093
+ if (typeof parsed === "object" && parsed !== null && typeof parsed.type === "string") {
1094
+ return parsed;
1095
+ }
1096
+ return null;
1097
+ } catch {
1098
+ return null;
1099
+ }
1100
+ }
1101
+ function isMessageType(msg, type) {
1102
+ return msg.type === type;
1103
+ }
1104
+
1105
+ export { 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 };
1106
+ //# sourceMappingURL=index.js.map
1107
+ //# sourceMappingURL=index.js.map