@paybond/kit 0.9.4 → 0.9.5

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
+ }
121
140
 
122
- export async function openPaybondFromEnv(): Promise<Paybond> {
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
+ }
151
+
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) {
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) {
@@ -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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paybond/kit",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
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",