@arbidocs/sdk 0.4.2 → 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
@@ -3496,6 +3496,12 @@ var ArbiError = class extends Error {
3496
3496
  this.name = "ArbiError";
3497
3497
  }
3498
3498
  };
3499
+ var ArbiSessionExpiredError = class extends ArbiError {
3500
+ constructor(message = "Your session expired. Sign in again.") {
3501
+ super(message);
3502
+ this.name = "ArbiSessionExpiredError";
3503
+ }
3504
+ };
3499
3505
  var ArbiApiError = class extends ArbiError {
3500
3506
  apiError;
3501
3507
  constructor(message, apiError) {
@@ -6191,11 +6197,23 @@ async function remindFact(arbi, factId, options = {}) {
6191
6197
  }
6192
6198
 
6193
6199
  // src/arbi.ts
6200
+ function tokenExpiresWithin(token, seconds) {
6201
+ const payload = token?.split(".")[1];
6202
+ if (!payload) return false;
6203
+ try {
6204
+ const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
6205
+ const { exp } = JSON.parse(json);
6206
+ return typeof exp === "number" && exp * 1e3 - Date.now() < seconds * 1e3;
6207
+ } catch {
6208
+ return false;
6209
+ }
6210
+ }
6194
6211
  var Arbi = class {
6195
6212
  client = null;
6196
6213
  loginResult = null;
6197
6214
  currentWorkspaceId = null;
6198
6215
  dmCryptoContext = null;
6216
+ sessionExpiredListeners = /* @__PURE__ */ new Set();
6199
6217
  options;
6200
6218
  constructor(options) {
6201
6219
  this.options = options;
@@ -6208,7 +6226,13 @@ var Arbi = class {
6208
6226
  this.client = client.createArbiClient({
6209
6227
  baseUrl: this.options.url,
6210
6228
  deploymentDomain: domain,
6211
- credentials: this.options.credentials ?? "omit"
6229
+ credentials: this.options.credentials ?? "omit",
6230
+ // A silent re-login rotates the server session key: keep ours in step, or the next
6231
+ // workspace open would seal the key for a session the server no longer has.
6232
+ onReloginSuccess: ({ serverSessionKey }) => {
6233
+ if (this.loginResult) this.loginResult = { ...this.loginResult, serverSessionKey };
6234
+ },
6235
+ onSessionExpired: () => this.expireSession()
6212
6236
  });
6213
6237
  await this.client.crypto.initSodium();
6214
6238
  return this.client;
@@ -6271,6 +6295,52 @@ var Arbi = class {
6271
6295
  });
6272
6296
  this.currentWorkspaceId = ws.external_id;
6273
6297
  }
6298
+ /**
6299
+ * Subscribe to "the session cannot be recovered". Fires when a request failed with 401 or
6300
+ * "No active workspace" and neither a workspace re-open nor a silent re-login was possible
6301
+ * (the server refused; an unreachable server does not fire). The local session is already
6302
+ * cleared when it fires: show "Your session expired, sign in again". Returns an unsubscribe.
6303
+ */
6304
+ onSessionExpired(listener) {
6305
+ this.sessionExpiredListeners.add(listener);
6306
+ return () => this.sessionExpiredListeners.delete(listener);
6307
+ }
6308
+ expireSession() {
6309
+ if (this.loginResult) {
6310
+ this.loginResult.signingPrivateKey.fill(0);
6311
+ this.loginResult.serverSessionKey.fill(0);
6312
+ }
6313
+ this.loginResult = null;
6314
+ this.currentWorkspaceId = null;
6315
+ this.dmCryptoContext = null;
6316
+ this.client?.session.clear();
6317
+ for (const listener of [...this.sessionExpiredListeners]) listener();
6318
+ }
6319
+ /**
6320
+ * Run a raw-fetch operation (streaming, multipart: no middleware) and, when it fails because
6321
+ * the token expired or the server lost the open workspace, recover once and run it again.
6322
+ * `run` must read the auth headers itself so the retry carries the fresh token.
6323
+ */
6324
+ async recovering(run) {
6325
+ if (this.client && tokenExpiresWithin(this.client.session.getState().accessToken, 30)) {
6326
+ await this.client.auth.relogin();
6327
+ }
6328
+ try {
6329
+ return await run();
6330
+ } catch (error) {
6331
+ const message = error instanceof Error ? error.message : "";
6332
+ const noWorkspace = /no active workspace/i.test(message);
6333
+ const unauthorised = /\b401\b|token has expired/i.test(message);
6334
+ if (!(error instanceof ArbiApiError) || !(noWorkspace || unauthorised)) throw error;
6335
+ const client = this.requireClient();
6336
+ const recovered = noWorkspace && await client.auth.reopenWorkspace() || await client.auth.relogin() !== null;
6337
+ if (!recovered) {
6338
+ this.expireSession();
6339
+ throw new ArbiSessionExpiredError();
6340
+ }
6341
+ return run();
6342
+ }
6343
+ }
6274
6344
  /** Log out and clear internal state. */
6275
6345
  async logout() {
6276
6346
  if (this.client) {
@@ -6437,16 +6507,18 @@ var Arbi = class {
6437
6507
  },
6438
6508
  uploadFile: (fileData, fileName, options) => {
6439
6509
  this.requireWorkspace();
6440
- return uploadFile(
6441
- this.getAuthHeaders(),
6442
- fileData,
6443
- fileName,
6444
- options?.folder ? { folder: options.folder } : void 0
6510
+ return this.recovering(
6511
+ () => uploadFile(
6512
+ this.getAuthHeaders(),
6513
+ fileData,
6514
+ fileName,
6515
+ options?.folder ? { folder: options.folder } : void 0
6516
+ )
6445
6517
  );
6446
6518
  },
6447
- download: (docId) => downloadDocument(this.getAuthHeaders(), docId),
6448
- getParsedContent: (docId, stage) => getParsedContent(this.getAuthHeaders(), docId, stage),
6449
- getThumbnails: (docIds) => getThumbnails(this.getAuthHeaders(), docIds)
6519
+ download: (docId) => this.recovering(() => downloadDocument(this.getAuthHeaders(), docId)),
6520
+ getParsedContent: (docId, stage) => this.recovering(() => getParsedContent(this.getAuthHeaders(), docId, stage)),
6521
+ getThumbnails: (docIds) => this.recovering(() => getThumbnails(this.getAuthHeaders(), docIds))
6450
6522
  };
6451
6523
  // ── Conversations ─────────────────────────────────────────────────────
6452
6524
  conversations = {
@@ -6465,16 +6537,17 @@ var Arbi = class {
6465
6537
  * Returns the accumulated result after the stream completes.
6466
6538
  */
6467
6539
  query: async (question, options) => {
6468
- const auth = this.getAuthHeaders();
6469
6540
  const workspaceId = this.requireWorkspace();
6470
- const response = await queryAssistant({
6471
- ...auth,
6472
- workspaceId,
6473
- question,
6474
- docIds: options.docIds,
6475
- previousResponseId: options.previousResponseId,
6476
- model: options.model
6477
- });
6541
+ const response = await this.recovering(
6542
+ () => queryAssistant({
6543
+ ...this.getAuthHeaders(),
6544
+ workspaceId,
6545
+ question,
6546
+ docIds: options.docIds,
6547
+ previousResponseId: options.previousResponseId,
6548
+ model: options.model
6549
+ })
6550
+ );
6478
6551
  const callbacks = {
6479
6552
  onToken: options.onToken,
6480
6553
  onStreamStart: options.onStreamStart,
@@ -6638,8 +6711,10 @@ var Arbi = class {
6638
6711
  list: (options) => listFiles(this.requireClient(), options),
6639
6712
  get: (fileId) => getFile(this.requireClient(), fileId),
6640
6713
  delete: (fileId) => deleteFile(this.requireClient(), fileId),
6641
- upload: (fileData, fileName, purpose) => uploadFile2(this.getAuthHeaders(), fileData, fileName, purpose),
6642
- getContent: (fileId) => getFileContent(this.getAuthHeaders(), fileId)
6714
+ upload: (fileData, fileName, purpose) => this.recovering(
6715
+ () => uploadFile2(this.getAuthHeaders(), fileData, fileName, purpose)
6716
+ ),
6717
+ getContent: (fileId) => this.recovering(() => getFileContent(this.getAuthHeaders(), fileId))
6643
6718
  };
6644
6719
  // ── Agents ──────────────────────────────────────────────────────────
6645
6720
  agents = {
@@ -7824,6 +7899,7 @@ async function startDmListener(options) {
7824
7899
  exports.Arbi = Arbi;
7825
7900
  exports.ArbiApiError = ArbiApiError;
7826
7901
  exports.ArbiError = ArbiError;
7902
+ exports.ArbiSessionExpiredError = ArbiSessionExpiredError;
7827
7903
  exports.ClaudeOrchestrator = ClaudeOrchestrator;
7828
7904
  exports.DOC_TERMINAL_STATUSES = DOC_TERMINAL_STATUSES;
7829
7905
  exports.FileConfigStore = FileConfigStore;