@hardfin/cli 0.0.1 → 0.0.2-dev.10

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/cli.js ADDED
@@ -0,0 +1,2976 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { Command, Option } from "commander";
4
+ import { z } from "zod";
5
+ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
8
+ import { spawnSync } from "node:child_process";
9
+ import { createServer } from "node:http";
10
+ import { createHash, randomBytes } from "node:crypto";
11
+ //#region src/command/registry.ts
12
+ /** ExitCode is what the process returns, and what an agent branches on. */
13
+ const ExitCode = {
14
+ OK: 0,
15
+ ERROR: 1,
16
+ USAGE: 2,
17
+ NOT_AUTHENTICATED: 4
18
+ };
19
+ /** defineCommand records one command so every surface reads the same declaration. */
20
+ function defineCommand(command) {
21
+ return command;
22
+ }
23
+ //#endregion
24
+ //#region src/config/settings.ts
25
+ /** The API version this build was written against, sent on every request. */
26
+ const API_VERSION = "2026-09-17";
27
+ /** The file a local build reads its overrides from, in the working directory. */
28
+ const CONFIG_FILE = "config.local.json";
29
+ const DEFAULT_API_URL = "https://api.hardfin.com/v2";
30
+ /**
31
+ * The client Hardfin seeded for this CLI. The key is generated from a fixed identifier, so
32
+ * it names the same first-party client in every environment.
33
+ */
34
+ const DEFAULT_CLIENT_ID = "oacl_10e2etq297nhjfhb";
35
+ const DEFAULT_ENV_FILE = ".env";
36
+ const FileSettings = z.strictObject({
37
+ apiUrl: z.string().optional(),
38
+ apiKey: z.string().optional(),
39
+ clientId: z.string().optional(),
40
+ issuerUrl: z.string().optional()
41
+ });
42
+ /** ConfigFailure is a config file that cannot be read or does not match the schema. */
43
+ var ConfigFailure = class extends Error {
44
+ constructor(message) {
45
+ super(message);
46
+ this.name = "ConfigFailure";
47
+ }
48
+ };
49
+ /** toSettings resolves what this invocation talks to, and where each value came from. */
50
+ function toSettings(flags = {}, directory = process.cwd()) {
51
+ const fromEnvFile = loadEnvFile(directory);
52
+ const file = toFileSettings(directory);
53
+ const sources = {};
54
+ const pick = (key, flag, variable, fromFile, fallback) => {
55
+ const [value, source] = toLayer(flag, process.env[variable], fromEnvFile.has(variable), fromFile, fallback);
56
+ sources[key] = source;
57
+ return value;
58
+ };
59
+ const apiUrl = toTrimmedUrl(pick("apiUrl", flags.apiUrl, "HARDFIN_API_URL", file.apiUrl, DEFAULT_API_URL) ?? DEFAULT_API_URL);
60
+ return {
61
+ settings: {
62
+ apiUrl,
63
+ apiKey: pick("apiKey", flags.apiKey, "HARDFIN_API_KEY", file.apiKey),
64
+ clientId: pick("clientId", flags.clientId, "HARDFIN_CLIENT_ID", file.clientId, DEFAULT_CLIENT_ID) ?? DEFAULT_CLIENT_ID,
65
+ issuerUrl: pick("issuerUrl", flags.issuerUrl, "HARDFIN_ISSUER_URL", file.issuerUrl, toOrigin(apiUrl)) ?? ""
66
+ },
67
+ sources
68
+ };
69
+ }
70
+ function toLayer(flag, environment, isFromEnvFile, file, fallback) {
71
+ if (flag) return [flag, "flag"];
72
+ if (environment) return [environment, isFromEnvFile ? "env file" : "environment"];
73
+ if (file) return [file, "config file"];
74
+ return [fallback, "default"];
75
+ }
76
+ /** toFileSettings reads the local override file, which a local build is expected to have. */
77
+ function toFileSettings(directory) {
78
+ const path = resolve(directory, CONFIG_FILE);
79
+ if (!existsSync(path)) return {};
80
+ let parsed;
81
+ try {
82
+ parsed = JSON.parse(readFileSync(path, "utf8"));
83
+ } catch {
84
+ throw new ConfigFailure(`${CONFIG_FILE} does not hold JSON`);
85
+ }
86
+ const result = FileSettings.safeParse(parsed);
87
+ if (!result.success) {
88
+ const issue = result.error.issues[0];
89
+ throw new ConfigFailure(`${CONFIG_FILE} is not valid: ${issue?.path.join(".") || "root"} ${issue?.message ?? ""}`.trim());
90
+ }
91
+ return result.data;
92
+ }
93
+ /**
94
+ * loadEnvFile reads a .env beside the command, so a local build needs no exports, and
95
+ * answers which variables it supplied. Node leaves an exported variable alone, so a
96
+ * shell export still wins over the file.
97
+ */
98
+ function loadEnvFile(directory) {
99
+ const path = process.env["HARDFIN_ENV_FILE"] ?? resolve(directory, DEFAULT_ENV_FILE);
100
+ if (!existsSync(path)) return /* @__PURE__ */ new Set();
101
+ const before = new Set(Object.keys(process.env));
102
+ try {
103
+ process.loadEnvFile(path);
104
+ } catch {
105
+ throw new ConfigFailure(`${path} cannot be read as an env file`);
106
+ }
107
+ return new Set(Object.keys(process.env).filter((name) => !before.has(name)));
108
+ }
109
+ function toTrimmedUrl(url) {
110
+ return url.replace(/\/+$/, "");
111
+ }
112
+ /** toOrigin drops the API's path, because the authorization server sits at the host root. */
113
+ function toOrigin(apiUrl) {
114
+ try {
115
+ return new URL(apiUrl).origin;
116
+ } catch {
117
+ return apiUrl;
118
+ }
119
+ }
120
+ //#endregion
121
+ //#region src/output/writer.ts
122
+ /** writeData prints a command's result on stdout. */
123
+ function writeData(value) {
124
+ if (typeof value === "string") {
125
+ process.stdout.write(value.endsWith("\n") ? value : `${value}\n`);
126
+ return;
127
+ }
128
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
129
+ }
130
+ /** writeFailure prints why a command failed on stderr, as text or as JSON. */
131
+ function writeFailure(message, isJSON, errors, requestId) {
132
+ if (!isJSON) {
133
+ process.stderr.write(`error: ${message}\n`);
134
+ for (const entry of errors?.slice(1) ?? []) process.stderr.write(` ${entry.error} (${entry.statusCode})\n`);
135
+ if (requestId) process.stderr.write(` request ${requestId}\n`);
136
+ return;
137
+ }
138
+ const payload = {
139
+ errors: errors ?? [{
140
+ error: message,
141
+ statusCode: 0
142
+ }],
143
+ requestId: requestId ?? null
144
+ };
145
+ process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
146
+ }
147
+ /** isJSONOutput decides whether this invocation prints machine-readable output. */
148
+ function isJSONOutput(flags) {
149
+ if (flags["json"] === true) return true;
150
+ return !process.stdout.isTTY;
151
+ }
152
+ /** version is what this build reports, read from the published package manifest. */
153
+ const version$1 = createRequire(import.meta.url)("../package.json").version;
154
+ //#endregion
155
+ //#region src/command/agent-guide.ts
156
+ const agentGuideCommand = defineCommand({
157
+ name: "agent-guide",
158
+ summary: "Print instructions an agent can follow to use this CLI",
159
+ description: "Writes Markdown describing every command, its flags, and its exit codes. Save it as a skill file, an AGENTS.md section, or paste it into a system prompt.",
160
+ arguments: [],
161
+ flags: [{
162
+ name: "json",
163
+ description: "Print the command registry as JSON instead of Markdown",
164
+ schema: z.boolean().optional()
165
+ }],
166
+ examples: [{
167
+ description: "Write a skill file",
168
+ command: "hardfin agent-guide > SKILL.md"
169
+ }, {
170
+ description: "Inspect the registry",
171
+ command: "hardfin agent-guide --json"
172
+ }],
173
+ run: runAgentGuide
174
+ });
175
+ /** toGuide renders the registry as the Markdown an agent reads. */
176
+ function toGuide(commands, version) {
177
+ const lines = [
178
+ "# Hardfin CLI",
179
+ "",
180
+ `The \`hardfin\` command calls the Hardfin API. This guide describes version ${version} of the CLI, which speaks API version ${API_VERSION}.`,
181
+ "",
182
+ "## Before calling",
183
+ "",
184
+ "- Authenticate by setting `HARDFIN_API_KEY`, or by running `hardfin login`",
185
+ "- Every command prints JSON on stdout and diagnostics on stderr",
186
+ "- Output is JSON whenever stdout is not a terminal, so no flag is needed when calling from code",
187
+ "",
188
+ "## Exit codes",
189
+ "",
190
+ "| Code | Means |",
191
+ "| --- | --- |",
192
+ "| 0 | The command succeeded |",
193
+ "| 1 | The command failed |",
194
+ "| 2 | The command was called wrongly |",
195
+ "| 4 | No usable credential |",
196
+ "",
197
+ "## Commands",
198
+ ""
199
+ ];
200
+ for (const command of commands) {
201
+ lines.push(`### \`hardfin ${command.name}\``, "", command.description ?? command.summary, "");
202
+ if (command.arguments.length > 0) {
203
+ lines.push("| Argument | Required | Holds |", "| --- | --- | --- |");
204
+ for (const argument of command.arguments) lines.push(`| \`${argument.name}\` | ${argument.required ? "yes" : "no"} | ${argument.description} |`);
205
+ lines.push("");
206
+ }
207
+ if (command.flags.length > 0) {
208
+ lines.push("| Flag | Takes | Does |", "| --- | --- | --- |");
209
+ for (const flag of command.flags) {
210
+ const name = flag.short ? `-${flag.short}, --${flag.name}` : `--${flag.name}`;
211
+ lines.push(`| \`${name}\` | ${flag.valueName ?? "nothing"} | ${flag.description} |`);
212
+ }
213
+ lines.push("");
214
+ }
215
+ for (const example of command.examples) lines.push(`${example.description}:`, "", "```sh", example.command, "```", "");
216
+ }
217
+ return lines.join("\n");
218
+ }
219
+ async function runAgentGuide(input) {
220
+ if (input.flags["json"] === true) {
221
+ writeData(input.commands.map(toSummary));
222
+ return ExitCode.OK;
223
+ }
224
+ writeData(toGuide(input.commands, version$1));
225
+ return ExitCode.OK;
226
+ }
227
+ function toSummary(command) {
228
+ return {
229
+ name: command.name,
230
+ summary: command.summary,
231
+ description: command.description ?? command.summary,
232
+ arguments: command.arguments,
233
+ flags: command.flags.map((flag) => ({
234
+ name: flag.name,
235
+ short: flag.short ?? null,
236
+ description: flag.description,
237
+ valueName: flag.valueName ?? null,
238
+ repeatable: flag.repeatable ?? false
239
+ })),
240
+ examples: command.examples
241
+ };
242
+ }
243
+ //#endregion
244
+ //#region src/auth/metadata.ts
245
+ const METADATA_PATH = "/.well-known/oauth-authorization-server";
246
+ const AuthorizationServerMetadata = z.looseObject({
247
+ issuer: z.string(),
248
+ authorization_endpoint: z.string(),
249
+ token_endpoint: z.string(),
250
+ revocation_endpoint: z.string().optional(),
251
+ device_authorization_endpoint: z.string().optional(),
252
+ scopes_supported: z.array(z.string()).optional(),
253
+ grant_types_supported: z.array(z.string()).optional(),
254
+ code_challenge_methods_supported: z.array(z.string()).optional()
255
+ });
256
+ /** DiscoveryFailure is an authorization server that cannot be read or does not describe itself. */
257
+ var DiscoveryFailure = class extends Error {
258
+ constructor(message) {
259
+ super(message);
260
+ this.name = "DiscoveryFailure";
261
+ }
262
+ };
263
+ /** toMetadata reads what an authorization server says about itself. */
264
+ async function toMetadata(issuer) {
265
+ const url = `${issuer.replace(/\/+$/, "")}${METADATA_PATH}`;
266
+ let response;
267
+ try {
268
+ response = await fetch(url, { headers: { Accept: "application/json" } });
269
+ } catch (error) {
270
+ throw new DiscoveryFailure(`${url} could not be reached: ${error instanceof Error ? error.message : String(error)}`);
271
+ }
272
+ if (!response.ok) throw new DiscoveryFailure(`${url} answered ${response.status}, so this host publishes no authorization server`);
273
+ const parsed = AuthorizationServerMetadata.safeParse(await response.json().catch(() => void 0));
274
+ if (!parsed.success) throw new DiscoveryFailure(`${url} does not describe an authorization server`);
275
+ if (parsed.data.issuer.replace(/\/+$/, "") !== issuer.replace(/\/+$/, "")) throw new DiscoveryFailure(`${url} names issuer ${parsed.data.issuer}, which is not the host it was read from`);
276
+ return parsed.data;
277
+ }
278
+ //#endregion
279
+ //#region src/auth/grant.ts
280
+ const TokenResponse = z.looseObject({
281
+ access_token: z.string(),
282
+ token_type: z.string(),
283
+ expires_in: z.number().optional(),
284
+ refresh_token: z.string().optional(),
285
+ scope: z.string().optional()
286
+ });
287
+ /** GrantFailure is a token request the authorization server refused. */
288
+ var GrantFailure = class extends Error {
289
+ code;
290
+ constructor(code, description) {
291
+ super(description ? `${code}: ${description}` : code);
292
+ this.name = "GrantFailure";
293
+ this.code = code;
294
+ }
295
+ };
296
+ /** toTokensFromCode trades an authorization code for tokens. */
297
+ async function toTokensFromCode(tokenUrl, clientId, code, redirectUri, pkce) {
298
+ return await toTokens(tokenUrl, {
299
+ grant_type: "authorization_code",
300
+ client_id: clientId,
301
+ code,
302
+ redirect_uri: redirectUri,
303
+ code_verifier: pkce.verifier
304
+ });
305
+ }
306
+ /** toTokensFromRefresh trades a refresh token for a fresh pair. */
307
+ async function toTokensFromRefresh(tokenUrl, clientId, refreshToken) {
308
+ return await toTokens(tokenUrl, {
309
+ grant_type: "refresh_token",
310
+ client_id: clientId,
311
+ refresh_token: refreshToken
312
+ });
313
+ }
314
+ /** revoke tells the server to forget a token, so signing out reaches every machine. */
315
+ async function revoke(revocationUrl, clientId, token) {
316
+ await fetch(revocationUrl, {
317
+ method: "POST",
318
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
319
+ body: new URLSearchParams({
320
+ client_id: clientId,
321
+ token,
322
+ token_type_hint: "refresh_token"
323
+ })
324
+ });
325
+ }
326
+ /** toTokens asks the token endpoint, which answers a flat RFC 6749 body, not the envelope. */
327
+ async function toTokens(url, form) {
328
+ const response = await fetch(url, {
329
+ method: "POST",
330
+ headers: {
331
+ "Content-Type": "application/x-www-form-urlencoded",
332
+ Accept: "application/json"
333
+ },
334
+ body: new URLSearchParams(form)
335
+ });
336
+ const body = await response.json().catch(() => void 0);
337
+ if (!response.ok) throw new GrantFailure(String(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : String(body["error_description"]));
338
+ const parsed = TokenResponse.safeParse(body);
339
+ if (!parsed.success) throw new GrantFailure("invalid_response", "the token endpoint did not answer with a token");
340
+ return {
341
+ accessToken: parsed.data.access_token,
342
+ refreshToken: parsed.data.refresh_token,
343
+ scope: parsed.data.scope,
344
+ expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
345
+ };
346
+ }
347
+ //#endregion
348
+ //#region src/credential/store.ts
349
+ const load = createRequire(import.meta.url);
350
+ /** The service a keyring entry is filed under, alongside the issuer it belongs to. */
351
+ const SERVICE = "hardfin-cli";
352
+ const FILE_MODE = 384;
353
+ const DIRECTORY_MODE$1 = 448;
354
+ /** toCredentialPath names the file the fallback keeps refresh tokens in. */
355
+ function toCredentialPath() {
356
+ const base = process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state");
357
+ return join(base, "hardfin", "credentials.json");
358
+ }
359
+ /**
360
+ * keep writes a refresh token for one issuer. Only the refresh token is stored, so a stolen
361
+ * entry is revocable, and access tokens live in the process that fetched them.
362
+ *
363
+ * A rotation keeps the date of the original sign in, because that is what the server's
364
+ * family lifetime runs from. Callers hold the credential lock, which is what makes a write
365
+ * from another process merge rather than disappear.
366
+ */
367
+ function keep(issuer, refreshToken) {
368
+ const now = (/* @__PURE__ */ new Date()).toISOString();
369
+ const record = {
370
+ refreshToken,
371
+ signedInAt: toCredential(issuer)?.signedInAt ?? now,
372
+ renewedAt: now
373
+ };
374
+ const keyring = toKeyring(issuer);
375
+ if (keyring) try {
376
+ keyring.setPassword(JSON.stringify(record));
377
+ return "keyring";
378
+ } catch {}
379
+ writeFile({
380
+ ...readFile(),
381
+ [issuer]: record
382
+ });
383
+ return "file";
384
+ }
385
+ /** toCredential reads what is held for an issuer, and where it was held. */
386
+ function toCredential(issuer) {
387
+ const keyring = toKeyring(issuer);
388
+ if (keyring) try {
389
+ const held = keyring.getPassword();
390
+ if (held) return {
391
+ ...toRecord(held),
392
+ backend: "keyring"
393
+ };
394
+ } catch {}
395
+ const held = readFile()[issuer];
396
+ return held === void 0 ? void 0 : {
397
+ ...held,
398
+ backend: "file",
399
+ path: toCredentialPath()
400
+ };
401
+ }
402
+ /** toRecord reads a stored entry, which older versions wrote as the bare token. */
403
+ function toRecord(held) {
404
+ if (!held.startsWith("{")) return { refreshToken: held };
405
+ try {
406
+ return JSON.parse(held);
407
+ } catch {
408
+ return { refreshToken: held };
409
+ }
410
+ }
411
+ /** forget removes whatever is held for an issuer, in both places. */
412
+ function forget(issuer) {
413
+ const keyring = toKeyring(issuer);
414
+ if (keyring) try {
415
+ keyring.deletePassword();
416
+ } catch {}
417
+ const held = readFile();
418
+ if (held[issuer] === void 0) return;
419
+ delete held[issuer];
420
+ if (Object.keys(held).length === 0) {
421
+ rmSync(toCredentialPath(), { force: true });
422
+ return;
423
+ }
424
+ writeFile(held);
425
+ }
426
+ /** toKeyring opens the OS keyring, or answers undefined where the platform has none. */
427
+ function toKeyring(issuer) {
428
+ if (process.env["HARDFIN_CREDENTIAL_STORE"] === "file") return;
429
+ try {
430
+ const { Entry } = load("@napi-rs/keyring");
431
+ return new Entry(SERVICE, issuer);
432
+ } catch {
433
+ return;
434
+ }
435
+ }
436
+ function readFile() {
437
+ try {
438
+ const parsed = JSON.parse(readFileSync(toCredentialPath(), "utf8"));
439
+ if (typeof parsed !== "object" || parsed === null) return {};
440
+ return Object.fromEntries(Object.entries(parsed).map(([issuer, held]) => [issuer, typeof held === "string" ? { refreshToken: held } : held]));
441
+ } catch {
442
+ return {};
443
+ }
444
+ }
445
+ function writeFile(held) {
446
+ const path = toCredentialPath();
447
+ mkdirSync(dirname(path), {
448
+ recursive: true,
449
+ mode: DIRECTORY_MODE$1
450
+ });
451
+ const pending = `${path}.${process.pid}.tmp`;
452
+ writeFileSync(pending, `${JSON.stringify(held, null, 2)}\n`, { mode: FILE_MODE });
453
+ chmodSync(pending, FILE_MODE);
454
+ renameSync(pending, path);
455
+ }
456
+ //#endregion
457
+ //#region src/credential/lock.ts
458
+ /** A lock held longer than this belongs to a process that died, so it is taken. */
459
+ const STALE_MS = 3e4;
460
+ const WAIT_MS$1 = 1e4;
461
+ const RETRY_MS = 25;
462
+ const DIRECTORY_MODE = 448;
463
+ /** toLockPath names the lock every process coordinates credential writes through. */
464
+ function toLockPath() {
465
+ return join(dirname(toCredentialPath()), "credentials.lock");
466
+ }
467
+ /** tryAcquire takes the lock, or reports that another process holds it. */
468
+ function tryAcquire() {
469
+ const path = toLockPath();
470
+ mkdirSync(dirname(path), {
471
+ recursive: true,
472
+ mode: DIRECTORY_MODE
473
+ });
474
+ try {
475
+ const handle = openSync(path, "wx");
476
+ writeSync(handle, String(process.pid));
477
+ closeSync(handle);
478
+ return true;
479
+ } catch {
480
+ return isStale(path) ? steal(path) : false;
481
+ }
482
+ }
483
+ /** release lets the next process in. */
484
+ function release$1() {
485
+ rmSync(toLockPath(), { force: true });
486
+ }
487
+ /**
488
+ * withLock runs one credential change at a time across every process. Rotation is why it
489
+ * exists: the server kills a token family when a refresh token is presented twice, so two
490
+ * commands refreshing at once would sign the person out.
491
+ */
492
+ async function withLock(work) {
493
+ const deadline = Date.now() + WAIT_MS$1;
494
+ while (!tryAcquire()) {
495
+ if (Date.now() > deadline) throw new Error("another hardfin command is holding the credential lock");
496
+ await new Promise((resolve) => setTimeout(resolve, RETRY_MS));
497
+ }
498
+ try {
499
+ return await work();
500
+ } finally {
501
+ release$1();
502
+ }
503
+ }
504
+ function isStale(path) {
505
+ try {
506
+ return Date.now() - statSync(path).mtimeMs > STALE_MS;
507
+ } catch {
508
+ return true;
509
+ }
510
+ }
511
+ function steal(path) {
512
+ rmSync(path, { force: true });
513
+ return tryAcquire();
514
+ }
515
+ //#endregion
516
+ //#region src/auth/session.ts
517
+ /** A token this close to expiry is refreshed rather than used. */
518
+ const EXPIRY_MARGIN_MS = 3e4;
519
+ /** NoCredential is a caller with neither an API key nor a stored sign-in. */
520
+ var NoCredential = class extends Error {
521
+ constructor(message) {
522
+ super(message);
523
+ this.name = "NoCredential";
524
+ }
525
+ };
526
+ /** Access tokens are held for the life of the process, never written anywhere. */
527
+ const held = /* @__PURE__ */ new Map();
528
+ /**
529
+ * toCredential answers what this invocation authenticates with. An API key wins, because an
530
+ * unattended caller sets it deliberately, and otherwise the stored refresh token buys an
531
+ * access token that lives only in memory.
532
+ */
533
+ async function toRequestCredential(settings) {
534
+ if (settings.apiKey) return {
535
+ header: "X-API-Key",
536
+ value: settings.apiKey,
537
+ kind: "api key"
538
+ };
539
+ const stored = toCredential(settings.issuerUrl);
540
+ if (!stored) throw new NoCredential("not authenticated. Run hardfin login, or set HARDFIN_API_KEY");
541
+ return {
542
+ header: "Authorization",
543
+ value: `Bearer ${(await toAccessToken(settings, stored.refreshToken)).accessToken}`,
544
+ kind: "access token",
545
+ backend: stored.backend
546
+ };
547
+ }
548
+ /** toAccessToken reuses the token this process already holds until it is near expiry. */
549
+ async function toAccessToken(settings, refreshToken) {
550
+ const current = held.get(settings.issuerUrl);
551
+ if (current && (current.expiresAt ?? 0) - EXPIRY_MARGIN_MS > Date.now()) return current;
552
+ const metadata = await toMetadata(settings.issuerUrl);
553
+ return await withLock(async () => {
554
+ const latest = toCredential(settings.issuerUrl)?.refreshToken ?? refreshToken;
555
+ try {
556
+ const tokens = await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest);
557
+ held.set(settings.issuerUrl, tokens);
558
+ if (tokens.refreshToken && tokens.refreshToken !== latest) keep(settings.issuerUrl, tokens.refreshToken);
559
+ return tokens;
560
+ } catch (error) {
561
+ if (error instanceof GrantFailure && isDead(error.code)) {
562
+ forget(settings.issuerUrl);
563
+ throw new NoCredential(`the sign in for ${settings.issuerUrl} is no longer valid, so it was removed. Run hardfin login`);
564
+ }
565
+ throw error;
566
+ }
567
+ });
568
+ }
569
+ /** Codes the authorization server uses when a refresh token can never work again. */
570
+ const DEAD_GRANT_CODES = /* @__PURE__ */ new Set([
571
+ "invalid_grant",
572
+ "invalid_client",
573
+ "unauthorized_client"
574
+ ]);
575
+ function isDead(code) {
576
+ return DEAD_GRANT_CODES.has(code);
577
+ }
578
+ /** forgetHeldTokens drops the access tokens this process is holding. */
579
+ function forgetHeldTokens() {
580
+ held.clear();
581
+ }
582
+ //#endregion
583
+ //#region src/http/client.ts
584
+ /** RequestFailure is a call the API refused, carrying what the envelope said. */
585
+ var RequestFailure = class extends Error {
586
+ status;
587
+ errors;
588
+ requestId;
589
+ constructor(status, errors, requestId) {
590
+ super(errors[0]?.error ?? `the request failed with status ${status}`);
591
+ this.name = "RequestFailure";
592
+ this.status = status;
593
+ this.errors = errors;
594
+ this.requestId = requestId;
595
+ }
596
+ };
597
+ /** request calls one /v2 endpoint and returns the envelope it answered with. */
598
+ async function request(options) {
599
+ const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
600
+ if (options.query) url.search = options.query.toString();
601
+ const headers = {
602
+ [options.credential.header]: options.credential.value,
603
+ "X-API-Version": API_VERSION,
604
+ Accept: "application/json"
605
+ };
606
+ if (options.body !== void 0) headers["Content-Type"] = "application/json";
607
+ const response = await fetch(url, {
608
+ method: options.method,
609
+ headers,
610
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
611
+ });
612
+ const envelope = toEnvelope(await response.text());
613
+ if (!response.ok && envelope === void 0) throw new RequestFailure(response.status, [{
614
+ error: toStatusMessage(response.status),
615
+ statusCode: response.status
616
+ }]);
617
+ if (!response.ok) {
618
+ const errors = envelope?.metadata?.errors ?? [];
619
+ throw new RequestFailure(response.status, errors.length > 0 ? errors : [{
620
+ error: toStatusMessage(response.status),
621
+ statusCode: response.status
622
+ }], envelope?.metadata?.requestId);
623
+ }
624
+ return envelope ?? { data: null };
625
+ }
626
+ function toLeadingSlash(path) {
627
+ return path.startsWith("/") ? path : `/${path}`;
628
+ }
629
+ function toEnvelope(text) {
630
+ if (text.trim() === "") return;
631
+ try {
632
+ return JSON.parse(text);
633
+ } catch {
634
+ return;
635
+ }
636
+ }
637
+ function toStatusMessage(status) {
638
+ if (status === 401) return "the credential is missing or is not valid";
639
+ if (status === 403) return "the organization is deactivated, or its access has ended";
640
+ return `the request failed with status ${status}`;
641
+ }
642
+ const apiCommand = defineCommand({
643
+ name: "api",
644
+ summary: "Call a Hardfin API endpoint and print what it answers",
645
+ description: "Reaches every endpoint the API publishes. The path is everything after /v2, and the response envelope is printed as it arrived.",
646
+ arguments: [{
647
+ name: "path",
648
+ description: "The endpoint path, such as /customer or /item/item_V1StGXR8Z5jdHi6B",
649
+ required: true
650
+ }],
651
+ flags: [
652
+ {
653
+ name: "method",
654
+ short: "X",
655
+ description: "The HTTP method to use",
656
+ valueName: "method",
657
+ schema: z.enum([
658
+ "GET",
659
+ "POST",
660
+ "PATCH",
661
+ "PUT",
662
+ "DELETE"
663
+ ]),
664
+ defaultValue: "GET"
665
+ },
666
+ {
667
+ name: "field",
668
+ short: "f",
669
+ description: "A query parameter as key=value, repeatable",
670
+ valueName: "key=value",
671
+ repeatable: true,
672
+ schema: z.array(z.string()).default([])
673
+ },
674
+ {
675
+ name: "input",
676
+ description: "A file holding the JSON request body, or - for stdin",
677
+ valueName: "file",
678
+ schema: z.string().optional()
679
+ },
680
+ {
681
+ name: "json",
682
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
683
+ schema: z.boolean().optional()
684
+ }
685
+ ],
686
+ examples: [
687
+ {
688
+ description: "List customers",
689
+ command: "hardfin api /customer"
690
+ },
691
+ {
692
+ description: "Take the second page",
693
+ command: "hardfin api /customer -f page=2 -f limit=50"
694
+ },
695
+ {
696
+ description: "Change an item",
697
+ command: "hardfin api -X PATCH /item/item_V1StGXR8Z5jdHi6B --input body.json"
698
+ }
699
+ ],
700
+ run: runApi
701
+ });
702
+ async function runApi(input) {
703
+ const path = input.args[0];
704
+ if (!path) {
705
+ writeFailure("a path is required, such as /customer", input.isJSON);
706
+ return ExitCode.USAGE;
707
+ }
708
+ const query = toQuery$1(input.flags["field"]);
709
+ if (query === void 0) {
710
+ writeFailure("each --field is key=value, such as -f limit=50", input.isJSON);
711
+ return ExitCode.USAGE;
712
+ }
713
+ let body;
714
+ if (typeof input.flags["input"] === "string") {
715
+ body = toBody$1(input.flags["input"]);
716
+ if (body === void 0) {
717
+ writeFailure(`${input.flags["input"]} does not hold JSON`, input.isJSON);
718
+ return ExitCode.USAGE;
719
+ }
720
+ }
721
+ try {
722
+ writeData((await request({
723
+ apiUrl: input.resolved.settings.apiUrl,
724
+ credential: await toRequestCredential(input.resolved.settings),
725
+ method: String(input.flags["method"] ?? "GET").toUpperCase(),
726
+ path,
727
+ query,
728
+ body
729
+ })).data);
730
+ return ExitCode.OK;
731
+ } catch (error) {
732
+ if (error instanceof NoCredential) {
733
+ writeFailure(error.message, input.isJSON);
734
+ return ExitCode.NOT_AUTHENTICATED;
735
+ }
736
+ if (error instanceof RequestFailure) {
737
+ writeFailure(error.message, input.isJSON, error.errors, error.requestId);
738
+ return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
739
+ }
740
+ writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
741
+ return ExitCode.ERROR;
742
+ }
743
+ }
744
+ /** toQuery folds the repeated --field flags into query parameters. */
745
+ function toQuery$1(fields) {
746
+ const query = new URLSearchParams();
747
+ for (const field of Array.isArray(fields) ? fields : []) {
748
+ const split = field.indexOf("=");
749
+ if (split < 1) return;
750
+ query.append(field.slice(0, split), field.slice(split + 1));
751
+ }
752
+ return query;
753
+ }
754
+ function toBody$1(source) {
755
+ const text = source === "-" ? readFileSync(0, "utf8") : readFileSync(source, "utf8");
756
+ try {
757
+ return JSON.parse(text);
758
+ } catch {
759
+ return;
760
+ }
761
+ }
762
+ //#endregion
763
+ //#region src/command/config.ts
764
+ const configCommand = defineCommand({
765
+ name: "config",
766
+ summary: "Print what this invocation talks to, and where each value came from",
767
+ description: `Resolves the API and authentication endpoints from the flags, the environment, a .env file, and ${CONFIG_FILE} in the working directory. Use it when a local build reaches the wrong server.`,
768
+ arguments: [],
769
+ flags: [{
770
+ name: "json",
771
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
772
+ schema: z.boolean()
773
+ }],
774
+ examples: [{
775
+ description: "See what a local build is pointed at",
776
+ command: "hardfin config"
777
+ }, {
778
+ description: "Read one value from a script",
779
+ command: "hardfin config --json | jq -r .apiUrl"
780
+ }],
781
+ run: runConfig
782
+ });
783
+ /** toReport pairs each setting with the layer that supplied it. */
784
+ function toReport(resolved) {
785
+ const report = { apiVersion: API_VERSION };
786
+ for (const [key, value] of Object.entries(resolved.settings)) {
787
+ const source = resolved.sources[key];
788
+ report[key] = key === "apiKey" ? {
789
+ set: value !== void 0,
790
+ from: source
791
+ } : {
792
+ value: value ?? null,
793
+ from: source
794
+ };
795
+ }
796
+ return report;
797
+ }
798
+ async function runConfig(input) {
799
+ const report = toReport(input.resolved);
800
+ if (input.isJSON) {
801
+ writeData(report);
802
+ return ExitCode.OK;
803
+ }
804
+ writeData(Object.entries(report).map(([key, entry]) => {
805
+ if (typeof entry !== "object" || entry === null) return `${key.padEnd(14)} ${String(entry)}`;
806
+ const holder = entry;
807
+ const shown = holder.value ?? (holder.set ? "set" : "not set");
808
+ return `${key.padEnd(14)} ${shown} (${holder.from})`;
809
+ }).join("\n"));
810
+ return ExitCode.OK;
811
+ }
812
+ //#endregion
813
+ //#region src/auth/browser.ts
814
+ /** isWSL reports whether this Linux is running under Windows. */
815
+ function isWSL() {
816
+ return existsSync("/proc/sys/fs/binfmt_misc/WSLInterop");
817
+ }
818
+ /**
819
+ * toOpeners lists the ways to open a URL here, best first. WSL needs Windows to do it,
820
+ * because the browser the person is looking at is the Windows one, and xdg-open reaches
821
+ * nothing outside the distribution.
822
+ */
823
+ function toOpeners(url) {
824
+ if (process.platform === "darwin") return [["open", [url]]];
825
+ if (process.platform === "win32") return [["cmd", [
826
+ "/c",
827
+ "start",
828
+ "",
829
+ url
830
+ ]]];
831
+ if (isWSL()) return [
832
+ ["wslview", [url]],
833
+ ["powershell.exe", [
834
+ "-NoProfile",
835
+ "-NonInteractive",
836
+ "-Command",
837
+ "Start-Process",
838
+ `'${url}'`
839
+ ]],
840
+ ["explorer.exe", [url]]
841
+ ];
842
+ return [["xdg-open", [url]]];
843
+ }
844
+ /** openBrowser asks the desktop to show a URL, and reports whether anything took it. */
845
+ function openBrowser(url) {
846
+ for (const [command, args] of toOpeners(url)) if (spawnSync(command, args, {
847
+ stdio: "ignore",
848
+ cwd: command.endsWith(".exe") ? "/mnt/c" : void 0,
849
+ timeout: 1e4
850
+ }).error === void 0) return true;
851
+ return false;
852
+ }
853
+ //#endregion
854
+ //#region src/auth/device.ts
855
+ const DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
856
+ /** The interval a server gives is in seconds, and five more are added on a slow_down. */
857
+ const DEFAULT_INTERVAL_SECONDS = 5;
858
+ const SLOW_DOWN_SECONDS = 5;
859
+ const DeviceResponse = z.looseObject({
860
+ device_code: z.string(),
861
+ user_code: z.string(),
862
+ verification_uri: z.string(),
863
+ verification_uri_complete: z.string().optional(),
864
+ expires_in: z.number(),
865
+ interval: z.number().optional()
866
+ });
867
+ /** toDeviceAuthorization asks for a code a person can type on another machine. */
868
+ async function toDeviceAuthorization(endpoint, clientId, scope) {
869
+ const response = await fetch(endpoint, {
870
+ method: "POST",
871
+ headers: {
872
+ "Content-Type": "application/x-www-form-urlencoded",
873
+ Accept: "application/json"
874
+ },
875
+ body: new URLSearchParams({
876
+ client_id: clientId,
877
+ scope
878
+ })
879
+ });
880
+ const body = await response.json().catch(() => void 0);
881
+ if (!response.ok) throw new GrantFailure(String(body?.["error"] ?? `http_${response.status}`), body?.["error_description"] === void 0 ? void 0 : String(body["error_description"]));
882
+ const parsed = DeviceResponse.safeParse(body);
883
+ if (!parsed.success) throw new GrantFailure("invalid_response", "the device endpoint did not answer with a code");
884
+ return {
885
+ deviceCode: parsed.data.device_code,
886
+ userCode: parsed.data.user_code,
887
+ verificationUri: parsed.data.verification_uri,
888
+ verificationUriComplete: parsed.data.verification_uri_complete,
889
+ expiresAt: Date.now() + parsed.data.expires_in * 1e3,
890
+ intervalMs: (parsed.data.interval ?? DEFAULT_INTERVAL_SECONDS) * 1e3
891
+ };
892
+ }
893
+ /**
894
+ * toTokensFromDevice waits for the person to approve on another machine. The server answers
895
+ * authorization_pending until they do, and slow_down when this asked too often.
896
+ */
897
+ async function toTokensFromDevice(tokenUrl, clientId, device, sleep = toSleep) {
898
+ let intervalMs = device.intervalMs;
899
+ for (;;) {
900
+ if (Date.now() > device.expiresAt) throw new GrantFailure("expired_token", "the code expired before it was approved");
901
+ await sleep(intervalMs);
902
+ try {
903
+ return await toTokens(tokenUrl, {
904
+ grant_type: DEVICE_GRANT,
905
+ client_id: clientId,
906
+ device_code: device.deviceCode
907
+ });
908
+ } catch (error) {
909
+ if (!(error instanceof GrantFailure)) throw error;
910
+ if (error.code === "slow_down") {
911
+ intervalMs += SLOW_DOWN_SECONDS * 1e3;
912
+ continue;
913
+ }
914
+ if (error.code !== "authorization_pending") throw error;
915
+ }
916
+ }
917
+ }
918
+ function toSleep(ms) {
919
+ return new Promise((resolve) => setTimeout(resolve, ms));
920
+ }
921
+ //#endregion
922
+ //#region src/auth/loopback.ts
923
+ /** The path the browser is sent back to, which the client metadata document registers. */
924
+ const CALLBACK_PATH = "/callback";
925
+ const HOST = "127.0.0.1";
926
+ /**
927
+ * toListener opens a loopback server for one authorization response. The port is whatever
928
+ * the machine hands out, which is why the server matches a loopback redirect by everything
929
+ * except its port.
930
+ */
931
+ async function toListener(timeoutMs) {
932
+ let settle = () => {};
933
+ let fail = () => {};
934
+ const callback = new Promise((resolve, reject) => {
935
+ settle = resolve;
936
+ fail = reject;
937
+ });
938
+ const server = createServer((request, response) => {
939
+ const answered = toCallback(request);
940
+ if (answered === void 0) {
941
+ response.writeHead(404).end();
942
+ return;
943
+ }
944
+ writePage(response, answered);
945
+ settle(answered);
946
+ });
947
+ await new Promise((resolve, reject) => {
948
+ server.once("error", reject);
949
+ server.listen(0, HOST, resolve);
950
+ });
951
+ const timer = setTimeout(() => {
952
+ fail(/* @__PURE__ */ new Error("the browser did not come back in time"));
953
+ close(server, timer);
954
+ }, timeoutMs);
955
+ return {
956
+ redirectUri: `http://${HOST}:${server.address().port}${CALLBACK_PATH}`,
957
+ callback: callback.finally(() => close(server, timer)),
958
+ close: () => close(server, timer)
959
+ };
960
+ }
961
+ function toCallback(request) {
962
+ const url = new URL(request.url ?? "/", `http://${HOST}`);
963
+ if (url.pathname !== CALLBACK_PATH) return;
964
+ return {
965
+ source: "listener",
966
+ code: url.searchParams.get("code") ?? void 0,
967
+ state: url.searchParams.get("state") ?? void 0,
968
+ issuer: url.searchParams.get("iss") ?? void 0,
969
+ error: url.searchParams.get("error") ?? void 0,
970
+ errorDescription: url.searchParams.get("error_description") ?? void 0
971
+ };
972
+ }
973
+ function writePage(response, callback) {
974
+ const title = callback.error ? "Sign in refused" : "Signed in";
975
+ const detail = callback.error ? `${callback.error}${callback.errorDescription ? `: ${callback.errorDescription}` : ""}` : "You can close this tab and return to your terminal.";
976
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family: system-ui, sans-serif; padding: 3rem; max-width: 34rem"><h1 style="font-size: 1.25rem">${title}</h1><p>${detail}</p></body></html>`);
977
+ }
978
+ function close(server, timer) {
979
+ clearTimeout(timer);
980
+ server.close();
981
+ server.closeAllConnections();
982
+ }
983
+ //#endregion
984
+ //#region src/auth/pkce.ts
985
+ /** toPkce makes a fresh verifier and the challenge derived from it. */
986
+ function toPkce() {
987
+ const verifier = toUrlSafe(randomBytes(32));
988
+ return {
989
+ verifier,
990
+ challenge: toUrlSafe(createHash("sha256").update(verifier).digest()),
991
+ method: "S256"
992
+ };
993
+ }
994
+ /** toState makes the value that ties a callback to the request that started it. */
995
+ function toState() {
996
+ return toUrlSafe(randomBytes(16));
997
+ }
998
+ function toUrlSafe(bytes) {
999
+ return bytes.toString("base64url");
1000
+ }
1001
+ //#endregion
1002
+ //#region src/auth/clipboard.ts
1003
+ /** toClipboardCommand names how this host takes text onto the clipboard. */
1004
+ function toClipboardCommand() {
1005
+ if (process.platform === "darwin") return ["pbcopy", []];
1006
+ if (process.platform === "win32") return ["clip", []];
1007
+ if (existsSync("/proc/sys/fs/binfmt_misc/WSLInterop")) return ["clip.exe", []];
1008
+ if (process.env["WAYLAND_DISPLAY"]) return ["wl-copy", []];
1009
+ return ["xclip", ["-selection", "clipboard"]];
1010
+ }
1011
+ /** copyToClipboard puts text on the clipboard, and reports whether anything took it. */
1012
+ function copyToClipboard(text) {
1013
+ const command = toClipboardCommand();
1014
+ if (!command) return false;
1015
+ const [name, args] = command;
1016
+ const result = spawnSync(name, args, { input: text });
1017
+ return result.error === void 0 && result.status === 0;
1018
+ }
1019
+ //#endregion
1020
+ //#region src/auth/prompt.ts
1021
+ const CTRL_C = "";
1022
+ const BACKSPACE = "";
1023
+ /**
1024
+ * toPastedCallback reads what a person pasted. A browser that could not reach the listener
1025
+ * leaves the whole redirect in the address bar, and a page that shows the code leaves
1026
+ * `code#state`, so both are accepted alongside a bare code.
1027
+ */
1028
+ function toPastedCallback(pasted) {
1029
+ const value = pasted.trim();
1030
+ if (value.startsWith("http://") || value.startsWith("https://")) {
1031
+ const url = new URL(value);
1032
+ return {
1033
+ source: "pasted",
1034
+ code: url.searchParams.get("code") ?? void 0,
1035
+ state: url.searchParams.get("state") ?? void 0,
1036
+ issuer: url.searchParams.get("iss") ?? void 0,
1037
+ error: url.searchParams.get("error") ?? void 0,
1038
+ errorDescription: url.searchParams.get("error_description") ?? void 0
1039
+ };
1040
+ }
1041
+ const [code, state] = value.split("#");
1042
+ return {
1043
+ source: "pasted",
1044
+ code: code || void 0,
1045
+ state: state || void 0
1046
+ };
1047
+ }
1048
+ /**
1049
+ * toPrompt watches the keyboard while the browser is away. Pressing c copies the URL, and
1050
+ * pasting a code finishes the sign in on a machine whose browser cannot reach this listener.
1051
+ */
1052
+ function toPrompt(url) {
1053
+ let settle = () => {};
1054
+ let fail = () => {};
1055
+ const pasted = new Promise((resolve, reject) => {
1056
+ settle = resolve;
1057
+ fail = reject;
1058
+ });
1059
+ const input = process.stdin;
1060
+ if (!input.isTTY) return {
1061
+ pasted,
1062
+ close: () => {}
1063
+ };
1064
+ let typed = "";
1065
+ const onData = (chunk) => {
1066
+ for (const character of chunk) {
1067
+ if (character === CTRL_C) {
1068
+ fail(/* @__PURE__ */ new Error("sign in was cancelled"));
1069
+ return;
1070
+ }
1071
+ if (character === "\r" || character === "\n") {
1072
+ if (typed.trim() === "") continue;
1073
+ process.stderr.write("\n");
1074
+ settle(toPastedCallback(typed));
1075
+ return;
1076
+ }
1077
+ if (character === BACKSPACE) {
1078
+ typed = typed.slice(0, -1);
1079
+ process.stderr.write("\b \b");
1080
+ continue;
1081
+ }
1082
+ if ((character === "c" || character === "C") && typed === "") {
1083
+ process.stderr.write(copyToClipboard(url) ? "Copied the URL to your clipboard\n" : "Nothing on this host takes a clipboard\n");
1084
+ continue;
1085
+ }
1086
+ typed += character;
1087
+ process.stderr.write(character);
1088
+ }
1089
+ };
1090
+ input.setRawMode(true);
1091
+ input.setEncoding("utf8");
1092
+ input.resume();
1093
+ input.on("data", onData);
1094
+ const close = () => {
1095
+ input.off("data", onData);
1096
+ input.setRawMode(false);
1097
+ input.pause();
1098
+ };
1099
+ return {
1100
+ pasted: pasted.finally(close),
1101
+ close
1102
+ };
1103
+ }
1104
+ //#endregion
1105
+ //#region src/command/login.ts
1106
+ /** The scopes a sign-in asks for, with offline access so a refresh token comes back. */
1107
+ const DEFAULT_SCOPES = "hardfin:read hardfin:write offline_access";
1108
+ const WAIT_MS = 3e5;
1109
+ const loginCommand = defineCommand({
1110
+ name: "login",
1111
+ summary: "Sign in to Hardfin through a browser",
1112
+ description: "Opens the browser to approve this CLI, then keeps the refresh token in the OS keyring, or in a file with owner-only permissions where there is no keyring. Access tokens are never written anywhere.",
1113
+ arguments: [],
1114
+ flags: [
1115
+ {
1116
+ name: "scope",
1117
+ description: "The scopes to ask for, separated by spaces",
1118
+ valueName: "scopes",
1119
+ schema: z.string(),
1120
+ defaultValue: DEFAULT_SCOPES
1121
+ },
1122
+ {
1123
+ name: "no-browser",
1124
+ description: "Print the URL instead of opening it",
1125
+ schema: z.boolean()
1126
+ },
1127
+ {
1128
+ name: "device",
1129
+ description: "Approve on another machine, by typing a code, with no listener on this one",
1130
+ schema: z.boolean()
1131
+ },
1132
+ {
1133
+ name: "json",
1134
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
1135
+ schema: z.boolean()
1136
+ }
1137
+ ],
1138
+ examples: [
1139
+ {
1140
+ description: "Sign in",
1141
+ command: "hardfin login"
1142
+ },
1143
+ {
1144
+ description: "Sign in over SSH, opening the URL yourself",
1145
+ command: "hardfin login --no-browser"
1146
+ },
1147
+ {
1148
+ description: "Approve from your phone or another machine",
1149
+ command: "hardfin login --device"
1150
+ }
1151
+ ],
1152
+ run: runLogin
1153
+ });
1154
+ async function runLogin(input) {
1155
+ const settings = input.resolved.settings;
1156
+ try {
1157
+ const clientId = settings.clientId;
1158
+ const metadata = await toMetadata(settings.issuerUrl);
1159
+ const scope = String(input.flags["scope"] ?? DEFAULT_SCOPES);
1160
+ if (input.flags["device"] === true) return await runDeviceLogin(input, metadata, scope);
1161
+ const listener = await toListener(WAIT_MS);
1162
+ const pkce = toPkce();
1163
+ const state = toState();
1164
+ const url = toAuthorizationUrl(metadata.authorization_endpoint, {
1165
+ clientId,
1166
+ redirectUri: listener.redirectUri,
1167
+ scope,
1168
+ state,
1169
+ challenge: pkce.challenge
1170
+ });
1171
+ const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
1172
+ process.stderr.write(opened ? `Opening your browser to sign in. If it did not open:\n\n${url}\n\n` : `Open this URL to sign in:\n\n${url}\n\n`);
1173
+ const prompt = toPrompt(url);
1174
+ if (process.stdin.isTTY) process.stderr.write("Press c to copy the URL, or paste the code or redirect URL here: ");
1175
+ const callback = await Promise.race([listener.callback, prompt.pasted]);
1176
+ listener.close();
1177
+ prompt.close();
1178
+ process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
1179
+ if (callback.error) {
1180
+ writeFailure(`sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
1181
+ return ExitCode.ERROR;
1182
+ }
1183
+ if (callback.source === "listener" ? callback.state !== state : callback.state !== void 0 && callback.state !== state) {
1184
+ writeFailure("the browser came back with a state this sign in did not send", input.isJSON);
1185
+ return ExitCode.ERROR;
1186
+ }
1187
+ if (callback.issuer !== void 0 && callback.issuer.replace(/\/+$/, "") !== metadata.issuer.replace(/\/+$/, "")) {
1188
+ writeFailure(`the browser came back from ${callback.issuer}, which is not ${metadata.issuer}`, input.isJSON);
1189
+ return ExitCode.ERROR;
1190
+ }
1191
+ if (!callback.code) {
1192
+ writeFailure("the browser came back without an authorization code", input.isJSON);
1193
+ return ExitCode.ERROR;
1194
+ }
1195
+ return await toSignedIn(input, metadata, await toTokensFromCode(metadata.token_endpoint, clientId, callback.code, listener.redirectUri, pkce));
1196
+ } catch (error) {
1197
+ if (error instanceof DiscoveryFailure || error instanceof GrantFailure) {
1198
+ writeFailure(error.message, input.isJSON);
1199
+ return ExitCode.ERROR;
1200
+ }
1201
+ writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
1202
+ return ExitCode.ERROR;
1203
+ }
1204
+ }
1205
+ /** runDeviceLogin waits while the person approves on a machine that has a browser. */
1206
+ async function runDeviceLogin(input, metadata, scope) {
1207
+ const settings = input.resolved.settings;
1208
+ if (!metadata.device_authorization_endpoint) {
1209
+ writeFailure(`${metadata.issuer} does not offer the device grant, so sign in without --device`, input.isJSON);
1210
+ return ExitCode.ERROR;
1211
+ }
1212
+ const device = await toDeviceAuthorization(metadata.device_authorization_endpoint, settings.clientId, scope);
1213
+ const url = device.verificationUriComplete ?? device.verificationUri;
1214
+ const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
1215
+ process.stderr.write([
1216
+ "",
1217
+ ` Code ${device.userCode}`,
1218
+ ` At ${device.verificationUri}`,
1219
+ "",
1220
+ opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
1221
+ ].join("\n"));
1222
+ const prompt = toPrompt(url);
1223
+ return await toSignedIn(input, metadata, await toTokensFromDevice(metadata.token_endpoint, settings.clientId, device).finally(() => prompt.close()));
1224
+ }
1225
+ /** toSignedIn stores what a sign in issued, whichever flow issued it. */
1226
+ async function toSignedIn(input, metadata, tokens) {
1227
+ const refreshToken = tokens.refreshToken;
1228
+ if (!refreshToken) {
1229
+ writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
1230
+ return ExitCode.ERROR;
1231
+ }
1232
+ const backend = await withLock(() => keep(input.resolved.settings.issuerUrl, refreshToken));
1233
+ if (input.isJSON) {
1234
+ writeData({
1235
+ signedIn: true,
1236
+ issuer: metadata.issuer,
1237
+ scope: tokens.scope ?? null,
1238
+ storedIn: backend
1239
+ });
1240
+ return ExitCode.OK;
1241
+ }
1242
+ const stored = backend === "keyring" ? "your OS keyring" : toCredentialPath();
1243
+ writeData([
1244
+ `Signed in to ${metadata.issuer}`,
1245
+ `Scope ${tokens.scope ?? "as granted"}`,
1246
+ `Stored in ${stored}`,
1247
+ "",
1248
+ "Run hardfin status to see what this CLI is using"
1249
+ ].join("\n"));
1250
+ return ExitCode.OK;
1251
+ }
1252
+ /** toAuthorizationUrl builds the URL the person approves this CLI at. */
1253
+ function toAuthorizationUrl(endpoint, request) {
1254
+ const url = new URL(endpoint);
1255
+ url.searchParams.set("response_type", "code");
1256
+ url.searchParams.set("client_id", request.clientId);
1257
+ url.searchParams.set("redirect_uri", request.redirectUri);
1258
+ url.searchParams.set("scope", request.scope);
1259
+ url.searchParams.set("state", request.state);
1260
+ url.searchParams.set("code_challenge", request.challenge);
1261
+ url.searchParams.set("code_challenge_method", "S256");
1262
+ return url.toString();
1263
+ }
1264
+ //#endregion
1265
+ //#region src/command/logout.ts
1266
+ const logoutCommand = defineCommand({
1267
+ name: "logout",
1268
+ summary: "Sign out, and tell Hardfin to forget this machine",
1269
+ description: "Removes the stored refresh token and asks the authorization server to revoke it, so a copy taken from this machine stops working.",
1270
+ arguments: [],
1271
+ flags: [{
1272
+ name: "json",
1273
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
1274
+ schema: z.boolean()
1275
+ }],
1276
+ examples: [{
1277
+ description: "Sign out",
1278
+ command: "hardfin logout"
1279
+ }],
1280
+ run: runLogout
1281
+ });
1282
+ async function runLogout(input) {
1283
+ const settings = input.resolved.settings;
1284
+ const stored = toCredential(settings.issuerUrl);
1285
+ if (!stored) {
1286
+ writeData({
1287
+ signedOut: false,
1288
+ issuer: settings.issuerUrl,
1289
+ reason: "no sign in was stored"
1290
+ });
1291
+ return ExitCode.OK;
1292
+ }
1293
+ let revoked = false;
1294
+ try {
1295
+ const metadata = await toMetadata(settings.issuerUrl);
1296
+ if (metadata.revocation_endpoint) {
1297
+ await revoke(metadata.revocation_endpoint, settings.clientId, stored.refreshToken);
1298
+ revoked = true;
1299
+ }
1300
+ } catch {
1301
+ revoked = false;
1302
+ }
1303
+ await withLock(() => forget(settings.issuerUrl));
1304
+ forgetHeldTokens();
1305
+ writeData({
1306
+ signedOut: true,
1307
+ issuer: settings.issuerUrl,
1308
+ revoked
1309
+ });
1310
+ return ExitCode.OK;
1311
+ }
1312
+ //#endregion
1313
+ //#region src/command/operation.ts
1314
+ const INPUT_FLAG = {
1315
+ name: "input",
1316
+ description: "A file holding the JSON request body, or - for stdin",
1317
+ valueName: "file",
1318
+ schema: z.string()
1319
+ };
1320
+ const JSON_FLAG = {
1321
+ name: "json",
1322
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
1323
+ schema: z.boolean()
1324
+ };
1325
+ /** defineOperation turns one endpoint into the command that calls it. */
1326
+ function defineOperation(operation) {
1327
+ const flags = [...operation.queryFlags, JSON_FLAG];
1328
+ if (operation.takesBody) flags.splice(flags.length - 1, 0, INPUT_FLAG);
1329
+ return {
1330
+ name: operation.name,
1331
+ summary: operation.summary,
1332
+ description: operation.description ?? operation.summary,
1333
+ arguments: operation.pathParameters,
1334
+ flags,
1335
+ examples: [],
1336
+ run: (input) => runOperation(operation, input)
1337
+ };
1338
+ }
1339
+ async function runOperation(operation, input) {
1340
+ const path = toPath(operation, input.args);
1341
+ if (path === void 0) {
1342
+ writeFailure(`this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
1343
+ return ExitCode.USAGE;
1344
+ }
1345
+ let body;
1346
+ if (typeof input.flags["input"] === "string") {
1347
+ body = toBody(input.flags["input"]);
1348
+ if (body === void 0) {
1349
+ writeFailure(`${input.flags["input"]} does not hold JSON`, input.isJSON);
1350
+ return ExitCode.USAGE;
1351
+ }
1352
+ }
1353
+ try {
1354
+ writeData((await request({
1355
+ apiUrl: input.resolved.settings.apiUrl,
1356
+ credential: await toRequestCredential(input.resolved.settings),
1357
+ method: operation.method,
1358
+ path,
1359
+ query: toQuery(operation, input.flags),
1360
+ body
1361
+ })).data);
1362
+ return ExitCode.OK;
1363
+ } catch (error) {
1364
+ if (error instanceof NoCredential) {
1365
+ writeFailure(error.message, input.isJSON);
1366
+ return ExitCode.NOT_AUTHENTICATED;
1367
+ }
1368
+ if (error instanceof RequestFailure) {
1369
+ writeFailure(error.message, input.isJSON, error.errors, error.requestId);
1370
+ return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
1371
+ }
1372
+ writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
1373
+ return ExitCode.ERROR;
1374
+ }
1375
+ }
1376
+ /** toPath fills the path template from the positional arguments, in order. */
1377
+ function toPath(operation, args) {
1378
+ if (args.length !== operation.pathParameters.length) return;
1379
+ let path = operation.path;
1380
+ for (const [index, parameter] of operation.pathParameters.entries()) path = path.replace(`{${parameter.name}}`, encodeURIComponent(args[index] ?? ""));
1381
+ return path;
1382
+ }
1383
+ /** toQuery carries only the flags this invocation actually set. */
1384
+ function toQuery(operation, flags) {
1385
+ const query = new URLSearchParams();
1386
+ for (const flag of operation.queryFlags) {
1387
+ const value = flags[toOptionKey(flag.name)];
1388
+ if (value === void 0) continue;
1389
+ for (const entry of Array.isArray(value) ? value : [value]) query.append(flag.queryName, String(entry));
1390
+ }
1391
+ return query;
1392
+ }
1393
+ /** toOptionKey names the parsed flag, which the parser reports in camel case. */
1394
+ function toOptionKey(name) {
1395
+ return name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
1396
+ }
1397
+ function toBody(source) {
1398
+ const text = source === "-" ? readFileSync(0, "utf8") : readFileSync(source, "utf8");
1399
+ try {
1400
+ return JSON.parse(text);
1401
+ } catch {
1402
+ return;
1403
+ }
1404
+ }
1405
+ //#endregion
1406
+ //#region src/command/surface.generated.ts
1407
+ const surfaceCommands = [
1408
+ {
1409
+ name: "asset",
1410
+ summary: "Asset commands",
1411
+ arguments: [],
1412
+ flags: [],
1413
+ examples: [],
1414
+ subcommands: [
1415
+ defineOperation({
1416
+ name: "list",
1417
+ summary: "Get asset listing",
1418
+ method: "GET",
1419
+ path: "/asset",
1420
+ pathParameters: [],
1421
+ queryFlags: [
1422
+ {
1423
+ name: "page",
1424
+ queryName: "page",
1425
+ description: "The page to return, starting at 1",
1426
+ valueName: "number",
1427
+ schema: z.coerce.number()
1428
+ },
1429
+ {
1430
+ name: "limit",
1431
+ queryName: "limit",
1432
+ description: "The number of records per page, from 1 to 100",
1433
+ valueName: "number",
1434
+ schema: z.coerce.number()
1435
+ },
1436
+ {
1437
+ name: "sort-by",
1438
+ queryName: "sortBy",
1439
+ description: "The field to sort by",
1440
+ valueName: "value",
1441
+ schema: z.enum([
1442
+ "serial",
1443
+ "project",
1444
+ "item",
1445
+ "location",
1446
+ "owner",
1447
+ "activity"
1448
+ ])
1449
+ },
1450
+ {
1451
+ name: "sort-order",
1452
+ queryName: "sortOrder",
1453
+ description: "The sort direction",
1454
+ valueName: "value",
1455
+ schema: z.enum(["ASC", "DESC"])
1456
+ },
1457
+ {
1458
+ name: "archived",
1459
+ queryName: "archived",
1460
+ description: "Whether to return unarchived records, archived records, or all of them",
1461
+ valueName: "value",
1462
+ schema: z.enum([
1463
+ "all",
1464
+ "false",
1465
+ "true"
1466
+ ])
1467
+ },
1468
+ {
1469
+ name: "search-query",
1470
+ queryName: "searchQuery",
1471
+ description: "Text to match against the serial, description, item, project, owner, and location, ignored when shorter than three characters",
1472
+ valueName: "value",
1473
+ schema: z.string()
1474
+ },
1475
+ {
1476
+ name: "for-asset-id",
1477
+ queryName: "forAssetId",
1478
+ description: "The IDs of the assets to list",
1479
+ valueName: "value",
1480
+ repeatable: true,
1481
+ schema: z.array(z.string())
1482
+ },
1483
+ {
1484
+ name: "for-asset-key",
1485
+ queryName: "forAssetKey",
1486
+ description: "The keys of the assets to list",
1487
+ valueName: "value",
1488
+ repeatable: true,
1489
+ schema: z.array(z.string())
1490
+ },
1491
+ {
1492
+ name: "for-customer",
1493
+ queryName: "forCustomer",
1494
+ description: "The IDs of the customers whose assets to list",
1495
+ valueName: "value",
1496
+ repeatable: true,
1497
+ schema: z.array(z.string())
1498
+ },
1499
+ {
1500
+ name: "for-item",
1501
+ queryName: "forItem",
1502
+ description: "The IDs of the items whose assets to list",
1503
+ valueName: "value",
1504
+ repeatable: true,
1505
+ schema: z.array(z.string())
1506
+ },
1507
+ {
1508
+ name: "at-site",
1509
+ queryName: "atSite",
1510
+ description: "The IDs of the locations whose assets to list",
1511
+ valueName: "value",
1512
+ repeatable: true,
1513
+ schema: z.array(z.string())
1514
+ },
1515
+ {
1516
+ name: "at-customer-sites",
1517
+ queryName: "atCustomerSites",
1518
+ description: "The IDs of the customers whose sites to list assets at",
1519
+ valueName: "value",
1520
+ repeatable: true,
1521
+ schema: z.array(z.string())
1522
+ },
1523
+ {
1524
+ name: "with-functional-statuses",
1525
+ queryName: "withFunctionalStatuses",
1526
+ description: "The functional statuses to list, where SCRAPPED lists scrapped assets",
1527
+ valueName: "value",
1528
+ repeatable: true,
1529
+ schema: z.array(z.enum([
1530
+ "FUNCTIONAL",
1531
+ "NEEDS_REVIEW",
1532
+ "NON-FUNCTIONAL",
1533
+ "SCRAPPED"
1534
+ ]))
1535
+ },
1536
+ {
1537
+ name: "with-transit-statuses",
1538
+ queryName: "withTransitStatuses",
1539
+ description: "The transit statuses to list",
1540
+ valueName: "value",
1541
+ repeatable: true,
1542
+ schema: z.array(z.enum([
1543
+ "IN_TRANSIT",
1544
+ "IN_TRANSIT_TO_FIELD",
1545
+ "IN_TRANSIT_TO_INVENTORY",
1546
+ "NOT_IN_TRANSIT"
1547
+ ]))
1548
+ },
1549
+ {
1550
+ name: "with-inventory-statuses",
1551
+ queryName: "withInventoryStatuses",
1552
+ description: "In_inventory, not_in_inventory, or both, which filters only when one is sent",
1553
+ valueName: "value",
1554
+ repeatable: true,
1555
+ schema: z.array(z.string())
1556
+ },
1557
+ {
1558
+ name: "with-location-company-type",
1559
+ queryName: "withLocationCompanyType",
1560
+ description: "Customer, manufacturer, or both, for assets at customer sites or your own, which filters only when one is sent",
1561
+ valueName: "value",
1562
+ repeatable: true,
1563
+ schema: z.array(z.string())
1564
+ },
1565
+ {
1566
+ name: "with-project-status",
1567
+ queryName: "withProjectStatus",
1568
+ description: "The project assignments to list: upcoming, active, past, or none",
1569
+ valueName: "value",
1570
+ repeatable: true,
1571
+ schema: z.array(z.string())
1572
+ },
1573
+ {
1574
+ name: "with-owner",
1575
+ queryName: "withOwner",
1576
+ description: "Manufacturer for assets your organization owns, customer for assets customers own, or both",
1577
+ valueName: "value",
1578
+ repeatable: true,
1579
+ schema: z.array(z.string())
1580
+ },
1581
+ {
1582
+ name: "for-project",
1583
+ queryName: "forProject",
1584
+ description: "The IDs of the projects whose assets to list",
1585
+ valueName: "value",
1586
+ repeatable: true,
1587
+ schema: z.array(z.string())
1588
+ },
1589
+ {
1590
+ name: "scrapped",
1591
+ queryName: "scrapped",
1592
+ description: "Whether to list unscrapped assets, scrapped assets, or all of them",
1593
+ valueName: "value",
1594
+ schema: z.enum([
1595
+ "all",
1596
+ "false",
1597
+ "true"
1598
+ ])
1599
+ }
1600
+ ],
1601
+ takesBody: false
1602
+ }),
1603
+ defineOperation({
1604
+ name: "create",
1605
+ summary: "Create asset",
1606
+ method: "POST",
1607
+ path: "/asset",
1608
+ pathParameters: [],
1609
+ queryFlags: [],
1610
+ takesBody: true
1611
+ }),
1612
+ {
1613
+ name: "move",
1614
+ summary: "Move commands",
1615
+ arguments: [],
1616
+ flags: [],
1617
+ examples: [],
1618
+ subcommands: [{
1619
+ name: "execute",
1620
+ summary: "Execute commands",
1621
+ arguments: [],
1622
+ flags: [],
1623
+ examples: [],
1624
+ subcommands: [defineOperation({
1625
+ name: "create",
1626
+ summary: "Execute asset move",
1627
+ method: "POST",
1628
+ path: "/asset/move/execute",
1629
+ pathParameters: [],
1630
+ queryFlags: [],
1631
+ takesBody: true
1632
+ })]
1633
+ }, {
1634
+ name: "plan",
1635
+ summary: "Plan commands",
1636
+ arguments: [],
1637
+ flags: [],
1638
+ examples: [],
1639
+ subcommands: [defineOperation({
1640
+ name: "create",
1641
+ summary: "Plan asset move",
1642
+ method: "POST",
1643
+ path: "/asset/move/plan",
1644
+ pathParameters: [],
1645
+ queryFlags: [],
1646
+ takesBody: true
1647
+ })]
1648
+ }]
1649
+ },
1650
+ defineOperation({
1651
+ name: "get",
1652
+ summary: "Get asset",
1653
+ method: "GET",
1654
+ path: "/asset/{assetKey}",
1655
+ pathParameters: [{
1656
+ name: "assetKey",
1657
+ description: "The asset's key",
1658
+ required: true
1659
+ }],
1660
+ queryFlags: [],
1661
+ takesBody: false
1662
+ }),
1663
+ defineOperation({
1664
+ name: "update",
1665
+ summary: "Patch asset",
1666
+ method: "PATCH",
1667
+ path: "/asset/{assetKey}",
1668
+ pathParameters: [{
1669
+ name: "assetKey",
1670
+ description: "The asset's key",
1671
+ required: true
1672
+ }],
1673
+ queryFlags: [],
1674
+ takesBody: true
1675
+ }),
1676
+ {
1677
+ name: "accounting",
1678
+ summary: "Accounting commands",
1679
+ arguments: [],
1680
+ flags: [],
1681
+ examples: [],
1682
+ subcommands: [defineOperation({
1683
+ name: "update",
1684
+ summary: "Update asset accounting",
1685
+ method: "PATCH",
1686
+ path: "/asset/{assetKey}/accounting",
1687
+ pathParameters: [{
1688
+ name: "assetKey",
1689
+ description: "The asset's key",
1690
+ required: true
1691
+ }],
1692
+ queryFlags: [],
1693
+ takesBody: true
1694
+ }), {
1695
+ name: "in-service-management",
1696
+ summary: "In service management commands",
1697
+ arguments: [],
1698
+ flags: [],
1699
+ examples: [],
1700
+ subcommands: [defineOperation({
1701
+ name: "update",
1702
+ summary: "Toggle in service date management",
1703
+ method: "PATCH",
1704
+ path: "/asset/{assetKey}/accounting/in-service-management",
1705
+ pathParameters: [{
1706
+ name: "assetKey",
1707
+ description: "The asset's key",
1708
+ required: true
1709
+ }],
1710
+ queryFlags: [],
1711
+ takesBody: true
1712
+ })]
1713
+ }]
1714
+ },
1715
+ {
1716
+ name: "cost-adjustment",
1717
+ summary: "Cost adjustment commands",
1718
+ arguments: [],
1719
+ flags: [],
1720
+ examples: [],
1721
+ subcommands: [defineOperation({
1722
+ name: "create",
1723
+ summary: "Create asset cost adjustment",
1724
+ method: "POST",
1725
+ path: "/asset/{assetKey}/cost-adjustment",
1726
+ pathParameters: [{
1727
+ name: "assetKey",
1728
+ description: "The asset's key",
1729
+ required: true
1730
+ }],
1731
+ queryFlags: [],
1732
+ takesBody: true
1733
+ })]
1734
+ },
1735
+ {
1736
+ name: "event",
1737
+ summary: "Event commands",
1738
+ arguments: [],
1739
+ flags: [],
1740
+ examples: [],
1741
+ subcommands: [defineOperation({
1742
+ name: "list",
1743
+ summary: "Get asset event list",
1744
+ method: "GET",
1745
+ path: "/asset/{assetKey}/event",
1746
+ pathParameters: [{
1747
+ name: "assetKey",
1748
+ description: "The asset's key",
1749
+ required: true
1750
+ }],
1751
+ queryFlags: [],
1752
+ takesBody: false
1753
+ })]
1754
+ },
1755
+ {
1756
+ name: "event-group",
1757
+ summary: "Event group commands",
1758
+ arguments: [],
1759
+ flags: [],
1760
+ examples: [],
1761
+ subcommands: [defineOperation({
1762
+ name: "list",
1763
+ summary: "Get asset event group listing",
1764
+ method: "GET",
1765
+ path: "/asset/{assetKey}/event-group",
1766
+ pathParameters: [{
1767
+ name: "assetKey",
1768
+ description: "The asset's key",
1769
+ required: true
1770
+ }],
1771
+ queryFlags: [{
1772
+ name: "start",
1773
+ queryName: "start",
1774
+ description: "The earliest an event group may have happened, or null to read from the first",
1775
+ valueName: "value",
1776
+ schema: z.string()
1777
+ }, {
1778
+ name: "end",
1779
+ queryName: "end",
1780
+ description: "The latest an event group may have happened, or null to read through the last",
1781
+ valueName: "value",
1782
+ schema: z.string()
1783
+ }],
1784
+ takesBody: false
1785
+ }), defineOperation({
1786
+ name: "get",
1787
+ summary: "Get asset event group",
1788
+ method: "GET",
1789
+ path: "/asset/{assetKey}/event-group/{eventGroupKey}",
1790
+ pathParameters: [{
1791
+ name: "assetKey",
1792
+ description: "The asset's key",
1793
+ required: true
1794
+ }, {
1795
+ name: "eventGroupKey",
1796
+ description: "The event group's key",
1797
+ required: true
1798
+ }],
1799
+ queryFlags: [],
1800
+ takesBody: false
1801
+ })]
1802
+ },
1803
+ {
1804
+ name: "file",
1805
+ summary: "File commands",
1806
+ arguments: [],
1807
+ flags: [],
1808
+ examples: [],
1809
+ subcommands: [defineOperation({
1810
+ name: "list",
1811
+ summary: "Get asset files",
1812
+ method: "GET",
1813
+ path: "/asset/{assetKey}/file",
1814
+ pathParameters: [{
1815
+ name: "assetKey",
1816
+ description: "The asset's key",
1817
+ required: true
1818
+ }],
1819
+ queryFlags: [],
1820
+ takesBody: false
1821
+ }), defineOperation({
1822
+ name: "delete",
1823
+ summary: "Delete asset file",
1824
+ method: "DELETE",
1825
+ path: "/asset/{assetKey}/file/{fileKey}",
1826
+ pathParameters: [{
1827
+ name: "assetKey",
1828
+ description: "The asset's key",
1829
+ required: true
1830
+ }, {
1831
+ name: "fileKey",
1832
+ description: "The file's key",
1833
+ required: true
1834
+ }],
1835
+ queryFlags: [],
1836
+ takesBody: false
1837
+ })]
1838
+ },
1839
+ {
1840
+ name: "functional-status",
1841
+ summary: "Functional status commands",
1842
+ arguments: [],
1843
+ flags: [],
1844
+ examples: [],
1845
+ subcommands: [defineOperation({
1846
+ name: "list",
1847
+ summary: "Get asset functional status history",
1848
+ method: "GET",
1849
+ path: "/asset/{assetKey}/functional-status",
1850
+ pathParameters: [{
1851
+ name: "assetKey",
1852
+ description: "The asset's key",
1853
+ required: true
1854
+ }],
1855
+ queryFlags: [],
1856
+ takesBody: false
1857
+ })]
1858
+ },
1859
+ {
1860
+ name: "ownership",
1861
+ summary: "Ownership commands",
1862
+ arguments: [],
1863
+ flags: [],
1864
+ examples: [],
1865
+ subcommands: [
1866
+ defineOperation({
1867
+ name: "list",
1868
+ summary: "Get asset ownership history",
1869
+ method: "GET",
1870
+ path: "/asset/{assetKey}/ownership",
1871
+ pathParameters: [{
1872
+ name: "assetKey",
1873
+ description: "The asset's key",
1874
+ required: true
1875
+ }],
1876
+ queryFlags: [],
1877
+ takesBody: false
1878
+ }),
1879
+ defineOperation({
1880
+ name: "create",
1881
+ summary: "Create asset ownership",
1882
+ method: "POST",
1883
+ path: "/asset/{assetKey}/ownership",
1884
+ pathParameters: [{
1885
+ name: "assetKey",
1886
+ description: "The asset's key",
1887
+ required: true
1888
+ }],
1889
+ queryFlags: [],
1890
+ takesBody: true
1891
+ }),
1892
+ defineOperation({
1893
+ name: "clear",
1894
+ summary: "Clear the ownership an asset holds today",
1895
+ method: "DELETE",
1896
+ path: "/asset/{assetKey}/ownership",
1897
+ pathParameters: [{
1898
+ name: "assetKey",
1899
+ description: "The asset's key",
1900
+ required: true
1901
+ }],
1902
+ queryFlags: [],
1903
+ takesBody: true
1904
+ }),
1905
+ defineOperation({
1906
+ name: "get",
1907
+ summary: "Get asset ownership segment",
1908
+ method: "GET",
1909
+ path: "/asset/{assetKey}/ownership/{segmentKey}",
1910
+ pathParameters: [{
1911
+ name: "assetKey",
1912
+ description: "The asset's key",
1913
+ required: true
1914
+ }, {
1915
+ name: "segmentKey",
1916
+ description: "The ownership segment's key",
1917
+ required: true
1918
+ }],
1919
+ queryFlags: [],
1920
+ takesBody: false
1921
+ }),
1922
+ defineOperation({
1923
+ name: "update",
1924
+ summary: "Patch asset ownership segment",
1925
+ method: "PATCH",
1926
+ path: "/asset/{assetKey}/ownership/{segmentKey}",
1927
+ pathParameters: [{
1928
+ name: "assetKey",
1929
+ description: "The asset's key",
1930
+ required: true
1931
+ }, {
1932
+ name: "segmentKey",
1933
+ description: "The ownership segment's key",
1934
+ required: true
1935
+ }],
1936
+ queryFlags: [],
1937
+ takesBody: true
1938
+ }),
1939
+ defineOperation({
1940
+ name: "delete",
1941
+ summary: "Delete asset ownership segment",
1942
+ method: "DELETE",
1943
+ path: "/asset/{assetKey}/ownership/{segmentKey}",
1944
+ pathParameters: [{
1945
+ name: "assetKey",
1946
+ description: "The asset's key",
1947
+ required: true
1948
+ }, {
1949
+ name: "segmentKey",
1950
+ description: "The ownership segment's key",
1951
+ required: true
1952
+ }],
1953
+ queryFlags: [],
1954
+ takesBody: false
1955
+ })
1956
+ ]
1957
+ },
1958
+ {
1959
+ name: "scrap",
1960
+ summary: "Scrap commands",
1961
+ arguments: [],
1962
+ flags: [],
1963
+ examples: [],
1964
+ subcommands: [defineOperation({
1965
+ name: "create",
1966
+ summary: "Scrap asset",
1967
+ method: "POST",
1968
+ path: "/asset/{assetKey}/scrap",
1969
+ pathParameters: [{
1970
+ name: "assetKey",
1971
+ description: "The asset's key",
1972
+ required: true
1973
+ }],
1974
+ queryFlags: [],
1975
+ takesBody: true
1976
+ })]
1977
+ },
1978
+ {
1979
+ name: "unscrap",
1980
+ summary: "Unscrap commands",
1981
+ arguments: [],
1982
+ flags: [],
1983
+ examples: [],
1984
+ subcommands: [defineOperation({
1985
+ name: "create",
1986
+ summary: "Unscrap asset",
1987
+ method: "POST",
1988
+ path: "/asset/{assetKey}/unscrap",
1989
+ pathParameters: [{
1990
+ name: "assetKey",
1991
+ description: "The asset's key",
1992
+ required: true
1993
+ }],
1994
+ queryFlags: [],
1995
+ takesBody: false
1996
+ })]
1997
+ },
1998
+ {
1999
+ name: "url-link",
2000
+ summary: "URL link commands",
2001
+ arguments: [],
2002
+ flags: [],
2003
+ examples: [],
2004
+ subcommands: [defineOperation({
2005
+ name: "list",
2006
+ summary: "Get asset URL links",
2007
+ method: "GET",
2008
+ path: "/asset/{assetKey}/url-link",
2009
+ pathParameters: [{
2010
+ name: "assetKey",
2011
+ description: "The asset's key",
2012
+ required: true
2013
+ }],
2014
+ queryFlags: [],
2015
+ takesBody: false
2016
+ }), defineOperation({
2017
+ name: "create",
2018
+ summary: "Create asset URL link",
2019
+ method: "POST",
2020
+ path: "/asset/{assetKey}/url-link",
2021
+ pathParameters: [{
2022
+ name: "assetKey",
2023
+ description: "The asset's key",
2024
+ required: true
2025
+ }],
2026
+ queryFlags: [],
2027
+ takesBody: true
2028
+ })]
2029
+ },
2030
+ {
2031
+ name: "useful-life-revision",
2032
+ summary: "Useful life revision commands",
2033
+ arguments: [],
2034
+ flags: [],
2035
+ examples: [],
2036
+ subcommands: [defineOperation({
2037
+ name: "create",
2038
+ summary: "Create asset useful life revision",
2039
+ method: "POST",
2040
+ path: "/asset/{assetKey}/useful-life-revision",
2041
+ pathParameters: [{
2042
+ name: "assetKey",
2043
+ description: "The asset's key",
2044
+ required: true
2045
+ }],
2046
+ queryFlags: [],
2047
+ takesBody: true
2048
+ })]
2049
+ }
2050
+ ]
2051
+ },
2052
+ {
2053
+ name: "customer",
2054
+ summary: "Customer commands",
2055
+ arguments: [],
2056
+ flags: [],
2057
+ examples: [],
2058
+ subcommands: [
2059
+ defineOperation({
2060
+ name: "list",
2061
+ summary: "Get customers",
2062
+ method: "GET",
2063
+ path: "/customer",
2064
+ pathParameters: [],
2065
+ queryFlags: [
2066
+ {
2067
+ name: "page",
2068
+ queryName: "page",
2069
+ description: "The page to return, starting at 1",
2070
+ valueName: "number",
2071
+ schema: z.coerce.number()
2072
+ },
2073
+ {
2074
+ name: "limit",
2075
+ queryName: "limit",
2076
+ description: "The number of records per page, from 1 to 100",
2077
+ valueName: "number",
2078
+ schema: z.coerce.number()
2079
+ },
2080
+ {
2081
+ name: "sort-by",
2082
+ queryName: "sortBy",
2083
+ description: "The field to sort by",
2084
+ valueName: "value",
2085
+ schema: z.string()
2086
+ },
2087
+ {
2088
+ name: "sort-order",
2089
+ queryName: "sortOrder",
2090
+ description: "The sort direction",
2091
+ valueName: "value",
2092
+ schema: z.enum(["ASC", "DESC"])
2093
+ },
2094
+ {
2095
+ name: "archived",
2096
+ queryName: "archived",
2097
+ description: "Whether to return unarchived records, archived records, or all of them",
2098
+ valueName: "value",
2099
+ schema: z.enum([
2100
+ "all",
2101
+ "false",
2102
+ "true"
2103
+ ])
2104
+ },
2105
+ {
2106
+ name: "is-customer",
2107
+ queryName: "isCustomer",
2108
+ description: "True to return only customers, or false to return only non-customers",
2109
+ schema: z.boolean()
2110
+ },
2111
+ {
2112
+ name: "is-supplier",
2113
+ queryName: "isSupplier",
2114
+ description: "True to return only suppliers, or false to return only non-suppliers",
2115
+ schema: z.boolean()
2116
+ },
2117
+ {
2118
+ name: "sync-statuses",
2119
+ queryName: "syncStatuses",
2120
+ description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
2121
+ valueName: "value",
2122
+ repeatable: true,
2123
+ schema: z.array(z.string())
2124
+ },
2125
+ {
2126
+ name: "search",
2127
+ queryName: "search",
2128
+ description: "Text to match against customer names",
2129
+ valueName: "value",
2130
+ schema: z.string()
2131
+ }
2132
+ ],
2133
+ takesBody: false
2134
+ }),
2135
+ defineOperation({
2136
+ name: "create",
2137
+ summary: "Create customer",
2138
+ method: "POST",
2139
+ path: "/customer",
2140
+ pathParameters: [],
2141
+ queryFlags: [],
2142
+ takesBody: true
2143
+ }),
2144
+ defineOperation({
2145
+ name: "get",
2146
+ summary: "Get customer",
2147
+ method: "GET",
2148
+ path: "/customer/{customerKey}",
2149
+ pathParameters: [{
2150
+ name: "customerKey",
2151
+ description: "The customer's key",
2152
+ required: true
2153
+ }],
2154
+ queryFlags: [],
2155
+ takesBody: false
2156
+ }),
2157
+ defineOperation({
2158
+ name: "update",
2159
+ summary: "Patch customer",
2160
+ method: "PATCH",
2161
+ path: "/customer/{customerKey}",
2162
+ pathParameters: [{
2163
+ name: "customerKey",
2164
+ description: "The customer's key",
2165
+ required: true
2166
+ }],
2167
+ queryFlags: [],
2168
+ takesBody: true
2169
+ })
2170
+ ]
2171
+ },
2172
+ {
2173
+ name: "file",
2174
+ summary: "File commands",
2175
+ arguments: [],
2176
+ flags: [],
2177
+ examples: [],
2178
+ subcommands: [defineOperation({
2179
+ name: "create",
2180
+ summary: "Upload file",
2181
+ method: "POST",
2182
+ path: "/file",
2183
+ pathParameters: [],
2184
+ queryFlags: [],
2185
+ takesBody: true
2186
+ }), defineOperation({
2187
+ name: "get",
2188
+ summary: "Get file",
2189
+ method: "GET",
2190
+ path: "/file/{fileKey}",
2191
+ pathParameters: [{
2192
+ name: "fileKey",
2193
+ description: "The file's key, and a public file is readable with any organization's API key while any other file is readable only with its own organization's",
2194
+ required: true
2195
+ }],
2196
+ queryFlags: [{
2197
+ name: "attachment",
2198
+ queryName: "attachment",
2199
+ description: "True when the file downloads as an attachment rather than opening inline",
2200
+ schema: z.boolean()
2201
+ }],
2202
+ takesBody: false
2203
+ })]
2204
+ },
2205
+ {
2206
+ name: "item",
2207
+ summary: "Item commands",
2208
+ arguments: [],
2209
+ flags: [],
2210
+ examples: [],
2211
+ subcommands: [
2212
+ defineOperation({
2213
+ name: "list",
2214
+ summary: "Get items",
2215
+ method: "GET",
2216
+ path: "/item",
2217
+ pathParameters: [],
2218
+ queryFlags: [
2219
+ {
2220
+ name: "type",
2221
+ queryName: "type",
2222
+ description: "The item type to list, SERVICE, DEVICE, or BULK, or absent for every type",
2223
+ valueName: "value",
2224
+ schema: z.enum([
2225
+ "BULK",
2226
+ "DEVICE",
2227
+ "SERVICE"
2228
+ ])
2229
+ },
2230
+ {
2231
+ name: "search",
2232
+ queryName: "search",
2233
+ description: "Text to match against item names, SKUs, and descriptions, in any case",
2234
+ valueName: "value",
2235
+ schema: z.string()
2236
+ },
2237
+ {
2238
+ name: "page",
2239
+ queryName: "page",
2240
+ description: "The page to return, starting at 1",
2241
+ valueName: "number",
2242
+ schema: z.coerce.number()
2243
+ },
2244
+ {
2245
+ name: "limit",
2246
+ queryName: "limit",
2247
+ description: "The number of records per page, from 1 to 100",
2248
+ valueName: "number",
2249
+ schema: z.coerce.number()
2250
+ },
2251
+ {
2252
+ name: "sort-by",
2253
+ queryName: "sortBy",
2254
+ description: "The field to sort by",
2255
+ valueName: "value",
2256
+ schema: z.enum([
2257
+ "name",
2258
+ "sku",
2259
+ "type",
2260
+ "lastUpdatedAt"
2261
+ ])
2262
+ },
2263
+ {
2264
+ name: "sort-order",
2265
+ queryName: "sortOrder",
2266
+ description: "The sort direction",
2267
+ valueName: "value",
2268
+ schema: z.enum(["ASC", "DESC"])
2269
+ },
2270
+ {
2271
+ name: "archived",
2272
+ queryName: "archived",
2273
+ description: "Whether to return unarchived records, archived records, or all of them",
2274
+ valueName: "value",
2275
+ schema: z.enum([
2276
+ "all",
2277
+ "false",
2278
+ "true"
2279
+ ])
2280
+ },
2281
+ {
2282
+ name: "exclude-linked-integration",
2283
+ queryName: "excludeLinkedIntegration",
2284
+ description: "An integration whose already-linked items to leave out",
2285
+ valueName: "value",
2286
+ schema: z.string()
2287
+ }
2288
+ ],
2289
+ takesBody: false
2290
+ }),
2291
+ defineOperation({
2292
+ name: "create",
2293
+ summary: "Create item",
2294
+ method: "POST",
2295
+ path: "/item",
2296
+ pathParameters: [],
2297
+ queryFlags: [],
2298
+ takesBody: true
2299
+ }),
2300
+ defineOperation({
2301
+ name: "get",
2302
+ summary: "Get item",
2303
+ method: "GET",
2304
+ path: "/item/{itemKey}",
2305
+ pathParameters: [{
2306
+ name: "itemKey",
2307
+ description: "The item's key",
2308
+ required: true
2309
+ }],
2310
+ queryFlags: [],
2311
+ takesBody: false
2312
+ }),
2313
+ defineOperation({
2314
+ name: "update",
2315
+ summary: "Update item",
2316
+ method: "PATCH",
2317
+ path: "/item/{itemKey}",
2318
+ pathParameters: [{
2319
+ name: "itemKey",
2320
+ description: "The item's key",
2321
+ required: true
2322
+ }],
2323
+ queryFlags: [],
2324
+ takesBody: true
2325
+ }),
2326
+ {
2327
+ name: "accounting",
2328
+ summary: "Accounting commands",
2329
+ arguments: [],
2330
+ flags: [],
2331
+ examples: [],
2332
+ subcommands: [defineOperation({
2333
+ name: "update",
2334
+ summary: "Update item accounting",
2335
+ method: "PATCH",
2336
+ path: "/item/{itemKey}/accounting",
2337
+ pathParameters: [{
2338
+ name: "itemKey",
2339
+ description: "The item's key",
2340
+ required: true
2341
+ }],
2342
+ queryFlags: [],
2343
+ takesBody: true
2344
+ })]
2345
+ },
2346
+ {
2347
+ name: "field",
2348
+ summary: "Field commands",
2349
+ arguments: [],
2350
+ flags: [],
2351
+ examples: [],
2352
+ subcommands: [
2353
+ defineOperation({
2354
+ name: "create",
2355
+ summary: "Create item field",
2356
+ method: "POST",
2357
+ path: "/item/{itemKey}/field",
2358
+ pathParameters: [{
2359
+ name: "itemKey",
2360
+ description: "The item's key",
2361
+ required: true
2362
+ }],
2363
+ queryFlags: [],
2364
+ takesBody: true
2365
+ }),
2366
+ defineOperation({
2367
+ name: "update",
2368
+ summary: "Update item field",
2369
+ method: "PATCH",
2370
+ path: "/item/{itemKey}/field/{fieldKey}",
2371
+ pathParameters: [{
2372
+ name: "itemKey",
2373
+ description: "The item's key",
2374
+ required: true
2375
+ }, {
2376
+ name: "fieldKey",
2377
+ description: "The field's key",
2378
+ required: true
2379
+ }],
2380
+ queryFlags: [],
2381
+ takesBody: true
2382
+ }),
2383
+ defineOperation({
2384
+ name: "delete",
2385
+ summary: "Delete item field",
2386
+ method: "DELETE",
2387
+ path: "/item/{itemKey}/field/{fieldKey}",
2388
+ pathParameters: [{
2389
+ name: "itemKey",
2390
+ description: "The item's key",
2391
+ required: true
2392
+ }, {
2393
+ name: "fieldKey",
2394
+ description: "The field's key",
2395
+ required: true
2396
+ }],
2397
+ queryFlags: [],
2398
+ takesBody: false
2399
+ })
2400
+ ]
2401
+ }
2402
+ ]
2403
+ },
2404
+ {
2405
+ name: "location",
2406
+ summary: "Location commands",
2407
+ arguments: [],
2408
+ flags: [],
2409
+ examples: [],
2410
+ subcommands: [
2411
+ defineOperation({
2412
+ name: "list",
2413
+ summary: "Get location listing",
2414
+ method: "GET",
2415
+ path: "/location",
2416
+ pathParameters: [],
2417
+ queryFlags: [
2418
+ {
2419
+ name: "page",
2420
+ queryName: "page",
2421
+ description: "The page to return, starting at 1",
2422
+ valueName: "number",
2423
+ schema: z.coerce.number()
2424
+ },
2425
+ {
2426
+ name: "limit",
2427
+ queryName: "limit",
2428
+ description: "The number of records per page, from 1 to 100",
2429
+ valueName: "number",
2430
+ schema: z.coerce.number()
2431
+ },
2432
+ {
2433
+ name: "sort-by",
2434
+ queryName: "sortBy",
2435
+ description: "The field to sort by",
2436
+ valueName: "value",
2437
+ schema: z.enum([
2438
+ "name",
2439
+ "company",
2440
+ "assetCount"
2441
+ ])
2442
+ },
2443
+ {
2444
+ name: "sort-order",
2445
+ queryName: "sortOrder",
2446
+ description: "The sort direction",
2447
+ valueName: "value",
2448
+ schema: z.enum(["ASC", "DESC"])
2449
+ },
2450
+ {
2451
+ name: "archived",
2452
+ queryName: "archived",
2453
+ description: "Whether to return unarchived records, archived records, or all of them",
2454
+ valueName: "value",
2455
+ schema: z.enum([
2456
+ "all",
2457
+ "false",
2458
+ "true"
2459
+ ])
2460
+ },
2461
+ {
2462
+ name: "is-transient",
2463
+ queryName: "isTransient",
2464
+ description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
2465
+ valueName: "value",
2466
+ schema: z.enum([
2467
+ "all",
2468
+ "false",
2469
+ "true"
2470
+ ])
2471
+ },
2472
+ {
2473
+ name: "search",
2474
+ queryName: "search",
2475
+ description: "Text to match against location and customer names",
2476
+ valueName: "value",
2477
+ schema: z.string()
2478
+ },
2479
+ {
2480
+ name: "for-customer-ids",
2481
+ queryName: "forCustomerIds",
2482
+ description: "The IDs of the customers whose locations to return",
2483
+ valueName: "value",
2484
+ repeatable: true,
2485
+ schema: z.array(z.string())
2486
+ },
2487
+ {
2488
+ name: "include-organization",
2489
+ queryName: "includeOrganization",
2490
+ description: "Whether the customer filter also matches your organization's own locations, which on its own returns only those",
2491
+ schema: z.boolean()
2492
+ },
2493
+ {
2494
+ name: "sync-statuses",
2495
+ queryName: "syncStatuses",
2496
+ description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
2497
+ valueName: "value",
2498
+ repeatable: true,
2499
+ schema: z.array(z.string())
2500
+ }
2501
+ ],
2502
+ takesBody: false
2503
+ }),
2504
+ defineOperation({
2505
+ name: "create",
2506
+ summary: "Create location",
2507
+ method: "POST",
2508
+ path: "/location",
2509
+ pathParameters: [],
2510
+ queryFlags: [],
2511
+ takesBody: true
2512
+ }),
2513
+ defineOperation({
2514
+ name: "get",
2515
+ summary: "Get location",
2516
+ method: "GET",
2517
+ path: "/location/{locationKey}",
2518
+ pathParameters: [{
2519
+ name: "locationKey",
2520
+ description: "The location's key",
2521
+ required: true
2522
+ }],
2523
+ queryFlags: [],
2524
+ takesBody: false
2525
+ }),
2526
+ defineOperation({
2527
+ name: "update",
2528
+ summary: "Patch location",
2529
+ method: "PATCH",
2530
+ path: "/location/{locationKey}",
2531
+ pathParameters: [{
2532
+ name: "locationKey",
2533
+ description: "The location's key",
2534
+ required: true
2535
+ }],
2536
+ queryFlags: [],
2537
+ takesBody: true
2538
+ }),
2539
+ {
2540
+ name: "zones",
2541
+ summary: "Zones commands",
2542
+ arguments: [],
2543
+ flags: [],
2544
+ examples: [],
2545
+ subcommands: [defineOperation({
2546
+ name: "list",
2547
+ summary: "Get zones",
2548
+ method: "GET",
2549
+ path: "/location/{locationKey}/zones",
2550
+ pathParameters: [{
2551
+ name: "locationKey",
2552
+ description: "The key of the site whose zones to list",
2553
+ required: true
2554
+ }],
2555
+ queryFlags: [{
2556
+ name: "archived",
2557
+ queryName: "archived",
2558
+ description: "Whether to return unarchived zones, archived zones, or all of them",
2559
+ valueName: "value",
2560
+ schema: z.enum([
2561
+ "all",
2562
+ "false",
2563
+ "true"
2564
+ ])
2565
+ }],
2566
+ takesBody: false
2567
+ })]
2568
+ }
2569
+ ]
2570
+ },
2571
+ {
2572
+ name: "url-link",
2573
+ summary: "URL link commands",
2574
+ arguments: [],
2575
+ flags: [],
2576
+ examples: [],
2577
+ subcommands: [
2578
+ defineOperation({
2579
+ name: "get",
2580
+ summary: "Get URL link by key",
2581
+ method: "GET",
2582
+ path: "/url-link/{linkKey}",
2583
+ pathParameters: [{
2584
+ name: "linkKey",
2585
+ description: "The URL link's key",
2586
+ required: true
2587
+ }],
2588
+ queryFlags: [],
2589
+ takesBody: false
2590
+ }),
2591
+ defineOperation({
2592
+ name: "update",
2593
+ summary: "Update URL link",
2594
+ method: "PATCH",
2595
+ path: "/url-link/{linkKey}",
2596
+ pathParameters: [{
2597
+ name: "linkKey",
2598
+ description: "The URL link's key",
2599
+ required: true
2600
+ }],
2601
+ queryFlags: [],
2602
+ takesBody: true
2603
+ }),
2604
+ defineOperation({
2605
+ name: "delete",
2606
+ summary: "Delete URL link",
2607
+ method: "DELETE",
2608
+ path: "/url-link/{linkKey}",
2609
+ pathParameters: [{
2610
+ name: "linkKey",
2611
+ description: "The URL link's key",
2612
+ required: true
2613
+ }],
2614
+ queryFlags: [],
2615
+ takesBody: false
2616
+ })
2617
+ ]
2618
+ }
2619
+ ];
2620
+ //#endregion
2621
+ //#region src/auth/jwt.ts
2622
+ /**
2623
+ * toClaims reads an access token's payload for display. Nothing here verifies the
2624
+ * signature, because the API is what decides whether a token is good.
2625
+ */
2626
+ function toClaims(token) {
2627
+ const payload = token.split(".")[1];
2628
+ if (!payload) return {};
2629
+ try {
2630
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
2631
+ return {
2632
+ expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
2633
+ issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
2634
+ scopes: toScopes(decoded),
2635
+ subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
2636
+ };
2637
+ } catch {
2638
+ return {};
2639
+ }
2640
+ }
2641
+ /** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
2642
+ function toScopes(decoded) {
2643
+ if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
2644
+ return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
2645
+ }
2646
+ //#endregion
2647
+ //#region src/system/host.ts
2648
+ const BYTES_PER_GB = 1024 ** 3;
2649
+ /** toDistribution reads the name a Linux distribution gives itself. */
2650
+ function toDistribution(osRelease) {
2651
+ const pretty = /^PRETTY_NAME="?([^"\n]+)"?$/m.exec(osRelease);
2652
+ if (pretty?.[1]) return pretty[1];
2653
+ const name = /^NAME="?([^"\n]+)"?$/m.exec(osRelease);
2654
+ const version = /^VERSION_ID="?([^"\n]+)"?$/m.exec(osRelease);
2655
+ if (!name?.[1]) return;
2656
+ return version?.[1] ? `${name[1]} ${version[1]}` : name[1];
2657
+ }
2658
+ /** toHostReport describes the machine, which is what a support request cannot ask for twice. */
2659
+ function toHostReport() {
2660
+ const report = {
2661
+ type: type(),
2662
+ kernel: release(),
2663
+ build: version(),
2664
+ arch: arch(),
2665
+ cpus: cpus().length,
2666
+ cpu: cpus()[0]?.model ?? null,
2667
+ memoryGb: Number((totalmem() / BYTES_PER_GB).toFixed(1)),
2668
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
2669
+ shell: process.env["SHELL"] ?? process.env["ComSpec"] ?? null,
2670
+ terminal: process.env["TERM"] ?? null
2671
+ };
2672
+ if (process.platform !== "linux") return report;
2673
+ report["distribution"] = toDistribution(toFile("/etc/os-release")) ?? null;
2674
+ if (isWSL()) report["wsl"] = {
2675
+ distribution: process.env["WSL_DISTRO_NAME"] ?? null,
2676
+ windowsTerminal: Boolean(process.env["WT_SESSION"])
2677
+ };
2678
+ return report;
2679
+ }
2680
+ function toFile(path) {
2681
+ try {
2682
+ return readFileSync(path, "utf8");
2683
+ } catch {
2684
+ return "";
2685
+ }
2686
+ }
2687
+ //#endregion
2688
+ //#region src/command/status.ts
2689
+ /**
2690
+ * Hardfin expires an unused refresh token after this long. It is the server's rule, not the
2691
+ * CLI's, and the token endpoint reports no expiry, so what this produces is an estimate.
2692
+ */
2693
+ const REFRESH_SLIDING_DAYS = 90;
2694
+ const statusCommand = defineCommand({
2695
+ name: "status",
2696
+ summary: "Report what this CLI is configured with, signed in as, and able to reach",
2697
+ description: "Gathers everything a support request needs: the version, the configuration and where each value came from, the authorization server's endpoints, where the credential is stored, and when the tokens expire. Secrets are reported as fingerprints, never printed.",
2698
+ arguments: [],
2699
+ flags: [{
2700
+ name: "offline",
2701
+ description: "Skip every network call, and report only what is on this machine",
2702
+ schema: z.boolean()
2703
+ }, {
2704
+ name: "json",
2705
+ description: "Print machine-readable output, which is the default when stdout is not a terminal",
2706
+ schema: z.boolean()
2707
+ }],
2708
+ examples: [
2709
+ {
2710
+ description: "See what the CLI is using",
2711
+ command: "hardfin status"
2712
+ },
2713
+ {
2714
+ description: "Attach the whole picture to a support request",
2715
+ command: "hardfin status --json > status.json"
2716
+ },
2717
+ {
2718
+ description: "Ask nothing of the network",
2719
+ command: "hardfin status --offline"
2720
+ }
2721
+ ],
2722
+ run: runStatus
2723
+ });
2724
+ async function runStatus(input) {
2725
+ const settings = input.resolved.settings;
2726
+ const stored = toCredential(settings.issuerUrl);
2727
+ const report = {
2728
+ cli: toEnvironmentReport(),
2729
+ host: toHostReport(),
2730
+ configuration: toConfigurationReport(input.resolved),
2731
+ credential: toCredentialReport(settings.apiKey, stored)
2732
+ };
2733
+ if (input.flags["offline"] === true) {
2734
+ report["authorizationServer"] = { checked: false };
2735
+ report["api"] = { checked: false };
2736
+ writeReport(report, input.isJSON);
2737
+ return stored || settings.apiKey ? ExitCode.OK : ExitCode.NOT_AUTHENTICATED;
2738
+ }
2739
+ const metadata = await toMetadata(settings.issuerUrl).catch((error) => error);
2740
+ report["authorizationServer"] = metadata instanceof Error ? {
2741
+ reachable: false,
2742
+ error: metadata.message
2743
+ } : toServerReport(metadata);
2744
+ report["api"] = await toApiReport(input);
2745
+ writeReport(report, input.isJSON);
2746
+ return report["api"].authenticated === false ? ExitCode.NOT_AUTHENTICATED : ExitCode.OK;
2747
+ }
2748
+ function toEnvironmentReport() {
2749
+ return {
2750
+ version: version$1,
2751
+ apiVersion: API_VERSION,
2752
+ node: process.version,
2753
+ platform: process.platform,
2754
+ interactive: Boolean(process.stdin.isTTY)
2755
+ };
2756
+ }
2757
+ /** toConfigurationReport names every setting, its source, and never a secret's value. */
2758
+ function toConfigurationReport(resolved) {
2759
+ const entries = Object.entries(resolved.settings).map(([key, value]) => {
2760
+ const from = resolved.sources[key];
2761
+ if (key === "apiKey") return [key, {
2762
+ set: value !== void 0,
2763
+ fingerprint: toFingerprint(value),
2764
+ from
2765
+ }];
2766
+ return [key, {
2767
+ value: value ?? null,
2768
+ from
2769
+ }];
2770
+ });
2771
+ return {
2772
+ ...Object.fromEntries(entries),
2773
+ configFile: CONFIG_FILE
2774
+ };
2775
+ }
2776
+ function toCredentialReport(apiKey, stored) {
2777
+ if (apiKey) return {
2778
+ kind: "api key",
2779
+ signedIn: false,
2780
+ note: "an API key is set, so it is used ahead of any sign in"
2781
+ };
2782
+ if (!stored) return {
2783
+ kind: "none",
2784
+ signedIn: false,
2785
+ note: "run hardfin login, or set HARDFIN_API_KEY"
2786
+ };
2787
+ return {
2788
+ kind: "browser sign in",
2789
+ signedIn: true,
2790
+ storedIn: stored.backend,
2791
+ path: stored.path ?? null,
2792
+ fingerprint: toFingerprint(stored.refreshToken),
2793
+ signedInAt: stored.signedInAt ?? null,
2794
+ renewedAt: stored.renewedAt ?? null,
2795
+ refreshExpiresAt: toRefreshExpiry(stored.renewedAt),
2796
+ refreshExpiryIsEstimated: true
2797
+ };
2798
+ }
2799
+ function toServerReport(metadata) {
2800
+ return {
2801
+ reachable: true,
2802
+ issuer: metadata.issuer,
2803
+ authorizationEndpoint: metadata.authorization_endpoint,
2804
+ tokenEndpoint: metadata.token_endpoint,
2805
+ revocationEndpoint: metadata.revocation_endpoint ?? null,
2806
+ deviceEndpoint: metadata.device_authorization_endpoint ?? null,
2807
+ grantTypes: metadata.grant_types_supported ?? [],
2808
+ scopes: metadata.scopes_supported ?? []
2809
+ };
2810
+ }
2811
+ /** toApiReport asks the API who this credential is, which is the only authoritative answer. */
2812
+ async function toApiReport(input) {
2813
+ const settings = input.resolved.settings;
2814
+ try {
2815
+ const credential = await toRequestCredential(settings);
2816
+ const claims = credential.kind === "access token" ? toClaims(credential.value.replace(/^Bearer /, "")) : {};
2817
+ const identity = await request({
2818
+ apiUrl: settings.apiUrl,
2819
+ credential,
2820
+ method: "GET",
2821
+ path: "/token"
2822
+ }).then((envelope) => envelope.data).catch((error) => error instanceof RequestFailure ? { unavailable: error.message } : { unavailable: String(error) });
2823
+ return {
2824
+ reachable: true,
2825
+ authenticated: true,
2826
+ url: settings.apiUrl,
2827
+ accessTokenExpiresAt: claims.expiresAt === void 0 ? null : new Date(claims.expiresAt).toISOString(),
2828
+ accessTokenScopes: claims.scopes ?? [],
2829
+ identity
2830
+ };
2831
+ } catch (error) {
2832
+ if (error instanceof NoCredential) return {
2833
+ reachable: null,
2834
+ authenticated: false,
2835
+ url: settings.apiUrl,
2836
+ error: error.message
2837
+ };
2838
+ return {
2839
+ reachable: false,
2840
+ authenticated: false,
2841
+ url: settings.apiUrl,
2842
+ error: error instanceof Error ? error.message : String(error)
2843
+ };
2844
+ }
2845
+ }
2846
+ /** toRefreshExpiry applies the server's sliding rule to when the token was last renewed. */
2847
+ function toRefreshExpiry(renewedAt) {
2848
+ if (!renewedAt) return null;
2849
+ const renewed = new Date(renewedAt);
2850
+ renewed.setDate(renewed.getDate() + REFRESH_SLIDING_DAYS);
2851
+ return renewed.toISOString();
2852
+ }
2853
+ /** toFingerprint identifies a secret in a support request without disclosing it. */
2854
+ function toFingerprint(secret) {
2855
+ if (!secret) return null;
2856
+ return `sha256:${createHash("sha256").update(secret).digest("hex").slice(0, 12)}`;
2857
+ }
2858
+ function writeReport(report, isJSON) {
2859
+ if (isJSON) {
2860
+ writeData(report);
2861
+ return;
2862
+ }
2863
+ writeData(toLines(report).join("\n"));
2864
+ }
2865
+ /** toFlattened reads a setting, which is a value and the layer that supplied it. */
2866
+ function toFlattened(value) {
2867
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return;
2868
+ const holder = value;
2869
+ if (holder.from === void 0) return;
2870
+ const shown = holder.value ?? (holder.set ? holder.fingerprint ?? "set" : "not set");
2871
+ return `${String(shown)} (${holder.from})`;
2872
+ }
2873
+ /** toLines lays the report out for a person, one indented line per value. */
2874
+ function toLines(report, depth = 0) {
2875
+ const lines = [];
2876
+ const pad = " ".repeat(depth);
2877
+ for (const [key, value] of Object.entries(report)) {
2878
+ const flattened = toFlattened(value);
2879
+ if (flattened !== void 0) {
2880
+ lines.push(`${pad}${key.padEnd(26 - depth * 2)} ${flattened}`);
2881
+ continue;
2882
+ }
2883
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
2884
+ lines.push(`${pad}${key}`, ...toLines(value, depth + 1));
2885
+ continue;
2886
+ }
2887
+ lines.push(`${pad}${key.padEnd(26 - depth * 2)} ${Array.isArray(value) ? value.join(", ") : String(value)}`);
2888
+ }
2889
+ return lines;
2890
+ }
2891
+ //#endregion
2892
+ //#region src/command/commands.ts
2893
+ /** commands is every command the CLI offers, and drives help and the agent guide. */
2894
+ const commands = [
2895
+ loginCommand,
2896
+ logoutCommand,
2897
+ statusCommand,
2898
+ ...surfaceCommands,
2899
+ apiCommand,
2900
+ configCommand,
2901
+ agentGuideCommand
2902
+ ];
2903
+ //#endregion
2904
+ //#region src/command/validate.ts
2905
+ /** toRejectedFlag names the first flag whose value its schema refuses. */
2906
+ function toRejectedFlag(command, flags) {
2907
+ for (const flag of command.flags) {
2908
+ const value = flags[toOptionKey(flag.name)];
2909
+ if (value === void 0) continue;
2910
+ if (!flag.schema.safeParse(value).success) return `--${flag.name} does not accept ${JSON.stringify(value)}`;
2911
+ }
2912
+ }
2913
+ //#endregion
2914
+ //#region src/cli.ts
2915
+ const program = new Command();
2916
+ program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version$1, "-v, --version").option("--api-url <url>", "The API to call, whose host also holds the authorization server").option("--issuer-url <url>", "The authorization server, when it does not sit at the API's host").showHelpAfterError().enablePositionalOptions();
2917
+ for (const command of commands) program.addCommand(toProgram(command));
2918
+ await program.parseAsync(process.argv);
2919
+ /** toProgram wires one registry command into the parser. */
2920
+ function toProgram(command) {
2921
+ const program = new Command(command.name).summary(command.summary).description(command.description ?? command.summary);
2922
+ for (const argument of command.arguments) {
2923
+ const name = argument.variadic ? `${argument.name}...` : argument.name;
2924
+ program.argument(argument.required ? `<${name}>` : `[${name}]`, argument.description);
2925
+ }
2926
+ for (const flag of command.flags) {
2927
+ const short = flag.short ? `-${flag.short}, ` : "";
2928
+ const value = flag.valueName ? ` <${flag.valueName}>` : "";
2929
+ const option = new Option(`${short}--${flag.name}${value}`, flag.description);
2930
+ if (flag.repeatable) option.argParser(collect);
2931
+ if (flag.defaultValue !== void 0) option.default(flag.defaultValue);
2932
+ program.addOption(option);
2933
+ }
2934
+ for (const example of command.examples) program.addHelpText("after", `\n${example.description}:\n $ ${example.command}`);
2935
+ for (const subcommand of command.subcommands ?? []) program.addCommand(toProgram(subcommand));
2936
+ if (!command.run) return program;
2937
+ program.action(async (...parsed) => {
2938
+ const flags = parsed[parsed.length - 2] ?? {};
2939
+ const args = parsed.slice(0, parsed.length - 2).flatMap(toArgumentList);
2940
+ process.exitCode = await toExitCode(command, args, flags);
2941
+ });
2942
+ return program;
2943
+ }
2944
+ async function toExitCode(command, args, flags) {
2945
+ const isJSON = isJSONOutput(flags);
2946
+ const rejected = toRejectedFlag(command, flags);
2947
+ if (rejected) {
2948
+ writeFailure(rejected, isJSON);
2949
+ return ExitCode.USAGE;
2950
+ }
2951
+ try {
2952
+ const resolved = toSettings({
2953
+ apiUrl: program.opts()["apiUrl"],
2954
+ issuerUrl: program.opts()["issuerUrl"]
2955
+ });
2956
+ return await command.run?.({
2957
+ args,
2958
+ flags,
2959
+ isJSON,
2960
+ commands,
2961
+ resolved
2962
+ }) ?? ExitCode.OK;
2963
+ } catch (error) {
2964
+ writeFailure(error instanceof Error ? error.message : String(error), isJSON);
2965
+ return error instanceof ConfigFailure ? ExitCode.USAGE : ExitCode.ERROR;
2966
+ }
2967
+ }
2968
+ function toArgumentList(value) {
2969
+ if (Array.isArray(value)) return value.map(String);
2970
+ return value === void 0 ? [] : [String(value)];
2971
+ }
2972
+ function collect(value, previous) {
2973
+ return [...previous ?? [], value];
2974
+ }
2975
+ //#endregion
2976
+ export {};