@lucid-agents/pi-xgate 0.1.5 → 0.1.6

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 ADDED
@@ -0,0 +1,45 @@
1
+ // Supported entry point. Hosts build the provider around their own transport; the default
2
+ // Pi extension (`./extension`) keeps the standalone wallet broker.
3
+ export {
4
+ createXgateProvider,
5
+ invocationId,
6
+ type Broker,
7
+ type XgateProviderOptions,
8
+ type XgatePurchaseFailure,
9
+ } from "./provider.ts";
10
+ export {
11
+ XgatePurchaseError,
12
+ type XgateChannelBalance,
13
+ type XgatePayment,
14
+ type XgatePurchaseResult,
15
+ type XgateResponseHead,
16
+ type XgateTransport,
17
+ } from "./transport.ts";
18
+ export {
19
+ createXgateExtension,
20
+ type XgateExtensionOptions,
21
+ } from "./extension.ts";
22
+ export { XgateError } from "@lucid-agents/xgate-client/types";
23
+ export type {
24
+ Invocation,
25
+ PurchaseResult,
26
+ Route,
27
+ Settlement,
28
+ } from "@lucid-agents/xgate-client/types";
29
+ export {
30
+ createTaskMarketTransport,
31
+ type TaskMarketChargeStatus,
32
+ type TaskMarketFundingBalance,
33
+ type TaskMarketInvocationState,
34
+ type TaskMarketInvocationStatus,
35
+ type TaskMarketPayCallOptions,
36
+ type TaskMarketPayErrorCode,
37
+ type TaskMarketPayer,
38
+ type TaskMarketPayerLimits,
39
+ type TaskMarketPayRequest,
40
+ type TaskMarketPayResponse,
41
+ type TaskMarketPayResult,
42
+ type TaskMarketPaymentSummary,
43
+ type TaskMarketPaymentTerms,
44
+ type TaskMarketTransportOptions,
45
+ } from "./taskmarket.ts";
package/src/provider.ts CHANGED
@@ -5,26 +5,34 @@ import {
5
5
  type Model,
6
6
  type Provider,
7
7
  type SimpleStreamOptions,
8
- type TranscriptContext,
9
8
  } from "@earendil-works/pi-ai";
9
+ import { XgateError } from "@lucid-agents/xgate-client/types";
10
+ import { catalogEntries, encode, decode, type PiContext } from "./codec.ts";
10
11
  import {
11
- XgateError,
12
- type PurchaseResult,
13
- type Invocation,
14
- } from "@lucid-agents/xgate-client/types";
15
- import { catalogEntries, encode, decode } from "./codec.ts";
12
+ XgatePurchaseError,
13
+ type XgatePayment,
14
+ type XgatePurchaseResult,
15
+ type XgateTransport,
16
+ } from "./transport.ts";
16
17
  const hash = (value: string): string =>
17
18
  createHash("sha256").update(value).digest("hex");
