@arbidocs/client 0.4.1 → 0.4.3

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.cjs CHANGED
@@ -392,6 +392,21 @@ function createBearerAuthMiddleware(config) {
392
392
  }
393
393
 
394
394
  // src/middleware/auto-relogin.ts
395
+ var NO_ACTIVE_WORKSPACE = /no active workspace/i;
396
+ async function isNoActiveWorkspace(response) {
397
+ try {
398
+ return NO_ACTIVE_WORKSPACE.test(await response.clone().text());
399
+ } catch {
400
+ return false;
401
+ }
402
+ }
403
+ function safeClone(request) {
404
+ try {
405
+ return request.clone();
406
+ } catch {
407
+ return null;
408
+ }
409
+ }
395
410
  function createAutoReloginMiddleware(config) {
396
411
  const cloneCache = /* @__PURE__ */ new WeakMap();
397
412
  return {
@@ -402,6 +417,25 @@ function createAutoReloginMiddleware(config) {
402
417
  }
403
418
  },
404
419
  async onResponse({ response, request }) {
420
+ if (response.status === 400 && await isNoActiveWorkspace(response)) {
421
+ let token = null;
422
+ let recovered = await config.reopenWorkspaceHandler?.() ?? false;
423
+ if (!recovered) {
424
+ token = await config.reloginHandler();
425
+ recovered = token !== null;
426
+ }
427
+ if (!recovered) {
428
+ config.onReloginFailed?.();
429
+ return response;
430
+ }
431
+ const retry = cloneCache.get(request) ?? safeClone(request);
432
+ if (retry) {
433
+ if (token) retry.headers.set("Authorization", `Bearer ${token}`);
434
+ return fetch(retry);
435
+ }
436
+ config.onRetryCloneFailed?.();
437
+ return response;
438
+ }
405
439
  if (response.status === 400) {
406
440
  const isSafeMethod = ["GET", "HEAD", "DELETE"].includes(request.method);
407
441
  if (isSafeMethod) {
@@ -562,6 +596,47 @@ function createReloginHandler(deps) {
562
596
  };
563
597
  }
564
598
 
599
+ // src/relogin/reopen.ts
600
+ function createWorkspaceReopener(deps) {
601
+ let inFlight = null;
602
+ return function reopenWorkspace() {
603
+ if (inFlight) return inFlight;
604
+ inFlight = (async () => {
605
+ try {
606
+ const workspaceId = deps.authState.getSelectedWorkspaceId();
607
+ const accessToken = deps.getAccessToken();
608
+ if (!workspaceId || !accessToken) return false;
609
+ const session = await deps.sessionStorage.getSession();
610
+ if (!session?.signingPrivateKey || !session.serverSessionKey) return false;
611
+ await deps.crypto.ensureReady();
612
+ const wrappedKey = await deps.workspaceKeyRefreshProvider.getWrappedKey(
613
+ accessToken,
614
+ workspaceId
615
+ );
616
+ if (!wrappedKey) return false;
617
+ const encryptionKeyPair = deps.crypto.deriveEncryptionKeypair({
618
+ publicKey: session.signingPrivateKey.slice(32, 64),
619
+ secretKey: session.signingPrivateKey
620
+ });
621
+ const workspaceKey = deps.crypto.sealedBoxDecrypt(wrappedKey, encryptionKeyPair.secretKey);
622
+ const sealed = await deps.crypto.sealKeyForSession(workspaceKey, session.serverSessionKey);
623
+ const opened = await deps.workspaceOpenProvider.openWorkspace(
624
+ accessToken,
625
+ workspaceId,
626
+ sealed
627
+ );
628
+ return opened === true;
629
+ } catch (error) {
630
+ console.error("[API] Failed to re-open workspace:", error);
631
+ return false;
632
+ } finally {
633
+ inFlight = null;
634
+ }
635
+ })();
636
+ return inFlight;
637
+ };
638
+ }
639
+
565
640
  // src/storage/indexed-db.ts
566
641
  var DB_NAME = "arbi-crypto";
567
642
  var DB_VERSION = 2;
@@ -1009,6 +1084,7 @@ function createArbiClient(options) {
1009
1084
  credentials = "include",
1010
1085
  ssoTokenProvider = null,
1011
1086
  onReloginSuccess,
1087
+ onSessionExpired,
1012
1088
  sessionStorage: sessionStorageOverride,
1013
1089
  userEmail
1014
1090
  } = options;
@@ -1029,6 +1105,7 @@ function createArbiClient(options) {
1029
1105
  sealKeyForSession,
1030
1106
  fromBase64: base64ToBytes
1031
1107
  };
1108
+ let reloginUnreachable = false;
1032
1109
  const loginProvider = {
1033
1110
  async login(payload) {
1034
1111
  const rawFetch = createFetchClient__default.default({
@@ -1036,14 +1113,21 @@ function createArbiClient(options) {
1036
1113
  credentials,
1037
1114
  fetch: retryFetch
1038
1115
  });
1039
- const response = await rawFetch.POST("/v1/user/login", {
1040
- body: {
1041
- email: payload.email,
1042
- signature: payload.signature,
1043
- timestamp: payload.timestamp,
1044
- sso_token: payload.ssoToken
1045
- }
1046
- });
1116
+ let response;
1117
+ try {
1118
+ response = await rawFetch.POST("/v1/user/login", {
1119
+ body: {
1120
+ email: payload.email,
1121
+ signature: payload.signature,
1122
+ timestamp: payload.timestamp,
1123
+ sso_token: payload.ssoToken
1124
+ }
1125
+ });
1126
+ } catch (error) {
1127
+ console.warn("[API] Login call unreachable:", error);
1128
+ reloginUnreachable = true;
1129
+ return null;
1130
+ }
1047
1131
  if (response.error || !response.data) {
1048
1132
  console.warn("[API] Login call failed:", response.error);
1049
1133
  return null;
@@ -1074,15 +1158,46 @@ function createArbiClient(options) {
1074
1158
  return workspace?.wrapped_key ?? null;
1075
1159
  }
1076
1160
  };
1077
- const reloginHandler = createReloginHandler({
1161
+ const workspaceOpenProvider = {
1162
+ async openWorkspace(accessToken, workspaceId, encryptedWorkspaceKey) {
1163
+ try {
1164
+ const response = await retryFetch(`${baseUrl}${API_PREFIX}/workspace/${workspaceId}/open`, {
1165
+ method: "POST",
1166
+ headers: {
1167
+ Authorization: `Bearer ${accessToken}`,
1168
+ "Content-Type": "application/json"
1169
+ },
1170
+ body: JSON.stringify({ workspace_key: encryptedWorkspaceKey }),
1171
+ credentials
1172
+ });
1173
+ return response.ok ? true : null;
1174
+ } catch {
1175
+ return null;
1176
+ }
1177
+ }
1178
+ };
1179
+ const rawReloginHandler = createReloginHandler({
1078
1180
  crypto: cryptoProvider,
1079
1181
  sessionStorage,
1080
1182
  authState,
1081
1183
  ssoTokenProvider,
1082
1184
  loginProvider,
1083
1185
  workspaceKeyRefreshProvider,
1186
+ workspaceOpenProvider,
1084
1187
  onReloginSuccess
1085
1188
  });
1189
+ const reloginHandler = () => {
1190
+ reloginUnreachable = false;
1191
+ return rawReloginHandler();
1192
+ };
1193
+ const reopenWorkspace = createWorkspaceReopener({
1194
+ crypto: cryptoProvider,
1195
+ sessionStorage,
1196
+ authState,
1197
+ getAccessToken: () => session.getState().accessToken,
1198
+ workspaceKeyRefreshProvider,
1199
+ workspaceOpenProvider
1200
+ });
1086
1201
  const fetchClient = createFetchClient__default.default({
1087
1202
  baseUrl,
1088
1203
  credentials,
@@ -1092,7 +1207,11 @@ function createArbiClient(options) {
1092
1207
  fetchClient.use(
1093
1208
  createAutoReloginMiddleware({
1094
1209
  reloginHandler,
1095
- reloginExcludePatterns
1210
+ reloginExcludePatterns,
1211
+ reopenWorkspaceHandler: reopenWorkspace,
1212
+ onReloginFailed: () => {
1213
+ if (!reloginUnreachable && session.getState().userEmail) onSessionExpired?.();
1214
+ }
1096
1215
  })
1097
1216
  );
1098
1217
  const authDeps = {
@@ -1109,7 +1228,8 @@ function createArbiClient(options) {
1109
1228
  loginWithKey: (params) => loginWithKey(params, authDeps),
1110
1229
  logout: () => logout(session),
1111
1230
  changePassword: (params) => changePassword(params, authDeps),
1112
- relogin: reloginHandler
1231
+ relogin: reloginHandler,
1232
+ reopenWorkspace
1113
1233
  },
1114
1234
  crypto: {
1115
1235
  initSodium,
@@ -1179,6 +1299,7 @@ exports.createBearerAuthMiddleware = createBearerAuthMiddleware;
1179
1299
  exports.createContentHash = createContentHash;
1180
1300
  exports.createReloginHandler = createReloginHandler;
1181
1301
  exports.createSessionManager = createSessionManager;
1302
+ exports.createWorkspaceReopener = createWorkspaceReopener;
1182
1303
  exports.decryptFile = decryptFile;
1183
1304
  exports.decryptMessage = decryptMessage;
1184
1305
  exports.decryptMessageWithSharedSecret = decryptMessageWithSharedSecret;