@arbidocs/sdk 0.4.2 → 0.4.4

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) {
@@ -5219,6 +5225,7 @@ __export(workspaces_exports, {
5219
5225
  copyDocuments: () => copyDocuments,
5220
5226
  createWorkspace: () => createWorkspace,
5221
5227
  deleteWorkspaces: () => deleteWorkspaces,
5228
+ grantTemporaryWorkspaceAccess: () => grantTemporaryWorkspaceAccess,
5222
5229
  listWorkspaceUsers: () => listWorkspaceUsers,
5223
5230
  listWorkspaces: () => listWorkspaces,
5224
5231
  removeWorkspaceUsers: () => removeWorkspaceUsers,
@@ -5259,14 +5266,33 @@ async function updateWorkspace(arbi, body) {
5259
5266
  async function listWorkspaceUsers(arbi) {
5260
5267
  return requireData(await arbi.fetch.GET("/v1/workspace/users"), "Failed to fetch workspace users");
5261
5268
  }
5262
- async function addWorkspaceUsers(arbi, emails, role = "collaborator") {
5269
+ async function addWorkspaceUsers(arbi, emails, role = "collaborator", target) {
5263
5270
  return requireData(
5264
5271
  await arbi.fetch.POST("/v1/workspace/users", {
5265
- body: { emails, role }
5272
+ body: target ? {
5273
+ emails,
5274
+ role,
5275
+ workspace_ext_id: target.workspaceExtId,
5276
+ workspace_key: target.workspaceKey
5277
+ } : { emails, role }
5266
5278
  }),
5267
5279
  "Failed to add users"
5268
5280
  );
5269
5281
  }
5282
+ async function grantTemporaryWorkspaceAccess(arbi, params) {
5283
+ return requireData(
5284
+ await arbi.fetch.POST("/v1/workspace/users", {
5285
+ body: {
5286
+ emails: params.emails,
5287
+ role: params.role ?? "collaborator",
5288
+ workspace_ext_id: params.workspaceExtId,
5289
+ workspace_key: params.workspaceKey,
5290
+ session_pubkey_b64: params.sessionPubkeyB64
5291
+ }
5292
+ }),
5293
+ "Failed to grant temporary access"
5294
+ );
5295
+ }
5270
5296
  async function removeWorkspaceUsers(arbi, userIds) {
5271
5297
  requireOk(
5272
5298
  await arbi.fetch.DELETE("/v1/workspace/users", {
@@ -5662,9 +5688,11 @@ __export(dm_exports, {
5662
5688
  // src/operations/agents.ts
5663
5689
  var agents_exports = {};
5664
5690
  __export(agents_exports, {
5691
+ authorizeAgent: () => authorizeAgent,
5665
5692
  createAgent: () => createAgent,
5666
5693
  deleteAgents: () => deleteAgents,
5667
5694
  getAgentConfig: () => getAgentConfig,
5695
+ getAuthorizeLimits: () => getAuthorizeLimits,
5668
5696
  listAgentWorkspaces: () => listAgentWorkspaces,
5669
5697
  listAgents: () => listAgents
5670
5698
  });
@@ -5699,6 +5727,21 @@ async function deleteAgents(arbi, agentExtIds) {
5699
5727
  "Failed to delete agents"
5700
5728
  );
5701
5729
  }
5730
+ async function authorizeAgent(arbi, agentExtId, workspaceExtId, ttlSeconds) {
5731
+ return requireData(
5732
+ await arbi.fetch.POST("/v1/agents/{agent_ext_id}/authorize", {
5733
+ params: { path: { agent_ext_id: agentExtId } },
5734
+ body: { workspace_ext_id: workspaceExtId, ttl_seconds: ttlSeconds ?? null }
5735
+ }),
5736
+ "Failed to authorize agent"
5737
+ );
5738
+ }
5739
+ async function getAuthorizeLimits(arbi) {
5740
+ return requireData(
5741
+ await arbi.fetch.GET("/v1/agents/authorize-limits"),
5742
+ "Failed to fetch authorize limits"
5743
+ );
5744
+ }
5702
5745
 
5703
5746
  // src/operations/dm.ts
5704
5747
  function createDmCryptoContext(arbi, signingPrivateKeyBytes, userExtId) {
@@ -6085,6 +6128,7 @@ __export(tasks_exports, {
6085
6128
  createTask: () => createTask,
6086
6129
  deleteTask: () => deleteTask,
6087
6130
  getTask: () => getTask,
6131
+ listTaskRuns: () => listTaskRuns,
6088
6132
  listTasks: () => listTasks,
6089
6133
  runTask: () => runTask,
6090
6134
  updateTask: () => updateTask
@@ -6122,6 +6166,12 @@ async function runTask(arbi, taskId) {
6122
6166
  "Failed to run task"
6123
6167
  );
6124
6168
  }
6169
+ async function listTaskRuns(arbi, taskId) {
6170
+ return requireData(
6171
+ await arbi.fetch.GET("/v1/tasks/{task_id}/runs", { params: { path: { task_id: taskId } } }),
6172
+ "Failed to list task runs"
6173
+ );
6174
+ }
6125
6175
 
6126
6176
  // src/operations/facts.ts
6127
6177
  var facts_exports = {};
@@ -6191,11 +6241,23 @@ async function remindFact(arbi, factId, options = {}) {
6191
6241
  }
6192
6242
 
6193
6243
  // src/arbi.ts
6244
+ function tokenExpiresWithin(token, seconds) {
6245
+ const payload = token?.split(".")[1];
6246
+ if (!payload) return false;
6247
+ try {
6248
+ const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
6249
+ const { exp } = JSON.parse(json);
6250
+ return typeof exp === "number" && exp * 1e3 - Date.now() < seconds * 1e3;
6251
+ } catch {
6252
+ return false;
6253
+ }
6254
+ }
6194
6255
  var Arbi = class {
6195
6256
  client = null;
6196
6257
  loginResult = null;
6197
6258
  currentWorkspaceId = null;
6198
6259
  dmCryptoContext = null;
6260
+ sessionExpiredListeners = /* @__PURE__ */ new Set();
6199
6261
  options;
6200
6262
  constructor(options) {
6201
6263
  this.options = options;
@@ -6208,7 +6270,13 @@ var Arbi = class {
6208
6270
  this.client = client.createArbiClient({
6209
6271
  baseUrl: this.options.url,
6210
6272
  deploymentDomain: domain,
6211
- credentials: this.options.credentials ?? "omit"
6273
+ credentials: this.options.credentials ?? "omit",
6274
+ // A silent re-login rotates the server session key: keep ours in step, or the next
6275
+ // workspace open would seal the key for a session the server no longer has.
6276
+ onReloginSuccess: ({ serverSessionKey }) => {
6277
+ if (this.loginResult) this.loginResult = { ...this.loginResult, serverSessionKey };
6278
+ },
6279
+ onSessionExpired: () => this.expireSession()
6212
6280
  });
6213
6281
  await this.client.crypto.initSodium();
6214
6282
  return this.client;
@@ -6245,6 +6313,22 @@ var Arbi = class {
6245
6313
  const signingPrivateKey = client.crypto.base64ToBytes(signingPrivateKeyBase64);
6246
6314
  this.loginResult = await client.auth.loginWithKey({ email, signingPrivateKey });
6247
6315
  }
6316
+ /** A workspace's key, sealed to the caller's own session (what `/open` and `POST /workspace/users` take). */
6317
+ async sealWorkspaceKey(workspaceId) {
6318
+ const client = this.requireClient();
6319
+ const lr2 = this.requireLogin();
6320
+ const all = await listWorkspaces(client);
6321
+ const ws = all.find((w2) => w2.external_id === workspaceId);
6322
+ if (!ws || !ws.wrapped_key) {
6323
+ throw new ArbiError(`Workspace ${workspaceId} not found or has no encryption key`);
6324
+ }
6325
+ return generateEncryptedWorkspaceKey(
6326
+ client,
6327
+ ws.wrapped_key,
6328
+ lr2.serverSessionKey,
6329
+ client.crypto.bytesToBase64(lr2.signingPrivateKey)
6330
+ );
6331
+ }
6248
6332
  /**
6249
6333
  * Select a workspace by ID. Fetches the workspace list, finds the matching
6250
6334
  * workspace, decrypts the wrapped key, and sets up auth headers.
@@ -6271,6 +6355,52 @@ var Arbi = class {
6271
6355
  });
6272
6356
  this.currentWorkspaceId = ws.external_id;
6273
6357
  }
6358
+ /**
6359
+ * Subscribe to "the session cannot be recovered". Fires when a request failed with 401 or
6360
+ * "No active workspace" and neither a workspace re-open nor a silent re-login was possible
6361
+ * (the server refused; an unreachable server does not fire). The local session is already
6362
+ * cleared when it fires: show "Your session expired, sign in again". Returns an unsubscribe.
6363
+ */
6364
+ onSessionExpired(listener) {
6365
+ this.sessionExpiredListeners.add(listener);
6366
+ return () => this.sessionExpiredListeners.delete(listener);
6367
+ }
6368
+ expireSession() {
6369
+ if (this.loginResult) {
6370
+ this.loginResult.signingPrivateKey.fill(0);
6371
+ this.loginResult.serverSessionKey.fill(0);
6372
+ }
6373
+ this.loginResult = null;
6374
+ this.currentWorkspaceId = null;
6375
+ this.dmCryptoContext = null;
6376
+ this.client?.session.clear();
6377
+ for (const listener of [...this.sessionExpiredListeners]) listener();
6378
+ }
6379
+ /**
6380
+ * Run a raw-fetch operation (streaming, multipart: no middleware) and, when it fails because
6381
+ * the token expired or the server lost the open workspace, recover once and run it again.
6382
+ * `run` must read the auth headers itself so the retry carries the fresh token.
6383
+ */
6384
+ async recovering(run) {
6385
+ if (this.client && tokenExpiresWithin(this.client.session.getState().accessToken, 30)) {
6386
+ await this.client.auth.relogin();
6387
+ }
6388
+ try {
6389
+ return await run();
6390
+ } catch (error) {
6391
+ const message = error instanceof Error ? error.message : "";
6392
+ const noWorkspace = /no active workspace/i.test(message);
6393
+ const unauthorised = /\b401\b|token has expired/i.test(message);
6394
+ if (!(error instanceof ArbiApiError) || !(noWorkspace || unauthorised)) throw error;
6395
+ const client = this.requireClient();
6396
+ const recovered = noWorkspace && await client.auth.reopenWorkspace() || await client.auth.relogin() !== null;
6397
+ if (!recovered) {
6398
+ this.expireSession();
6399
+ throw new ArbiSessionExpiredError();
6400
+ }
6401
+ return run();
6402
+ }
6403
+ }
6274
6404
  /** Log out and clear internal state. */
6275
6405
  async logout() {
6276
6406
  if (this.client) {
@@ -6381,6 +6511,30 @@ var Arbi = class {
6381
6511
  update: (body) => updateWorkspace(this.requireClient(), body),
6382
6512
  listUsers: () => listWorkspaceUsers(this.requireClient()),
6383
6513
  addUsers: (emails, role) => addWorkspaceUsers(this.requireClient(), emails, role),
6514
+ /**
6515
+ * Grant users a temporary key scoped to ONE session (identified by its public key), for
6516
+ * example an agent's run session minted by `agents.authorize()`. Nothing standing is created:
6517
+ * the grant expires with that session. Works for any workspace the caller holds a key for;
6518
+ * it does not need to be the open one.
6519
+ */
6520
+ grantTemporaryAccess: async (params) => {
6521
+ const client = this.requireClient();
6522
+ return grantTemporaryWorkspaceAccess(client, {
6523
+ emails: params.emails,
6524
+ workspaceExtId: params.workspaceId,
6525
+ workspaceKey: await this.sealWorkspaceKey(params.workspaceId),
6526
+ sessionPubkeyB64: params.sessionPubkeyB64,
6527
+ role: params.role
6528
+ });
6529
+ },
6530
+ /**
6531
+ * Add users (standing, permanent membership) to ANY workspace the caller holds a key for,
6532
+ * without opening it. `addUsers` does the same for the currently open workspace.
6533
+ */
6534
+ grantAccess: async (params) => addWorkspaceUsers(this.requireClient(), params.emails, params.role, {
6535
+ workspaceExtId: params.workspaceId,
6536
+ workspaceKey: await this.sealWorkspaceKey(params.workspaceId)
6537
+ }),
6384
6538
  removeUsers: (userIds) => removeWorkspaceUsers(this.requireClient(), userIds),
6385
6539
  setUserRole: (userIds, role) => setUserRole(this.requireClient(), userIds, role),
6386
6540
  /**
@@ -6437,16 +6591,18 @@ var Arbi = class {
6437
6591
  },
6438
6592
  uploadFile: (fileData, fileName, options) => {
6439
6593
  this.requireWorkspace();
6440
- return uploadFile(
6441
- this.getAuthHeaders(),
6442
- fileData,
6443
- fileName,
6444
- options?.folder ? { folder: options.folder } : void 0
6594
+ return this.recovering(
6595
+ () => uploadFile(
6596
+ this.getAuthHeaders(),
6597
+ fileData,
6598
+ fileName,
6599
+ options?.folder ? { folder: options.folder } : void 0
6600
+ )
6445
6601
  );
6446
6602
  },
6447
- download: (docId) => downloadDocument(this.getAuthHeaders(), docId),
6448
- getParsedContent: (docId, stage) => getParsedContent(this.getAuthHeaders(), docId, stage),
6449
- getThumbnails: (docIds) => getThumbnails(this.getAuthHeaders(), docIds)
6603
+ download: (docId) => this.recovering(() => downloadDocument(this.getAuthHeaders(), docId)),
6604
+ getParsedContent: (docId, stage) => this.recovering(() => getParsedContent(this.getAuthHeaders(), docId, stage)),
6605
+ getThumbnails: (docIds) => this.recovering(() => getThumbnails(this.getAuthHeaders(), docIds))
6450
6606
  };
6451
6607
  // ── Conversations ─────────────────────────────────────────────────────
6452
6608
  conversations = {
@@ -6465,16 +6621,17 @@ var Arbi = class {
6465
6621
  * Returns the accumulated result after the stream completes.
6466
6622
  */
6467
6623
  query: async (question, options) => {
6468
- const auth = this.getAuthHeaders();
6469
6624
  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
- });
6625
+ const response = await this.recovering(
6626
+ () => queryAssistant({
6627
+ ...this.getAuthHeaders(),
6628
+ workspaceId,
6629
+ question,
6630
+ docIds: options.docIds,
6631
+ previousResponseId: options.previousResponseId,
6632
+ model: options.model
6633
+ })
6634
+ );
6478
6635
  const callbacks = {
6479
6636
  onToken: options.onToken,
6480
6637
  onStreamStart: options.onStreamStart,
@@ -6493,6 +6650,8 @@ var Arbi = class {
6493
6650
  * Respond to an agent's question during a human-in-the-loop workflow.
6494
6651
  */
6495
6652
  respond: (assistantMessageExtId, answer) => respondToAgent(this.requireClient(), assistantMessageExtId, answer),
6653
+ /** The skills the caller can run (own + shared marketplace), metadata only. */
6654
+ listSkills: (options) => listSkills(this.requireClient(), options),
6496
6655
  /**
6497
6656
  * Search documents without LLM generation (retrieval only).
6498
6657
  */
@@ -6638,8 +6797,10 @@ var Arbi = class {
6638
6797
  list: (options) => listFiles(this.requireClient(), options),
6639
6798
  get: (fileId) => getFile(this.requireClient(), fileId),
6640
6799
  delete: (fileId) => deleteFile(this.requireClient(), fileId),
6641
- upload: (fileData, fileName, purpose) => uploadFile2(this.getAuthHeaders(), fileData, fileName, purpose),
6642
- getContent: (fileId) => getFileContent(this.getAuthHeaders(), fileId)
6800
+ upload: (fileData, fileName, purpose) => this.recovering(
6801
+ () => uploadFile2(this.getAuthHeaders(), fileData, fileName, purpose)
6802
+ ),
6803
+ getContent: (fileId) => this.recovering(() => getFileContent(this.getAuthHeaders(), fileId))
6643
6804
  };
6644
6805
  // ── Agents ──────────────────────────────────────────────────────────
6645
6806
  agents = {
@@ -6647,7 +6808,28 @@ var Arbi = class {
6647
6808
  create: (name) => createAgent(this.requireClient(), name),
6648
6809
  getConfig: (agentExtId) => getAgentConfig(this.requireClient(), agentExtId),
6649
6810
  listWorkspaces: (agentExtId) => listAgentWorkspaces(this.requireClient(), agentExtId),
6650
- delete: (agentExtIds) => deleteAgents(this.requireClient(), agentExtIds)
6811
+ delete: (agentExtIds) => deleteAgents(this.requireClient(), agentExtIds),
6812
+ /** Mint a TTL'd run session for an agent; see `grantTemporaryAccess` to also deposit a key. */
6813
+ authorize: (agentExtId, workspaceId, ttlSeconds) => authorizeAgent(this.requireClient(), agentExtId, workspaceId, ttlSeconds),
6814
+ authorizeLimits: () => getAuthorizeLimits(this.requireClient()),
6815
+ /**
6816
+ * Give an agent temporary, session-scoped access to a workspace: mint its run session, then
6817
+ * deposit the workspace key on it. Returns the session details (`ttl` in seconds).
6818
+ */
6819
+ grantTemporaryAccess: async (agentExtId, workspaceId, ttlSeconds) => {
6820
+ const session = await authorizeAgent(
6821
+ this.requireClient(),
6822
+ agentExtId,
6823
+ workspaceId,
6824
+ ttlSeconds
6825
+ );
6826
+ await this.workspaces.grantTemporaryAccess({
6827
+ workspaceId,
6828
+ emails: [session.agent_email],
6829
+ sessionPubkeyB64: session.session_pubkey_b64
6830
+ });
6831
+ return session;
6832
+ }
6651
6833
  };
6652
6834
  // ── Scheduled tasks ───────────────────────────────────────────────────
6653
6835
  tasks = {
@@ -6656,7 +6838,26 @@ var Arbi = class {
6656
6838
  get: (taskId) => getTask(this.requireClient(), taskId),
6657
6839
  update: (taskId, body) => updateTask(this.requireClient(), taskId, body),
6658
6840
  delete: (taskId) => deleteTask(this.requireClient(), taskId),
6659
- run: (taskId) => runTask(this.requireClient(), taskId)
6841
+ run: (taskId) => runTask(this.requireClient(), taskId),
6842
+ listRuns: (taskId) => listTaskRuns(this.requireClient(), taskId),
6843
+ /**
6844
+ * Run a task and, when the run is parked waiting for workspace access (a temporary
6845
+ * authorization tier), grant the agent's run session a temporary key and run it again.
6846
+ * A task that needs no approval returns from the first call already running.
6847
+ */
6848
+ runWithAuth: async (taskId) => {
6849
+ const client = this.requireClient();
6850
+ const first = await runTask(client, taskId);
6851
+ if (!first.pending_auth_request_id || !first.pending_auth_session_pubkey || !first.pending_auth_agent_email || !first.workspace_ext_id) {
6852
+ return first;
6853
+ }
6854
+ await this.workspaces.grantTemporaryAccess({
6855
+ workspaceId: first.workspace_ext_id,
6856
+ emails: [first.pending_auth_agent_email],
6857
+ sessionPubkeyB64: first.pending_auth_session_pubkey
6858
+ });
6859
+ return runTask(client, taskId);
6860
+ }
6660
6861
  };
6661
6862
  // ── Workspace facts ─────────────────────────────────────────────────
6662
6863
  facts = {
@@ -7824,6 +8025,7 @@ async function startDmListener(options) {
7824
8025
  exports.Arbi = Arbi;
7825
8026
  exports.ArbiApiError = ArbiApiError;
7826
8027
  exports.ArbiError = ArbiError;
8028
+ exports.ArbiSessionExpiredError = ArbiSessionExpiredError;
7827
8029
  exports.ClaudeOrchestrator = ClaudeOrchestrator;
7828
8030
  exports.DOC_TERMINAL_STATUSES = DOC_TERMINAL_STATUSES;
7829
8031
  exports.FileConfigStore = FileConfigStore;