@ziggs-ai/api-client 0.15.1 → 0.17.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 +10 -1
  3. package/dist/capabilities/agreements.js +2 -1
  4. package/dist/capabilities/artifacts.js +16 -6
  5. package/dist/capabilities/connections.js +5 -5
  6. package/dist/capabilities/grants.js +8 -2
  7. package/dist/capabilities/index.d.ts +1 -0
  8. package/dist/capabilities/index.js +1 -0
  9. package/dist/capabilities/links.js +3 -3
  10. package/dist/capabilities/listedFields.d.ts +17 -0
  11. package/dist/capabilities/listedFields.js +45 -0
  12. package/dist/capabilities/marketplace.js +7 -2
  13. package/dist/capabilities/payments.js +7 -2
  14. package/dist/capabilities/tasks.js +9 -3
  15. package/dist/http/AgreementClient.js +3 -10
  16. package/dist/http/ArtifactsClient.d.ts +5 -16
  17. package/dist/http/ArtifactsClient.js +8 -5
  18. package/dist/http/ChatClient.d.ts +2 -6
  19. package/dist/http/ChatClient.js +3 -10
  20. package/dist/http/ConnectionsClient.d.ts +6 -4
  21. package/dist/http/ConnectionsClient.js +7 -5
  22. package/dist/http/ContextReadClient.js +3 -14
  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.js +3 -10
  27. package/dist/http/OrgsClient.d.ts +0 -14
  28. package/dist/http/OrgsClient.js +2 -17
  29. package/dist/http/PaymentsClient.d.ts +27 -3
  30. package/dist/http/PaymentsClient.js +18 -3
  31. package/dist/http/TaskClient.js +4 -11
  32. package/dist/http/index.d.ts +2 -1
  33. package/dist/http/index.js +5 -1
  34. package/dist/http/operatorHeaders.d.ts +22 -3
  35. package/dist/http/operatorHeaders.js +28 -4
  36. package/dist/http/paymentGrantSelection.d.ts +5 -0
  37. package/dist/http/paymentGrantSelection.js +26 -16
  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
@@ -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
  }
@@ -2,17 +2,10 @@
2
2
  import { shapeAgreement } from './AgreementClient.js';
3
3
  import { getBackendUrl } from '../utils/urlUtils.js';
4
4
  import { throwApiError } from '../shared/apiError.js';
5
+ import { buildCredsHeaders } from './operatorHeaders.js';
5
6
  function getMarketplaceBaseUrl() { return `${getBackendUrl()}/marketplace`; }
