@pay-skill/sdk 0.1.8 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +80 -91
  2. package/dist/auth.d.ts +11 -6
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +19 -7
  5. package/dist/auth.js.map +1 -1
  6. package/dist/errors.d.ts +4 -2
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +8 -3
  9. package/dist/errors.js.map +1 -1
  10. package/dist/index.d.ts +2 -13
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -6
  13. package/dist/index.js.map +1 -1
  14. package/dist/keychain.d.ts +8 -0
  15. package/dist/keychain.d.ts.map +1 -0
  16. package/dist/keychain.js +17 -0
  17. package/dist/keychain.js.map +1 -0
  18. package/dist/wallet.d.ts +136 -104
  19. package/dist/wallet.d.ts.map +1 -1
  20. package/dist/wallet.js +658 -275
  21. package/dist/wallet.js.map +1 -1
  22. package/jsr.json +1 -1
  23. package/package.json +5 -2
  24. package/src/auth.ts +28 -18
  25. package/src/errors.ts +10 -3
  26. package/src/index.ts +12 -39
  27. package/src/keychain.ts +18 -0
  28. package/src/wallet.ts +1054 -355
  29. package/tests/test_auth_rejection.ts +43 -95
  30. package/tests/test_crypto.ts +59 -172
  31. package/tests/test_e2e.ts +46 -105
  32. package/tests/test_errors.ts +9 -1
  33. package/tests/test_ows.ts +153 -0
  34. package/tests/test_wallet.ts +194 -0
  35. package/dist/client.d.ts +0 -94
  36. package/dist/client.d.ts.map +0 -1
  37. package/dist/client.js +0 -443
  38. package/dist/client.js.map +0 -1
  39. package/dist/models.d.ts +0 -78
  40. package/dist/models.d.ts.map +0 -1
  41. package/dist/models.js +0 -2
  42. package/dist/models.js.map +0 -1
  43. package/dist/ows-signer.d.ts +0 -75
  44. package/dist/ows-signer.d.ts.map +0 -1
  45. package/dist/ows-signer.js +0 -130
  46. package/dist/ows-signer.js.map +0 -1
  47. package/dist/signer.d.ts +0 -46
  48. package/dist/signer.d.ts.map +0 -1
  49. package/dist/signer.js +0 -111
  50. package/dist/signer.js.map +0 -1
  51. package/src/client.ts +0 -644
  52. package/src/models.ts +0 -77
  53. package/src/ows-signer.ts +0 -223
  54. package/src/signer.ts +0 -147
  55. package/tests/test_ows_integration.ts +0 -92
  56. package/tests/test_ows_signer.ts +0 -365
  57. package/tests/test_signer.ts +0 -47
  58. package/tests/test_validation.ts +0 -66