18
- export interface Broker {
19
- models(signal?: AbortSignal): Promise<any[]>;
20
- purchase(
21
- input: Invocation,
22
- signal?: AbortSignal,
23
- onResponse?: (response: {
24
- status: number;
25
- headers: Record<string, string>;
26
- }) => Promise<void>,
27
- ): Promise<PurchaseResult>;
19
+ /** @deprecated Use `XgateTransport`. */
20
+ export type Broker = XgateTransport;
21
+ export interface XgateProviderOptions {
22
+ /** Invocation identity scope when Pi supplies no `sessionId`. Defaults to `"standalone"`. */
23
+ sessionId?: () => string;
24
+ /** Display-only notification of a verified paid result; it cannot change the outcome. */
25
+ onPaid?: (result: XgatePurchaseResult) => void;
26
+ /** Terminal notification of a purchase that ended without a verified result. */
27
+ onPurchaseFailed?: (failure: XgatePurchaseFailure) => void;
28
+ }
29
+ export interface XgatePurchaseFailure {
30
+ /** The original invocation ID; reconcile it, never retry under a new one. */
31
+ invocationId: string;
32
+ code: string;
33
+ /** False only when the transport proved no unresolved payment. */
34
+ pending: boolean;
35
+ payment?: XgatePayment;
28
36
  }
29
37
  const zero = () => ({
30
38
  input: 0,
@@ -37,9 +45,14 @@ const zero = () => ({
37
45
  /** Stable across automatic retries and payload hooks; a changed paid payload cannot claim a new intent. */
38
46
  export function invocationId(
39
47
  model: string,
40
- context: TranscriptContext,
48
+ context: PiContext,
41
49
  session: string,
42
50
  ): string {
51
+ // Pi 0.85 carries the prompt and tools beside the messages; Pi 1.1 carries them inside.
52
+ const legacy =
53
+ "systemPrompt" in context || "tools" in context
54
+ ? { systemPrompt: context.systemPrompt, tools: context.tools }
55
+ : {};
43
56
  return hash(
44
57
  JSON.stringify({
45
58
  session,
@@ -49,18 +62,22 @@ export function invocationId(
49
62
  m.role !== "assistant" ||
50
63
  !["error", "aborted"].includes(m.stopReason),
51
64
  ),
65
+ ...legacy,
52
66
  }),
53
67
  );
54
68
  }
55
69
  export async function createXgateProvider(
56
- broker: Broker,
57
- identity: () => string = () => "standalone",
58
- onPaid?: (result: PurchaseResult) => void,
70
+ transport: XgateTransport,
71
+ {
72
+ sessionId: identity = () => "standalone",
73
+ onPaid,
74
+ onPurchaseFailed,
75
+ }: XgateProviderOptions = {},
59
76
  ): Promise<Provider> {
60
- let entries = catalogEntries(await broker.models());
77
+ let entries = catalogEntries(await transport.models());
61
78
  const stream = (
62
79
  model: Model<any>,
63
- context: TranscriptContext,
80
+ context: PiContext,
64
81
  options: SimpleStreamOptions = {},
65
82
  ) => {
66
83
  const events = createAssistantMessageEventStream();
@@ -86,10 +103,11 @@ export async function createXgateProvider(
86
103
  stopReason: "pending",
87
104
  timestamp: Date.now(),
88
105
  };
106
+ let attempted: string | undefined, paid: XgatePurchaseResult | undefined;
89
107
  void (async () => {
90
108
  try {
91
109
  options.signal?.throwIfAborted();
92
- const current = catalogEntries(await broker.models(options.signal)),
110
+ const current = catalogEntries(await transport.models(options.signal)),
93
111
  entry = current.find(
94
112
  (e) => e.model.id === model.id && e.model.api === model.api,
95
113
  );
@@ -127,7 +145,8 @@ export async function createXgateProvider(
127
145
  options.sessionId ?? identity(),
128
146
  );
129
147
  let observed = false;
130
- const result = await broker.purchase(
148
+ attempted = id;
149
+ const result = await transport.purchase(
131
150
  { id, route: entry.route, body },
132
151
  options.signal,
133
152
  async (response) => {
@@ -135,7 +154,8 @@ export async function createXgateProvider(
135
154
  await options.onResponse?.(response, model);
136
155
  },
137
156
  );
138
- // In-process injected brokers have no HTTP body; retain the observation hook there too.
157
+ paid = result;
158
+ // In-process injected transports have no HTTP body; retain the observation hook there too.
139
159
  if (!observed)
140
160
  await options.onResponse?.(
141
161
  {
@@ -234,6 +254,28 @@ export async function createXgateProvider(
234
254
  : error instanceof XgateError
235
255
  ? error.message
236
256
  : "Xgate operation stopped. Inspect /xgate status before requesting another purchase.";
257
+ if (attempted !== undefined) {
258
+ // After a verified result the charge is known even if Pi discards the content.
259
+ const payment =
260
+ paid?.payment ??
261
+ (error instanceof XgatePurchaseError ? error.payment : undefined);
262
+ const failure: XgatePurchaseFailure = {
263
+ invocationId: attempted,
264
+ code: error instanceof XgateError ? error.code : "purchase_failed",
265
+ pending:
266
+ paid === undefined &&
267
+ !(error instanceof XgatePurchaseError && !error.pending),
268
+ ...(payment ? { payment } : {}),
269
+ };
270
+ // The original ID is the only reconciliation key. "Billing" also keeps Pi's transient
271
+ // error classifier from retrying a message whose hex ID happens to contain "500".
272
+ message.errorMessage += ` Billing invocation: ${attempted}.`;
273
+ try {
274
+ onPurchaseFailed?.(failure);
275
+ } catch {
276
+ /* Diagnostics cannot change the terminal outcome. */
277
+ }
278
+ }
237
279
  events.push({
238
280
  type: "error",
239
281
  reason: message.stopReason,
@@ -257,7 +299,7 @@ export async function createXgateProvider(
257
299
  getModels: () => entries.map((e) => e.model),
258
300
  refreshModels: async (context) => {
259
301
  if (!context.allowNetwork) return;
260
- const updated = catalogEntries(await broker.models(context.signal));
302
+ const updated = catalogEntries(await transport.models(context.signal));
261
303
  await context.publish({
262
304
  update: () => {
263
305
  entries = updated;
@@ -0,0 +1,484 @@
1
+ // Adapts TaskMarket's x402 payer to the xgate Pi transport. The payer package is private, so
2
+ // this file declares the structural subset of its v1 contract (`@taskmarket/x402-payer`
3
+ // `src/api.ts`, X402_PAYER_API_VERSION 1) that the adapter uses; TaskMarket injects its
4
+ // instance. The payer owns the wallet, signer, rails, policy and journals: this adapter keeps
5
+ // no payment state of its own.
6
+ //
7
+ // The payer's string unions may gain values without a version bump. Every type here accepts
8
+ // unknown strings, and the adapter treats an unknown state as unverified.
9
+ import {
10
+ atomic,
11
+ origin as checkOrigin,
12
+ record,
13
+ route as checkRoute,
14
+ XgateError,
15
+ type Invocation,
16
+ } from "@lucid-agents/xgate-client/types";
17
+ import {
18
+ XgatePurchaseError,
19
+ type XgateChannelBalance,
20
+ type XgatePayment,
21
+ type XgatePurchaseResult,
22
+ type XgateResponseHead,
23
+ type XgateTransport,
24
+ } from "./transport.ts";
25
+ type Open<T extends string> = T | (string & {});
26
+ export type TaskMarketChargeStatus = Open<
27
+ | "none"
28
+ | "reserved"
29
+ | "pending"
30
+ | "settled"
31
+ | "amount_unknown"
32
+ | "released"
33
+ | "manually_resolved"
34
+ >;
35
+ export type TaskMarketInvocationState = Open<
36
+ "in_progress" | "interrupted" | "completed" | "failed"
37
+ >;
38
+ export type TaskMarketPayErrorCode = Open<
39
+ | "invalid_request"
40
+ | "request_body_too_large"
41
+ | "response_body_too_large"
42
+ | "challenge_invalid"
43
+ | "invocation_conflict"
44
+ | "invocation_duplicate"
45
+ | "invocation_not_found"
46
+ | "destination_rejected"
47
+ | "policy_denied"
48
+ | "payment_declined"
49
+ | "no_supported_rail"
50
+ | "signer_unavailable"
51
+ | "journal_unavailable"
52
+ | "aborted"
53
+ | "outcome_uncertain"
54
+ | "paid_retry_prohibited"
55
+ | "request_failed"
56
+ | "allocation_not_found"
57
+ | "allocation_conflict"
58
+ | "allocation_closed"
59
+ | "allocation_exhausted"
60
+ | "allocation_mismatch"
61
+ | "funding_exhausted"
62
+ | "allocation_required"
63
+ | "reservation_not_found"
64
+ >;
65
+ export interface TaskMarketPayerLimits {
66
+ maxRequestBodyBytes: number;
67
+ maxResponseBodyBytes: number;
68
+ maxChallengeBodyBytes: number;
69
+ }
70
+ export interface TaskMarketPayRequest {
71
+ apiVersion: 1;
72
+ invocationId: string;
73
+ url: string;
74
+ method: "GET" | "POST";
75
+ headers?: Record<string, string>;
76
+ body?: Uint8Array;
77
+ policyRuleId?: string;
78
+ nonInteractive?: boolean;
79
+ limits?: Partial<TaskMarketPayerLimits>;
80
+ allocationId?: string;
81
+ }
82
+ export interface TaskMarketPaymentTerms {
83
+ origin: string;
84
+ pathname: string;
85
+ method: "GET" | "POST";
86
+ rail: { id: string; kind: string };
87
+ scheme: string;
88
+ network: string;
89
+ asset: string;
90
+ payTo: string;
91
+ authorizedAmount: string;
92
+ policyRuleId?: string;
93
+ windowMaximum?: string;
94
+ unattended: boolean;
95
+ }
96
+ export interface TaskMarketPayCallOptions {
97
+ signal?: AbortSignal;
98
+ confirmPayment?: (terms: TaskMarketPaymentTerms) => Promise<boolean>;
99
+ }
100
+ export interface TaskMarketPaymentSummary {
101
+ paymentId: string;
102
+ rail: { id: string; kind: string };
103
+ network: string;
104
+ scheme: string;
105
+ asset: string;
106
+ payTo: string;
107
+ payer: string;
108
+ authorizedAmount: string;
109
+ settledAmount?: string;
110
+ transaction?: string;
111
+ commitment?: string;
112
+ charge: TaskMarketChargeStatus;
113
+ }
114
+ export interface TaskMarketPayResponse {
115
+ status: number;
116
+ headers: Record<string, string>;
117
+ contentType: string;
118
+ bytes: number;
119
+ sha256: string;
120
+ body: Uint8Array;
121
+ }
122
+ export interface TaskMarketPayResult {
123
+ apiVersion: number;
124
+ invocationId: string;
125
+ requestFingerprint: string;
126
+ payment?: TaskMarketPaymentSummary;
127
+ response: TaskMarketPayResponse;
128
+ }
129
+ export interface TaskMarketInvocationStatus {
130
+ apiVersion: number;
131
+ invocationId: string;
132
+ requestFingerprint: string;
133
+ state: TaskMarketInvocationState;
134
+ charge: TaskMarketChargeStatus;
135
+ uncertain: boolean;
136
+ errorCode?: TaskMarketPayErrorCode;
137
+ createdAt: string;
138
+ updatedAt: string;
139
+ payment?: TaskMarketPaymentSummary;
140
+ }
141
+ /** Structural `X402Payer` v1. */
142
+ export interface TaskMarketPayer {
143
+ readonly apiVersion: 1;
144
+ request(
145
+ input: TaskMarketPayRequest,
146
+ options?: TaskMarketPayCallOptions,
147
+ ): Promise<TaskMarketPayResult>;
148
+ status(invocationId: string): Promise<TaskMarketInvocationStatus>;
149
+ reconcile(invocationId: string): Promise<TaskMarketInvocationStatus>;
150
+ }
151
+ /** Structural `X402FundingBalance`: one session-rail deposit. */
152
+ export interface TaskMarketFundingBalance {
153
+ railId: string;
154
+ sessionKey: string;
155
+ network: string;
156
+ asset: string;
157
+ depositing: string;
158
+ locked: string;
159
+ refunded: string;
160
+ charged: string;
161
+ pending: string;
162
+ refundable: string;
163
+ }
164
+ export interface TaskMarketTransportOptions {
165
+ payer: TaskMarketPayer;
166
+ /** Router origin, e.g. `https://api.xgate.run`. */
167
+ origin: string;
168
+ /** Allocation that bounds this agent's inference; forwarded on every request. */
169
+ allocationId?: string;
170
+ policyRuleId?: string;
171
+ /** Authorize only through unattended policy rules. */
172
+ nonInteractive?: boolean;
173
+ /** Attended confirmation, forwarded to the payer. */
174
+ confirmPayment?: (terms: TaskMarketPaymentTerms) => Promise<boolean>;
175
+ /** Restrict the offered models; the router catalog still qualifies them. */
176
+ models?: readonly string[];
177
+ /** Session-rail balances for display, e.g. TaskMarket's `X402Allocations.balances`. */
178
+ balances?: () => Promise<readonly TaskMarketFundingBalance[]>;
179
+ /** Unpaid catalog reads. Defaults to the global `fetch`. */
180
+ fetch?: typeof fetch;
181
+ }
182
+ const RESOLVED_CHARGES = ["none", "released", "settled", "manually_resolved"],
183
+ RESOLVED_STATES = ["completed", "failed"];
184
+ const CATALOG_BYTES = 2 * 1024 * 1024,
185
+ REQUEST_BYTES = 8 * 1024 * 1024,
186
+ CATALOG_DEADLINE_MS = 15000;
187
+ /** Never sends the request again for an ID the payer has seen: `purchase` maps every non-verified outcome to a terminal error. */
188
+ export function createTaskMarketTransport(
189
+ options: TaskMarketTransportOptions,
190
+ ): XgateTransport & {
191
+ status(): Promise<unknown>;
192
+ reconcile(invocationId: string): Promise<unknown>;
193
+ } {
194
+ const { payer } = options,
195
+ base = checkOrigin(options.origin),
196
+ fetcher = options.fetch ?? fetch;
197
+ if (payer.apiVersion !== 1) throw new XgateError("unsupported_payer_api");
198
+ const channel = async (
199
+ payment: TaskMarketPaymentSummary,
200
+ ): Promise<XgateChannelBalance | undefined> => {
201
+ if (!options.balances || payment.rail.kind !== "session") return;
202
+ try {
203
+ const matches = (await options.balances()).filter(
204
+ (b) =>
205
+ b.railId === payment.rail.id &&
206
+ b.network === payment.network &&
207
+ b.asset === payment.asset,
208
+ );
209
+ // Several sessions on one rail are not one channel; unlike balances are never summed.
210
+ if (matches.length !== 1) return;
211
+ const b = matches[0]!;
212
+ return {
213
+ depositingAtomic: b.depositing,
214
+ lockedAtomic: b.locked,
215
+ chargedAtomic: b.charged,
216
+ pendingAtomic: b.pending,
217
+ refundableAtomic: b.refundable,
218
+ };
219
+ } catch {
220
+ return; // Display only.
221
+ }
222
+ };
223
+ return {
224
+ async models(signal) {
225
+ const deadline = AbortSignal.timeout(CATALOG_DEADLINE_MS);
226
+ let response: Response;
227
+ try {
228
+ response = await fetcher(base + "/v1/models", {
229
+ signal: signal ? AbortSignal.any([signal, deadline]) : deadline,
230
+ redirect: "error",
231
+ cache: "no-store",
232
+ credentials: "omit",
233
+ referrerPolicy: "no-referrer",
234
+ headers: { accept: "application/json" },
235
+ });
236
+ } catch {
237
+ throw new XgateError("catalog_unavailable");
238
+ }
239
+ if (!response.ok) {
240
+ await response.body?.cancel().catch(() => {});
241
+ throw new XgateError("catalog_unavailable");
242
+ }
243
+ const v = parse(await bounded(response, CATALOG_BYTES));
244
+ if (!record(v) || !Array.isArray(v.data) || v.data.length > 128)
245
+ throw new XgateError("invalid_catalog");
246
+ return v.data
247
+ .filter(
248
+ (m: unknown) =>
249
+ record(m) &&
250
+ (options.models === undefined || options.models.includes(m.id)),
251
+ )
252
+ .map((m: Record<string, unknown>) => ({ ...m, xgate_origin: base }));
253
+ },
254
+ async purchase(invocation: Invocation, signal, onResponse) {
255
+ checkRoute(invocation.route);
256
+ const body = new TextEncoder().encode(invocation.body);
257
+ if (body.byteLength === 0 || body.byteLength > REQUEST_BYTES)
258
+ throw new XgateError("invalid_invocation");
259
+ const id = invocation.id;
260
+ let result: TaskMarketPayResult;
261
+ try {
262
+ result = await payer.request(
263
+ {
264
+ apiVersion: 1,
265
+ invocationId: id,
266
+ url: base + invocation.route,
267
+ method: "POST",
268
+ // Application headers only: the payer owns every payment header.
269
+ headers: {
270
+ "content-type": "application/json",
271
+ accept: "application/json",
272
+ },
273
+ body,
274
+ limits: { maxRequestBodyBytes: REQUEST_BYTES },
275
+ ...(options.allocationId !== undefined
276
+ ? { allocationId: options.allocationId }
277
+ : {}),
278
+ ...(options.policyRuleId !== undefined
279
+ ? { policyRuleId: options.policyRuleId }
280
+ : {}),
281
+ ...(options.nonInteractive !== undefined
282
+ ? { nonInteractive: options.nonInteractive }
283
+ : {}),
284
+ },
285
+ {
286
+ ...(signal ? { signal } : {}),
287
+ ...(options.confirmPayment
288
+ ? { confirmPayment: options.confirmPayment }
289
+ : {}),
290
+ },
291
+ );
292
+ } catch (error) {
293
+ throw failure(error, id);
294
+ }
295
+ const payment = result.payment;
296
+ if (result.invocationId !== id || result.apiVersion !== 1)
297
+ throw new XgatePurchaseError(
298
+ "payer_result_mismatch",
299
+ id,
300
+ true,
301
+ payment && display(payment),
302
+ );
303
+ // A paid xgate route never answers without a payment challenge.
304
+ if (!payment)
305
+ throw new XgatePurchaseError(
306
+ result.response.status === 200
307
+ ? "payment_missing"
308
+ : "request_rejected",
309
+ id,
310
+ false,
311
+ );
312
+ const shown = display(payment);
313
+ if (
314
+ payment.charge !== "settled" ||
315
+ typeof payment.settledAmount !== "string"
316
+ )
317
+ throw new XgatePurchaseError("payment_unverified", id, true, shown);
318
+ shown.chargedAtomic = payment.settledAmount;
319
+ try {
320
+ if (atomic(payment.settledAmount) > atomic(payment.authorizedAmount))
321
+ throw new XgateError("charge_over_authorization");
322
+ } catch {
323
+ throw new XgatePurchaseError("invalid_settlement", id, true, shown);
324
+ }
325
+ const r = result.response,
326
+ h = r.headers;
327
+ // Charged, so not pending, but the content is unusable.
328
+ if (
329
+ r.status !== 200 ||
330
+ !/^application\/json\b/i.test(r.contentType) ||
331
+ h["x-inference-privacy"] !== "anonymous" ||
332
+ h["x-result-retention"] !== "none" ||
333
+ !h["cache-control"]?.includes("no-store")
334
+ )
335
+ throw new XgatePurchaseError("response_rejected", id, false, shown);
336
+ let value: unknown;
337
+ try {
338
+ value = parse(r.body);
339
+ } catch {
340
+ throw new XgatePurchaseError("invalid_response", id, false, shown);
341
+ }
342
+ if (
343
+ !record(value) ||
344
+ typeof value.id !== "string" ||
345
+ value.result_replay_available !== undefined
346
+ )
347
+ throw new XgatePurchaseError("invalid_response", id, false, shown);
348
+ const head: XgateResponseHead = {
349
+ status: r.status,
350
+ headers: { "content-type": r.contentType, "cache-control": "no-store" },
351
+ };
352
+ try {
353
+ await onResponse?.(head);
354
+ } catch {
355
+ throw new XgatePurchaseError("response_hook_failed", id, false, shown);
356
+ }
357
+ const balance = await channel(payment);
358
+ if (balance) shown.channel = balance;
359
+ return {
360
+ body: value,
361
+ settlement: {
362
+ chargedAtomic: payment.settledAmount,
363
+ reference:
364
+ payment.transaction ?? payment.commitment ?? payment.paymentId,
365
+ },
366
+ purchase: payment.commitment ?? payment.paymentId,
367
+ payment: shown,
368
+ } satisfies XgatePurchaseResult;
369
+ },
370
+ async status() {
371
+ return {
372
+ transport: "taskmarket",
373
+ origin: base,
374
+ ...(options.balances
375
+ ? {
376
+ balances: (await options.balances()).map((b) => ({
377
+ rail: b.railId,
378
+ network: b.network,
379
+ asset: b.asset,
380
+ depositingAtomic: b.depositing,
381
+ lockedAtomic: b.locked,
382
+ chargedAtomic: b.charged,
383
+ pendingAtomic: b.pending,
384
+ refundableAtomic: b.refundable,
385
+ })),
386
+ }
387
+ : {}),
388
+ };
389
+ },
390
+ async reconcile(invocationId) {
391
+ const s = await payer.reconcile(invocationId);
392
+ return {
393
+ invocationId: s.invocationId,
394
+ state: s.state,
395
+ charge: s.charge,
396
+ uncertain: s.uncertain,
397
+ ...(s.errorCode ? { errorCode: s.errorCode } : {}),
398
+ ...(s.payment ? { payment: display(s.payment) } : {}),
399
+ };
400
+ },
401
+ };
402
+ }
403
+ function display(p: TaskMarketPaymentSummary): XgatePayment {
404
+ return {
405
+ network: p.network,
406
+ scheme: p.scheme,
407
+ asset: p.asset,
408
+ payTo: p.payTo,
409
+ rail: p.rail.kind,
410
+ authorizedAtomic: p.authorizedAmount,
411
+ ...(p.charge === "settled" && p.settledAmount !== undefined
412
+ ? { chargedAtomic: p.settledAmount }
413
+ : {}),
414
+ status: p.charge,
415
+ ...((p.transaction ?? p.commitment)
416
+ ? { reference: p.transaction ?? p.commitment }
417
+ : {}),
418
+ };
419
+ }
420
+ /**
421
+ * Maps a payer rejection. Only the stable code crosses into Pi; the payer's message may carry
422
+ * URLs or server text. A rejection that does not state `pending: false` counts as pending.
423
+ */
424
+ function failure(error: unknown, id: string): XgatePurchaseError {
425
+ const e = error as {
426
+ code?: unknown;
427
+ pending?: unknown;
428
+ invocation?: { uncertain?: unknown; payment?: TaskMarketPaymentSummary };
429
+ };
430
+ const code =
431
+ typeof e?.code === "string" && /^[a-z0-9_]{1,64}$/.test(e.code)
432
+ ? e.code
433
+ : "payer_failed";
434
+ // A duplicate never sends again, but its recorded invocation may still be unresolved: only a
435
+ // positively resolved record counts as not pending.
436
+ const recorded = e?.invocation as
437
+ | { uncertain?: unknown; charge?: unknown; state?: unknown }
438
+ | undefined;
439
+ const pending =
440
+ e?.pending !== false ||
441
+ (recorded !== undefined &&
442
+ !(
443
+ recorded.uncertain === false &&
444
+ RESOLVED_CHARGES.includes(recorded.charge as string) &&
445
+ RESOLVED_STATES.includes(recorded.state as string)
446
+ ));
447
+ const summary = e?.invocation?.payment;
448
+ return new XgatePurchaseError(
449
+ code,
450
+ id,
451
+ pending,
452
+ record(summary) ? display(summary) : undefined,
453
+ );
454
+ }
455
+ async function bounded(response: Response, limit: number): Promise<Uint8Array> {
456
+ if (!response.body) throw new XgateError("invalid_catalog");
457
+ const reader = response.body.getReader(),
458
+ chunks: Uint8Array[] = [];
459
+ let length = 0;
460
+ try {
461
+ for (;;) {
462
+ const part = await reader.read();
463
+ if (part.done) break;
464
+ length += part.value.byteLength;
465
+ if (length > limit) throw new XgateError("catalog_bound");
466
+ chunks.push(part.value);
467
+ }
468
+ } catch (error) {
469
+ await reader.cancel().catch(() => {});
470
+ throw error instanceof XgateError
471
+ ? error
472
+ : new XgateError("catalog_unavailable");
473
+ } finally {
474
+ reader.releaseLock();
475
+ }
476
+ return Buffer.concat(chunks, length);
477
+ }
478
+ function parse(bytes: Uint8Array): unknown {
479
+ try {
480
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
481
+ } catch {
482
+ throw new XgateError("invalid_response");
483
+ }
484
+ }