@paybond/kit 0.9.4 → 0.9.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/README.md CHANGED
@@ -28,17 +28,20 @@ Create a sandbox key for local development:
28
28
  npx -p @paybond/kit paybond login
29
29
  ```
30
30
 
31
- `paybond login` writes `PAYBOND_API_KEY` to `.env.local` with file mode `0600`, refuses to overwrite an existing key unless `--force` is passed, and refuses env files that are not ignored by git. Live production keys are created by tenant admins in Console and stored in deployment secret managers.
31
+ `paybond login` writes a sandbox `PAYBOND_API_KEY` to `.env.local` with file mode `0600`, adds the default `.env.local` target to `.gitignore` when needed, and refuses to overwrite an existing key unless `--force` is passed. Custom env-file paths inside a git repo must already be ignored. Live production keys are created by tenant admins in Console and stored in deployment secret managers.
32
32
 
33
33
  ## First guardrail scaffold
34
34
 
35
35
  Use this first when you have a paid tool and want Paybond guardrails in the sandbox:
36
36
 
37
37
  ```bash
38
- npx -p @paybond/kit paybond-init --preset paid-tool-guard --framework provider-agnostic --out paybond-guardrail-demo.ts
38
+ npx -p @paybond/kit paybond-init \
39
+ --preset paid-tool-guard \
40
+ --framework provider-agnostic \
41
+ --out paybond-paid-tool-guard.ts
39
42
  ```
40
43
 
41
- The generated demo opens Paybond, bootstraps a sandbox guardrail intent, wraps one replaceable paid-tool handler, submits sandbox evidence, and prints the lifecycle result. Free Developer is sandbox-only; live settlement rails start on paid production plans.
44
+ The generated integration opens Paybond from the environment, loads `.env.local` when `PAYBOND_API_KEY` is not already present, bootstraps a sandbox guardrail intent, wraps your paid-tool handler, and submits sandbox evidence. It does not generate a paid-tool implementation. Free Developer is sandbox-only; live settlement rails start on paid production plans.
42
45
 
43
46
  ## Tenant isolation
44
47
 
@@ -100,7 +103,7 @@ const guardedTool = guard.guardTool(
100
103
  async (input) => bookHotel(input),
101
104
  );
102
105
 
103
- const result = await guardedTool({ hotelId: "hotel_demo", maxPriceCents: 20_000 });
106
+ const result = await guardedTool({ hotelId: "hotel_123", maxPriceCents: 20_000 });
104
107
  await paybond.guardrails.submitSandboxEvidence({
105
108
  intentId: guardrail.intent_id,
106
109
  payload: { result, sandbox: true },
@@ -112,7 +115,10 @@ The `paybond.harbor` and `paybond.guardrails` clients are created by `Paybond.op
112
115
  Scaffold a guardrail integration:
113
116
 
114
117
  ```bash
115
- npx -p @paybond/kit paybond-init --preset paid-tool-guard --framework provider-agnostic --out paybond-guardrail-demo.ts
118
+ npx -p @paybond/kit paybond-init \
119
+ --preset paid-tool-guard \
120
+ --framework provider-agnostic \
121
+ --out paybond-paid-tool-guard.ts
116
122
  ```
117
123
 
118
124
  ## What the package includes
@@ -134,7 +140,7 @@ Gateway and trust helpers:
134
140
  - Protocol-v2 helpers for mandate verification, replay-safe recognition proof verification, receipt reads, and A2A discovery
135
141
  - `paybond login` for sandbox device approval and local `.env.local` API-key setup
136
142
  - `paybond-mcp-server` for tenant-bound MCP tool exposure to any MCP-compatible host
137
- - `paybond-init` for generating a Paybond guardrail integration with a sandbox smoke path
143
+ - `paybond-init` for generating a Paybond guardrail integration helper
138
144
 
139
145
  Agent-facing surfaces are model-provider agnostic. Paybond verifies tool operations and tenant scope, not whether a tool call came from OpenAI, Anthropic, Gemini, a local model, or another runtime.