package/src/client.ts DELETED
@@ -1,644 +0,0 @@
1
- /**
2
- * PayClient — single entry point for the pay SDK.
3
- */
4
-
5
- import type {
6
- DirectPaymentResult,
7
- DiscoverOptions,
8
- DiscoverService,
9
- StatusResponse,
10
- Tab,
11
- WebhookRegistration,
12
- } from "./models.js";
13
- import {
14
- PayNetworkError,
15
- PayServerError,
16
- PayValidationError,
17
- } from "./errors.js";
18
- import type { Signer } from "./signer.js";
19
- import { createSigner } from "./signer.js";
20
- import {
21
- buildAuthHeaders,
22
- buildAuthHeadersWithSigner,
23
- type AuthConfig,
24
- type AuthHeaders,
25
- } from "./auth.js";
26
- import type { Hex, Address } from "viem";
27
- import { sign as viemSign, serializeSignature, privateKeyToAccount } from "viem/accounts";
28
- import {
29
- signTransferAuthorization,
30
- combinedSignature,
31
- } from "./eip3009.js";
32
-
33
- const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
34
- const DIRECT_MIN = 1_000_000; // $1.00 USDC
35
- const TAB_MIN = 5_000_000; // $5.00 USDC
36
-
37
- export const DEFAULT_API_URL = "https://pay-skill.com/api/v1";
38
-
39
- function validateAddress(address: string, field = "address"): void {
40
- if (!ADDRESS_RE.test(address)) {
41
- throw new PayValidationError(
42
- `Invalid Ethereum address: ${address}`,
43
- field
44
- );
45
- }
46
- }
47
-
48
- function validateAmount(
49
- amount: number,
50
- minimum: number,
51
- field = "amount"
52
- ): void {
53
- if (amount < minimum) {
54
- const minUsd = minimum / 1_000_000;
55
- throw new PayValidationError(
56
- `Amount ${amount} below minimum ($${minUsd.toFixed(2)})`,
57
- field
58
- );
59
- }
60
- }
61
-
62
- export interface PayClientOptions {
63
- apiUrl?: string;
64
- signer?: Signer | "cli" | "raw" | "custom";
65
- signerOptions?: {
66
- command?: string;
67
- key?: string;
68
- address?: string;
69
- callback?: (hash: Uint8Array) => Uint8Array;
70
- };
71
- /** Private key for direct auth signing (alternative to signer). */
72
- privateKey?: string;
73
- /** Chain ID for EIP-712 domain (default: 8453 for Base). */
74
- chainId?: number;
75
- /** Router contract address for EIP-712 domain. */
76
- routerAddress?: string;
77
- }
78
-
79
- export class PayClient {
80
- private readonly apiUrl: string;
81
- /** URL path prefix extracted from apiUrl (e.g., "/api/v1"). */
82
- private readonly _basePath: string;
83
- private readonly signer: Signer;
84
- private readonly _privateKey: Hex | null;
85
- private readonly _authConfig: AuthConfig | null;
86
- private readonly _chainId: number;
87
- private readonly _address: string;
88
-
89
- constructor(options: PayClientOptions = {}) {
90
- this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
91
- // Extract the URL path to prepend to auth signing paths.
92
- // e.g., "http://host:3001/api/v1" → "/api/v1"
93
- try {
94
- this._basePath = new URL(this.apiUrl).pathname.replace(/\/+$/, "");
95
- } catch {
96
- this._basePath = "";
97
- }
98
- if (typeof options.signer === "object") {
99
- this.signer = options.signer;
100
- } else {
101
- this.signer = createSigner(options.signer ?? "cli", {
102
- ...options.signerOptions,
103
- key: options.signerOptions?.key ?? options.privateKey,
104
- });
105
- }
106
-
107
- // Private key for direct signing (preferred over Signer for auth)
108
- this._privateKey = options.privateKey
109
- ? ((options.privateKey.startsWith("0x")
110
- ? options.privateKey
111
- : "0x" + options.privateKey) as Hex)
112
- : null;
113
-
114
- this._chainId = options.chainId ?? 8453;
115
- this._address = this._privateKey
116
- ? privateKeyToAccount(this._privateKey).address
117
- : (options.signerOptions?.address ?? "");
118
-
119
- // Auth config for EIP-712 domain
120
- if (options.chainId && options.routerAddress) {
121
- this._authConfig = {
122
- chainId: options.chainId,
123
- routerAddress: options.routerAddress as Address,
124
- };
125
- } else {
126
- this._authConfig = null;
127
- }
128
- }
129
-
130
- private async getContracts(): Promise<{ router: string; tab: string; direct: string; usdc: string; chain_id: number }> {
131
- return this.get("/contracts");
132
- }
133
-
134
- // ── Direct Payment ──────────────────────────────────────────────
135
-
136
- async payDirect(
137
- to: string,
138
- amount: number,
139
- options: { memo?: string } = {}
140
- ): Promise<DirectPaymentResult> {
141
- validateAddress(to, "to");
142
- validateAmount(amount, DIRECT_MIN);
143
-
144
- // Get contract addresses to determine the spender
145
- const contracts = await this.get<{ direct: string }>("/contracts");
146
- const permit = await this.prepareAndSignPermit(amount, contracts.direct);
147
-
148
- const data = await this.post<DirectPaymentResult>("/direct", {
149
- to,
150
- amount,
151
- memo: options.memo ?? "",
152
- permit,
153
- });
154
- return data;
155
- }
156
-
157
- // ── Tab Management ──────────────────────────────────────────────
158
-
159
- async openTab(
160
- provider: string,
161
- amount: number,
162
- options: { maxChargePerCall: number }
163
- ): Promise<Tab> {
164
- validateAddress(provider, "provider");
165
- validateAmount(amount, TAB_MIN);
166
- if (options.maxChargePerCall <= 0) {
167
- throw new PayValidationError(
168
- "maxChargePerCall must be positive",
169
- "maxChargePerCall"
170
- );
171
- }
172
- const contracts = await this.get<{ tab: string }>("/contracts");
173
- const permit = await this.prepareAndSignPermit(amount, contracts.tab);
174
-
175
- return this.post<Tab>("/tabs", {
176
- provider,
177
- amount,
178
- max_charge_per_call: options.maxChargePerCall,
179
- permit,
180
- });
181
- }
182
-
183
- async closeTab(tabId: string): Promise<Tab> {
184
- return this.post<Tab>(`/tabs/${tabId}/close`, {});
185
- }
186
-
187
- async topUpTab(tabId: string, amount: number): Promise<Tab> {
188
- validateAmount(amount, 1, "amount");
189
- const contracts = await this.get<{ tab: string }>("/contracts");
190
- const permit = await this.prepareAndSignPermit(amount, contracts.tab);
191
- return this.post<Tab>(`/tabs/${tabId}/topup`, { amount, permit });
192
- }
193
-
194
- async listTabs(): Promise<Tab[]> {
195
- return this.get<Tab[]>("/tabs");
196
- }
197
-
198
- async getTab(tabId: string): Promise<Tab> {
199
- return this.get<Tab>(`/tabs/${tabId}`);
200
- }
201
-
202
- // ── x402 ────────────────────────────────────────────────────────
203
-
204
- private static readonly X402_TAB_MULTIPLIER = 10;
205
-
206
- async request(
207
- url: string,
208
- options: {
209
- method?: string;
210
- body?: unknown;
211
- headers?: Record<string, string>;
212
- } = {}
213
- ): Promise<Response> {
214
- const method = options.method ?? "GET";
215
- const headers = options.headers ?? {};
216
- const bodyStr = options.body ? JSON.stringify(options.body) : undefined;
217
-
218
- const resp = await fetch(url, { method, body: bodyStr, headers });
219
-
220
- if (resp.status !== 402) return resp;
221
-
222
- return this.handle402(resp, url, method, bodyStr, headers);
223
- }
224
-
225
- /**
226
- * Parse x402 V2 payment requirements from a 402 response.
227
- *
228
- * Checks PAYMENT-REQUIRED header first (base64-encoded JSON),
229
- * falls back to response body for requirements.
230
- */
231
- private async parse402Requirements(resp: Response): Promise<{
232
- settlement: string;
233
- amount: number;
234
- to: string;
235
- accepted?: Record<string, unknown>;
236
- }> {
237
- // Try PAYMENT-REQUIRED header (base64-encoded JSON)
238
- const prHeader = resp.headers.get("payment-required");
239
- if (prHeader) {
240
- try {
241
- const decoded = JSON.parse(atob(prHeader)) as Record<string, unknown>;
242
- return PayClient.extractRequirements(decoded);
243
- } catch {
244
- // Fall through to body parsing
245
- }
246
- }
247
-
248
- // Fallback: parse from response body
249
- const body = (await resp.json()) as Record<string, unknown>;
250
- const requirements = (body.requirements ?? body) as Record<string, unknown>;
251
- return PayClient.extractRequirements(requirements);
252
- }
253
-
254
- private static extractRequirements(obj: Record<string, unknown>): {
255
- settlement: string;
256
- amount: number;
257
- to: string;
258
- accepted?: Record<string, unknown>;
259
- } {
260
- // x402 v2 format: { accepts: [{ payTo, amount, extra: { settlement } }] }
261
- const accepts = obj.accepts as Array<Record<string, unknown>> | undefined;
262
- if (Array.isArray(accepts) && accepts.length > 0) {
263
- const offer = accepts[0];
264
- const extra = (offer.extra ?? {}) as Record<string, unknown>;
265
- return {
266
- settlement: String(extra.settlement ?? "direct"),
267
- amount: Number(offer.amount ?? 0),
268
- to: String(offer.payTo ?? ""),
269
- accepted: offer,
270
- };
271
- }
272
-
273
- // Legacy v1 format
274
- return {
275
- settlement: String(obj.settlement ?? "direct"),
276
- amount: Number(obj.amount ?? 0),
277
- to: String(obj.to ?? ""),
278
- };
279
- }
280
-
281
- private async handle402(
282
- resp: Response,
283
- url: string,
284
- method: string,
285
- body: string | undefined,
286
- headers: Record<string, string>
287
- ): Promise<Response> {
288
- const reqs = await this.parse402Requirements(resp);
289
-
290
- if (reqs.settlement === "tab") {
291
- return this.settleViaTab(url, method, body, headers, reqs);
292
- }
293
- return this.settleViaDirect(url, method, body, headers, reqs);
294
- }
295
-
296
- private async settleViaDirect(
297
- url: string,
298
- method: string,
299
- body: string | undefined,
300
- headers: Record<string, string>,
301
- reqs: { settlement: string; amount: number; to: string; accepted?: Record<string, unknown> },
302
- ): Promise<Response> {
303
- if (!this._privateKey) {
304
- throw new PayValidationError("privateKey required for x402 direct settlement", "privateKey");
305
- }
306
-
307
- const contracts = await this.getContracts();
308
- const auth = await signTransferAuthorization(
309
- this._privateKey,
310
- reqs.to as Address,
311
- reqs.amount,
312
- this._chainId,
313
- contracts.usdc as Address,
314
- );
315
-
316
- const paymentPayload = {
317
- x402Version: 2,
318
- accepted: reqs.accepted ?? {
319
- scheme: "exact",
320
- network: `eip155:${this._chainId}`,
321
- amount: String(reqs.amount),
322
- payTo: reqs.to,
323
- },
324
- payload: {
325
- signature: combinedSignature(auth),
326
- authorization: {
327
- from: auth.from,
328
- to: auth.to,
329
- value: String(reqs.amount),
330
- validAfter: "0",
331
- validBefore: "0",
332
- nonce: auth.nonce,
333
- },
334
- },
335
- extensions: {},
336
- };
337
-
338
- return fetch(url, {
339
- method,
340
- body,
341
- headers: {
342
- ...headers,
343
- "Content-Type": "application/json",
344
- "PAYMENT-SIGNATURE": btoa(JSON.stringify(paymentPayload)),
345
- },
346
- });
347
- }
348
-
349
- private async settleViaTab(
350
- url: string,
351
- method: string,
352
- body: string | undefined,
353
- headers: Record<string, string>,
354
- reqs: { settlement: string; amount: number; to: string; accepted?: Record<string, unknown> },
355
- ): Promise<Response> {
356
- const tabs = await this.listTabs();
357
- let tab = tabs.find((t) => t.provider === reqs.to && t.status === "open");
358
-
359
- if (!tab) {
360
- const tabAmount = Math.max(
361
- reqs.amount * PayClient.X402_TAB_MULTIPLIER,
362
- TAB_MIN
363
- );
364
- tab = await this.openTab(reqs.to, tabAmount, {
365
- maxChargePerCall: reqs.amount,
366
- });
367
- }
368
-
369
- const chargeData = await this.post<{ charge_id: string }>(
370
- `/tabs/${tab.tabId}/charge`,
371
- { amount: reqs.amount }
372
- );
373
-
374
- const paymentPayload = {
375
- x402Version: 2,
376
- accepted: reqs.accepted ?? {
377
- scheme: "exact",
378
- network: `eip155:${this._chainId}`,
379
- amount: String(reqs.amount),
380
- payTo: reqs.to,
381
- },
382
- payload: {
383
- authorization: { from: this._address },
384
- },
385
- extensions: {
386
- pay: {
387
- settlement: "tab",
388
- tabId: tab.tabId,
389
- chargeId: chargeData.charge_id ?? "",
390
- },
391
- },
392
- };
393
-
394
- return fetch(url, {
395
- method,
396
- body,
397
- headers: {
398
- ...headers,
399
- "Content-Type": "application/json",
400
- "PAYMENT-SIGNATURE": btoa(JSON.stringify(paymentPayload)),
401
- },
402
- });
403
- }
404
-
405
- // ── Wallet ──────────────────────────────────────────────────────
406
-
407
- async getStatus(): Promise<StatusResponse> {
408
- const raw = await this.get<{
409
- wallet: string;
410
- balance_usdc: string | null;
411
- open_tabs: number;
412
- total_locked: number;
413
- }>("/status");
414
- return {
415
- address: raw.wallet,
416
- balance: raw.balance_usdc ? Number(raw.balance_usdc) : 0,
417
- openTabs: [],
418
- };
419
- }
420
-
421
- // ── Webhooks ────────────────────────────────────────────────────
422
-
423
- async registerWebhook(
424
- url: string,
425
- options: { events?: string[]; secret?: string } = {}
426
- ): Promise<WebhookRegistration> {
427
- const payload: Record<string, unknown> = { url };
428
- if (options.events) payload.events = options.events;
429
- if (options.secret) payload.secret = options.secret;
430
- const raw = await this.post<{ id: string; wallet: string; url: string; events: string[]; active: boolean }>("/webhooks", payload);
431
- return { webhookId: raw.id, url: raw.url, events: raw.events };
432
- }
433
-
434
- async listWebhooks(): Promise<WebhookRegistration[]> {
435
- const raw = await this.get<{ id: string; wallet: string; url: string; events: string[]; active: boolean }[]>("/webhooks");
436
- return raw.map(w => ({ webhookId: w.id, url: w.url, events: w.events }));
437
- }
438
-
439
- async deleteWebhook(webhookId: string): Promise<void> {
440
- await this.del(`/webhooks/${webhookId}`);
441
- }
442
-
443
- // ── Funding ─────────────────────────────────────────────────────
444
-
445
- /** Create a one-time fund link via the server. Returns the dashboard URL. */
446
- async createFundLink(options?: {
447
- messages?: unknown[];
448
- agentName?: string;
449
- }): Promise<string> {
450
- const data = await this.post<{ url: string }>("/links/fund", {
451
- messages: options?.messages ?? [],
452
- agent_name: options?.agentName,
453
- });
454
- return data.url;
455
- }
456
-
457
- /** Create a one-time withdraw link via the server. Returns the dashboard URL. */
458
- async createWithdrawLink(options?: {
459
- messages?: unknown[];
460
- agentName?: string;
461
- }): Promise<string> {
462
- const data = await this.post<{ url: string }>("/links/withdraw", {
463
- messages: options?.messages ?? [],
464
- agent_name: options?.agentName,
465
- });
466
- return data.url;
467
- }
468
-
469
- // ── Discovery ──────────────────────────────────────────────────
470
-
471
- /** Search for discoverable paid API services. Public, no auth required. */
472
- async discover(options?: DiscoverOptions): Promise<DiscoverService[]> {
473
- const params = new URLSearchParams();
474
- if (options?.query) params.set("q", options.query);
475
- if (options?.sort) params.set("sort", options.sort);
476
- if (options?.category) params.set("category", options.category);
477
- if (options?.settlement) params.set("settlement", options.settlement);
478
-
479
- const qs = params.toString();
480
- const url = `${this.apiUrl}/discover${qs ? `?${qs}` : ""}`;
481
- const resp = await fetch(url);
482
-
483
- if (!resp.ok) {
484
- const body = await resp.text().catch(() => "");
485
- throw new PayServerError(`discover failed: ${body}`, resp.status);
486
- }
487
-
488
- const data = (await resp.json()) as { services: DiscoverService[] };
489
- return data.services;
490
- }
491
-
492
- // ── Permit signing ────────────────────────────────────────────
493
-
494
- /**
495
- * Prepare and sign a USDC EIP-2612 permit.
496
- *
497
- * 1. Calls GET /api/v1/permit/prepare to get the EIP-712 hash
498
- * 2. Signs the hash with the agent's private key
499
- * 3. Returns {nonce, deadline, v, r, s} for inclusion in payment body
500
- */
501
- private async prepareAndSignPermit(
502
- amount: number,
503
- spender: string
504
- ): Promise<{ nonce: string; deadline: number; v: number; r: string; s: string }> {
505
- if (!this._privateKey) {
506
- throw new PayValidationError(
507
- "privateKey required for permit signing",
508
- "privateKey"
509
- );
510
- }
511
-
512
- const prepare = await this.post<{
513
- hash: string;
514
- nonce: string;
515
- deadline: number;
516
- }>("/permit/prepare", { amount, spender });
517
-
518
- // Sign the hash
519
- const hashHex = prepare.hash as Hex;
520
- const raw = await viemSign({ hash: hashHex, privateKey: this._privateKey });
521
- const sigHex = serializeSignature(raw);
522
-
523
- // Parse signature into v, r, s
524
- const sigBytes = Buffer.from(sigHex.slice(2), "hex");
525
- const r = "0x" + sigBytes.subarray(0, 32).toString("hex");
526
- const s = "0x" + sigBytes.subarray(32, 64).toString("hex");
527
- const v = sigBytes[64];
528
-
529
- return {
530
- nonce: prepare.nonce,
531
- deadline: prepare.deadline,
532
- v,
533
- r,
534
- s,
535
- };
536
- }
537
-
538
- // ── Auth headers ──────────────────────────────────────────────
539
-
540
- private async authHeaders(
541
- method: string,
542
- path: string
543
- ): Promise<AuthHeaders | null> {
544
- if (!this._authConfig) return null;
545
-
546
- // Sign only the path portion (no query string) — server verifies against uri.path().
547
- // e.g., basePath="/api/v1" + path="/status" → "/api/v1/status"
548
- const fullPath = this._basePath + path.split("?")[0];
549
-
550
- if (this._privateKey) {
551
- return buildAuthHeaders(
552
- this._privateKey,
553
- method,
554
- fullPath,
555
- this._authConfig
556
- );
557
- }
558
-
559
- if (this.signer.address) {
560
- return buildAuthHeadersWithSigner(
561
- this.signer,
562
- method,
563
- fullPath,
564
- this._authConfig
565
- );
566
- }
567
-
568
- return null;
569
- }
570
-
571
- // ── HTTP helpers ────────────────────────────────────────────────
572
-
573
- private async get<T>(path: string): Promise<T> {
574
- let resp: Response;
575
- try {
576
- const auth = await this.authHeaders("GET", path);
577
- resp = await fetch(`${this.apiUrl}${path}`, {
578
- method: "GET",
579
- headers: {
580
- "Content-Type": "application/json",
581
- ...auth,
582
- },
583
- });
584
- } catch (e) {
585
- throw new PayNetworkError(String(e));
586
- }
587
- return this.handleResponse<T>(resp);
588
- }
589
-
590
- private async post<T>(path: string, payload: unknown): Promise<T> {
591
- let resp: Response;
592
- try {
593
- const auth = await this.authHeaders("POST", path);
594
- resp = await fetch(`${this.apiUrl}${path}`, {
595
- method: "POST",
596
- headers: {
597
- "Content-Type": "application/json",
598
- ...auth,
599
- },
600
- body: JSON.stringify(payload),
601
- });
602
- } catch (e) {
603
- throw new PayNetworkError(String(e));
604
- }
605
- return this.handleResponse<T>(resp);
606
- }
607
-
608
- private async del(path: string): Promise<void> {
609
- let resp: Response;
610
- try {
611
- const auth = await this.authHeaders("DELETE", path);
612
- resp = await fetch(`${this.apiUrl}${path}`, {
613
- method: "DELETE",
614
- headers: {
615
- "Content-Type": "application/json",
616
- ...auth,
617
- },
618
- });
619
- } catch (e) {
620
- throw new PayNetworkError(String(e));
621
- }
622
- if (resp.status >= 400) {
623
- const text = await resp.text();
624
- throw new PayServerError(text, resp.status);
625
- }
626
- }
627
-
628
- private async handleResponse<T>(resp: Response): Promise<T> {
629
- if (resp.status >= 400) {
630
- let msg: string;
631
- try {
632
- const body = (await resp.json()) as { error?: string };
633
- msg = body.error ?? (await resp.text());
634
- } catch {
635
- msg = await resp.text();
636
- }
637
- throw new PayServerError(msg, resp.status);
638
- }
639
- if (resp.status === 204) {
640
- return undefined as T;
641
- }
642
- return (await resp.json()) as T;
643
- }
644
- }