@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/browser.cjs CHANGED
@@ -3484,6 +3484,12 @@ var ArbiError = class extends Error {
3484
3484
  this.name = "ArbiError";
3485
3485
  }
3486
3486
  };
3487
+ var ArbiSessionExpiredError = class extends ArbiError {
3488
+ constructor(message = "Your session expired. Sign in again.") {
3489
+ super(message);
3490
+ this.name = "ArbiSessionExpiredError";
3491
+ }
3492
+ };
3487
3493
  var ArbiApiError = class extends ArbiError {
3488
3494
  apiError;
3489
3495
  constructor(message, apiError) {
@@ -3638,6 +3644,16 @@ async function selectWorkspace(arbi, workspaceId, wrappedKey, serverSessionKey,
3638
3644
  arbi.session.setSelectedWorkspace(workspaceId);
3639
3645
  return header;
3640
3646
  }
3647
+ async function generateEncryptedWorkspaceKey(arbi, wrappedKey, serverSessionKey, signingPrivateKeyBase64) {
3648
+ const signingPrivateKey = client.base64ToBytes(signingPrivateKeyBase64);
3649
+ const ed25519PublicKey = signingPrivateKey.slice(32, 64);
3650
+ const encryptionKeyPair = client.deriveEncryptionKeypairFromSigning({
3651
+ publicKey: ed25519PublicKey,
3652
+ secretKey: signingPrivateKey
3653
+ });
3654
+ const workspaceKey = client.sealedBoxDecrypt(wrappedKey, encryptionKeyPair.secretKey);
3655
+ return client.sealKeyForSession(workspaceKey, serverSessionKey);
3656
+ }
3641
3657
  async function getRawWorkspaceKey(arbi, workspaceId, signingPrivateKeyBase64) {
3642
3658
  const { data: workspaces, error } = await arbi.fetch.GET("/v1/user/workspaces");
3643
3659
  if (error || !workspaces) {
@@ -4660,6 +4676,7 @@ __export(workspaces_exports, {
4660
4676
  copyDocuments: () => copyDocuments,
4661
4677
  createWorkspace: () => createWorkspace,
4662
4678
  deleteWorkspaces: () => deleteWorkspaces,
4679
+ grantTemporaryWorkspaceAccess: () => grantTemporaryWorkspaceAccess,
4663
4680
  listWorkspaceUsers: () => listWorkspaceUsers,
4664
4681
  listWorkspaces: () => listWorkspaces,
4665
4682
  removeWorkspaceUsers: () => removeWorkspaceUsers,
@@ -4700,14 +4717,33 @@ async function updateWorkspace(arbi, body) {
4700
4717
  async function listWorkspaceUsers(arbi) {
4701
4718
  return requireData(await arbi.fetch.GET("/v1/workspace/users"), "Failed to fetch workspace users");
4702
4719
  }
4703
- async function addWorkspaceUsers(arbi, emails, role = "collaborator") {
4720
+ async function addWorkspaceUsers(arbi, emails, role = "collaborator", target) {
4704
4721
  return requireData(
4705
4722
  await arbi.fetch.POST("/v1/workspace/users", {
4706
- body: { emails, role }
4723
+ body: target ? {
4724
+ emails,
4725
+ role,
4726
+ workspace_ext_id: target.workspaceExtId,
4727
+ workspace_key: target.workspaceKey
4728
+ } : { emails, role }
4707
4729
  }),
4708
4730
  "Failed to add users"
4709
4731
  );
4710
4732
  }
4733
+ async function grantTemporaryWorkspaceAccess(arbi, params) {
4734
+ return requireData(
4735
+ await arbi.fetch.POST("/v1/workspace/users", {
4736
+ body: {
4737
+ emails: params.emails,
4738
+ role: params.role ?? "collaborator",
4739
+ workspace_ext_id: params.workspaceExtId,
4740
+ workspace_key: params.workspaceKey,
4741
+ session_pubkey_b64: params.sessionPubkeyB64
4742
+ }
4743
+ }),
4744
+ "Failed to grant temporary access"
4745
+ );
4746
+ }
4711
4747
  async function removeWorkspaceUsers(arbi, userIds) {
4712
4748
  requireOk(
4713
4749
  await arbi.fetch.DELETE("/v1/workspace/users", {
@@ -5132,6 +5168,21 @@ async function deleteAgents(arbi, agentExtIds) {
5132
5168
  "Failed to delete agents"
5133
5169
  );
5134
5170
  }
5171
+ async function authorizeAgent(arbi, agentExtId, workspaceExtId, ttlSeconds) {
5172
+ return requireData(
5173
+ await arbi.fetch.POST("/v1/agents/{agent_ext_id}/authorize", {
5174
+ params: { path: { agent_ext_id: agentExtId } },
5175
+ body: { workspace_ext_id: workspaceExtId, ttl_seconds: ttlSeconds ?? null }
5176
+ }),
5177
+ "Failed to authorize agent"
5178
+ );
5179
+ }
5180
+ async function getAuthorizeLimits(arbi) {
5181
+ return requireData(
5182
+ await arbi.fetch.GET("/v1/agents/authorize-limits"),
5183
+ "Failed to fetch authorize limits"
5184
+ );
5185
+ }
5135
5186
 
5136
5187
  // src/operations/dm.ts
5137
5188
  function createDmCryptoContext(arbi, signingPrivateKeyBytes, userExtId) {
@@ -5514,6 +5565,7 @@ __export(tasks_exports, {
5514
5565
  createTask: () => createTask,
5515
5566
  deleteTask: () => deleteTask,
5516
5567
  getTask: () => getTask,
5568
+ listTaskRuns: () => listTaskRuns,
5517
5569
  listTasks: () => listTasks,
5518
5570
  runTask: () => runTask,
5519
5571
  updateTask: () => updateTask
@@ -5551,6 +5603,12 @@ async function runTask(arbi, taskId) {
5551
5603
  "Failed to run task"
5552
5604
  );
5553
5605
  }
5606
+ async function listTaskRuns(arbi, taskId) {
5607
+ return requireData(
5608
+ await arbi.fetch.GET("/v1/tasks/{task_id}/runs", { params: { path: { task_id: taskId } } }),
5609
+ "Failed to list task runs"
5610
+ );
5611
+ }
5554
5612
 
5555
5613
  // src/operations/facts.ts
5556
5614
  var facts_exports = {};
@@ -5620,11 +5678,23 @@ async function remindFact(arbi, factId, options = {}) {
5620
5678
  }
5621
5679
 
5622
5680
  // src/arbi.ts
5681
+ function tokenExpiresWithin(token, seconds) {
5682
+ const payload = token?.split(".")[1];
5683
+ if (!payload) return false;
5684
+ try {
5685
+ const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
5686
+ const { exp } = JSON.parse(json);
5687
+ return typeof exp === "number" && exp * 1e3 - Date.now() < seconds * 1e3;
5688
+ } catch {
5689
+ return false;
5690
+ }
5691
+ }
5623
5692
  var Arbi = class {
5624
5693
  client = null;
5625
5694
  loginResult = null;
5626
5695
  currentWorkspaceId = null;
5627
5696
  dmCryptoContext = null;
5697
+ sessionExpiredListeners = /* @__PURE__ */ new Set();
5628
5698
  options;
5629
5699
  constructor(options) {
5630
5700
  this.options = options;
@@ -5637,7 +5707,13 @@ var Arbi = class {
5637
5707
  this.client = client.createArbiClient({
5638
5708
  baseUrl: this.options.url,
5639
5709
  deploymentDomain: domain,
5640
- credentials: this.options.credentials ?? "omit"
5710
+ credentials: this.options.credentials ?? "omit",
5711
+ // A silent re-login rotates the server session key: keep ours in step, or the next
5712
+ // workspace open would seal the key for a session the server no longer has.
5713
+ onReloginSuccess: ({ serverSessionKey }) => {
5714
+ if (this.loginResult) this.loginResult = { ...this.loginResult, serverSessionKey };
5715
+ },
5716
+ onSessionExpired: () => this.expireSession()
5641
5717
  });
5642
5718
  await this.client.crypto.initSodium();
5643
5719
  return this.client;
@@ -5674,6 +5750,22 @@ var Arbi = class {
5674
5750
  const signingPrivateKey = client.crypto.base64ToBytes(signingPrivateKeyBase64);
5675
5751
  this.loginResult = await client.auth.loginWithKey({ email, signingPrivateKey });
5676
5752
  }
5753
+ /** A workspace's key, sealed to the caller's own session (what `/open` and `POST /workspace/users` take). */
5754
+ async sealWorkspaceKey(workspaceId) {
5755
+ const client = this.requireClient();
5756
+ const lr2 = this.requireLogin();
5757
+ const all = await listWorkspaces(client);
5758
+ const ws = all.find((w2) => w2.external_id === workspaceId);
5759
+ if (!ws || !ws.wrapped_key) {
5760
+ throw new ArbiError(`Workspace ${workspaceId} not found or has no encryption key`);
5761
+ }
5762
+ return generateEncryptedWorkspaceKey(
5763
+ client,
5764
+ ws.wrapped_key,
5765
+ lr2.serverSessionKey,
5766
+ client.crypto.bytesToBase64(lr2.signingPrivateKey)
5767
+ );
5768
+ }
5677
5769
  /**
5678
5770
  * Select a workspace by ID. Fetches the workspace list, finds the matching
5679
5771
  * workspace, decrypts the wrapped key, and sets up auth headers.
@@ -5700,6 +5792,52 @@ var Arbi = class {
5700
5792
  });
5701
5793
  this.currentWorkspaceId = ws.external_id;
5702
5794
  }
5795
+ /**
5796
+ * Subscribe to "the session cannot be recovered". Fires when a request failed with 401 or
5797
+ * "No active workspace" and neither a workspace re-open nor a silent re-login was possible
5798
+ * (the server refused; an unreachable server does not fire). The local session is already
5799
+ * cleared when it fires: show "Your session expired, sign in again". Returns an unsubscribe.
5800
+ */
5801
+ onSessionExpired(listener) {
5802
+ this.sessionExpiredListeners.add(listener);
5803
+ return () => this.sessionExpiredListeners.delete(listener);
5804
+ }
5805
+ expireSession() {
5806
+ if (this.loginResult) {
5807
+ this.loginResult.signingPrivateKey.fill(0);
5808
+ this.loginResult.serverSessionKey.fill(0);
5809
+ }
5810
+ this.loginResult = null;
5811
+ this.currentWorkspaceId = null;
5812
+ this.dmCryptoContext = null;
5813
+ this.client?.session.clear();
5814
+ for (const listener of [...this.sessionExpiredListeners]) listener();
5815
+ }
5816
+ /**
5817
+ * Run a raw-fetch operation (streaming, multipart: no middleware) and, when it fails because
5818
+ * the token expired or the server lost the open workspace, recover once and run it again.
5819
+ * `run` must read the auth headers itself so the retry carries the fresh token.
5820
+ */
5821
+ async recovering(run) {
5822
+ if (this.client && tokenExpiresWithin(this.client.session.getState().accessToken, 30)) {
5823
+ await this.client.auth.relogin();
5824
+ }
5825
+ try {
5826
+ return await run();
5827
+ } catch (error) {
5828
+ const message = error instanceof Error ? error.message : "";
5829
+ const noWorkspace = /no active workspace/i.test(message);
5830
+ const unauthorised = /\b401\b|token has expired/i.test(message);
5831
+ if (!(error instanceof ArbiApiError) || !(noWorkspace || unauthorised)) throw error;
5832
+ const client = this.requireClient();
5833
+ const recovered = noWorkspace && await client.auth.reopenWorkspace() || await client.auth.relogin() !== null;
5834
+ if (!recovered) {
5835
+ this.expireSession();
5836
+ throw new ArbiSessionExpiredError();
5837
+ }
5838
+ return run();
5839
+ }
5840
+ }
5703
5841
  /** Log out and clear internal state. */
5704
5842
  async logout() {
5705
5843
  if (this.client) {
@@ -5810,6 +5948,30 @@ var Arbi = class {
5810
5948
  update: (body) => updateWorkspace(this.requireClient(), body),
5811
5949
  listUsers: () => listWorkspaceUsers(this.requireClient()),
5812
5950
  addUsers: (emails, role) => addWorkspaceUsers(this.requireClient(), emails, role),
5951
+ /**
5952
+ * Grant users a temporary key scoped to ONE session (identified by its public key), for
5953
+ * example an agent's run session minted by `agents.authorize()`. Nothing standing is created:
5954
+ * the grant expires with that session. Works for any workspace the caller holds a key for;
5955
+ * it does not need to be the open one.
5956
+ */
5957
+ grantTemporaryAccess: async (params) => {
5958
+ const client = this.requireClient();
5959
+ return grantTemporaryWorkspaceAccess(client, {
5960
+ emails: params.emails,
5961
+ workspaceExtId: params.workspaceId,
5962
+ workspaceKey: await this.sealWorkspaceKey(params.workspaceId),
5963
+ sessionPubkeyB64: params.sessionPubkeyB64,
5964
+ role: params.role
5965
+ });
5966
+ },
5967
+ /**
5968
+ * Add users (standing, permanent membership) to ANY workspace the caller holds a key for,
5969
+ * without opening it. `addUsers` does the same for the currently open workspace.
5970
+ */
5971
+ grantAccess: async (params) => addWorkspaceUsers(this.requireClient(), params.emails, params.role, {
5972
+ workspaceExtId: params.workspaceId,
5973
+ workspaceKey: await this.sealWorkspaceKey(params.workspaceId)
5974
+ }),
5813
5975
  removeUsers: (userIds) => removeWorkspaceUsers(this.requireClient(), userIds),
5814
5976
  setUserRole: (userIds, role) => setUserRole(this.requireClient(), userIds, role),
5815
5977
  /**
@@ -5866,16 +6028,18 @@ var Arbi = class {
5866
6028
  },
5867
6029
  uploadFile: (fileData, fileName, options) => {
5868
6030
  this.requireWorkspace();
5869
- return uploadFile(
5870
- this.getAuthHeaders(),
5871
- fileData,
5872
- fileName,
5873
- options?.folder ? { folder: options.folder } : void 0
6031
+ return this.recovering(
6032
+ () => uploadFile(
6033
+ this.getAuthHeaders(),
6034
+ fileData,
6035
+ fileName,
6036
+ options?.folder ? { folder: options.folder } : void 0
6037
+ )
5874
6038
  );
5875
6039
  },
5876
- download: (docId) => downloadDocument(this.getAuthHeaders(), docId),
5877
- getParsedContent: (docId, stage) => getParsedContent(this.getAuthHeaders(), docId, stage),
5878
- getThumbnails: (docIds) => getThumbnails(this.getAuthHeaders(), docIds)
6040
+ download: (docId) => this.recovering(() => downloadDocument(this.getAuthHeaders(), docId)),
6041
+ getParsedContent: (docId, stage) => this.recovering(() => getParsedContent(this.getAuthHeaders(), docId, stage)),
6042
+ getThumbnails: (docIds) => this.recovering(() => getThumbnails(this.getAuthHeaders(), docIds))
5879
6043
  };
5880
6044
  // ── Conversations ─────────────────────────────────────────────────────
5881
6045
  conversations = {
@@ -5894,16 +6058,17 @@ var Arbi = class {
5894
6058
  * Returns the accumulated result after the stream completes.
5895
6059
  */
5896
6060
  query: async (question, options) => {
5897
- const auth = this.getAuthHeaders();
5898
6061
  const workspaceId = this.requireWorkspace();
5899
- const response = await queryAssistant({
5900
- ...auth,
5901
- workspaceId,
5902
- question,
5903
- docIds: options.docIds,
5904
- previousResponseId: options.previousResponseId,
5905
- model: options.model
5906
- });
6062
+ const response = await this.recovering(
6063
+ () => queryAssistant({
6064
+ ...this.getAuthHeaders(),
6065
+ workspaceId,
6066
+ question,
6067
+ docIds: options.docIds,
6068
+ previousResponseId: options.previousResponseId,
6069
+ model: options.model
6070
+ })
6071
+ );
5907
6072
  const callbacks = {
5908
6073
  onToken: options.onToken,
5909
6074
  onStreamStart: options.onStreamStart,
@@ -5922,6 +6087,8 @@ var Arbi = class {
5922
6087
  * Respond to an agent's question during a human-in-the-loop workflow.
5923
6088
  */
5924
6089
  respond: (assistantMessageExtId, answer) => respondToAgent(this.requireClient(), assistantMessageExtId, answer),
6090
+ /** The skills the caller can run (own + shared marketplace), metadata only. */
6091
+ listSkills: (options) => listSkills(this.requireClient(), options),
5925
6092
  /**
5926
6093
  * Search documents without LLM generation (retrieval only).
5927
6094
  */
@@ -6067,8 +6234,10 @@ var Arbi = class {
6067
6234
  list: (options) => listFiles(this.requireClient(), options),
6068
6235
  get: (fileId) => getFile(this.requireClient(), fileId),
6069
6236
  delete: (fileId) => deleteFile(this.requireClient(), fileId),
6070
- upload: (fileData, fileName, purpose) => uploadFile2(this.getAuthHeaders(), fileData, fileName, purpose),
6071
- getContent: (fileId) => getFileContent(this.getAuthHeaders(), fileId)
6237
+ upload: (fileData, fileName, purpose) => this.recovering(
6238
+ () => uploadFile2(this.getAuthHeaders(), fileData, fileName, purpose)
6239
+ ),
6240
+ getContent: (fileId) => this.recovering(() => getFileContent(this.getAuthHeaders(), fileId))
6072
6241
  };
6073
6242
  // ── Agents ──────────────────────────────────────────────────────────
6074
6243
  agents = {
@@ -6076,7 +6245,28 @@ var Arbi = class {
6076
6245
  create: (name) => createAgent(this.requireClient(), name),
6077
6246
  getConfig: (agentExtId) => getAgentConfig(this.requireClient(), agentExtId),
6078
6247
  listWorkspaces: (agentExtId) => listAgentWorkspaces(this.requireClient(), agentExtId),
6079
- delete: (agentExtIds) => deleteAgents(this.requireClient(), agentExtIds)
6248
+ delete: (agentExtIds) => deleteAgents(this.requireClient(), agentExtIds),
6249
+ /** Mint a TTL'd run session for an agent; see `grantTemporaryAccess` to also deposit a key. */
6250
+ authorize: (agentExtId, workspaceId, ttlSeconds) => authorizeAgent(this.requireClient(), agentExtId, workspaceId, ttlSeconds),
6251
+ authorizeLimits: () => getAuthorizeLimits(this.requireClient()),
6252
+ /**
6253
+ * Give an agent temporary, session-scoped access to a workspace: mint its run session, then
6254
+ * deposit the workspace key on it. Returns the session details (`ttl` in seconds).
6255
+ */
6256
+ grantTemporaryAccess: async (agentExtId, workspaceId, ttlSeconds) => {
6257
+ const session = await authorizeAgent(
6258
+ this.requireClient(),
6259
+ agentExtId,
6260
+ workspaceId,
6261
+ ttlSeconds
6262
+ );
6263
+ await this.workspaces.grantTemporaryAccess({
6264
+ workspaceId,
6265
+ emails: [session.agent_email],
6266
+ sessionPubkeyB64: session.session_pubkey_b64
6267
+ });
6268
+ return session;
6269
+ }
6080
6270
  };
6081
6271
  // ── Scheduled tasks ───────────────────────────────────────────────────
6082
6272
  tasks = {
@@ -6085,7 +6275,26 @@ var Arbi = class {
6085
6275
  get: (taskId) => getTask(this.requireClient(), taskId),
6086
6276
  update: (taskId, body) => updateTask(this.requireClient(), taskId, body),
6087
6277
  delete: (taskId) => deleteTask(this.requireClient(), taskId),
6088
- run: (taskId) => runTask(this.requireClient(), taskId)
6278
+ run: (taskId) => runTask(this.requireClient(), taskId),
6279
+ listRuns: (taskId) => listTaskRuns(this.requireClient(), taskId),
6280
+ /**
6281
+ * Run a task and, when the run is parked waiting for workspace access (a temporary
6282
+ * authorization tier), grant the agent's run session a temporary key and run it again.
6283
+ * A task that needs no approval returns from the first call already running.
6284
+ */
6285
+ runWithAuth: async (taskId) => {
6286
+ const client = this.requireClient();
6287
+ const first = await runTask(client, taskId);
6288
+ if (!first.pending_auth_request_id || !first.pending_auth_session_pubkey || !first.pending_auth_agent_email || !first.workspace_ext_id) {
6289
+ return first;
6290
+ }
6291
+ await this.workspaces.grantTemporaryAccess({
6292
+ workspaceId: first.workspace_ext_id,
6293
+ emails: [first.pending_auth_agent_email],
6294
+ sessionPubkeyB64: first.pending_auth_session_pubkey
6295
+ });
6296
+ return runTask(client, taskId);
6297
+ }
6089
6298
  };
6090
6299
  // ── Workspace facts ─────────────────────────────────────────────────
6091
6300
  facts = {