@hardfin/cli 0.0.2-dev.8 → 0.0.2-dev.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -6
- package/dist/cli.js +2122 -1118
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Command, Option } from "commander";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import { resolve } from "node:path";
|
|
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";
|
|
7
11
|
//#region src/command/registry.ts
|
|
8
12
|
/** ExitCode is what the process returns, and what an agent branches on. */
|
|
9
13
|
const ExitCode = {
|
|
@@ -23,17 +27,17 @@ const API_VERSION = "2026-09-17";
|
|
|
23
27
|
/** The file a local build reads its overrides from, in the working directory. */
|
|
24
28
|
const CONFIG_FILE = "config.local.json";
|
|
25
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";
|
|
26
35
|
const DEFAULT_ENV_FILE = ".env";
|
|
27
36
|
const FileSettings = z.strictObject({
|
|
28
37
|
apiUrl: z.string().optional(),
|
|
29
38
|
apiKey: z.string().optional(),
|
|
30
39
|
clientId: z.string().optional(),
|
|
31
|
-
|
|
32
|
-
authorizeUrl: z.string().optional(),
|
|
33
|
-
tokenUrl: z.string().optional(),
|
|
34
|
-
deviceUrl: z.string().optional(),
|
|
35
|
-
revokeUrl: z.string().optional()
|
|
36
|
-
}).optional()
|
|
40
|
+
issuerUrl: z.string().optional()
|
|
37
41
|
});
|
|
38
42
|
/** ConfigFailure is a config file that cannot be read or does not match the schema. */
|
|
39
43
|
var ConfigFailure = class extends Error {
|
|
@@ -57,11 +61,8 @@ function toSettings(flags = {}, directory = process.cwd()) {
|
|
|
57
61
|
settings: {
|
|
58
62
|
apiUrl,
|
|
59
63
|
apiKey: pick("apiKey", flags.apiKey, "HARDFIN_API_KEY", file.apiKey),
|
|
60
|
-
clientId: pick("clientId", flags.clientId, "HARDFIN_CLIENT_ID", file.clientId),
|
|
61
|
-
|
|
62
|
-
tokenUrl: pick("tokenUrl", flags.tokenUrl, "HARDFIN_TOKEN_URL", file.auth?.tokenUrl, `${apiUrl}/auth/token`) ?? "",
|
|
63
|
-
deviceUrl: pick("deviceUrl", flags.deviceUrl, "HARDFIN_DEVICE_URL", file.auth?.deviceUrl, `${apiUrl}/auth/device`) ?? "",
|
|
64
|
-
revokeUrl: pick("revokeUrl", flags.revokeUrl, "HARDFIN_REVOKE_URL", file.auth?.revokeUrl, `${apiUrl}/auth/revoke`) ?? ""
|
|
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)) ?? ""
|
|
65
66
|
},
|
|
66
67
|
sources
|
|
67
68
|
};
|
|
@@ -108,6 +109,14 @@ function loadEnvFile(directory) {
|
|
|
108
109
|
function toTrimmedUrl(url) {
|
|
109
110
|
return url.replace(/\/+$/, "");
|
|
110
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
|
+
}
|
|
111
120
|
//#endregion
|
|
112
121
|
//#region src/output/writer.ts
|
|
113
122
|
/** writeData prints a command's result on stdout. */
|
|
@@ -141,7 +150,7 @@ function isJSONOutput(flags) {
|
|
|
141
150
|
return !process.stdout.isTTY;
|
|
142
151
|
}
|
|
143
152
|
/** version is what this build reports, read from the published package manifest. */
|
|
144
|
-
const version = createRequire(import.meta.url)("../package.json").version;
|
|
153
|
+
const version$1 = createRequire(import.meta.url)("../package.json").version;
|
|
145
154
|
//#endregion
|
|
146
155
|
//#region src/command/agent-guide.ts
|
|
147
156
|
const agentGuideCommand = defineCommand({
|
|
@@ -212,7 +221,7 @@ async function runAgentGuide(input) {
|
|
|
212
221
|
writeData(input.commands.map(toSummary));
|
|
213
222
|
return ExitCode.OK;
|
|
214
223
|
}
|
|
215
|
-
writeData(toGuide(input.commands, version));
|
|
224
|
+
writeData(toGuide(input.commands, version$1));
|
|
216
225
|
return ExitCode.OK;
|
|
217
226
|
}
|
|
218
227
|
function toSummary(command) {
|
|
@@ -232,6 +241,328 @@ function toSummary(command) {
|
|
|
232
241
|
};
|
|
233
242
|
}
|
|
234
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 request$1(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 request$1(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
|
+
/** The token endpoint answers a flat RFC 6749 body, not the API's envelope. */
|
|
327
|
+
async function request$1(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
|
+
const tokens = await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest);
|
|
556
|
+
held.set(settings.issuerUrl, tokens);
|
|
557
|
+
if (tokens.refreshToken && tokens.refreshToken !== latest) keep(settings.issuerUrl, tokens.refreshToken);
|
|
558
|
+
return tokens;
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/** forgetHeldTokens drops the access tokens this process is holding. */
|
|
562
|
+
function forgetHeldTokens() {
|
|
563
|
+
held.clear();
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
235
566
|
//#region src/http/client.ts
|
|
236
567
|
/** RequestFailure is a call the API refused, carrying what the envelope said. */
|
|
237
568
|
var RequestFailure = class extends Error {
|
|
@@ -251,7 +582,7 @@ async function request(options) {
|
|
|
251
582
|
const url = new URL(`${options.apiUrl}${toLeadingSlash(options.path)}`);
|
|
252
583
|
if (options.query) url.search = options.query.toString();
|
|
253
584
|
const headers = {
|
|
254
|
-
|
|
585
|
+
[options.credential.header]: options.credential.value,
|
|
255
586
|
"X-API-Version": API_VERSION,
|
|
256
587
|
Accept: "application/json"
|
|
257
588
|
};
|
|
@@ -352,11 +683,6 @@ const apiCommand = defineCommand({
|
|
|
352
683
|
run: runApi
|
|
353
684
|
});
|
|
354
685
|
async function runApi(input) {
|
|
355
|
-
const apiKey = input.resolved.settings.apiKey;
|
|
356
|
-
if (!apiKey) {
|
|
357
|
-
writeFailure("not authenticated. Set HARDFIN_API_KEY to an API key for your organization", input.isJSON);
|
|
358
|
-
return ExitCode.NOT_AUTHENTICATED;
|
|
359
|
-
}
|
|
360
686
|
const path = input.args[0];
|
|
361
687
|
if (!path) {
|
|
362
688
|
writeFailure("a path is required, such as /customer", input.isJSON);
|
|
@@ -378,7 +704,7 @@ async function runApi(input) {
|
|
|
378
704
|
try {
|
|
379
705
|
writeData((await request({
|
|
380
706
|
apiUrl: input.resolved.settings.apiUrl,
|
|
381
|
-
|
|
707
|
+
credential: await toRequestCredential(input.resolved.settings),
|
|
382
708
|
method: String(input.flags["method"] ?? "GET").toUpperCase(),
|
|
383
709
|
path,
|
|
384
710
|
query,
|
|
@@ -386,6 +712,10 @@ async function runApi(input) {
|
|
|
386
712
|
})).data);
|
|
387
713
|
return ExitCode.OK;
|
|
388
714
|
} catch (error) {
|
|
715
|
+
if (error instanceof NoCredential) {
|
|
716
|
+
writeFailure(error.message, input.isJSON);
|
|
717
|
+
return ExitCode.NOT_AUTHENTICATED;
|
|
718
|
+
}
|
|
389
719
|
if (error instanceof RequestFailure) {
|
|
390
720
|
writeFailure(error.message, input.isJSON, error.errors, error.requestId);
|
|
391
721
|
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
@@ -463,6 +793,401 @@ async function runConfig(input) {
|
|
|
463
793
|
return ExitCode.OK;
|
|
464
794
|
}
|
|
465
795
|
//#endregion
|
|
796
|
+
//#region src/auth/browser.ts
|
|
797
|
+
/** isWSL reports whether this Linux is running under Windows. */
|
|
798
|
+
function isWSL() {
|
|
799
|
+
return existsSync("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* toOpeners lists the ways to open a URL here, best first. WSL needs Windows to do it,
|
|
803
|
+
* because the browser the person is looking at is the Windows one, and xdg-open reaches
|
|
804
|
+
* nothing outside the distribution.
|
|
805
|
+
*/
|
|
806
|
+
function toOpeners(url) {
|
|
807
|
+
if (process.platform === "darwin") return [["open", [url]]];
|
|
808
|
+
if (process.platform === "win32") return [["cmd", [
|
|
809
|
+
"/c",
|
|
810
|
+
"start",
|
|
811
|
+
"",
|
|
812
|
+
url
|
|
813
|
+
]]];
|
|
814
|
+
if (isWSL()) return [
|
|
815
|
+
["wslview", [url]],
|
|
816
|
+
["powershell.exe", [
|
|
817
|
+
"-NoProfile",
|
|
818
|
+
"-NonInteractive",
|
|
819
|
+
"-Command",
|
|
820
|
+
"Start-Process",
|
|
821
|
+
`'${url}'`
|
|
822
|
+
]],
|
|
823
|
+
["explorer.exe", [url]]
|
|
824
|
+
];
|
|
825
|
+
return [["xdg-open", [url]]];
|
|
826
|
+
}
|
|
827
|
+
/** openBrowser asks the desktop to show a URL, and reports whether anything took it. */
|
|
828
|
+
function openBrowser(url) {
|
|
829
|
+
for (const [command, args] of toOpeners(url)) if (spawnSync(command, args, {
|
|
830
|
+
stdio: "ignore",
|
|
831
|
+
cwd: command.endsWith(".exe") ? "/mnt/c" : void 0,
|
|
832
|
+
timeout: 1e4
|
|
833
|
+
}).error === void 0) return true;
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
//#endregion
|
|
837
|
+
//#region src/auth/loopback.ts
|
|
838
|
+
/** The path the browser is sent back to, which the client metadata document registers. */
|
|
839
|
+
const CALLBACK_PATH = "/callback";
|
|
840
|
+
const HOST = "127.0.0.1";
|
|
841
|
+
/**
|
|
842
|
+
* toListener opens a loopback server for one authorization response. The port is whatever
|
|
843
|
+
* the machine hands out, which is why the server matches a loopback redirect by everything
|
|
844
|
+
* except its port.
|
|
845
|
+
*/
|
|
846
|
+
async function toListener(timeoutMs) {
|
|
847
|
+
let settle = () => {};
|
|
848
|
+
let fail = () => {};
|
|
849
|
+
const callback = new Promise((resolve, reject) => {
|
|
850
|
+
settle = resolve;
|
|
851
|
+
fail = reject;
|
|
852
|
+
});
|
|
853
|
+
const server = createServer((request, response) => {
|
|
854
|
+
const answered = toCallback(request);
|
|
855
|
+
if (answered === void 0) {
|
|
856
|
+
response.writeHead(404).end();
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
writePage(response, answered);
|
|
860
|
+
settle(answered);
|
|
861
|
+
});
|
|
862
|
+
await new Promise((resolve, reject) => {
|
|
863
|
+
server.once("error", reject);
|
|
864
|
+
server.listen(0, HOST, resolve);
|
|
865
|
+
});
|
|
866
|
+
const timer = setTimeout(() => {
|
|
867
|
+
fail(/* @__PURE__ */ new Error("the browser did not come back in time"));
|
|
868
|
+
close(server, timer);
|
|
869
|
+
}, timeoutMs);
|
|
870
|
+
return {
|
|
871
|
+
redirectUri: `http://${HOST}:${server.address().port}${CALLBACK_PATH}`,
|
|
872
|
+
callback: callback.finally(() => close(server, timer)),
|
|
873
|
+
close: () => close(server, timer)
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function toCallback(request) {
|
|
877
|
+
const url = new URL(request.url ?? "/", `http://${HOST}`);
|
|
878
|
+
if (url.pathname !== CALLBACK_PATH) return;
|
|
879
|
+
return {
|
|
880
|
+
source: "listener",
|
|
881
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
882
|
+
state: url.searchParams.get("state") ?? void 0,
|
|
883
|
+
issuer: url.searchParams.get("iss") ?? void 0,
|
|
884
|
+
error: url.searchParams.get("error") ?? void 0,
|
|
885
|
+
errorDescription: url.searchParams.get("error_description") ?? void 0
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function writePage(response, callback) {
|
|
889
|
+
const title = callback.error ? "Sign in refused" : "Signed in";
|
|
890
|
+
const detail = callback.error ? `${callback.error}${callback.errorDescription ? `: ${callback.errorDescription}` : ""}` : "You can close this tab and return to your terminal.";
|
|
891
|
+
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>`);
|
|
892
|
+
}
|
|
893
|
+
function close(server, timer) {
|
|
894
|
+
clearTimeout(timer);
|
|
895
|
+
server.close();
|
|
896
|
+
server.closeAllConnections();
|
|
897
|
+
}
|
|
898
|
+
//#endregion
|
|
899
|
+
//#region src/auth/pkce.ts
|
|
900
|
+
/** toPkce makes a fresh verifier and the challenge derived from it. */
|
|
901
|
+
function toPkce() {
|
|
902
|
+
const verifier = toUrlSafe(randomBytes(32));
|
|
903
|
+
return {
|
|
904
|
+
verifier,
|
|
905
|
+
challenge: toUrlSafe(createHash("sha256").update(verifier).digest()),
|
|
906
|
+
method: "S256"
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
/** toState makes the value that ties a callback to the request that started it. */
|
|
910
|
+
function toState() {
|
|
911
|
+
return toUrlSafe(randomBytes(16));
|
|
912
|
+
}
|
|
913
|
+
function toUrlSafe(bytes) {
|
|
914
|
+
return bytes.toString("base64url");
|
|
915
|
+
}
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/auth/clipboard.ts
|
|
918
|
+
/** toClipboardCommand names how this host takes text onto the clipboard. */
|
|
919
|
+
function toClipboardCommand() {
|
|
920
|
+
if (process.platform === "darwin") return ["pbcopy", []];
|
|
921
|
+
if (process.platform === "win32") return ["clip", []];
|
|
922
|
+
if (existsSync("/proc/sys/fs/binfmt_misc/WSLInterop")) return ["clip.exe", []];
|
|
923
|
+
if (process.env["WAYLAND_DISPLAY"]) return ["wl-copy", []];
|
|
924
|
+
return ["xclip", ["-selection", "clipboard"]];
|
|
925
|
+
}
|
|
926
|
+
/** copyToClipboard puts text on the clipboard, and reports whether anything took it. */
|
|
927
|
+
function copyToClipboard(text) {
|
|
928
|
+
const command = toClipboardCommand();
|
|
929
|
+
if (!command) return false;
|
|
930
|
+
const [name, args] = command;
|
|
931
|
+
const result = spawnSync(name, args, { input: text });
|
|
932
|
+
return result.error === void 0 && result.status === 0;
|
|
933
|
+
}
|
|
934
|
+
//#endregion
|
|
935
|
+
//#region src/auth/prompt.ts
|
|
936
|
+
const CTRL_C = "";
|
|
937
|
+
const BACKSPACE = "";
|
|
938
|
+
/**
|
|
939
|
+
* toPastedCallback reads what a person pasted. A browser that could not reach the listener
|
|
940
|
+
* leaves the whole redirect in the address bar, and a page that shows the code leaves
|
|
941
|
+
* `code#state`, so both are accepted alongside a bare code.
|
|
942
|
+
*/
|
|
943
|
+
function toPastedCallback(pasted) {
|
|
944
|
+
const value = pasted.trim();
|
|
945
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
946
|
+
const url = new URL(value);
|
|
947
|
+
return {
|
|
948
|
+
source: "pasted",
|
|
949
|
+
code: url.searchParams.get("code") ?? void 0,
|
|
950
|
+
state: url.searchParams.get("state") ?? void 0,
|
|
951
|
+
issuer: url.searchParams.get("iss") ?? void 0,
|
|
952
|
+
error: url.searchParams.get("error") ?? void 0,
|
|
953
|
+
errorDescription: url.searchParams.get("error_description") ?? void 0
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
const [code, state] = value.split("#");
|
|
957
|
+
return {
|
|
958
|
+
source: "pasted",
|
|
959
|
+
code: code || void 0,
|
|
960
|
+
state: state || void 0
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* toPrompt watches the keyboard while the browser is away. Pressing c copies the URL, and
|
|
965
|
+
* pasting a code finishes the sign in on a machine whose browser cannot reach this listener.
|
|
966
|
+
*/
|
|
967
|
+
function toPrompt(url) {
|
|
968
|
+
let settle = () => {};
|
|
969
|
+
let fail = () => {};
|
|
970
|
+
const pasted = new Promise((resolve, reject) => {
|
|
971
|
+
settle = resolve;
|
|
972
|
+
fail = reject;
|
|
973
|
+
});
|
|
974
|
+
const input = process.stdin;
|
|
975
|
+
if (!input.isTTY) return {
|
|
976
|
+
pasted,
|
|
977
|
+
close: () => {}
|
|
978
|
+
};
|
|
979
|
+
let typed = "";
|
|
980
|
+
const onData = (chunk) => {
|
|
981
|
+
for (const character of chunk) {
|
|
982
|
+
if (character === CTRL_C) {
|
|
983
|
+
fail(/* @__PURE__ */ new Error("sign in was cancelled"));
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (character === "\r" || character === "\n") {
|
|
987
|
+
if (typed.trim() === "") continue;
|
|
988
|
+
process.stderr.write("\n");
|
|
989
|
+
settle(toPastedCallback(typed));
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
if (character === BACKSPACE) {
|
|
993
|
+
typed = typed.slice(0, -1);
|
|
994
|
+
process.stderr.write("\b \b");
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if ((character === "c" || character === "C") && typed === "") {
|
|
998
|
+
process.stderr.write(copyToClipboard(url) ? "Copied the URL to your clipboard\n" : "Nothing on this host takes a clipboard\n");
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
typed += character;
|
|
1002
|
+
process.stderr.write(character);
|
|
1003
|
+
}
|
|
1004
|
+
};
|
|
1005
|
+
input.setRawMode(true);
|
|
1006
|
+
input.setEncoding("utf8");
|
|
1007
|
+
input.resume();
|
|
1008
|
+
input.on("data", onData);
|
|
1009
|
+
const close = () => {
|
|
1010
|
+
input.off("data", onData);
|
|
1011
|
+
input.setRawMode(false);
|
|
1012
|
+
input.pause();
|
|
1013
|
+
};
|
|
1014
|
+
return {
|
|
1015
|
+
pasted: pasted.finally(close),
|
|
1016
|
+
close
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
//#endregion
|
|
1020
|
+
//#region src/command/login.ts
|
|
1021
|
+
/** The scopes a sign-in asks for, with offline access so a refresh token comes back. */
|
|
1022
|
+
const DEFAULT_SCOPES = "hardfin:read hardfin:write offline_access";
|
|
1023
|
+
const WAIT_MS = 3e5;
|
|
1024
|
+
const loginCommand = defineCommand({
|
|
1025
|
+
name: "login",
|
|
1026
|
+
summary: "Sign in to Hardfin through a browser",
|
|
1027
|
+
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.",
|
|
1028
|
+
arguments: [],
|
|
1029
|
+
flags: [
|
|
1030
|
+
{
|
|
1031
|
+
name: "scope",
|
|
1032
|
+
description: "The scopes to ask for, separated by spaces",
|
|
1033
|
+
valueName: "scopes",
|
|
1034
|
+
schema: z.string(),
|
|
1035
|
+
defaultValue: DEFAULT_SCOPES
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
name: "no-browser",
|
|
1039
|
+
description: "Print the URL instead of opening it",
|
|
1040
|
+
schema: z.boolean()
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
name: "json",
|
|
1044
|
+
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
1045
|
+
schema: z.boolean()
|
|
1046
|
+
}
|
|
1047
|
+
],
|
|
1048
|
+
examples: [{
|
|
1049
|
+
description: "Sign in",
|
|
1050
|
+
command: "hardfin login"
|
|
1051
|
+
}, {
|
|
1052
|
+
description: "Sign in over SSH, opening the URL yourself",
|
|
1053
|
+
command: "hardfin login --no-browser"
|
|
1054
|
+
}],
|
|
1055
|
+
run: runLogin
|
|
1056
|
+
});
|
|
1057
|
+
async function runLogin(input) {
|
|
1058
|
+
const settings = input.resolved.settings;
|
|
1059
|
+
try {
|
|
1060
|
+
const clientId = settings.clientId;
|
|
1061
|
+
const metadata = await toMetadata(settings.issuerUrl);
|
|
1062
|
+
const listener = await toListener(WAIT_MS);
|
|
1063
|
+
const pkce = toPkce();
|
|
1064
|
+
const state = toState();
|
|
1065
|
+
const url = toAuthorizationUrl(metadata.authorization_endpoint, {
|
|
1066
|
+
clientId,
|
|
1067
|
+
redirectUri: listener.redirectUri,
|
|
1068
|
+
scope: String(input.flags["scope"] ?? DEFAULT_SCOPES),
|
|
1069
|
+
state,
|
|
1070
|
+
challenge: pkce.challenge
|
|
1071
|
+
});
|
|
1072
|
+
const opened = input.flags["noBrowser"] !== true && !process.env["HARDFIN_NO_BROWSER"] && openBrowser(url);
|
|
1073
|
+
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`);
|
|
1074
|
+
const prompt = toPrompt(url);
|
|
1075
|
+
if (process.stdin.isTTY) process.stderr.write("Press c to copy the URL, or paste the code or redirect URL here: ");
|
|
1076
|
+
const callback = await Promise.race([listener.callback, prompt.pasted]);
|
|
1077
|
+
listener.close();
|
|
1078
|
+
prompt.close();
|
|
1079
|
+
process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
|
|
1080
|
+
if (callback.error) {
|
|
1081
|
+
writeFailure(`sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
|
|
1082
|
+
return ExitCode.ERROR;
|
|
1083
|
+
}
|
|
1084
|
+
if (callback.source === "listener" ? callback.state !== state : callback.state !== void 0 && callback.state !== state) {
|
|
1085
|
+
writeFailure("the browser came back with a state this sign in did not send", input.isJSON);
|
|
1086
|
+
return ExitCode.ERROR;
|
|
1087
|
+
}
|
|
1088
|
+
if (callback.issuer !== void 0 && callback.issuer.replace(/\/+$/, "") !== metadata.issuer.replace(/\/+$/, "")) {
|
|
1089
|
+
writeFailure(`the browser came back from ${callback.issuer}, which is not ${metadata.issuer}`, input.isJSON);
|
|
1090
|
+
return ExitCode.ERROR;
|
|
1091
|
+
}
|
|
1092
|
+
if (!callback.code) {
|
|
1093
|
+
writeFailure("the browser came back without an authorization code", input.isJSON);
|
|
1094
|
+
return ExitCode.ERROR;
|
|
1095
|
+
}
|
|
1096
|
+
const tokens = await toTokensFromCode(metadata.token_endpoint, clientId, callback.code, listener.redirectUri, pkce);
|
|
1097
|
+
const refreshToken = tokens.refreshToken;
|
|
1098
|
+
if (!refreshToken) {
|
|
1099
|
+
writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
1100
|
+
return ExitCode.ERROR;
|
|
1101
|
+
}
|
|
1102
|
+
const backend = await withLock(() => keep(settings.issuerUrl, refreshToken));
|
|
1103
|
+
if (input.isJSON) {
|
|
1104
|
+
writeData({
|
|
1105
|
+
signedIn: true,
|
|
1106
|
+
issuer: metadata.issuer,
|
|
1107
|
+
scope: tokens.scope ?? null,
|
|
1108
|
+
storedIn: backend
|
|
1109
|
+
});
|
|
1110
|
+
return ExitCode.OK;
|
|
1111
|
+
}
|
|
1112
|
+
const stored = backend === "keyring" ? "your OS keyring" : toCredentialPath();
|
|
1113
|
+
writeData([
|
|
1114
|
+
`Signed in to ${metadata.issuer}`,
|
|
1115
|
+
`Scope ${tokens.scope ?? "as granted"}`,
|
|
1116
|
+
`Stored in ${stored}`,
|
|
1117
|
+
"",
|
|
1118
|
+
"Run hardfin status to see what this CLI is using"
|
|
1119
|
+
].join("\n"));
|
|
1120
|
+
return ExitCode.OK;
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (error instanceof DiscoveryFailure || error instanceof GrantFailure) {
|
|
1123
|
+
writeFailure(error.message, input.isJSON);
|
|
1124
|
+
return ExitCode.ERROR;
|
|
1125
|
+
}
|
|
1126
|
+
writeFailure(error instanceof Error ? error.message : String(error), input.isJSON);
|
|
1127
|
+
return ExitCode.ERROR;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
/** toAuthorizationUrl builds the URL the person approves this CLI at. */
|
|
1131
|
+
function toAuthorizationUrl(endpoint, request) {
|
|
1132
|
+
const url = new URL(endpoint);
|
|
1133
|
+
url.searchParams.set("response_type", "code");
|
|
1134
|
+
url.searchParams.set("client_id", request.clientId);
|
|
1135
|
+
url.searchParams.set("redirect_uri", request.redirectUri);
|
|
1136
|
+
url.searchParams.set("scope", request.scope);
|
|
1137
|
+
url.searchParams.set("state", request.state);
|
|
1138
|
+
url.searchParams.set("code_challenge", request.challenge);
|
|
1139
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
1140
|
+
return url.toString();
|
|
1141
|
+
}
|
|
1142
|
+
//#endregion
|
|
1143
|
+
//#region src/command/logout.ts
|
|
1144
|
+
const logoutCommand = defineCommand({
|
|
1145
|
+
name: "logout",
|
|
1146
|
+
summary: "Sign out, and tell Hardfin to forget this machine",
|
|
1147
|
+
description: "Removes the stored refresh token and asks the authorization server to revoke it, so a copy taken from this machine stops working.",
|
|
1148
|
+
arguments: [],
|
|
1149
|
+
flags: [{
|
|
1150
|
+
name: "json",
|
|
1151
|
+
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
1152
|
+
schema: z.boolean()
|
|
1153
|
+
}],
|
|
1154
|
+
examples: [{
|
|
1155
|
+
description: "Sign out",
|
|
1156
|
+
command: "hardfin logout"
|
|
1157
|
+
}],
|
|
1158
|
+
run: runLogout
|
|
1159
|
+
});
|
|
1160
|
+
async function runLogout(input) {
|
|
1161
|
+
const settings = input.resolved.settings;
|
|
1162
|
+
const stored = toCredential(settings.issuerUrl);
|
|
1163
|
+
if (!stored) {
|
|
1164
|
+
writeData({
|
|
1165
|
+
signedOut: false,
|
|
1166
|
+
issuer: settings.issuerUrl,
|
|
1167
|
+
reason: "no sign in was stored"
|
|
1168
|
+
});
|
|
1169
|
+
return ExitCode.OK;
|
|
1170
|
+
}
|
|
1171
|
+
let revoked = false;
|
|
1172
|
+
try {
|
|
1173
|
+
const metadata = await toMetadata(settings.issuerUrl);
|
|
1174
|
+
if (metadata.revocation_endpoint) {
|
|
1175
|
+
await revoke(metadata.revocation_endpoint, settings.clientId, stored.refreshToken);
|
|
1176
|
+
revoked = true;
|
|
1177
|
+
}
|
|
1178
|
+
} catch {
|
|
1179
|
+
revoked = false;
|
|
1180
|
+
}
|
|
1181
|
+
await withLock(() => forget(settings.issuerUrl));
|
|
1182
|
+
forgetHeldTokens();
|
|
1183
|
+
writeData({
|
|
1184
|
+
signedOut: true,
|
|
1185
|
+
issuer: settings.issuerUrl,
|
|
1186
|
+
revoked
|
|
1187
|
+
});
|
|
1188
|
+
return ExitCode.OK;
|
|
1189
|
+
}
|
|
1190
|
+
//#endregion
|
|
466
1191
|
//#region src/command/operation.ts
|
|
467
1192
|
const INPUT_FLAG = {
|
|
468
1193
|
name: "input",
|
|
@@ -490,11 +1215,6 @@ function defineOperation(operation) {
|
|
|
490
1215
|
};
|
|
491
1216
|
}
|
|
492
1217
|
async function runOperation(operation, input) {
|
|
493
|
-
const apiKey = input.resolved.settings.apiKey;
|
|
494
|
-
if (!apiKey) {
|
|
495
|
-
writeFailure("not authenticated. Set HARDFIN_API_KEY to an API key for your organization", input.isJSON);
|
|
496
|
-
return ExitCode.NOT_AUTHENTICATED;
|
|
497
|
-
}
|
|
498
1218
|
const path = toPath(operation, input.args);
|
|
499
1219
|
if (path === void 0) {
|
|
500
1220
|
writeFailure(`this command takes ${operation.pathParameters.length} argument(s)`, input.isJSON);
|
|
@@ -511,7 +1231,7 @@ async function runOperation(operation, input) {
|
|
|
511
1231
|
try {
|
|
512
1232
|
writeData((await request({
|
|
513
1233
|
apiUrl: input.resolved.settings.apiUrl,
|
|
514
|
-
|
|
1234
|
+
credential: await toRequestCredential(input.resolved.settings),
|
|
515
1235
|
method: operation.method,
|
|
516
1236
|
path,
|
|
517
1237
|
query: toQuery(operation, input.flags),
|
|
@@ -519,6 +1239,10 @@ async function runOperation(operation, input) {
|
|
|
519
1239
|
})).data);
|
|
520
1240
|
return ExitCode.OK;
|
|
521
1241
|
} catch (error) {
|
|
1242
|
+
if (error instanceof NoCredential) {
|
|
1243
|
+
writeFailure(error.message, input.isJSON);
|
|
1244
|
+
return ExitCode.NOT_AUTHENTICATED;
|
|
1245
|
+
}
|
|
522
1246
|
if (error instanceof RequestFailure) {
|
|
523
1247
|
writeFailure(error.message, input.isJSON, error.errors, error.requestId);
|
|
524
1248
|
return error.status === 401 ? ExitCode.NOT_AUTHENTICATED : ExitCode.ERROR;
|
|
@@ -557,452 +1281,471 @@ function toBody(source) {
|
|
|
557
1281
|
}
|
|
558
1282
|
}
|
|
559
1283
|
//#endregion
|
|
560
|
-
//#region src/command/
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
name: "execute",
|
|
776
|
-
summary: "Execute commands",
|
|
777
|
-
arguments: [],
|
|
778
|
-
flags: [],
|
|
779
|
-
examples: [],
|
|
780
|
-
subcommands: [defineOperation({
|
|
781
|
-
name: "create",
|
|
782
|
-
summary: "Execute asset move",
|
|
783
|
-
method: "POST",
|
|
784
|
-
path: "/asset/move/execute",
|
|
785
|
-
pathParameters: [],
|
|
786
|
-
queryFlags: [],
|
|
787
|
-
takesBody: true
|
|
788
|
-
})]
|
|
789
|
-
}, {
|
|
790
|
-
name: "plan",
|
|
791
|
-
summary: "Plan commands",
|
|
792
|
-
arguments: [],
|
|
793
|
-
flags: [],
|
|
794
|
-
examples: [],
|
|
795
|
-
subcommands: [defineOperation({
|
|
796
|
-
name: "create",
|
|
797
|
-
summary: "Plan asset move",
|
|
798
|
-
method: "POST",
|
|
799
|
-
path: "/asset/move/plan",
|
|
800
|
-
pathParameters: [],
|
|
801
|
-
queryFlags: [],
|
|
802
|
-
takesBody: true
|
|
803
|
-
})]
|
|
804
|
-
}]
|
|
805
|
-
},
|
|
806
|
-
defineOperation({
|
|
807
|
-
name: "get",
|
|
808
|
-
summary: "Get asset",
|
|
809
|
-
method: "GET",
|
|
810
|
-
path: "/asset/{assetKey}",
|
|
811
|
-
pathParameters: [{
|
|
812
|
-
name: "assetKey",
|
|
813
|
-
description: "The asset's key",
|
|
814
|
-
required: true
|
|
815
|
-
}],
|
|
816
|
-
queryFlags: [],
|
|
817
|
-
takesBody: false
|
|
818
|
-
}),
|
|
819
|
-
defineOperation({
|
|
820
|
-
name: "update",
|
|
821
|
-
summary: "Patch asset",
|
|
822
|
-
method: "PATCH",
|
|
823
|
-
path: "/asset/{assetKey}",
|
|
824
|
-
pathParameters: [{
|
|
825
|
-
name: "assetKey",
|
|
826
|
-
description: "The asset's key",
|
|
827
|
-
required: true
|
|
828
|
-
}],
|
|
829
|
-
queryFlags: [],
|
|
830
|
-
takesBody: true
|
|
831
|
-
}),
|
|
832
|
-
{
|
|
833
|
-
name: "accounting",
|
|
834
|
-
summary: "Accounting commands",
|
|
1284
|
+
//#region src/command/surface.generated.ts
|
|
1285
|
+
const surfaceCommands = [
|
|
1286
|
+
{
|
|
1287
|
+
name: "asset",
|
|
1288
|
+
summary: "Asset commands",
|
|
1289
|
+
arguments: [],
|
|
1290
|
+
flags: [],
|
|
1291
|
+
examples: [],
|
|
1292
|
+
subcommands: [
|
|
1293
|
+
defineOperation({
|
|
1294
|
+
name: "list",
|
|
1295
|
+
summary: "Get asset listing",
|
|
1296
|
+
method: "GET",
|
|
1297
|
+
path: "/asset",
|
|
1298
|
+
pathParameters: [],
|
|
1299
|
+
queryFlags: [
|
|
1300
|
+
{
|
|
1301
|
+
name: "page",
|
|
1302
|
+
queryName: "page",
|
|
1303
|
+
description: "The page to return, starting at 1",
|
|
1304
|
+
valueName: "number",
|
|
1305
|
+
schema: z.coerce.number()
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
name: "limit",
|
|
1309
|
+
queryName: "limit",
|
|
1310
|
+
description: "The number of records per page, from 1 to 100",
|
|
1311
|
+
valueName: "number",
|
|
1312
|
+
schema: z.coerce.number()
|
|
1313
|
+
},
|
|
1314
|
+
{
|
|
1315
|
+
name: "sort-by",
|
|
1316
|
+
queryName: "sortBy",
|
|
1317
|
+
description: "The field to sort by",
|
|
1318
|
+
valueName: "value",
|
|
1319
|
+
schema: z.enum([
|
|
1320
|
+
"serial",
|
|
1321
|
+
"project",
|
|
1322
|
+
"item",
|
|
1323
|
+
"location",
|
|
1324
|
+
"owner",
|
|
1325
|
+
"activity"
|
|
1326
|
+
])
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
name: "sort-order",
|
|
1330
|
+
queryName: "sortOrder",
|
|
1331
|
+
description: "The sort direction",
|
|
1332
|
+
valueName: "value",
|
|
1333
|
+
schema: z.enum(["ASC", "DESC"])
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
name: "archived",
|
|
1337
|
+
queryName: "archived",
|
|
1338
|
+
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1339
|
+
valueName: "value",
|
|
1340
|
+
schema: z.enum([
|
|
1341
|
+
"all",
|
|
1342
|
+
"false",
|
|
1343
|
+
"true"
|
|
1344
|
+
])
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
name: "search-query",
|
|
1348
|
+
queryName: "searchQuery",
|
|
1349
|
+
description: "Text to match against the serial, description, item, project, owner, and location, ignored when shorter than three characters",
|
|
1350
|
+
valueName: "value",
|
|
1351
|
+
schema: z.string()
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
name: "for-asset-id",
|
|
1355
|
+
queryName: "forAssetId",
|
|
1356
|
+
description: "The IDs of the assets to list",
|
|
1357
|
+
valueName: "value",
|
|
1358
|
+
repeatable: true,
|
|
1359
|
+
schema: z.array(z.string())
|
|
1360
|
+
},
|
|
1361
|
+
{
|
|
1362
|
+
name: "for-asset-key",
|
|
1363
|
+
queryName: "forAssetKey",
|
|
1364
|
+
description: "The keys of the assets to list",
|
|
1365
|
+
valueName: "value",
|
|
1366
|
+
repeatable: true,
|
|
1367
|
+
schema: z.array(z.string())
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
name: "for-customer",
|
|
1371
|
+
queryName: "forCustomer",
|
|
1372
|
+
description: "The IDs of the customers whose assets to list",
|
|
1373
|
+
valueName: "value",
|
|
1374
|
+
repeatable: true,
|
|
1375
|
+
schema: z.array(z.string())
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
name: "for-item",
|
|
1379
|
+
queryName: "forItem",
|
|
1380
|
+
description: "The IDs of the items whose assets to list",
|
|
1381
|
+
valueName: "value",
|
|
1382
|
+
repeatable: true,
|
|
1383
|
+
schema: z.array(z.string())
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
name: "at-site",
|
|
1387
|
+
queryName: "atSite",
|
|
1388
|
+
description: "The IDs of the locations whose assets to list",
|
|
1389
|
+
valueName: "value",
|
|
1390
|
+
repeatable: true,
|
|
1391
|
+
schema: z.array(z.string())
|
|
1392
|
+
},
|
|
1393
|
+
{
|
|
1394
|
+
name: "at-customer-sites",
|
|
1395
|
+
queryName: "atCustomerSites",
|
|
1396
|
+
description: "The IDs of the customers whose sites to list assets at",
|
|
1397
|
+
valueName: "value",
|
|
1398
|
+
repeatable: true,
|
|
1399
|
+
schema: z.array(z.string())
|
|
1400
|
+
},
|
|
1401
|
+
{
|
|
1402
|
+
name: "with-functional-statuses",
|
|
1403
|
+
queryName: "withFunctionalStatuses",
|
|
1404
|
+
description: "The functional statuses to list, where SCRAPPED lists scrapped assets",
|
|
1405
|
+
valueName: "value",
|
|
1406
|
+
repeatable: true,
|
|
1407
|
+
schema: z.array(z.enum([
|
|
1408
|
+
"FUNCTIONAL",
|
|
1409
|
+
"NEEDS_REVIEW",
|
|
1410
|
+
"NON-FUNCTIONAL",
|
|
1411
|
+
"SCRAPPED"
|
|
1412
|
+
]))
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
name: "with-transit-statuses",
|
|
1416
|
+
queryName: "withTransitStatuses",
|
|
1417
|
+
description: "The transit statuses to list",
|
|
1418
|
+
valueName: "value",
|
|
1419
|
+
repeatable: true,
|
|
1420
|
+
schema: z.array(z.enum([
|
|
1421
|
+
"IN_TRANSIT",
|
|
1422
|
+
"IN_TRANSIT_TO_FIELD",
|
|
1423
|
+
"IN_TRANSIT_TO_INVENTORY",
|
|
1424
|
+
"NOT_IN_TRANSIT"
|
|
1425
|
+
]))
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
name: "with-inventory-statuses",
|
|
1429
|
+
queryName: "withInventoryStatuses",
|
|
1430
|
+
description: "In_inventory, not_in_inventory, or both, which filters only when one is sent",
|
|
1431
|
+
valueName: "value",
|
|
1432
|
+
repeatable: true,
|
|
1433
|
+
schema: z.array(z.string())
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
name: "with-location-company-type",
|
|
1437
|
+
queryName: "withLocationCompanyType",
|
|
1438
|
+
description: "Customer, manufacturer, or both, for assets at customer sites or your own, which filters only when one is sent",
|
|
1439
|
+
valueName: "value",
|
|
1440
|
+
repeatable: true,
|
|
1441
|
+
schema: z.array(z.string())
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
name: "with-project-status",
|
|
1445
|
+
queryName: "withProjectStatus",
|
|
1446
|
+
description: "The project assignments to list: upcoming, active, past, or none",
|
|
1447
|
+
valueName: "value",
|
|
1448
|
+
repeatable: true,
|
|
1449
|
+
schema: z.array(z.string())
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
name: "with-owner",
|
|
1453
|
+
queryName: "withOwner",
|
|
1454
|
+
description: "Manufacturer for assets your organization owns, customer for assets customers own, or both",
|
|
1455
|
+
valueName: "value",
|
|
1456
|
+
repeatable: true,
|
|
1457
|
+
schema: z.array(z.string())
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
name: "for-project",
|
|
1461
|
+
queryName: "forProject",
|
|
1462
|
+
description: "The IDs of the projects whose assets to list",
|
|
1463
|
+
valueName: "value",
|
|
1464
|
+
repeatable: true,
|
|
1465
|
+
schema: z.array(z.string())
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
name: "scrapped",
|
|
1469
|
+
queryName: "scrapped",
|
|
1470
|
+
description: "Whether to list unscrapped assets, scrapped assets, or all of them",
|
|
1471
|
+
valueName: "value",
|
|
1472
|
+
schema: z.enum([
|
|
1473
|
+
"all",
|
|
1474
|
+
"false",
|
|
1475
|
+
"true"
|
|
1476
|
+
])
|
|
1477
|
+
}
|
|
1478
|
+
],
|
|
1479
|
+
takesBody: false
|
|
1480
|
+
}),
|
|
1481
|
+
defineOperation({
|
|
1482
|
+
name: "create",
|
|
1483
|
+
summary: "Create asset",
|
|
1484
|
+
method: "POST",
|
|
1485
|
+
path: "/asset",
|
|
1486
|
+
pathParameters: [],
|
|
1487
|
+
queryFlags: [],
|
|
1488
|
+
takesBody: true
|
|
1489
|
+
}),
|
|
1490
|
+
{
|
|
1491
|
+
name: "move",
|
|
1492
|
+
summary: "Move commands",
|
|
1493
|
+
arguments: [],
|
|
1494
|
+
flags: [],
|
|
1495
|
+
examples: [],
|
|
1496
|
+
subcommands: [{
|
|
1497
|
+
name: "execute",
|
|
1498
|
+
summary: "Execute commands",
|
|
835
1499
|
arguments: [],
|
|
836
1500
|
flags: [],
|
|
837
1501
|
examples: [],
|
|
838
1502
|
subcommands: [defineOperation({
|
|
839
|
-
name: "
|
|
840
|
-
summary: "
|
|
841
|
-
method: "
|
|
842
|
-
path: "/asset/
|
|
843
|
-
pathParameters: [
|
|
844
|
-
name: "assetKey",
|
|
845
|
-
description: "The asset's key",
|
|
846
|
-
required: true
|
|
847
|
-
}],
|
|
1503
|
+
name: "create",
|
|
1504
|
+
summary: "Execute asset move",
|
|
1505
|
+
method: "POST",
|
|
1506
|
+
path: "/asset/move/execute",
|
|
1507
|
+
pathParameters: [],
|
|
848
1508
|
queryFlags: [],
|
|
849
1509
|
takesBody: true
|
|
850
|
-
})
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
flags: [],
|
|
855
|
-
examples: [],
|
|
856
|
-
subcommands: [defineOperation({
|
|
857
|
-
name: "update",
|
|
858
|
-
summary: "Toggle in service date management",
|
|
859
|
-
method: "PATCH",
|
|
860
|
-
path: "/asset/{assetKey}/accounting/in-service-management",
|
|
861
|
-
pathParameters: [{
|
|
862
|
-
name: "assetKey",
|
|
863
|
-
description: "The asset's key",
|
|
864
|
-
required: true
|
|
865
|
-
}],
|
|
866
|
-
queryFlags: [],
|
|
867
|
-
takesBody: true
|
|
868
|
-
})]
|
|
869
|
-
}]
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
name: "cost-adjustment",
|
|
873
|
-
summary: "Cost adjustment commands",
|
|
1510
|
+
})]
|
|
1511
|
+
}, {
|
|
1512
|
+
name: "plan",
|
|
1513
|
+
summary: "Plan commands",
|
|
874
1514
|
arguments: [],
|
|
875
1515
|
flags: [],
|
|
876
1516
|
examples: [],
|
|
877
1517
|
subcommands: [defineOperation({
|
|
878
1518
|
name: "create",
|
|
879
|
-
summary: "
|
|
1519
|
+
summary: "Plan asset move",
|
|
880
1520
|
method: "POST",
|
|
881
|
-
path: "/asset/
|
|
882
|
-
pathParameters: [
|
|
883
|
-
name: "assetKey",
|
|
884
|
-
description: "The asset's key",
|
|
885
|
-
required: true
|
|
886
|
-
}],
|
|
1521
|
+
path: "/asset/move/plan",
|
|
1522
|
+
pathParameters: [],
|
|
887
1523
|
queryFlags: [],
|
|
888
1524
|
takesBody: true
|
|
889
1525
|
})]
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
}],
|
|
940
|
-
takesBody: false
|
|
941
|
-
}), defineOperation({
|
|
942
|
-
name: "get",
|
|
943
|
-
summary: "Get asset event group",
|
|
944
|
-
method: "GET",
|
|
945
|
-
path: "/asset/{assetKey}/event-group/{eventGroupKey}",
|
|
946
|
-
pathParameters: [{
|
|
947
|
-
name: "assetKey",
|
|
948
|
-
description: "The asset's key",
|
|
949
|
-
required: true
|
|
950
|
-
}, {
|
|
951
|
-
name: "eventGroupKey",
|
|
952
|
-
description: "The event group's key",
|
|
953
|
-
required: true
|
|
954
|
-
}],
|
|
955
|
-
queryFlags: [],
|
|
956
|
-
takesBody: false
|
|
957
|
-
})]
|
|
958
|
-
},
|
|
959
|
-
{
|
|
960
|
-
name: "file",
|
|
961
|
-
summary: "File commands",
|
|
1526
|
+
}]
|
|
1527
|
+
},
|
|
1528
|
+
defineOperation({
|
|
1529
|
+
name: "get",
|
|
1530
|
+
summary: "Get asset",
|
|
1531
|
+
method: "GET",
|
|
1532
|
+
path: "/asset/{assetKey}",
|
|
1533
|
+
pathParameters: [{
|
|
1534
|
+
name: "assetKey",
|
|
1535
|
+
description: "The asset's key",
|
|
1536
|
+
required: true
|
|
1537
|
+
}],
|
|
1538
|
+
queryFlags: [],
|
|
1539
|
+
takesBody: false
|
|
1540
|
+
}),
|
|
1541
|
+
defineOperation({
|
|
1542
|
+
name: "update",
|
|
1543
|
+
summary: "Patch asset",
|
|
1544
|
+
method: "PATCH",
|
|
1545
|
+
path: "/asset/{assetKey}",
|
|
1546
|
+
pathParameters: [{
|
|
1547
|
+
name: "assetKey",
|
|
1548
|
+
description: "The asset's key",
|
|
1549
|
+
required: true
|
|
1550
|
+
}],
|
|
1551
|
+
queryFlags: [],
|
|
1552
|
+
takesBody: true
|
|
1553
|
+
}),
|
|
1554
|
+
{
|
|
1555
|
+
name: "accounting",
|
|
1556
|
+
summary: "Accounting commands",
|
|
1557
|
+
arguments: [],
|
|
1558
|
+
flags: [],
|
|
1559
|
+
examples: [],
|
|
1560
|
+
subcommands: [defineOperation({
|
|
1561
|
+
name: "update",
|
|
1562
|
+
summary: "Update asset accounting",
|
|
1563
|
+
method: "PATCH",
|
|
1564
|
+
path: "/asset/{assetKey}/accounting",
|
|
1565
|
+
pathParameters: [{
|
|
1566
|
+
name: "assetKey",
|
|
1567
|
+
description: "The asset's key",
|
|
1568
|
+
required: true
|
|
1569
|
+
}],
|
|
1570
|
+
queryFlags: [],
|
|
1571
|
+
takesBody: true
|
|
1572
|
+
}), {
|
|
1573
|
+
name: "in-service-management",
|
|
1574
|
+
summary: "In service management commands",
|
|
962
1575
|
arguments: [],
|
|
963
1576
|
flags: [],
|
|
964
1577
|
examples: [],
|
|
965
1578
|
subcommands: [defineOperation({
|
|
966
|
-
name: "
|
|
967
|
-
summary: "
|
|
968
|
-
method: "
|
|
969
|
-
path: "/asset/{assetKey}/
|
|
970
|
-
pathParameters: [{
|
|
971
|
-
name: "assetKey",
|
|
972
|
-
description: "The asset's key",
|
|
973
|
-
required: true
|
|
974
|
-
}],
|
|
975
|
-
queryFlags: [],
|
|
976
|
-
takesBody: false
|
|
977
|
-
}), defineOperation({
|
|
978
|
-
name: "delete",
|
|
979
|
-
summary: "Delete asset file",
|
|
980
|
-
method: "DELETE",
|
|
981
|
-
path: "/asset/{assetKey}/file/{fileKey}",
|
|
1579
|
+
name: "update",
|
|
1580
|
+
summary: "Toggle in service date management",
|
|
1581
|
+
method: "PATCH",
|
|
1582
|
+
path: "/asset/{assetKey}/accounting/in-service-management",
|
|
982
1583
|
pathParameters: [{
|
|
983
1584
|
name: "assetKey",
|
|
984
1585
|
description: "The asset's key",
|
|
985
1586
|
required: true
|
|
986
|
-
}, {
|
|
987
|
-
name: "fileKey",
|
|
988
|
-
description: "The file's key",
|
|
989
|
-
required: true
|
|
990
1587
|
}],
|
|
991
1588
|
queryFlags: [],
|
|
992
|
-
takesBody:
|
|
1589
|
+
takesBody: true
|
|
993
1590
|
})]
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1591
|
+
}]
|
|
1592
|
+
},
|
|
1593
|
+
{
|
|
1594
|
+
name: "cost-adjustment",
|
|
1595
|
+
summary: "Cost adjustment commands",
|
|
1596
|
+
arguments: [],
|
|
1597
|
+
flags: [],
|
|
1598
|
+
examples: [],
|
|
1599
|
+
subcommands: [defineOperation({
|
|
1600
|
+
name: "create",
|
|
1601
|
+
summary: "Create asset cost adjustment",
|
|
1602
|
+
method: "POST",
|
|
1603
|
+
path: "/asset/{assetKey}/cost-adjustment",
|
|
1604
|
+
pathParameters: [{
|
|
1605
|
+
name: "assetKey",
|
|
1606
|
+
description: "The asset's key",
|
|
1607
|
+
required: true
|
|
1608
|
+
}],
|
|
1609
|
+
queryFlags: [],
|
|
1610
|
+
takesBody: true
|
|
1611
|
+
})]
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
name: "event",
|
|
1615
|
+
summary: "Event commands",
|
|
1616
|
+
arguments: [],
|
|
1617
|
+
flags: [],
|
|
1618
|
+
examples: [],
|
|
1619
|
+
subcommands: [defineOperation({
|
|
1620
|
+
name: "list",
|
|
1621
|
+
summary: "Get asset event list",
|
|
1622
|
+
method: "GET",
|
|
1623
|
+
path: "/asset/{assetKey}/event",
|
|
1624
|
+
pathParameters: [{
|
|
1625
|
+
name: "assetKey",
|
|
1626
|
+
description: "The asset's key",
|
|
1627
|
+
required: true
|
|
1628
|
+
}],
|
|
1629
|
+
queryFlags: [],
|
|
1630
|
+
takesBody: false
|
|
1631
|
+
})]
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
name: "event-group",
|
|
1635
|
+
summary: "Event group commands",
|
|
1636
|
+
arguments: [],
|
|
1637
|
+
flags: [],
|
|
1638
|
+
examples: [],
|
|
1639
|
+
subcommands: [defineOperation({
|
|
1640
|
+
name: "list",
|
|
1641
|
+
summary: "Get asset event group listing",
|
|
1642
|
+
method: "GET",
|
|
1643
|
+
path: "/asset/{assetKey}/event-group",
|
|
1644
|
+
pathParameters: [{
|
|
1645
|
+
name: "assetKey",
|
|
1646
|
+
description: "The asset's key",
|
|
1647
|
+
required: true
|
|
1648
|
+
}],
|
|
1649
|
+
queryFlags: [{
|
|
1650
|
+
name: "start",
|
|
1651
|
+
queryName: "start",
|
|
1652
|
+
description: "The earliest an event group may have happened, or null to read from the first",
|
|
1653
|
+
valueName: "value",
|
|
1654
|
+
schema: z.string()
|
|
1655
|
+
}, {
|
|
1656
|
+
name: "end",
|
|
1657
|
+
queryName: "end",
|
|
1658
|
+
description: "The latest an event group may have happened, or null to read through the last",
|
|
1659
|
+
valueName: "value",
|
|
1660
|
+
schema: z.string()
|
|
1661
|
+
}],
|
|
1662
|
+
takesBody: false
|
|
1663
|
+
}), defineOperation({
|
|
1664
|
+
name: "get",
|
|
1665
|
+
summary: "Get asset event group",
|
|
1666
|
+
method: "GET",
|
|
1667
|
+
path: "/asset/{assetKey}/event-group/{eventGroupKey}",
|
|
1668
|
+
pathParameters: [{
|
|
1669
|
+
name: "assetKey",
|
|
1670
|
+
description: "The asset's key",
|
|
1671
|
+
required: true
|
|
1672
|
+
}, {
|
|
1673
|
+
name: "eventGroupKey",
|
|
1674
|
+
description: "The event group's key",
|
|
1675
|
+
required: true
|
|
1676
|
+
}],
|
|
1677
|
+
queryFlags: [],
|
|
1678
|
+
takesBody: false
|
|
1679
|
+
})]
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
name: "file",
|
|
1683
|
+
summary: "File commands",
|
|
1684
|
+
arguments: [],
|
|
1685
|
+
flags: [],
|
|
1686
|
+
examples: [],
|
|
1687
|
+
subcommands: [defineOperation({
|
|
1688
|
+
name: "list",
|
|
1689
|
+
summary: "Get asset files",
|
|
1690
|
+
method: "GET",
|
|
1691
|
+
path: "/asset/{assetKey}/file",
|
|
1692
|
+
pathParameters: [{
|
|
1693
|
+
name: "assetKey",
|
|
1694
|
+
description: "The asset's key",
|
|
1695
|
+
required: true
|
|
1696
|
+
}],
|
|
1697
|
+
queryFlags: [],
|
|
1698
|
+
takesBody: false
|
|
1699
|
+
}), defineOperation({
|
|
1700
|
+
name: "delete",
|
|
1701
|
+
summary: "Delete asset file",
|
|
1702
|
+
method: "DELETE",
|
|
1703
|
+
path: "/asset/{assetKey}/file/{fileKey}",
|
|
1704
|
+
pathParameters: [{
|
|
1705
|
+
name: "assetKey",
|
|
1706
|
+
description: "The asset's key",
|
|
1707
|
+
required: true
|
|
1708
|
+
}, {
|
|
1709
|
+
name: "fileKey",
|
|
1710
|
+
description: "The file's key",
|
|
1711
|
+
required: true
|
|
1712
|
+
}],
|
|
1713
|
+
queryFlags: [],
|
|
1714
|
+
takesBody: false
|
|
1715
|
+
})]
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
name: "functional-status",
|
|
1719
|
+
summary: "Functional status commands",
|
|
1720
|
+
arguments: [],
|
|
1721
|
+
flags: [],
|
|
1722
|
+
examples: [],
|
|
1723
|
+
subcommands: [defineOperation({
|
|
1724
|
+
name: "list",
|
|
1725
|
+
summary: "Get asset functional status history",
|
|
1726
|
+
method: "GET",
|
|
1727
|
+
path: "/asset/{assetKey}/functional-status",
|
|
1728
|
+
pathParameters: [{
|
|
1729
|
+
name: "assetKey",
|
|
1730
|
+
description: "The asset's key",
|
|
1731
|
+
required: true
|
|
1732
|
+
}],
|
|
1733
|
+
queryFlags: [],
|
|
1734
|
+
takesBody: false
|
|
1735
|
+
})]
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
name: "ownership",
|
|
1739
|
+
summary: "Ownership commands",
|
|
1740
|
+
arguments: [],
|
|
1741
|
+
flags: [],
|
|
1742
|
+
examples: [],
|
|
1743
|
+
subcommands: [
|
|
1744
|
+
defineOperation({
|
|
1002
1745
|
name: "list",
|
|
1003
|
-
summary: "Get asset
|
|
1746
|
+
summary: "Get asset ownership history",
|
|
1004
1747
|
method: "GET",
|
|
1005
|
-
path: "/asset/{assetKey}/
|
|
1748
|
+
path: "/asset/{assetKey}/ownership",
|
|
1006
1749
|
pathParameters: [{
|
|
1007
1750
|
name: "assetKey",
|
|
1008
1751
|
description: "The asset's key",
|
|
@@ -1010,118 +1753,12 @@ const commands = [
|
|
|
1010
1753
|
}],
|
|
1011
1754
|
queryFlags: [],
|
|
1012
1755
|
takesBody: false
|
|
1013
|
-
})
|
|
1014
|
-
|
|
1015
|
-
{
|
|
1016
|
-
name: "ownership",
|
|
1017
|
-
summary: "Ownership commands",
|
|
1018
|
-
arguments: [],
|
|
1019
|
-
flags: [],
|
|
1020
|
-
examples: [],
|
|
1021
|
-
subcommands: [
|
|
1022
|
-
defineOperation({
|
|
1023
|
-
name: "list",
|
|
1024
|
-
summary: "Get asset ownership history",
|
|
1025
|
-
method: "GET",
|
|
1026
|
-
path: "/asset/{assetKey}/ownership",
|
|
1027
|
-
pathParameters: [{
|
|
1028
|
-
name: "assetKey",
|
|
1029
|
-
description: "The asset's key",
|
|
1030
|
-
required: true
|
|
1031
|
-
}],
|
|
1032
|
-
queryFlags: [],
|
|
1033
|
-
takesBody: false
|
|
1034
|
-
}),
|
|
1035
|
-
defineOperation({
|
|
1036
|
-
name: "create",
|
|
1037
|
-
summary: "Create asset ownership",
|
|
1038
|
-
method: "POST",
|
|
1039
|
-
path: "/asset/{assetKey}/ownership",
|
|
1040
|
-
pathParameters: [{
|
|
1041
|
-
name: "assetKey",
|
|
1042
|
-
description: "The asset's key",
|
|
1043
|
-
required: true
|
|
1044
|
-
}],
|
|
1045
|
-
queryFlags: [],
|
|
1046
|
-
takesBody: true
|
|
1047
|
-
}),
|
|
1048
|
-
defineOperation({
|
|
1049
|
-
name: "clear",
|
|
1050
|
-
summary: "Clear the ownership an asset holds today",
|
|
1051
|
-
method: "DELETE",
|
|
1052
|
-
path: "/asset/{assetKey}/ownership",
|
|
1053
|
-
pathParameters: [{
|
|
1054
|
-
name: "assetKey",
|
|
1055
|
-
description: "The asset's key",
|
|
1056
|
-
required: true
|
|
1057
|
-
}],
|
|
1058
|
-
queryFlags: [],
|
|
1059
|
-
takesBody: true
|
|
1060
|
-
}),
|
|
1061
|
-
defineOperation({
|
|
1062
|
-
name: "get",
|
|
1063
|
-
summary: "Get asset ownership segment",
|
|
1064
|
-
method: "GET",
|
|
1065
|
-
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1066
|
-
pathParameters: [{
|
|
1067
|
-
name: "assetKey",
|
|
1068
|
-
description: "The asset's key",
|
|
1069
|
-
required: true
|
|
1070
|
-
}, {
|
|
1071
|
-
name: "segmentKey",
|
|
1072
|
-
description: "The ownership segment's key",
|
|
1073
|
-
required: true
|
|
1074
|
-
}],
|
|
1075
|
-
queryFlags: [],
|
|
1076
|
-
takesBody: false
|
|
1077
|
-
}),
|
|
1078
|
-
defineOperation({
|
|
1079
|
-
name: "update",
|
|
1080
|
-
summary: "Patch asset ownership segment",
|
|
1081
|
-
method: "PATCH",
|
|
1082
|
-
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1083
|
-
pathParameters: [{
|
|
1084
|
-
name: "assetKey",
|
|
1085
|
-
description: "The asset's key",
|
|
1086
|
-
required: true
|
|
1087
|
-
}, {
|
|
1088
|
-
name: "segmentKey",
|
|
1089
|
-
description: "The ownership segment's key",
|
|
1090
|
-
required: true
|
|
1091
|
-
}],
|
|
1092
|
-
queryFlags: [],
|
|
1093
|
-
takesBody: true
|
|
1094
|
-
}),
|
|
1095
|
-
defineOperation({
|
|
1096
|
-
name: "delete",
|
|
1097
|
-
summary: "Delete asset ownership segment",
|
|
1098
|
-
method: "DELETE",
|
|
1099
|
-
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1100
|
-
pathParameters: [{
|
|
1101
|
-
name: "assetKey",
|
|
1102
|
-
description: "The asset's key",
|
|
1103
|
-
required: true
|
|
1104
|
-
}, {
|
|
1105
|
-
name: "segmentKey",
|
|
1106
|
-
description: "The ownership segment's key",
|
|
1107
|
-
required: true
|
|
1108
|
-
}],
|
|
1109
|
-
queryFlags: [],
|
|
1110
|
-
takesBody: false
|
|
1111
|
-
})
|
|
1112
|
-
]
|
|
1113
|
-
},
|
|
1114
|
-
{
|
|
1115
|
-
name: "scrap",
|
|
1116
|
-
summary: "Scrap commands",
|
|
1117
|
-
arguments: [],
|
|
1118
|
-
flags: [],
|
|
1119
|
-
examples: [],
|
|
1120
|
-
subcommands: [defineOperation({
|
|
1756
|
+
}),
|
|
1757
|
+
defineOperation({
|
|
1121
1758
|
name: "create",
|
|
1122
|
-
summary: "
|
|
1759
|
+
summary: "Create asset ownership",
|
|
1123
1760
|
method: "POST",
|
|
1124
|
-
path: "/asset/{assetKey}/
|
|
1761
|
+
path: "/asset/{assetKey}/ownership",
|
|
1125
1762
|
pathParameters: [{
|
|
1126
1763
|
name: "assetKey",
|
|
1127
1764
|
description: "The asset's key",
|
|
@@ -1129,348 +1766,452 @@ const commands = [
|
|
|
1129
1766
|
}],
|
|
1130
1767
|
queryFlags: [],
|
|
1131
1768
|
takesBody: true
|
|
1132
|
-
})
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
flags: [],
|
|
1139
|
-
examples: [],
|
|
1140
|
-
subcommands: [defineOperation({
|
|
1141
|
-
name: "create",
|
|
1142
|
-
summary: "Unscrap asset",
|
|
1143
|
-
method: "POST",
|
|
1144
|
-
path: "/asset/{assetKey}/unscrap",
|
|
1769
|
+
}),
|
|
1770
|
+
defineOperation({
|
|
1771
|
+
name: "clear",
|
|
1772
|
+
summary: "Clear the ownership an asset holds today",
|
|
1773
|
+
method: "DELETE",
|
|
1774
|
+
path: "/asset/{assetKey}/ownership",
|
|
1145
1775
|
pathParameters: [{
|
|
1146
1776
|
name: "assetKey",
|
|
1147
1777
|
description: "The asset's key",
|
|
1148
1778
|
required: true
|
|
1149
1779
|
}],
|
|
1150
1780
|
queryFlags: [],
|
|
1151
|
-
takesBody:
|
|
1152
|
-
})
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
summary: "URL link commands",
|
|
1157
|
-
arguments: [],
|
|
1158
|
-
flags: [],
|
|
1159
|
-
examples: [],
|
|
1160
|
-
subcommands: [defineOperation({
|
|
1161
|
-
name: "list",
|
|
1162
|
-
summary: "Get asset URL links",
|
|
1781
|
+
takesBody: true
|
|
1782
|
+
}),
|
|
1783
|
+
defineOperation({
|
|
1784
|
+
name: "get",
|
|
1785
|
+
summary: "Get asset ownership segment",
|
|
1163
1786
|
method: "GET",
|
|
1164
|
-
path: "/asset/{assetKey}/
|
|
1787
|
+
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1165
1788
|
pathParameters: [{
|
|
1166
1789
|
name: "assetKey",
|
|
1167
1790
|
description: "The asset's key",
|
|
1168
1791
|
required: true
|
|
1792
|
+
}, {
|
|
1793
|
+
name: "segmentKey",
|
|
1794
|
+
description: "The ownership segment's key",
|
|
1795
|
+
required: true
|
|
1169
1796
|
}],
|
|
1170
1797
|
queryFlags: [],
|
|
1171
1798
|
takesBody: false
|
|
1172
|
-
}),
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1799
|
+
}),
|
|
1800
|
+
defineOperation({
|
|
1801
|
+
name: "update",
|
|
1802
|
+
summary: "Patch asset ownership segment",
|
|
1803
|
+
method: "PATCH",
|
|
1804
|
+
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1177
1805
|
pathParameters: [{
|
|
1178
1806
|
name: "assetKey",
|
|
1179
1807
|
description: "The asset's key",
|
|
1180
1808
|
required: true
|
|
1809
|
+
}, {
|
|
1810
|
+
name: "segmentKey",
|
|
1811
|
+
description: "The ownership segment's key",
|
|
1812
|
+
required: true
|
|
1181
1813
|
}],
|
|
1182
1814
|
queryFlags: [],
|
|
1183
1815
|
takesBody: true
|
|
1184
|
-
})
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
flags: [],
|
|
1191
|
-
examples: [],
|
|
1192
|
-
subcommands: [defineOperation({
|
|
1193
|
-
name: "create",
|
|
1194
|
-
summary: "Create asset useful life revision",
|
|
1195
|
-
method: "POST",
|
|
1196
|
-
path: "/asset/{assetKey}/useful-life-revision",
|
|
1816
|
+
}),
|
|
1817
|
+
defineOperation({
|
|
1818
|
+
name: "delete",
|
|
1819
|
+
summary: "Delete asset ownership segment",
|
|
1820
|
+
method: "DELETE",
|
|
1821
|
+
path: "/asset/{assetKey}/ownership/{segmentKey}",
|
|
1197
1822
|
pathParameters: [{
|
|
1198
1823
|
name: "assetKey",
|
|
1199
1824
|
description: "The asset's key",
|
|
1200
1825
|
required: true
|
|
1826
|
+
}, {
|
|
1827
|
+
name: "segmentKey",
|
|
1828
|
+
description: "The ownership segment's key",
|
|
1829
|
+
required: true
|
|
1201
1830
|
}],
|
|
1202
1831
|
queryFlags: [],
|
|
1203
|
-
takesBody:
|
|
1204
|
-
})
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
subcommands: [
|
|
1215
|
-
defineOperation({
|
|
1216
|
-
name: "list",
|
|
1217
|
-
summary: "Get customers",
|
|
1218
|
-
method: "GET",
|
|
1219
|
-
path: "/customer",
|
|
1220
|
-
pathParameters: [],
|
|
1221
|
-
queryFlags: [
|
|
1222
|
-
{
|
|
1223
|
-
name: "page",
|
|
1224
|
-
queryName: "page",
|
|
1225
|
-
description: "The page to return, starting at 1",
|
|
1226
|
-
valueName: "number",
|
|
1227
|
-
schema: z.coerce.number()
|
|
1228
|
-
},
|
|
1229
|
-
{
|
|
1230
|
-
name: "limit",
|
|
1231
|
-
queryName: "limit",
|
|
1232
|
-
description: "The number of records per page, from 1 to 100",
|
|
1233
|
-
valueName: "number",
|
|
1234
|
-
schema: z.coerce.number()
|
|
1235
|
-
},
|
|
1236
|
-
{
|
|
1237
|
-
name: "sort-by",
|
|
1238
|
-
queryName: "sortBy",
|
|
1239
|
-
description: "The field to sort by",
|
|
1240
|
-
valueName: "value",
|
|
1241
|
-
schema: z.string()
|
|
1242
|
-
},
|
|
1243
|
-
{
|
|
1244
|
-
name: "sort-order",
|
|
1245
|
-
queryName: "sortOrder",
|
|
1246
|
-
description: "The sort direction",
|
|
1247
|
-
valueName: "value",
|
|
1248
|
-
schema: z.enum(["ASC", "DESC"])
|
|
1249
|
-
},
|
|
1250
|
-
{
|
|
1251
|
-
name: "archived",
|
|
1252
|
-
queryName: "archived",
|
|
1253
|
-
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1254
|
-
valueName: "value",
|
|
1255
|
-
schema: z.enum([
|
|
1256
|
-
"all",
|
|
1257
|
-
"false",
|
|
1258
|
-
"true"
|
|
1259
|
-
])
|
|
1260
|
-
},
|
|
1261
|
-
{
|
|
1262
|
-
name: "is-customer",
|
|
1263
|
-
queryName: "isCustomer",
|
|
1264
|
-
description: "True to return only customers, or false to return only non-customers",
|
|
1265
|
-
schema: z.boolean()
|
|
1266
|
-
},
|
|
1267
|
-
{
|
|
1268
|
-
name: "is-supplier",
|
|
1269
|
-
queryName: "isSupplier",
|
|
1270
|
-
description: "True to return only suppliers, or false to return only non-suppliers",
|
|
1271
|
-
schema: z.boolean()
|
|
1272
|
-
},
|
|
1273
|
-
{
|
|
1274
|
-
name: "sync-statuses",
|
|
1275
|
-
queryName: "syncStatuses",
|
|
1276
|
-
description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
|
|
1277
|
-
valueName: "value",
|
|
1278
|
-
repeatable: true,
|
|
1279
|
-
schema: z.array(z.string())
|
|
1280
|
-
},
|
|
1281
|
-
{
|
|
1282
|
-
name: "search",
|
|
1283
|
-
queryName: "search",
|
|
1284
|
-
description: "Text to match against customer names",
|
|
1285
|
-
valueName: "value",
|
|
1286
|
-
schema: z.string()
|
|
1287
|
-
}
|
|
1288
|
-
],
|
|
1289
|
-
takesBody: false
|
|
1290
|
-
}),
|
|
1291
|
-
defineOperation({
|
|
1832
|
+
takesBody: false
|
|
1833
|
+
})
|
|
1834
|
+
]
|
|
1835
|
+
},
|
|
1836
|
+
{
|
|
1837
|
+
name: "scrap",
|
|
1838
|
+
summary: "Scrap commands",
|
|
1839
|
+
arguments: [],
|
|
1840
|
+
flags: [],
|
|
1841
|
+
examples: [],
|
|
1842
|
+
subcommands: [defineOperation({
|
|
1292
1843
|
name: "create",
|
|
1293
|
-
summary: "
|
|
1844
|
+
summary: "Scrap asset",
|
|
1294
1845
|
method: "POST",
|
|
1295
|
-
path: "/
|
|
1296
|
-
pathParameters: [
|
|
1846
|
+
path: "/asset/{assetKey}/scrap",
|
|
1847
|
+
pathParameters: [{
|
|
1848
|
+
name: "assetKey",
|
|
1849
|
+
description: "The asset's key",
|
|
1850
|
+
required: true
|
|
1851
|
+
}],
|
|
1297
1852
|
queryFlags: [],
|
|
1298
1853
|
takesBody: true
|
|
1299
|
-
})
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1854
|
+
})]
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
name: "unscrap",
|
|
1858
|
+
summary: "Unscrap commands",
|
|
1859
|
+
arguments: [],
|
|
1860
|
+
flags: [],
|
|
1861
|
+
examples: [],
|
|
1862
|
+
subcommands: [defineOperation({
|
|
1863
|
+
name: "create",
|
|
1864
|
+
summary: "Unscrap asset",
|
|
1865
|
+
method: "POST",
|
|
1866
|
+
path: "/asset/{assetKey}/unscrap",
|
|
1867
|
+
pathParameters: [{
|
|
1868
|
+
name: "assetKey",
|
|
1869
|
+
description: "The asset's key",
|
|
1870
|
+
required: true
|
|
1871
|
+
}],
|
|
1872
|
+
queryFlags: [],
|
|
1873
|
+
takesBody: false
|
|
1874
|
+
})]
|
|
1875
|
+
},
|
|
1876
|
+
{
|
|
1877
|
+
name: "url-link",
|
|
1878
|
+
summary: "URL link commands",
|
|
1879
|
+
arguments: [],
|
|
1880
|
+
flags: [],
|
|
1881
|
+
examples: [],
|
|
1882
|
+
subcommands: [defineOperation({
|
|
1883
|
+
name: "list",
|
|
1884
|
+
summary: "Get asset URL links",
|
|
1303
1885
|
method: "GET",
|
|
1304
|
-
path: "/
|
|
1886
|
+
path: "/asset/{assetKey}/url-link",
|
|
1305
1887
|
pathParameters: [{
|
|
1306
|
-
name: "
|
|
1307
|
-
description: "The
|
|
1888
|
+
name: "assetKey",
|
|
1889
|
+
description: "The asset's key",
|
|
1308
1890
|
required: true
|
|
1309
1891
|
}],
|
|
1310
1892
|
queryFlags: [],
|
|
1311
1893
|
takesBody: false
|
|
1312
|
-
}),
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
path: "/customer/{customerKey}",
|
|
1894
|
+
}), defineOperation({
|
|
1895
|
+
name: "create",
|
|
1896
|
+
summary: "Create asset URL link",
|
|
1897
|
+
method: "POST",
|
|
1898
|
+
path: "/asset/{assetKey}/url-link",
|
|
1318
1899
|
pathParameters: [{
|
|
1319
|
-
name: "
|
|
1320
|
-
description: "The
|
|
1900
|
+
name: "assetKey",
|
|
1901
|
+
description: "The asset's key",
|
|
1321
1902
|
required: true
|
|
1322
1903
|
}],
|
|
1323
1904
|
queryFlags: [],
|
|
1324
1905
|
takesBody: true
|
|
1325
|
-
})
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1906
|
+
})]
|
|
1907
|
+
},
|
|
1908
|
+
{
|
|
1909
|
+
name: "useful-life-revision",
|
|
1910
|
+
summary: "Useful life revision commands",
|
|
1911
|
+
arguments: [],
|
|
1912
|
+
flags: [],
|
|
1913
|
+
examples: [],
|
|
1914
|
+
subcommands: [defineOperation({
|
|
1915
|
+
name: "create",
|
|
1916
|
+
summary: "Create asset useful life revision",
|
|
1917
|
+
method: "POST",
|
|
1918
|
+
path: "/asset/{assetKey}/useful-life-revision",
|
|
1919
|
+
pathParameters: [{
|
|
1920
|
+
name: "assetKey",
|
|
1921
|
+
description: "The asset's key",
|
|
1922
|
+
required: true
|
|
1923
|
+
}],
|
|
1924
|
+
queryFlags: [],
|
|
1925
|
+
takesBody: true
|
|
1926
|
+
})]
|
|
1927
|
+
}
|
|
1928
|
+
]
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
name: "customer",
|
|
1932
|
+
summary: "Customer commands",
|
|
1933
|
+
arguments: [],
|
|
1934
|
+
flags: [],
|
|
1935
|
+
examples: [],
|
|
1936
|
+
subcommands: [
|
|
1937
|
+
defineOperation({
|
|
1938
|
+
name: "list",
|
|
1939
|
+
summary: "Get customers",
|
|
1940
|
+
method: "GET",
|
|
1941
|
+
path: "/customer",
|
|
1942
|
+
pathParameters: [],
|
|
1943
|
+
queryFlags: [
|
|
1944
|
+
{
|
|
1945
|
+
name: "page",
|
|
1946
|
+
queryName: "page",
|
|
1947
|
+
description: "The page to return, starting at 1",
|
|
1948
|
+
valueName: "number",
|
|
1949
|
+
schema: z.coerce.number()
|
|
1950
|
+
},
|
|
1951
|
+
{
|
|
1952
|
+
name: "limit",
|
|
1953
|
+
queryName: "limit",
|
|
1954
|
+
description: "The number of records per page, from 1 to 100",
|
|
1955
|
+
valueName: "number",
|
|
1956
|
+
schema: z.coerce.number()
|
|
1957
|
+
},
|
|
1958
|
+
{
|
|
1959
|
+
name: "sort-by",
|
|
1960
|
+
queryName: "sortBy",
|
|
1961
|
+
description: "The field to sort by",
|
|
1962
|
+
valueName: "value",
|
|
1963
|
+
schema: z.string()
|
|
1964
|
+
},
|
|
1965
|
+
{
|
|
1966
|
+
name: "sort-order",
|
|
1967
|
+
queryName: "sortOrder",
|
|
1968
|
+
description: "The sort direction",
|
|
1969
|
+
valueName: "value",
|
|
1970
|
+
schema: z.enum(["ASC", "DESC"])
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
name: "archived",
|
|
1974
|
+
queryName: "archived",
|
|
1975
|
+
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1976
|
+
valueName: "value",
|
|
1977
|
+
schema: z.enum([
|
|
1978
|
+
"all",
|
|
1979
|
+
"false",
|
|
1980
|
+
"true"
|
|
1981
|
+
])
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
name: "is-customer",
|
|
1985
|
+
queryName: "isCustomer",
|
|
1986
|
+
description: "True to return only customers, or false to return only non-customers",
|
|
1987
|
+
schema: z.boolean()
|
|
1988
|
+
},
|
|
1989
|
+
{
|
|
1990
|
+
name: "is-supplier",
|
|
1991
|
+
queryName: "isSupplier",
|
|
1992
|
+
description: "True to return only suppliers, or false to return only non-suppliers",
|
|
1993
|
+
schema: z.boolean()
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
name: "sync-statuses",
|
|
1997
|
+
queryName: "syncStatuses",
|
|
1998
|
+
description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
|
|
1999
|
+
valueName: "value",
|
|
2000
|
+
repeatable: true,
|
|
2001
|
+
schema: z.array(z.string())
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
name: "search",
|
|
2005
|
+
queryName: "search",
|
|
2006
|
+
description: "Text to match against customer names",
|
|
2007
|
+
valueName: "value",
|
|
2008
|
+
schema: z.string()
|
|
2009
|
+
}
|
|
2010
|
+
],
|
|
2011
|
+
takesBody: false
|
|
2012
|
+
}),
|
|
2013
|
+
defineOperation({
|
|
1335
2014
|
name: "create",
|
|
1336
|
-
summary: "
|
|
2015
|
+
summary: "Create customer",
|
|
1337
2016
|
method: "POST",
|
|
1338
|
-
path: "/
|
|
2017
|
+
path: "/customer",
|
|
1339
2018
|
pathParameters: [],
|
|
1340
2019
|
queryFlags: [],
|
|
1341
2020
|
takesBody: true
|
|
1342
|
-
}),
|
|
2021
|
+
}),
|
|
2022
|
+
defineOperation({
|
|
1343
2023
|
name: "get",
|
|
1344
|
-
summary: "Get
|
|
2024
|
+
summary: "Get customer",
|
|
1345
2025
|
method: "GET",
|
|
1346
|
-
path: "/
|
|
2026
|
+
path: "/customer/{customerKey}",
|
|
1347
2027
|
pathParameters: [{
|
|
1348
|
-
name: "
|
|
1349
|
-
description: "The
|
|
2028
|
+
name: "customerKey",
|
|
2029
|
+
description: "The customer's key",
|
|
1350
2030
|
required: true
|
|
1351
2031
|
}],
|
|
1352
|
-
queryFlags: [
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
2032
|
+
queryFlags: [],
|
|
2033
|
+
takesBody: false
|
|
2034
|
+
}),
|
|
2035
|
+
defineOperation({
|
|
2036
|
+
name: "update",
|
|
2037
|
+
summary: "Patch customer",
|
|
2038
|
+
method: "PATCH",
|
|
2039
|
+
path: "/customer/{customerKey}",
|
|
2040
|
+
pathParameters: [{
|
|
2041
|
+
name: "customerKey",
|
|
2042
|
+
description: "The customer's key",
|
|
2043
|
+
required: true
|
|
1357
2044
|
}],
|
|
2045
|
+
queryFlags: [],
|
|
2046
|
+
takesBody: true
|
|
2047
|
+
})
|
|
2048
|
+
]
|
|
2049
|
+
},
|
|
2050
|
+
{
|
|
2051
|
+
name: "file",
|
|
2052
|
+
summary: "File commands",
|
|
2053
|
+
arguments: [],
|
|
2054
|
+
flags: [],
|
|
2055
|
+
examples: [],
|
|
2056
|
+
subcommands: [defineOperation({
|
|
2057
|
+
name: "create",
|
|
2058
|
+
summary: "Upload file",
|
|
2059
|
+
method: "POST",
|
|
2060
|
+
path: "/file",
|
|
2061
|
+
pathParameters: [],
|
|
2062
|
+
queryFlags: [],
|
|
2063
|
+
takesBody: true
|
|
2064
|
+
}), defineOperation({
|
|
2065
|
+
name: "get",
|
|
2066
|
+
summary: "Get file",
|
|
2067
|
+
method: "GET",
|
|
2068
|
+
path: "/file/{fileKey}",
|
|
2069
|
+
pathParameters: [{
|
|
2070
|
+
name: "fileKey",
|
|
2071
|
+
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",
|
|
2072
|
+
required: true
|
|
2073
|
+
}],
|
|
2074
|
+
queryFlags: [{
|
|
2075
|
+
name: "attachment",
|
|
2076
|
+
queryName: "attachment",
|
|
2077
|
+
description: "True when the file downloads as an attachment rather than opening inline",
|
|
2078
|
+
schema: z.boolean()
|
|
2079
|
+
}],
|
|
2080
|
+
takesBody: false
|
|
2081
|
+
})]
|
|
2082
|
+
},
|
|
2083
|
+
{
|
|
2084
|
+
name: "item",
|
|
2085
|
+
summary: "Item commands",
|
|
2086
|
+
arguments: [],
|
|
2087
|
+
flags: [],
|
|
2088
|
+
examples: [],
|
|
2089
|
+
subcommands: [
|
|
2090
|
+
defineOperation({
|
|
2091
|
+
name: "list",
|
|
2092
|
+
summary: "Get items",
|
|
2093
|
+
method: "GET",
|
|
2094
|
+
path: "/item",
|
|
2095
|
+
pathParameters: [],
|
|
2096
|
+
queryFlags: [
|
|
2097
|
+
{
|
|
2098
|
+
name: "type",
|
|
2099
|
+
queryName: "type",
|
|
2100
|
+
description: "The item type to list, SERVICE, DEVICE, or BULK, or absent for every type",
|
|
2101
|
+
valueName: "value",
|
|
2102
|
+
schema: z.enum([
|
|
2103
|
+
"BULK",
|
|
2104
|
+
"DEVICE",
|
|
2105
|
+
"SERVICE"
|
|
2106
|
+
])
|
|
2107
|
+
},
|
|
2108
|
+
{
|
|
2109
|
+
name: "search",
|
|
2110
|
+
queryName: "search",
|
|
2111
|
+
description: "Text to match against item names, SKUs, and descriptions, in any case",
|
|
2112
|
+
valueName: "value",
|
|
2113
|
+
schema: z.string()
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
name: "page",
|
|
2117
|
+
queryName: "page",
|
|
2118
|
+
description: "The page to return, starting at 1",
|
|
2119
|
+
valueName: "number",
|
|
2120
|
+
schema: z.coerce.number()
|
|
2121
|
+
},
|
|
2122
|
+
{
|
|
2123
|
+
name: "limit",
|
|
2124
|
+
queryName: "limit",
|
|
2125
|
+
description: "The number of records per page, from 1 to 100",
|
|
2126
|
+
valueName: "number",
|
|
2127
|
+
schema: z.coerce.number()
|
|
2128
|
+
},
|
|
2129
|
+
{
|
|
2130
|
+
name: "sort-by",
|
|
2131
|
+
queryName: "sortBy",
|
|
2132
|
+
description: "The field to sort by",
|
|
2133
|
+
valueName: "value",
|
|
2134
|
+
schema: z.enum([
|
|
2135
|
+
"name",
|
|
2136
|
+
"sku",
|
|
2137
|
+
"type",
|
|
2138
|
+
"lastUpdatedAt"
|
|
2139
|
+
])
|
|
2140
|
+
},
|
|
2141
|
+
{
|
|
2142
|
+
name: "sort-order",
|
|
2143
|
+
queryName: "sortOrder",
|
|
2144
|
+
description: "The sort direction",
|
|
2145
|
+
valueName: "value",
|
|
2146
|
+
schema: z.enum(["ASC", "DESC"])
|
|
2147
|
+
},
|
|
2148
|
+
{
|
|
2149
|
+
name: "archived",
|
|
2150
|
+
queryName: "archived",
|
|
2151
|
+
description: "Whether to return unarchived records, archived records, or all of them",
|
|
2152
|
+
valueName: "value",
|
|
2153
|
+
schema: z.enum([
|
|
2154
|
+
"all",
|
|
2155
|
+
"false",
|
|
2156
|
+
"true"
|
|
2157
|
+
])
|
|
2158
|
+
},
|
|
2159
|
+
{
|
|
2160
|
+
name: "exclude-linked-integration",
|
|
2161
|
+
queryName: "excludeLinkedIntegration",
|
|
2162
|
+
description: "An integration whose already-linked items to leave out",
|
|
2163
|
+
valueName: "value",
|
|
2164
|
+
schema: z.string()
|
|
2165
|
+
}
|
|
2166
|
+
],
|
|
1358
2167
|
takesBody: false
|
|
1359
|
-
})
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
queryName: "limit",
|
|
1403
|
-
description: "The number of records per page, from 1 to 100",
|
|
1404
|
-
valueName: "number",
|
|
1405
|
-
schema: z.coerce.number()
|
|
1406
|
-
},
|
|
1407
|
-
{
|
|
1408
|
-
name: "sort-by",
|
|
1409
|
-
queryName: "sortBy",
|
|
1410
|
-
description: "The field to sort by",
|
|
1411
|
-
valueName: "value",
|
|
1412
|
-
schema: z.enum([
|
|
1413
|
-
"name",
|
|
1414
|
-
"sku",
|
|
1415
|
-
"type",
|
|
1416
|
-
"lastUpdatedAt"
|
|
1417
|
-
])
|
|
1418
|
-
},
|
|
1419
|
-
{
|
|
1420
|
-
name: "sort-order",
|
|
1421
|
-
queryName: "sortOrder",
|
|
1422
|
-
description: "The sort direction",
|
|
1423
|
-
valueName: "value",
|
|
1424
|
-
schema: z.enum(["ASC", "DESC"])
|
|
1425
|
-
},
|
|
1426
|
-
{
|
|
1427
|
-
name: "archived",
|
|
1428
|
-
queryName: "archived",
|
|
1429
|
-
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1430
|
-
valueName: "value",
|
|
1431
|
-
schema: z.enum([
|
|
1432
|
-
"all",
|
|
1433
|
-
"false",
|
|
1434
|
-
"true"
|
|
1435
|
-
])
|
|
1436
|
-
},
|
|
1437
|
-
{
|
|
1438
|
-
name: "exclude-linked-integration",
|
|
1439
|
-
queryName: "excludeLinkedIntegration",
|
|
1440
|
-
description: "An integration whose already-linked items to leave out",
|
|
1441
|
-
valueName: "value",
|
|
1442
|
-
schema: z.string()
|
|
1443
|
-
}
|
|
1444
|
-
],
|
|
1445
|
-
takesBody: false
|
|
1446
|
-
}),
|
|
1447
|
-
defineOperation({
|
|
1448
|
-
name: "create",
|
|
1449
|
-
summary: "Create item",
|
|
1450
|
-
method: "POST",
|
|
1451
|
-
path: "/item",
|
|
1452
|
-
pathParameters: [],
|
|
1453
|
-
queryFlags: [],
|
|
1454
|
-
takesBody: true
|
|
1455
|
-
}),
|
|
1456
|
-
defineOperation({
|
|
1457
|
-
name: "get",
|
|
1458
|
-
summary: "Get item",
|
|
1459
|
-
method: "GET",
|
|
1460
|
-
path: "/item/{itemKey}",
|
|
1461
|
-
pathParameters: [{
|
|
1462
|
-
name: "itemKey",
|
|
1463
|
-
description: "The item's key",
|
|
1464
|
-
required: true
|
|
1465
|
-
}],
|
|
1466
|
-
queryFlags: [],
|
|
1467
|
-
takesBody: false
|
|
1468
|
-
}),
|
|
1469
|
-
defineOperation({
|
|
2168
|
+
}),
|
|
2169
|
+
defineOperation({
|
|
2170
|
+
name: "create",
|
|
2171
|
+
summary: "Create item",
|
|
2172
|
+
method: "POST",
|
|
2173
|
+
path: "/item",
|
|
2174
|
+
pathParameters: [],
|
|
2175
|
+
queryFlags: [],
|
|
2176
|
+
takesBody: true
|
|
2177
|
+
}),
|
|
2178
|
+
defineOperation({
|
|
2179
|
+
name: "get",
|
|
2180
|
+
summary: "Get item",
|
|
2181
|
+
method: "GET",
|
|
2182
|
+
path: "/item/{itemKey}",
|
|
2183
|
+
pathParameters: [{
|
|
2184
|
+
name: "itemKey",
|
|
2185
|
+
description: "The item's key",
|
|
2186
|
+
required: true
|
|
2187
|
+
}],
|
|
2188
|
+
queryFlags: [],
|
|
2189
|
+
takesBody: false
|
|
2190
|
+
}),
|
|
2191
|
+
defineOperation({
|
|
2192
|
+
name: "update",
|
|
2193
|
+
summary: "Update item",
|
|
2194
|
+
method: "PATCH",
|
|
2195
|
+
path: "/item/{itemKey}",
|
|
2196
|
+
pathParameters: [{
|
|
2197
|
+
name: "itemKey",
|
|
2198
|
+
description: "The item's key",
|
|
2199
|
+
required: true
|
|
2200
|
+
}],
|
|
2201
|
+
queryFlags: [],
|
|
2202
|
+
takesBody: true
|
|
2203
|
+
}),
|
|
2204
|
+
{
|
|
2205
|
+
name: "accounting",
|
|
2206
|
+
summary: "Accounting commands",
|
|
2207
|
+
arguments: [],
|
|
2208
|
+
flags: [],
|
|
2209
|
+
examples: [],
|
|
2210
|
+
subcommands: [defineOperation({
|
|
1470
2211
|
name: "update",
|
|
1471
|
-
summary: "Update item",
|
|
2212
|
+
summary: "Update item accounting",
|
|
1472
2213
|
method: "PATCH",
|
|
1473
|
-
path: "/item/{itemKey}",
|
|
2214
|
+
path: "/item/{itemKey}/accounting",
|
|
1474
2215
|
pathParameters: [{
|
|
1475
2216
|
name: "itemKey",
|
|
1476
2217
|
description: "The item's key",
|
|
@@ -1478,301 +2219,561 @@ const commands = [
|
|
|
1478
2219
|
}],
|
|
1479
2220
|
queryFlags: [],
|
|
1480
2221
|
takesBody: true
|
|
1481
|
-
})
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
2222
|
+
})]
|
|
2223
|
+
},
|
|
2224
|
+
{
|
|
2225
|
+
name: "field",
|
|
2226
|
+
summary: "Field commands",
|
|
2227
|
+
arguments: [],
|
|
2228
|
+
flags: [],
|
|
2229
|
+
examples: [],
|
|
2230
|
+
subcommands: [
|
|
2231
|
+
defineOperation({
|
|
2232
|
+
name: "create",
|
|
2233
|
+
summary: "Create item field",
|
|
2234
|
+
method: "POST",
|
|
2235
|
+
path: "/item/{itemKey}/field",
|
|
2236
|
+
pathParameters: [{
|
|
2237
|
+
name: "itemKey",
|
|
2238
|
+
description: "The item's key",
|
|
2239
|
+
required: true
|
|
2240
|
+
}],
|
|
2241
|
+
queryFlags: [],
|
|
2242
|
+
takesBody: true
|
|
2243
|
+
}),
|
|
2244
|
+
defineOperation({
|
|
1489
2245
|
name: "update",
|
|
1490
|
-
summary: "Update item
|
|
2246
|
+
summary: "Update item field",
|
|
1491
2247
|
method: "PATCH",
|
|
1492
|
-
path: "/item/{itemKey}/
|
|
2248
|
+
path: "/item/{itemKey}/field/{fieldKey}",
|
|
1493
2249
|
pathParameters: [{
|
|
1494
2250
|
name: "itemKey",
|
|
1495
2251
|
description: "The item's key",
|
|
1496
2252
|
required: true
|
|
2253
|
+
}, {
|
|
2254
|
+
name: "fieldKey",
|
|
2255
|
+
description: "The field's key",
|
|
2256
|
+
required: true
|
|
1497
2257
|
}],
|
|
1498
2258
|
queryFlags: [],
|
|
1499
2259
|
takesBody: true
|
|
1500
|
-
})
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
flags: [],
|
|
1507
|
-
examples: [],
|
|
1508
|
-
subcommands: [
|
|
1509
|
-
defineOperation({
|
|
1510
|
-
name: "create",
|
|
1511
|
-
summary: "Create item field",
|
|
1512
|
-
method: "POST",
|
|
1513
|
-
path: "/item/{itemKey}/field",
|
|
1514
|
-
pathParameters: [{
|
|
1515
|
-
name: "itemKey",
|
|
1516
|
-
description: "The item's key",
|
|
1517
|
-
required: true
|
|
1518
|
-
}],
|
|
1519
|
-
queryFlags: [],
|
|
1520
|
-
takesBody: true
|
|
1521
|
-
}),
|
|
1522
|
-
defineOperation({
|
|
1523
|
-
name: "update",
|
|
1524
|
-
summary: "Update item field",
|
|
1525
|
-
method: "PATCH",
|
|
1526
|
-
path: "/item/{itemKey}/field/{fieldKey}",
|
|
1527
|
-
pathParameters: [{
|
|
1528
|
-
name: "itemKey",
|
|
1529
|
-
description: "The item's key",
|
|
1530
|
-
required: true
|
|
1531
|
-
}, {
|
|
1532
|
-
name: "fieldKey",
|
|
1533
|
-
description: "The field's key",
|
|
1534
|
-
required: true
|
|
1535
|
-
}],
|
|
1536
|
-
queryFlags: [],
|
|
1537
|
-
takesBody: true
|
|
1538
|
-
}),
|
|
1539
|
-
defineOperation({
|
|
1540
|
-
name: "delete",
|
|
1541
|
-
summary: "Delete item field",
|
|
1542
|
-
method: "DELETE",
|
|
1543
|
-
path: "/item/{itemKey}/field/{fieldKey}",
|
|
1544
|
-
pathParameters: [{
|
|
1545
|
-
name: "itemKey",
|
|
1546
|
-
description: "The item's key",
|
|
1547
|
-
required: true
|
|
1548
|
-
}, {
|
|
1549
|
-
name: "fieldKey",
|
|
1550
|
-
description: "The field's key",
|
|
1551
|
-
required: true
|
|
1552
|
-
}],
|
|
1553
|
-
queryFlags: [],
|
|
1554
|
-
takesBody: false
|
|
1555
|
-
})
|
|
1556
|
-
]
|
|
1557
|
-
}
|
|
1558
|
-
]
|
|
1559
|
-
},
|
|
1560
|
-
{
|
|
1561
|
-
name: "location",
|
|
1562
|
-
summary: "Location commands",
|
|
1563
|
-
arguments: [],
|
|
1564
|
-
flags: [],
|
|
1565
|
-
examples: [],
|
|
1566
|
-
subcommands: [
|
|
1567
|
-
defineOperation({
|
|
1568
|
-
name: "list",
|
|
1569
|
-
summary: "Get location listing",
|
|
1570
|
-
method: "GET",
|
|
1571
|
-
path: "/location",
|
|
1572
|
-
pathParameters: [],
|
|
1573
|
-
queryFlags: [
|
|
1574
|
-
{
|
|
1575
|
-
name: "page",
|
|
1576
|
-
queryName: "page",
|
|
1577
|
-
description: "The page to return, starting at 1",
|
|
1578
|
-
valueName: "number",
|
|
1579
|
-
schema: z.coerce.number()
|
|
1580
|
-
},
|
|
1581
|
-
{
|
|
1582
|
-
name: "limit",
|
|
1583
|
-
queryName: "limit",
|
|
1584
|
-
description: "The number of records per page, from 1 to 100",
|
|
1585
|
-
valueName: "number",
|
|
1586
|
-
schema: z.coerce.number()
|
|
1587
|
-
},
|
|
1588
|
-
{
|
|
1589
|
-
name: "sort-by",
|
|
1590
|
-
queryName: "sortBy",
|
|
1591
|
-
description: "The field to sort by",
|
|
1592
|
-
valueName: "value",
|
|
1593
|
-
schema: z.enum([
|
|
1594
|
-
"name",
|
|
1595
|
-
"company",
|
|
1596
|
-
"assetCount"
|
|
1597
|
-
])
|
|
1598
|
-
},
|
|
1599
|
-
{
|
|
1600
|
-
name: "sort-order",
|
|
1601
|
-
queryName: "sortOrder",
|
|
1602
|
-
description: "The sort direction",
|
|
1603
|
-
valueName: "value",
|
|
1604
|
-
schema: z.enum(["ASC", "DESC"])
|
|
1605
|
-
},
|
|
1606
|
-
{
|
|
1607
|
-
name: "archived",
|
|
1608
|
-
queryName: "archived",
|
|
1609
|
-
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1610
|
-
valueName: "value",
|
|
1611
|
-
schema: z.enum([
|
|
1612
|
-
"all",
|
|
1613
|
-
"false",
|
|
1614
|
-
"true"
|
|
1615
|
-
])
|
|
1616
|
-
},
|
|
1617
|
-
{
|
|
1618
|
-
name: "is-transient",
|
|
1619
|
-
queryName: "isTransient",
|
|
1620
|
-
description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
|
|
1621
|
-
valueName: "value",
|
|
1622
|
-
schema: z.enum([
|
|
1623
|
-
"all",
|
|
1624
|
-
"false",
|
|
1625
|
-
"true"
|
|
1626
|
-
])
|
|
1627
|
-
},
|
|
1628
|
-
{
|
|
1629
|
-
name: "search",
|
|
1630
|
-
queryName: "search",
|
|
1631
|
-
description: "Text to match against location and customer names",
|
|
1632
|
-
valueName: "value",
|
|
1633
|
-
schema: z.string()
|
|
1634
|
-
},
|
|
1635
|
-
{
|
|
1636
|
-
name: "for-customer-ids",
|
|
1637
|
-
queryName: "forCustomerIds",
|
|
1638
|
-
description: "The IDs of the customers whose locations to return",
|
|
1639
|
-
valueName: "value",
|
|
1640
|
-
repeatable: true,
|
|
1641
|
-
schema: z.array(z.string())
|
|
1642
|
-
},
|
|
1643
|
-
{
|
|
1644
|
-
name: "include-organization",
|
|
1645
|
-
queryName: "includeOrganization",
|
|
1646
|
-
description: "Whether the customer filter also matches your organization's own locations, which on its own returns only those",
|
|
1647
|
-
schema: z.boolean()
|
|
1648
|
-
},
|
|
1649
|
-
{
|
|
1650
|
-
name: "sync-statuses",
|
|
1651
|
-
queryName: "syncStatuses",
|
|
1652
|
-
description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
|
|
1653
|
-
valueName: "value",
|
|
1654
|
-
repeatable: true,
|
|
1655
|
-
schema: z.array(z.string())
|
|
1656
|
-
}
|
|
1657
|
-
],
|
|
1658
|
-
takesBody: false
|
|
1659
|
-
}),
|
|
1660
|
-
defineOperation({
|
|
1661
|
-
name: "create",
|
|
1662
|
-
summary: "Create location",
|
|
1663
|
-
method: "POST",
|
|
1664
|
-
path: "/location",
|
|
1665
|
-
pathParameters: [],
|
|
1666
|
-
queryFlags: [],
|
|
1667
|
-
takesBody: true
|
|
1668
|
-
}),
|
|
1669
|
-
defineOperation({
|
|
1670
|
-
name: "get",
|
|
1671
|
-
summary: "Get location",
|
|
1672
|
-
method: "GET",
|
|
1673
|
-
path: "/location/{locationKey}",
|
|
1674
|
-
pathParameters: [{
|
|
1675
|
-
name: "locationKey",
|
|
1676
|
-
description: "The location's key",
|
|
1677
|
-
required: true
|
|
1678
|
-
}],
|
|
1679
|
-
queryFlags: [],
|
|
1680
|
-
takesBody: false
|
|
1681
|
-
}),
|
|
1682
|
-
defineOperation({
|
|
1683
|
-
name: "update",
|
|
1684
|
-
summary: "Patch location",
|
|
1685
|
-
method: "PATCH",
|
|
1686
|
-
path: "/location/{locationKey}",
|
|
1687
|
-
pathParameters: [{
|
|
1688
|
-
name: "locationKey",
|
|
1689
|
-
description: "The location's key",
|
|
1690
|
-
required: true
|
|
1691
|
-
}],
|
|
1692
|
-
queryFlags: [],
|
|
1693
|
-
takesBody: true
|
|
1694
|
-
}),
|
|
1695
|
-
{
|
|
1696
|
-
name: "zones",
|
|
1697
|
-
summary: "Zones commands",
|
|
1698
|
-
arguments: [],
|
|
1699
|
-
flags: [],
|
|
1700
|
-
examples: [],
|
|
1701
|
-
subcommands: [defineOperation({
|
|
1702
|
-
name: "list",
|
|
1703
|
-
summary: "Get zones",
|
|
1704
|
-
method: "GET",
|
|
1705
|
-
path: "/location/{locationKey}/zones",
|
|
2260
|
+
}),
|
|
2261
|
+
defineOperation({
|
|
2262
|
+
name: "delete",
|
|
2263
|
+
summary: "Delete item field",
|
|
2264
|
+
method: "DELETE",
|
|
2265
|
+
path: "/item/{itemKey}/field/{fieldKey}",
|
|
1706
2266
|
pathParameters: [{
|
|
1707
|
-
name: "
|
|
1708
|
-
description: "The key
|
|
2267
|
+
name: "itemKey",
|
|
2268
|
+
description: "The item's key",
|
|
2269
|
+
required: true
|
|
2270
|
+
}, {
|
|
2271
|
+
name: "fieldKey",
|
|
2272
|
+
description: "The field's key",
|
|
1709
2273
|
required: true
|
|
1710
2274
|
}],
|
|
1711
|
-
queryFlags: [
|
|
1712
|
-
name: "archived",
|
|
1713
|
-
queryName: "archived",
|
|
1714
|
-
description: "Whether to return unarchived zones, archived zones, or all of them",
|
|
1715
|
-
valueName: "value",
|
|
1716
|
-
schema: z.enum([
|
|
1717
|
-
"all",
|
|
1718
|
-
"false",
|
|
1719
|
-
"true"
|
|
1720
|
-
])
|
|
1721
|
-
}],
|
|
2275
|
+
queryFlags: [],
|
|
1722
2276
|
takesBody: false
|
|
1723
|
-
})
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2277
|
+
})
|
|
2278
|
+
]
|
|
2279
|
+
}
|
|
2280
|
+
]
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
name: "location",
|
|
2284
|
+
summary: "Location commands",
|
|
2285
|
+
arguments: [],
|
|
2286
|
+
flags: [],
|
|
2287
|
+
examples: [],
|
|
2288
|
+
subcommands: [
|
|
2289
|
+
defineOperation({
|
|
2290
|
+
name: "list",
|
|
2291
|
+
summary: "Get location listing",
|
|
2292
|
+
method: "GET",
|
|
2293
|
+
path: "/location",
|
|
2294
|
+
pathParameters: [],
|
|
2295
|
+
queryFlags: [
|
|
2296
|
+
{
|
|
2297
|
+
name: "page",
|
|
2298
|
+
queryName: "page",
|
|
2299
|
+
description: "The page to return, starting at 1",
|
|
2300
|
+
valueName: "number",
|
|
2301
|
+
schema: z.coerce.number()
|
|
2302
|
+
},
|
|
2303
|
+
{
|
|
2304
|
+
name: "limit",
|
|
2305
|
+
queryName: "limit",
|
|
2306
|
+
description: "The number of records per page, from 1 to 100",
|
|
2307
|
+
valueName: "number",
|
|
2308
|
+
schema: z.coerce.number()
|
|
2309
|
+
},
|
|
2310
|
+
{
|
|
2311
|
+
name: "sort-by",
|
|
2312
|
+
queryName: "sortBy",
|
|
2313
|
+
description: "The field to sort by",
|
|
2314
|
+
valueName: "value",
|
|
2315
|
+
schema: z.enum([
|
|
2316
|
+
"name",
|
|
2317
|
+
"company",
|
|
2318
|
+
"assetCount"
|
|
2319
|
+
])
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
name: "sort-order",
|
|
2323
|
+
queryName: "sortOrder",
|
|
2324
|
+
description: "The sort direction",
|
|
2325
|
+
valueName: "value",
|
|
2326
|
+
schema: z.enum(["ASC", "DESC"])
|
|
2327
|
+
},
|
|
2328
|
+
{
|
|
2329
|
+
name: "archived",
|
|
2330
|
+
queryName: "archived",
|
|
2331
|
+
description: "Whether to return unarchived records, archived records, or all of them",
|
|
2332
|
+
valueName: "value",
|
|
2333
|
+
schema: z.enum([
|
|
2334
|
+
"all",
|
|
2335
|
+
"false",
|
|
2336
|
+
"true"
|
|
2337
|
+
])
|
|
2338
|
+
},
|
|
2339
|
+
{
|
|
2340
|
+
name: "is-transient",
|
|
2341
|
+
queryName: "isTransient",
|
|
2342
|
+
description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
|
|
2343
|
+
valueName: "value",
|
|
2344
|
+
schema: z.enum([
|
|
2345
|
+
"all",
|
|
2346
|
+
"false",
|
|
2347
|
+
"true"
|
|
2348
|
+
])
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
name: "search",
|
|
2352
|
+
queryName: "search",
|
|
2353
|
+
description: "Text to match against location and customer names",
|
|
2354
|
+
valueName: "value",
|
|
2355
|
+
schema: z.string()
|
|
2356
|
+
},
|
|
2357
|
+
{
|
|
2358
|
+
name: "for-customer-ids",
|
|
2359
|
+
queryName: "forCustomerIds",
|
|
2360
|
+
description: "The IDs of the customers whose locations to return",
|
|
2361
|
+
valueName: "value",
|
|
2362
|
+
repeatable: true,
|
|
2363
|
+
schema: z.array(z.string())
|
|
2364
|
+
},
|
|
2365
|
+
{
|
|
2366
|
+
name: "include-organization",
|
|
2367
|
+
queryName: "includeOrganization",
|
|
2368
|
+
description: "Whether the customer filter also matches your organization's own locations, which on its own returns only those",
|
|
2369
|
+
schema: z.boolean()
|
|
2370
|
+
},
|
|
2371
|
+
{
|
|
2372
|
+
name: "sync-statuses",
|
|
2373
|
+
queryName: "syncStatuses",
|
|
2374
|
+
description: "The CRM sync statuses to return: in-sync, out-of-sync, or not-linked",
|
|
2375
|
+
valueName: "value",
|
|
2376
|
+
repeatable: true,
|
|
2377
|
+
schema: z.array(z.string())
|
|
2378
|
+
}
|
|
2379
|
+
],
|
|
2380
|
+
takesBody: false
|
|
2381
|
+
}),
|
|
2382
|
+
defineOperation({
|
|
2383
|
+
name: "create",
|
|
2384
|
+
summary: "Create location",
|
|
2385
|
+
method: "POST",
|
|
2386
|
+
path: "/location",
|
|
2387
|
+
pathParameters: [],
|
|
2388
|
+
queryFlags: [],
|
|
2389
|
+
takesBody: true
|
|
2390
|
+
}),
|
|
2391
|
+
defineOperation({
|
|
2392
|
+
name: "get",
|
|
2393
|
+
summary: "Get location",
|
|
2394
|
+
method: "GET",
|
|
2395
|
+
path: "/location/{locationKey}",
|
|
2396
|
+
pathParameters: [{
|
|
2397
|
+
name: "locationKey",
|
|
2398
|
+
description: "The location's key",
|
|
2399
|
+
required: true
|
|
2400
|
+
}],
|
|
2401
|
+
queryFlags: [],
|
|
2402
|
+
takesBody: false
|
|
2403
|
+
}),
|
|
2404
|
+
defineOperation({
|
|
2405
|
+
name: "update",
|
|
2406
|
+
summary: "Patch location",
|
|
2407
|
+
method: "PATCH",
|
|
2408
|
+
path: "/location/{locationKey}",
|
|
2409
|
+
pathParameters: [{
|
|
2410
|
+
name: "locationKey",
|
|
2411
|
+
description: "The location's key",
|
|
2412
|
+
required: true
|
|
2413
|
+
}],
|
|
2414
|
+
queryFlags: [],
|
|
2415
|
+
takesBody: true
|
|
2416
|
+
}),
|
|
2417
|
+
{
|
|
2418
|
+
name: "zones",
|
|
2419
|
+
summary: "Zones commands",
|
|
2420
|
+
arguments: [],
|
|
2421
|
+
flags: [],
|
|
2422
|
+
examples: [],
|
|
2423
|
+
subcommands: [defineOperation({
|
|
2424
|
+
name: "list",
|
|
2425
|
+
summary: "Get zones",
|
|
1737
2426
|
method: "GET",
|
|
1738
|
-
path: "/
|
|
2427
|
+
path: "/location/{locationKey}/zones",
|
|
1739
2428
|
pathParameters: [{
|
|
1740
|
-
name: "
|
|
1741
|
-
description: "The
|
|
2429
|
+
name: "locationKey",
|
|
2430
|
+
description: "The key of the site whose zones to list",
|
|
1742
2431
|
required: true
|
|
1743
2432
|
}],
|
|
1744
|
-
queryFlags: [
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
description: "The URL link's key",
|
|
1755
|
-
required: true
|
|
2433
|
+
queryFlags: [{
|
|
2434
|
+
name: "archived",
|
|
2435
|
+
queryName: "archived",
|
|
2436
|
+
description: "Whether to return unarchived zones, archived zones, or all of them",
|
|
2437
|
+
valueName: "value",
|
|
2438
|
+
schema: z.enum([
|
|
2439
|
+
"all",
|
|
2440
|
+
"false",
|
|
2441
|
+
"true"
|
|
2442
|
+
])
|
|
1756
2443
|
}],
|
|
1757
|
-
queryFlags: [],
|
|
1758
|
-
takesBody: true
|
|
1759
|
-
}),
|
|
1760
|
-
defineOperation({
|
|
1761
|
-
name: "delete",
|
|
1762
|
-
summary: "Delete URL link",
|
|
1763
|
-
method: "DELETE",
|
|
1764
|
-
path: "/url-link/{linkKey}",
|
|
1765
|
-
pathParameters: [{
|
|
1766
|
-
name: "linkKey",
|
|
1767
|
-
description: "The URL link's key",
|
|
1768
|
-
required: true
|
|
1769
|
-
}],
|
|
1770
|
-
queryFlags: [],
|
|
1771
2444
|
takesBody: false
|
|
1772
|
-
})
|
|
1773
|
-
|
|
2445
|
+
})]
|
|
2446
|
+
}
|
|
2447
|
+
]
|
|
2448
|
+
},
|
|
2449
|
+
{
|
|
2450
|
+
name: "url-link",
|
|
2451
|
+
summary: "URL link commands",
|
|
2452
|
+
arguments: [],
|
|
2453
|
+
flags: [],
|
|
2454
|
+
examples: [],
|
|
2455
|
+
subcommands: [
|
|
2456
|
+
defineOperation({
|
|
2457
|
+
name: "get",
|
|
2458
|
+
summary: "Get URL link by key",
|
|
2459
|
+
method: "GET",
|
|
2460
|
+
path: "/url-link/{linkKey}",
|
|
2461
|
+
pathParameters: [{
|
|
2462
|
+
name: "linkKey",
|
|
2463
|
+
description: "The URL link's key",
|
|
2464
|
+
required: true
|
|
2465
|
+
}],
|
|
2466
|
+
queryFlags: [],
|
|
2467
|
+
takesBody: false
|
|
2468
|
+
}),
|
|
2469
|
+
defineOperation({
|
|
2470
|
+
name: "update",
|
|
2471
|
+
summary: "Update URL link",
|
|
2472
|
+
method: "PATCH",
|
|
2473
|
+
path: "/url-link/{linkKey}",
|
|
2474
|
+
pathParameters: [{
|
|
2475
|
+
name: "linkKey",
|
|
2476
|
+
description: "The URL link's key",
|
|
2477
|
+
required: true
|
|
2478
|
+
}],
|
|
2479
|
+
queryFlags: [],
|
|
2480
|
+
takesBody: true
|
|
2481
|
+
}),
|
|
2482
|
+
defineOperation({
|
|
2483
|
+
name: "delete",
|
|
2484
|
+
summary: "Delete URL link",
|
|
2485
|
+
method: "DELETE",
|
|
2486
|
+
path: "/url-link/{linkKey}",
|
|
2487
|
+
pathParameters: [{
|
|
2488
|
+
name: "linkKey",
|
|
2489
|
+
description: "The URL link's key",
|
|
2490
|
+
required: true
|
|
2491
|
+
}],
|
|
2492
|
+
queryFlags: [],
|
|
2493
|
+
takesBody: false
|
|
2494
|
+
})
|
|
2495
|
+
]
|
|
2496
|
+
}
|
|
2497
|
+
];
|
|
2498
|
+
//#endregion
|
|
2499
|
+
//#region src/auth/jwt.ts
|
|
2500
|
+
/**
|
|
2501
|
+
* toClaims reads an access token's payload for display. Nothing here verifies the
|
|
2502
|
+
* signature, because the API is what decides whether a token is good.
|
|
2503
|
+
*/
|
|
2504
|
+
function toClaims(token) {
|
|
2505
|
+
const payload = token.split(".")[1];
|
|
2506
|
+
if (!payload) return {};
|
|
2507
|
+
try {
|
|
2508
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
2509
|
+
return {
|
|
2510
|
+
expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
|
|
2511
|
+
issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
|
|
2512
|
+
scopes: toScopes(decoded),
|
|
2513
|
+
subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
|
|
2514
|
+
};
|
|
2515
|
+
} catch {
|
|
2516
|
+
return {};
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
/** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
|
|
2520
|
+
function toScopes(decoded) {
|
|
2521
|
+
if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
|
|
2522
|
+
return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
|
|
2523
|
+
}
|
|
2524
|
+
//#endregion
|
|
2525
|
+
//#region src/system/host.ts
|
|
2526
|
+
const BYTES_PER_GB = 1024 ** 3;
|
|
2527
|
+
/** toDistribution reads the name a Linux distribution gives itself. */
|
|
2528
|
+
function toDistribution(osRelease) {
|
|
2529
|
+
const pretty = /^PRETTY_NAME="?([^"\n]+)"?$/m.exec(osRelease);
|
|
2530
|
+
if (pretty?.[1]) return pretty[1];
|
|
2531
|
+
const name = /^NAME="?([^"\n]+)"?$/m.exec(osRelease);
|
|
2532
|
+
const version = /^VERSION_ID="?([^"\n]+)"?$/m.exec(osRelease);
|
|
2533
|
+
if (!name?.[1]) return;
|
|
2534
|
+
return version?.[1] ? `${name[1]} ${version[1]}` : name[1];
|
|
2535
|
+
}
|
|
2536
|
+
/** toHostReport describes the machine, which is what a support request cannot ask for twice. */
|
|
2537
|
+
function toHostReport() {
|
|
2538
|
+
const report = {
|
|
2539
|
+
type: type(),
|
|
2540
|
+
kernel: release(),
|
|
2541
|
+
build: version(),
|
|
2542
|
+
arch: arch(),
|
|
2543
|
+
cpus: cpus().length,
|
|
2544
|
+
cpu: cpus()[0]?.model ?? null,
|
|
2545
|
+
memoryGb: Number((totalmem() / BYTES_PER_GB).toFixed(1)),
|
|
2546
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
2547
|
+
shell: process.env["SHELL"] ?? process.env["ComSpec"] ?? null,
|
|
2548
|
+
terminal: process.env["TERM"] ?? null
|
|
2549
|
+
};
|
|
2550
|
+
if (process.platform !== "linux") return report;
|
|
2551
|
+
report["distribution"] = toDistribution(toFile("/etc/os-release")) ?? null;
|
|
2552
|
+
if (isWSL()) report["wsl"] = {
|
|
2553
|
+
distribution: process.env["WSL_DISTRO_NAME"] ?? null,
|
|
2554
|
+
windowsTerminal: Boolean(process.env["WT_SESSION"])
|
|
2555
|
+
};
|
|
2556
|
+
return report;
|
|
2557
|
+
}
|
|
2558
|
+
function toFile(path) {
|
|
2559
|
+
try {
|
|
2560
|
+
return readFileSync(path, "utf8");
|
|
2561
|
+
} catch {
|
|
2562
|
+
return "";
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
//#endregion
|
|
2566
|
+
//#region src/command/status.ts
|
|
2567
|
+
/**
|
|
2568
|
+
* Hardfin expires an unused refresh token after this long. It is the server's rule, not the
|
|
2569
|
+
* CLI's, and the token endpoint reports no expiry, so what this produces is an estimate.
|
|
2570
|
+
*/
|
|
2571
|
+
const REFRESH_SLIDING_DAYS = 90;
|
|
2572
|
+
const statusCommand = defineCommand({
|
|
2573
|
+
name: "status",
|
|
2574
|
+
summary: "Report what this CLI is configured with, signed in as, and able to reach",
|
|
2575
|
+
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.",
|
|
2576
|
+
arguments: [],
|
|
2577
|
+
flags: [{
|
|
2578
|
+
name: "offline",
|
|
2579
|
+
description: "Skip every network call, and report only what is on this machine",
|
|
2580
|
+
schema: z.boolean()
|
|
2581
|
+
}, {
|
|
2582
|
+
name: "json",
|
|
2583
|
+
description: "Print machine-readable output, which is the default when stdout is not a terminal",
|
|
2584
|
+
schema: z.boolean()
|
|
2585
|
+
}],
|
|
2586
|
+
examples: [
|
|
2587
|
+
{
|
|
2588
|
+
description: "See what the CLI is using",
|
|
2589
|
+
command: "hardfin status"
|
|
2590
|
+
},
|
|
2591
|
+
{
|
|
2592
|
+
description: "Attach the whole picture to a support request",
|
|
2593
|
+
command: "hardfin status --json > status.json"
|
|
2594
|
+
},
|
|
2595
|
+
{
|
|
2596
|
+
description: "Ask nothing of the network",
|
|
2597
|
+
command: "hardfin status --offline"
|
|
1774
2598
|
}
|
|
1775
2599
|
],
|
|
2600
|
+
run: runStatus
|
|
2601
|
+
});
|
|
2602
|
+
async function runStatus(input) {
|
|
2603
|
+
const settings = input.resolved.settings;
|
|
2604
|
+
const stored = toCredential(settings.issuerUrl);
|
|
2605
|
+
const report = {
|
|
2606
|
+
cli: toEnvironmentReport(),
|
|
2607
|
+
host: toHostReport(),
|
|
2608
|
+
configuration: toConfigurationReport(input.resolved),
|
|
2609
|
+
credential: toCredentialReport(settings.apiKey, stored)
|
|
2610
|
+
};
|
|
2611
|
+
if (input.flags["offline"] === true) {
|
|
2612
|
+
report["authorizationServer"] = { checked: false };
|
|
2613
|
+
report["api"] = { checked: false };
|
|
2614
|
+
writeReport(report, input.isJSON);
|
|
2615
|
+
return stored || settings.apiKey ? ExitCode.OK : ExitCode.NOT_AUTHENTICATED;
|
|
2616
|
+
}
|
|
2617
|
+
const metadata = await toMetadata(settings.issuerUrl).catch((error) => error);
|
|
2618
|
+
report["authorizationServer"] = metadata instanceof Error ? {
|
|
2619
|
+
reachable: false,
|
|
2620
|
+
error: metadata.message
|
|
2621
|
+
} : toServerReport(metadata);
|
|
2622
|
+
report["api"] = await toApiReport(input);
|
|
2623
|
+
writeReport(report, input.isJSON);
|
|
2624
|
+
return report["api"].authenticated === false ? ExitCode.NOT_AUTHENTICATED : ExitCode.OK;
|
|
2625
|
+
}
|
|
2626
|
+
function toEnvironmentReport() {
|
|
2627
|
+
return {
|
|
2628
|
+
version: version$1,
|
|
2629
|
+
apiVersion: API_VERSION,
|
|
2630
|
+
node: process.version,
|
|
2631
|
+
platform: process.platform,
|
|
2632
|
+
interactive: Boolean(process.stdin.isTTY)
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
/** toConfigurationReport names every setting, its source, and never a secret's value. */
|
|
2636
|
+
function toConfigurationReport(resolved) {
|
|
2637
|
+
const entries = Object.entries(resolved.settings).map(([key, value]) => {
|
|
2638
|
+
const from = resolved.sources[key];
|
|
2639
|
+
if (key === "apiKey") return [key, {
|
|
2640
|
+
set: value !== void 0,
|
|
2641
|
+
fingerprint: toFingerprint(value),
|
|
2642
|
+
from
|
|
2643
|
+
}];
|
|
2644
|
+
return [key, {
|
|
2645
|
+
value: value ?? null,
|
|
2646
|
+
from
|
|
2647
|
+
}];
|
|
2648
|
+
});
|
|
2649
|
+
return {
|
|
2650
|
+
...Object.fromEntries(entries),
|
|
2651
|
+
configFile: CONFIG_FILE
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
function toCredentialReport(apiKey, stored) {
|
|
2655
|
+
if (apiKey) return {
|
|
2656
|
+
kind: "api key",
|
|
2657
|
+
signedIn: false,
|
|
2658
|
+
note: "an API key is set, so it is used ahead of any sign in"
|
|
2659
|
+
};
|
|
2660
|
+
if (!stored) return {
|
|
2661
|
+
kind: "none",
|
|
2662
|
+
signedIn: false,
|
|
2663
|
+
note: "run hardfin login, or set HARDFIN_API_KEY"
|
|
2664
|
+
};
|
|
2665
|
+
return {
|
|
2666
|
+
kind: "browser sign in",
|
|
2667
|
+
signedIn: true,
|
|
2668
|
+
storedIn: stored.backend,
|
|
2669
|
+
path: stored.path ?? null,
|
|
2670
|
+
fingerprint: toFingerprint(stored.refreshToken),
|
|
2671
|
+
signedInAt: stored.signedInAt ?? null,
|
|
2672
|
+
renewedAt: stored.renewedAt ?? null,
|
|
2673
|
+
refreshExpiresAt: toRefreshExpiry(stored.renewedAt),
|
|
2674
|
+
refreshExpiryIsEstimated: true
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
function toServerReport(metadata) {
|
|
2678
|
+
return {
|
|
2679
|
+
reachable: true,
|
|
2680
|
+
issuer: metadata.issuer,
|
|
2681
|
+
authorizationEndpoint: metadata.authorization_endpoint,
|
|
2682
|
+
tokenEndpoint: metadata.token_endpoint,
|
|
2683
|
+
revocationEndpoint: metadata.revocation_endpoint ?? null,
|
|
2684
|
+
deviceEndpoint: metadata.device_authorization_endpoint ?? null,
|
|
2685
|
+
grantTypes: metadata.grant_types_supported ?? [],
|
|
2686
|
+
scopes: metadata.scopes_supported ?? []
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
/** toApiReport asks the API who this credential is, which is the only authoritative answer. */
|
|
2690
|
+
async function toApiReport(input) {
|
|
2691
|
+
const settings = input.resolved.settings;
|
|
2692
|
+
try {
|
|
2693
|
+
const credential = await toRequestCredential(settings);
|
|
2694
|
+
const claims = credential.kind === "access token" ? toClaims(credential.value.replace(/^Bearer /, "")) : {};
|
|
2695
|
+
const identity = await request({
|
|
2696
|
+
apiUrl: settings.apiUrl,
|
|
2697
|
+
credential,
|
|
2698
|
+
method: "GET",
|
|
2699
|
+
path: "/token"
|
|
2700
|
+
}).then((envelope) => envelope.data).catch((error) => error instanceof RequestFailure ? { unavailable: error.message } : { unavailable: String(error) });
|
|
2701
|
+
return {
|
|
2702
|
+
reachable: true,
|
|
2703
|
+
authenticated: true,
|
|
2704
|
+
url: settings.apiUrl,
|
|
2705
|
+
accessTokenExpiresAt: claims.expiresAt === void 0 ? null : new Date(claims.expiresAt).toISOString(),
|
|
2706
|
+
accessTokenScopes: claims.scopes ?? [],
|
|
2707
|
+
identity
|
|
2708
|
+
};
|
|
2709
|
+
} catch (error) {
|
|
2710
|
+
if (error instanceof NoCredential) return {
|
|
2711
|
+
reachable: null,
|
|
2712
|
+
authenticated: false,
|
|
2713
|
+
url: settings.apiUrl,
|
|
2714
|
+
error: error.message
|
|
2715
|
+
};
|
|
2716
|
+
return {
|
|
2717
|
+
reachable: false,
|
|
2718
|
+
authenticated: false,
|
|
2719
|
+
url: settings.apiUrl,
|
|
2720
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
/** toRefreshExpiry applies the server's sliding rule to when the token was last renewed. */
|
|
2725
|
+
function toRefreshExpiry(renewedAt) {
|
|
2726
|
+
if (!renewedAt) return null;
|
|
2727
|
+
const renewed = new Date(renewedAt);
|
|
2728
|
+
renewed.setDate(renewed.getDate() + REFRESH_SLIDING_DAYS);
|
|
2729
|
+
return renewed.toISOString();
|
|
2730
|
+
}
|
|
2731
|
+
/** toFingerprint identifies a secret in a support request without disclosing it. */
|
|
2732
|
+
function toFingerprint(secret) {
|
|
2733
|
+
if (!secret) return null;
|
|
2734
|
+
return `sha256:${createHash("sha256").update(secret).digest("hex").slice(0, 12)}`;
|
|
2735
|
+
}
|
|
2736
|
+
function writeReport(report, isJSON) {
|
|
2737
|
+
if (isJSON) {
|
|
2738
|
+
writeData(report);
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
writeData(toLines(report).join("\n"));
|
|
2742
|
+
}
|
|
2743
|
+
/** toFlattened reads a setting, which is a value and the layer that supplied it. */
|
|
2744
|
+
function toFlattened(value) {
|
|
2745
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return;
|
|
2746
|
+
const holder = value;
|
|
2747
|
+
if (holder.from === void 0) return;
|
|
2748
|
+
const shown = holder.value ?? (holder.set ? holder.fingerprint ?? "set" : "not set");
|
|
2749
|
+
return `${String(shown)} (${holder.from})`;
|
|
2750
|
+
}
|
|
2751
|
+
/** toLines lays the report out for a person, one indented line per value. */
|
|
2752
|
+
function toLines(report, depth = 0) {
|
|
2753
|
+
const lines = [];
|
|
2754
|
+
const pad = " ".repeat(depth);
|
|
2755
|
+
for (const [key, value] of Object.entries(report)) {
|
|
2756
|
+
const flattened = toFlattened(value);
|
|
2757
|
+
if (flattened !== void 0) {
|
|
2758
|
+
lines.push(`${pad}${key.padEnd(26 - depth * 2)} ${flattened}`);
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
2762
|
+
lines.push(`${pad}${key}`, ...toLines(value, depth + 1));
|
|
2763
|
+
continue;
|
|
2764
|
+
}
|
|
2765
|
+
lines.push(`${pad}${key.padEnd(26 - depth * 2)} ${Array.isArray(value) ? value.join(", ") : String(value)}`);
|
|
2766
|
+
}
|
|
2767
|
+
return lines;
|
|
2768
|
+
}
|
|
2769
|
+
//#endregion
|
|
2770
|
+
//#region src/command/commands.ts
|
|
2771
|
+
/** commands is every command the CLI offers, and drives help and the agent guide. */
|
|
2772
|
+
const commands = [
|
|
2773
|
+
loginCommand,
|
|
2774
|
+
logoutCommand,
|
|
2775
|
+
statusCommand,
|
|
2776
|
+
...surfaceCommands,
|
|
1776
2777
|
apiCommand,
|
|
1777
2778
|
configCommand,
|
|
1778
2779
|
agentGuideCommand
|
|
@@ -1790,7 +2791,7 @@ function toRejectedFlag(command, flags) {
|
|
|
1790
2791
|
//#endregion
|
|
1791
2792
|
//#region src/cli.ts
|
|
1792
2793
|
const program = new Command();
|
|
1793
|
-
program.name("hardfin").description("Call the Hardfin API from a terminal or an agent").version(version, "-v, --version").option("--api-url <url>", "The API to call,
|
|
2794
|
+
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();
|
|
1794
2795
|
for (const command of commands) program.addCommand(toProgram(command));
|
|
1795
2796
|
await program.parseAsync(process.argv);
|
|
1796
2797
|
/** toProgram wires one registry command into the parser. */
|
|
@@ -1826,7 +2827,10 @@ async function toExitCode(command, args, flags) {
|
|
|
1826
2827
|
return ExitCode.USAGE;
|
|
1827
2828
|
}
|
|
1828
2829
|
try {
|
|
1829
|
-
const resolved = toSettings({
|
|
2830
|
+
const resolved = toSettings({
|
|
2831
|
+
apiUrl: program.opts()["apiUrl"],
|
|
2832
|
+
issuerUrl: program.opts()["issuerUrl"]
|
|
2833
|
+
});
|
|
1830
2834
|
return await command.run?.({
|
|
1831
2835
|
args,
|
|
1832
2836
|
flags,
|