@paybond/kit 0.9.3 → 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/dist/login.js ADDED
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env node
2
+ const DEFAULT_GATEWAY = "https://api.paybond.ai";
3
+ const DEFAULT_ENV_FILE = ".env.local";
4
+ const CLIENT_ID = "paybond-kit-cli";
5
+ const CLIENT_NAME = "Paybond CLI";
6
+ const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
7
+ const MIN_POLL_INTERVAL_SECONDS = 1;
8
+ class PaybondLoginError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "PaybondLoginError";
12
+ }
13
+ }
14
+ class OAuthPollError extends PaybondLoginError {
15
+ code;
16
+ interval;
17
+ constructor(body) {
18
+ super(body.error_description?.trim() || body.error);
19
+ this.name = "OAuthPollError";
20
+ this.code = body.error;
21
+ this.interval = body.interval;
22
+ }
23
+ }
24
+ function usage() {
25
+ return [
26
+ "Usage: paybond login [--env sandbox] [--env-file .env.local] [--gateway https://api.paybond.ai] [--no-open] [--force]",
27
+ "",
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
+ ].join("\n");
32
+ }
33
+ function parseEnvironment(raw) {
34
+ const value = raw.trim().toLowerCase();
35
+ if (value === "sandbox") {
36
+ return value;
37
+ }
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)");
42
+ }
43
+ export function parseArgs(argv) {
44
+ if (argv.length === 0) {
45
+ throw new PaybondLoginError(`missing command: login\n\n${usage()}`);
46
+ }
47
+ const [command, ...rest] = argv;
48
+ if (command === "--help" || command === "-h") {
49
+ return "help";
50
+ }
51
+ if (command !== "login") {
52
+ throw new PaybondLoginError(`unknown command: ${command}\n\n${usage()}`);
53
+ }
54
+ let envFile = DEFAULT_ENV_FILE;
55
+ let gateway = DEFAULT_GATEWAY;
56
+ let environment = "sandbox";
57
+ let noOpen = false;
58
+ let force = false;
59
+ for (let i = 0; i < rest.length; i += 1) {
60
+ const arg = rest[i];
61
+ if (arg === "--help" || arg === "-h") {
62
+ return "help";
63
+ }
64
+ if (arg === "--no-open") {
65
+ noOpen = true;
66
+ continue;
67
+ }
68
+ if (arg === "--force") {
69
+ force = true;
70
+ continue;
71
+ }
72
+ if (arg === "--live") {
73
+ throw new PaybondLoginError("live device login is not supported; create production keys in Console and store them in a secret manager");
74
+ }
75
+ if (arg === "--env" || arg.startsWith("--env=")) {
76
+ const raw = arg === "--env" ? rest[++i] : arg.slice("--env=".length);
77
+ if (!raw || raw.startsWith("-")) {
78
+ throw new PaybondLoginError("invalid --env (expected sandbox)");
79
+ }
80
+ environment = parseEnvironment(raw);
81
+ continue;
82
+ }
83
+ if (arg === "--env-file" || arg.startsWith("--env-file=")) {
84
+ const raw = arg === "--env-file" ? rest[++i] : arg.slice("--env-file=".length);
85
+ if (!raw || raw.startsWith("-")) {
86
+ throw new PaybondLoginError("invalid --env-file");
87
+ }
88
+ envFile = raw;
89
+ continue;
90
+ }
91
+ if (arg === "--gateway" || arg.startsWith("--gateway=")) {
92
+ const raw = arg === "--gateway" ? rest[++i] : arg.slice("--gateway=".length);
93
+ if (!raw || raw.startsWith("-")) {
94
+ throw new PaybondLoginError("invalid --gateway");
95
+ }
96
+ gateway = raw;
97
+ continue;
98
+ }
99
+ throw new PaybondLoginError(`unknown argument: ${arg}`);
100
+ }
101
+ if (!envFile.trim()) {
102
+ throw new PaybondLoginError("invalid --env-file");
103
+ }
104
+ if (!gateway.trim()) {
105
+ throw new PaybondLoginError("invalid --gateway");
106
+ }
107
+ return { envFile, gateway, environment, noOpen, force };
108
+ }
109
+ function envKeyPattern() {
110
+ return /^\s*(?:export\s+)?PAYBOND_API_KEY\s*=/m;
111
+ }
112
+ function quoteEnvValue(value) {
113
+ if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) {
114
+ return value;
115
+ }
116
+ return JSON.stringify(value);
117
+ }
118
+ function replaceOrAppendEnvValue(existing, rawKey, force) {
119
+ const line = `PAYBOND_API_KEY=${quoteEnvValue(rawKey)}`;
120
+ const pattern = /^(\s*(?:export\s+)?PAYBOND_API_KEY\s*=).*$/m;
121
+ if (pattern.test(existing)) {
122
+ if (!force) {
123
+ throw new PaybondLoginError("PAYBOND_API_KEY already exists in the target env file; pass --force to replace it.");
124
+ }
125
+ return existing.replace(pattern, line);
126
+ }
127
+ const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
128
+ return `${existing}${suffix}${line}\n`;
129
+ }
130
+ async function pathModule() {
131
+ // @ts-expect-error Node builtins are available in the published CLI runtime.
132
+ return await import("node:path");
133
+ }
134
+ async function fsModule() {
135
+ // @ts-expect-error Node builtins are available in the published CLI runtime.
136
+ return await import("node:fs/promises");
137
+ }
138
+ async function spawnCommand(command, args, cwd) {
139
+ // @ts-expect-error Node builtins are available in the published CLI runtime.
140
+ const childProcess = await import("node:child_process");
141
+ return await new Promise((resolve) => {
142
+ const child = childProcess.spawn(command, args, {
143
+ cwd,
144
+ stdio: ["ignore", "pipe", "pipe"],
145
+ });
146
+ let stdout = "";
147
+ let stderr = "";
148
+ child.stdout?.on("data", (chunk) => {
149
+ stdout += String(chunk);
150
+ });
151
+ child.stderr?.on("data", (chunk) => {
152
+ stderr += String(chunk);
153
+ });
154
+ child.on("error", (err) => {
155
+ resolve({ code: 127, stdout: "", stderr: err.message });
156
+ });
157
+ child.on("close", (code) => {
158
+ resolve({ code, stdout, stderr });
159
+ });
160
+ });
161
+ }
162
+ async function resolveEnvFile(envFile, cwd) {
163
+ const path = await pathModule();
164
+ return path.isAbsolute(envFile) ? path.resolve(envFile) : path.resolve(cwd, envFile);
165
+ }
166
+ export async function assertGitIgnored(envPath, cwd) {
167
+ await ensureGitIgnored(envPath, cwd, false);
168
+ }
169
+ async function ensureGitIgnored(envPath, cwd, autoAddDefaultEnvFile) {
170
+ const path = await pathModule();
171
+ const fs = await fsModule();
172
+ const rootResult = await spawnCommand("git", ["rev-parse", "--show-toplevel"], cwd);
173
+ if (rootResult.code !== 0) {
174
+ return;
175
+ }
176
+ const repoRoot = await fs.realpath(path.resolve(rootResult.stdout.trim()));
177
+ const targetDir = await fs.realpath(path.dirname(path.resolve(envPath)));
178
+ const target = path.resolve(targetDir, path.basename(envPath));
179
+ const relativeTarget = path.relative(repoRoot, target);
180
+ if (relativeTarget !== "" && (relativeTarget.startsWith("..") || path.isAbsolute(relativeTarget))) {
181
+ return;
182
+ }
183
+ const ignoreResult = await spawnCommand("git", ["-C", repoRoot, "check-ignore", "--quiet", "--", relativeTarget], cwd);
184
+ if (ignoreResult.code === 0) {
185
+ return;
186
+ }
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
+ }
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.`);
207
+ }
208
+ throw new PaybondLoginError(`Unable to verify git ignore status for ${target}: ${ignoreResult.stderr.trim() || "git check-ignore failed"}`);
209
+ }
210
+ export async function assertCanWriteEnvFile(envPath, force) {
211
+ const fs = await fsModule();
212
+ try {
213
+ const existing = await fs.readFile(envPath, "utf8");
214
+ if (envKeyPattern().test(existing) && !force) {
215
+ throw new PaybondLoginError("PAYBOND_API_KEY already exists in the target env file; pass --force to replace it.");
216
+ }
217
+ }
218
+ catch (err) {
219
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
220
+ return;
221
+ }
222
+ throw err;
223
+ }
224
+ }
225
+ export async function writeEnvFile(envPath, rawKey, force) {
226
+ const fs = await fsModule();
227
+ let existing = "";
228
+ try {
229
+ existing = await fs.readFile(envPath, "utf8");
230
+ }
231
+ catch (err) {
232
+ if (!(err && typeof err === "object" && "code" in err && err.code === "ENOENT")) {
233
+ throw err;
234
+ }
235
+ }
236
+ const next = replaceOrAppendEnvValue(existing, rawKey, force);
237
+ await fs.writeFile(envPath, next, { encoding: "utf8", mode: 0o600 });
238
+ await fs.chmod(envPath, 0o600);
239
+ }
240
+ function gatewayUrl(base, path) {
241
+ return `${base.trim().replace(/\/+$/, "")}${path}`;
242
+ }
243
+ async function parseJsonResponse(response) {
244
+ const text = await response.text();
245
+ if (!text.trim()) {
246
+ return {};
247
+ }
248
+ try {
249
+ const parsed = JSON.parse(text);
250
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
251
+ return parsed;
252
+ }
253
+ }
254
+ catch {
255
+ // Fall through to the shaped error below.
256
+ }
257
+ throw new PaybondLoginError(`Gateway returned non-JSON response (${response.status}).`);
258
+ }
259
+ function stringField(body, field) {
260
+ const value = body[field];
261
+ return typeof value === "string" ? value.trim() : "";
262
+ }
263
+ function numberField(body, field, fallback) {
264
+ const value = body[field];
265
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
266
+ }
267
+ async function postGatewayJson(fetchFn, gateway, path, body) {
268
+ const response = await fetchFn(gatewayUrl(gateway, path), {
269
+ method: "POST",
270
+ headers: { "content-type": "application/json" },
271
+ body: JSON.stringify(body),
272
+ });
273
+ const parsed = await parseJsonResponse(response);
274
+ if (response.ok) {
275
+ return parsed;
276
+ }
277
+ const error = stringField(parsed, "error");
278
+ if (error) {
279
+ throw new OAuthPollError({
280
+ error,
281
+ error_description: stringField(parsed, "error_description") || stringField(parsed, "message"),
282
+ interval: numberField(parsed, "interval", 0) || undefined,
283
+ });
284
+ }
285
+ throw new PaybondLoginError(`Gateway ${path} HTTP ${response.status}.`);
286
+ }
287
+ function toDeviceStartResponse(body) {
288
+ const response = {
289
+ device_code: stringField(body, "device_code"),
290
+ user_code: stringField(body, "user_code"),
291
+ verification_uri: stringField(body, "verification_uri"),
292
+ verification_uri_complete: stringField(body, "verification_uri_complete") || undefined,
293
+ expires_in: numberField(body, "expires_in", 600),
294
+ interval: numberField(body, "interval", 5),
295
+ };
296
+ if (!response.device_code || !response.user_code || !response.verification_uri) {
297
+ throw new PaybondLoginError("Gateway device start response was missing required fields.");
298
+ }
299
+ return response;
300
+ }
301
+ function toDeviceTokenResponse(body, environment) {
302
+ const response = {
303
+ access_token: stringField(body, "access_token"),
304
+ token_type: stringField(body, "token_type"),
305
+ tenant_id: stringField(body, "tenant_id"),
306
+ tenant_uuid: stringField(body, "tenant_uuid"),
307
+ environment: stringField(body, "environment"),
308
+ service_account_role: stringField(body, "service_account_role"),
309
+ expires_at: stringField(body, "expires_at"),
310
+ };
311
+ if (!response.access_token || !response.tenant_id || !response.tenant_uuid) {
312
+ throw new PaybondLoginError("Gateway device token response was missing required fields.");
313
+ }
314
+ if (response.environment !== environment) {
315
+ throw new PaybondLoginError(`Gateway returned a ${response.environment || "unknown"} key but ${environment} was requested.`);
316
+ }
317
+ if (response.service_account_role !== "operator") {
318
+ throw new PaybondLoginError(`Gateway returned a non-operator key (${response.service_account_role || "unknown"}).`);
319
+ }
320
+ if (!response.access_token.startsWith(`paybond_sk_${environment}_`)) {
321
+ throw new PaybondLoginError(`Gateway returned an unexpected ${environment} API key shape.`);
322
+ }
323
+ return response;
324
+ }
325
+ async function startDeviceFlow(fetchFn, gateway, environment) {
326
+ const body = await postGatewayJson(fetchFn, gateway, "/v1/public/auth/device/start", {
327
+ client_id: CLIENT_ID,
328
+ client_name: CLIENT_NAME,
329
+ requested_environment: environment,
330
+ service_account_role: "operator",
331
+ });
332
+ return toDeviceStartResponse(body);
333
+ }
334
+ async function pollDeviceToken(fetchFn, gateway, environment, start, deps) {
335
+ let intervalSeconds = Math.max(MIN_POLL_INTERVAL_SECONDS, Math.trunc(start.interval || 5));
336
+ const expiresAtMs = deps.now() + Math.max(1, start.expires_in) * 1000;
337
+ for (;;) {
338
+ await deps.sleep(intervalSeconds * 1000);
339
+ if (deps.now() > expiresAtMs + 1000) {
340
+ throw new PaybondLoginError("Device authorization expired before approval.");
341
+ }
342
+ try {
343
+ const body = await postGatewayJson(fetchFn, gateway, "/v1/public/auth/device/token", {
344
+ grant_type: DEVICE_GRANT_TYPE,
345
+ device_code: start.device_code,
346
+ client_id: CLIENT_ID,
347
+ });
348
+ return toDeviceTokenResponse(body, environment);
349
+ }
350
+ catch (err) {
351
+ if (!(err instanceof OAuthPollError)) {
352
+ throw err;
353
+ }
354
+ if (err.code === "authorization_pending") {
355
+ intervalSeconds = Math.max(intervalSeconds, Math.trunc(err.interval || intervalSeconds));
356
+ continue;
357
+ }
358
+ if (err.code === "slow_down") {
359
+ intervalSeconds = Math.max(intervalSeconds + MIN_POLL_INTERVAL_SECONDS, Math.trunc(err.interval || 0));
360
+ continue;
361
+ }
362
+ if (err.code === "access_denied") {
363
+ throw new PaybondLoginError("Device authorization was denied.");
364
+ }
365
+ if (err.code === "expired_token") {
366
+ throw new PaybondLoginError("Device authorization expired before approval.");
367
+ }
368
+ throw new PaybondLoginError(err.message);
369
+ }
370
+ }
371
+ }
372
+ function maskAPIKey(rawKey) {
373
+ const trimmed = rawKey.trim();
374
+ const parts = trimmed.split("_");
375
+ if (parts.length >= 5 && parts[0] === "paybond" && parts[1] === "sk") {
376
+ const environment = parts[2];
377
+ const keyID = parts[3];
378
+ const redactedKeyID = keyID.length > 12 ? `${keyID.slice(0, 8)}...${keyID.slice(-4)}` : "redacted";
379
+ return `paybond_sk_${environment}_${redactedKeyID}`;
380
+ }
381
+ return "paybond_sk_...";
382
+ }
383
+ async function defaultSleep(ms) {
384
+ await new Promise((resolve) => setTimeout(resolve, ms));
385
+ }
386
+ async function defaultOpenBrowser(url) {
387
+ const platform = process.platform;
388
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
389
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
390
+ const result = await spawnCommand(command, args, process.cwd());
391
+ return result.code === 0;
392
+ }
393
+ export async function runLogin(options, deps = {}) {
394
+ const cwd = deps.cwd ?? process.cwd();
395
+ const stdout = deps.stdout ?? process.stdout;
396
+ const fetchFn = deps.fetch ?? fetch;
397
+ const sleep = deps.sleep ?? defaultSleep;
398
+ const now = deps.now ?? Date.now;
399
+ const openBrowser = deps.openBrowser ?? defaultOpenBrowser;
400
+ const envPath = await resolveEnvFile(options.envFile, cwd);
401
+ await assertCanWriteEnvFile(envPath, options.force);
402
+ await ensureGitIgnored(envPath, cwd, options.envFile === DEFAULT_ENV_FILE);
403
+ const start = await startDeviceFlow(fetchFn, options.gateway, options.environment);
404
+ const verificationUrl = start.verification_uri_complete || start.verification_uri;
405
+ stdout.write(`Paybond ${options.environment} login\n`);
406
+ stdout.write(`Verification URL: ${verificationUrl}\n`);
407
+ stdout.write(`Code: ${start.user_code}\n`);
408
+ if (!options.noOpen) {
409
+ const opened = await openBrowser(verificationUrl);
410
+ if (!opened) {
411
+ stdout.write(`Open the verification URL in a browser to approve this login.\n`);
412
+ }
413
+ }
414
+ stdout.write(`Waiting for approval...\n`);
415
+ const token = await pollDeviceToken(fetchFn, options.gateway, options.environment, start, { sleep, now });
416
+ await writeEnvFile(envPath, token.access_token, options.force);
417
+ stdout.write(`Wrote PAYBOND_API_KEY to ${envPath}\n`);
418
+ stdout.write(`Key: ${maskAPIKey(token.access_token)}\n`);
419
+ stdout.write(`Target ${token.environment} tenant: ${token.tenant_id} (${token.tenant_uuid})\n`);
420
+ if (token.expires_at) {
421
+ stdout.write(`This key auto-expires at ${token.expires_at}; re-run paybond login to mint a new one.\n`);
422
+ }
423
+ return 0;
424
+ }
425
+ export async function main(argv = process.argv.slice(2), deps = {}) {
426
+ let parsed;
427
+ try {
428
+ parsed = parseArgs(argv);
429
+ if (parsed === "help") {
430
+ (deps.stdout ?? process.stdout).write(`${usage()}\n`);
431
+ return 0;
432
+ }
433
+ return await runLogin(parsed, deps);
434
+ }
435
+ catch (err) {
436
+ (deps.stderr ?? process.stderr).write(`${err instanceof Error ? err.message : String(err)}\n`);
437
+ return 1;
438
+ }
439
+ }
440
+ function normalizeFileURL(url) {
441
+ return url.startsWith("file:///var/") ? url.replace("file:///var/", "file:///private/var/") : url;
442
+ }
443
+ function invokedFromCLI() {
444
+ const invokedPath = process.argv[1] ? normalizeFileURL(new URL("file://" + process.argv[1]).href) : "";
445
+ return Boolean(invokedPath && normalizeFileURL(import.meta.url) === invokedPath);
446
+ }
447
+ if (invokedFromCLI()) {
448
+ main().then((code) => {
449
+ process.exitCode = code;
450
+ }, (err) => {
451
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
452
+ process.exitCode = 1;
453
+ });
454
+ }
@@ -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;
@@ -204,6 +235,59 @@ class PaybondMCPRuntime {
204
235
  }
205
236
  return body;
206
237
  }
238
+ async bootstrapSandboxGuardrail(init) {
239
+ const expectedTenant = await this.tenantId();
240
+ const payload = {
241
+ operation: init.operation,
242
+ requested_spend_cents: init.requestedSpendCents,
243
+ };
244
+ if (init.currency?.trim()) {
245
+ payload.currency = init.currency.trim();
246
+ }
247
+ if (init.evidenceSchema !== undefined) {
248
+ payload.evidence_schema = init.evidenceSchema;
249
+ }
250
+ if (init.metadata !== undefined) {
251
+ payload.metadata = init.metadata;
252
+ }
253
+ const body = await this.gateway.postJSON("/v1/sandbox/guardrails/bootstrap", payload, optionalMutationHeaders(init.idempotencyKey));
254
+ assertSandboxGuardrailTenant(body, expectedTenant);
255
+ stringArg(body.intent_id, "intent_id");
256
+ stringArg(body.capability_token, "capability_token");
257
+ stringArg(body.operation, "operation");
258
+ intArg(body.requested_spend_cents, "requested_spend_cents");
259
+ stringArg(body.sandbox_lifecycle_status, "sandbox_lifecycle_status");
260
+ return body;
261
+ }
262
+ async submitSandboxGuardrailEvidence(init) {
263
+ const expectedTenant = await this.tenantId();
264
+ const payload = {};
265
+ if (init.payload !== undefined) {
266
+ payload.payload = init.payload;
267
+ }
268
+ if (init.artifacts !== undefined) {
269
+ payload.artifacts = init.artifacts;
270
+ }
271
+ if (init.operation?.trim()) {
272
+ payload.operation = init.operation.trim();
273
+ }
274
+ if (init.requestedSpendCents !== undefined) {
275
+ payload.requested_spend_cents = init.requestedSpendCents;
276
+ }
277
+ if (init.metadata !== undefined) {
278
+ payload.metadata = init.metadata;
279
+ }
280
+ const body = await this.gateway.postJSON(`/v1/sandbox/guardrails/${encodeURIComponent(init.intentId)}/evidence`, payload, optionalMutationHeaders(init.idempotencyKey));
281
+ assertSandboxGuardrailTenant(body, expectedTenant);
282
+ const echoedIntent = stringArg(body.intent_id, "intent_id");
283
+ if (echoedIntent !== init.intentId) {
284
+ throw new Error(`sandbox guardrail intent mismatch: requested=${init.intentId} gateway=${echoedIntent}`);
285
+ }
286
+ stringArg(body.operation, "operation");
287
+ intArg(body.requested_spend_cents, "requested_spend_cents");
288
+ stringArg(body.sandbox_lifecycle_status, "sandbox_lifecycle_status");
289
+ return body;
290
+ }
207
291
  async verifyAgentMandateV1(signedMandate) {
208
292
  return this.gateway.postJSON("/protocol/v2/mandates/verify", signedMandate, {
209
293
  "x-tenant-id": await this.tenantId(),
@@ -495,6 +579,56 @@ export class PaybondMCPServer {
495
579
  : intArg(args.requested_spend_cents, "requested_spend_cents"),
496
580
  }),
497
581
  },
582
+ {
583
+ name: "paybond_bootstrap_sandbox_guardrail",
584
+ description: "Bootstrap a sandbox-only Paybond guardrail intent for a first paid-tool integration. Tenant scope is derived from the configured service-account API key and the route never touches live settlement rails.",
585
+ inputSchema: objectSchema({
586
+ operation: { type: "string", description: "Delegated operation or paid tool name." },
587
+ requested_spend_cents: {
588
+ type: "integer",
589
+ description: "Sandbox spend amount in cents to authorize for the sample tool call.",
590
+ },
591
+ currency: { type: "string", description: "Optional ISO currency code; defaults at the gateway." },
592
+ evidence_schema: { type: "object", additionalProperties: true },
593
+ metadata: { type: "object", additionalProperties: true },
594
+ idempotency_key: { type: "string" },
595
+ }, ["operation", "requested_spend_cents"]),
596
+ call: async (args) => this.runtime.bootstrapSandboxGuardrail({
597
+ operation: stringArg(args.operation, "operation"),
598
+ requestedSpendCents: intArg(args.requested_spend_cents, "requested_spend_cents"),
599
+ currency: optionalString(args.currency),
600
+ evidenceSchema: args.evidence_schema === undefined ? undefined : ensureObject(args.evidence_schema, "evidence_schema"),
601
+ metadata: args.metadata === undefined ? undefined : ensureObject(args.metadata, "metadata"),
602
+ idempotencyKey: optionalString(args.idempotency_key),
603
+ }),
604
+ },
605
+ {
606
+ name: "paybond_submit_sandbox_guardrail_evidence",
607
+ description: "Submit evidence for a sandbox-only Paybond guardrail intent. Tenant scope is derived from the configured service-account API key and simulator settlement remains sandbox-only.",
608
+ inputSchema: objectSchema({
609
+ intent_id: { type: "string", description: "Sandbox guardrail intent UUID." },
610
+ payload: { type: "object", additionalProperties: true },
611
+ artifacts: { type: "array", items: { type: "string" } },
612
+ operation: { type: "string", description: "Optional operation override for the evidence record." },
613
+ requested_spend_cents: {
614
+ type: "integer",
615
+ description: "Optional sandbox spend amount override for the evidence record.",
616
+ },
617
+ metadata: { type: "object", additionalProperties: true },
618
+ idempotency_key: { type: "string" },
619
+ }, ["intent_id"]),
620
+ call: async (args) => this.runtime.submitSandboxGuardrailEvidence({
621
+ intentId: uuidArg(args.intent_id, "intent_id"),
622
+ payload: args.payload === undefined ? undefined : ensureObject(args.payload, "payload"),
623
+ artifacts: args.artifacts === undefined ? undefined : stringArrayArg(args.artifacts, "artifacts"),
624
+ operation: optionalString(args.operation),
625
+ requestedSpendCents: args.requested_spend_cents === undefined
626
+ ? undefined
627
+ : intArg(args.requested_spend_cents, "requested_spend_cents"),
628
+ metadata: args.metadata === undefined ? undefined : ensureObject(args.metadata, "metadata"),
629
+ idempotencyKey: optionalString(args.idempotency_key),
630
+ }),
631
+ },
498
632
  {
499
633
  name: "paybond_list_intents",
500
634
  description: "List tenant-scoped Harbor intents through the gateway operator view with optional filters.",
@@ -742,12 +876,13 @@ export class PaybondMCPServer {
742
876
  }
743
877
  }
744
878
  export function settingsFromEnv(env = process.env) {
745
- 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();
746
881
  if (!apiKey) {
747
- 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");
748
883
  }
749
884
  return {
750
- gatewayBaseUrl: DEFAULT_PAYBOND_GATEWAY_BASE_URL,
885
+ gatewayBaseUrl: optionalEnv(env.PAYBOND_GATEWAY_BASE_URL) ?? DEFAULT_PAYBOND_GATEWAY_BASE_URL,
751
886
  apiKey,
752
887
  principalPath: optionalEnv(env.PAYBOND_PRINCIPAL_PATH) ?? DEFAULT_PRINCIPAL_PATH,
753
888
  maxRetries: optionalEnv(env.PAYBOND_MCP_MAX_RETRIES)
@@ -820,6 +955,18 @@ function uuidArg(value, field) {
820
955
  }
821
956
  return raw;
822
957
  }
958
+ function stringArrayArg(value, field) {
959
+ if (!Array.isArray(value)) {
960
+ throw new Error(`${field} must be an array`);
961
+ }
962
+ return value.map((item, index) => stringArg(item, `${field}[${index}]`));
963
+ }
964
+ function assertSandboxGuardrailTenant(body, expectedTenant) {
965
+ const echoedTenant = stringArg(body.tenant_id, "tenant_id");
966
+ if (echoedTenant !== expectedTenant) {
967
+ throw new Error(`sandbox guardrail tenant mismatch: expected=${expectedTenant} gateway=${echoedTenant}`);
968
+ }
969
+ }
823
970
  function encodeRecognitionProofHeader(proof) {
824
971
  return Buffer.from(JSON.stringify(proof), "utf8").toString("base64url");
825
972
  }
@@ -899,13 +1046,16 @@ function formatError(err) {
899
1046
  }
900
1047
  return String(err);
901
1048
  }
1049
+ function normalizeFileURL(url) {
1050
+ return url.startsWith("file:///var/") ? url.replace("file:///var/", "file:///private/var/") : url;
1051
+ }
902
1052
  const isMainModule = (() => {
903
1053
  const scriptPath = process.argv[1];
904
1054
  if (!scriptPath) {
905
1055
  return false;
906
1056
  }
907
1057
  try {
908
- return import.meta.url === new URL(scriptPath, "file://").href;
1058
+ return normalizeFileURL(import.meta.url) === normalizeFileURL(new URL("file://" + scriptPath).href);
909
1059
  }
910
1060
  catch {
911
1061
  return import.meta.url.endsWith(scriptPath);
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@paybond/kit",
3
- "version": "0.9.3",
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",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
9
9
  "bin": {
10
+ "paybond": "dist/login.js",
10
11
  "paybond-init": "dist/init.js",
11
12
  "paybond-mcp-server": "dist/mcp-server.js"
12
13
  },