@ziggs-ai/api-client 0.9.0 → 0.9.1

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.
@@ -25,6 +25,54 @@ function assertCreds(creds, op) {
25
25
  if (!creds?.agentId)
26
26
  throw new Error(`agentId is required for ${op}`);
27
27
  }
28
+ /**
29
+ * A trust link's agreement, as every caller sees it.
30
+ *
31
+ * A link is reach, not commerce: it carries no money, no escrow, no execution
32
+ * state and no approvals ledger. Handing the raw document over anyway put Mongo
33
+ * bookkeeping in front of an LLM, which is what ZIG-957 forbade.
34
+ *
35
+ * Lives here rather than in `capabilities/links.ts` because this is where the
36
+ * rule is applied (ZIG-1111); that module re-exports it so the public name is
37
+ * unchanged.
38
+ */
39
+ export function linkSummary(a) {
40
+ return {
41
+ agreementId: a.agreementId,
42
+ // Kept deliberately: callers branch on this, and a summary that hides what
43
+ // kind of thing it describes breaks the code it is meant to protect.
44
+ engagementKind: a.engagementKind,
45
+ status: a.status,
46
+ proposalStatus: a.proposalStatus,
47
+ parties: {
48
+ creatorAgent: a.parties?.creatorAgent ?? null,
49
+ providerAgent: a.parties?.providerAgent ?? null,
50
+ creator: a.parties?.creator ?? null,
51
+ proposedTo: a.parties?.proposedTo ?? null,
52
+ },
53
+ ...(a.description ? { description: a.description } : {}),
54
+ // Seat bookkeeping on an open invite — link state, not commerce.
55
+ ...(a.linkInvite ? { linkInvite: a.linkInvite } : {}),
56
+ createdAt: a.createdAt,
57
+ };
58
+ }
59
+ /**
60
+ * Every agreement document this client parses passes through here.
61
+ *
62
+ * The rule — a link is returned as its summary, anything else verbatim — used to
63
+ * be written out at six call sites across the SDK runner, the MCP tools and the
64
+ * capability layer, and three verbs never got it: `agreement_counter`,
65
+ * `agreement_fulfill` and `agreement_subcontract` returned the raw document
66
+ * (ZIG-1111). Applying it at the parse boundary means a new verb inherits the
67
+ * rule instead of remembering to opt in, and no surface can word its own verdict.
68
+ *
69
+ * Typed as `Agreement` on the way out: every key the summary keeps IS an
70
+ * Agreement field, so this narrows a document rather than returning a different
71
+ * shape.
72
+ */
73
+ export function shapeAgreement(a) {
74
+ return a.engagementKind === 'link' ? linkSummary(a) : a;
75
+ }
28
76
  export async function proposeAgreement(proposalData, creds) {
29
77
  if (!proposalData)
30
78
  throw new Error('Proposal data is required for proposal creation');
@@ -48,7 +96,7 @@ export async function proposeAgreement(proposalData, creds) {
48
96
  if (!data?.['agreement']) {
49
97
  throw new Error('Invalid response: expected { agreement } from POST /agreements/proposals');
50
98
  }
51
- return data['agreement'];
99
+ return shapeAgreement(data['agreement']);
52
100
  }
53
101
  /** Propose a contract to one party (user or agent). Server defaults `engagementKind` to `service`. */
54
102
  export async function proposeDirectTo(input, creds) {
@@ -96,7 +144,7 @@ export async function delegateAgreement(proposalData, creds) {
96
144
  if (!data?.['agreement']) {
97
145
  throw new Error('Invalid response: expected { agreement } from POST /agreements/:parentAgreementId/delegations');
98
146
  }
99
- return data['agreement'];
147
+ return shapeAgreement(data['agreement']);
100
148
  }
101
149
  /**
102
150
  * Approve or reject a pending agreement (ZIG-524 canonical client path).
@@ -110,7 +158,10 @@ export async function respondToAgreement(agreementId, action, creds, opts = {})
110
158
  if (!agreementId || !action)
111
159
  throw new Error('Agreement ID and action are required for proposal response');
112
160
  assertCreds(creds, 'proposal response');
113
- const agreement = opts.agreement ?? (await getAgreement(agreementId, creds));
161
+ // Unshaped on purpose: the approvals ledger below is exactly what the link
162
+ // summary drops, and resolving the caller's slot from a summary would fail
163
+ // every link approval with "no pending approval entry".
164
+ const agreement = opts.agreement ?? (await getAgreementDocument(agreementId, creds));
114
165
  if (!agreement) {
115
166
  throw new Error(`Agreement ${agreementId} not found`);
116
167
  }
@@ -213,7 +264,7 @@ export async function approveAgreementAsParty(agreementId, partyId, status, cred
213
264
  if (!data?.['agreement']) {
214
265
  throw new Error('Invalid response: expected { agreement } from PUT /agreements/:id/approvals/:partyId');
215
266
  }
216
- return data['agreement'];
267
+ return shapeAgreement(data['agreement']);
217
268
  }
218
269
  export async function counterAgreement(agreementId, counter, creds) {
219
270
  if (!agreementId)
@@ -232,7 +283,7 @@ export async function counterAgreement(agreementId, counter, creds) {
232
283
  if (!data?.['agreement']) {
233
284
  throw new Error('Invalid response: expected { agreement } from /agreements/:id/counter');
234
285
  }
235
- return data['agreement'];
286
+ return shapeAgreement(data['agreement']);
236
287
  }
237
288
  export async function getAgreementStatus(agreementId, creds) {
238
289
  if (!agreementId)
@@ -261,8 +312,7 @@ export async function listAgreements(filters = {}, creds) {
261
312
  assertCreds(creds, 'list agreements');
262
313
  // ZIG-699 — return an empty array only for a genuine empty 200 result. Any
263
314
  // failure (non-2xx / network) throws so the MCP tool reports a real error
264
- // instead of "you have no agreements". The status stays whitespace-delimited
265
- // so the tool's toolError/classifyToolError maps it to a stable code.
315
+ // instead of "you have no agreements". ZIG-1124 — HTTP failures are ApiError.
266
316
  // Canonical query path: GET /agreements?scope=&status=&engagementKind=&...
267
317
  const url = new URL(getAgreementBaseUrl());
268
318
  if (filters.status)
@@ -284,10 +334,12 @@ export async function listAgreements(filters = {}, creds) {
284
334
  if (!res.ok) {
285
335
  const body = await res.text().catch(() => '');
286
336
  runtimeLog.warn('AgreementClient', `⚠️ List agreements failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
287
- throw new Error(`GET /agreements ${res.status} ${body?.slice(0, 200)}`);
337
+ throwApiError(res, body, `GET /agreements failed: ${res.status}`);
288
338
  }
289
339
  const data = await res.json().catch(() => null);
290
- return Array.isArray(data?.['agreements']) ? data['agreements'] : [];
340
+ return Array.isArray(data?.['agreements'])
341
+ ? data['agreements'].map(shapeAgreement)
342
+ : [];
291
343
  }
292
344
  export async function getMyAgreements(filters = {}, creds) {
293
345
  assertCreds(creds, 'get my agreements');
@@ -316,20 +368,29 @@ export async function getMyAgreements(filters = {}, creds) {
316
368
  if (!res.ok) {
317
369
  const body = await res.text().catch(() => '');
318
370
  runtimeLog.warn('AgreementClient', `⚠️ Get my agreements failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
319
- throw new Error(`GET /agreements?scope=mine ${res.status} ${body?.slice(0, 200)}`);
371
+ throwApiError(res, body, `GET /agreements?scope=mine failed: ${res.status}`);
320
372
  }
321
373
  const data = await res.json().catch(() => null);
322
- return Array.isArray(data?.['agreements']) ? data['agreements'] : [];
374
+ return Array.isArray(data?.['agreements'])
375
+ ? data['agreements'].map(shapeAgreement)
376
+ : [];
323
377
  }
324
- export async function getAgreement(agreementId, creds) {
378
+ /**
379
+ * The full agreement document, unshaped — for this client's OWN reads.
380
+ *
381
+ * `respondToAgreement` resolves which approvals slot the caller may fill, and
382
+ * `claimOpenAgreement` routes on the broadcast's open side; both need fields the
383
+ * link summary deliberately drops. Callers outside this module get the shaped
384
+ * `getAgreement`.
385
+ */
386
+ async function getAgreementDocument(agreementId, creds) {
325
387
  if (!agreementId)
326
388
  return null;
327
389
  assertCreds(creds, 'get agreement');
328
390
  // ZIG-698 — a genuine 404 (and 200-with-no-agreement) is a real "not found"
329
391
  // and returns null. Every other failure (403/5xx/network) must throw so the
330
392
  // caller can tell "does not exist" from "could not fetch" instead of a 403
331
- // masquerading as not-found. The MCP tool classifies the thrown status via
332
- // toolError; the message keeps the status whitespace-delimited for that.
393
+ // masquerading as not-found. ZIG-1124 — thrown as ApiError for toolError.
333
394
  let res;
334
395
  try {
335
396
  res = await fetch(`${getAgreementBaseUrl()}/${agreementId}`, {
@@ -346,11 +407,16 @@ export async function getAgreement(agreementId, creds) {
346
407
  if (!res.ok) {
347
408
  const body = await res.text().catch(() => '');
348
409
  runtimeLog.warn('AgreementClient', `⚠️ Get agreement failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
349
- throw new Error(`GET /agreements/${agreementId} ${res.status} ${body?.slice(0, 200)}`);
410
+ throwApiError(res, body, `GET /agreements/${agreementId} failed: ${res.status}`);
350
411
  }
351
412
  const data = await res.json().catch(() => null);
352
413
  return data?.['agreement'] ?? null;
353
414
  }
415
+ /** One agreement, shaped: a link comes back as its summary. */
416
+ export async function getAgreement(agreementId, creds) {
417
+ const agreement = await getAgreementDocument(agreementId, creds);
418
+ return agreement ? shapeAgreement(agreement) : null;
419
+ }
354
420
  export async function createAgreement(body, creds) {
355
421
  if (!body)
356
422
  throw new Error('Body is required for agreement creation');
@@ -369,7 +435,8 @@ export async function createAgreement(body, creds) {
369
435
  if (!data?.['agreement']) {
370
436
  throw new Error('Invalid response: expected { ok, agreement } from POST /agreements');
371
437
  }
372
- return data;
438
+ const envelope = data;
439
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
373
440
  }
374
441
  export async function revokeAgreement(agreementId, creds) {
375
442
  if (!agreementId)
@@ -389,7 +456,8 @@ export async function revokeAgreement(agreementId, creds) {
389
456
  if (!data?.['agreement']) {
390
457
  throw new Error('Invalid response: expected { ok, agreement } from DELETE /agreements/:id');
391
458
  }
392
- return data;
459
+ const envelope = data;
460
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
393
461
  }
394
462
  /** ZIG-832: mark an agreement fulfilled (complete). A provider closing its own
395
463
  * delivered work — party-gated server-side. */
@@ -409,7 +477,8 @@ export async function fulfillAgreement(agreementId, creds) {
409
477
  if (!data?.['agreement']) {
410
478
  throw new Error('Invalid response: expected { ok, agreement } from POST /agreements/:id/fulfill');
411
479
  }
412
- return data;
480
+ const envelope = data;
481
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
413
482
  }
414
483
  /**
415
484
  * Claim an open agreement (ZIG-524 phase 2 / ZIG-525):
@@ -433,7 +502,12 @@ export async function claimAgreement(agreementId, creds) {
433
502
  if (!data?.['agreement']) {
434
503
  throw new Error('Invalid response: expected { ok, agreement } from POST /agreements/:id/claim');
435
504
  }
436
- return data;
505
+ // ZIG-1155: the route reports which kind of broadcast this turned out to be.
506
+ // A caller cannot work it out from the row — post-claim no sentinel is left,
507
+ // and a quest (you do the work) reads the same shape as a standing offer (you
508
+ // pay for it) unless you know which slot you landed in.
509
+ const envelope = data;
510
+ return { ...envelope, agreement: shapeAgreement(envelope.agreement) };
437
511
  }
438
512
  // ---------------------------------------------------------------------------
439
513
  // Chat links
@@ -104,7 +104,7 @@ export async function listMyChats(creds) {
104
104
  assertCreds(creds, 'list my chats');
105
105
  // ZIG-699 — empty array only for a genuine empty 200; any failure (non-2xx /
106
106
  // network) throws so ziggs_chat_list reports a real error instead of "you
107
- // have no chats". Status stays whitespace-delimited for toolError classification.
107
+ // have no chats". ZIG-1124 — HTTP failures are ApiError (status/body/code).
108
108
  let res;
109
109
  try {
110
110
  res = await fetch(`${getBackendUrl()}/chats/mine`, {
@@ -119,7 +119,7 @@ export async function listMyChats(creds) {
119
119
  if (!res.ok) {
120
120
  const body = await res.text().catch(() => '');
121
121
  runtimeLog.warn('ChatClient', `⚠️ listMyChats failed: ${res.status} ${res.statusText} ${body?.slice(0, 200)}`);
122
- throw new Error(`GET /chats/mine ${res.status} ${body?.slice(0, 200)}`);
122
+ throwApiError(res, body, `GET /chats/mine failed: ${res.status}`);
123
123
  }
124
124
  const data = await res.json().catch(() => null);
125
125
  return Array.isArray(data?.['chats']) ? data['chats'] : [];
@@ -1,5 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { throwApiError } from '../shared/apiError.js';
3
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
5
  import { GrantsClient } from './GrantsClient.js';
5
6
  // ZIG-569 — defense-in-depth mirror of the backend leak-guard
@@ -169,10 +170,9 @@ export class ConnectionsClient {
169
170
  const response = await fetch(`${this.baseUrl}${path}`, init);
170
171
  const text = await response.text().catch(() => '');
171
172
  if (!response.ok) {
172
- const err = new Error(`${method} ${path} ${response.status} ${text.slice(0, 200)}`);
173
- err.status = response.status;
174
- err.body = text;
175
- throw err;
173
+ // ZIG-1124 — ApiError (status/body/code); ConnectionsError remains the
174
+ // documented duck type for callers that branch on `.status`.
175
+ throwApiError(response, text, `${method} ${path} failed: ${response.status}`);
176
176
  }
177
177
  return text;
178
178
  }
@@ -1,5 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { throwApiError } from '../shared/apiError.js';
3
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
5
  /**
5
6
  * P4 discovery — labels-only pointers to context the agent could REQUEST but
@@ -34,7 +35,7 @@ export class ContextDiscoveryClient {
34
35
  });
35
36
  const body = await res.text().catch(() => '');
36
37
  if (!res.ok) {
37
- throw new Error(`ContextDiscoveryClient.discoverGrantable ${res.status} ${body.slice(0, 200)}`);
38
+ throwApiError(res, body, `ContextDiscoveryClient.discoverGrantable failed: ${res.status}`);
38
39
  }
39
40
  const parsed = JSON.parse(body);
40
41
  return (parsed.items ?? []).map((i) => ({
@@ -105,18 +105,11 @@ export class ContextReadClient {
105
105
  const res = await fetch(url.toString(), { headers });
106
106
  const body = await res.text().catch(() => '');
107
107
  if (!res.ok) {
108
- // Carry the status like `snapshot()` does, so callers can branch on it.
109
108
  // A 403 here is a legitimate outcome, not a transport failure: addressing
110
109
  // and authorisation are separate, so an agent can be told about mail it
111
- // is not (or is no longer) allowed to open.
112
- // ZIG-1019: a 429 carries the server's own wait; everything else keeps
113
- // the plain status-tagged error callers already branch on.
114
- if (res.status === 429) {
115
- throw pollSurfaceError(`ContextReadClient.read ${type}`, res, body);
116
- }
117
- const err = new Error(`ContextReadClient.read ${type} ${res.status} ${body.slice(0, 200)}`);
118
- err.status = res.status;
119
- throw err;
110
+ // is not (or is no longer) allowed to open. ZIG-1124 — one ApiError shape
111
+ // (429 → RateLimitedError with Retry-After).
112
+ throw pollSurfaceError(`ContextReadClient.read ${type}`, res, body);
120
113
  }
121
114
  return JSON.parse(body);
122
115
  }
@@ -151,12 +144,8 @@ export class ContextReadClient {
151
144
  const res = await fetch(url.toString(), { headers });
152
145
  const body = await res.text().catch(() => '');
153
146
  if (!res.ok) {
154
- if (res.status === 429) {
155
- throw pollSurfaceError('ContextReadClient.snapshot', res, body);
156
- }
157
- const err = new Error(`ContextReadClient.snapshot ${res.status} ${body.slice(0, 200)}`);
158
- err.status = res.status;
159
- throw err;
147
+ // ZIG-1124 — ApiError for every non-OK (429 → RateLimitedError).
148
+ throw pollSurfaceError('ContextReadClient.snapshot', res, body);
160
149
  }
161
150
  return JSON.parse(body);
162
151
  }
@@ -1,5 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { throwApiError } from '../shared/apiError.js';
3
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
5
  /**
5
6
  * ZIG-648 — unified grant listing across every rail. `GET /grants` returns the
@@ -41,7 +42,7 @@ export class GrantsClient {
41
42
  });
42
43
  const body = await res.text().catch(() => '');
43
44
  if (!res.ok) {
44
- throw new Error(`GrantsClient.listGrants ${res.status} ${body.slice(0, 200)}`);
45
+ throwApiError(res, body, `GrantsClient.listGrants failed: ${res.status}`);
45
46
  }
46
47
  const parsed = JSON.parse(body);
47
48
  return {
@@ -127,45 +127,6 @@ export interface InboxAckResult {
127
127
  */
128
128
  export interface InboxReadOptions {
129
129
  waitSeconds?: number;
130
- /**
131
- * Operator read only (ZIG-965): restrict the sweep to this roster. The
132
- * server intersects it with the agents the key owner runs — it narrows,
133
- * never widens. A launcher hosting a subset should always pass the agents
134
- * it actually registered, or it pays for a sweep of the owner's whole
135
- * seeded fleet.
136
- */
137
- agents?: string[];
138
- }
139
- /**
140
- * One agent's line in the operator sweep: enough to decide whether to start a
141
- * host, and nothing more.
142
- *
143
- * Deliberately NOT that agent's envelope. The launcher only chooses who to
144
- * run; the host it starts reads its own inbox on its own credentials a moment
145
- * later, so building N envelopes here was work thrown away.
146
- */
147
- export interface InboxOperatorAgentEntry {
148
- agentId: string;
149
- /** Newest delivery addressed to this agent, or null if it never had one. */
150
- deliveredUpTo: string | null;
151
- /** How far it has acked. Mail exists when `deliveredUpTo > ackedUpTo`. */
152
- ackedUpTo: string | null;
153
- /**
154
- * True when this agent holds open assigned work. Independent of the
155
- * watermark: a host that died mid-task already acked past the task's
156
- * delivery, and the open task is the only durable trace it needs restarting.
157
- */
158
- hasOpenTasks: boolean;
159
- }
160
- /** GET /inbox/operator: which of the key owner's agents have mail or open work. */
161
- export interface InboxOperatorEnvelope {
162
- asOf: string;
163
- /** Agents with mail or open work. */
164
- agents: InboxOperatorAgentEntry[];
165
- /** Agents examined that had neither. */
166
- idleAgents: number;
167
- /** Owned agents beyond the server's per-pass cap — not examined. */
168
- truncatedAgents: number;
169
130
  }
170
131
  /**
171
132
  * The doorbell, not the door (ZIG-434): references addressed to this agent
@@ -184,14 +145,6 @@ export declare class InboxClient {
184
145
  private headers;
185
146
  private inboxUrl;
186
147
  getInbox(opts?: InboxReadOptions): Promise<InboxEnvelope>;
187
- /**
188
- * Operator-level multiplexed read: which of this key owner's agents have
189
- * mail or open work (an agent-scoped key collapses to its one agent).
190
- *
191
- * Returns who to start, never what they were sent — the host you start
192
- * reads its own inbox on its own identity. Ack stays per agent.
193
- */
194
- getOperatorInbox(opts?: InboxReadOptions): Promise<InboxOperatorEnvelope>;
195
148
  /**
196
149
  * Advance this agent's watermark — pass the envelope's `ackTo`. Monotonic
197
150
  * server-side: an older value is a no-op, so a replayed ack can never
@@ -35,9 +35,6 @@ export class InboxClient {
35
35
  if (opts.waitSeconds != null && opts.waitSeconds > 0) {
36
36
  url.searchParams.set('wait', String(opts.waitSeconds));
37
37
  }
38
- if (opts.agents?.length) {
39
- url.searchParams.set('agents', opts.agents.join(','));
40
- }
41
38
  return url.toString();
42
39
  }
43
40
  async getInbox(opts = {}) {
@@ -50,42 +47,6 @@ export class InboxClient {
50
47
  }
51
48
  return JSON.parse(body);
52
49
  }
53
- /**
54
- * Operator-level multiplexed read: which of this key owner's agents have
55
- * mail or open work (an agent-scoped key collapses to its one agent).
56
- *
57
- * Returns who to start, never what they were sent — the host you start
58
- * reads its own inbox on its own identity. Ack stays per agent.
59
- */
60
- async getOperatorInbox(opts = {}) {
61
- // Bounded wait (ZIG-965): the server holds at most `waitSeconds` (+ sweep
62
- // time); a poll that outlives that by a wide margin is a dead sweep, and
63
- // without a timeout it blocked the launcher's whole poll loop — lazy wake
64
- // simply stopped. Abort and let the caller's retry loop take over.
65
- const timeoutMs = ((opts.waitSeconds ?? 0) + 30) * 1000;
66
- const ac = new AbortController();
67
- const timer = setTimeout(() => ac.abort(), timeoutMs);
68
- let res;
69
- try {
70
- res = await fetch(this.inboxUrl('/inbox/operator', opts), {
71
- headers: this.headers(),
72
- signal: ac.signal,
73
- });
74
- }
75
- catch (err) {
76
- throw ac.signal.aborted
77
- ? new Error(`InboxClient.getOperatorInbox timed out after ${timeoutMs}ms`)
78
- : err;
79
- }
80
- finally {
81
- clearTimeout(timer);
82
- }
83
- const body = await res.text().catch(() => '');
84
- if (!res.ok) {
85
- throw pollSurfaceError('InboxClient.getOperatorInbox', res, body);
86
- }
87
- return JSON.parse(body);
88
- }
89
50
  /**
90
51
  * Advance this agent's watermark — pass the envelope's `ackTo`. Monotonic
91
52
  * server-side: an older value is a no-op, so a replayed ack can never
@@ -1,4 +1,6 @@
1
1
  import 'dotenv/config';
2
+ // ZIG-1111: one shaping rule for every agreement this package parses.
3
+ import { shapeAgreement } from './AgreementClient.js';
2
4
  import { getBackendUrl } from '../utils/urlUtils.js';
3
5
  import { throwApiError } from '../shared/apiError.js';
4
6
  function getMarketplaceBaseUrl() { return `${getBackendUrl()}/marketplace`; }
@@ -30,7 +32,7 @@ export async function publishOffer(payload, creds) {
30
32
  throwApiError(res, body, `Marketplace offer publish failed: ${res.status}`);
31
33
  }
32
34
  const data = await res.json().catch(() => null);
33
- return (data?.['offer'] ?? data);
35
+ return shapeAgreement((data?.['offer'] ?? data));
34
36
  }
35
37
  export async function pullOffers(options, creds) {
36
38
  assertCreds(creds, 'marketplace offers pull');
@@ -67,7 +69,7 @@ export async function claimOffer(agreementId, creds) {
67
69
  const data = await res.json().catch(() => null);
68
70
  if (!data?.['ok'])
69
71
  throw new Error(data?.['error'] || 'Claim failed');
70
- return data['offer'];
72
+ return shapeAgreement(data['offer']);
71
73
  }
72
74
  export async function publishQuest(payload, creds) {
73
75
  assertCreds(creds, 'quest publish');
@@ -83,7 +85,7 @@ export async function publishQuest(payload, creds) {
83
85
  const data = await res.json().catch(() => null);
84
86
  if (!data?.['agreement'])
85
87
  throw new Error('Quest publish returned no agreement');
86
- return data['agreement'];
88
+ return shapeAgreement(data['agreement']);
87
89
  }
88
90
  export async function pullQuests(options, creds) {
89
91
  assertCreds(creds, 'quest pull');
@@ -1,5 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { throwApiError } from '../shared/apiError.js';
3
4
  /**
4
5
  * Read-side client for forward-delta message reads.
5
6
  *
@@ -35,11 +36,8 @@ export class MessagesClient {
35
36
  const res = await fetch(url.toString(), { headers: this._headers() });
36
37
  if (!res.ok) {
37
38
  const body = await res.text().catch(() => '');
38
- const err = new Error(`MessagesClient.list ${res.status} ${res.statusText} ${body.slice(0, 200)}`);
39
- // Callers branch on the HTTP status (404 = chat deleted/not visible)
40
- // without parsing the message string.
41
- err.status = res.status;
42
- throw err;
39
+ // Callers branch on ApiError.status (404 = chat deleted/not visible).
40
+ throwApiError(res, body, `MessagesClient.list failed: ${res.status}`);
43
41
  }
44
42
  return (await res.json());
45
43
  }
@@ -1,5 +1,6 @@
1
1
  import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
+ import { throwApiError } from '../shared/apiError.js';
3
4
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
5
  /**
5
6
  * ZIG-739 / ZIG-956 — the operator's full org membership (not just granted
@@ -15,7 +16,7 @@ export async function fetchMyOrgs(creds, baseUrl) {
15
16
  });
16
17
  const body = await res.text().catch(() => '');
17
18
  if (!res.ok) {
18
- throw new Error(`GET /orgs/me ${res.status} ${body.slice(0, 200)}`);
19
+ throwApiError(res, body, `GET /orgs/me failed: ${res.status}`);
19
20
  }
20
21
  const parsed = body ? JSON.parse(body) : {};
21
22
  return (parsed.orgs ?? []).map((o) => ({
@@ -55,7 +56,7 @@ export async function fetchDelegateAccess(creds, baseUrl) {
55
56
  });
56
57
  const body = await res.text().catch(() => '');
57
58
  if (!res.ok) {
58
- throw new Error(`GET /agents/claude-delegate/access ${res.status} ${body.slice(0, 200)}`);
59
+ throwApiError(res, body, `GET /agents/claude-delegate/access failed: ${res.status}`);
59
60
  }
60
61
  return body ? JSON.parse(body) : {};
61
62
  }
@@ -2,7 +2,7 @@ import 'dotenv/config';
2
2
  import { getBackendUrl } from '../utils/urlUtils.js';
3
3
  import { buildOperatorHeaders } from './operatorHeaders.js';
4
4
  import { GrantsClient } from './GrantsClient.js';
5
- import { parseErrorMessage } from '../shared/apiError.js';
5
+ import { throwApiError } from '../shared/apiError.js';
6
6
  function randomIdempotencyKey(prefix = 'op') {
7
7
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
8
8
  }
@@ -217,10 +217,8 @@ export class PaymentsClient {
217
217
  const response = await fetch(`${this.baseUrl}${path}`, init);
218
218
  const text = await response.text();
219
219
  if (!response.ok) {
220
- const err = new Error(parseErrorMessage(text, `HTTP ${response.status}`));
221
- err.status = response.status;
222
- err.body = text;
223
- throw err;
220
+ // ZIG-1124 — ApiError; PaymentsError remains the duck type for `.status`.
221
+ throwApiError(response, text, `HTTP ${response.status}`);
224
222
  }
225
223
  return text ? JSON.parse(text) : null;
226
224
  }
@@ -1,4 +1,4 @@
1
- import { type ProposeTerms } from './AgreementClient.js';
1
+ import { type ClaimedKind, type ProposeTerms } from './AgreementClient.js';
2
2
  import { type Agreement, type Creds, type EngagementKind } from '../types.js';
3
3
  /**
4
4
  * ZIG-1022 — one propose grammar. Direct, broadcast (quest and standing
@@ -22,11 +22,19 @@ export declare function proposeUnified(input: UnifiedProposeInput, creds: Creds)
22
22
  agreement: Agreement;
23
23
  shape: ProposeShape;
24
24
  }>;
25
- export type ClaimedKind = 'link' | 'offer' | 'quest' | 'hand-off';
26
25
  /**
27
- * ZIG-1021 — one claim verb for any open broadcast. Fetches the agreement to
28
- * route: link invites and quests claim through POST /agreements/:id/claim;
29
- * standing offers (open payer side) through POST /marketplace/offers/claim.
26
+ * ZIG-1021 — one claim verb for any open broadcast: link invite, quest,
27
+ * hand-off, or standing offer.
28
+ *
29
+ * One request. This used to read the agreement first to decide which endpoint to
30
+ * post to, and `GET /agreements/:id` is party-scoped — a claimer is by definition
31
+ * not yet a party to the broadcast it is claiming, so the routing read 404'd and
32
+ * every standing offer in the store failed with "Agreement not found" before
33
+ * either claim endpoint was called (ZIG-1155). The backend routes it now, where
34
+ * the row is readable without being a party to it.
35
+ *
36
+ * `kind` arrives on the claim response — the route that did the routing reports
37
+ * which broadcast kind this turned out to be.
30
38
  */
31
39
  export declare function claimOpenAgreement(agreementId: string, creds: Creds): Promise<{
32
40
  agreement: Agreement;
@@ -1,5 +1,5 @@
1
- import { createAgreement, claimAgreement, getAgreement, proposeBroadcast, proposeDirectTo, } from './AgreementClient.js';
2
- import { claimOffer, publishOffer } from './MarketplaceClient.js';
1
+ import { createAgreement, claimAgreement, proposeBroadcast, proposeDirectTo, } from './AgreementClient.js';
2
+ import { publishOffer } from './MarketplaceClient.js';
3
3
  import { isBroadcastTarget, } from '../types.js';
4
4
  export async function proposeUnified(input, creds) {
5
5
  const { proposedTo, chatId, engagementKind, providerId, ...terms } = input;
@@ -56,30 +56,24 @@ export async function proposeUnified(input, creds) {
56
56
  return { agreement, shape: 'direct' };
57
57
  }
58
58
  /**
59
- * ZIG-1021 — one claim verb for any open broadcast. Fetches the agreement to
60
- * route: link invites and quests claim through POST /agreements/:id/claim;
61
- * standing offers (open payer side) through POST /marketplace/offers/claim.
59
+ * ZIG-1021 — one claim verb for any open broadcast: link invite, quest,
60
+ * hand-off, or standing offer.
61
+ *
62
+ * One request. This used to read the agreement first to decide which endpoint to
63
+ * post to, and `GET /agreements/:id` is party-scoped — a claimer is by definition
64
+ * not yet a party to the broadcast it is claiming, so the routing read 404'd and
65
+ * every standing offer in the store failed with "Agreement not found" before
66
+ * either claim endpoint was called (ZIG-1155). The backend routes it now, where
67
+ * the row is readable without being a party to it.
68
+ *
69
+ * `kind` arrives on the claim response — the route that did the routing reports
70
+ * which broadcast kind this turned out to be.
62
71
  */
63
72
  export async function claimOpenAgreement(agreementId, creds) {
64
73
  if (!agreementId)
65
74
  throw new Error('agreementId is required');
66
- const existing = await getAgreement(agreementId, creds);
67
- if (!existing)
68
- throw new Error(`Agreement not found: ${agreementId}`);
69
- if (existing.engagementKind === 'link') {
70
- const { agreement } = await claimAgreement(agreementId, creds);
71
- return { agreement, kind: 'link' };
72
- }
73
- if (isBroadcastTarget(existing.parties?.payer)) {
74
- // Seller-broadcast standing offer: the open side is the payer — you buy.
75
- const agreement = await claimOffer(agreementId, creds);
76
- return { agreement, kind: 'offer' };
77
- }
78
- const { agreement } = await claimAgreement(agreementId, creds);
79
- // ZIG-1059: a pinned provider inverts the quest reading — the publisher's
80
- // hired agent does the work and the claimer is who it is done FOR.
81
- return {
82
- agreement,
83
- kind: existing.providerPinned === true ? 'hand-off' : 'quest',
84
- };
75
+ const { agreement, kind } = await claimAgreement(agreementId, creds);
76
+ // A server that has not shipped the `kind` field yet still claims correctly;
77
+ // 'quest' is the shape the route has always handled.
78
+ return { agreement, kind: kind ?? 'quest' };
85
79
  }
@@ -26,4 +26,4 @@ export type { MyOrg, OrgResolution } from './OrgsClient.js';
26
26
  export { AgentSearchClient } from './AgentSearchClient.js';
27
27
  export { TelemetryClient } from './TelemetryClient.js';
28
28
  export { InboxClient } from './InboxClient.js';
29
- export type { InboxDeliveryKind, InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, InboxOperatorAgentEntry, InboxOperatorEnvelope, } from './InboxClient.js';
29
+ export type { InboxDeliveryKind, InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, } from './InboxClient.js';