@agentlayer.tech/wallet 0.1.18 → 0.1.19

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.
@@ -102,6 +102,35 @@ def _unwrap_payload(response: httpx.Response) -> dict[str, Any]:
102
102
  return data
103
103
 
104
104
 
105
+ def _unwrap_list_payload(response: httpx.Response) -> list[dict[str, Any]]:
106
+ try:
107
+ payload = response.json()
108
+ except Exception as exc: # pragma: no cover - defensive
109
+ raise WalletBackendError(
110
+ f"wdk-evm-wallet returned a non-JSON response ({response.status_code}).",
111
+ code="network_unavailable",
112
+ details={
113
+ "service": "wdk-evm-wallet",
114
+ "http_status": response.status_code,
115
+ },
116
+ ) from exc
117
+ if response.status_code >= 400 or payload.get("ok") is False:
118
+ detail = payload.get("error") or f"HTTP {response.status_code}"
119
+ raise WalletBackendError(
120
+ str(detail),
121
+ code=str(payload.get("error_code") or "").strip() or None,
122
+ details=_error_details_from_payload(payload),
123
+ )
124
+ data = payload.get("data")
125
+ if not isinstance(data, list):
126
+ raise WalletBackendError("wdk-evm-wallet returned an invalid list response payload.")
127
+ wallets: list[dict[str, Any]] = []
128
+ for item in data:
129
+ if isinstance(item, dict):
130
+ wallets.append(dict(item))
131
+ return wallets
132
+
133
+
105
134
  class WdkEvmLocalClient:
106
135
  """Small client for the local EVM wallet service."""
107
136
 
@@ -203,3 +232,26 @@ class WdkEvmLocalClient:
203
232
  details={"service": "wdk-evm-wallet", "path": path},
204
233
  ) from exc
205
234
  return _unwrap_payload(response)
