@paybond/kit 0.9.8 → 0.10.0

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.
Files changed (58) hide show
  1. package/dist/cli/audit-export.d.ts +7 -0
  2. package/dist/cli/audit-export.js +120 -0
  3. package/dist/cli/automation.d.ts +25 -0
  4. package/dist/cli/automation.js +297 -0
  5. package/dist/cli/body.d.ts +7 -0
  6. package/dist/cli/body.js +22 -0
  7. package/dist/cli/color.d.ts +15 -0
  8. package/dist/cli/color.js +39 -0
  9. package/dist/cli/command-spec.d.ts +12 -0
  10. package/dist/cli/command-spec.js +330 -0
  11. package/dist/cli/commands/discovery.d.ts +8 -0
  12. package/dist/cli/commands/discovery.js +194 -0
  13. package/dist/cli/commands/setup.d.ts +15 -0
  14. package/dist/cli/commands/setup.js +397 -0
  15. package/dist/cli/commands/workflows.d.ts +7 -0
  16. package/dist/cli/commands/workflows.js +209 -0
  17. package/dist/cli/config.d.ts +14 -0
  18. package/dist/cli/config.js +96 -0
  19. package/dist/cli/context.d.ts +22 -0
  20. package/dist/cli/context.js +109 -0
  21. package/dist/cli/credentials.d.ts +21 -0
  22. package/dist/cli/credentials.js +141 -0
  23. package/dist/cli/doctor-agent.d.ts +15 -0
  24. package/dist/cli/doctor-agent.js +311 -0
  25. package/dist/cli/envelope.d.ts +14 -0
  26. package/dist/cli/envelope.js +46 -0
  27. package/dist/cli/globals.d.ts +22 -0
  28. package/dist/cli/globals.js +238 -0
  29. package/dist/cli/help.d.ts +3 -0
  30. package/dist/cli/help.js +51 -0
  31. package/dist/cli/mcp-install.d.ts +41 -0
  32. package/dist/cli/mcp-install.js +95 -0
  33. package/dist/cli/mcp-policy.d.ts +23 -0
  34. package/dist/cli/mcp-policy.js +104 -0
  35. package/dist/cli/mcp-verify-config.d.ts +37 -0
  36. package/dist/cli/mcp-verify-config.js +174 -0
  37. package/dist/cli/redact.d.ts +4 -0
  38. package/dist/cli/redact.js +67 -0
  39. package/dist/cli/request-id.d.ts +2 -0
  40. package/dist/cli/request-id.js +5 -0
  41. package/dist/cli/router.d.ts +2 -0
  42. package/dist/cli/router.js +275 -0
  43. package/dist/cli/suggest.d.ts +4 -0
  44. package/dist/cli/suggest.js +58 -0
  45. package/dist/cli/support-diagnostics.d.ts +19 -0
  46. package/dist/cli/support-diagnostics.js +47 -0
  47. package/dist/cli/types.d.ts +72 -0
  48. package/dist/cli/types.js +60 -0
  49. package/dist/cli/ux.d.ts +8 -0
  50. package/dist/cli/ux.js +164 -0
  51. package/dist/cli.d.ts +2 -0
  52. package/dist/cli.js +38 -0
  53. package/dist/init.js +9 -14
  54. package/dist/login.d.ts +14 -1
  55. package/dist/login.js +123 -63
  56. package/dist/mcp-server.d.ts +4 -0
  57. package/dist/mcp-server.js +204 -76
  58. package/package.json +5 -2
