@paybond/kit 0.9.3 → 0.9.4

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,435 @@
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|live] [--env-file .env.local] [--gateway https://api.paybond.ai] [--no-open] [--force]",
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.",
31
+ ].join("\n");
32
+ }
33
+ function parseEnvironment(raw) {
34
+ const value = raw.trim().toLowerCase();
35
+ if (value === "sandbox" || value === "live") {
36
+ return value;
37
+ }
38
+ throw new PaybondLoginError("invalid --env (expected sandbox or live)");
39
+ }
40
+ export function parseArgs(argv) {
41
+ if (argv.length === 0) {
42
+ throw new PaybondLoginError(`missing command: login\n\n${usage()}`);
43
+ }
44
+ const [command, ...rest] = argv;
45
+ if (command === "--help" || command === "-h") {
46
+ return "help";
47
+ }
48
+ if (command !== "login") {
49
+ throw new PaybondLoginError(`unknown command: ${command}\n\n${usage()}`);
50
+ }
51
+ let envFile = DEFAULT_ENV_FILE;
52
+ let gateway = DEFAULT_GATEWAY;
53
+ let environment = "sandbox";
54
+ let noOpen = false;
55
+ let force = false;
56
+ for (let i = 0; i < rest.length; i += 1) {
57
+ const arg = rest[i];
58
+ if (arg === "--help" || arg === "-h") {
59
+ return "help";
60
+ }
61
+ if (arg === "--no-open") {
62
+ noOpen = true;
63
+ continue;
64
+ }
65
+ if (arg === "--force") {
66
+ force = true;
67
+ continue;
68
+ }
69
+ if (arg === "--live") {
70
+ environment = "live";
71
+ continue;
72
+ }
73
+ if (arg === "--env" || arg.startsWith("--env=")) {
74
+ const raw = arg === "--env" ? rest[++i] : arg.slice("--env=".length);
75
+ if (!raw || raw.startsWith("-")) {
76
+ throw new PaybondLoginError("invalid --env (expected sandbox or live)");
77
+ }
78
+ environment = parseEnvironment(raw);
79
+ continue;
80
+ }
81
+ if (arg === "--env-file" || arg.startsWith("--env-file=")) {
82
+ const raw = arg === "--env-file" ? rest[++i] : arg.slice("--env-file=".length);
83
+ if (!raw || raw.startsWith("-")) {
84
+ throw new PaybondLoginError("invalid --env-file");
85
+ }
86
+ envFile = raw;
87
+ continue;
88
+ }
89
+ if (arg === "--gateway" || arg.startsWith("--gateway=")) {
90
+ const raw = arg === "--gateway" ? rest[++i] : arg.slice("--gateway=".length);
91
+ if (!raw || raw.startsWith("-")) {
92
+ throw new PaybondLoginError("invalid --gateway");
93
+ }
94
+ gateway = raw;
95
+ continue;
96
+ }
97
+ throw new PaybondLoginError(`unknown argument: ${arg}`);
98
+ }
99
+ if (!envFile.trim()) {
100
+ throw new PaybondLoginError("invalid --env-file");
101
+ }
102
+ if (!gateway.trim()) {
103
+ throw new PaybondLoginError("invalid --gateway");
104
+ }
105
+ return { envFile, gateway, environment, noOpen, force };
106
+ }
107
+ function envKeyPattern() {
108
+ return /^\s*(?:export\s+)?PAYBOND_API_KEY\s*=/m;
109
+ }
110
+ function quoteEnvValue(value) {
111
+ if (/^[A-Za-z0-9_./:@+-]+$/.test(value)) {
112
+ return value;
113
+ }
114
+ return JSON.stringify(value);
115
+ }
116
+ function replaceOrAppendEnvValue(existing, rawKey, force) {
117
+ const line = `PAYBOND_API_KEY=${quoteEnvValue(rawKey)}`;
118
+ const pattern = /^(\s*(?:export\s+)?PAYBOND_API_KEY\s*=).*$/m;
119
+ if (pattern.test(existing)) {
120
+ if (!force) {
121
+ throw new PaybondLoginError("PAYBOND_API_KEY already exists in the target env file; pass --force to replace it.");
122
+ }
123
+ return existing.replace(pattern, line);
124
+ }
125
+ const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
126
+ return `${existing}${suffix}${line}\n`;
127
+ }
128
+ async function pathModule() {
129
+ // @ts-expect-error Node builtins are available in the published CLI runtime.
130
+ return await import("node:path");
131
+ }
132
+ async function fsModule() {
133
+ // @ts-expect-error Node builtins are available in the published CLI runtime.
134
+ return await import("node:fs/promises");
135
+ }
136
+ async function spawnCommand(command, args, cwd) {
137
+ // @ts-expect-error Node builtins are available in the published CLI runtime.
138
+ const childProcess = await import("node:child_process");
139
+ return await new Promise((resolve) => {
140
+ const child = childProcess.spawn(command, args, {
141
+ cwd,
142
+ stdio: ["ignore", "pipe", "pipe"],
143
+ });
144
+ let stdout = "";
145
+ let stderr = "";
146
+ child.stdout?.on("data", (chunk) => {
147
+ stdout += String(chunk);
148
+ });
149
+ child.stderr?.on("data", (chunk) => {
150
+ stderr += String(chunk);
151
+ });
152
+ child.on("error", (err) => {
153
+ resolve({ code: 127, stdout: "", stderr: err.message });
154
+ });
155
+ child.on("close", (code) => {
156
+ resolve({ code, stdout, stderr });
157
+ });
158
+ });
159
+ }
160
+ async function resolveEnvFile(envFile, cwd) {
161
+ const path = await pathModule();
162
+ return path.isAbsolute(envFile) ? path.resolve(envFile) : path.resolve(cwd, envFile);
163
+ }
164
+ export async function assertGitIgnored(envPath, cwd) {
165
+ const path = await pathModule();
166
+ const fs = await fsModule();
167
+ const rootResult = await spawnCommand("git", ["rev-parse", "--show-toplevel"], cwd);
168
+ if (rootResult.code !== 0) {
169
+ return;
170
+ }
171
+ const repoRoot = await fs.realpath(path.resolve(rootResult.stdout.trim()));
172
+ const targetDir = await fs.realpath(path.dirname(path.resolve(envPath)));
173
+ const target = path.resolve(targetDir, path.basename(envPath));
174
+ const relativeTarget = path.relative(repoRoot, target);
175
+ if (relativeTarget !== "" && (relativeTarget.startsWith("..") || path.isAbsolute(relativeTarget))) {
176
+ return;
177
+ }
178
+ const ignoreResult = await spawnCommand("git", ["-C", repoRoot, "check-ignore", "--quiet", "--", relativeTarget], cwd);
179
+ if (ignoreResult.code === 0) {
180
+ return;
181
+ }
182
+ if (ignoreResult.code === 1) {
183
+ 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
+ }
185
+ throw new PaybondLoginError(`Unable to verify git ignore status for ${target}: ${ignoreResult.stderr.trim() || "git check-ignore failed"}`);
186
+ }
187
+ export async function assertCanWriteEnvFile(envPath, force) {
188
+ const fs = await fsModule();
189
+ try {
190
+ const existing = await fs.readFile(envPath, "utf8");
191
+ if (envKeyPattern().test(existing) && !force) {
192
+ throw new PaybondLoginError("PAYBOND_API_KEY already exists in the target env file; pass --force to replace it.");
193
+ }
194
+ }
195
+ catch (err) {
196
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
197
+ return;
198
+ }
199
+ throw err;
200
+ }
201
+ }
202
+ export async function writeEnvFile(envPath, rawKey, force) {
203
+ const fs = await fsModule();
204
+ let existing = "";
205
+ try {
206
+ existing = await fs.readFile(envPath, "utf8");
207
+ }
208
+ catch (err) {
209
+ if (!(err && typeof err === "object" && "code" in err && err.code === "ENOENT")) {
210
+ throw err;
211
+ }
212
+ }
213
+ const next = replaceOrAppendEnvValue(existing, rawKey, force);
214
+ await fs.writeFile(envPath, next, { encoding: "utf8", mode: 0o600 });
215
+ await fs.chmod(envPath, 0o600);
216
+ }
217
+ function gatewayUrl(base, path) {
218
+ return `${base.trim().replace(/\/+$/, "")}${path}`;
219
+ }
220
+ async function parseJsonResponse(response) {
221
+ const text = await response.text();
222
+ if (!text.trim()) {
223
+ return {};
224
+ }
225
+ try {
226
+ const parsed = JSON.parse(text);
227
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
228
+ return parsed;
229
+ }
230
+ }
231
+ catch {
232
+ // Fall through to the shaped error below.
233
+ }
234
+ throw new PaybondLoginError(`Gateway returned non-JSON response (${response.status}).`);
235
+ }
236
+ function stringField(body, field) {
237
+ const value = body[field];
238
+ return typeof value === "string" ? value.trim() : "";
239
+ }
240
+ function numberField(body, field, fallback) {
241
+ const value = body[field];
242
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
243
+ }
244
+ async function postGatewayJson(fetchFn, gateway, path, body) {
245
+ const response = await fetchFn(gatewayUrl(gateway, path), {
246
+ method: "POST",
247
+ headers: { "content-type": "application/json" },
248
+ body: JSON.stringify(body),
249
+ });
250
+ const parsed = await parseJsonResponse(response);
251
+ if (response.ok) {
252
+ return parsed;
253
+ }
254
+ const error = stringField(parsed, "error");
255
+ if (error) {
256
+ throw new OAuthPollError({
257
+ error,
258
+ error_description: stringField(parsed, "error_description") || stringField(parsed, "message"),
259
+ interval: numberField(parsed, "interval", 0) || undefined,
260
+ });
261
+ }
262
+ throw new PaybondLoginError(`Gateway ${path} HTTP ${response.status}.`);
263
+ }
264
+ function toDeviceStartResponse(body) {
265
+ const response = {
266
+ device_code: stringField(body, "device_code"),
267
+ user_code: stringField(body, "user_code"),
268
+ verification_uri: stringField(body, "verification_uri"),
269
+ verification_uri_complete: stringField(body, "verification_uri_complete") || undefined,
270
+ expires_in: numberField(body, "expires_in", 600),
271
+ interval: numberField(body, "interval", 5),
272
+ };
273
+ if (!response.device_code || !response.user_code || !response.verification_uri) {
274
+ throw new PaybondLoginError("Gateway device start response was missing required fields.");
275
+ }
276
+ return response;
277
+ }
278
+ function toDeviceTokenResponse(body, environment) {
279
+ const response = {
280
+ access_token: stringField(body, "access_token"),
281
+ token_type: stringField(body, "token_type"),
282
+ tenant_id: stringField(body, "tenant_id"),
283
+ tenant_uuid: stringField(body, "tenant_uuid"),
284
+ environment: stringField(body, "environment"),
285
+ service_account_role: stringField(body, "service_account_role"),
286
+ expires_at: stringField(body, "expires_at"),
287
+ };
288
+ if (!response.access_token || !response.tenant_id || !response.tenant_uuid) {
289
+ throw new PaybondLoginError("Gateway device token response was missing required fields.");
290
+ }
291
+ if (response.environment !== environment) {
292
+ throw new PaybondLoginError(`Gateway returned a ${response.environment || "unknown"} key but ${environment} was requested.`);
293
+ }
294
+ if (response.service_account_role !== "operator") {
295
+ throw new PaybondLoginError(`Gateway returned a non-operator key (${response.service_account_role || "unknown"}).`);
296
+ }
297
+ if (!response.access_token.startsWith(`paybond_sk_${environment}_`)) {
298
+ throw new PaybondLoginError(`Gateway returned an unexpected ${environment} API key shape.`);
299
+ }
300
+ return response;
301
+ }
302
+ async function startDeviceFlow(fetchFn, gateway, environment) {
303
+ const body = await postGatewayJson(fetchFn, gateway, "/v1/public/auth/device/start", {
304
+ client_id: CLIENT_ID,
305
+ client_name: CLIENT_NAME,
306
+ requested_environment: environment,
307
+ service_account_role: "operator",
308
+ });
309
+ return toDeviceStartResponse(body);
310
+ }
311
+ async function pollDeviceToken(fetchFn, gateway, environment, start, deps) {
312
+ let intervalSeconds = Math.max(MIN_POLL_INTERVAL_SECONDS, Math.trunc(start.interval || 5));
313
+ const expiresAtMs = deps.now() + Math.max(1, start.expires_in) * 1000;
314
+ for (;;) {
315
+ await deps.sleep(intervalSeconds * 1000);
316
+ if (deps.now() > expiresAtMs + 1000) {
317
+ throw new PaybondLoginError("Device authorization expired before approval.");
318
+ }
319
+ try {
320
+ const body = await postGatewayJson(fetchFn, gateway, "/v1/public/auth/device/token", {
321
+ grant_type: DEVICE_GRANT_TYPE,
322
+ device_code: start.device_code,
323
+ client_id: CLIENT_ID,
324
+ });
325
+ return toDeviceTokenResponse(body, environment);
326
+ }
327
+ catch (err) {
328
+ if (!(err instanceof OAuthPollError)) {
329
+ throw err;
330
+ }
331
+ if (err.code === "authorization_pending") {
332
+ intervalSeconds = Math.max(intervalSeconds, Math.trunc(err.interval || intervalSeconds));
333
+ continue;
334
+ }
335
+ if (err.code === "slow_down") {
336
+ intervalSeconds = Math.max(intervalSeconds + MIN_POLL_INTERVAL_SECONDS, Math.trunc(err.interval || 0));
337
+ continue;
338
+ }
339
+ if (err.code === "access_denied") {
340
+ throw new PaybondLoginError("Device authorization was denied.");
341
+ }
342
+ if (err.code === "expired_token") {
343
+ throw new PaybondLoginError("Device authorization expired before approval.");
344
+ }
345
+ throw new PaybondLoginError(err.message);
346
+ }
347
+ }
348
+ }
349
+ function maskAPIKey(rawKey) {
350
+ const trimmed = rawKey.trim();
351
+ const parts = trimmed.split("_");
352
+ if (parts.length >= 5 && parts[0] === "paybond" && parts[1] === "sk") {
353
+ const environment = parts[2];
354
+ const keyID = parts[3];
355
+ const redactedKeyID = keyID.length > 12 ? `${keyID.slice(0, 8)}...${keyID.slice(-4)}` : "redacted";
356
+ return `paybond_sk_${environment}_${redactedKeyID}`;
357
+ }
358
+ return "paybond_sk_...";
359
+ }
360
+ async function defaultSleep(ms) {
361
+ await new Promise((resolve) => setTimeout(resolve, ms));
362
+ }
363
+ async function defaultOpenBrowser(url) {
364
+ const platform = process.platform;
365
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
366
+ const args = platform === "win32" ? ["/c", "start", "", url] : [url];
367
+ const result = await spawnCommand(command, args, process.cwd());
368
+ return result.code === 0;
369
+ }
370
+ export async function runLogin(options, deps = {}) {
371
+ const cwd = deps.cwd ?? process.cwd();
372
+ const stdout = deps.stdout ?? process.stdout;
373
+ const fetchFn = deps.fetch ?? fetch;
374
+ const sleep = deps.sleep ?? defaultSleep;
375
+ const now = deps.now ?? Date.now;
376
+ const openBrowser = deps.openBrowser ?? defaultOpenBrowser;
377
+ const envPath = await resolveEnvFile(options.envFile, cwd);
378
+ await assertCanWriteEnvFile(envPath, options.force);
379
+ await assertGitIgnored(envPath, cwd);
380
+ const start = await startDeviceFlow(fetchFn, options.gateway, options.environment);
381
+ const verificationUrl = start.verification_uri_complete || start.verification_uri;
382
+ 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
+ stdout.write(`Verification URL: ${verificationUrl}\n`);
388
+ stdout.write(`Code: ${start.user_code}\n`);
389
+ if (!options.noOpen) {
390
+ const opened = await openBrowser(verificationUrl);
391
+ if (!opened) {
392
+ stdout.write(`Open the verification URL in a browser to approve this login.\n`);
393
+ }
394
+ }
395
+ stdout.write(`Waiting for approval...\n`);
396
+ const token = await pollDeviceToken(fetchFn, options.gateway, options.environment, start, { sleep, now });
397
+ await writeEnvFile(envPath, token.access_token, options.force);
398
+ stdout.write(`Wrote PAYBOND_API_KEY to ${envPath}\n`);
399
+ stdout.write(`Key: ${maskAPIKey(token.access_token)}\n`);
400
+ stdout.write(`Target ${token.environment} tenant: ${token.tenant_id} (${token.tenant_uuid})\n`);
401
+ if (token.expires_at) {
402
+ stdout.write(`This key auto-expires at ${token.expires_at}; re-run paybond login to mint a new one.\n`);
403
+ }
404
+ return 0;
405
+ }
406
+ export async function main(argv = process.argv.slice(2), deps = {}) {
407
+ let parsed;
408
+ try {
409
+ parsed = parseArgs(argv);
410
+ if (parsed === "help") {
411
+ (deps.stdout ?? process.stdout).write(`${usage()}\n`);
412
+ return 0;
413
+ }
414
+ return await runLogin(parsed, deps);
415
+ }
416
+ catch (err) {
417
+ (deps.stderr ?? process.stderr).write(`${err instanceof Error ? err.message : String(err)}\n`);
418
+ return 1;
419
+ }
420
+ }
421
+ function normalizeFileURL(url) {
422
+ return url.startsWith("file:///var/") ? url.replace("file:///var/", "file:///private/var/") : url;
423
+ }
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);
427
+ }
428
+ if (invokedFromCLI()) {
429
+ main().then((code) => {
430
+ process.exitCode = code;
431
+ }, (err) => {
432
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
433
+ process.exitCode = 1;
434
+ });
435
+ }
@@ -204,6 +204,59 @@ class PaybondMCPRuntime {
204
204
  }
