@ziggs-ai/api-client 0.14.3 → 0.16.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.
Files changed (46) hide show
  1. package/dist/capabilities/agreementVerbs.d.ts +9 -0
  2. package/dist/capabilities/agreementVerbs.js +29 -3
  3. package/dist/capabilities/agreements.js +2 -1
  4. package/dist/capabilities/artifacts.js +16 -6
  5. package/dist/capabilities/connections.d.ts +1 -1
  6. package/dist/capabilities/connections.js +82 -59
  7. package/dist/capabilities/grants.js +8 -2
  8. package/dist/capabilities/index.d.ts +1 -0
  9. package/dist/capabilities/index.js +1 -0
  10. package/dist/capabilities/links.js +12 -4
  11. package/dist/capabilities/listedFields.d.ts +17 -0
  12. package/dist/capabilities/listedFields.js +45 -0
  13. package/dist/capabilities/marketplace.js +7 -2
  14. package/dist/capabilities/payments.js +7 -2
  15. package/dist/capabilities/tasks.js +9 -3
  16. package/dist/http/AgreementClient.d.ts +29 -6
  17. package/dist/http/AgreementClient.js +2 -0
  18. package/dist/http/ArtifactsClient.d.ts +5 -16
  19. package/dist/http/ArtifactsClient.js +8 -5
  20. package/dist/http/ChatClient.d.ts +2 -6
  21. package/dist/http/ConnectionsClient.d.ts +15 -4
  22. package/dist/http/ConnectionsClient.js +53 -39
  23. package/dist/http/GrantsClient.d.ts +10 -1
  24. package/dist/http/GrantsClient.js +12 -2
  25. package/dist/http/IntroductionsClient.d.ts +11 -4
  26. package/dist/http/MarketplaceClient.d.ts +7 -0
  27. package/dist/http/MarketplaceClient.js +12 -3
  28. package/dist/http/OrgsClient.d.ts +0 -14
  29. package/dist/http/OrgsClient.js +2 -17
  30. package/dist/http/PaymentsClient.d.ts +47 -5
  31. package/dist/http/PaymentsClient.js +66 -22
  32. package/dist/http/TaskClient.js +1 -1
  33. package/dist/http/agreementFlows.js +9 -3
  34. package/dist/http/index.d.ts +1 -1
  35. package/dist/http/index.js +1 -1
  36. package/dist/http/paymentGrantSelection.d.ts +58 -0
  37. package/dist/http/paymentGrantSelection.js +121 -0
  38. package/dist/index.d.ts +3 -0
  39. package/dist/index.js +3 -0
  40. package/dist/shared/operatorKey.d.ts +47 -0
  41. package/dist/shared/operatorKey.js +64 -0
  42. package/dist/types.d.ts +6 -26
  43. package/dist/types.js +1 -19
  44. package/dist/utils/appUrls.d.ts +7 -0
  45. package/dist/utils/appUrls.js +38 -0
  46. package/package.json +3 -2
@@ -1,5 +1,6 @@
1
1
  import { listTasks } from '../http/TaskClient.js';
2
2
  import { fullCreds } from './types.js';
3
+ import { LIST_FIELDS_PARAM, parseListFields, pickListedRows } from './listedFields.js';
3
4
  /**
4
5
  * One list-tasks verb. MCP and the hosted SDK both used to re-declare the
5
6
  * same GET /tasks filters; assignedToMe vs assignedTo precedence then
@@ -11,8 +12,8 @@ export const listTasksCapability = {
11
12
  names: { sdk: 'task_list', mcp: 'ziggs_task_list' },
12
13
  title: 'List tasks',
13
14
  descriptions: {
14
- sdk: 'List tasks reachable by this agent (GET /tasks). Scope follows the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else.',
15
- mcp: 'List tasks reachable by the acting agent (GET /tasks). Scope is determined by the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else.',
15
+ sdk: 'List tasks reachable by this agent (GET /tasks). Scope follows the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else. Pass fields to keep only those keys on each row.',
16
+ mcp: 'List tasks reachable by the acting agent (GET /tasks). Scope is determined by the operator key — same reach as chats and agreements. Optional state / assignee filters and cursor pagination. Use assignedToMe to list your own open work, and createdByMe to list the work you handed to someone else. Pass fields to keep only those keys on each row.',
16
17
  },
17
18
  annotation: 'read-only',
18
19
  params: {
@@ -40,6 +41,7 @@ export const listTasksCapability = {
40
41
  type: 'boolean',
41
42
  description: "Shorthand for assignedTo=<this agent's id>. Takes precedence over assignedTo when both are set.",
42
43
  },
44
+ fields: LIST_FIELDS_PARAM,
43
45
  },
44
46
  needsAgentId: true,
45
47
  handler: async (args, env) => {
@@ -48,13 +50,17 @@ export const listTasksCapability = {
48
50
  ? creds.agentId
49
51
  : args['assignedTo'];
50
52
  const createdBy = args['createdByMe'] ? creds.agentId : undefined;
51
- return listTasks({
53
+ const listed = await listTasks({
52
54
  state: args['state'],
53
55
  cursor: args['cursor'],
54
56
  limit: typeof args['limit'] === 'number' ? args['limit'] : undefined,
55
57
  assignedTo,
56
58
  ...(createdBy ? { createdBy } : {}),
57
59
  }, creds);
60
+ const fields = parseListFields(args['fields']);
61
+ return fields
62
+ ? { ...listed, tasks: pickListedRows(listed.tasks, fields) }
63
+ : listed;
58
64
  },
59
65
  };
60
66
  export const TASK_CAPABILITIES = [listTasksCapability];
@@ -34,7 +34,17 @@ export interface ProposeTerms {
34
34
  /** 1:1 proposal to a specific user or agent (`proposedTo` = their id). */