140
146
 
package/dist/init.js CHANGED
@@ -26,15 +26,15 @@ const FRAMEWORK_NOTES = {
26
26
  };
27
27
  function usage() {
28
28
  return [
29
- "Usage: paybond-init [--preset paid-tool-guard] [--framework generic|provider-agnostic|openai|claude|anthropic|gemini|google-ai|vercel-ai|langgraph|mcp] [--out paybond-guardrail-demo.ts] [--force]",
29
+ "Usage: paybond-init [--preset paid-tool-guard] [--framework generic|provider-agnostic|openai|claude|anthropic|gemini|google-ai|vercel-ai|langgraph|mcp] [--out paybond-paid-tool-guard.ts] [--force]",
30
30
  "",
31
- "Scaffolds a production-shaped Paybond guardrail integration with a sandbox smoke path.",
31
+ "Scaffolds a production-shaped Paybond guardrail integration helper.",
32
32
  ].join("\n");
33
33
  }
34
34
  function parseArgs(argv) {
35
35
  let preset = "paid-tool-guard";
36
36
  let framework = "provider-agnostic";
37
- let out = "paybond-guardrail-demo.ts";
37
+ let out = "paybond-paid-tool-guard.ts";
38
38
  let force = false;
39
39
  for (let i = 0; i < argv.length; i += 1) {
40
40
  const arg = argv[i];
@@ -85,7 +85,13 @@ function template(framework) {
85
85
  type SandboxGuardrailEvidenceResult,
86
86
  } from "@paybond/kit";
87
87
 
88
- const DEFAULT_OPERATION = "paid_tool.smoke_test";
88
+ declare const process: {
89
+ env: Record<string, string | undefined>;
90
+ };
91
+
92
+ // Production integration helpers only. Add your paid-tool handler in
93
+ // application code and pass it to wrapPaidTool(...).
94
+ const DEFAULT_OPERATION = "paid_tool.operation";
89
95
  const DEFAULT_REQUESTED_SPEND_CENTS = 500;
90
96
 
91
97
  export type PaidToolHandler<TInput, TResult> = (input: TInput) => TResult | Promise<TResult>;
@@ -107,22 +113,59 @@ export type SubmitSandboxEvidenceOptions = {
107
113
  idempotencyKey?: string;
108
114
  };
109
115
 
110
- export type SmokePaidToolInput = {
111
- itemId: string;
112
- maxPriceCents: number;
116
+ export type OpenPaybondFromEnvOptions = {
117
+ /**
118
+ * Load PAYBOND_API_KEY from this local env file when the process environment
119
+ * does not already provide it. Pass false when your agent host injects secrets.
120
+ */
121
+ envFile?: string | false;
113
122
  };
114
123
 
115
- export type SmokePaidToolResult = {
116
- confirmationId: string;
117
- itemId: string;
118
- chargedCents: number;
119
- sandbox: true;
120
- };
124
+ function readEnvValue(body: string, key: string): string | undefined {
125
+ const pattern = new RegExp("^\\\\s*(?:export\\\\s+)?" + key + "\\\\s*=\\\\s*(.*)$", "m");
126
+ const match = body.match(pattern);
127
+ if (!match) return undefined;
128
+ let value = (match[1] ?? "").trim();
129
+ if (value.startsWith('"') && value.endsWith('"')) {
130
+ try {
131
+ value = JSON.parse(value);
132
+ } catch {
133
+ value = value.slice(1, -1);
134
+ }
135
+ } else if (value.startsWith("'") && value.endsWith("'")) {
136
+ value = value.slice(1, -1);
137
+ }
138
+ return value.trim() || undefined;
139
+ }
140
+
141
+ async function readTextFile(envFile: string): Promise<string | undefined> {
142
+ // @ts-ignore Node builtins are available in agent and CLI Node runtimes.
143
+ const fs: { readFile(path: string, encoding: "utf8"): Promise<string> } = await import("node:fs/promises");
144
+ try {
145
+ return await fs.readFile(envFile, "utf8");
146
+ } catch (err) {
147
+ if ((err as { code?: unknown })?.code === "ENOENT") return undefined;
148
+ throw err;
149
+ }
150
+ }
121
151
 
122
- export async function openPaybondFromEnv(): Promise<Paybond> {
152
+ export async function loadPaybondEnvFile(envFile = ".env.local"): Promise<void> {
153
+ if (process.env.PAYBOND_API_KEY?.trim()) return;
154
+ const body = await readTextFile(envFile);
155
+ if (body === undefined) return;
156
+ const apiKey = readEnvValue(body, "PAYBOND_API_KEY");
157
+ if (apiKey) {
158
+ process.env.PAYBOND_API_KEY = apiKey;
159
+ }
160
+ }
161
+
162
+ export async function openPaybondFromEnv(options: OpenPaybondFromEnvOptions = {}): Promise<Paybond> {
163
+ if (options.envFile !== false) {
164
+ await loadPaybondEnvFile(options.envFile ?? ".env.local");
165
+ }
123
166
  const apiKey = process.env.PAYBOND_API_KEY?.trim();
124
167
  if (!apiKey) {
125
- throw new Error("PAYBOND_API_KEY is required");
168
+ throw new Error("PAYBOND_API_KEY is required; run paybond login or configure your agent host to pass it");
126
169
  }
127
170
 
128
171
  return Paybond.open({
@@ -193,56 +236,6 @@ export async function submitSandboxEvidence(
193
236
  idempotencyKey: options.idempotencyKey,
194
237
  });
195
238
  }
196
-
197
- export async function replaceableSmokeTestPaidTool(
198
- input: SmokePaidToolInput,
199
- ): Promise<SmokePaidToolResult> {
200
- // Replace this sandbox smoke-test function with the real paid side-effecting tool.
201
- return {
202
- confirmationId: "sandbox-confirmation-" + input.itemId,
203
- itemId: input.itemId,
204
- chargedCents: Math.min(input.maxPriceCents, DEFAULT_REQUESTED_SPEND_CENTS),
205
- sandbox: true,
206
- };
207
- }
208
-
209
- export async function runSandboxSmokePath(): Promise<{
210
- guardrail: SandboxGuardrailBootstrapResult;
211
- toolResult: SmokePaidToolResult;
212
- evidence: SandboxGuardrailEvidenceResult;
213
- }> {
214
- const paybond = await openPaybondFromEnv();
215
- const guardrail = await bootstrapSandboxGuardrailIntent(paybond);
216
- const guardedTool = wrapPaidTool(paybond, guardrail, replaceableSmokeTestPaidTool);
217
- const toolResult = await guardedTool({
218
- itemId: "replace-with-your-tool-input",
219
- maxPriceCents: DEFAULT_REQUESTED_SPEND_CENTS,
220
- });
221
- const evidence = await submitSandboxEvidence(paybond, guardrail, {
222
- confirmation_id: toolResult.confirmationId,
223
- charged_cents: toolResult.chargedCents,
224
- item_id: toolResult.itemId,
225
- sandbox: toolResult.sandbox,
226
- });
227
- return { guardrail, toolResult, evidence };
228
- }
229
-
230
- async function main(): Promise<void> {
231
- const result = await runSandboxSmokePath();
232
- console.log(JSON.stringify(result, null, 2));
233
- }
234
-
235
- function normalizeFileURL(url: string): string {
236
- return url.startsWith("file:///var/") ? url.replace("file:///var/", "file:///private/var/") : url;
237
- }
238
-
239
- const invokedPath = process.argv[1] ? normalizeFileURL(new URL("file://" + process.argv[1]).href) : "";
240
- if (invokedPath && normalizeFileURL(import.meta.url) === invokedPath) {
241
- main().catch((err) => {
242
- console.error(err instanceof Error ? err.message : String(err));
243
- process.exitCode = 1;
244
- });
245
- }
246
239
  `;
247
240
  }
248
241
  async function writeScaffold(out, body, force) {
@@ -288,15 +281,41 @@ export async function main(argv = process.argv.slice(2)) {
288
281
  function normalizeFileURL(url) {
289
282
  return url.startsWith("file:///var/") ? url.replace("file:///var/", "file:///private/var/") : url;
290
283
  }
291
- function invokedFromCLI() {
292
- const invokedPath = process.argv[1] ? normalizeFileURL(new URL("file://" + process.argv[1]).href) : "";
293
- return Boolean(invokedPath && normalizeFileURL(import.meta.url) === invokedPath);
284
+ async function invokedFromCLI() {
285
+ const scriptPath = process.argv[1];
286
+ if (!scriptPath) {
287
+ return false;
288
+ }
289
+ // @ts-ignore Node builtins are available in the published CLI runtime.
290
+ const fs = (await import("node:fs/promises"));
291
+ // @ts-ignore Node builtins are available in the published CLI runtime.
292
+ const path = (await import("node:path"));
293
+ // @ts-ignore Node builtins are available in the published CLI runtime.
294
+ const url = (await import("node:url"));
295
+ async function realFileURL(filePath) {
296
+ let resolved = path.resolve(filePath);
297
+ try {
298
+ resolved = await fs.realpath(resolved);
299
+ }
300
+ catch {
301
+ // If realpath fails, compare the absolute path. This keeps direct execution
302
+ // working even when the script path disappears during process startup.
303
+ }
304
+ return normalizeFileURL(url.pathToFileURL(resolved).href);
305
+ }
306
+ return (await realFileURL(scriptPath)) === (await realFileURL(url.fileURLToPath(import.meta.url)));
294
307
  }
295
- if (invokedFromCLI()) {
308
+ invokedFromCLI().then((invoked) => {
309
+ if (!invoked) {
310
+ return;
311
+ }
296
312
  main().then((code) => {
297
313
  process.exitCode = code;
298
314
  }, (err) => {
299
315
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
300
316
  process.exitCode = 1;
301
317
  });
302
- }
318
+ }, (err) => {
319
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
320
+ process.exitCode = 1;
321
+ });
package/dist/login.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  type Writable = {
3
3
  write(chunk: string): boolean;
4
4
  };
5
- export type DeviceEnvironment = "sandbox" | "live";
5
+ export type DeviceEnvironment = "sandbox";
6
6
  export type LoginOptions = {
7
7
  envFile: string;
8
8
  gateway: string;
package/dist/login.js CHANGED
@@ -23,19 +23,22 @@ class OAuthPollError extends PaybondLoginError {
23
23
  }
24
24
  function usage() {
25
25
  return [
26
- "Usage: paybond login [--env sandbox|live] [--env-file .env.local] [--gateway https://api.paybond.ai] [--no-open] [--force]",
26
+ "Usage: paybond login [--env sandbox] [--env-file .env.local] [--gateway https://api.paybond.ai] [--no-open] [--force]",
27
27
  "",
28
- "Starts a device login and writes PAYBOND_API_KEY to an ignored env file.",
29
- "Defaults to the sandbox environment. Pass --env live to mint a production operator key;",
30
- "a tenant admin must approve it from an active live console session.",
28
+ "Starts a device login and writes PAYBOND_API_KEY to a local env file.",
29
+ "The default .env.local target is added to .gitignore when needed.",
30
+ "Defaults to the sandbox environment. Production keys are created in Console and stored in secret managers.",
31
31
  ].join("\n");
32
32
  }
33
33
  function parseEnvironment(raw) {
34
34
  const value = raw.trim().toLowerCase();
35
- if (value === "sandbox" || value === "live") {
35
+ if (value === "sandbox") {
36
36
  return value;
37
37
  }
38
- throw new PaybondLoginError("invalid --env (expected sandbox or live)");
38
+ if (value === "live") {
39
+ throw new PaybondLoginError("live device login is not supported; create production keys in Console and store them in a secret manager");
40
+ }
41
+ throw new PaybondLoginError("invalid --env (expected sandbox)");
39
42
  }
40
43
  export function parseArgs(argv) {
41
44
  if (argv.length === 0) {
@@ -67,13 +70,12 @@ export function parseArgs(argv) {
67
70
  continue;
68
71
  }
69
72
  if (arg === "--live") {
70
- environment = "live";
71
- continue;
73
+ throw new PaybondLoginError("live device login is not supported; create production keys in Console and store them in a secret manager");
72
74
  }
73
75
  if (arg === "--env" || arg.startsWith("--env=")) {
74
76
  const raw = arg === "--env" ? rest[++i] : arg.slice("--env=".length);
75
77
  if (!raw || raw.startsWith("-")) {
76
- throw new PaybondLoginError("invalid --env (expected sandbox or live)");
78
+ throw new PaybondLoginError("invalid --env (expected sandbox)");
77
79
  }
78
80
  environment = parseEnvironment(raw);
79
81
  continue;
@@ -162,6 +164,9 @@ async function resolveEnvFile(envFile, cwd) {
162
164
  return path.isAbsolute(envFile) ? path.resolve(envFile) : path.resolve(cwd, envFile);
163
165
  }
164
166
  export async function assertGitIgnored(envPath, cwd) {
167
+ await ensureGitIgnored(envPath, cwd, false);
168
+ }
169
+ async function ensureGitIgnored(envPath, cwd, autoAddDefaultEnvFile) {
165
170
  const path = await pathModule();
166
171
  const fs = await fsModule();
167
172
  const rootResult = await spawnCommand("git", ["rev-parse", "--show-toplevel"], cwd);
@@ -180,6 +185,24 @@ export async function assertGitIgnored(envPath, cwd) {
180
185
  return;
181
186
  }
182
187
  if (ignoreResult.code === 1) {
188
+ if (autoAddDefaultEnvFile && relativeTarget === DEFAULT_ENV_FILE) {
189
+ const gitignorePath = path.resolve(repoRoot, ".gitignore");
190
+ let existing = "";
191
+ try {
192
+ existing = await fs.readFile(gitignorePath, "utf8");
193
+ }
194
+ catch (err) {
195
+ if (!(err && typeof err === "object" && "code" in err && err.code === "ENOENT")) {
196
+ throw err;
197
+ }
198
+ }
199
+ const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
200
+ await fs.writeFile(gitignorePath, `${existing}${suffix}${DEFAULT_ENV_FILE}\n`, { encoding: "utf8", mode: 0o644 });
201
+ const recheck = await spawnCommand("git", ["-C", repoRoot, "check-ignore", "--quiet", "--", relativeTarget], cwd);
202
+ if (recheck.code === 0) {
203
+ return;
204
+ }
205
+ }
183
206
  throw new PaybondLoginError(`Refusing to write ${target} because it is not ignored by git. Add ${relativeTarget} to .gitignore or pass --env-file pointing outside the repo.`);
184
207
  }
185
208
  throw new PaybondLoginError(`Unable to verify git ignore status for ${target}: ${ignoreResult.stderr.trim() || "git check-ignore failed"}`);
@@ -376,14 +399,10 @@ export async function runLogin(options, deps = {}) {
376
399
  const openBrowser = deps.openBrowser ?? defaultOpenBrowser;
377
400
  const envPath = await resolveEnvFile(options.envFile, cwd);
378
401
  await assertCanWriteEnvFile(envPath, options.force);
379
- await assertGitIgnored(envPath, cwd);
402
+ await ensureGitIgnored(envPath, cwd, options.envFile === DEFAULT_ENV_FILE);
380
403
  const start = await startDeviceFlow(fetchFn, options.gateway, options.environment);
381
404
  const verificationUrl = start.verification_uri_complete || start.verification_uri;
382
405
  stdout.write(`Paybond ${options.environment} login\n`);
383
- if (options.environment === "live") {
384
- stdout.write(`WARNING: this mints a PRODUCTION operator API key with access to live tenant data and money movement.\n`);
385
- stdout.write(`A tenant admin must approve it from an active live console session and confirm the live environment.\n`);
386
- }
387
406
  stdout.write(`Verification URL: ${verificationUrl}\n`);
388
407
  stdout.write(`Code: ${start.user_code}\n`);
389
408
  if (!options.noOpen) {
@@ -421,15 +440,41 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
421
440
  function normalizeFileURL(url) {
422
441
  return url.startsWith("file:///var/") ? url.replace("file:///var/", "file:///private/var/") : url;
423
442
  }
424
- function invokedFromCLI() {
425
- const invokedPath = process.argv[1] ? normalizeFileURL(new URL("file://" + process.argv[1]).href) : "";
426
- return Boolean(invokedPath && normalizeFileURL(import.meta.url) === invokedPath);
443
+ async function invokedFromCLI() {
444
+ const scriptPath = process.argv[1];
445
+ if (!scriptPath) {
446
+ return false;
447
+ }
448
+ // @ts-ignore Node builtins are available in the published CLI runtime.
449
+ const fs = (await import("node:fs/promises"));
450
+ // @ts-ignore Node builtins are available in the published CLI runtime.
451
+ const path = (await import("node:path"));
452
+ // @ts-ignore Node builtins are available in the published CLI runtime.
453
+ const url = (await import("node:url"));
454
+ async function realFileURL(filePath) {
455
+ let resolved = path.resolve(filePath);
456
+ try {
457
+ resolved = await fs.realpath(resolved);
458
+ }
459
+ catch {
460
+ // If realpath fails, compare the absolute path. This keeps direct execution
461
+ // working even when the script path disappears during process startup.
462
+ }
463
+ return normalizeFileURL(url.pathToFileURL(resolved).href);
464
+ }
465
+ return (await realFileURL(scriptPath)) === (await realFileURL(url.fileURLToPath(import.meta.url)));
427
466
  }
428
- if (invokedFromCLI()) {
467
+ invokedFromCLI().then((invoked) => {
468
+ if (!invoked) {
469
+ return;
470
+ }
429
471
  main().then((code) => {
430
472
  process.exitCode = code;
431
473
  }, (err) => {
432
474
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
433
475
  process.exitCode = 1;
434
476
  });
435
- }
477
+ }, (err) => {
478
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
479
+ process.exitCode = 1;
480
+ });
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ // @ts-ignore Node builtins are available in the published CLI runtime.
3
+ import { readFileSync } from "node:fs";
2
4
  import { GatewayAuthError, GatewayFraudClient, GatewaySignalClient, SignalHttpError, DEFAULT_PAYBOND_GATEWAY_BASE_URL, } from "./index.js";
3
5
  const SERVER_NAME = "Paybond MCP";
4
6
  const SERVER_VERSION = "0.6.0";
@@ -6,6 +8,35 @@ const MCP_PROTOCOL_VERSION = "2025-11-25";
6
8
  const DEFAULT_PRINCIPAL_PATH = "/v1/auth/principal";
7
9
  const DEFAULT_RECOGNITION_VERIFIER_ID = "paybond-gateway";
8
10
  const agentRecognitionProofHeader = "x-paybond-agent-recognition-proof";
11
+ const DEFAULT_ENV_FILE = ".env.local";
12
+ function readEnvFileValue(envFile, key) {
13
+ let body;
14
+ try {
15
+ body = readFileSync(envFile, "utf8");
16
+ }
17
+ catch (err) {
18
+ if (err?.code === "ENOENT")
19
+ return undefined;
20
+ throw err;
21
+ }
22
+ const pattern = new RegExp("^\\s*(?:export\\s+)?" + key + "\\s*=\\s*(.*)$", "m");
23
+ const match = body.match(pattern);
24
+ if (!match)
25
+ return undefined;
26
+ let value = String(match[1] ?? "").trim();
27
+ if (value.startsWith('"') && value.endsWith('"')) {
28
+ try {
29
+ value = JSON.parse(value);
30
+ }
31
+ catch {
32
+ value = value.slice(1, -1);
33
+ }
34
+ }
35
+ else if (value.startsWith("'") && value.endsWith("'")) {
36
+ value = value.slice(1, -1);
37
+ }
38
+ return value.trim() || undefined;
39
+ }
9
40
  class GatewayHTTPError extends Error {
10
41
  statusCode;
11
42
  url;
@@ -845,12 +876,13 @@ export class PaybondMCPServer {
845
876
  }
846
877
  }
847
878
  export function settingsFromEnv(env = process.env) {
848
- const apiKey = String(env.PAYBOND_API_KEY ?? "").trim();
879
+ const envFile = optionalEnv(env.PAYBOND_ENV_FILE) ?? DEFAULT_ENV_FILE;
880
+ const apiKey = String(env.PAYBOND_API_KEY ?? readEnvFileValue(envFile, "PAYBOND_API_KEY") ?? "").trim();
849
881
  if (!apiKey) {
850
- throw new Error("PAYBOND_API_KEY is required");
882
+ throw new Error("PAYBOND_API_KEY is required; run paybond login or configure your MCP host environment");
851
883
  }
852
884
  return {
853
- gatewayBaseUrl: DEFAULT_PAYBOND_GATEWAY_BASE_URL,
885
+ gatewayBaseUrl: optionalEnv(env.PAYBOND_GATEWAY_BASE_URL) ?? DEFAULT_PAYBOND_GATEWAY_BASE_URL,
854
886
  apiKey,
855
887
  principalPath: optionalEnv(env.PAYBOND_PRINCIPAL_PATH) ?? DEFAULT_PRINCIPAL_PATH,
856
888
  maxRetries: optionalEnv(env.PAYBOND_MCP_MAX_RETRIES)
@@ -1017,18 +1049,35 @@ function formatError(err) {
1017
1049
  function normalizeFileURL(url) {
1018
1050
  return url.startsWith("file:///var/") ? url.replace("file:///var/", "file:///private/var/") : url;
1019
1051
  }
1020
- const isMainModule = (() => {
1052
+ async function invokedFromCLI() {
1021
1053
  const scriptPath = process.argv[1];
1022
1054
  if (!scriptPath) {
1023
1055
  return false;
1024
1056
  }
1025
- try {
1026
- return normalizeFileURL(import.meta.url) === normalizeFileURL(new URL("file://" + scriptPath).href);
1057
+ // @ts-ignore Node builtins are available in the published CLI runtime.
1058
+ const fs = (await import("node:fs/promises"));
1059
+ // @ts-ignore Node builtins are available in the published CLI runtime.
1060
+ const path = (await import("node:path"));
1061
+ // @ts-ignore Node builtins are available in the published CLI runtime.
1062
+ const url = (await import("node:url"));
1063
+ async function realFileURL(filePath) {
1064
+ let resolved = path.resolve(filePath);
1065
+ try {
1066
+ resolved = await fs.realpath(resolved);
1067
+ }
1068
+ catch {
1069
+ // If realpath fails, compare the absolute path. This keeps direct execution
1070
+ // working even when the script path disappears during process startup.
1071
+ }
1072
+ return normalizeFileURL(url.pathToFileURL(resolved).href);
1027
1073
  }
1028
- catch {
1029
- return import.meta.url.endsWith(scriptPath);
1074
+ return (await realFileURL(scriptPath)) === (await realFileURL(url.fileURLToPath(import.meta.url)));
1075
+ }
1076
+ invokedFromCLI().then((invoked) => {
1077
+ if (invoked && main() !== 0) {
1078
+ process.exitCode = 1;
1030
1079
  }
1031
- })();
1032
- if (isMainModule && main() !== 0) {
1080
+ }, (err) => {
1081
+ process.stderr.write(`${formatError(err)}\n`);
1033
1082
  process.exitCode = 1;
1034
- }
1083
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paybond/kit",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Paybond Kit for TypeScript: agent spend governance for paid tool calls with spend authorization, evidence receipts, refunds, disputes, hosted Gateway sessions, and settlement.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",