205
205
  return body;
206
206
  }
207
+ async bootstrapSandboxGuardrail(init) {
208
+ const expectedTenant = await this.tenantId();
209
+ const payload = {
210
+ operation: init.operation,
211
+ requested_spend_cents: init.requestedSpendCents,
212
+ };
213
+ if (init.currency?.trim()) {
214
+ payload.currency = init.currency.trim();
215
+ }
216
+ if (init.evidenceSchema !== undefined) {
217
+ payload.evidence_schema = init.evidenceSchema;
218
+ }
219
+ if (init.metadata !== undefined) {
220
+ payload.metadata = init.metadata;
221
+ }
222
+ const body = await this.gateway.postJSON("/v1/sandbox/guardrails/bootstrap", payload, optionalMutationHeaders(init.idempotencyKey));
223
+ assertSandboxGuardrailTenant(body, expectedTenant);
224
+ stringArg(body.intent_id, "intent_id");
225
+ stringArg(body.capability_token, "capability_token");
226
+ stringArg(body.operation, "operation");
227
+ intArg(body.requested_spend_cents, "requested_spend_cents");
228
+ stringArg(body.sandbox_lifecycle_status, "sandbox_lifecycle_status");
229
+ return body;
230
+ }
231
+ async submitSandboxGuardrailEvidence(init) {
232
+ const expectedTenant = await this.tenantId();
233
+ const payload = {};
234
+ if (init.payload !== undefined) {
235
+ payload.payload = init.payload;
236
+ }
237
+ if (init.artifacts !== undefined) {
238
+ payload.artifacts = init.artifacts;
239
+ }
240
+ if (init.operation?.trim()) {
241
+ payload.operation = init.operation.trim();
242
+ }
243
+ if (init.requestedSpendCents !== undefined) {
244
+ payload.requested_spend_cents = init.requestedSpendCents;
245
+ }
246
+ if (init.metadata !== undefined) {
247
+ payload.metadata = init.metadata;
248
+ }
249
+ const body = await this.gateway.postJSON(`/v1/sandbox/guardrails/${encodeURIComponent(init.intentId)}/evidence`, payload, optionalMutationHeaders(init.idempotencyKey));
250
+ assertSandboxGuardrailTenant(body, expectedTenant);
251
+ const echoedIntent = stringArg(body.intent_id, "intent_id");
252
+ if (echoedIntent !== init.intentId) {
253
+ throw new Error(`sandbox guardrail intent mismatch: requested=${init.intentId} gateway=${echoedIntent}`);
254
+ }
255
+ stringArg(body.operation, "operation");
256
+ intArg(body.requested_spend_cents, "requested_spend_cents");
257
+ stringArg(body.sandbox_lifecycle_status, "sandbox_lifecycle_status");
258
+ return body;
259
+ }
207
260
  async verifyAgentMandateV1(signedMandate) {
208
261
  return this.gateway.postJSON("/protocol/v2/mandates/verify", signedMandate, {
209
262
  "x-tenant-id": await this.tenantId(),
@@ -495,6 +548,56 @@ export class PaybondMCPServer {
495
548
  : intArg(args.requested_spend_cents, "requested_spend_cents"),
496
549
  }),
497
550
  },
551
+ {
552
+ name: "paybond_bootstrap_sandbox_guardrail",
553
+ 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.",
554
+ inputSchema: objectSchema({
555
+ operation: { type: "string", description: "Delegated operation or paid tool name." },
556
+ requested_spend_cents: {
557
+ type: "integer",
558
+ description: "Sandbox spend amount in cents to authorize for the sample tool call.",
559
+ },
560
+ currency: { type: "string", description: "Optional ISO currency code; defaults at the gateway." },
561
+ evidence_schema: { type: "object", additionalProperties: true },
562
+ metadata: { type: "object", additionalProperties: true },
563
+ idempotency_key: { type: "string" },
564
+ }, ["operation", "requested_spend_cents"]),
565
+ call: async (args) => this.runtime.bootstrapSandboxGuardrail({
566
+ operation: stringArg(args.operation, "operation"),
567
+ requestedSpendCents: intArg(args.requested_spend_cents, "requested_spend_cents"),
568
+ currency: optionalString(args.currency),
569
+ evidenceSchema: args.evidence_schema === undefined ? undefined : ensureObject(args.evidence_schema, "evidence_schema"),
570
+ metadata: args.metadata === undefined ? undefined : ensureObject(args.metadata, "metadata"),
571
+ idempotencyKey: optionalString(args.idempotency_key),
572
+ }),
573
+ },
574
+ {
575
+ name: "paybond_submit_sandbox_guardrail_evidence",
576
+ 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.",
577
+ inputSchema: objectSchema({
578
+ intent_id: { type: "string", description: "Sandbox guardrail intent UUID." },
579
+ payload: { type: "object", additionalProperties: true },
580
+ artifacts: { type: "array", items: { type: "string" } },
581
+ operation: { type: "string", description: "Optional operation override for the evidence record." },
582
+ requested_spend_cents: {
583
+ type: "integer",
584
+ description: "Optional sandbox spend amount override for the evidence record.",
585
+ },
586
+ metadata: { type: "object", additionalProperties: true },
587
+ idempotency_key: { type: "string" },
588
+ }, ["intent_id"]),
589
+ call: async (args) => this.runtime.submitSandboxGuardrailEvidence({
590
+ intentId: uuidArg(args.intent_id, "intent_id"),
591
+ payload: args.payload === undefined ? undefined : ensureObject(args.payload, "payload"),
592
+ artifacts: args.artifacts === undefined ? undefined : stringArrayArg(args.artifacts, "artifacts"),
593
+ operation: optionalString(args.operation),
594
+ requestedSpendCents: args.requested_spend_cents === undefined
595
+ ? undefined
596
+ : intArg(args.requested_spend_cents, "requested_spend_cents"),
597
+ metadata: args.metadata === undefined ? undefined : ensureObject(args.metadata, "metadata"),
598
+ idempotencyKey: optionalString(args.idempotency_key),
599
+ }),
600
+ },
498
601
  {
499
602
  name: "paybond_list_intents",
500
603
  description: "List tenant-scoped Harbor intents through the gateway operator view with optional filters.",
@@ -820,6 +923,18 @@ function uuidArg(value, field) {
820
923
  }
821
924
  return raw;
822
925
  }
926
+ function stringArrayArg(value, field) {
927
+ if (!Array.isArray(value)) {
928
+ throw new Error(`${field} must be an array`);
929
+ }
930
+ return value.map((item, index) => stringArg(item, `${field}[${index}]`));
931
+ }
932
+ function assertSandboxGuardrailTenant(body, expectedTenant) {
933
+ const echoedTenant = stringArg(body.tenant_id, "tenant_id");
934
+ if (echoedTenant !== expectedTenant) {
935
+ throw new Error(`sandbox guardrail tenant mismatch: expected=${expectedTenant} gateway=${echoedTenant}`);
936
+ }
937
+ }
823
938
  function encodeRecognitionProofHeader(proof) {
824
939
  return Buffer.from(JSON.stringify(proof), "utf8").toString("base64url");
825
940
  }
@@ -899,13 +1014,16 @@ function formatError(err) {
899
1014
  }
900
1015
  return String(err);
901
1016
  }
1017
+ function normalizeFileURL(url) {
1018
+ return url.startsWith("file:///var/") ? url.replace("file:///var/", "file:///private/var/") : url;
1019
+ }
902
1020
  const isMainModule = (() => {
903
1021
  const scriptPath = process.argv[1];
904
1022
  if (!scriptPath) {
905
1023
  return false;
906
1024
  }
907
1025
  try {
908
- return import.meta.url === new URL(scriptPath, "file://").href;
1026
+ return normalizeFileURL(import.meta.url) === normalizeFileURL(new URL("file://" + scriptPath).href);
909
1027
  }
910
1028
  catch {
911
1029
  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.4",
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
  },