235
+
236
+ def list_wallets_sync(self) -> list[dict[str, Any]]:
237
+ try:
238
+ with httpx.Client(
239
+ timeout=float(settings.http_timeout),
240
+ headers=self._headers,
241
+ follow_redirects=False,
242
+ trust_env=False,
243
+ ) as client:
244
+ response = client.get(f"{self.base_url}/v1/evm/wallets")
245
+ except httpx.TimeoutException as exc:
246
+ raise WalletBackendError(
247
+ "wdk-evm-wallet request timed out.",
248
+ code="network_unavailable",
249
+ details={"service": "wdk-evm-wallet", "path": "/v1/evm/wallets"},
250
+ ) from exc
251
+ except httpx.RequestError as exc:
252
+ raise WalletBackendError(
253
+ f"wdk-evm-wallet request failed: {exc}",
254
+ code="network_unavailable",
255
+ details={"service": "wdk-evm-wallet", "path": "/v1/evm/wallets"},
256
+ ) from exc
257
+ return _unwrap_list_payload(response)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openclaw-agent-wallet"
7
- version = "0.1.18"
7
+ version = "0.1.19"
8
8
  description = "Plugin-friendly wallet backend for OpenClaw agents"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -33,6 +33,7 @@ INCLUDED_TOP_LEVEL_DIRS = [
33
33
  EXCLUDED_EXACT_RELATIVE_PATHS = {
34
34
  ".openclaw/extensions-local",
35
35
  ".openclaw/openclaw.local.example.json",
36
+ ".openclaw/extensions/pay-bridge",
36
37
  }
37
38
  EXCLUDED_DIR_NAMES = {
38
39
  ".git",
@@ -201,6 +201,9 @@ def _ignore_runtime_entries(_directory: str, names: list[str]) -> set[str]:
201
201
  keep_dist = ".openclaw" in directory.parts and "extensions" in directory.parts
202
202
  ignored: set[str] = set()
203
203
  for name in names:
204
+ if name == "pay-bridge" and directory.parts[-2:] == (".openclaw", "extensions"):
205
+ ignored.add(name)
206
+ continue
204
207
  if name == "dist" and keep_dist:
205
208
  continue
206
209
  if name in EXCLUDED_RUNTIME_DIR_NAMES:
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import json
7
7
  import os
8
- import shutil
8
+ import secrets
9
9
  import sys
10
10
  from datetime import datetime, timezone
11
11
  from pathlib import Path
@@ -49,15 +49,6 @@ OPTIONAL_TOOLS = [
49
49
  "flash_trade_close_position",
50
50
  ]
51
51
 
52
- PAY_BRIDGE_PLUGIN_ID = "pay-bridge"
53
- PAY_BRIDGE_TOOLS = [
54
- "pay_status",
55
- "pay_wallet_info",
56
- "pay_search_services",
57
- "pay_get_service_endpoints",
58
- "pay_api_request",
59
- ]
60
-
61
52
  X402_TOOLS = [
62
53
  "x402_search_services",
63
54
  "x402_get_service_details",
@@ -104,13 +95,6 @@ def _default_extension_path() -> Path:
104
95
  return _repo_root() / ".openclaw" / "extensions" / "agent-wallet"
105
96
 
106
97
 
107
- def _default_pay_bridge_extension_path() -> Path:
108
- runtime_root = _trusted_runtime_root()
109
- if runtime_root is not None:
110
- return runtime_root / ".openclaw" / "extensions" / PAY_BRIDGE_PLUGIN_ID
111
- return _repo_root() / ".openclaw" / "extensions" / PAY_BRIDGE_PLUGIN_ID
112
-
113
-
114
98
  def _default_package_root() -> Path:
115
99
  runtime_root = _trusted_runtime_root()
116
100
  if runtime_root is not None:
@@ -133,14 +117,6 @@ def _default_python_bin() -> str:
133
117
  return sys.executable
134
118
 
135
119
 
136
- def _default_pay_binary() -> str:
137
- explicit = os.getenv("OPENCLAW_PAY_BINARY", "").strip()
138
- if explicit:
139
- return explicit
140
- resolved = shutil.which("pay")
141
- return resolved or "pay"
142
-
143
-
144
120
  def _default_user_id() -> str:
145
121
  return f"{os.getenv('USER', 'openclaw-user')}-local"
146
122
 
@@ -178,10 +154,8 @@ def build_parser() -> argparse.ArgumentParser:
178
154
  default=True,
179
155
  )
180
156
  parser.add_argument("--extension-path", default=str(_default_extension_path()))
181
- parser.add_argument("--pay-bridge-extension-path", default=str(_default_pay_bridge_extension_path()))
182
157
  parser.add_argument("--package-root", default=str(_default_package_root()))
183
158
  parser.add_argument("--python-bin", default=_default_python_bin())
184
- parser.add_argument("--pay-binary", default=_default_pay_binary())
185
159
  parser.add_argument("--write-master-key", action=argparse.BooleanOptionalAction, default=False)
186
160
  return parser
187
161
 
@@ -191,12 +165,15 @@ def _collect_sealed_secret_updates() -> dict[str, str]:
191
165
  master_key = os.getenv("AGENT_WALLET_MASTER_KEY", "").strip()
192
166
  approval_secret = os.getenv("AGENT_WALLET_APPROVAL_SECRET", "").strip()
193
167
  private_key = os.getenv("SOLANA_AGENT_PRIVATE_KEY", "").strip()
168
+ evm_wallet_password = os.getenv("WDK_EVM_WALLET_PASSWORD", "").strip()
194
169
  if master_key:
195
170
  updates["master_key"] = master_key
196
171
  if approval_secret:
197
172
  updates["approval_secret"] = approval_secret
198
173
  if private_key:
199
174
  updates["private_key"] = private_key
175
+ if evm_wallet_password:
176
+ updates["wdk_evm_wallet_password"] = evm_wallet_password
200
177
  return updates
201
178
 
202
179
 
@@ -205,10 +182,12 @@ def _maybe_install_sealed_keys() -> str | None:
205
182
  if not boot_key:
206
183
  return None
207
184
  updates = _collect_sealed_secret_updates()
208
- if not updates:
209
- return None
210
185
  sealed_path = resolve_sealed_keys_path()
211
186
  existing = unseal_keys(boot_key) if sealed_path.exists() else {}
187
+ if "wdk_evm_wallet_password" not in existing and "wdk_evm_wallet_password" not in updates:
188
+ updates["wdk_evm_wallet_password"] = secrets.token_urlsafe(24)
189
+ if not updates:
190
+ return None
212
191
  return str(seal_keys(boot_key, {**existing, **updates}))
213
192
 
214
193
 
@@ -255,17 +234,14 @@ def main() -> None:
255
234
  allow = plugins.setdefault("allow", [])
256
235
  if args.plugin_id not in allow:
257
236
  allow.append(args.plugin_id)
258
- if PAY_BRIDGE_PLUGIN_ID not in allow:
259
- allow.append(PAY_BRIDGE_PLUGIN_ID)
237
+ allow[:] = [item for item in allow if item != "pay-bridge"]
260
238
 
261
239
  load = plugins.setdefault("load", {})
262
240
  paths = load.setdefault("paths", [])
263
241
  extension_path_text = str(Path(args.extension_path).expanduser().resolve())
264
242
  if extension_path_text not in paths:
265
243
  paths.append(extension_path_text)
266
- pay_bridge_extension_path_text = str(Path(args.pay_bridge_extension_path).expanduser().resolve())
267
- if pay_bridge_extension_path_text not in paths:
268
- paths.append(pay_bridge_extension_path_text)
244
+ paths[:] = [item for item in paths if "extensions/pay-bridge" not in str(item)]
269
245
 
270
246
  entries = plugins.setdefault("entries", {})
271
247
  effective_network = _normalize_network(args.backend, args.network)
@@ -318,25 +294,19 @@ def main() -> None:
318
294
  "enabled": True,
319
295
  "config": plugin_config,
320
296
  }
321
- existing_pay_entry = entries.get(PAY_BRIDGE_PLUGIN_ID) if isinstance(entries.get(PAY_BRIDGE_PLUGIN_ID), dict) else {}
322
- existing_pay_config = (
323
- dict(existing_pay_entry.get("config"))
324
- if isinstance(existing_pay_entry.get("config"), dict)
325
- else {}
326
- )
327
- pay_bridge_config = {
328
- **existing_pay_config,
329
- "payBinary": args.pay_binary.strip() or _default_pay_binary(),
330
- "requireHttps": bool(existing_pay_config.get("requireHttps", True)),
331
- }
332
- entries[PAY_BRIDGE_PLUGIN_ID] = {
333
- "enabled": True,
334
- "config": pay_bridge_config,
335
- }
297
+ entries.pop("pay-bridge", None)
336
298
 
337
299
  tools = data.setdefault("tools", {})
338
300
  also_allow = tools.setdefault("alsoAllow", [])
339
- for tool_name in OPTIONAL_TOOLS + PAY_BRIDGE_TOOLS + X402_TOOLS:
301
+ removed_pay_tools = {
302
+ "pay_status",
303
+ "pay_wallet_info",
304
+ "pay_search_services",
305
+ "pay_get_service_endpoints",
306
+ "pay_api_request",
307
+ }
308
+ also_allow[:] = [tool_name for tool_name in also_allow if tool_name not in removed_pay_tools]
309
+ for tool_name in OPTIONAL_TOOLS + X402_TOOLS:
340
310
  if tool_name not in also_allow:
341
311
  also_allow.append(tool_name)
342
312
 
@@ -352,7 +322,6 @@ def main() -> None:
352
322
  "config_path": str(config_path),
353
323
  "backup_path": str(backup_path),
354
324
  "extension_path": extension_path_text,
355
- "pay_bridge_extension_path": pay_bridge_extension_path_text,
356
325
  "python_bin": args.python_bin,
357
326
  "package_root": plugin_config["packageRoot"],
358
327
  "plugin_id": args.plugin_id,
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import argparse
6
6
  import json
7
7
  import os
8
+ import secrets as py_secrets
8
9
  import sys
9
10
  from pathlib import Path
10
11
 
@@ -49,12 +50,15 @@ def _collect_secret_updates() -> dict[str, str]:
49
50
  master_key = os.getenv("AGENT_WALLET_MASTER_KEY", "").strip()
50
51
  approval_secret = os.getenv("AGENT_WALLET_APPROVAL_SECRET", "").strip()
51
52
  private_key = os.getenv("SOLANA_AGENT_PRIVATE_KEY", "").strip()
53
+ evm_wallet_password = os.getenv("WDK_EVM_WALLET_PASSWORD", "").strip()
52
54
  if master_key:
53
55
  updates["master_key"] = master_key
54
56
  if approval_secret:
55
57
  updates["approval_secret"] = approval_secret
56
58
  if private_key:
57
59
  updates["private_key"] = private_key
60
+ if evm_wallet_password:
61
+ updates["wdk_evm_wallet_password"] = evm_wallet_password
58
62
  return updates
59
63
 
60
64
 
@@ -80,6 +84,10 @@ def main() -> None:
80
84
  sealed_path = resolve_sealed_keys_path()
81
85
  existing = unseal_keys(boot_key) if sealed_path.exists() and not args.replace else {}
82
86
  secrets = {**existing, **updates}
87
+ generated_keys: list[str] = []
88
+ if "wdk_evm_wallet_password" not in secrets:
89
+ secrets["wdk_evm_wallet_password"] = py_secrets.token_urlsafe(24)
90
+ generated_keys.append("wdk_evm_wallet_password")
83
91
  if not secrets:
84
92
  raise SystemExit(
85
93
  "No secrets provided. Set AGENT_WALLET_MASTER_KEY, AGENT_WALLET_APPROVAL_SECRET, "
@@ -93,7 +101,7 @@ def main() -> None:
93
101
  "ok": True,
94
102
  "path": str(path),
95
103
  "stored_keys": sorted(secrets.keys()),
96
- "updated_keys": sorted(updates.keys()),
104
+ "updated_keys": sorted(set(updates.keys()) | set(generated_keys)),
97
105
  "replaced": bool(args.replace),
98
106
  },
99
107
  indent=2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentlayer.tech/wallet",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "NPM installer for the OpenClaw Agent Wallet local runtime.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -40,7 +40,6 @@
40
40
  "agent-wallet/pyproject.toml",
41
41
  ".openclaw/AGENTS.md",
42
42
  ".openclaw/extensions/agent-wallet/",
43
- ".openclaw/extensions/pay-bridge/",
44
43
  "hermes/plugins/agent_wallet/",
45
44
  "wdk-btc-wallet/src/",
46
45
  "wdk-btc-wallet/bootstrap.sh",
@@ -1,38 +0,0 @@
1
- # pay-bridge
2
-
3
- Thin OpenClaw bridge to the locally installed `pay` CLI.
4
-
5
- External install path:
6
-
7
- ```bash
8
- openclaw plugins install clawhub:@agentlayertech/pay-bridge-plugin
9
- ```
10
-
11
- This plugin is intentionally separate from `agent-wallet`:
12
-
13
- - `agent-wallet` remains the execution wallet stack for Solana/EVM/BTC
14
- - `pay-bridge` only discovers and calls paid APIs through `pay`
15
- - the `pay` wallet stays separate from the AgentLayer wallet runtime
16
-
17
- ## Exposed tools
18
-
19
- - `pay_status`
20
- - `pay_wallet_info`
21
- - `pay_search_services`
22
- - `pay_get_service_endpoints`
23
- - `pay_api_request`
24
-
25
- ## Intended workflow
26
-
27
- 1. `pay_status`
28
- 2. `pay_search_services`
29
- 3. `pay_get_service_endpoints`
30
- 4. `pay_api_request`
31
-
32
- `pay_api_request` is deliberately narrow:
33
-
34
- - it requires a `service_fqn`, `resource`, and `url`
35
- - it validates the URL against `pay skills endpoints`
36
- - it requires `purpose` and `user_confirmed=true`
37
-
38
- This keeps the bridge thin and prevents it from becoming a generic arbitrary paid-curl launcher.
@@ -1,287 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { promisify } from "node:util";
3
-
4
- const execFileAsync = promisify(execFile);
5
- const ANSI_RE = /\u001b\[[0-9;]*m/g;
6
-
7
- function stripAnsi(text) {
8
- return String(text || "").replace(ANSI_RE, "");
9
- }
10
-
11
- function nonEmptyLines(text) {
12
- return stripAnsi(text)
13
- .split(/\r?\n/)
14
- .map((line) => line.trim())
15
- .filter(Boolean);
16
- }
17
-
18
- function extractLastJsonValue(text) {
19
- const lines = nonEmptyLines(text);
20
- for (let i = lines.length - 1; i >= 0; i -= 1) {
21
- const candidate = lines[i];
22
- if (!candidate.startsWith("{") && !candidate.startsWith("[")) continue;
23
- try {
24
- return JSON.parse(candidate);
25
- } catch {
26
- continue;
27
- }
28
- }
29
- return null;
30
- }
31
-
32
- function collectStringLeaves(value, acc = new Set()) {
33
- if (typeof value === "string") {
34
- acc.add(value);
35
- return acc;
36
- }
37
- if (Array.isArray(value)) {
38
- for (const item of value) collectStringLeaves(item, acc);
39
- return acc;
40
- }
41
- if (value && typeof value === "object") {
42
- for (const item of Object.values(value)) collectStringLeaves(item, acc);
43
- }
44
- return acc;
45
- }
46
-
47
- function withAccountArgs(args, account) {
48
- if (!account) return args;
49
- return [...args, "--account", account];
50
- }
51
-
52
- export function resolvePayBinary(config = {}) {
53
- return (
54
- config.payBinary ||
55
- process.env.OPENCLAW_PAY_BINARY ||
56
- "pay"
57
- );
58
- }
59
-
60
- export async function runPayCommand(payBinary, args, options = {}) {
61
- const { cwd, input = null } = options;
62
- let stdout = "";
63
- let stderr = "";
64
- try {
65
- const result = await execFileAsync(payBinary, args, {
66
- cwd,
67
- env: { ...process.env },
68
- input,
69
- maxBuffer: 1024 * 1024 * 8,
70
- });
71
- stdout = result.stdout ?? "";
72
- stderr = result.stderr ?? "";
73
- } catch (error) {
74
- stdout = typeof error?.stdout === "string" ? error.stdout : "";
75
- stderr = typeof error?.stderr === "string" ? error.stderr : "";
76
- const payload = extractLastJsonValue(stdout) || extractLastJsonValue(stderr);
77
- const message =
78
- payload?.error?.message ||
79
- payload?.message ||
80
- stripAnsi(stderr || stdout || error?.message || "pay command failed").trim() ||
81
- "pay command failed";
82
- const wrapped = new Error(message);
83
- wrapped.stdout = stdout;
84
- wrapped.stderr = stderr;
85
- wrapped.details = payload && typeof payload === "object" ? payload : null;
86
- throw wrapped;
87
- }
88
- return { stdout, stderr };
89
- }
90
-
91
- export function parseWhoamiOutput(stdout) {
92
- const lines = nonEmptyLines(stdout);
93
- const systemUser = lines[0] || null;
94
- const noAccount = lines.some((line) => /no mainnet account/i.test(line));
95
- return {
96
- system_user: systemUser,
97
- has_mainnet_account: !noAccount,
98
- raw_lines: lines,
99
- };
100
- }
101
-
102
- export function parseAccountListOutput(stdout) {
103
- const lines = nonEmptyLines(stdout);
104
- const noAccounts = lines.some((line) => /no accounts found/i.test(line));
105
- return {
106
- has_accounts: !noAccounts,
107
- raw_lines: lines,
108
- };
109
- }
110
-
111
- export async function getPayStatus(config = {}, options = {}) {
112
- const payBinary = resolvePayBinary(config);
113
- const versionResult = await runPayCommand(payBinary, ["--version"], options);
114
- const whoamiResult = await runPayCommand(
115
- payBinary,
116
- withAccountArgs(["whoami"], config.defaultAccount),
117
- options
118
- );
119
- const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
120
- return {
121
- installed: true,
122
- pay_binary: payBinary,
123
- version: stripAnsi(versionResult.stdout).trim() || null,
124
- account_configured: parseWhoamiOutput(whoamiResult.stdout).has_mainnet_account,
125
- has_any_accounts: parseAccountListOutput(accountListResult.stdout).has_accounts,
126
- whoami: parseWhoamiOutput(whoamiResult.stdout),
127
- accounts: parseAccountListOutput(accountListResult.stdout),
128
- };
129
- }
130
-
131
- export async function getPayWalletInfo(config = {}, options = {}) {
132
- const payBinary = resolvePayBinary(config);
133
- const whoamiResult = await runPayCommand(
134
- payBinary,
135
- withAccountArgs(["whoami"], config.defaultAccount),
136
- options
137
- );
138
- const accountListResult = await runPayCommand(payBinary, ["account", "list"], options);
139
- return {
140
- pay_binary: payBinary,
141
- default_account: config.defaultAccount || null,
142
- whoami: parseWhoamiOutput(whoamiResult.stdout),
143
- accounts: parseAccountListOutput(accountListResult.stdout),
144
- notes: [
145
- "This wallet is managed by pay.sh and is separate from the AgentLayer execution wallet.",
146
- ],
147
- };
148
- }
149
-
150
- export async function searchPayServices(config = {}, params = {}, options = {}) {
151
- const payBinary = resolvePayBinary(config);
152
- const args = ["skills", "search"];
153
- if (params.query) args.push(String(params.query));
154
- if (params.category) args.push("--category", String(params.category));
155
- args.push("--json");
156
- const { stdout, stderr } = await runPayCommand(
157
- payBinary,
158
- withAccountArgs(args, params.account || config.defaultAccount),
159
- options
160
- );
161
- const parsed = JSON.parse(stdout.trim() || "{}");
162
- return {
163
- query: params.query || "",
164
- category: params.category || null,
165
- results: parsed,
166
- warnings: nonEmptyLines(stderr),
167
- };
168
- }
169
-
170
- export async function getPayServiceEndpoints(config = {}, params = {}, options = {}) {
171
- const payBinary = resolvePayBinary(config);
172
- const args = [
173
- "skills",
174
- "endpoints",
175
- String(params.service_fqn),
176
- String(params.resource),
177
- "--json",
178
- ];
179
- const { stdout, stderr } = await runPayCommand(
180
- payBinary,
181
- withAccountArgs(args, params.account || config.defaultAccount),
182
- options
183
- );
184
- const parsed = JSON.parse(stdout.trim() || "{}");
185
- return {
186
- service_fqn: String(params.service_fqn),
187
- resource: String(params.resource),
188
- endpoints: parsed,
189
- warnings: nonEmptyLines(stderr),
190
- };
191
- }
192
-
193
- function ensureHttps(url, requireHttps = true) {
194
- if (!requireHttps) return;
195
- const parsed = new URL(url);
196
- if (parsed.protocol !== "https:") {
197
- throw new Error("pay_api_request only allows https URLs.");
198
- }
199
- }
200
-
201
- function appendQuery(url, query) {
202
- const parsed = new URL(url);
203
- if (query && typeof query === "object") {
204
- for (const [key, value] of Object.entries(query)) {
205
- if (value === undefined || value === null) continue;
206
- parsed.searchParams.set(key, String(value));
207
- }
208
- }
209
- return parsed.toString();
210
- }
211
-
212
- export function endpointPayloadContainsUrl(endpointPayload, url) {
213
- const strings = collectStringLeaves(endpointPayload);
214
- return strings.has(url);
215
- }
216
-
217
- export async function executePayApiRequest(config = {}, params = {}, options = {}) {
218
- if (params.user_confirmed !== true) {
219
- throw new Error("pay_api_request requires user_confirmed=true.");
220
- }
221
- if (!params.purpose || !String(params.purpose).trim()) {
222
- throw new Error("pay_api_request requires a non-empty purpose.");
223
- }
224
- if (!params.service_fqn || !params.resource || !params.url) {
225
- throw new Error("pay_api_request requires service_fqn, resource, and url.");
226
- }
227
- if (params.json_body !== undefined && params.text_body !== undefined) {
228
- throw new Error("Provide either json_body or text_body, not both.");
229
- }
230
-
231
- const endpointData = await getPayServiceEndpoints(config, {
232
- account: params.account,
233
- resource: params.resource,
234
- service_fqn: params.service_fqn,
235
- }, options);
236
-
237
- const finalUrl = appendQuery(String(params.url), params.query);
238
- ensureHttps(finalUrl, config.requireHttps !== false);
239
- if (!endpointPayloadContainsUrl(endpointData.endpoints, String(params.url))) {
240
- throw new Error("The requested URL is not present in pay_get_service_endpoints for this service/resource.");
241
- }
242
-
243
- const payBinary = resolvePayBinary(config);
244
- const method = String(params.method || "GET").toUpperCase();
245
- const args = ["curl"];
246
- if (params.account || config.defaultAccount) {
247
- args.push("--account", String(params.account || config.defaultAccount));
248
- }
249
- args.push("--request", method);
250
-
251
- const headers = params.headers && typeof params.headers === "object" ? params.headers : {};
252
- for (const [key, value] of Object.entries(headers)) {
253
- args.push("--header", `${key}: ${String(value)}`);
254
- }
255
-
256
- if (params.json_body !== undefined) {
257
- const hasContentType = Object.keys(headers).some((key) => key.toLowerCase() === "content-type");
258
- if (!hasContentType) {
259
- args.push("--header", "content-type: application/json");
260
- }
261
- args.push("--data", JSON.stringify(params.json_body));
262
- } else if (params.text_body !== undefined) {
263
- args.push("--data", String(params.text_body));
264
- }
265
-
266
- args.push(finalUrl);
267
- const { stdout, stderr } = await runPayCommand(payBinary, args, options);
268
- const trimmed = stdout.trim();
269
- let responseBody = trimmed;
270
- if (params.parse_json_response !== false) {
271
- try {
272
- responseBody = JSON.parse(trimmed);
273
- } catch {
274
- responseBody = trimmed;
275
- }
276
- }
277
- return {
278
- method,
279
- purpose: String(params.purpose),
280
- request_url: finalUrl,
281
- service_fqn: String(params.service_fqn),
282
- resource: String(params.resource),
283
- response: responseBody,
284
- raw_response_text: trimmed,
285
- warnings: nonEmptyLines(stderr),
286
- };
287
- }