6
- function buildHeaders(creds) {
7
- return {
8
- 'content-type': 'application/json',
9
- Authorization: `Bearer ${creds.operatorKey}`,
10
- 'X-Agent-Id': creds.agentId,
11
- // the wake's lane, so the backend can fence this call to the
12
- // engagement it belongs to rather than the agent's whole authority.
13
- ...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
14
- };
15
- }
7
+ // Auth, agent and the wake's lane, from the one helper — see operatorHeaders.
8
+ const buildHeaders = buildCredsHeaders;
16
9
  function assertCreds(creds, op) {
17
10
  if (!creds?.operatorKey)
18
11
  throw new Error(`operatorKey is required for ${op}`);
@@ -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);
@@ -87,7 +87,21 @@ export declare class PaymentsClient {
87
87
  private readonly operatorKey;
88
88
  private readonly agentId?;
89
89
  private readonly baseUrl;
90
- constructor(operatorKey: string, agentId?: string, baseUrl?: string);
90
+ private readonly laneId?;
91
+ /**
92
+ * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
93
+ * X-Ziggs-Lane. It tells the backend which hire this wake is in, and the
94
+ * backend reads off it WHO the spend is for. An agent that serves several
95
+ * hirers holds all of their grants at once and the holder is the same agent
96
+ * in every one, so without a lane, spend authority one customer gave is
97
+ * spendable while the agent works for another. ConnectionsClient and
98
+ * ArtifactsClient send the same header for the same reason.
99
+ *
100
+ * Omitting it cannot widen anything — a stated lane can only cause a
101
+ * refusal — but it does leave the spend unfenced, so a caller that knows
102
+ * its lane should pass it.
103
+ */
104
+ constructor(operatorKey: string, agentId?: string, baseUrl?: string, laneId?: string);
91
105
  balance(): Promise<WalletBalance>;
92
106
  resolve({ userId, agentId }?: {
93
107
  userId?: string;
@@ -99,7 +113,12 @@ export declare class PaymentsClient {
99
113
  idempotencyKey?: string;
100
114
  description?: string;
101
115
  paymentGrantId?: string;
102
- /** The engagement this spend belongs to, so its own grant is preferred. */
116
+ /**
117
+ * The hire this spend belongs to. Used for attribution, and to prefer that
118
+ * hire's own grant over a standing one — a preference about which budget is
119
+ * billed, never about which grants may be spent. That answer is the
120
+ * server's, and it turns on who gave the grant.
121
+ */
103
122
  agreementId?: string;
104
123
  }): Promise<TransferResult>;
105
124
  hold({ amount, idempotencyKey, description, paymentGrantId, agreementId, }: {
@@ -107,7 +126,12 @@ export declare class PaymentsClient {
107
126
  idempotencyKey?: string;
108
127
  description?: string;
109
128
  paymentGrantId?: string;
110
- /** The engagement this spend belongs to, so its own grant is preferred. */
129
+ /**
130
+ * The hire this spend belongs to. Used for attribution, and to prefer that
131
+ * hire's own grant over a standing one — a preference about which budget is
132
+ * billed, never about which grants may be spent. That answer is the
133
+ * server's, and it turns on who gave the grant.
134
+ */
111
135
  agreementId?: string;
112
136
  }): Promise<HoldResult>;
113
137
  /**
@@ -18,12 +18,27 @@ export class PaymentsClient {
18
18
  operatorKey;
19
19
  agentId;
20
20
  baseUrl;
21
- constructor(operatorKey, agentId, baseUrl) {
21
+ laneId;
22
+ /**
23
+ * @param laneId the wake's lane (a chat id, or `agrn-<agreementId>`), sent as
24
+ * X-Ziggs-Lane. It tells the backend which hire this wake is in, and the
25
+ * backend reads off it WHO the spend is for. An agent that serves several
26
+ * hirers holds all of their grants at once and the holder is the same agent
27
+ * in every one, so without a lane, spend authority one customer gave is
28
+ * spendable while the agent works for another. ConnectionsClient and
29
+ * ArtifactsClient send the same header for the same reason.
30
+ *
31
+ * Omitting it cannot widen anything — a stated lane can only cause a
32
+ * refusal — but it does leave the spend unfenced, so a caller that knows
33
+ * its lane should pass it.
34
+ */
35
+ constructor(operatorKey, agentId, baseUrl, laneId) {
22
36
  if (!operatorKey)
23
37
  throw new Error('PaymentsClient: operatorKey is required');
24
38
  this.operatorKey = operatorKey;
25
39
  this.agentId = agentId;
26
40
  this.baseUrl = baseUrl || getBackendUrl();
41
+ this.laneId = laneId;
27
42
  }
28
43
  async balance() {
29
44
  const w = (await this._get('/payments/wallet'));
@@ -180,7 +195,7 @@ export class PaymentsClient {
180
195
  * (retired GET /payments/grants), following the cursor to completion.
181
196
  */
182
197
  async listGrants() {
183
- const grants = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl);
198
+ const grants = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl, this.laneId);
184
199
  return grants.listAllGrants({ scopeKind: 'wallet', health: 'active' });
185
200
  }
186
201
  async createTopUpIntent({ amount, description, currency, } = {}) {
@@ -261,7 +276,7 @@ export class PaymentsClient {
261
276
  async _request(method, path, body) {
262
277
  const init = {
263
278
  method,
264
- headers: buildOperatorHeaders(this.operatorKey, this.agentId, body !== undefined ? { 'content-type': 'application/json' } : {}),
279
+ headers: buildOperatorHeaders(this.operatorKey, this.agentId, body !== undefined ? { 'content-type': 'application/json' } : {}, this.laneId),
265
280
  };
266
281
  if (body !== undefined)
267
282
  init.body = JSON.stringify(body);
@@ -1,17 +1,10 @@
1
1
  import { getBackendUrl } from '../utils/urlUtils.js';
2
2
  import { partyActorIds } from '../types.js';
3
3
  import { throwApiError } from '../shared/apiError.js';
4
+ import { buildCredsHeaders } from './operatorHeaders.js';
4
5
  function getTaskBaseUrl() { return `${getBackendUrl()}/tasks`; }
5
- function buildHeaders(creds) {
6
- return {
7
- 'content-type': 'application/json',
8
- Authorization: `Bearer ${creds.operatorKey}`,
9
- 'X-Agent-Id': creds.agentId,
10
- // the wake's lane, so the backend can fence this call to the
11
- // engagement it belongs to rather than the agent's whole authority.
12
- ...(creds.laneId ? { 'X-Ziggs-Lane': creds.laneId } : {}),
13
- };
14
- }
6
+ // Auth, agent and the wake's lane, from the one helper — see operatorHeaders.
7
+ const buildHeaders = buildCredsHeaders;
15
8
  function assertCreds(creds, op) {
16
9
  if (!creds?.operatorKey)
17
10
  throw new Error(`operatorKey is required for ${op}`);
@@ -165,7 +158,7 @@ export async function getActiveTasksForAgent(agentId, creds) {
165
158
  * assignee's own work list hands over a task nobody woke it for. Every caller of
166
159
  * this function is asking "what can I act on now", and for a withheld row the
167
160
  * answer is nothing — `updateTaskState` refuses it with `dependencies_unmet`.
168
- * Two of AgentHost's three callers do worse than waste a wake: the needs gate
161
+ * Two of Agent's three callers do worse than waste a wake: the needs gate
169
162
  * and the unmet-access sweep both FAIL the tasks they find, so a withheld node
170
163
  * was one missing grant away from being failed before its turn came.
171
164
  *
@@ -23,8 +23,9 @@ export { PaymentsClient } from './PaymentsClient.js';
23
23
  export type { PaymentsError, WalletBalance, WalletRef, PaymentTransactionView, TransferResult, HoldResult, ReleaseResult, PaymentGrantView, PaymentGrantEnvelope, RevokeGrantResult, PaymentApproval, WaitForApprovalResult, } from './PaymentsClient.js';
24
24
  export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
25
25
  export type { ConnectionsError, ConnectionProxyParams, ConnectionGrant, ConnectionWithGrants, McpConnectionRequestParams, } from './ConnectionsClient.js';
26
- export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, isMcpOAuthDelegateAgentId, MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX, } from './OrgsClient.js';
26
+ export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, } from './OrgsClient.js';
27
27
  export type { MyOrg, OrgResolution } from './OrgsClient.js';
28
28
  export { AgentSearchClient } from './AgentSearchClient.js';
29
29
  export { TelemetryClient } from './TelemetryClient.js';
30
30
  export { InboxClient } from './InboxClient.js';
31
+ export { WAKE_LANE_HEADER, laneHeader, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
@@ -15,7 +15,11 @@ export { ContextGrantsClient } from './ContextGrantsClient.js';
15
15
  export { grantCaveat, CONTEXT_GRANT_SCOPE_KINDS, GRANT_SCOPE_KINDS, } from './grants.js';
16
16
  export { PaymentsClient } from './PaymentsClient.js';
17
17
  export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
18
- export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, isMcpOAuthDelegateAgentId, MCP_OAUTH_DELEGATE_AGENT_ID_PREFIX, } from './OrgsClient.js';
18
+ export { fetchMyOrgs, resolveOrgSelector, fetchDelegateAccess, fetchHostedAgentAccess, fetchSessionAccess, } from './OrgsClient.js';
19
19
  export { AgentSearchClient } from './AgentSearchClient.js';
20
20
  export { TelemetryClient } from './TelemetryClient.js';
21
21
  export { InboxClient } from './InboxClient.js';
22
+ // The lane header, built in one place — see operatorHeaders.ts. Exported
23
+ // because the MCP transports in agent-sdk and ziggs-mcp send it too, and a
24
+ // copy of a header nobody notices missing goes stale unseen.
25
+ export { WAKE_LANE_HEADER, laneHeader, buildOperatorHeaders, buildCredsHeaders, } from './operatorHeaders.js';
@@ -1,8 +1,21 @@
1
1
  /**
2
- * Auth headers for operator-key HTTP clients. `X-Agent-Id` is only sent when
3
- * an agentId is present — agent-scoped keys identify the agent themselves;
4
- * fleet keys must pass one.
2
+ * Auth headers for operator-key HTTP clients.
3
+ *
4
+ * ONE place builds them, because the lane is the header a caller cannot be
5
+ * trusted to remember. Omitting `X-Ziggs-Lane` never fails: the server fences
6
+ * the wake to the agent's own org instead, so a door that forgets it answers
7
+ * short lists and refuses grants the agent's customer gave it — silently, and
8
+ * only for the agents that serve someone other than their owner. A header
9
+ * whose absence is indistinguishable from a legitimate answer has to be built
10
+ * in one place, not copied into each caller.
5
11
  */
12
+ /** The header the backend's wake fence reads (`access/wake-fence.ts`). */
13
+ export declare const WAKE_LANE_HEADER = "X-Ziggs-Lane";
14
+ /**
15
+ * The lane header, or nothing. `laneId` is a chat id, or `agrn-<agreementId>`
16
+ * for a task with no origin chat.
17
+ */
18
+ export declare function laneHeader(laneId?: string): Record<string, string>;
6
19
  export declare function buildOperatorHeaders(operatorKey: string, agentId?: string, extra?: Record<string, string>,
7
20
  /**
8
21
  * the lane this call belongs to, sent as `X-Ziggs-Lane`. The
@@ -10,3 +23,9 @@ export declare function buildOperatorHeaders(operatorKey: string, agentId?: stri
10
23
  * narrows to the agent's own org, so it can never widen reach.
11
24
  */
12
25
  laneId?: string): Record<string, string>;
26
+ /** What a `Creds`-shaped client sends on a JSON call: auth, agent, lane. */
27
+ export declare function buildCredsHeaders(creds: {
28
+ operatorKey: string;
29
+ agentId: string;
30
+ laneId?: string;
31
+ }): Record<string, string>;
@@ -1,8 +1,23 @@
1
1
  /**
2
- * Auth headers for operator-key HTTP clients. `X-Agent-Id` is only sent when
3
- * an agentId is present — agent-scoped keys identify the agent themselves;
4
- * fleet keys must pass one.
2
+ * Auth headers for operator-key HTTP clients.
3
+ *
4
+ * ONE place builds them, because the lane is the header a caller cannot be
5
+ * trusted to remember. Omitting `X-Ziggs-Lane` never fails: the server fences
6
+ * the wake to the agent's own org instead, so a door that forgets it answers
7
+ * short lists and refuses grants the agent's customer gave it — silently, and
8
+ * only for the agents that serve someone other than their owner. A header
9
+ * whose absence is indistinguishable from a legitimate answer has to be built
10
+ * in one place, not copied into each caller.
5
11
  */
12
+ /** The header the backend's wake fence reads (`access/wake-fence.ts`). */
13
+ export const WAKE_LANE_HEADER = 'X-Ziggs-Lane';
14
+ /**
15
+ * The lane header, or nothing. `laneId` is a chat id, or `agrn-<agreementId>`
16
+ * for a task with no origin chat.
17
+ */
18
+ export function laneHeader(laneId) {
19
+ return laneId ? { [WAKE_LANE_HEADER]: laneId } : {};
20
+ }
6
21
  export function buildOperatorHeaders(operatorKey, agentId, extra,
7
22
  /**
8
23
  * the lane this call belongs to, sent as `X-Ziggs-Lane`. The
@@ -12,8 +27,17 @@ export function buildOperatorHeaders(operatorKey, agentId, extra,
12
27
  laneId) {
13
28
  return {
14
29
  Authorization: `Bearer ${operatorKey}`,
30
+ // `X-Agent-Id` is only sent when an agentId is present — agent-scoped keys
31
+ // identify the agent themselves; fleet keys must pass one.
15
32
  ...(agentId ? { 'X-Agent-Id': agentId } : {}),
16
- ...(laneId ? { 'X-Ziggs-Lane': laneId } : {}),
33
+ ...laneHeader(laneId),
17
34
  ...extra,
18
35
  };
19
36
  }
37
+ /** What a `Creds`-shaped client sends on a JSON call: auth, agent, lane. */
38
+ export function buildCredsHeaders(creds) {
39
+ return {
40
+ 'content-type': 'application/json',
41
+ ...buildOperatorHeaders(creds.operatorKey, creds.agentId, undefined, creds.laneId),
42
+ };
43
+ }
@@ -39,6 +39,11 @@ export interface GrantSelection {
39
39
  * spend names one, then the standing grant. A standing grant is the buyer
40
40
  * bootstrap and the right default; reaching for it while an agreement grant
41
41
  * covers the same spend would bill the wrong budget.
42
+ *
43
+ * This is a preference, never a fence. Every grant here is already one the
44
+ * server said this wake may spend, so picking a different one cannot reach
45
+ * another party's authority — it would only bill the wrong budget of the same
46
+ * party. That is why the mismatch is no longer a rejection above.
42
47
  */
43
48
  export declare function selectPaymentGrant(grants: readonly GrantView[], spend: SpendShape): GrantSelection;
44
49
  /**
@@ -52,18 +52,18 @@ function whyNot(grant, spend) {
52
52
  if (allowed && spend.toWalletId && !allowed.includes(spend.toWalletId)) {
53
53
  return `may only pay ${allowed.join(', ')}, and this one pays ${spend.toWalletId}`;
54
54
  }
55
- // An agreement-conferred grant is FOR that agreement's work. Spending it on
56
- // something else would pass every caveat above and still be the wrong money:
57
- // the ceiling was agreed for one engagement, and nothing on the wire would
58
- // show it had been used for another.
59
- if (grant.agreementId) {
60
- if (!spend.agreementId) {
61
- return `belongs to agreement ${grant.agreementId}, and this spend names no agreement`;
62
- }
63
- if (grant.agreementId !== spend.agreementId) {
64
- return `belongs to agreement ${grant.agreementId}, not ${spend.agreementId}`;
65
- }
66
- }
55
+ // A grant's agreement is NOT checked here, deliberately.
56
+ //
57
+ // It used to be refused when it did not match the spend's agreement, which
58
+ // made a customer's own grant unusable inside their second hire of the same
59
+ // agent, and made a standing grant unusable on any spend that named a deal.
60
+ // Whether a grant may be spent at all is the server's answer, and it turns on
61
+ // who GAVE the grant against who the agent is acting for — not on which of
62
+ // that party's deals is running. A list the server answered for this wake
63
+ // holds only grants it may spend, so there is nothing left to refuse here.
64
+ //
65
+ // The agreement still decides the ORDER below, which is a budget preference
66
+ // rather than a question of authority.
67
67
  return null;
68
68
  }
69
69
  /**
@@ -73,6 +73,11 @@ function whyNot(grant, spend) {
73
73
  * spend names one, then the standing grant. A standing grant is the buyer
74
74
  * bootstrap and the right default; reaching for it while an agreement grant
75
75
  * covers the same spend would bill the wrong budget.
76
+ *
77
+ * This is a preference, never a fence. Every grant here is already one the
78
+ * server said this wake may spend, so picking a different one cannot reach
79
+ * another party's authority — it would only bill the wrong budget of the same
80
+ * party. That is why the mismatch is no longer a rejection above.
76
81
  */
77
82
  export function selectPaymentGrant(grants, spend) {
78
83
  const rejected = [];
@@ -86,10 +91,15 @@ export function selectPaymentGrant(grants, spend) {
86
91
  }
87
92
  if (fitting.length === 0)
88
93
  return { grant: null, rejected };
89
- const forAgreement = spend.agreementId
90
- ? fitting.find((g) => g.agreementId === spend.agreementId)
91
- : undefined;
92
- return { grant: forAgreement ?? fitting[0], rejected };
94
+ // Explicit both ways, because the deal is no longer a fence: until it was
95
+ // removed, a deal-bound grant simply never reached this list on a spend that
96
+ // named nothing, so "prefer the standing grant" happened by accident of the
97
+ // rejection. Now it has to be said.
98
+ const standing = fitting.find((g) => !g.agreementId);
99
+ const preferred = spend.agreementId
100
+ ? (fitting.find((g) => g.agreementId === spend.agreementId) ?? standing)
101
+ : standing;
102
+ return { grant: preferred ?? fitting[0], rejected };
93
103
  }
94
104
  /**
95
105
  * The sentence an agent gets when nothing fits.
package/dist/index.d.ts CHANGED
@@ -12,3 +12,6 @@ export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiE
12
12
  export { ApiError } from './types.js';
13
13
  export type { Creds, Task, TaskState, PlanStep, PlanStepStatus, Agreement, AgreementParties, AgreementPartySide, EngagementKind, BroadcastAudience, EntryType, ContentType, MessageMetadata, MessageHandler, InboxDeliveryKind, InboxDeliveryRef, InboxChatNews, InboxProposalRef, InboxTaskRef, InboxRequestRef, InboxConnectionRequestRef, InboxHumanAttention, InboxEnvelope, InboxAckResult, InboxReadOptions, } from './types.js';
14
14
  export type { ProposeTerms, ProposeDirectInput, ProposeBroadcastInput, ProposeAgreementData, ClaimOptions, } from './http/AgreementClient.js';
15
+ export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
16
+ export * from './shared/operatorKey.js';
17
+ export * from './utils/appUrls.js';
package/dist/index.js CHANGED
@@ -13,3 +13,6 @@ export { RateLimitedError, isRateLimited, parseRetryAfterMs, pollSurfaceError, }
13
13
  // half of them had drifted).
14
14
  export { parseErrorMessage, parseErrorCode, throwApiError, } from './shared/apiError.js';
15
15
  export { ApiError } from './types.js';
16
+ export { agreementLaneId, agreementIdFromLane, isAgreementLaneId, isAgreementId } from '@ziggs-ai/contracts';
17
+ export * from './shared/operatorKey.js';
18
+ export * from './utils/appUrls.js';
@@ -0,0 +1,47 @@
1
+ /** JWT payload fields we read client-side (signature not verified — identity hint only). */
2
+ export interface OperatorKeyClaims {
3
+ type?: string;
4
+ keyId?: string;
5
+ ownerId?: string;
6
+ boundAgentId?: string | null;
7
+ /**
8
+ * How the key was authorized. MCP consent uses `mcp_oauth` or `device_code`;
9
+ * provisioning and other mint paths carry their own provenance.
10
+ */
11
+ issuedVia?: string;
12
+ exp?: number;
13
+ }
14
+ /** Decode operator JWT payload without verifying signature (boundAgentId). */
15
+ export declare function decodeOperatorKeyClaims(token: string): OperatorKeyClaims | null;
16
+ export declare function isOperatorKeyExpired(claims: OperatorKeyClaims | null): boolean;
17
+ /** The stamp the MCP OAuth authorization-code consent flow puts on its tokens. */
18
+ export declare const ISSUED_VIA_MCP_OAUTH = "mcp_oauth";
19
+ /** The stamp the MCP OAuth device-code flow puts on its tokens. */
20
+ export declare const ISSUED_VIA_DEVICE_CODE = "device_code";
21
+ /**
22
+ * Did a person board this credential through the connector directory?
23
+ *
24
+ * That is the question the tool surface turns on: a connected assistant
25
+ * (Claude, ChatGPT, Cursor) gets the listed, directory-reviewed surface, and
26
+ * anyone who configured this server themselves with an operator key gets the
27
+ * whole thing.
28
+ *
29
+ * Ask it as a predicate, never as `issuedVia === 'mcp_oauth'`. Two consent rails
30
+ * board an assistant through that door — the authorization-code flow and the
31
+ * device-code flow — and both want the same narrowed surface, but the server
32
+ * records them as the different acts they are. An equality test against one
33
+ * value serves the whole catalogue to every credential from the other one.
34
+ *
35
+ * Read off the unverified payload. On the remote endpoint the backend verified
36
+ * the signature before this ran; on stdio the key is the caller's own. Either
37
+ * way the payload is the one the backend signed. And what hangs on the answer
38
+ * is which tools are registered, not what a call may do: every call is still
39
+ * authorized by the backend. Provenance may decide what is OFFERED, never what
40
+ * is PERMITTED.
41
+ */
42
+ export declare function isDirectoryBoarded(claims: OperatorKeyClaims | null): boolean;
43
+ /** Routing hint only. Every request is still authenticated by the backend. */
44
+ export declare function isMcpOAuthDelegateSession(creds: {
45
+ operatorKey: string;
46
+ agentId: string;
47
+ }): boolean;
@@ -0,0 +1,64 @@
1
+ /** Decode operator JWT payload without verifying signature (boundAgentId). */
2
+ export function decodeOperatorKeyClaims(token) {
3
+ const trimmed = token.trim();
4
+ const parts = trimmed.split('.');
5
+ if (parts.length !== 3)
6
+ return null;
7
+ try {
8
+ const encoded = parts[1].replace(/-/g, '+').replace(/_/g, '/');
9
+ const json = new TextDecoder().decode(Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0)));
10
+ const payload = JSON.parse(json);
11
+ return {
12
+ type: payload.type,
13
+ keyId: payload.keyId,
14
+ ownerId: payload.ownerId,
15
+ boundAgentId: typeof payload.boundAgentId === 'string' ? payload.boundAgentId : null,
16
+ issuedVia: typeof payload.issuedVia === 'string' ? payload.issuedVia : undefined,
17
+ exp: payload.exp,
18
+ };
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function isOperatorKeyExpired(claims) {
25
+ if (!claims?.exp)
26
+ return false;
27
+ return claims.exp * 1000 <= Date.now();
28
+ }
29
+ /** The stamp the MCP OAuth authorization-code consent flow puts on its tokens. */
30
+ export const ISSUED_VIA_MCP_OAUTH = 'mcp_oauth';
31
+ /** The stamp the MCP OAuth device-code flow puts on its tokens. */
32
+ export const ISSUED_VIA_DEVICE_CODE = 'device_code';
33
+ /**
34
+ * Did a person board this credential through the connector directory?
35
+ *
36
+ * That is the question the tool surface turns on: a connected assistant
37
+ * (Claude, ChatGPT, Cursor) gets the listed, directory-reviewed surface, and
38
+ * anyone who configured this server themselves with an operator key gets the
39
+ * whole thing.
40
+ *
41
+ * Ask it as a predicate, never as `issuedVia === 'mcp_oauth'`. Two consent rails
42
+ * board an assistant through that door — the authorization-code flow and the
43
+ * device-code flow — and both want the same narrowed surface, but the server
44
+ * records them as the different acts they are. An equality test against one
45
+ * value serves the whole catalogue to every credential from the other one.
46
+ *
47
+ * Read off the unverified payload. On the remote endpoint the backend verified
48
+ * the signature before this ran; on stdio the key is the caller's own. Either
49
+ * way the payload is the one the backend signed. And what hangs on the answer
50
+ * is which tools are registered, not what a call may do: every call is still
51
+ * authorized by the backend. Provenance may decide what is OFFERED, never what
52
+ * is PERMITTED.
53
+ */
54
+ export function isDirectoryBoarded(claims) {
55
+ return (claims?.issuedVia === ISSUED_VIA_MCP_OAUTH ||
56
+ claims?.issuedVia === ISSUED_VIA_DEVICE_CODE);
57
+ }
58
+ /** Routing hint only. Every request is still authenticated by the backend. */
59
+ export function isMcpOAuthDelegateSession(creds) {
60
+ const claims = decodeOperatorKeyClaims(creds.operatorKey);
61
+ return (!!creds.agentId &&
62
+ claims?.boundAgentId === creds.agentId &&
63
+ isDirectoryBoarded(claims));
64
+ }