35
35
  export interface ProposeDirectInput extends ProposeTerms {
36
36
  proposedTo: string;
37
- chatId: string;
37
+ /**
38
+ * The room the terms are discussed in, when there is one.
39
+ *
40
+ * Optional, matching the server, which has always accepted a proposal
41
+ * without one. Delivery does not depend on it: the create-time agreement
42
+ * event targets the party slots, so the counterparty is told either way; a
43
+ * room only gives them somewhere to talk about it. Required here was what
44
+ * forced a bookkeeping agreement between parties with no conversation to
45
+ * invent a chat id in order to exist.
46
+ */
47
+ chatId?: string;
38
48
  }
39
49
  /**
40
50
  * Open buyer-broadcast. `audience` selects who may see/claim it:
@@ -205,16 +215,29 @@ export interface CreateLinkBody {
205
215
  maxClaims?: number;
206
216
  }
207
217
  /**
208
- * The route's one answer, whatever was in `to`.
218
+ * The route's answer.
219
+ *
220
+ * `status` is constant — it went — and the two null-able fields are per-ARM,
221
+ * chosen by what the caller put in `to` and therefore known to it before the
222
+ * call. `shareUrl` is present exactly for an open invite.
223
+ *
224
+ * `agreementId` names the row for an agent id and for an open invite, and is
225
+ * null for an EMAIL. That split is what keeps the answer from becoming an
226
+ * existence oracle: the knock returns nothing for "no such person", "already
227
+ * linked", "already knocked" and "that is yourself" alike, so an id on some of
228
+ * those and not others would answer the one question the route refuses to
229
+ * answer. An agent id is different — the route resolves it through root
230
+ * authority and refuses a name that answers to nobody, so it already discloses
231
+ * that target's existence.
209
232
  *
210
- * Deliberately carries no agreement. Returning the row for an id target and a
211
- * bare "sent" for an address made one of the two an existence oracle over the
212
- * user table, so both say the same thing now: it went. `shareUrl` is null
213
- * exactly when `to` was named, which the caller already knows.
233
+ * Without it a caller could not reference what it had just created, check on it
234
+ * later, or tell a repeat from a first attempt.
214
235
  */
215
236
  export interface CreateLinkResult {
216
237
  ok: boolean;
217
238
  status: 'sent';
239
+ /** The link row, for a named agent id or an open invite. Null for an email. */
240
+ agreementId: string | null;
218
241
  shareUrl: string | null;
219
242
  }
220
243
  /**
@@ -444,9 +444,11 @@ export async function createLink(body, creds) {
444
444
  throw new Error('Invalid response: expected { ok, status: "sent", shareUrl } from POST /agreements/links');
445
445
  }
446
446
  const shareUrl = data['shareUrl'];
447
+ const agreementId = data['agreementId'];
447
448
  return {
448
449
  ok: data['ok'] === true,
449
450
  status: 'sent',
451
+ agreementId: typeof agreementId === 'string' ? agreementId : null,
450
452
  shareUrl: typeof shareUrl === 'string' ? shareUrl : null,
451
453
  };
452
454
  }
@@ -28,6 +28,8 @@ export interface ListArtifactsResult {
28
28
  }
29
29
  export interface WriteArtifactInput {
30
30
  text: string;
31
+ /** Short list name. An upload defaults to filename when omitted. */
32
+ name?: string;
31
33
  contentType?: string;
32
34
  visibility?: ArtifactVisibility;
33
35
  /** When the artifact is associated with a chat. */
