@acedatacloud/sdk 2026.418.3 → 2026.420.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.
package/src/index.ts CHANGED
@@ -18,6 +18,14 @@ export {
18
18
 
19
19
  export { TaskHandle, TaskHandleOptions } from './runtime/tasks';
20
20
 
21
+ export type {
22
+ PaymentHandler,
23
+ PaymentHandlerContext,
24
+ PaymentHandlerResult,
25
+ PaymentRequirement,
26
+ PaymentRequiredBody,
27
+ } from './runtime/payment';
28
+
21
29
  export type { ImageProvider } from './resources/images';
22
30
  export type { VideoProvider } from './resources/video';
23
31
  export type { AudioProvider } from './resources/audio';
@@ -1,5 +1,12 @@
1
1
  export { Transport, TransportOptions } from './transport';
2
2
  export { TaskHandle, TaskHandleOptions } from './tasks';
3
+ export type {
4
+ PaymentHandler,
5
+ PaymentHandlerContext,
6
+ PaymentHandlerResult,
7
+ PaymentRequirement,
8
+ PaymentRequiredBody,
9
+ } from './payment';
3
10
  export {
4
11
  AceDataCloudError,
5
12
  APIError,
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Pluggable payment handler hook for the SDK transport.
3
+ *
4
+ * When the API returns `402 Payment Required`, the transport calls the
5
+ * configured `PaymentHandler` to produce the extra headers (typically
6
+ * `X-Payment`) to attach to the retry. This keeps the SDK free of any
7
+ * chain-specific signing logic, and lets callers plug in a real x402
8
+ * implementation such as `@acedatacloud/x402-client`.
9
+ */
10
+
11
+ /** Payment requirement as returned by the server in a 402 response. */
12
+ export interface PaymentRequirement {
13
+ scheme: string;
14
+ network: string;
15
+ maxAmountRequired: string;
16
+ maxTimeoutSeconds?: number;
17
+ resource?: string;
18
+ description?: string;
19
+ payTo: string;
20
+ asset: string;
21
+ extra?: Record<string, unknown>;
22
+ }
23
+
24
+ /** Shape of a 402 response body. */
25
+ export interface PaymentRequiredBody {
26
+ x402Version?: number;
27
+ accepts: PaymentRequirement[];
28
+ error?: string;
29
+ }
30
+
31
+ /** Context passed to a payment handler when a 402 is observed. */
32
+ export interface PaymentHandlerContext {
33
+ url: string;
34
+ method: string;
35
+ body?: unknown;
36
+ accepts: PaymentRequirement[];
37
+ }
38
+
39
+ /** Result a payment handler must return. */
40
+ export interface PaymentHandlerResult {
41
+ /** Extra headers to attach to the retry (must include `X-Payment`). */
42
+ headers: Record<string, string>;
43
+ }
44
+
45
+ /** A callable that signs/settles a payment and returns retry headers. */
46
+ export type PaymentHandler = (
47
+ ctx: PaymentHandlerContext
48
+ ) => Promise<PaymentHandlerResult> | PaymentHandlerResult;
@@ -12,6 +12,10 @@ import {
12
12
  TransportError,
13
13
  ValidationError,
14
14
  } from './errors';
15
+ import type {
16
+ PaymentHandler,
17
+ PaymentRequiredBody,
18
+ } from './payment';
15
19
 
16
20
  const ERROR_CODE_MAP: Record<string, typeof APIError> = {
17
21
  invalid_token: AuthenticationError,
@@ -26,11 +30,11 @@ const ERROR_CODE_MAP: Record<string, typeof APIError> = {
26
30
 
27
31
  const RETRY_STATUS_CODES = new Set([408, 409, 429, 500, 502, 503, 504]);
28
32
 
29
- function mapError(statusCode: number, body: Record<string, unknown>): APIError {
30
- const errorData = (body.error ?? {}) as Record<string, unknown>;
33
+ export function mapError(statusCode: number, body: any): APIError {
34
+ const errorData = (body && typeof body === "object" ? (body.error ?? {}) : {}) as Record<string, unknown>;
31
35
  const code = (errorData.code ?? '') as string;
32
36
  const message = (errorData.message ?? '') as string;
33
- const traceId = body.trace_id as string | undefined;
37
+ const traceId = (body && typeof body === "object" ? body.trace_id : undefined) as string | undefined;
34
38
 
35
39
  let ErrorClass = ERROR_CODE_MAP[code];
36
40
  if (!ErrorClass) {
@@ -60,6 +64,14 @@ export interface TransportOptions {
60
64
  timeout?: number;
61
65
  maxRetries?: number;
62
66
  headers?: Record<string, string>;
67
+ /**
68
+ * Optional handler invoked when a request returns `402 Payment Required`.
69
+ * The handler receives the parsed `accepts` list and must return the extra
70
+ * headers (typically `X-Payment`) to attach to the automatic retry.
71
+ *
72
+ * See `@acedatacloud/x402-client` for a drop-in implementation.
73
+ */
74
+ paymentHandler?: PaymentHandler;
63
75
  }
64
76
 
65
77
  export class Transport {
@@ -68,12 +80,15 @@ export class Transport {
68
80
  private timeout: number;
69
81
  private maxRetries: number;
70
82
  private headers: Record<string, string>;
83
+ private paymentHandler?: PaymentHandler;
71
84
 
72
85
  constructor(opts: TransportOptions = {}) {
73
86
  const token = opts.apiToken ?? process.env.ACEDATACLOUD_API_TOKEN ?? '';
74
- if (!token) {
87
+ if (!token && !opts.paymentHandler) {
75
88
  throw new AuthenticationError({
76
- message: 'apiToken is required. Pass it to the client or set ACEDATACLOUD_API_TOKEN.',
89
+ message:
90
+ 'apiToken is required (or provide a paymentHandler, e.g. from @acedatacloud/x402-client). ' +
91
+ 'Pass it to the client or set ACEDATACLOUD_API_TOKEN.',
77
92
  statusCode: 0,
78
93
  code: 'no_token',
79
94
  });
@@ -82,13 +97,17 @@ export class Transport {
82
97
  this.platformBaseURL = (opts.platformBaseURL ?? 'https://platform.acedata.cloud').replace(/\/+$/, '');
83
98
  this.timeout = opts.timeout ?? 300_000;
84
99
  this.maxRetries = opts.maxRetries ?? 2;
85
- this.headers = {
100
+ this.paymentHandler = opts.paymentHandler;
101
+ const baseHeaders: Record<string, string> = {
86
102
  accept: 'application/json',
87
- authorization: `Bearer ${token}`,
88
103
  'content-type': 'application/json',
89
104
  'user-agent': 'acedatacloud-node/0.1.0',
90
105
  ...(opts.headers ?? {}),
91
106
  };
107
+ if (token) {
108
+ baseHeaders.authorization = `Bearer ${token}`;
109
+ }
110
+ this.headers = baseHeaders;
92
111
  }
93
112
 
94
113
  async request(
@@ -112,18 +131,42 @@ export class Transport {
112
131
  const timeoutMs = opts.timeout ?? this.timeout;
113
132
 
114
133
  let lastError: Error | null = null;
134
+ let paymentAttempted = false;
135
+ let extraHeaders: Record<string, string> = {};
115
136
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
116
137
  const controller = new AbortController();
117
138
  const timer = setTimeout(() => controller.abort(), timeoutMs);
118
139
  try {
119
140
  const resp = await fetch(url, {
120
141
  method,
121
- headers,
142
+ headers: { ...headers, ...extraHeaders },
122
143
  body: opts.json ? JSON.stringify(opts.json) : undefined,
123
144
  signal: controller.signal,
124
145
  });
125
146
  clearTimeout(timer);
126
147
 
148
+ if (resp.status === 402 && this.paymentHandler && !paymentAttempted) {
149
+ const text = await resp.text();
150
+ let body: PaymentRequiredBody;
151
+ try {
152
+ body = JSON.parse(text) as PaymentRequiredBody;
153
+ } catch {
154
+ throw mapError(402, { error: { code: 'invalid_402', message: text } });
155
+ }
156
+ if (!body.accepts?.length) {
157
+ throw mapError(402, { error: { code: 'invalid_402', message: 'No payment requirements' } });
158
+ }
159
+ const result = await this.paymentHandler({
160
+ url,
161
+ method,
162
+ body: opts.json,
163
+ accepts: body.accepts,
164
+ });
165
+ extraHeaders = { ...extraHeaders, ...result.headers };
166
+ paymentAttempted = true;
167
+ continue;
168
+ }
169
+
127
170
  if (resp.status >= 400) {
128
171
  const text = await resp.text();
129
172
  let body: Record<string, unknown>;