@ziggs-ai/api-client 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http/ArtifactsClient.d.ts +11 -0
- package/dist/http/ArtifactsClient.js +35 -5
- package/dist/http/ConnectionsClient.d.ts +89 -0
- package/dist/http/ConnectionsClient.js +154 -0
- package/dist/http/ContextDiscoveryClient.d.ts +3 -14
- package/dist/http/ContextDiscoveryClient.js +3 -12
- package/dist/http/ContextGrantsClient.d.ts +27 -0
- package/dist/http/ContextGrantsClient.js +25 -0
- package/dist/http/GrantsClient.d.ts +45 -0
- package/dist/http/GrantsClient.js +72 -0
- package/dist/http/PaymentsClient.d.ts +94 -0
- package/dist/http/PaymentsClient.js +231 -0
- package/dist/http/grantRails.d.ts +20 -0
- package/dist/http/grantRails.js +50 -0
- package/dist/http/grants.d.ts +6 -0
- package/dist/http/index.d.ts +10 -2
- package/dist/http/index.js +4 -0
- package/dist/websocket/WebSocketClient.js +9 -0
- package/package.json +1 -1
|
@@ -49,5 +49,16 @@ export declare class ArtifactsClient {
|
|
|
49
49
|
*/
|
|
50
50
|
recordThought(chatId: string, text: string): Promise<void>;
|
|
51
51
|
write(input: WriteArtifactInput): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* ZIG-899 — the deliberate-record variant: a deliverable the model chose to
|
|
54
|
+
* record must fail loudly and hand back the artifactId, unlike `write`'s
|
|
55
|
+
* soft-fail breadcrumb contract. This is the one wire call for both agent
|
|
56
|
+
* surfaces (the SDK record_artifact tool and ziggs-mcp's
|
|
57
|
+
* ziggs_record_artifact).
|
|
58
|
+
*/
|
|
59
|
+
writeStrict(input: WriteArtifactInput): Promise<{
|
|
60
|
+
artifactId?: string;
|
|
61
|
+
}>;
|
|
62
|
+
private _assertScopeXor;
|
|
52
63
|
private _headers;
|
|
53
64
|
}
|
|
@@ -60,9 +60,28 @@ export class ArtifactsClient {
|
|
|
60
60
|
async write(input) {
|
|
61
61
|
if (!input.text || !input.text.trim())
|
|
62
62
|
return;
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
this._assertScopeXor(input);
|
|
64
|
+
try {
|
|
65
|
+
await this.writeStrict(input);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
// Soft-fail on the wire only: breadcrumb writes are not load-bearing.
|
|
69
|
+
// Caller mistakes (scope xor, empty text) still throw above.
|
|
70
|
+
runtimeLog.warn('ArtifactsClient', `⚠️ ${e.message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* ZIG-899 — the deliberate-record variant: a deliverable the model chose to
|
|
75
|
+
* record must fail loudly and hand back the artifactId, unlike `write`'s
|
|
76
|
+
* soft-fail breadcrumb contract. This is the one wire call for both agent
|
|
77
|
+
* surfaces (the SDK record_artifact tool and ziggs-mcp's
|
|
78
|
+
* ziggs_record_artifact).
|
|
79
|
+
*/
|
|
80
|
+
async writeStrict(input) {
|
|
81
|
+
if (!input.text || !input.text.trim()) {
|
|
82
|
+
throw new Error('ArtifactsClient.writeStrict: text is required');
|
|
65
83
|
}
|
|
84
|
+
this._assertScopeXor(input);
|
|
66
85
|
const url = `${getBackendUrl()}/artifacts`;
|
|
67
86
|
const res = await fetch(url, {
|
|
68
87
|
method: 'POST',
|
|
@@ -77,10 +96,21 @@ export class ArtifactsClient {
|
|
|
77
96
|
service: input.service,
|
|
78
97
|
}),
|
|
79
98
|
});
|
|
99
|
+
const body = await res.text().catch(() => '');
|
|
80
100
|
if (!res.ok) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
101
|
+
throw new Error(`POST /artifacts ${res.status} ${res.statusText} ${body.slice(0, 200)}`);
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
105
|
+
return { artifactId: parsed.artifactId };
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
_assertScopeXor(input) {
|
|
112
|
+
if ((input.chatId && input.agreementId) || (!input.chatId && !input.agreementId)) {
|
|
113
|
+
throw new Error('ArtifactsClient.write: pass exactly one of chatId or agreementId');
|
|
84
114
|
}
|
|
85
115
|
}
|
|
86
116
|
_headers() {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
export declare function assertNoLeakedConnectionSecret(serialized: string): void;
|
|
3
|
+
/** Thrown by ConnectionsClient with the HTTP status and raw body attached. */
|
|
4
|
+
export interface ConnectionsError extends Error {
|
|
5
|
+
status?: number;
|
|
6
|
+
body?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ConnectionProxyParams {
|
|
9
|
+
connectionId: string;
|
|
10
|
+
grantId: string;
|
|
11
|
+
action: string;
|
|
12
|
+
payload?: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface McpConnectionRequestParams {
|
|
15
|
+
serverUrl: string;
|
|
16
|
+
tools: string[];
|
|
17
|
+
reason?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Working chat the consent card is opened into. Required by the backend for
|
|
20
|
+
* the agent-tool path; the per-user gateway trigger (ZIG-771) omits it.
|
|
21
|
+
*/
|
|
22
|
+
chatId?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Target the consent request at this end-user (X-On-Behalf-Of-User) instead
|
|
25
|
+
* of the operator — the per-user MCP gateway's connect prompt (ZIG-771).
|
|
26
|
+
*/
|
|
27
|
+
onBehalfOfUserId?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* ZIG-894 — the one connections client for every surface. Consolidates the
|
|
31
|
+
* former agent-sdk private ZiggsConnectClient and ziggs-mcp's inline fetches
|
|
32
|
+
* (proxyConnection, createMcpConnectionRequest). Proxied provider calls never
|
|
33
|
+
* return the raw OAuth token; every proxy response is additionally scanned by
|
|
34
|
+
* the client-side leak guard before it reaches the caller. Grant *listing*
|
|
35
|
+
* across connections is the unified GET /grants; the per-connection grant
|
|
36
|
+
* mutations (issue / attenuate / revoke) live here.
|
|
37
|
+
*/
|
|
38
|
+
export declare class ConnectionsClient {
|
|
39
|
+
private readonly operatorKey;
|
|
40
|
+
private readonly agentId?;
|
|
41
|
+
private readonly baseUrl;
|
|
42
|
+
constructor(operatorKey: string, agentId?: string, baseUrl?: string);
|
|
43
|
+
/**
|
|
44
|
+
* Present a ConnectionGrant to the broker and perform a provider action.
|
|
45
|
+
* Returns the provider result — never the raw OAuth token. Requires an
|
|
46
|
+
* impersonated agent (the grant holder).
|
|
47
|
+
*/
|
|
48
|
+
proxy({ connectionId, grantId, action, payload }: ConnectionProxyParams): Promise<unknown>;
|
|
49
|
+
/**
|
|
50
|
+
* List ConnectionGrants on a connection. When impersonating an agent the
|
|
51
|
+
* server already scopes rows to that holder; the client-side filter is kept
|
|
52
|
+
* as defense-in-depth (same behavior as the former ZiggsConnectClient).
|
|
53
|
+
*/
|
|
54
|
+
listGrants({ connectionId }: {
|
|
55
|
+
connectionId: string;
|
|
56
|
+
}): Promise<unknown[]>;
|
|
57
|
+
/** Issue a connection grant to an agent holder (connection owner side). */
|
|
58
|
+
issueGrant({ connectionId, holderId, caveats, }: {
|
|
59
|
+
connectionId: string;
|
|
60
|
+
holderId: string;
|
|
61
|
+
caveats?: unknown;
|
|
62
|
+
}): Promise<unknown>;
|
|
63
|
+
/**
|
|
64
|
+
* Attenuate (re-delegate) a connection grant with tighter caveats. Narrowing
|
|
65
|
+
* a grant whose connection you own mints immediately; a grant whose owner is
|
|
66
|
+
* a different party returns `{ status: 'pending_approval', agreementId }`
|
|
67
|
+
* (ZIG-701 consent flow) and no grant is minted yet.
|
|
68
|
+
*/
|
|
69
|
+
attenuateGrant({ connectionId, grantId, holderId, caveats, }: {
|
|
70
|
+
connectionId: string;
|
|
71
|
+
grantId: string;
|
|
72
|
+
holderId: string;
|
|
73
|
+
caveats?: unknown;
|
|
74
|
+
}): Promise<unknown>;
|
|
75
|
+
/** Revoke a single connection grant. */
|
|
76
|
+
revokeGrant({ connectionId, grantId, }: {
|
|
77
|
+
connectionId: string;
|
|
78
|
+
grantId: string;
|
|
79
|
+
}): Promise<unknown>;
|
|
80
|
+
/**
|
|
81
|
+
* ZIG-686 — agent-initiated MCP connection request: ask the principal to
|
|
82
|
+
* connect a remote MCP server and grant this agent the listed tools. Opens a
|
|
83
|
+
* connection-consent agreement in the working chat (ZIG-798); on approval the
|
|
84
|
+
* server is connected (if needed) and the agent is granted the tools.
|
|
85
|
+
*/
|
|
86
|
+
requestMcpConnection({ serverUrl, tools, reason, chatId, onBehalfOfUserId, }: McpConnectionRequestParams): Promise<Record<string, unknown>>;
|
|
87
|
+
private _requestRaw;
|
|
88
|
+
private _request;
|
|
89
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
+
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
4
|
+
// ZIG-569 — defense-in-depth mirror of the backend leak-guard
|
|
5
|
+
// (assertProxyResponseDoesNotLeakTokens). The backend strips the *specific*
|
|
6
|
+
// vault token from the response; clients never see that token, so this layer
|
|
7
|
+
// instead pattern-scans the proxied body for high-signal provider credential
|
|
8
|
+
// shapes and refuses to hand a likely-leaked secret to the model.
|
|
9
|
+
const LEAKED_SECRET_PATTERNS = [
|
|
10
|
+
/\bgh[posru]_[A-Za-z0-9]{16,}\b/, // GitHub PAT / OAuth / user / server / refresh
|
|
11
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/, // fine-grained GitHub PAT
|
|
12
|
+
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, // Slack bot/user/app/refresh tokens
|
|
13
|
+
/\bxapp-[A-Za-z0-9-]{10,}\b/, // Slack app-level token
|
|
14
|
+
/"(?:access_token|refresh_token)"\s*:\s*"[^"]{8,}"/, // raw OAuth token JSON keys
|
|
15
|
+
];
|
|
16
|
+
export function assertNoLeakedConnectionSecret(serialized) {
|
|
17
|
+
for (const re of LEAKED_SECRET_PATTERNS) {
|
|
18
|
+
if (re.test(serialized)) {
|
|
19
|
+
throw new Error('connection proxy response withheld: it appears to contain a credential (token-leak guard)');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* ZIG-894 — the one connections client for every surface. Consolidates the
|
|
25
|
+
* former agent-sdk private ZiggsConnectClient and ziggs-mcp's inline fetches
|
|
26
|
+
* (proxyConnection, createMcpConnectionRequest). Proxied provider calls never
|
|
27
|
+
* return the raw OAuth token; every proxy response is additionally scanned by
|
|
28
|
+
* the client-side leak guard before it reaches the caller. Grant *listing*
|
|
29
|
+
* across connections is the unified GET /grants; the per-connection grant
|
|
30
|
+
* mutations (issue / attenuate / revoke) live here.
|
|
31
|
+
*/
|
|
32
|
+
export class ConnectionsClient {
|
|
33
|
+
operatorKey;
|
|
34
|
+
agentId;
|
|
35
|
+
baseUrl;
|
|
36
|
+
constructor(operatorKey, agentId, baseUrl) {
|
|
37
|
+
if (!operatorKey)
|
|
38
|
+
throw new Error('ConnectionsClient: operatorKey is required');
|
|
39
|
+
this.operatorKey = operatorKey;
|
|
40
|
+
this.agentId = agentId;
|
|
41
|
+
this.baseUrl = baseUrl || getBackendUrl();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Present a ConnectionGrant to the broker and perform a provider action.
|
|
45
|
+
* Returns the provider result — never the raw OAuth token. Requires an
|
|
46
|
+
* impersonated agent (the grant holder).
|
|
47
|
+
*/
|
|
48
|
+
async proxy({ connectionId, grantId, action, payload }) {
|
|
49
|
+
if (!connectionId)
|
|
50
|
+
throw new Error('proxy: connectionId is required');
|
|
51
|
+
if (!grantId)
|
|
52
|
+
throw new Error('proxy: grantId is required');
|
|
53
|
+
if (!action)
|
|
54
|
+
throw new Error('proxy: action is required');
|
|
55
|
+
if (!this.agentId) {
|
|
56
|
+
throw new Error('proxy: agentId is required — connection broker calls must impersonate the grant holder agent');
|
|
57
|
+
}
|
|
58
|
+
const text = await this._requestRaw('POST', `/connections/${encodeURIComponent(connectionId)}/proxy`, { grantId, action, payload: payload ?? {} });
|
|
59
|
+
assertNoLeakedConnectionSecret(text);
|
|
60
|
+
let parsed = text;
|
|
61
|
+
try {
|
|
62
|
+
parsed = text ? JSON.parse(text) : null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
parsed = text;
|
|
66
|
+
}
|
|
67
|
+
const result = parsed?.['result'];
|
|
68
|
+
return result ?? parsed;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* List ConnectionGrants on a connection. When impersonating an agent the
|
|
72
|
+
* server already scopes rows to that holder; the client-side filter is kept
|
|
73
|
+
* as defense-in-depth (same behavior as the former ZiggsConnectClient).
|
|
74
|
+
*/
|
|
75
|
+
async listGrants({ connectionId }) {
|
|
76
|
+
if (!connectionId)
|
|
77
|
+
throw new Error('listGrants: connectionId is required');
|
|
78
|
+
if (!this.agentId) {
|
|
79
|
+
throw new Error('listGrants: agentId is required — grants are scoped to the impersonated agent');
|
|
80
|
+
}
|
|
81
|
+
const res = (await this._request('GET', `/connections/${encodeURIComponent(connectionId)}/grants`, undefined));
|
|
82
|
+
const grants = res['grants'] || [];
|
|
83
|
+
return grants.filter((g) => g['holderId'] === this.agentId);
|
|
84
|
+
}
|
|
85
|
+
/** Issue a connection grant to an agent holder (connection owner side). */
|
|
86
|
+
async issueGrant({ connectionId, holderId, caveats, }) {
|
|
87
|
+
if (!connectionId)
|
|
88
|
+
throw new Error('issueGrant: connectionId is required');
|
|
89
|
+
if (!holderId)
|
|
90
|
+
throw new Error('issueGrant: holderId is required');
|
|
91
|
+
return this._request('POST', `/connections/${encodeURIComponent(connectionId)}/grants`, {
|
|
92
|
+
holderId,
|
|
93
|
+
caveats,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Attenuate (re-delegate) a connection grant with tighter caveats. Narrowing
|
|
98
|
+
* a grant whose connection you own mints immediately; a grant whose owner is
|
|
99
|
+
* a different party returns `{ status: 'pending_approval', agreementId }`
|
|
100
|
+
* (ZIG-701 consent flow) and no grant is minted yet.
|
|
101
|
+
*/
|
|
102
|
+
async attenuateGrant({ connectionId, grantId, holderId, caveats, }) {
|
|
103
|
+
if (!connectionId)
|
|
104
|
+
throw new Error('attenuateGrant: connectionId is required');
|
|
105
|
+
if (!grantId)
|
|
106
|
+
throw new Error('attenuateGrant: grantId is required');
|
|
107
|
+
if (!holderId)
|
|
108
|
+
throw new Error('attenuateGrant: holderId is required');
|
|
109
|
+
return this._request('POST', `/connections/${encodeURIComponent(connectionId)}/grants/${encodeURIComponent(grantId)}/attenuate`, { holderId, caveats });
|
|
110
|
+
}
|
|
111
|
+
/** Revoke a single connection grant. */
|
|
112
|
+
async revokeGrant({ connectionId, grantId, }) {
|
|
113
|
+
if (!connectionId)
|
|
114
|
+
throw new Error('revokeGrant: connectionId is required');
|
|
115
|
+
if (!grantId)
|
|
116
|
+
throw new Error('revokeGrant: grantId is required');
|
|
117
|
+
return this._request('DELETE', `/connections/${encodeURIComponent(connectionId)}/grants/${encodeURIComponent(grantId)}`, undefined);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* ZIG-686 — agent-initiated MCP connection request: ask the principal to
|
|
121
|
+
* connect a remote MCP server and grant this agent the listed tools. Opens a
|
|
122
|
+
* connection-consent agreement in the working chat (ZIG-798); on approval the
|
|
123
|
+
* server is connected (if needed) and the agent is granted the tools.
|
|
124
|
+
*/
|
|
125
|
+
async requestMcpConnection({ serverUrl, tools, reason, chatId, onBehalfOfUserId, }) {
|
|
126
|
+
if (!serverUrl)
|
|
127
|
+
throw new Error('requestMcpConnection: serverUrl is required');
|
|
128
|
+
return (await this._request('POST', '/connections/mcp/requests', { serverUrl, tools, reason, chatId }, onBehalfOfUserId ? { 'X-On-Behalf-Of-User': onBehalfOfUserId } : undefined));
|
|
129
|
+
}
|
|
130
|
+
async _requestRaw(method, path, body, extraHeaders) {
|
|
131
|
+
const init = {
|
|
132
|
+
method,
|
|
133
|
+
headers: buildOperatorHeaders(this.operatorKey, this.agentId, {
|
|
134
|
+
...(body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
135
|
+
...extraHeaders,
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
if (body !== undefined)
|
|
139
|
+
init.body = JSON.stringify(body);
|
|
140
|
+
const response = await fetch(`${this.baseUrl}${path}`, init);
|
|
141
|
+
const text = await response.text().catch(() => '');
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const err = new Error(`${method} ${path} ${response.status} ${text.slice(0, 200)}`);
|
|
144
|
+
err.status = response.status;
|
|
145
|
+
err.body = text;
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
return text;
|
|
149
|
+
}
|
|
150
|
+
async _request(method, path, body, extraHeaders) {
|
|
151
|
+
const text = await this._requestRaw(method, path, body, extraHeaders);
|
|
152
|
+
return text ? JSON.parse(text) : null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
|
-
export interface ContextReachDescriptor {
|
|
3
|
-
grantId: string;
|
|
4
|
-
scope: {
|
|
5
|
-
kind: 'chat' | 'agreement' | 'org';
|
|
6
|
-
id: string;
|
|
7
|
-
};
|
|
8
|
-
temporal: 'from-now' | 'from-start';
|
|
9
|
-
watermarkAt: string;
|
|
10
|
-
expiresAt: string | null;
|
|
11
|
-
parentGrantId: string | null;
|
|
12
|
-
createdAt: string;
|
|
13
|
-
}
|
|
14
2
|
/** Labels-only pointer to context the agent could request access to (P4). */
|
|
15
3
|
export interface DiscoverableItem {
|
|
16
4
|
type: 'chat';
|
|
@@ -22,7 +10,9 @@ export interface DiscoverableItem {
|
|
|
22
10
|
orgId: string;
|
|
23
11
|
}
|
|
24
12
|
/**
|
|
25
|
-
*
|
|
13
|
+
* P4 discovery — labels-only pointers to context the agent could REQUEST but
|
|
14
|
+
* does not yet hold (`GET /context/discovery/available`). Grants the agent
|
|
15
|
+
* already holds are listed via the unified `GrantsClient` (GET /grants).
|
|
26
16
|
*/
|
|
27
17
|
export declare class ContextDiscoveryClient {
|
|
28
18
|
private readonly operatorKey;
|
|
@@ -33,7 +23,6 @@ export declare class ContextDiscoveryClient {
|
|
|
33
23
|
* @param agentId Required for fleet keys (sent as X-Agent-Id). Omit for agent-scoped keys.
|
|
34
24
|
*/
|
|
35
25
|
constructor(operatorKey: string, agentId?: string, baseUrl?: string);
|
|
36
|
-
discover(): Promise<ContextReachDescriptor[]>;
|
|
37
26
|
/**
|
|
38
27
|
* P4: what context EXISTS in the agent's engaged orgs that it does NOT hold
|
|
39
28
|
* a grant for — so it can request access rather than fail blind. Labels only.
|
|
@@ -2,7 +2,9 @@ import 'dotenv/config';
|
|
|
2
2
|
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
3
|
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* P4 discovery — labels-only pointers to context the agent could REQUEST but
|
|
6
|
+
* does not yet hold (`GET /context/discovery/available`). Grants the agent
|
|
7
|
+
* already holds are listed via the unified `GrantsClient` (GET /grants).
|
|
6
8
|
*/
|
|
7
9
|
export class ContextDiscoveryClient {
|
|
8
10
|
operatorKey;
|
|
@@ -20,17 +22,6 @@ export class ContextDiscoveryClient {
|
|
|
20
22
|
this.agentId = agentId;
|
|
21
23
|
this.baseUrl = baseUrl || getBackendUrl();
|
|
22
24
|
}
|
|
23
|
-
async discover() {
|
|
24
|
-
const res = await fetch(`${this.baseUrl}/context/discovery`, {
|
|
25
|
-
headers: buildOperatorHeaders(this.operatorKey, this.agentId),
|
|
26
|
-
});
|
|
27
|
-
const body = await res.text().catch(() => '');
|
|
28
|
-
if (!res.ok) {
|
|
29
|
-
throw new Error(`ContextDiscoveryClient.discover ${res.status} ${body.slice(0, 200)}`);
|
|
30
|
-
}
|
|
31
|
-
const parsed = JSON.parse(body);
|
|
32
|
-
return parsed.reach ?? [];
|
|
33
|
-
}
|
|
34
25
|
/**
|
|
35
26
|
* P4: what context EXISTS in the agent's engaged orgs that it does NOT hold
|
|
36
27
|
* a grant for — so it can request access rather than fail blind. Labels only.
|
|
@@ -39,6 +39,26 @@ export type DelegateContextGrantResult = {
|
|
|
39
39
|
agreementId: string;
|
|
40
40
|
ownerId?: string;
|
|
41
41
|
};
|
|
42
|
+
/** A single readable entry inside a grant's scope — id + label only (ZIG-870). */
|
|
43
|
+
export interface ReachEntry {
|
|
44
|
+
id: string;
|
|
45
|
+
label: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* ZIG-870 reach expansion — the chat/agreement ids a grant you hold actually
|
|
49
|
+
* covers, so an org/agreement-scoped grant becomes a concrete list you can
|
|
50
|
+
* `context_read` through (via=chat:<id> / agreement:<id>). Ids + labels only,
|
|
51
|
+
* never content. Org scope is capped; `truncatedChats`/`truncatedAgreements`
|
|
52
|
+
* report how many were left off, never silently dropped.
|
|
53
|
+
*/
|
|
54
|
+
export interface GrantReachResult {
|
|
55
|
+
grantId: string;
|
|
56
|
+
scope: ContextGrantScope;
|
|
57
|
+
chats: ReachEntry[];
|
|
58
|
+
agreements: ReachEntry[];
|
|
59
|
+
truncatedChats: number;
|
|
60
|
+
truncatedAgreements: number;
|
|
61
|
+
}
|
|
42
62
|
/**
|
|
43
63
|
* ZIG-411 context grant management — list / issue / delegate / revoke.
|
|
44
64
|
*/
|
|
@@ -53,6 +73,13 @@ export declare class ContextGrantsClient {
|
|
|
53
73
|
constructor(operatorKey: string, agentId?: string, baseUrl?: string);
|
|
54
74
|
issueGrant(input: IssueContextGrantInput): Promise<GrantView>;
|
|
55
75
|
delegateGrant(parentGrantId: string, input: DelegateContextGrantInput): Promise<DelegateContextGrantResult>;
|
|
76
|
+
/**
|
|
77
|
+
* ZIG-870 — expand a grant you hold into the chat/agreement ids inside its
|
|
78
|
+
* scope. A grant is a fence, not a listing: discovery says "you hold
|
|
79
|
+
* org:acme", this says which chats/agreements that covers. Holder-only,
|
|
80
|
+
* labels-only, grant-fenced server-side.
|
|
81
|
+
*/
|
|
82
|
+
getReach(grantId: string): Promise<GrantReachResult>;
|
|
56
83
|
revokeGrant(grantId: string): Promise<{
|
|
57
84
|
status: string;
|
|
58
85
|
revokedCount?: number;
|
|
@@ -88,6 +88,31 @@ export class ContextGrantsClient {
|
|
|
88
88
|
}
|
|
89
89
|
return { status: 'granted', grant: parsed.grant };
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* ZIG-870 — expand a grant you hold into the chat/agreement ids inside its
|
|
93
|
+
* scope. A grant is a fence, not a listing: discovery says "you hold
|
|
94
|
+
* org:acme", this says which chats/agreements that covers. Holder-only,
|
|
95
|
+
* labels-only, grant-fenced server-side.
|
|
96
|
+
*/
|
|
97
|
+
async getReach(grantId) {
|
|
98
|
+
const res = await fetch(`${this.baseUrl}/context/grants/${encodeURIComponent(grantId)}/reach`, { headers: buildOperatorHeaders(this.operatorKey, this.agentId) });
|
|
99
|
+
const body = await res.text().catch(() => '');
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
throwApiError(res, body, `getReach failed: ${res.status} ${res.statusText}`);
|
|
102
|
+
}
|
|
103
|
+
const parsed = JSON.parse(body);
|
|
104
|
+
if (!parsed.scope) {
|
|
105
|
+
throw new Error('Invalid response: expected { scope, chats, agreements } from GET /context/grants/:id/reach');
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
grantId: parsed.grantId ?? grantId,
|
|
109
|
+
scope: parsed.scope,
|
|
110
|
+
chats: parsed.chats ?? [],
|
|
111
|
+
agreements: parsed.agreements ?? [],
|
|
112
|
+
truncatedChats: parsed.truncatedChats ?? 0,
|
|
113
|
+
truncatedAgreements: parsed.truncatedAgreements ?? 0,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
91
116
|
async revokeGrant(grantId) {
|
|
92
117
|
const res = await fetch(`${this.baseUrl}/context/grants/${encodeURIComponent(grantId)}`, {
|
|
93
118
|
method: 'DELETE',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import type { GrantView, GrantScopeKind, GrantHealth } from './grants.js';
|
|
3
|
+
export interface ListGrantsQuery {
|
|
4
|
+
/**
|
|
5
|
+
* Filter to grants held by this agent. Admin-gated server-side when the caller
|
|
6
|
+
* is not impersonating that agent.
|
|
7
|
+
*/
|
|
8
|
+
holderId?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Rail/resource filter. One kind or several (the context rail spans three
|
|
11
|
+
* kinds: chat, agreement, org). Absent = every rail.
|
|
12
|
+
*/
|
|
13
|
+
scopeKind?: GrantScopeKind | GrantScopeKind[];
|
|
14
|
+
/** Filter to grants of one health. */
|
|
15
|
+
health?: GrantHealth;
|
|
16
|
+
cursor?: string;
|
|
17
|
+
limit?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ListGrantsResult {
|
|
20
|
+
items: GrantView[];
|
|
21
|
+
nextCursor: string | null;
|
|
22
|
+
hasMore: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* ZIG-648 — unified grant listing across every rail. `GET /grants` returns the
|
|
26
|
+
* canonical GrantView for each grant the caller holds (or, admin-gated, a named
|
|
27
|
+
* holder), filterable by scope kind and health, with cursor pagination. Replaces
|
|
28
|
+
* the old per-rail listers (context discovery, connection-grants,
|
|
29
|
+
* payment-grants).
|
|
30
|
+
*/
|
|
31
|
+
export declare class GrantsClient {
|
|
32
|
+
private readonly operatorKey;
|
|
33
|
+
private readonly agentId?;
|
|
34
|
+
private readonly baseUrl;
|
|
35
|
+
constructor(operatorKey: string, agentId?: string, baseUrl?: string);
|
|
36
|
+
listGrants(query?: ListGrantsQuery): Promise<ListGrantsResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Every grant matching `query`, following the cursor to completion. Use when a
|
|
39
|
+
* caller needs the whole set (reach tagging, catch-up, an owner listing) rather
|
|
40
|
+
* than a single page — `listGrants` returns only one page (server default 30),
|
|
41
|
+
* so a holder with more grants than a page would otherwise be silently
|
|
42
|
+
* truncated. `maxPages` bounds the loop as a runaway guard.
|
|
43
|
+
*/
|
|
44
|
+
listAllGrants(query?: Omit<ListGrantsQuery, 'cursor'>, maxPages?: number): Promise<GrantView[]>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
+
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
4
|
+
/**
|
|
5
|
+
* ZIG-648 — unified grant listing across every rail. `GET /grants` returns the
|
|
6
|
+
* canonical GrantView for each grant the caller holds (or, admin-gated, a named
|
|
7
|
+
* holder), filterable by scope kind and health, with cursor pagination. Replaces
|
|
8
|
+
* the old per-rail listers (context discovery, connection-grants,
|
|
9
|
+
* payment-grants).
|
|
10
|
+
*/
|
|
11
|
+
export class GrantsClient {
|
|
12
|
+
operatorKey;
|
|
13
|
+
agentId;
|
|
14
|
+
baseUrl;
|
|
15
|
+
constructor(operatorKey, agentId, baseUrl) {
|
|
16
|
+
if (!operatorKey)
|
|
17
|
+
throw new Error('GrantsClient: operatorKey is required');
|
|
18
|
+
this.operatorKey = operatorKey;
|
|
19
|
+
this.agentId = agentId;
|
|
20
|
+
this.baseUrl = baseUrl || getBackendUrl();
|
|
21
|
+
}
|
|
22
|
+
async listGrants(query = {}) {
|
|
23
|
+
const url = new URL(`${this.baseUrl}/grants`);
|
|
24
|
+
if (query.holderId)
|
|
25
|
+
url.searchParams.set('holderId', query.holderId);
|
|
26
|
+
if (query.scopeKind) {
|
|
27
|
+
const kinds = Array.isArray(query.scopeKind)
|
|
28
|
+
? query.scopeKind
|
|
29
|
+
: [query.scopeKind];
|
|
30
|
+
for (const k of kinds)
|
|
31
|
+
url.searchParams.append('scopeKind', k);
|
|
32
|
+
}
|
|
33
|
+
if (query.health)
|
|
34
|
+
url.searchParams.set('health', query.health);
|
|
35
|
+
if (query.cursor)
|
|
36
|
+
url.searchParams.set('cursor', query.cursor);
|
|
37
|
+
if (query.limit != null)
|
|
38
|
+
url.searchParams.set('limit', String(query.limit));
|
|
39
|
+
const res = await fetch(url.toString(), {
|
|
40
|
+
headers: buildOperatorHeaders(this.operatorKey, this.agentId),
|
|
41
|
+
});
|
|
42
|
+
const body = await res.text().catch(() => '');
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
throw new Error(`GrantsClient.listGrants ${res.status} ${body.slice(0, 200)}`);
|
|
45
|
+
}
|
|
46
|
+
const parsed = JSON.parse(body);
|
|
47
|
+
return {
|
|
48
|
+
items: parsed.items ?? [],
|
|
49
|
+
nextCursor: parsed.nextCursor ?? null,
|
|
50
|
+
hasMore: parsed.hasMore ?? false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Every grant matching `query`, following the cursor to completion. Use when a
|
|
55
|
+
* caller needs the whole set (reach tagging, catch-up, an owner listing) rather
|
|
56
|
+
* than a single page — `listGrants` returns only one page (server default 30),
|
|
57
|
+
* so a holder with more grants than a page would otherwise be silently
|
|
58
|
+
* truncated. `maxPages` bounds the loop as a runaway guard.
|
|
59
|
+
*/
|
|
60
|
+
async listAllGrants(query = {}, maxPages = 50) {
|
|
61
|
+
const all = [];
|
|
62
|
+
let cursor;
|
|
63
|
+
for (let page = 0; page < maxPages; page++) {
|
|
64
|
+
const res = await this.listGrants({ ...query, cursor });
|
|
65
|
+
all.push(...res.items);
|
|
66
|
+
if (!res.nextCursor)
|
|
67
|
+
return all;
|
|
68
|
+
cursor = res.nextCursor;
|
|
69
|
+
}
|
|
70
|
+
return all;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import type { GrantView } from './grants.js';
|
|
3
|
+
/** Thrown by PaymentsClient with the HTTP status and raw body attached. */
|
|
4
|
+
export interface PaymentsError extends Error {
|
|
5
|
+
status?: number;
|
|
6
|
+
body?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface WalletBalance {
|
|
9
|
+
walletId: string | null;
|
|
10
|
+
currency: string;
|
|
11
|
+
balance: number;
|
|
12
|
+
availableBalance: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* ZIG-894 — the one payments client for every surface (agent-sdk, ziggs-mcp,
|
|
16
|
+
* scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
|
|
17
|
+
* ops ride the operator key; agent-impersonated calls send X-Agent-Id and are
|
|
18
|
+
* policy-gated server-side (transfers above threshold return
|
|
19
|
+
* `approval_required`). Wallet-rail grant mutations (issue / attenuate /
|
|
20
|
+
* revoke) live here; grant *listing* is the unified GET /grants (ZIG-648).
|
|
21
|
+
*/
|
|
22
|
+
export declare class PaymentsClient {
|
|
23
|
+
private readonly operatorKey;
|
|
24
|
+
private readonly agentId?;
|
|
25
|
+
private readonly baseUrl;
|
|
26
|
+
constructor(operatorKey: string, agentId?: string, baseUrl?: string);
|
|
27
|
+
balance(): Promise<WalletBalance>;
|
|
28
|
+
resolve({ userId, agentId }?: {
|
|
29
|
+
userId?: string;
|
|
30
|
+
agentId?: string;
|
|
31
|
+
}): Promise<unknown>;
|
|
32
|
+
transfer({ to, amount, idempotencyKey, description, paymentGrantId, }: {
|
|
33
|
+
to: string;
|
|
34
|
+
amount: number;
|
|
35
|
+
idempotencyKey?: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
paymentGrantId?: string;
|
|
38
|
+
}): Promise<unknown>;
|
|
39
|
+
hold({ amount, idempotencyKey, description, }: {
|
|
40
|
+
amount: number;
|
|
41
|
+
idempotencyKey?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
}): Promise<unknown>;
|
|
44
|
+
release({ holdId, action, toWalletId, idempotencyKey, }: {
|
|
45
|
+
holdId: string;
|
|
46
|
+
action?: string;
|
|
47
|
+
toWalletId?: string;
|
|
48
|
+
idempotencyKey?: string;
|
|
49
|
+
}): Promise<unknown>;
|
|
50
|
+
history(params?: {
|
|
51
|
+
limit?: number;
|
|
52
|
+
offset?: number;
|
|
53
|
+
type?: string;
|
|
54
|
+
}): Promise<unknown>;
|
|
55
|
+
issueGrant({ holderId, caveats }: {
|
|
56
|
+
holderId: string;
|
|
57
|
+
caveats?: unknown;
|
|
58
|
+
}): Promise<unknown>;
|
|
59
|
+
attenuateGrant({ grantId, holderId, caveats, }: {
|
|
60
|
+
grantId: string;
|
|
61
|
+
holderId: string;
|
|
62
|
+
caveats?: unknown;
|
|
63
|
+
}): Promise<unknown>;
|
|
64
|
+
revokeGrant(grantId: string): Promise<unknown>;
|
|
65
|
+
/**
|
|
66
|
+
* Live wallet grants the acting agent holds, via the unified GET /grants
|
|
67
|
+
* (ZIG-648 retired GET /payments/grants), following the cursor to completion.
|
|
68
|
+
*/
|
|
69
|
+
listGrants(): Promise<GrantView[]>;
|
|
70
|
+
createTopUpIntent({ amount, description, currency, }?: {
|
|
71
|
+
amount?: number;
|
|
72
|
+
description?: string;
|
|
73
|
+
currency?: string;
|
|
74
|
+
}): Promise<unknown>;
|
|
75
|
+
confirmMockIntent(intentId: string): Promise<unknown>;
|
|
76
|
+
faucet({ amount, description, idempotencyKey, }?: {
|
|
77
|
+
amount?: number;
|
|
78
|
+
description?: string;
|
|
79
|
+
idempotencyKey?: string;
|
|
80
|
+
}): Promise<unknown>;
|
|
81
|
+
approvals({ status }?: {
|
|
82
|
+
status?: string;
|
|
83
|
+
}): Promise<unknown[]>;
|
|
84
|
+
getApproval(approvalId: string): Promise<unknown | null>;
|
|
85
|
+
decide(approvalId: string, decision: string, note?: string): Promise<unknown>;
|
|
86
|
+
waitForApproval(approvalId: string, opts?: {
|
|
87
|
+
pollMs?: number;
|
|
88
|
+
timeoutMs?: number;
|
|
89
|
+
signal?: AbortSignal;
|
|
90
|
+
}): Promise<unknown>;
|
|
91
|
+
private _request;
|
|
92
|
+
private _get;
|
|
93
|
+
private _post;
|
|
94
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { getBackendUrl } from '../utils/urlUtils.js';
|
|
3
|
+
import { buildOperatorHeaders } from './operatorHeaders.js';
|
|
4
|
+
import { GrantsClient } from './GrantsClient.js';
|
|
5
|
+
function randomIdempotencyKey(prefix = 'op') {
|
|
6
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
7
|
+
}
|
|
8
|
+
function parseError(text, fallback) {
|
|
9
|
+
if (!text)
|
|
10
|
+
return fallback;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(text);
|
|
13
|
+
return parsed['error'] || parsed['message'] || text;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* ZIG-894 — the one payments client for every surface (agent-sdk, ziggs-mcp,
|
|
21
|
+
* scripts). Consolidates the former agent-sdk private ZiggsPayClient. Wallet
|
|
22
|
+
* ops ride the operator key; agent-impersonated calls send X-Agent-Id and are
|
|
23
|
+
* policy-gated server-side (transfers above threshold return
|
|
24
|
+
* `approval_required`). Wallet-rail grant mutations (issue / attenuate /
|
|
25
|
+
* revoke) live here; grant *listing* is the unified GET /grants (ZIG-648).
|
|
26
|
+
*/
|
|
27
|
+
export class PaymentsClient {
|
|
28
|
+
operatorKey;
|
|
29
|
+
agentId;
|
|
30
|
+
baseUrl;
|
|
31
|
+
constructor(operatorKey, agentId, baseUrl) {
|
|
32
|
+
if (!operatorKey)
|
|
33
|
+
throw new Error('PaymentsClient: operatorKey is required');
|
|
34
|
+
this.operatorKey = operatorKey;
|
|
35
|
+
this.agentId = agentId;
|
|
36
|
+
this.baseUrl = baseUrl || getBackendUrl();
|
|
37
|
+
}
|
|
38
|
+
async balance() {
|
|
39
|
+
const w = (await this._get('/payments/wallet'));
|
|
40
|
+
const wallet = w['wallet'];
|
|
41
|
+
return {
|
|
42
|
+
walletId: wallet?.['walletId'] || null,
|
|
43
|
+
currency: wallet?.['currency'] || 'pez',
|
|
44
|
+
balance: w['balance'] ?? 0,
|
|
45
|
+
availableBalance: w['availableBalance'] ?? 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async resolve({ userId, agentId } = {}) {
|
|
49
|
+
if (!userId && !agentId)
|
|
50
|
+
throw new Error('resolve: provide userId or agentId');
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
if (userId)
|
|
53
|
+
params.set('userId', userId);
|
|
54
|
+
if (agentId)
|
|
55
|
+
params.set('agentId', agentId);
|
|
56
|
+
const res = (await this._get(`/payments/wallets/resolve?${params}`));
|
|
57
|
+
return res['wallet'] || null;
|
|
58
|
+
}
|
|
59
|
+
async transfer({ to, amount, idempotencyKey, description, paymentGrantId, }) {
|
|
60
|
+
if (!to)
|
|
61
|
+
throw new Error('transfer: `to` is required');
|
|
62
|
+
if (!(Number.isInteger(amount) && amount > 0))
|
|
63
|
+
throw new Error('transfer: `amount` must be a positive integer (cents)');
|
|
64
|
+
if (this.agentId && !paymentGrantId)
|
|
65
|
+
throw new Error('transfer: paymentGrantId is required for agent-impersonated transfers. The wallet owner must have issued a payment grant to this agentId.');
|
|
66
|
+
let toWalletId = to;
|
|
67
|
+
if (!to.startsWith('wal_')) {
|
|
68
|
+
const w = (await this.resolve(to.startsWith('agent_') ? { agentId: to } : { userId: to }));
|
|
69
|
+
if (!w?.['walletId'])
|
|
70
|
+
throw new Error(`transfer: could not resolve wallet for "${to}"`);
|
|
71
|
+
toWalletId = w['walletId'];
|
|
72
|
+
}
|
|
73
|
+
const result = (await this._post('/payments/transfer', {
|
|
74
|
+
toWalletId,
|
|
75
|
+
amount,
|
|
76
|
+
idempotencyKey: idempotencyKey || randomIdempotencyKey('xfer'),
|
|
77
|
+
description,
|
|
78
|
+
paymentGrantId,
|
|
79
|
+
}));
|
|
80
|
+
if (result?.['status'] === 'approval_required') {
|
|
81
|
+
const approval = result['approval'] || {};
|
|
82
|
+
return {
|
|
83
|
+
status: 'approval_required',
|
|
84
|
+
approvalId: approval['approvalId'] || null,
|
|
85
|
+
expiresAt: approval['expiresAt'] || null,
|
|
86
|
+
reason: approval['reason'] || 'Amount exceeds auto-approve threshold',
|
|
87
|
+
toWalletId,
|
|
88
|
+
amount,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const tx = result?.['transaction'];
|
|
92
|
+
return { status: 'transferred', transactionId: tx?.['transactionId'] || null, toWalletId, amount };
|
|
93
|
+
}
|
|
94
|
+
async hold({ amount, idempotencyKey, description, }) {
|
|
95
|
+
if (!(Number.isInteger(amount) && amount > 0))
|
|
96
|
+
throw new Error('hold: `amount` must be a positive integer (cents)');
|
|
97
|
+
return this._post('/payments/hold', {
|
|
98
|
+
amount,
|
|
99
|
+
idempotencyKey: idempotencyKey || randomIdempotencyKey('hold'),
|
|
100
|
+
description,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async release({ holdId, action = 'complete', toWalletId, idempotencyKey, }) {
|
|
104
|
+
return this._post(`/payments/release/${holdId}`, {
|
|
105
|
+
idempotencyKey: idempotencyKey || randomIdempotencyKey('rel'),
|
|
106
|
+
action,
|
|
107
|
+
toWalletId,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async history(params = {}) {
|
|
111
|
+
const q = new URLSearchParams();
|
|
112
|
+
if (params.limit != null)
|
|
113
|
+
q.set('limit', String(params.limit));
|
|
114
|
+
if (params.offset != null)
|
|
115
|
+
q.set('offset', String(params.offset));
|
|
116
|
+
if (params.type)
|
|
117
|
+
q.set('type', params.type);
|
|
118
|
+
return this._get('/payments/history' + (q.toString() ? `?${q}` : ''));
|
|
119
|
+
}
|
|
120
|
+
async issueGrant({ holderId, caveats }) {
|
|
121
|
+
return this._post('/payments/grants', { holderId, caveats });
|
|
122
|
+
}
|
|
123
|
+
async attenuateGrant({ grantId, holderId, caveats, }) {
|
|
124
|
+
return this._post(`/payments/grants/${grantId}/attenuate`, { holderId, caveats });
|
|
125
|
+
}
|
|
126
|
+
async revokeGrant(grantId) {
|
|
127
|
+
return this._request('DELETE', `/payments/grants/${grantId}`, undefined);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Live wallet grants the acting agent holds, via the unified GET /grants
|
|
131
|
+
* (ZIG-648 retired GET /payments/grants), following the cursor to completion.
|
|
132
|
+
*/
|
|
133
|
+
async listGrants() {
|
|
134
|
+
const grants = new GrantsClient(this.operatorKey, this.agentId, this.baseUrl);
|
|
135
|
+
return grants.listAllGrants({ scopeKind: 'wallet', health: 'active' });
|
|
136
|
+
}
|
|
137
|
+
async createTopUpIntent({ amount, description, currency, } = {}) {
|
|
138
|
+
return this._post('/payments/onramp/intent', { amount, description, currency });
|
|
139
|
+
}
|
|
140
|
+
async confirmMockIntent(intentId) {
|
|
141
|
+
return this._post(`/payments/onramp/mock-confirm/${intentId}`, {});
|
|
142
|
+
}
|
|
143
|
+
async faucet({ amount, description = 'Faucet', idempotencyKey, } = {}) {
|
|
144
|
+
return this._post('/payments/wallet/fund', {
|
|
145
|
+
amount,
|
|
146
|
+
idempotencyKey: idempotencyKey || randomIdempotencyKey('faucet'),
|
|
147
|
+
description,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async approvals({ status = 'pending' } = {}) {
|
|
151
|
+
const res = (await this._get(`/payments/approvals?status=${encodeURIComponent(status)}`));
|
|
152
|
+
return res['approvals'] || [];
|
|
153
|
+
}
|
|
154
|
+
async getApproval(approvalId) {
|
|
155
|
+
if (!approvalId)
|
|
156
|
+
throw new Error('getApproval: approvalId is required');
|
|
157
|
+
try {
|
|
158
|
+
const res = (await this._get(`/payments/approvals/${approvalId}`));
|
|
159
|
+
return res?.['approval'] || null;
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
if (err.status === 404)
|
|
163
|
+
return null;
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async decide(approvalId, decision, note = '') {
|
|
168
|
+
return this._post(`/payments/approvals/${approvalId}/decide`, { decision, note });
|
|
169
|
+
}
|
|
170
|
+
async waitForApproval(approvalId, opts = {}) {
|
|
171
|
+
const pollMs = Math.max(500, opts.pollMs ?? 3_000);
|
|
172
|
+
const timeoutMs = Math.max(pollMs, opts.timeoutMs ?? 120_000);
|
|
173
|
+
const signal = opts.signal;
|
|
174
|
+
const deadline = Date.now() + timeoutMs;
|
|
175
|
+
const sleep = (ms) => new Promise((resolve, reject) => {
|
|
176
|
+
const timer = setTimeout(resolve, ms);
|
|
177
|
+
if (signal) {
|
|
178
|
+
if (signal.aborted) {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
reject(new Error('aborted'));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
signal.addEventListener('abort', () => {
|
|
184
|
+
clearTimeout(timer);
|
|
185
|
+
reject(new Error('aborted'));
|
|
186
|
+
}, { once: true });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
while (true) {
|
|
190
|
+
if (signal?.aborted)
|
|
191
|
+
throw new Error('aborted');
|
|
192
|
+
const approval = (await this.getApproval(approvalId));
|
|
193
|
+
if (!approval)
|
|
194
|
+
return { status: 'gone' };
|
|
195
|
+
const s = approval['status'];
|
|
196
|
+
if (s === 'executed')
|
|
197
|
+
return { status: 'executed', approval, transactionId: approval['executedTransactionId'] || null };
|
|
198
|
+
if (s === 'rejected')
|
|
199
|
+
return { status: 'rejected', approval };
|
|
200
|
+
if (s === 'expired')
|
|
201
|
+
return { status: 'expired', approval };
|
|
202
|
+
const remaining = deadline - Date.now();
|
|
203
|
+
if (remaining <= 0)
|
|
204
|
+
return { status: 'timeout', approval };
|
|
205
|
+
await sleep(Math.min(pollMs, remaining));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async _request(method, path, body) {
|
|
209
|
+
const init = {
|
|
210
|
+
method,
|
|
211
|
+
headers: buildOperatorHeaders(this.operatorKey, this.agentId, body !== undefined ? { 'content-type': 'application/json' } : {}),
|
|
212
|
+
};
|
|
213
|
+
if (body !== undefined)
|
|
214
|
+
init.body = JSON.stringify(body);
|
|
215
|
+
const response = await fetch(`${this.baseUrl}${path}`, init);
|
|
216
|
+
const text = await response.text();
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
const err = new Error(parseError(text, `HTTP ${response.status}`));
|
|
219
|
+
err.status = response.status;
|
|
220
|
+
err.body = text;
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
return text ? JSON.parse(text) : null;
|
|
224
|
+
}
|
|
225
|
+
_get(path) {
|
|
226
|
+
return this._request('GET', path, undefined);
|
|
227
|
+
}
|
|
228
|
+
_post(path, body) {
|
|
229
|
+
return this._request('POST', path, body ?? {});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GrantScopeKind } from './grants.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read the `scopes` claim of an operator JWT without verifying its signature —
|
|
4
|
+
* an identity hint only, exactly as the backend re-checks the key row on every
|
|
5
|
+
* request. Returns null when the token isn't a decodable JWT (so callers treat
|
|
6
|
+
* entitlement as unknown rather than empty).
|
|
7
|
+
*/
|
|
8
|
+
export declare function decodeOperatorScopes(operatorKey: string): string[] | null;
|
|
9
|
+
export interface UnreadableRail {
|
|
10
|
+
rail: 'context' | 'connection' | 'wallet';
|
|
11
|
+
requiredScope: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Which grant rails the operator key can't read, among those requested (or all
|
|
15
|
+
* rails when no filter is given). Returns null when the key's scopes can't be
|
|
16
|
+
* decoded — the caller then omits the "what I can't see" note rather than
|
|
17
|
+
* guessing. `GET /grants` enforces the same boundary server-side; this only
|
|
18
|
+
* lets the tool surface report it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function unreadableGrantRails(operatorKey: string, requested?: readonly GrantScopeKind[]): UnreadableRail[] | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const RAIL_GROUPS = [
|
|
2
|
+
{ rail: 'context', kinds: ['chat', 'agreement', 'org'], requiredScope: 'context:read' },
|
|
3
|
+
{ rail: 'connection', kinds: ['connection'], requiredScope: 'connections:read' },
|
|
4
|
+
{ rail: 'wallet', kinds: ['wallet'], requiredScope: 'payments:read' },
|
|
5
|
+
];
|
|
6
|
+
/**
|
|
7
|
+
* Read the `scopes` claim of an operator JWT without verifying its signature —
|
|
8
|
+
* an identity hint only, exactly as the backend re-checks the key row on every
|
|
9
|
+
* request. Returns null when the token isn't a decodable JWT (so callers treat
|
|
10
|
+
* entitlement as unknown rather than empty).
|
|
11
|
+
*/
|
|
12
|
+
export function decodeOperatorScopes(operatorKey) {
|
|
13
|
+
const parts = (operatorKey ?? '').trim().split('.');
|
|
14
|
+
if (parts.length !== 3)
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const json = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
18
|
+
const payload = JSON.parse(json);
|
|
19
|
+
if (!Array.isArray(payload.scopes))
|
|
20
|
+
return null;
|
|
21
|
+
return payload.scopes.filter((s) => typeof s === 'string');
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Which grant rails the operator key can't read, among those requested (or all
|
|
29
|
+
* rails when no filter is given). Returns null when the key's scopes can't be
|
|
30
|
+
* decoded — the caller then omits the "what I can't see" note rather than
|
|
31
|
+
* guessing. `GET /grants` enforces the same boundary server-side; this only
|
|
32
|
+
* lets the tool surface report it.
|
|
33
|
+
*/
|
|
34
|
+
export function unreadableGrantRails(operatorKey, requested) {
|
|
35
|
+
const scopes = decodeOperatorScopes(operatorKey);
|
|
36
|
+
if (scopes === null)
|
|
37
|
+
return null;
|
|
38
|
+
const held = new Set(scopes);
|
|
39
|
+
const wanted = requested && requested.length > 0 ? new Set(requested) : null;
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const group of RAIL_GROUPS) {
|
|
42
|
+
const inScope = wanted ? group.kinds.some((k) => wanted.has(k)) : true;
|
|
43
|
+
if (!inScope)
|
|
44
|
+
continue;
|
|
45
|
+
if (!held.has(group.requiredScope)) {
|
|
46
|
+
out.push({ rail: group.rail, requiredScope: group.requiredScope });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
package/dist/http/grants.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export type GrantScopeKind = 'chat' | 'agreement' | 'org' | 'connection' | 'wall
|
|
|
15
15
|
export interface GrantScopeView {
|
|
16
16
|
kind: GrantScopeKind;
|
|
17
17
|
id: string;
|
|
18
|
+
/**
|
|
19
|
+
* Human-readable name for the scoped resource, populated only by the unified
|
|
20
|
+
* list read (GET /grants / GrantsClient); the per-rail issue/delegate
|
|
21
|
+
* responses omit it. Optional so the one shape stays additive.
|
|
22
|
+
*/
|
|
23
|
+
label?: string;
|
|
18
24
|
}
|
|
19
25
|
export interface GrantCaveatView {
|
|
20
26
|
type: string;
|
package/dist/http/index.d.ts
CHANGED
|
@@ -9,11 +9,19 @@ export type { ArtifactVisibility, ListArtifactsOptions, ListArtifactsQuery, List
|
|
|
9
9
|
export { ContextReadClient } from './ContextReadClient.js';
|
|
10
10
|
export type { ContextReadType, ContextReadQuery, ContextReadEnvelope, ContextSnapshotResult, } from './ContextReadClient.js';
|
|
11
11
|
export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
|
|
12
|
-
export type {
|
|
12
|
+
export type { DiscoverableItem } from './ContextDiscoveryClient.js';
|
|
13
|
+
export { GrantsClient } from './GrantsClient.js';
|
|
14
|
+
export type { ListGrantsQuery, ListGrantsResult } from './GrantsClient.js';
|
|
13
15
|
export { ContextGrantsClient } from './ContextGrantsClient.js';
|
|
14
|
-
export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, ContextTemporal, IssueContextGrantInput, DelegateContextGrantInput, } from './ContextGrantsClient.js';
|
|
16
|
+
export type { ContextGrantRecord, ContextGrantScope, ContextGrantScopeKind, ContextTemporal, IssueContextGrantInput, DelegateContextGrantInput, DelegateContextGrantResult, ReachEntry, GrantReachResult, } from './ContextGrantsClient.js';
|
|
15
17
|
export { grantCaveat } from './grants.js';
|
|
16
18
|
export type { GrantView, GrantScopeKind, GrantScopeView, GrantCaveatView, GrantHealth, } from './grants.js';
|
|
19
|
+
export { decodeOperatorScopes, unreadableGrantRails } from './grantRails.js';
|
|
20
|
+
export type { UnreadableRail } from './grantRails.js';
|
|
21
|
+
export { PaymentsClient } from './PaymentsClient.js';
|
|
22
|
+
export type { PaymentsError, WalletBalance } from './PaymentsClient.js';
|
|
23
|
+
export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
|
|
24
|
+
export type { ConnectionsError, ConnectionProxyParams, McpConnectionRequestParams, } from './ConnectionsClient.js';
|
|
17
25
|
export { AgentSearchClient } from './AgentSearchClient.js';
|
|
18
26
|
export { TelemetryClient } from './TelemetryClient.js';
|
|
19
27
|
export { InboxClient } from './InboxClient.js';
|
package/dist/http/index.js
CHANGED
|
@@ -6,8 +6,12 @@ export { MessagesClient } from './MessagesClient.js';
|
|
|
6
6
|
export { ArtifactsClient } from './ArtifactsClient.js';
|
|
7
7
|
export { ContextReadClient } from './ContextReadClient.js';
|
|
8
8
|
export { ContextDiscoveryClient } from './ContextDiscoveryClient.js';
|
|
9
|
+
export { GrantsClient } from './GrantsClient.js';
|
|
9
10
|
export { ContextGrantsClient } from './ContextGrantsClient.js';
|
|
10
11
|
export { grantCaveat } from './grants.js';
|
|
12
|
+
export { decodeOperatorScopes, unreadableGrantRails } from './grantRails.js';
|
|
13
|
+
export { PaymentsClient } from './PaymentsClient.js';
|
|
14
|
+
export { ConnectionsClient, assertNoLeakedConnectionSecret, } from './ConnectionsClient.js';
|
|
11
15
|
export { AgentSearchClient } from './AgentSearchClient.js';
|
|
12
16
|
export { TelemetryClient } from './TelemetryClient.js';
|
|
13
17
|
export { InboxClient } from './InboxClient.js';
|
|
@@ -109,6 +109,15 @@ export class WebSocketClient {
|
|
|
109
109
|
this.socket.on('error', (error) => {
|
|
110
110
|
runtimeLog.error(this.label, `Socket.IO error: ${String(error)}`);
|
|
111
111
|
});
|
|
112
|
+
// ZIG-860: the send path is fire-and-forget (no ack callback), so the
|
|
113
|
+
// backend's structured rejection is the ONLY failure signal. Not listening
|
|
114
|
+
// made every rejected send silent — an agent's message just vanished (the
|
|
115
|
+
// a2a schema bug hid behind exactly this). Log loudly; delivery recovery
|
|
116
|
+
// stays with the inbox/catch-up machinery.
|
|
117
|
+
this.socket.on('chat:error', (payload) => {
|
|
118
|
+
const p = (payload ?? {});
|
|
119
|
+
runtimeLog.error(this.label, `chat:error [${p['code'] ?? 'UNKNOWN'}] chatId=${p['chatId'] ?? '-'} messageId=${p['messageId'] ?? '-'}: ${p['message'] ?? ''}`);
|
|
120
|
+
});
|
|
112
121
|
}
|
|
113
122
|
connectAsync(timeout = 10_000) {
|
|
114
123
|
if (this.socket?.connected)
|