package/dist/cli/ux.js ADDED
@@ -0,0 +1,164 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { COMMAND_EXAMPLES, COMPLETION_SCRIPTS, WORKFLOWS } from "./command-spec.js";
4
+ import { resolvedDefaultsForDoctor } from "./credentials.js";
5
+ import { helpForCommand } from "./help.js";
6
+ import { consumeFlag } from "./globals.js";
7
+ import { planMcpInstall, parseMcpInstallFormat, parseMcpInstallHost, parseMcpInstallScope } from "./mcp-install.js";
8
+ import { CliError } from "./types.js";
9
+ import { handleDoctor } from "./commands/setup.js";
10
+ export function resolveHelpPath(argv) {
11
+ return argv.filter((part) => part !== "--help" && part !== "-h").join(" ");
12
+ }
13
+ export function renderHelpText(path) {
14
+ return helpForCommand(path);
15
+ }
16
+ export function handleHelpCommand(argv) {
17
+ const helpPath = resolveHelpPath(argv);
18
+ return { data: { text: renderHelpText(helpPath), path: helpPath || "paybond" } };
19
+ }
20
+ export function handleExamplesCommand(argv) {
21
+ const filterPath = argv.filter((part) => part !== "--help" && part !== "-h").join(" ");
22
+ const lines = [];
23
+ if (!filterPath) {
24
+ lines.push("Workflows:");
25
+ for (const workflow of WORKFLOWS) {
26
+ lines.push("", workflow.title);
27
+ if (workflow.description) {
28
+ lines.push(workflow.description);
29
+ }
30
+ for (const example of workflow.examples) {
31
+ lines.push(` $ ${example}`);
32
+ }
33
+ if (workflow.next) {
34
+ lines.push(` Next: ${workflow.next}`);
35
+ }
36
+ }
37
+ lines.push("", "Commands:");
38
+ }
39
+ const entries = filterPath
40
+ ? Object.entries(COMMAND_EXAMPLES).filter(([commandPath]) => commandPath === filterPath || commandPath.startsWith(`${filterPath} `))
41
+ : Object.entries(COMMAND_EXAMPLES);
42
+ if (filterPath && entries.length === 0) {
43
+ throw new CliError(`no examples found for: ${filterPath}`, {
44
+ category: "usage",
45
+ code: "cli.usage.unknown_command",
46
+ });
47
+ }
48
+ for (const [commandPath, examples] of entries) {
49
+ lines.push("", `paybond ${commandPath}`);
50
+ for (const example of examples) {
51
+ lines.push(` $ ${example}`);
52
+ }
53
+ }
54
+ return { data: { text: lines.join("\n").trim(), filter: filterPath || null, count: entries.length } };
55
+ }
56
+ export function handleCompletionCommand(argv) {
57
+ const shell = argv[0];
58
+ if (!shell || shell === "--help" || shell === "-h") {
59
+ throw new CliError("completion requires bash|zsh|fish", {
60
+ category: "usage",
61
+ code: "cli.usage.missing_completion_shell",
62
+ });
63
+ }
64
+ const script = COMPLETION_SCRIPTS[shell];
65
+ if (!script) {
66
+ throw new CliError(`unsupported completion shell: ${shell} (expected bash|zsh|fish)`, {
67
+ category: "usage",
68
+ code: "cli.usage.invalid_completion_shell",
69
+ });
70
+ }
71
+ return { data: { shell, script } };
72
+ }
73
+ export async function handleOnboarding(ctx, argv) {
74
+ const hostFlag = consumeFlag(argv, "--host");
75
+ let installHost = "generic";
76
+ try {
77
+ if (hostFlag.present) {
78
+ installHost = parseMcpInstallHost(hostFlag.value);
79
+ }
80
+ }
81
+ catch (err) {
82
+ throw new CliError(err instanceof Error ? err.message : String(err), {
83
+ category: "usage",
84
+ code: "cli.usage.invalid_mcp_install",
85
+ });
86
+ }
87
+ const steps = [];
88
+ steps.push({
89
+ name: "runtime",
90
+ ok: true,
91
+ message: `node ${process.version}`,
92
+ });
93
+ const defaults = resolvedDefaultsForDoctor(ctx.globals);
94
+ const envPath = path.isAbsolute(defaults.envFile)
95
+ ? path.resolve(defaults.envFile)
96
+ : path.resolve(ctx.cwd, defaults.envFile);
97
+ let loggedIn = false;
98
+ try {
99
+ const { resolveApiKey } = await import("./credentials.js");
100
+ const apiKey = await resolveApiKey(ctx.globals, ctx.cwd);
101
+ loggedIn = true;
102
+ const { maskApiKey } = await import("./redact.js");
103
+ steps.push({
104
+ name: "login",
105
+ ok: true,
106
+ message: `credentials found (${maskApiKey(apiKey)})`,
107
+ });
108
+ }
109
+ catch (err) {
110
+ steps.push({
111
+ name: "login",
112
+ ok: false,
113
+ message: err instanceof Error ? err.message : String(err),
114
+ command: "paybond login",
115
+ });
116
+ }
117
+ const guardrailPath = path.resolve(ctx.cwd, "paybond-paid-tool-guard.ts");
118
+ let guardrailExists = false;
119
+ try {
120
+ await access(guardrailPath);
121
+ guardrailExists = true;
122
+ }
123
+ catch {
124
+ guardrailExists = false;
125
+ }
126
+ steps.push({
127
+ name: "guardrail",
128
+ ok: guardrailExists,
129
+ message: guardrailExists ? `found ${guardrailPath}` : `guardrail file not found (${guardrailPath})`,
130
+ command: guardrailExists ? undefined : "paybond init guardrail",
131
+ });
132
+ const plan = planMcpInstall({
133
+ host: installHost,
134
+ scope: parseMcpInstallScope("local"),
135
+ format: parseMcpInstallFormat(undefined, installHost),
136
+ envFile: defaults.envFile,
137
+ cwd: ctx.cwd,
138
+ home: process.env.HOME ?? process.env.USERPROFILE ?? ctx.cwd,
139
+ });
140
+ steps.push({
141
+ name: "mcp_config",
142
+ ok: true,
143
+ message: `preview ready for host=${plan.host} (non-destructive --scope local)`,
144
+ command: `paybond mcp install --host ${plan.host} --scope local`,
145
+ });
146
+ const doctor = await handleDoctor(ctx, loggedIn ? ["--agent"] : []);
147
+ const doctorOk = String(doctor.data.summary) === "pass";
148
+ steps.push({
149
+ name: "doctor",
150
+ ok: doctorOk,
151
+ message: `doctor ${doctor.data.summary}`,
152
+ command: doctorOk ? undefined : "paybond doctor --agent",
153
+ });
154
+ const summary = steps.every((step) => step.ok) ? "pass" : "fail";
155
+ return {
156
+ data: {
157
+ steps,
158
+ summary,
159
+ env_file: envPath,
160
+ mcp_preview: plan.printed ? plan.payload : undefined,
161
+ doctor_checks: doctor.data.checks,
162
+ },
163
+ };
164
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { runCli } from "./cli/router.js";
package/dist/cli.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { runCli } from "./cli/router.js";
6
+ async function invokedFromCLI() {
7
+ const scriptPath = process.argv[1];
8
+ if (!scriptPath) {
9
+ return false;
10
+ }
11
+ async function realFileURL(filePath) {
12
+ let resolved = path.resolve(filePath);
13
+ try {
14
+ resolved = await fs.realpath(resolved);
15
+ }
16
+ catch {
17
+ // keep absolute path
18
+ }
19
+ const href = pathToFileURL(resolved).href;
20
+ return href.startsWith("file:///var/") ? href.replace("file:///var/", "file:///private/var/") : href;
21
+ }
22
+ return (await realFileURL(scriptPath)) === (await realFileURL(fileURLToPath(import.meta.url)));
23
+ }
24
+ invokedFromCLI().then((invoked) => {
25
+ if (!invoked) {
26
+ return;
27
+ }
28
+ runCli(process.argv.slice(2)).then((code) => {
29
+ process.exitCode = code;
30
+ }, (err) => {
31
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
32
+ process.exitCode = 1;
33
+ });
34
+ }, (err) => {
35
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
36
+ process.exitCode = 1;
37
+ });
38
+ export { runCli } from "./cli/router.js";
package/dist/init.js CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+ import { runCli } from "./cli/router.js";
2
6
  const FRAMEWORKS = new Set([
3
7
  "generic",
4
8
  "provider-agnostic",
@@ -79,7 +83,8 @@ function parseArgs(argv) {
79
83
  return { preset, framework, out, force };
80
84
  }
81
85
  function template(framework) {
82
- return `import {
86
+ return `import fs from "node:fs/promises";
87
+ import {
83
88
  Paybond,
84
89
  type SandboxGuardrailBootstrapResult,
85
90
  type SandboxGuardrailEvidenceResult,
@@ -139,8 +144,6 @@ function readEnvValue(body: string, key: string): string | undefined {
139
144
  }
140
145
 
141
146
  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
147
  try {
145
148
  return await fs.readFile(envFile, "utf8");
146
149
  } catch (err) {
@@ -239,8 +242,6 @@ export async function submitSandboxEvidence(
239
242
  `;
240
243
  }
241
244
  async function writeScaffold(out, body, force) {
242
- // @ts-expect-error Node builtins are available in the published CLI runtime.
243
- const fs = await import("node:fs/promises");
244
245
  try {
245
246
  await fs.stat(out);
246
247
  if (!force) {
@@ -286,12 +287,6 @@ async function invokedFromCLI() {
286
287
  if (!scriptPath) {
287
288
  return false;
288
289
  }
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
290
  async function realFileURL(filePath) {
296
291
  let resolved = path.resolve(filePath);
297
292
  try {
@@ -301,15 +296,15 @@ async function invokedFromCLI() {
301
296
  // If realpath fails, compare the absolute path. This keeps direct execution
302
297
  // working even when the script path disappears during process startup.
303
298
  }
304
- return normalizeFileURL(url.pathToFileURL(resolved).href);
299
+ return normalizeFileURL(pathToFileURL(resolved).href);
305
300
  }
306
- return (await realFileURL(scriptPath)) === (await realFileURL(url.fileURLToPath(import.meta.url)));
301
+ return (await realFileURL(scriptPath)) === (await realFileURL(fileURLToPath(import.meta.url)));
307
302
  }
308
303
  invokedFromCLI().then((invoked) => {
309
304
  if (!invoked) {
310
305
  return;
311
306
  }
312
- main().then((code) => {
307
+ runCli(["init", "guardrail", ...process.argv.slice(2)]).then((code) => {
313
308
  process.exitCode = code;
314
309
  }, (err) => {
315
310
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
package/dist/login.d.ts CHANGED
@@ -10,6 +10,17 @@ export type LoginOptions = {
10
10
  noOpen: boolean;
11
11
  force: boolean;
12
12
  };
13
+ export type LoginResult = {
14
+ envPath: string;
15
+ keyMasked: string;
16
+ keyWritten: true;
17
+ environment: DeviceEnvironment;
18
+ tenantId: string;
19
+ tenantUuid: string;
20
+ expiresAt?: string;
21
+ verificationUri: string;
22
+ userCode: string;
23
+ };
13
24
  type FetchInput = string | URL;
14
25
  type FetchInit = {
15
26
  method?: string;
@@ -25,11 +36,13 @@ export type LoginDependencies = {
25
36
  stdout?: Writable;
26
37
  stderr?: Writable;
27
38
  now?: () => number;
39
+ /** When false, suppress human-readable progress lines (for JSON CLI output). */
40
+ humanOutput?: boolean;
28
41
  };
29
42
  export declare function parseArgs(argv: string[]): LoginOptions | "help";
30
43
  export declare function assertGitIgnored(envPath: string, cwd: string): Promise<void>;
31
44
  export declare function assertCanWriteEnvFile(envPath: string, force: boolean): Promise<void>;
32
45
  export declare function writeEnvFile(envPath: string, rawKey: string, force: boolean): Promise<void>;
33
- export declare function runLogin(options: LoginOptions, deps?: LoginDependencies): Promise<number>;
46
+ export declare function runLogin(options: LoginOptions, deps?: LoginDependencies): Promise<LoginResult>;
34
47
  export declare function main(argv?: string[], deps?: LoginDependencies): Promise<number>;
35
48
  export {};
package/dist/login.js CHANGED
@@ -1,10 +1,30 @@
1
1
  #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { runCli } from "./cli/router.js";
2
7
  const DEFAULT_GATEWAY = "https://api.paybond.ai";
3
8
  const DEFAULT_ENV_FILE = ".env.local";
4
9
  const CLIENT_ID = "paybond-kit-cli";
5
10
  const CLIENT_NAME = "Paybond CLI";
6
11
  const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
7
12
  const MIN_POLL_INTERVAL_SECONDS = 1;
13
+ const DEFAULT_DEVICE_EXPIRES_IN_SECONDS = 600;
14
+ const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
15
+ const MS_PER_SECOND = 1_000;
16
+ const DEVICE_EXPIRY_GRACE_MS = MS_PER_SECOND;
17
+ const PROCESS_EXIT_SUCCESS = 0;
18
+ const SPAWN_COMMAND_NOT_FOUND_EXIT_CODE = 127;
19
+ const GIT_CHECK_IGNORE_MATCHED = PROCESS_EXIT_SUCCESS;
20
+ const GIT_CHECK_IGNORE_NOT_MATCHED = 1;
21
+ const MAX_GIT_WORKTREE_WALK_DEPTH = 256;
22
+ const ENV_FILE_MODE = 0o600;
23
+ const GITIGNORE_FILE_MODE = 0o644;
24
+ const API_KEY_MASK_MIN_PARTS = 5;
25
+ const API_KEY_MASK_MIN_ID_LENGTH = 12;
26
+ const API_KEY_MASK_ID_PREFIX_LENGTH = 8;
27
+ const API_KEY_MASK_ID_SUFFIX_LENGTH = 4;
8
28
  class PaybondLoginError extends Error {
9
29
  constructor(message) {
10
30
  super(message);
@@ -127,19 +147,9 @@ function replaceOrAppendEnvValue(existing, rawKey, force) {
127
147
  const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
128
148
  return `${existing}${suffix}${line}\n`;
129
149
  }
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
150
  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
151
  return await new Promise((resolve) => {
142
- const child = childProcess.spawn(command, args, {
152
+ const child = spawn(command, args, {
143
153
  cwd,
144
154
  stdio: ["ignore", "pipe", "pipe"],
145
155
  });
@@ -152,7 +162,7 @@ async function spawnCommand(command, args, cwd) {
152
162
  stderr += String(chunk);
153
163
  });
154
164
  child.on("error", (err) => {
155
- resolve({ code: 127, stdout: "", stderr: err.message });
165
+ resolve({ code: SPAWN_COMMAND_NOT_FOUND_EXIT_CODE, stdout: "", stderr: err.message });
156
166
  });
157
167
  child.on("close", (code) => {
158
168
  resolve({ code, stdout, stderr });
@@ -160,31 +170,55 @@ async function spawnCommand(command, args, cwd) {
160
170
  });
161
171
  }
162
172
  async function resolveEnvFile(envFile, cwd) {
163
- const path = await pathModule();
164
173
  return path.isAbsolute(envFile) ? path.resolve(envFile) : path.resolve(cwd, envFile);
165
174
  }
166
175
  export async function assertGitIgnored(envPath, cwd) {
167
176
  await ensureGitIgnored(envPath, cwd, false);
168
177
  }
178
+ async function inGitWorkTree(start) {
179
+ let current = path.resolve(start);
180
+ for (let depth = 0; depth < MAX_GIT_WORKTREE_WALK_DEPTH; depth += 1) {
181
+ try {
182
+ await fs.access(path.join(current, ".git"));
183
+ return true;
184
+ }
185
+ catch {
186
+ // continue walking parents
187
+ }
188
+ const parent = path.dirname(current);
189
+ if (parent === current) {
190
+ return false;
191
+ }
192
+ current = parent;
193
+ }
194
+ return false;
195
+ }
196
+ function gitMissingForSecretWriteError() {
197
+ return new PaybondLoginError("git is required to verify the env file is ignored before writing secrets; install git or pass --env-file outside the repository");
198
+ }
169
199
  async function ensureGitIgnored(envPath, cwd, autoAddDefaultEnvFile) {
170
- const path = await pathModule();
171
- const fs = await fsModule();
172
200
  const rootResult = await spawnCommand("git", ["rev-parse", "--show-toplevel"], cwd);
173
201
  if (rootResult.code !== 0) {
202
+ if (rootResult.code === SPAWN_COMMAND_NOT_FOUND_EXIT_CODE || /ENOENT/i.test(rootResult.stderr)) {
203
+ if (await inGitWorkTree(cwd)) {
204
+ throw gitMissingForSecretWriteError();
205
+ }
206
+ }
174
207
  return;
175
208
  }
176
209
  const repoRoot = await fs.realpath(path.resolve(rootResult.stdout.trim()));
177
210
  const targetDir = await fs.realpath(path.dirname(path.resolve(envPath)));
178
211
  const target = path.resolve(targetDir, path.basename(envPath));
179
212
  const relativeTarget = path.relative(repoRoot, target);
180
- if (relativeTarget !== "" && (relativeTarget.startsWith("..") || path.isAbsolute(relativeTarget))) {
213
+ if (relativeTarget !== "" &&
214
+ (relativeTarget.startsWith("..") || path.isAbsolute(relativeTarget))) {
181
215
  return;
182
216
  }
183
217
  const ignoreResult = await spawnCommand("git", ["-C", repoRoot, "check-ignore", "--quiet", "--", relativeTarget], cwd);
184
- if (ignoreResult.code === 0) {
218
+ if (ignoreResult.code === GIT_CHECK_IGNORE_MATCHED) {
185
219
  return;
186
220
  }
187
- if (ignoreResult.code === 1) {
221
+ if (ignoreResult.code === GIT_CHECK_IGNORE_NOT_MATCHED) {
188
222
  if (autoAddDefaultEnvFile && relativeTarget === DEFAULT_ENV_FILE) {
189
223
  const gitignorePath = path.resolve(repoRoot, ".gitignore");
190
224
  let existing = "";
@@ -192,14 +226,17 @@ async function ensureGitIgnored(envPath, cwd, autoAddDefaultEnvFile) {
192
226
  existing = await fs.readFile(gitignorePath, "utf8");
193
227
  }
194
228
  catch (err) {
195
- if (!(err && typeof err === "object" && "code" in err && err.code === "ENOENT")) {
229
+ if (!(err &&
230
+ typeof err === "object" &&
231
+ "code" in err &&
232
+ err.code === "ENOENT")) {
196
233
  throw err;
197
234
  }
198
235
  }
199
236
  const suffix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
200
- await fs.writeFile(gitignorePath, `${existing}${suffix}${DEFAULT_ENV_FILE}\n`, { encoding: "utf8", mode: 0o644 });
237
+ await fs.writeFile(gitignorePath, `${existing}${suffix}${DEFAULT_ENV_FILE}\n`, { encoding: "utf8", mode: GITIGNORE_FILE_MODE });
201
238
  const recheck = await spawnCommand("git", ["-C", repoRoot, "check-ignore", "--quiet", "--", relativeTarget], cwd);
202
- if (recheck.code === 0) {
239
+ if (recheck.code === GIT_CHECK_IGNORE_MATCHED) {
203
240
  return;
204
241
  }
205
242
  }
@@ -208,7 +245,6 @@ async function ensureGitIgnored(envPath, cwd, autoAddDefaultEnvFile) {
208
245
  throw new PaybondLoginError(`Unable to verify git ignore status for ${target}: ${ignoreResult.stderr.trim() || "git check-ignore failed"}`);
209
246
  }
210
247
  export async function assertCanWriteEnvFile(envPath, force) {
211
- const fs = await fsModule();
212
248
  try {
213
249
  const existing = await fs.readFile(envPath, "utf8");
214
250
  if (envKeyPattern().test(existing) && !force) {
@@ -216,26 +252,31 @@ export async function assertCanWriteEnvFile(envPath, force) {
216
252
  }
217
253
  }
218
254
  catch (err) {
219
- if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
255
+ if (err &&
256
+ typeof err === "object" &&
257
+ "code" in err &&
258
+ err.code === "ENOENT") {
220
259
  return;
221
260
  }
222
261
  throw err;
223
262
  }
224
263
  }
225
264
  export async function writeEnvFile(envPath, rawKey, force) {
226
- const fs = await fsModule();
227
265
  let existing = "";
228
266
  try {
229
267
  existing = await fs.readFile(envPath, "utf8");
230
268
  }
231
269
  catch (err) {
232
- if (!(err && typeof err === "object" && "code" in err && err.code === "ENOENT")) {
270
+ if (!(err &&
271
+ typeof err === "object" &&
272
+ "code" in err &&
273
+ err.code === "ENOENT")) {
233
274
  throw err;
234
275
  }
235
276
  }
236
277
  const next = replaceOrAppendEnvValue(existing, rawKey, force);
237
- await fs.writeFile(envPath, next, { encoding: "utf8", mode: 0o600 });
238
- await fs.chmod(envPath, 0o600);
278
+ await fs.writeFile(envPath, next, { encoding: "utf8", mode: ENV_FILE_MODE });
279
+ await fs.chmod(envPath, ENV_FILE_MODE);
239
280
  }
240
281
  function gatewayUrl(base, path) {
241
282
  return `${base.trim().replace(/\/+$/, "")}${path}`;
@@ -278,7 +319,8 @@ async function postGatewayJson(fetchFn, gateway, path, body) {
278
319
  if (error) {
279
320
  throw new OAuthPollError({
280
321
  error,
281
- error_description: stringField(parsed, "error_description") || stringField(parsed, "message"),
322
+ error_description: stringField(parsed, "error_description") ||
323
+ stringField(parsed, "message"),
282
324
  interval: numberField(parsed, "interval", 0) || undefined,
283
325
  });
284
326
  }
@@ -290,10 +332,12 @@ function toDeviceStartResponse(body) {
290
332
  user_code: stringField(body, "user_code"),
291
333
  verification_uri: stringField(body, "verification_uri"),
292
334
  verification_uri_complete: stringField(body, "verification_uri_complete") || undefined,
293
- expires_in: numberField(body, "expires_in", 600),
294
- interval: numberField(body, "interval", 5),
335
+ expires_in: numberField(body, "expires_in", DEFAULT_DEVICE_EXPIRES_IN_SECONDS),
336
+ interval: numberField(body, "interval", DEFAULT_DEVICE_POLL_INTERVAL_SECONDS),
295
337
  };
296
- if (!response.device_code || !response.user_code || !response.verification_uri) {
338
+ if (!response.device_code ||
339
+ !response.user_code ||
340
+ !response.verification_uri) {
297
341
  throw new PaybondLoginError("Gateway device start response was missing required fields.");
298
342
  }
299
343
  return response;
@@ -332,11 +376,11 @@ async function startDeviceFlow(fetchFn, gateway, environment) {
332
376
  return toDeviceStartResponse(body);
333
377
  }
334
378
  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;
379
+ let intervalSeconds = Math.max(MIN_POLL_INTERVAL_SECONDS, Math.trunc(start.interval || DEFAULT_DEVICE_POLL_INTERVAL_SECONDS));
380
+ const expiresAtMs = deps.now() + Math.max(1, start.expires_in) * MS_PER_SECOND;
337
381
  for (;;) {
338
- await deps.sleep(intervalSeconds * 1000);
339
- if (deps.now() > expiresAtMs + 1000) {
382
+ await deps.sleep(intervalSeconds * MS_PER_SECOND);
383
+ if (deps.now() > expiresAtMs + DEVICE_EXPIRY_GRACE_MS) {
340
384
  throw new PaybondLoginError("Device authorization expired before approval.");
341
385
  }
342
386
  try {
@@ -372,10 +416,12 @@ async function pollDeviceToken(fetchFn, gateway, environment, start, deps) {
372
416
  function maskAPIKey(rawKey) {
373
417
  const trimmed = rawKey.trim();
374
418
  const parts = trimmed.split("_");
375
- if (parts.length >= 5 && parts[0] === "paybond" && parts[1] === "sk") {
419
+ if (parts.length >= API_KEY_MASK_MIN_PARTS && parts[0] === "paybond" && parts[1] === "sk") {
376
420
  const environment = parts[2];
377
421
  const keyID = parts[3];
378
- const redactedKeyID = keyID.length > 12 ? `${keyID.slice(0, 8)}...${keyID.slice(-4)}` : "redacted";
422
+ const redactedKeyID = keyID.length > API_KEY_MASK_MIN_ID_LENGTH
423
+ ? `${keyID.slice(0, API_KEY_MASK_ID_PREFIX_LENGTH)}...${keyID.slice(-API_KEY_MASK_ID_SUFFIX_LENGTH)}`
424
+ : "redacted";
379
425
  return `paybond_sk_${environment}_${redactedKeyID}`;
380
426
  }
381
427
  return "paybond_sk_...";
@@ -388,11 +434,12 @@ async function defaultOpenBrowser(url) {
388
434
  const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
389
435
  const args = platform === "win32" ? ["/c", "start", "", url] : [url];
390
436
  const result = await spawnCommand(command, args, process.cwd());
391
- return result.code === 0;
437
+ return result.code === PROCESS_EXIT_SUCCESS;
392
438
  }
393
439
  export async function runLogin(options, deps = {}) {
394
440
  const cwd = deps.cwd ?? process.cwd();
395
441
  const stdout = deps.stdout ?? process.stdout;
442
+ const humanOutput = deps.humanOutput ?? true;
396
443
  const fetchFn = deps.fetch ?? fetch;
397
444
  const sleep = deps.sleep ?? defaultSleep;
398
445
  const now = deps.now ?? Date.now;
@@ -402,25 +449,40 @@ export async function runLogin(options, deps = {}) {
402
449
  await ensureGitIgnored(envPath, cwd, options.envFile === DEFAULT_ENV_FILE);
403
450
  const start = await startDeviceFlow(fetchFn, options.gateway, options.environment);
404
451
  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`);
452
+ if (humanOutput) {
453
+ stdout.write(`Paybond ${options.environment} login\n`);
454
+ stdout.write(`Verification URL: ${verificationUrl}\n`);
455
+ stdout.write(`Code: ${start.user_code}\n`);
456
+ if (!options.noOpen) {
457
+ const opened = await openBrowser(verificationUrl);
458
+ if (!opened) {
459
+ stdout.write(`Open the verification URL in a browser to approve this login.\n`);
460
+ }
412
461
  }
462
+ stdout.write(`Waiting for approval...\n`);
413
463
  }
414
- stdout.write(`Waiting for approval...\n`);
415
464
  const token = await pollDeviceToken(fetchFn, options.gateway, options.environment, start, { sleep, now });
416
465
  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`);
466
+ const keyMasked = maskAPIKey(token.access_token);
467
+ if (humanOutput) {
468
+ stdout.write(`Wrote PAYBOND_API_KEY to ${envPath}\n`);
469
+ stdout.write(`Key: ${keyMasked}\n`);
470
+ stdout.write(`Target ${token.environment} tenant: ${token.tenant_id} (${token.tenant_uuid})\n`);
471
+ if (token.expires_at) {
472
+ stdout.write(`This key auto-expires at ${token.expires_at}; re-run paybond login to mint a new one.\n`);
473
+ }
422
474
  }
423
- return 0;
475
+ return {
476
+ envPath,
477
+ keyMasked,
478
+ keyWritten: true,
479
+ environment: options.environment,
480
+ tenantId: token.tenant_id,
481
+ tenantUuid: token.tenant_uuid,
482
+ expiresAt: token.expires_at || undefined,
483
+ verificationUri: verificationUrl,
484
+ userCode: start.user_code,
485
+ };
424
486
  }
425
487
  export async function main(argv = process.argv.slice(2), deps = {}) {
426
488
  let parsed;
@@ -430,7 +492,8 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
430
492
  (deps.stdout ?? process.stdout).write(`${usage()}\n`);
431
493
  return 0;
432
494
  }
433
- return await runLogin(parsed, deps);
495
+ await runLogin(parsed, deps);
496
+ return 0;
434
497
  }
435
498
  catch (err) {
436
499
  (deps.stderr ?? process.stderr).write(`${err instanceof Error ? err.message : String(err)}\n`);
@@ -438,19 +501,15 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
438
501
  }
439
502
  }
440
503
  function normalizeFileURL(url) {
441
- return url.startsWith("file:///var/") ? url.replace("file:///var/", "file:///private/var/") : url;
504
+ return url.startsWith("file:///var/")
505
+ ? url.replace("file:///var/", "file:///private/var/")
506
+ : url;
442
507
  }
443
508
  async function invokedFromCLI() {
444
509
  const scriptPath = process.argv[1];
445
510
  if (!scriptPath) {
446
511
  return false;
447
512
  }
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
513
  async function realFileURL(filePath) {
455
514
  let resolved = path.resolve(filePath);
456
515
  try {
@@ -460,15 +519,16 @@ async function invokedFromCLI() {
460
519
  // If realpath fails, compare the absolute path. This keeps direct execution
461
520
  // working even when the script path disappears during process startup.
462
521
  }
463
- return normalizeFileURL(url.pathToFileURL(resolved).href);
522
+ return normalizeFileURL(pathToFileURL(resolved).href);
464
523
  }
465
- return (await realFileURL(scriptPath)) === (await realFileURL(url.fileURLToPath(import.meta.url)));
524
+ return ((await realFileURL(scriptPath)) ===
525
+ (await realFileURL(fileURLToPath(import.meta.url))));
466
526
  }
467
527
  invokedFromCLI().then((invoked) => {
468
528
  if (!invoked) {
469
529
  return;
470
530
  }
471
- main().then((code) => {
531
+ runCli(["login", ...process.argv.slice(2)]).then((code) => {
472
532
  process.exitCode = code;
473
533
  }, (err) => {
474
534
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);