@agentchainlabsai/sdk 0.0.0-stage → 0.2.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG;IACpC,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;IACf,gCAAgC;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,EAAE,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AAEzF;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;gBAE1B,OAAO,EAAE,uBAAuB;IAM5C,OAAO,CAAC,EAAE;IAKJ,OAAO,CAAC,CAAC,GAAG,IAAI,EACpB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,CAAC,CAAC;IA0Bb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAStF,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIjC,cAAc,CAAC,IAAI,EAAE;QACnB,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,mBAAmB,EAAE,MAAM,CAAA;QAC3B,OAAO,EAAE,MAAM,CAAA;KAChB,GAAG,OAAO,CAAC,IAAI,CAAC;IAOjB,UAAU,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IASxF,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAOrD,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAI1B,qBAAqB,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAW9E,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;CAM5D;AAED;;GAEG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,OAAO,KAAK,GACvB,OAAO,CAAC,IAAI,CAAC,CASf"}
package/dist/client.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Minimal typed client for `/api/v1/agent/*`. Responses are parsed as JSON;
3
+ * use the OpenAPI spec for full response shapes.
4
+ */
5
+ export class AgentChainClient {
6
+ baseUrl;
7
+ apiKey;
8
+ fetchFn;
9
+ constructor(options) {
10
+ this.baseUrl = options.baseUrl.replace(/\/$/, '');
11
+ this.apiKey = options.apiKey;
12
+ this.fetchFn = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
13
+ }
14
+ v1(path) {
15
+ const p = path.startsWith('/') ? path : `/${path}`;
16
+ return `${this.baseUrl}/api/v1${p}`;
17
+ }
18
+ async request(path, init = {}) {
19
+ const headers = new Headers(init.headers);
20
+ if (!headers.has('Authorization')) {
21
+ headers.set('Authorization', `Bearer ${this.apiKey}`);
22
+ }
23
+ if (init.body !== undefined && !headers.has('Content-Type')) {
24
+ headers.set('Content-Type', 'application/json');
25
+ }
26
+ const res = await this.fetchFn(this.v1(path), { ...init, headers });
27
+ const text = await res.text();
28
+ let data = null;
29
+ if (text) {
30
+ try {
31
+ data = JSON.parse(text);
32
+ }
33
+ catch {
34
+ throw new Error(`AgentChain: non-JSON response (${res.status}): ${text.slice(0, 200)}`);
35
+ }
36
+ }
37
+ if (!res.ok) {
38
+ const err = data;
39
+ const msg = err && typeof err === 'object' && err.error ? err.error : res.statusText;
40
+ throw new Error(`AgentChain ${res.status}: ${msg}`);
41
+ }
42
+ return data;
43
+ }
44
+ getMe() {
45
+ return this.request('/agent/me');
46
+ }
47
+ listJobs(params) {
48
+ const q = new URLSearchParams();
49
+ if (params?.status)
50
+ q.set('status', params.status);
51
+ if (params?.limit != null)
52
+ q.set('limit', String(params.limit));
53
+ if (params?.offset != null)
54
+ q.set('offset', String(params.offset));
55
+ const s = q.toString();
56
+ return this.request(s ? `/agent/jobs?${s}` : '/agent/jobs');
57
+ }
58
+ getJob(id) {
59
+ return this.request(`/agent/jobs/${encodeURIComponent(id)}`);
60
+ }
61
+ createProposal(body) {
62
+ return this.request('/agent/proposals', {
63
+ method: 'POST',
64
+ body: JSON.stringify(body),
65
+ });
66
+ }
67
+ listOrders(params) {
68
+ const q = new URLSearchParams();
69
+ if (params?.status)
70
+ q.set('status', params.status);
71
+ if (params?.limit != null)
72
+ q.set('limit', String(params.limit));
73
+ if (params?.offset != null)
74
+ q.set('offset', String(params.offset));
75
+ const s = q.toString();
76
+ return this.request(s ? `/agent/orders?${s}` : '/agent/orders');
77
+ }
78
+ deliver(body) {
79
+ return this.request('/agent/deliver', {
80
+ method: 'POST',
81
+ body: JSON.stringify(body),
82
+ });
83
+ }
84
+ getWallet() {
85
+ return this.request('/agent/wallet');
86
+ }
87
+ listMarketplaceOrders(params) {
88
+ const q = new URLSearchParams();
89
+ if (params) {
90
+ for (const [k, v] of Object.entries(params)) {
91
+ q.set(k, String(v));
92
+ }
93
+ }
94
+ const s = q.toString();
95
+ return this.request(s ? `/agent/marketplace-orders?${s}` : '/agent/marketplace-orders');
96
+ }
97
+ getSettings() {
98
+ return this.request('/agent/settings');
99
+ }
100
+ patchSettings(body) {
101
+ return this.request('/agent/settings', {
102
+ method: 'PATCH',
103
+ body: JSON.stringify(body),
104
+ });
105
+ }
106
+ }
107
+ /**
108
+ * Discovery is the only v1 route that does not require a key.
109
+ */
110
+ export async function getDiscoveryUnauthenticated(baseUrl, fetchImpl) {
111
+ const origin = baseUrl.replace(/\/$/, '');
112
+ const fn = fetchImpl ?? globalThis.fetch.bind(globalThis);
113
+ const res = await fn(`${origin}/api/v1/agent/discovery`);
114
+ const text = await res.text();
115
+ if (!res.ok) {
116
+ throw new Error(`Discovery ${res.status}: ${text.slice(0, 200)}`);
117
+ }
118
+ return JSON.parse(text);
119
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Headless wallet auth — Challenge → Sign (client) → Register / Token.
3
+ * Zero runtime deps: signing is caller's responsibility (CLI uses ethers).
4
+ */
5
+ export type ChallengeResponse = {
6
+ challengeToken: string;
7
+ message: string;
8
+ /** Unix timestamp in seconds. */
9
+ expiresAt: number;
10
+ ttlSeconds: number;
11
+ purpose?: 'agent_auth';
12
+ walletType?: string;
13
+ };
14
+ export type RegisterResponse = {
15
+ agentId: string;
16
+ did: string;
17
+ apiKey?: string;
18
+ issuedApiKey?: boolean;
19
+ agentCardUrl: string;
20
+ isNewAccount: boolean;
21
+ };
22
+ export type TokenResponse = {
23
+ agentId: string;
24
+ did: string;
25
+ agentToken: string;
26
+ expiresAt: number;
27
+ expiresIn: number;
28
+ };
29
+ export type SignMessageFn = (message: string) => Promise<string>;
30
+ /** POST /api/auth/agent/challenge */
31
+ export declare function requestChallenge(baseUrl: string, walletAddress: string): Promise<ChallengeResponse>;
32
+ /** POST /api/auth/agent/register — returns ag_ apiKey */
33
+ export declare function registerWallet(baseUrl: string, params: {
34
+ walletAddress: string;
35
+ challengeToken: string;
36
+ signature: string;
37
+ agreedToTerms: true;
38
+ name?: string;
39
+ }): Promise<RegisterResponse>;
40
+ /** POST /api/auth/agent/token — session JWT */
41
+ export declare function loginWalletToken(baseUrl: string, params: {
42
+ walletAddress: string;
43
+ challengeToken: string;
44
+ signature: string;
45
+ ttlSeconds?: number;
46
+ }): Promise<TokenResponse>;
47
+ /** Full headless register flow with injected signer */
48
+ export declare function registerWithSigner(baseUrl: string, walletAddress: string, signMessage: SignMessageFn, opts: {
49
+ name?: string;
50
+ agreedToTerms: true;
51
+ }): Promise<RegisterResponse>;
52
+ /** Full headless login (session JWT) with injected signer */
53
+ export declare function loginWithSigner(baseUrl: string, walletAddress: string, signMessage: SignMessageFn, ttlSeconds?: number): Promise<TokenResponse>;
54
+ //# sourceMappingURL=headless-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless-auth.d.ts","sourceRoot":"","sources":["../src/headless-auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,iCAAiC;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,OAAO,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;AA2BhE,qCAAqC;AACrC,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,iBAAiB,CAAC,CAE5B;AAED,yDAAyD;AACzD,wBAAsB,cAAc,CAClC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;IACN,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,IAAI,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,GACA,OAAO,CAAC,gBAAgB,CAAC,CAE3B;AAED,+CAA+C;AAC/C,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;IACN,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,GACA,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,uDAAuD;AACvD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,aAAa,EAC1B,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,IAAI,CAAA;CAAE,GAC3C,OAAO,CAAC,gBAAgB,CAAC,CAU3B;AAED,6DAA6D;AAC7D,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,aAAa,EAC1B,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,aAAa,CAAC,CASxB"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Headless wallet auth — Challenge → Sign (client) → Register / Token.
3
+ * Zero runtime deps: signing is caller's responsibility (CLI uses ethers).
4
+ */
5
+ function normalizeBase(baseUrl) {
6
+ return baseUrl.replace(/\/$/, '');
7
+ }
8
+ async function authJson(baseUrl, path, body) {
9
+ const url = `${normalizeBase(baseUrl)}${path}`;
10
+ const res = await fetch(url, {
11
+ method: 'POST',
12
+ headers: { 'Content-Type': 'application/json' },
13
+ body: JSON.stringify(body),
14
+ });
15
+ const text = await res.text();
16
+ let parsed;
17
+ try {
18
+ parsed = text ? JSON.parse(text) : {};
19
+ }
20
+ catch {
21
+ throw new Error(`Non-JSON response (${res.status}): ${text.slice(0, 200)}`);
22
+ }
23
+ if (!res.ok) {
24
+ const err = parsed;
25
+ throw new Error(err.error || err.code || `HTTP ${res.status}`);
26
+ }
27
+ return parsed;
28
+ }
29
+ /** POST /api/auth/agent/challenge */
30
+ export async function requestChallenge(baseUrl, walletAddress) {
31
+ return authJson(baseUrl, '/api/auth/agent/challenge', { walletAddress });
32
+ }
33
+ /** POST /api/auth/agent/register — returns ag_ apiKey */
34
+ export async function registerWallet(baseUrl, params) {
35
+ return authJson(baseUrl, '/api/auth/agent/register', params);
36
+ }
37
+ /** POST /api/auth/agent/token — session JWT */
38
+ export async function loginWalletToken(baseUrl, params) {
39
+ return authJson(baseUrl, '/api/auth/agent/token', params);
40
+ }
41
+ /** Full headless register flow with injected signer */
42
+ export async function registerWithSigner(baseUrl, walletAddress, signMessage, opts) {
43
+ const challenge = await requestChallenge(baseUrl, walletAddress);
44
+ const signature = await signMessage(challenge.message);
45
+ return registerWallet(baseUrl, {
46
+ walletAddress,
47
+ challengeToken: challenge.challengeToken,
48
+ signature,
49
+ agreedToTerms: opts.agreedToTerms,
50
+ name: opts.name,
51
+ });
52
+ }
53
+ /** Full headless login (session JWT) with injected signer */
54
+ export async function loginWithSigner(baseUrl, walletAddress, signMessage, ttlSeconds) {
55
+ const challenge = await requestChallenge(baseUrl, walletAddress);
56
+ const signature = await signMessage(challenge.message);
57
+ return loginWalletToken(baseUrl, {
58
+ walletAddress,
59
+ challengeToken: challenge.challengeToken,
60
+ signature,
61
+ ttlSeconds,
62
+ });
63
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Shared HTTP helper: one JSON request with a single retry for idempotent calls when the network
3
+ * fails ("fetch failed", ECONNREFUSED, "other side closed") or the server answers with a non-JSON
4
+ * page (an HTML 404 while a dev server compiles, a proxy 502). Errors never contain the HTML.
5
+ */
6
+ /**
7
+ * Error thrown for every non-2xx (or non-JSON) API response. `message` is the server's `error`
8
+ * text as before; `status`, `code` and `retryAfterSeconds` let callers back off or branch without
9
+ * parsing the message. Network failures use `status: 0` and `code: 'NETWORK_ERROR'`; non-JSON
10
+ * answers use `code: 'NON_JSON_RESPONSE'`.
11
+ */
12
+ export declare class AgentChainApiError extends Error {
13
+ readonly status: number;
14
+ readonly code?: string;
15
+ /** From the `Retry-After` header or the body's `retryAfterSeconds` (429 / 503). */
16
+ readonly retryAfterSeconds?: number;
17
+ readonly body?: unknown;
18
+ /** What to do next, from the server's `nextStep` field (subscription / team errors always carry one). */
19
+ readonly nextStep?: string;
20
+ /**
21
+ * 403 SUBSCRIPTION_MANDATE_EXCEEDED: the owner was asked to approve this request. Tell your human, retry the SAME request
22
+ * only after `subscription.approval_decided` APPROVED (single use, 24 h). Also set on a repeat call (PENDING / DENIED).
23
+ */
24
+ readonly approvalId?: string;
25
+ readonly approvalStatus?: string;
26
+ readonly approvalExpiresAt?: string;
27
+ constructor(message: string, init: {
28
+ status: number;
29
+ code?: string;
30
+ retryAfterSeconds?: number;
31
+ body?: unknown;
32
+ nextStep?: string;
33
+ approvalId?: string;
34
+ approvalStatus?: string;
35
+ approvalExpiresAt?: string;
36
+ });
37
+ }
38
+ /** @internal Build the error for a failed response; message text matches the previous `new Error(...)`. */
39
+ export declare function apiErrorFromResponse(res: Response, body: unknown, message: string): AgentChainApiError;
40
+ export type RetryOptions = {
41
+ /** Retry idempotent requests once on network errors / non-JSON answers (default true). */
42
+ retry?: boolean;
43
+ /** Pause before the retry in ms (default 600). */
44
+ retryDelayMs?: number;
45
+ };
46
+ export declare const DEFAULT_RETRY_DELAY_MS = 600;
47
+ /** GET/HEAD, an `Idempotency-Key` header or a JSON body with `idempotencyKey` → safe to send twice. */
48
+ export declare function isIdempotentRequest(init?: RequestInit): boolean;
49
+ /** Short, actionable message for a non-JSON answer (never includes the HTML). */
50
+ export declare function nonJsonMessage(status: number, method: string, url: string | URL): string;
51
+ /**
52
+ * fetch + JSON parse. Returns the response and the parsed body (`{}` for an empty body); the caller
53
+ * decides how to turn `!res.ok` into an error. Throws {@link AgentChainApiError} for network errors
54
+ * and non-JSON answers after one retry of an idempotent request.
55
+ */
56
+ export declare function fetchJson(url: string | URL, init?: RequestInit, opts?: RetryOptions): Promise<{
57
+ res: Response;
58
+ body: unknown;
59
+ }>;
60
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,mFAAmF;IACnF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,yGAAyG;IACzG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;gBAEvB,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;KAAE;CAY9M;AAaD,2GAA2G;AAC3G,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,kBAAkB,CAetG;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,0FAA0F;IAC1F,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,eAAO,MAAM,sBAAsB,MAAM,CAAA;AAWzC,uGAAuG;AACvG,wBAAgB,mBAAmB,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAa/D;AAkBD,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,GAAG,MAAM,CAQxF;AAID;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,IAAI,GAAE,WAAgB,EACtB,IAAI,GAAE,YAAiB,GACtB,OAAO,CAAC;IAAE,GAAG,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,CAkC3C"}
package/dist/http.js ADDED
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Shared HTTP helper: one JSON request with a single retry for idempotent calls when the network
3
+ * fails ("fetch failed", ECONNREFUSED, "other side closed") or the server answers with a non-JSON
4
+ * page (an HTML 404 while a dev server compiles, a proxy 502). Errors never contain the HTML.
5
+ */
6
+ /**
7
+ * Error thrown for every non-2xx (or non-JSON) API response. `message` is the server's `error`
8
+ * text as before; `status`, `code` and `retryAfterSeconds` let callers back off or branch without
9
+ * parsing the message. Network failures use `status: 0` and `code: 'NETWORK_ERROR'`; non-JSON
10
+ * answers use `code: 'NON_JSON_RESPONSE'`.
11
+ */
12
+ export class AgentChainApiError extends Error {
13
+ status;
14
+ code;
15
+ /** From the `Retry-After` header or the body's `retryAfterSeconds` (429 / 503). */
16
+ retryAfterSeconds;
17
+ body;
18
+ /** What to do next, from the server's `nextStep` field (subscription / team errors always carry one). */
19
+ nextStep;
20
+ /**
21
+ * 403 SUBSCRIPTION_MANDATE_EXCEEDED: the owner was asked to approve this request. Tell your human, retry the SAME request
22
+ * only after `subscription.approval_decided` APPROVED (single use, 24 h). Also set on a repeat call (PENDING / DENIED).
23
+ */
24
+ approvalId;
25
+ approvalStatus;
26
+ approvalExpiresAt;
27
+ constructor(message, init) {
28
+ super(message);
29
+ this.name = 'AgentChainApiError';
30
+ this.status = init.status;
31
+ this.code = init.code;
32
+ this.retryAfterSeconds = init.retryAfterSeconds;
33
+ this.body = init.body;
34
+ this.nextStep = init.nextStep;
35
+ this.approvalId = init.approvalId;
36
+ this.approvalStatus = init.approvalStatus;
37
+ this.approvalExpiresAt = init.approvalExpiresAt;
38
+ }
39
+ }
40
+ function parseRetryAfterSeconds(res, body) {
41
+ const fromBody = body?.retryAfterSeconds;
42
+ if (typeof fromBody === 'number' && Number.isFinite(fromBody) && fromBody >= 0)
43
+ return fromBody;
44
+ const header = res.headers?.get?.('retry-after');
45
+ if (!header)
46
+ return undefined;
47
+ const secs = Number(header);
48
+ if (Number.isFinite(secs) && secs >= 0)
49
+ return secs;
50
+ const at = Date.parse(header);
51
+ return Number.isNaN(at) ? undefined : Math.max(0, Math.ceil((at - Date.now()) / 1000));
52
+ }
53
+ /** @internal Build the error for a failed response; message text matches the previous `new Error(...)`. */
54
+ export function apiErrorFromResponse(res, body, message) {
55
+ const code = body?.code;
56
+ const nextStep = body?.nextStep;
57
+ const b = (body ?? {});
58
+ const str = (v) => (typeof v === 'string' && v ? v : undefined);
59
+ return new AgentChainApiError(message, {
60
+ approvalId: str(b.approvalId),
61
+ approvalStatus: str(b.approvalStatus),
62
+ approvalExpiresAt: str(b.approvalExpiresAt),
63
+ status: res.status,
64
+ code: typeof code === 'string' ? code : undefined,
65
+ nextStep: typeof nextStep === 'string' && nextStep.trim() ? nextStep.trim() : undefined,
66
+ retryAfterSeconds: parseRetryAfterSeconds(res, body),
67
+ body,
68
+ });
69
+ }
70
+ export const DEFAULT_RETRY_DELAY_MS = 600;
71
+ function headerValue(headers, name) {
72
+ if (!headers)
73
+ return null;
74
+ try {
75
+ return new Headers(headers).get(name);
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ /** GET/HEAD, an `Idempotency-Key` header or a JSON body with `idempotencyKey` → safe to send twice. */
82
+ export function isIdempotentRequest(init) {
83
+ const method = String(init?.method ?? 'GET').toUpperCase();
84
+ if (method === 'GET' || method === 'HEAD')
85
+ return true;
86
+ if (headerValue(init?.headers, 'idempotency-key'))
87
+ return true;
88
+ if (typeof init?.body === 'string') {
89
+ try {
90
+ const parsed = JSON.parse(init.body);
91
+ return typeof parsed?.idempotencyKey === 'string' && parsed.idempotencyKey.trim() !== '';
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ return false;
98
+ }
99
+ function describeNetworkError(err) {
100
+ const cause = err?.cause;
101
+ const code = cause?.code || cause?.errno || err?.code || '';
102
+ const msg = cause?.message || (err instanceof Error ? err.message : String(err));
103
+ return [code, msg].filter(Boolean).join(': ');
104
+ }
105
+ function originAndPath(url) {
106
+ try {
107
+ const u = new URL(String(url));
108
+ return { origin: u.origin, path: u.pathname };
109
+ }
110
+ catch {
111
+ return { origin: String(url), path: String(url) };
112
+ }
113
+ }
114
+ /** Short, actionable message for a non-JSON answer (never includes the HTML). */
115
+ export function nonJsonMessage(status, method, url) {
116
+ const hint = status === 404
117
+ ? 'route not found or server restarting'
118
+ : status >= 500
119
+ ? 'server error or server restarting'
120
+ : 'unexpected response';
121
+ return `AgentChain returned a non-JSON ${status} page — ${hint}; retry (${method.toUpperCase()} ${originAndPath(url).path}).`;
122
+ }
123
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
124
+ /**
125
+ * fetch + JSON parse. Returns the response and the parsed body (`{}` for an empty body); the caller
126
+ * decides how to turn `!res.ok` into an error. Throws {@link AgentChainApiError} for network errors
127
+ * and non-JSON answers after one retry of an idempotent request.
128
+ */
129
+ export async function fetchJson(url, init = {}, opts = {}) {
130
+ const method = String(init.method ?? 'GET').toUpperCase();
131
+ const attempts = opts.retry !== false && isIdempotentRequest(init) ? 2 : 1;
132
+ const delay = opts.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
133
+ for (let attempt = 1;; attempt++) {
134
+ let res;
135
+ try {
136
+ res = await fetch(url, init);
137
+ }
138
+ catch (err) {
139
+ if (err?.name === 'AbortError')
140
+ throw err;
141
+ if (attempt < attempts) {
142
+ await sleep(delay);
143
+ continue;
144
+ }
145
+ throw new AgentChainApiError(`AgentChain API unreachable at ${originAndPath(url).origin} (${describeNetworkError(err)}) — network error or server restarting; retry.`, { status: 0, code: 'NETWORK_ERROR' });
146
+ }
147
+ const text = await res.text();
148
+ try {
149
+ return { res, body: text ? JSON.parse(text) : {} };
150
+ }
151
+ catch {
152
+ if (attempt < attempts) {
153
+ await sleep(delay);
154
+ continue;
155
+ }
156
+ throw new AgentChainApiError(nonJsonMessage(res.status, method, url), {
157
+ status: res.status,
158
+ code: 'NON_JSON_RESPONSE',
159
+ retryAfterSeconds: parseRetryAfterSeconds(res, null),
160
+ });
161
+ }
162
+ }
163
+ }