@@ -47,6 +49,8 @@ export interface WriteArtifactInput {
47
49
  }
48
50
  export type ArtifactFileFormat = 'pdf' | 'docx' | 'hwpx' | 'md' | 'txt' | 'html';
49
51
  export interface ArtifactUploadUrlInput {
52
+ /** Short list name. Defaults to filename on the server when omitted. */
53
+ name?: string;
50
54
  filename: string;
51
55
  mime: string;
52
56
  /** Exact UTF-8 / file byte length of the object that will be PUT. */
@@ -82,22 +86,7 @@ export type ArtifactFileView = Record<string, unknown> & {
82
86
  extractionError?: string | null;
83
87
  filename?: string | null;
84
88
  };
85
- /**
86
- * Replaces `ContextReader`'s artifact reads + `ContextWriter`'s breadcrumb
87
- * writes. `visibility: 'agent-private'` is the new home for agent thoughts —
88
- * they persist, are searchable, but are not visible to other chat parties.
89
- */
90
- /**
91
- * turn a runtime lane id into a scope the backend can accept.
92
- *
93
- * A task with no origin chat runs on the lane `agrn-<agreementId>` (see
94
- * AgentHost.laneSessionIdForTask). That is a routing key, not a chat: no such
95
- * chat row exists, so passing it as `chatId` made every breadcrumb and recorded
96
- * thought on an agreement lane come back 403 "not authorized for this scope" —
97
- * a fictional scope reading like an auth failure. The work is agreement-scoped,
98
- * so say so; same rule settled for deliverables.
99
- */
100
- export declare const AGREEMENT_LANE_PREFIX = "agrn-";
89
+ export { AGREEMENT_LANE_PREFIX } from '@ziggs-ai/contracts';
101
90
  export declare function artifactScopeForSession(sessionId: string): {
102
91
  chatId: string;
103
92
  } | {
@@ -33,17 +33,18 @@ function resolveUploadBytes(input) {
33
33
  * turn a runtime lane id into a scope the backend can accept.
34
34
  *
35
35
  * A task with no origin chat runs on the lane `agrn-<agreementId>` (see
36
- * AgentHost.laneSessionIdForTask). That is a routing key, not a chat: no such
36
+ * Agent.laneSessionIdForTask). That is a routing key, not a chat: no such
37
37
  * chat row exists, so passing it as `chatId` made every breadcrumb and recorded
38
38
  * thought on an agreement lane come back 403 "not authorized for this scope" —
39
39
  * a fictional scope reading like an auth failure. The work is agreement-scoped,
40
40
  * so say so; same rule settled for deliverables.
41
41
  */
42
- export const AGREEMENT_LANE_PREFIX = 'agrn-';
42
+ import { agreementIdFromLane } from '@ziggs-ai/contracts';
43
+ export { AGREEMENT_LANE_PREFIX } from '@ziggs-ai/contracts';
43
44
  export function artifactScopeForSession(sessionId) {
44
- if (sessionId.startsWith(AGREEMENT_LANE_PREFIX)) {
45
- return { agreementId: sessionId.slice(AGREEMENT_LANE_PREFIX.length) };
46
- }
45
+ const agreementId = agreementIdFromLane(sessionId);
46
+ if (agreementId)
47
+ return { agreementId };
47
48
  return { chatId: sessionId };
48
49
  }
49
50
  export class ArtifactsClient {
@@ -145,6 +146,7 @@ export class ArtifactsClient {
145
146
  headers: this._headers(),
146
147
  body: JSON.stringify({
147
148
  text,
149
+ ...(input.name?.trim() ? { name: input.name.trim() } : {}),
148
150
  contentType: input.contentType ?? 'text',
149
151
  visibility: input.visibility ?? 'chat',
150
152
  chatId: input.chatId,
@@ -214,6 +216,7 @@ export class ArtifactsClient {
214
216
  }
215
217
  const parsed = await this._post('/artifacts/upload-url', {
216
218
  filename: input.filename.trim(),
219
+ ...(input.name?.trim() ? { name: input.name.trim() } : {}),
217
220
  mime: input.mime.trim(),
218
221
  byteSize: input.byteSize,
219
222
  format: input.format,
@@ -1,3 +1,4 @@
1
+ import type { SendChatMessageResult } from '@ziggs-ai/contracts';
1
2
  import { type Creds } from '../types.js';
2
3
  /** Shape of GET /chats/mine items — the backend's ChatReadDto. */
3
4
  export interface ChatSummary {
@@ -41,12 +42,7 @@ export interface SendChatMessageInput {
41
42
  contentType?: string;
42
43
  underAgreementId?: string;
43
44
  }
44
- export interface SendChatMessageResult {
45
- success: boolean;
46
- message: string;
47
- messageId: string;
48
- chatId: string;
49
- }
45
+ export type { SendChatMessageResult } from '@ziggs-ai/contracts';
50
46
  export type ContextTemporal = 'from-now' | 'from-start';
51
47
  export interface AddChatMemberInput {
52
48
  chatId: string;
@@ -1,4 +1,4 @@
1
- import type { GrantView } from './grants.js';
1
+ import type { GrantView } from "./grants.js";
2
2
  export declare function assertNoLeakedConnectionSecret(serialized: string): void;
3
3
  /** Thrown by ConnectionsClient with the HTTP status and raw body attached. */
4
4
  export interface ConnectionsError extends Error {
@@ -56,19 +56,30 @@ export declare class ConnectionsClient {
56
56
  private readonly operatorKey;
57
57
  private readonly agentId?;
58
58
  private readonly baseUrl;
59
- constructor(operatorKey: string, agentId?: string, baseUrl?: string);
59
+ private readonly laneId?;
60
+ /**
61
+ * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
62
+ * X-Ziggs-Lane. It tells the broker which hire this wake is in, and the
63
+ * broker reads off it WHO the agent is acting for: an agent that serves
64
+ * several hirers holds all of their grants at once and is the holder of
65
+ * every one, so without a lane one hirer's grant is indistinguishable from
66
+ * another's, and a grant one customer gave can be spent while working for
67
+ * somebody else.
68
+ * ArtifactsClient sends the same header for the same reason.
69
+ */
70
+ constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
60
71
  /**
61
72
  * Present a ConnectionGrant to the broker and perform a provider action.
62
73
  * Returns the provider result — never the raw OAuth token. Requires an
63
74
  * impersonated agent (the grant holder).
64
75
  */
65
- proxy({ connectionId, grantId, action, payload }: ConnectionProxyParams): Promise<unknown>;
76
+ proxy({ connectionId, grantId, action, payload, }: ConnectionProxyParams): Promise<unknown>;
66
77
  /**
67
78
  * List ConnectionGrants on a connection. When impersonating an agent the
68
79
  * server already scopes rows to that holder; the client-side filter is kept
69
80
  * as defense-in-depth (same behavior as the former ZiggsConnectClient).
70
81
  */
71
- listGrants({ connectionId }: {
82
+ listGrants({ connectionId, }: {
72
83
  connectionId: string;
73
84
  }): Promise<ConnectionGrant[]>;
74
85
  /**
@@ -1,7 +1,7 @@
1
- import { getBackendUrl } from '../utils/urlUtils.js';
2
- import { throwApiError } from '../shared/apiError.js';
3
- import { buildOperatorHeaders } from './operatorHeaders.js';
4
- import { GrantsClient } from './GrantsClient.js';
1
+ import { getBackendUrl } from "../utils/urlUtils.js";
2
+ import { throwApiError } from "../shared/apiError.js";
3
+ import { buildOperatorHeaders } from "./operatorHeaders.js";
4
+ import { GrantsClient } from "./GrantsClient.js";
5
5
  // defense-in-depth mirror of the backend leak-guard
6
6
  // (assertProxyResponseDoesNotLeakTokens). The backend strips the *specific*
7
7
  // vault token from the response; clients never see that token, so this layer
@@ -17,7 +17,7 @@ const LEAKED_SECRET_PATTERNS = [
17
17
  export function assertNoLeakedConnectionSecret(serialized) {
18
18
  for (const re of LEAKED_SECRET_PATTERNS) {
19
19
  if (re.test(serialized)) {
20
- throw new Error('connection proxy response withheld: it appears to contain a credential (token-leak guard)');
20
+ throw new Error("connection proxy response withheld: it appears to contain a credential (token-leak guard)");
21
21
  }
22
22
  }
23
23
  }
@@ -34,29 +34,41 @@ export class ConnectionsClient {
34
34
  operatorKey;
35
35
  agentId;
36
36
  baseUrl;
37
- constructor(operatorKey, agentId, baseUrl) {
37
+ laneId;
38
+ /**
39
+ * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
40
+ * X-Ziggs-Lane. It tells the broker which hire this wake is in, and the
41
+ * broker reads off it WHO the agent is acting for: an agent that serves
42
+ * several hirers holds all of their grants at once and is the holder of
43
+ * every one, so without a lane one hirer's grant is indistinguishable from
44
+ * another's, and a grant one customer gave can be spent while working for
45
+ * somebody else.
46
+ * ArtifactsClient sends the same header for the same reason.
47
+ */
48
+ constructor(operatorKey, agentId, baseUrl, laneId) {
38
49
  if (!operatorKey)
39
- throw new Error('ConnectionsClient: operatorKey is required');
50
+ throw new Error("ConnectionsClient: operatorKey is required");
40
51
  this.operatorKey = operatorKey;
41
52
  this.agentId = agentId;
42
53
  this.baseUrl = baseUrl || getBackendUrl();
54
+ this.laneId = laneId;
43
55
  }
44
56
  /**
45
57
  * Present a ConnectionGrant to the broker and perform a provider action.
46
58
  * Returns the provider result — never the raw OAuth token. Requires an
47
59
  * impersonated agent (the grant holder).
48
60
  */
49
- async proxy({ connectionId, grantId, action, payload }) {
61
+ async proxy({ connectionId, grantId, action, payload, }) {
50
62
  if (!connectionId)
51
- throw new Error('proxy: connectionId is required');
63
+ throw new Error("proxy: connectionId is required");
52
64
  if (!grantId)
53
- throw new Error('proxy: grantId is required');
65
+ throw new Error("proxy: grantId is required");
54
66
  if (!action)
55
- throw new Error('proxy: action is required');
67
+ throw new Error("proxy: action is required");
56
68
  if (!this.agentId) {
57
- throw new Error('proxy: agentId is required — connection broker calls must impersonate the grant holder agent');
69
+ throw new Error("proxy: agentId is required — connection broker calls must impersonate the grant holder agent");
58
70
  }
59
- const text = await this._requestRaw('POST', `/connections/${encodeURIComponent(connectionId)}/proxy`, { grantId, action, payload: payload ?? {} });
71
+ const text = await this._requestRaw("POST", `/connections/${encodeURIComponent(connectionId)}/proxy`, { grantId, action, payload: payload ?? {} });
60
72
  assertNoLeakedConnectionSecret(text);
61
73
  let parsed = text;
62
74
  try {
@@ -65,7 +77,7 @@ export class ConnectionsClient {
65
77
  catch {
66
78
  parsed = text;
67
79
  }
68
- const result = parsed?.['result'];
80
+ const result = parsed?.["result"];
69
81
  return result ?? parsed;
70
82
  }
71
83
  /**
@@ -73,15 +85,15 @@ export class ConnectionsClient {
73
85
  * server already scopes rows to that holder; the client-side filter is kept
74
86
  * as defense-in-depth (same behavior as the former ZiggsConnectClient).
75
87
  */
76
- async listGrants({ connectionId }) {
88
+ async listGrants({ connectionId, }) {
77
89
  if (!connectionId)
78
- throw new Error('listGrants: connectionId is required');
90
+ throw new Error("listGrants: connectionId is required");
79
91
  if (!this.agentId) {
80
- throw new Error('listGrants: agentId is required — grants are scoped to the impersonated agent');
92
+ throw new Error("listGrants: agentId is required — grants are scoped to the impersonated agent");
81
93
  }
82
- const res = (await this._request('GET', `/connections/${encodeURIComponent(connectionId)}/grants`, undefined));
83
- const grants = res['grants'] || [];
84
- return grants.filter((g) => g['holderId'] === this.agentId);
94
+ const res = (await this._request("GET", `/connections/${encodeURIComponent(connectionId)}/grants`, undefined));
95
+ const grants = res["grants"] || [];
96
+ return grants.filter((g) => g["holderId"] === this.agentId);
85
97
  }
86
98
  /**
87
99
  * cross-connection discovery over the unified GET /grants:
@@ -91,18 +103,18 @@ export class ConnectionsClient {
91
103
  * defensively for leaked secrets, as `proxy` does.
92
104
  */
93
105
  async listForHolder() {
94
- const grantsClient = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl);
106
+ const grantsClient = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl, this.laneId);
95
107
  // All pages of the agent's live connection grants (not just the first page).
96
108
  const { items, unreadableRails } = await grantsClient.listAllGrantsWithRails({
97
- scopeKind: 'connection',
98
- health: 'active',
109
+ scopeKind: "connection",
110
+ health: "active",
99
111
  });
100
112
  // An unreadable rail is not an empty one. Returning [] here made
101
113
  // every caller say "no connection grant" when the truth was "this key may not
102
114
  // look" — the needs gate refused work the agent could do, and the MCP tool
103
115
  // told the owner to issue a grant that already existed. Throwing puts the
104
116
  // missing scope in the message, where the person who can fix it will read it.
105
- const railBlocked = unreadableRails.find((r) => r.rail === 'connection');
117
+ const railBlocked = unreadableRails.find((r) => r.rail === "connection");
106
118
  if (railBlocked && items.length === 0) {
107
119
  throw new Error(`Cannot read this agent's connection grants: the operator key is missing the ` +
108
120
  `"${railBlocked.requiredScope}" scope, so the grant list came back empty whether or not ` +
@@ -125,10 +137,10 @@ export class ConnectionsClient {
125
137
  /** Issue a connection grant to an agent holder (connection owner side). */
126
138
  async issueGrant({ connectionId, holderId, caveats, }) {
127
139
  if (!connectionId)
128
- throw new Error('issueGrant: connectionId is required');
140
+ throw new Error("issueGrant: connectionId is required");
129
141
  if (!holderId)
130
- throw new Error('issueGrant: holderId is required');
131
- return this._request('POST', `/connections/${encodeURIComponent(connectionId)}/grants`, {
142
+ throw new Error("issueGrant: holderId is required");
143
+ return this._request("POST", `/connections/${encodeURIComponent(connectionId)}/grants`, {
132
144
  holderId,
133
145
  caveats,
134
146
  });
@@ -141,20 +153,20 @@ export class ConnectionsClient {
141
153
  */
142
154
  async attenuateGrant({ connectionId, grantId, holderId, caveats, }) {
143
155
  if (!connectionId)
144
- throw new Error('attenuateGrant: connectionId is required');
156
+ throw new Error("attenuateGrant: connectionId is required");
145
157
  if (!grantId)
146
- throw new Error('attenuateGrant: grantId is required');
158
+ throw new Error("attenuateGrant: grantId is required");
147
159
  if (!holderId)
148
- throw new Error('attenuateGrant: holderId is required');
149
- return this._request('POST', `/connections/${encodeURIComponent(connectionId)}/grants/${encodeURIComponent(grantId)}/attenuate`, { holderId, caveats });
160
+ throw new Error("attenuateGrant: holderId is required");
161
+ return this._request("POST", `/connections/${encodeURIComponent(connectionId)}/grants/${encodeURIComponent(grantId)}/attenuate`, { holderId, caveats });
150
162
  }
151
163
  /** Revoke a single connection grant. */
152
164
  async revokeGrant({ connectionId, grantId, }) {
153
165
  if (!connectionId)
154
- throw new Error('revokeGrant: connectionId is required');
166
+ throw new Error("revokeGrant: connectionId is required");
155
167
  if (!grantId)
156
- throw new Error('revokeGrant: grantId is required');
157
- return this._request('DELETE', `/connections/${encodeURIComponent(connectionId)}/grants/${encodeURIComponent(grantId)}`, undefined);
168
+ throw new Error("revokeGrant: grantId is required");
169
+ return this._request("DELETE", `/connections/${encodeURIComponent(connectionId)}/grants/${encodeURIComponent(grantId)}`, undefined);
158
170
  }
159
171
  /**
160
172
  * agent-initiated MCP connection request: ask the principal to
@@ -164,21 +176,23 @@ export class ConnectionsClient {
164
176
  */
165
177
  async requestMcpConnection({ serverUrl, tools, reason, chatId, onBehalfOfUserId, }) {
166
178
  if (!serverUrl)
167
- throw new Error('requestMcpConnection: serverUrl is required');
168
- return (await this._request('POST', '/connections/mcp/requests', { serverUrl, tools, reason, chatId }, onBehalfOfUserId ? { 'X-On-Behalf-Of-User': onBehalfOfUserId } : undefined));
179
+ throw new Error("requestMcpConnection: serverUrl is required");
180
+ return (await this._request("POST", "/connections/mcp/requests", { serverUrl, tools, reason, chatId }, onBehalfOfUserId
181
+ ? { "X-On-Behalf-Of-User": onBehalfOfUserId }
182
+ : undefined));
169
183
  }
170
184
  async _requestRaw(method, path, body, extraHeaders) {
171
185
  const init = {
172
186
  method,
173
187
  headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
174
- ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
188
+ ...(body !== undefined ? { "content-type": "application/json" } : {}),
175
189
  ...extraHeaders,
176
- }),
190
+ }, this.laneId),
177
191
  };
178
192
  if (body !== undefined)
179
193
  init.body = JSON.stringify(body);
180
194
  const response = await fetch(`${this.baseUrl}${path}`, init);
181
- const text = await response.text().catch(() => '');
195
+ const text = await response.text().catch(() => "");
182
196
  if (!response.ok) {
183
197
  // ApiError (status/body/code); ConnectionsError remains the
184
198
  // documented duck type for callers that branch on `.status`.
@@ -56,7 +56,16 @@ export declare class GrantsClient {
56
56
  private readonly operatorKey;
57
57
  private readonly agentId?;
58
58
  private readonly baseUrl;
59
- constructor(operatorKey: string, agentId?: string, baseUrl?: string);
59
+ private readonly laneId?;
60
+ /**
61
+ * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
62
+ * X-Ziggs-Lane. The server reads off it who the agent is acting for, and
63
+ * answers the spendable rails with only the grants this wake may spend —
64
+ * so an agent serving several customers is never handed a list it has to
65
+ * choose between. Without it the list is everything the agent holds, and
66
+ * the grant it picks may then be refused with nothing saying why.
67
+ */
68
+ constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
60
69
  listGrants(query?: ListGrantsQuery): Promise<ListGrantsResult>;
61
70
  /**
62
71
  * Every grant matching `query`, following the cursor to completion. Use when a
@@ -12,12 +12,22 @@ export class GrantsClient {
12
12
  operatorKey;
13
13
  agentId;
14
14
  baseUrl;
15
- constructor(operatorKey, agentId, baseUrl) {
15
+ laneId;
16
+ /**
17
+ * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
18
+ * X-Ziggs-Lane. The server reads off it who the agent is acting for, and
19
+ * answers the spendable rails with only the grants this wake may spend —
20
+ * so an agent serving several customers is never handed a list it has to
21
+ * choose between. Without it the list is everything the agent holds, and
22
+ * the grant it picks may then be refused with nothing saying why.
23
+ */
24
+ constructor(operatorKey, agentId, baseUrl, laneId) {
16
25
  if (!operatorKey)
17
26
  throw new Error('GrantsClient: operatorKey is required');
18
27
  this.operatorKey = operatorKey;
19
28
  this.agentId = agentId;
20
29
  this.baseUrl = baseUrl || getBackendUrl();
30
+ this.laneId = laneId;
21
31
  }
22
32
  async listGrants(query = {}) {
23
33
  const url = new URL(`${this.baseUrl}/grants`);
@@ -41,7 +51,7 @@ export class GrantsClient {
41
51
  if (query.limit != null)
42
52
  url.searchParams.set('limit', String(query.limit));
43
53
  const res = await fetch(url.toString(), {
44
- headers: buildOperatorHeaders(this.operatorKey, this.agentId),
54
+ headers: buildOperatorHeaders(this.operatorKey, this.agentId, undefined, this.laneId),
45
55
  });
46
56
  const body = await res.text().catch(() => '');
47
57
  if (!res.ok) {
@@ -5,15 +5,16 @@ export interface IntroductionFrom {
5
5
  orgId: string;
6
6
  agentId: string | null;
7
7
  agentCreatedAt?: string | null;
8
- claimedLabel?: {
8
+ claimedLabel: {
9
9
  text: string;
10
10
  selfChosen: boolean;
11
11
  verified: boolean;
12
12
  };
13
13
  /**
14
- * Same text as claimedLabel.text. Not identity — anyone can pick it.
14
+ * Same text as claimedLabel.text. Not identity. Remove after 2026-10-07;
15
+ * readers must use claimedLabel.
15
16
  */
16
- label: string;
17
+ label?: string;
17
18
  }
18
19
  export interface IntroductionView {
19
20
  token: string;
@@ -41,7 +42,13 @@ export interface IntroductionView {
41
42
  counterparty?: {
42
43
  principal: string;
43
44
  agentId: string | null;
44
- label: string;
45
+ claimedLabel: {
46
+ text: string;
47
+ selfChosen: boolean;
48
+ verified: boolean;
49
+ };
50
+ /** Same text as claimedLabel.text. Remove after 2026-10-07. */
51
+ label?: string;
45
52
  nextStep: string;
46
53
  };
47
54
  }
@@ -16,6 +16,13 @@ export interface PublishOfferPayload {
16
16
  /** Broadcast audience: 'everyone' (default, fully public) or 'org' (members of your active org only). */
17
17
  audience?: BroadcastAudience;
18
18
  metadata?: Record<string, unknown>;
19
+ /**
20
+ * Replay handle, sent as the `Idempotency-Key` header — the same way
21
+ * `proposeAgreement` sends it. Re-publishing under the same key resolves to
22
+ * the listing already on the board; a NEW key republishes and retires the
23
+ * previous one. It used to ride in the body on this one route.
24
+ */
25
+ idempotencyKey?: string;
19
26
  }
20
27
  export declare function publishOffer(payload: PublishOfferPayload, creds: Creds): Promise<Agreement>;
21
28
  export interface PullOffersOptions {
@@ -21,17 +21,26 @@ function assertCreds(creds, op) {
21
21
  }
22
22
  export async function publishOffer(payload, creds) {
23
23
  assertCreds(creds, 'marketplace offer publish');
24
+ const { idempotencyKey, ...bodyData } = payload ?? {};
25
+ const headers = buildHeaders(creds);
26
+ if (idempotencyKey)
27
+ headers['Idempotency-Key'] = idempotencyKey;
24
28
  const res = await fetch(`${getMarketplaceBaseUrl()}/offers/publish`, {
25
29
  method: 'POST',
26
- headers: buildHeaders(creds),
27
- body: JSON.stringify(payload || {}),
30
+ headers,
31
+ body: JSON.stringify(bodyData),
28
32
  });
29
33
  if (!res.ok) {
30
34
  const body = await res.text().catch(() => '');
31
35
  throwApiError(res, body, `Marketplace offer publish failed: ${res.status}`);
32
36
  }
33
37
  const data = await res.json().catch(() => null);
34
- return shapeAgreement((data?.['offer'] ?? data));
38
+ // One key name across every agreement-creating route. This used to read
39
+ // `offer`, which was this route's own name for the same thing.
40
+ if (!data?.['agreement']) {
41
+ throw new Error('Invalid response: expected { agreement } from POST /marketplace/offers/publish');
42
+ }
43
+ return shapeAgreement(data['agreement']);
35
44
  }
36
45
  export async function pullOffers(options, creds) {
37
46
  assertCreds(creds, 'marketplace offers pull');
@@ -27,20 +27,6 @@ export type OrgResolution = {
27
27
  * match. Ambiguous names return the candidates rather than guessing.
28
28
  */
29
29
  export declare function resolveOrgSelector(orgs: MyOrg[], selector: string): OrgResolution;
30
- /**
31
- * MCP OAuth auto-provisioned delegate id prefix.
32
- *
33
- * The backend stopped classifying delegates by their id and now reads a stamp
34
- * on the agent row, which is what freed the prefix to drop the vendor name. A
35
- * client holds credentials, not rows, so this is still a shape test — but it is
36
- * a shape test against a NAME, and the only thing it decides is which access
37
- * endpoint to read. It said `delegate--` until the ids were renamed;
38
- * left alone, every delegate session would have fallen through to the hosted
39
- * reader and reported the wrong org and connection state.
40
- */
41
- export declare const MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX = "delegate--";
42
- /** True when `agentId` is an inbound MCP OAuth auto-provisioned delegate. */
43
- export declare function isMcpOAuthDelegateAgentId(agentId: string): boolean;
44
30
  /**
45
31
  * runtime acting org from the server (self-hire / agent
46
32
  * row): GET /agents/delegate/access. Moved here from ziggs-mcp's inline
@@ -1,3 +1,4 @@
1
+ import { isMcpOAuthDelegateSession } from '../shared/operatorKey.js';
1
2
  import { getBackendUrl } from '../utils/urlUtils.js';
2
3
  import { throwApiError } from '../shared/apiError.js';
3
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
@@ -42,22 +43,6 @@ export function resolveOrgSelector(orgs, selector) {
42
43
  return { status: 'ambiguous', matches: byName };
43
44
  return { status: 'not-found' };
44
45
  }
45
- /**
46
- * MCP OAuth auto-provisioned delegate id prefix.
47
- *
48
- * The backend stopped classifying delegates by their id and now reads a stamp
49
- * on the agent row, which is what freed the prefix to drop the vendor name. A
50
- * client holds credentials, not rows, so this is still a shape test — but it is
51
- * a shape test against a NAME, and the only thing it decides is which access
52
- * endpoint to read. It said `delegate--` until the ids were renamed;
53
- * left alone, every delegate session would have fallen through to the hosted
54
- * reader and reported the wrong org and connection state.
55
- */
56
- export const MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX = 'delegate--';
57
- /** True when `agentId` is an inbound MCP OAuth auto-provisioned delegate. */
58
- export function isMcpOAuthDelegateAgentId(agentId) {
59
- return !!agentId && agentId.startsWith(MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX);
60
- }
61
46
  /**
62
47
  * runtime acting org from the server (self-hire / agent
63
48
  * row): GET /agents/delegate/access. Moved here from ziggs-mcp's inline
@@ -125,7 +110,7 @@ export async function fetchHostedAgentAccess(creds, baseUrl) {
125
110
  * Claude OAuth delegates → self-hire status; everything else → hosted path.
126
111
  */
127
112
  export async function fetchSessionAccess(creds, baseUrl) {
128
- if (isMcpOAuthDelegateAgentId(creds.agentId)) {
113
+ if (isMcpOAuthDelegateSession(creds)) {
129
114
  return fetchDelegateAccess(creds, baseUrl);
130
115
  }
131
116
  return fetchHostedAgentAccess(creds, baseUrl);