@itpay/cli 2.0.3 → 2.0.7
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/README.md +96 -142
- package/dist/src/client/backend.js +26 -8
- package/dist/src/client/http.js +29 -23
- package/dist/src/commands/buy.js +84 -132
- package/dist/src/commands/cart.js +274 -169
- package/dist/src/commands/catalog.js +64 -38
- package/dist/src/commands/checkout.js +128 -79
- package/dist/src/commands/docs.js +97 -51
- package/dist/src/commands/guidance.js +112 -16
- package/dist/src/commands/install.js +50 -87
- package/dist/src/commands/next.js +45 -0
- package/dist/src/commands/order.js +44 -69
- package/dist/src/commands/orders.js +43 -15
- package/dist/src/commands/pay.js +51 -22
- package/dist/src/commands/readyz.js +8 -4
- package/dist/src/commands/refund.js +132 -11
- package/dist/src/commands/services.js +799 -147
- package/dist/src/commands/skill.js +55 -0
- package/dist/src/main.js +820 -193
- package/dist/src/render/output.js +2 -3
- package/dist/src/state/agent_type.js +19 -0
- package/dist/src/state/cart_session.js +13 -17
- package/dist/src/state/client_context.js +4 -2
- package/dist/src/state/config.js +5 -15
- package/dist/src/state/device_authority.js +175 -57
- package/docs/agent/buyer/cart-checkout.json +27 -83
- package/docs/agent/buyer/catalog-list.json +2 -1
- package/docs/agent/buyer/identity-and-sessions.json +64 -0
- package/docs/agent/buyer/install-and-setup.json +35 -65
- package/docs/agent/buyer/orders-refunds.json +31 -53
- package/docs/agent/buyer/payment-flow.json +28 -57
- package/docs/agent/buyer/quickstart.json +46 -161
- package/docs/agent/buyer/render-hosts.json +43 -57
- package/docs/cli-reference/agent-types.md +51 -0
- package/docs/cli-reference/commands/buy.md +167 -0
- package/docs/cli-reference/commands/cart/add.md +86 -0
- package/docs/cli-reference/commands/cart/clear.md +53 -0
- package/docs/cli-reference/commands/cart/index.md +30 -0
- package/docs/cli-reference/commands/cart/next.md +71 -0
- package/docs/cli-reference/commands/cart/remove.md +53 -0
- package/docs/cli-reference/commands/cart/show.md +65 -0
- package/docs/cli-reference/commands/catalog/index.md +26 -0
- package/docs/cli-reference/commands/catalog/list.md +45 -0
- package/docs/cli-reference/commands/checkout.md +74 -0
- package/docs/cli-reference/commands/device.md +13 -0
- package/docs/cli-reference/commands/docs/index.md +28 -0
- package/docs/cli-reference/commands/docs/list.md +51 -0
- package/docs/cli-reference/commands/docs/search.md +69 -0
- package/docs/cli-reference/commands/docs/show.md +68 -0
- package/docs/cli-reference/commands/install.md +114 -0
- package/docs/cli-reference/commands/next.md +87 -0
- package/docs/cli-reference/commands/order.md +92 -0
- package/docs/cli-reference/commands/orders.md +83 -0
- package/docs/cli-reference/commands/pay.md +103 -0
- package/docs/cli-reference/commands/readyz.md +38 -0
- package/docs/cli-reference/commands/refund/cancel.md +62 -0
- package/docs/cli-reference/commands/refund/create.md +85 -0
- package/docs/cli-reference/commands/refund/get.md +60 -0
- package/docs/cli-reference/commands/refund/index.md +33 -0
- package/docs/cli-reference/commands/refund/list.md +68 -0
- package/docs/cli-reference/commands/refund/watch.md +73 -0
- package/docs/cli-reference/commands/services/action.md +48 -0
- package/docs/cli-reference/commands/services/checkout.md +82 -0
- package/docs/cli-reference/commands/services/events.md +73 -0
- package/docs/cli-reference/commands/services/get.md +66 -0
- package/docs/cli-reference/commands/services/index.md +45 -0
- package/docs/cli-reference/commands/services/invoke.md +67 -0
- package/docs/cli-reference/commands/services/list.md +61 -0
- package/docs/cli-reference/commands/services/next.md +181 -0
- package/docs/cli-reference/commands/services/quote.md +63 -0
- package/docs/cli-reference/commands/services/read-result.md +98 -0
- package/docs/cli-reference/commands/services/start.md +55 -0
- package/docs/cli-reference/commands/skill.md +17 -0
- package/docs/cli-reference/conventions.md +97 -0
- package/docs/cli-reference/index.md +65 -0
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +71 -110
package/README.md
CHANGED
|
@@ -1,152 +1,106 @@
|
|
|
1
1
|
# ItPay CLI
|
|
2
2
|
|
|
3
|
-
The V3
|
|
3
|
+
The official V3 CLI for Agent-driven ItPay service discovery, checkout, delivery, order recovery, and refunds.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @itpay/cli
|
|
7
|
-
itpay readyz
|
|
8
|
-
itpay
|
|
7
|
+
itpay readyz --json
|
|
8
|
+
itpay skill show itpay-buyer --json
|
|
9
|
+
itpay install --json
|
|
10
|
+
itpay --agent-type codex-desktop readyz --json
|
|
11
|
+
# follow next.command: typed skill show, then catalog list
|
|
9
12
|
```
|
|
10
13
|
|
|
11
|
-
The
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
14
|
+
The default API is `https://app.itpay.ai`. Set `ITPAY_BACKEND_URL` only for an intentional test or local environment.
|
|
15
|
+
|
|
16
|
+
## Output Contract
|
|
17
|
+
|
|
18
|
+
Normal JSON commands return one bounded envelope:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
status current command state
|
|
22
|
+
result facts needed at this step
|
|
23
|
+
handoff optional human-visible URL/image fields for the current Host
|
|
24
|
+
instruction how the Agent must use the result
|
|
25
|
+
next zero or one preferred executable command
|
|
26
|
+
recovery exceptional recovery commands only
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Run `next.command` unchanged after filling only explicit placeholders or user-provided required fields. Do not inspect raw APIs or hardcode a service workflow.
|
|
30
|
+
|
|
31
|
+
Normative per-command contracts: [CLI Command Reference](docs/cli-reference/index.md).
|
|
32
|
+
|
|
33
|
+
## Supported Agent Types
|
|
34
|
+
|
|
35
|
+
| Agent Type | Default Host |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `codex-desktop` | `codex` |
|
|
38
|
+
| `codex-cli` | `terminal` |
|
|
39
|
+
| `claude-code-desktop` | `claude-code` |
|
|
40
|
+
| `claude-code-cli` | `terminal` |
|
|
41
|
+
| `workbuddy` | `plain-chat` |
|
|
42
|
+
|
|
43
|
+
`--agent-type` identifies the stable runtime and registered Agent instance. Every returned ItPay command preserves it. Different windows or chats of the same type reuse one Agent Instance; they are not separate identities. `--host` only selects the human presentation surface, and `--target` only routes output to a Host destination. Use `itpay install <agent_type> --json` for the exact responsibility.
|
|
44
|
+
|
|
45
|
+
The local installation keeps one Ed25519 private key. Each normalized Backend API base URL (`dev`, `test`, `app`, or local) has an independent server device registration, quota lineage, Agent instances, and sessions. A rejected session is renewed and the same request is retried once; revoked v2 registrations are never silently replaced.
|
|
46
|
+
|
|
47
|
+
## Command Families
|
|
48
|
+
|
|
49
|
+
- `readyz`, `catalog list`: compatibility and discovery.
|
|
50
|
+
- `services start/invoke/action/checkout/next`: generic Service Execution flow.
|
|
51
|
+
- `cart add/show/remove/clear/next`, `buy`: canonical Cart and ordinary Checkout flow.
|
|
52
|
+
- `checkout`: authoritative payment and fulfillment recovery.
|
|
53
|
+
- `services read-result`: read one human-granted protected result.
|
|
54
|
+
- `order`, `orders`: exact order and account order views.
|
|
55
|
+
- `refund create/list/get/watch/cancel`: Refund Owner flow.
|
|
56
|
+
- `services get/events`: redacted support diagnostics; normal flows should use `services next`.
|
|
57
|
+
- `install`, `skill show`, `docs list/show/search`: offline packaged guidance.
|
|
58
|
+
- `pay`: operator escape hatch only; normal buyers use the ItPay Checkout page.
|
|
59
|
+
|
|
60
|
+
Run `itpay <command> --help` or browse [the command index](docs/cli-reference/index.md) for parameters.
|
|
61
|
+
|
|
62
|
+
## Recovery
|
|
63
|
+
|
|
64
|
+
Use server-backed recovery before creating another resource:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
itpay --agent-type <agent_type> next --json
|
|
68
|
+
itpay --agent-type <agent_type> services list --json
|
|
69
|
+
itpay --agent-type <agent_type> services next <service_execution_id> --json
|
|
70
|
+
itpay --agent-type <agent_type> services checkout <service_execution_id> --resume --json
|
|
71
|
+
itpay checkout --id <checkout_id> --token <display_token> --json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The local `~/.itpay-v3` directory stores one owner-only signing key, Backend-scoped Device registrations and Agent instances, idempotency operations, and recovery handles. Backend state remains authoritative. Do not delete or rotate this identity to recover quota.
|
|
54
75
|
|
|
55
76
|
## Environment
|
|
56
77
|
|
|
57
|
-
- `ITPAY_BACKEND_URL
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
60
|
-
- `
|
|
61
|
-
- `
|
|
62
|
-
- `
|
|
63
|
-
- `
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`itpay order`) downloads the
|
|
87
|
-
brand checkout QR from the backend and writes it to a stable local
|
|
88
|
-
file the agent can hand to the IDE image viewer (Trae `Read` tool,
|
|
89
|
-
Codex, Claude Code). The contract is:
|
|
90
|
-
|
|
91
|
-
- canonical file: `<os.tmpdir()>/itpay-v3-qr/itpay-v3-<kind>-<id>.png`
|
|
92
|
-
(override with `ITPAY_IDE_IMAGE_DIR_OVERRIDE`)
|
|
93
|
-
- when `/tmp/itpay-v3-qr` is a separate, writable location it gets a
|
|
94
|
-
mirror of the same file under the same name
|
|
95
|
-
- filename is stable per checkout, so re-runs overwrite the same
|
|
96
|
-
local file rather than scattering copies across the scratch dir
|
|
97
|
-
|
|
98
|
-
Outputs that carry the IDE image attach:
|
|
99
|
-
|
|
100
|
-
- `itpay buy --json` — fields `brand_qr_local_path`, `brand_qr_mirrors`,
|
|
101
|
-
`brand_qr_stable_name`, `brand_qr_status` (`downloaded` / `failed`
|
|
102
|
-
/ `disabled` / `fallback`), `brand_qr_error`, `brand_qr_data_url`,
|
|
103
|
-
`brand_qr_must_render_reason`, `brand_qr_render_action`. Read the
|
|
104
|
-
path with the IDE's `Read` tool so the human sees the picture.
|
|
105
|
-
- `itpay services checkout --json` — same brand QR fields, plus
|
|
106
|
-
`next_action: "open_human_checkout"` and the checkout-scoped
|
|
107
|
-
`display_token`. Agents must show this ItPay checkout QR/URL to the
|
|
108
|
-
human and must not call `itpay pay` for the normal buyer flow.
|
|
109
|
-
- Markdown (Trae / Codex / Claude Code) — inlines a `data:image/png;base64,...`
|
|
110
|
-
copy of the picture plus a `[ATTACH] IDE image` reference block that
|
|
111
|
-
points at the canonical local path and mirrors.
|
|
112
|
-
- Terminal — prints `Branded QR: /path/...png` and `QR mirrors: ...`;
|
|
113
|
-
iTerm inline image renders the same file when the session supports it.
|
|
114
|
-
- Telegram — the `ide_image_attach` block on `presentation.ide_image_attach`
|
|
115
|
-
carries `status`, `local_path`, `mirrors`, `mime_type`, `source`,
|
|
116
|
-
`caption`, `error` (when failed), `must_render_reason`, and a
|
|
117
|
-
step-by-step `instructions` array.
|
|
118
|
-
- Feishu / Lark — same `ide_image_attach` block on the
|
|
119
|
-
`message.ide_image_attach` envelope.
|
|
120
|
-
|
|
121
|
-
Disable the contract with `ITPAY_IDE_IMAGE_ATTACH=0` for runners on a
|
|
122
|
-
read-only filesystem. The plan carries `status: "disabled"` instead
|
|
123
|
-
of `status: "downloaded"` and no PNG is downloaded.
|
|
124
|
-
|
|
125
|
-
## Layout
|
|
126
|
-
|
|
127
|
-
- `src/main.ts` — `commander` entrypoint, command registration
|
|
128
|
-
- `src/client/` — HTTP/JSON client and DTOs
|
|
129
|
-
- `src/commands/` — one file per command family
|
|
130
|
-
- `src/render/` — terminal formatting
|
|
131
|
-
- `plan.ts` — `RenderPlan` contract shared by all renderers
|
|
132
|
-
- `qr.ts` — local QR + format selection
|
|
133
|
-
- `terminal.ts`, `markdown.ts`, `plain_chat.ts`, `telegram.ts`, `feishu.ts` — per-host renderers
|
|
134
|
-
- `index.ts` — `dispatchRender()` picks the right renderer
|
|
135
|
-
- `sink.ts` — `OutputSink` so tests can silence stdout
|
|
136
|
-
- `src/state/` — local CLI config, cart session, client context
|
|
137
|
-
- `tests/` — node:test smoke test + in-process mock backend
|
|
138
|
-
|
|
139
|
-
## Rules
|
|
140
|
-
|
|
141
|
-
- commands orchestrate user intent only
|
|
142
|
-
- render code must not issue HTTP calls
|
|
143
|
-
- keep API access under `src/client/`
|
|
144
|
-
- persist checkout-scoped `display_token` and last server handles only in the owner-only local cart session file
|
|
145
|
-
- the default `buy` command must not create a payment intent unless `--pay` is explicit
|
|
146
|
-
- the default `services checkout` command must render the ItPay
|
|
147
|
-
checkout handoff; provider payment intents are created by the human
|
|
148
|
-
checkout page, not by the agent
|
|
149
|
-
- a renderer must consume the brand QR the V3 backend hands back
|
|
150
|
-
(`qr_payload` / `qr_png_url` / `mobile_wallet_url`) and only
|
|
151
|
-
self-generate a QR for `auth_qr` / `checkout_qr` with the explicit
|
|
152
|
-
`--qr-file` opt-in
|
|
78
|
+
- `ITPAY_BACKEND_URL`: deliberate API override.
|
|
79
|
+
- `ITPAY_AGENT_TYPE`: stable alternative to global `--agent-type`.
|
|
80
|
+
- `ITPAY_BEARER_TOKEN`: account-scoped Buyer session for account-only commands such as `orders`.
|
|
81
|
+
- `ITPAY_CART_SESSION_PATH`: local recovery-state path override.
|
|
82
|
+
- `ITPAY_CURRENCY`: ordinary Cart currency, default `CNY`.
|
|
83
|
+
- `ITPAY_IDEMPOTENCY_KEY`: explicit operation key for deterministic testing; normal use persists operation IDs automatically.
|
|
84
|
+
- `ITPAY_IDE_IMAGE_ATTACH=0`: disable local Checkout image download when the runtime filesystem is read-only.
|
|
85
|
+
- `ITPAY_IDE_IMAGE_DIR_OVERRIDE`: override the local Checkout image directory.
|
|
86
|
+
|
|
87
|
+
Provider credentials, Buyer identity, payment provider choice, amount, refund policy, quota, grant scope, and delivery access are never client-owned environment settings.
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm run lint
|
|
93
|
+
npm test
|
|
94
|
+
npm run test:package
|
|
95
|
+
npm run pack:dry-run
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Source boundaries:
|
|
99
|
+
|
|
100
|
+
- `src/main.ts`: parser and error-envelope wiring.
|
|
101
|
+
- `src/client`: typed HTTP access.
|
|
102
|
+
- `src/commands`: command orchestration and public output projection.
|
|
103
|
+
- `src/render`: Host presentation only; no business HTTP.
|
|
104
|
+
- `src/state`: local Device Authority, idempotency journal, and recovery handles.
|
|
105
|
+
- `docs/cli-reference`: normative command contracts.
|
|
106
|
+
- `docs/agent/buyer`: packaged progressive workflow guidance.
|
|
@@ -32,17 +32,16 @@ export class BackendClient {
|
|
|
32
32
|
return this.http.delete(`/v1/carts/${encodeURIComponent(cartID)}`);
|
|
33
33
|
}
|
|
34
34
|
// --- Checkout ---
|
|
35
|
-
createCheckout(input) {
|
|
36
|
-
return this.http.post("/v1/checkouts", input);
|
|
35
|
+
createCheckout(input, idempotencyKey) {
|
|
36
|
+
return this.http.post("/v1/checkouts", input, idempotencyKey ? { idempotencyKey } : undefined);
|
|
37
37
|
}
|
|
38
38
|
getCheckoutPresentation(checkoutID, displayToken) {
|
|
39
39
|
const qs = new URLSearchParams({ display_token: displayToken });
|
|
40
40
|
return this.http.get(`/v1/checkouts/${encodeURIComponent(checkoutID)}/presentation?${qs}`);
|
|
41
41
|
}
|
|
42
42
|
// --- Payment intents ---
|
|
43
|
-
createPaymentIntent(checkoutID, input
|
|
44
|
-
|
|
45
|
-
return this.http.post(`/v1/checkouts/${encodeURIComponent(checkoutID)}/payment-intents`, input, options);
|
|
43
|
+
createPaymentIntent(checkoutID, input) {
|
|
44
|
+
return this.http.post(`/v1/checkouts/${encodeURIComponent(checkoutID)}/payment-intents`, input);
|
|
46
45
|
}
|
|
47
46
|
// --- SSE streaming ---
|
|
48
47
|
streamCheckoutEvents(checkoutID, displayToken, onEvent, signal) {
|
|
@@ -54,6 +53,9 @@ export class BackendClient {
|
|
|
54
53
|
getOrder(orderID) {
|
|
55
54
|
return this.http.get(`/v1/orders/${encodeURIComponent(orderID)}`);
|
|
56
55
|
}
|
|
56
|
+
getOrderDeliveryAccess(orderID) {
|
|
57
|
+
return this.http.get(`/v1/orders/${encodeURIComponent(orderID)}/delivery-access`);
|
|
58
|
+
}
|
|
57
59
|
listAccountOrders(limit, status, bearer) {
|
|
58
60
|
const qs = new URLSearchParams({ limit: String(limit) });
|
|
59
61
|
if (status) {
|
|
@@ -63,9 +65,18 @@ export class BackendClient {
|
|
|
63
65
|
}
|
|
64
66
|
// --- Refund ---
|
|
65
67
|
createRefund(orderID, input, bearer, idempotencyKey) {
|
|
66
|
-
const options = { bearer, ...(idempotencyKey ? { idempotencyKey } : {}) };
|
|
68
|
+
const options = { ...(bearer ? { bearer } : {}), ...(idempotencyKey ? { idempotencyKey } : {}) };
|
|
67
69
|
return this.http.post(`/v1/orders/${encodeURIComponent(orderID)}/refunds`, input, options);
|
|
68
70
|
}
|
|
71
|
+
listOrderRefunds(orderID) {
|
|
72
|
+
return this.http.get(`/v1/orders/${encodeURIComponent(orderID)}/refunds`);
|
|
73
|
+
}
|
|
74
|
+
getRefund(refundRequestID) {
|
|
75
|
+
return this.http.get(`/v1/refunds/${encodeURIComponent(refundRequestID)}`);
|
|
76
|
+
}
|
|
77
|
+
cancelRefund(refundRequestID, reason = "buyer_cancelled") {
|
|
78
|
+
return this.http.post(`/v1/refunds/${encodeURIComponent(refundRequestID)}/cancel`, { reason });
|
|
79
|
+
}
|
|
69
80
|
// --- Service Execution ---
|
|
70
81
|
startServiceExecution(input) {
|
|
71
82
|
return this.http.post("/v1/service-executions", input);
|
|
@@ -79,14 +90,21 @@ export class BackendClient {
|
|
|
79
90
|
createServiceExecutionCheckout(serviceExecutionID, input) {
|
|
80
91
|
return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/checkout`, input);
|
|
81
92
|
}
|
|
93
|
+
prepareServiceQuote(serviceExecutionID, input) {
|
|
94
|
+
return this.http.post(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/quotes`, input);
|
|
95
|
+
}
|
|
82
96
|
getServiceExecution(serviceExecutionID) {
|
|
83
97
|
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}`);
|
|
84
98
|
}
|
|
85
99
|
listServiceExecutions(limit = 50) {
|
|
86
100
|
return this.http.get(`/v1/service-executions?limit=${limit}`);
|
|
87
101
|
}
|
|
88
|
-
listServiceExecutionEvents(serviceExecutionID) {
|
|
89
|
-
|
|
102
|
+
listServiceExecutionEvents(serviceExecutionID, afterSequence = 0, limit = 50) {
|
|
103
|
+
const query = new URLSearchParams({
|
|
104
|
+
after_sequence: String(afterSequence),
|
|
105
|
+
limit: String(limit),
|
|
106
|
+
});
|
|
107
|
+
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/events?${query}`);
|
|
90
108
|
}
|
|
91
109
|
getGrantedServiceResult(serviceExecutionID) {
|
|
92
110
|
return this.http.get(`/v1/service-executions/${encodeURIComponent(serviceExecutionID)}/granted-result`);
|
package/dist/src/client/http.js
CHANGED
|
@@ -17,6 +17,7 @@ export class HttpClient {
|
|
|
17
17
|
fetchImpl;
|
|
18
18
|
defaultHeaders;
|
|
19
19
|
requestAuthorizer;
|
|
20
|
+
recoverAuthorization;
|
|
20
21
|
constructor(config) {
|
|
21
22
|
this.baseURL = config.baseURL.replace(/\/$/, "");
|
|
22
23
|
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
@@ -26,35 +27,40 @@ export class HttpClient {
|
|
|
26
27
|
...(config.defaultHeaders ?? {}),
|
|
27
28
|
};
|
|
28
29
|
this.requestAuthorizer = config.requestAuthorizer;
|
|
30
|
+
this.recoverAuthorization = config.recoverAuthorization;
|
|
29
31
|
}
|
|
30
32
|
async request(path, options = {}) {
|
|
31
33
|
const url = path.startsWith("http") ? path : this.baseURL + path;
|
|
32
|
-
const headers = { ...this.defaultHeaders };
|
|
33
34
|
const method = options.method ?? "GET";
|
|
34
35
|
const body = options.body !== undefined ? JSON.stringify(options.body) : "";
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
const requestPath = new URL(url).pathname + new URL(url).search;
|
|
37
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
38
|
+
const headers = { ...this.defaultHeaders };
|
|
39
|
+
if (this.requestAuthorizer) {
|
|
40
|
+
Object.assign(headers, await this.requestAuthorizer({ method, path: requestPath, body }));
|
|
41
|
+
}
|
|
42
|
+
if (options.bearer)
|
|
43
|
+
headers.Authorization = `Bearer ${options.bearer}`;
|
|
44
|
+
if (options.idempotencyKey)
|
|
45
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
46
|
+
const response = await this.fetchImpl(url, {
|
|
47
|
+
method,
|
|
48
|
+
headers,
|
|
49
|
+
...(options.body !== undefined ? { body } : {}),
|
|
50
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
51
|
+
});
|
|
52
|
+
const text = await response.text();
|
|
53
|
+
const parsed = text.length > 0 ? safeParseJson(text) : undefined;
|
|
54
|
+
if (response.ok)
|
|
55
|
+
return parsed;
|
|
56
|
+
const error = new HttpError(response.status, parsed, `HTTP ${response.status}`);
|
|
57
|
+
if (attempt === 0 && error.status === 401 && error.code === "agent_device_session_required" && this.recoverAuthorization) {
|
|
58
|
+
await this.recoverAuthorization();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
37
62
|
}
|
|
38
|
-
|
|
39
|
-
headers.Authorization = `Bearer ${options.bearer}`;
|
|
40
|
-
}
|
|
41
|
-
if (options.idempotencyKey) {
|
|
42
|
-
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
43
|
-
}
|
|
44
|
-
const requestInit = {
|
|
45
|
-
method,
|
|
46
|
-
headers,
|
|
47
|
-
...(options.body !== undefined ? { body } : {}),
|
|
48
|
-
...(options.signal ? { signal: options.signal } : {}),
|
|
49
|
-
};
|
|
50
|
-
const response = await this.fetchImpl(url, requestInit);
|
|
51
|
-
const text = await response.text();
|
|
52
|
-
const parsed = text.length > 0 ? safeParseJson(text) : undefined;
|
|
53
|
-
if (!response.ok) {
|
|
54
|
-
const errPayload = parsed;
|
|
55
|
-
throw new HttpError(response.status, errPayload, `HTTP ${response.status}`);
|
|
56
|
-
}
|
|
57
|
-
return parsed;
|
|
63
|
+
throw new Error("unreachable HTTP retry state");
|
|
58
64
|
}
|
|
59
65
|
get(path, options = {}) {
|
|
60
66
|
return this.request(path, { ...options, method: "GET" });
|