@hardfin/cli 0.0.2-dev.14 → 0.0.2-dev.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +201 -105
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,10 @@ import { Command, Option } from "commander";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { chmodSync, closeSync, existsSync, mkdirSync, openAsBlob, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
6
6
|
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
7
8
|
import { arch, cpus, homedir, release, totalmem, type, version } from "node:os";
|
|
8
9
|
import { spawn, spawnSync } from "node:child_process";
|
|
9
10
|
import { createServer } from "node:http";
|
|
10
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
11
11
|
import { createInterface } from "node:readline";
|
|
12
12
|
//#region src/command/registry.ts
|
|
13
13
|
/** ExitCode is what the process returns, and what an agent branches on. */
|
|
@@ -252,6 +252,32 @@ function toSummary(command) {
|
|
|
252
252
|
};
|
|
253
253
|
}
|
|
254
254
|
//#endregion
|
|
255
|
+
//#region src/auth/jwt.ts
|
|
256
|
+
/**
|
|
257
|
+
* toClaims reads an access token's payload for display. Nothing here verifies the
|
|
258
|
+
* signature, because the API is what decides whether a token is good.
|
|
259
|
+
*/
|
|
260
|
+
function toClaims(token) {
|
|
261
|
+
const payload = token.split(".")[1];
|
|
262
|
+
if (!payload) return {};
|
|
263
|
+
try {
|
|
264
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
265
|
+
return {
|
|
266
|
+
expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
|
|
267
|
+
issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
|
|
268
|
+
scopes: toScopes(decoded),
|
|
269
|
+
subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
|
|
270
|
+
};
|
|
271
|
+
} catch {
|
|
272
|
+
return {};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
|
|
276
|
+
function toScopes(decoded) {
|
|
277
|
+
if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
|
|
278
|
+
return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
255
281
|
//#region src/auth/metadata.ts
|
|
256
282
|
const METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
257
283
|
const AuthorizationServerMetadata = z.looseObject({
|
|
@@ -388,30 +414,44 @@ function keep(issuer, refreshToken, expiresAt) {
|
|
|
388
414
|
const keyring = toKeyring(issuer);
|
|
389
415
|
if (keyring) try {
|
|
390
416
|
keyring.setPassword(JSON.stringify(record));
|
|
417
|
+
forgetFile(issuer);
|
|
391
418
|
return "keyring";
|
|
392
419
|
} catch {}
|
|
393
420
|
writeFile({
|
|
394
421
|
...readFile(),
|
|
395
422
|
[issuer]: record
|
|
396
423
|
});
|
|
424
|
+
forgetKeyring(issuer);
|
|
397
425
|
return "file";
|
|
398
426
|
}
|
|
399
|
-
/**
|
|
427
|
+
/**
|
|
428
|
+
* toCredential reads what is held for an issuer, and where it was held. Both backends are
|
|
429
|
+
* read, because a host that lost its keyring for a while wrote to the file instead, and the
|
|
430
|
+
* newer of the two is the one the server has not spent.
|
|
431
|
+
*/
|
|
400
432
|
function toCredential(issuer) {
|
|
401
|
-
const
|
|
402
|
-
if (keyring) try {
|
|
403
|
-
const held = keyring.getPassword();
|
|
404
|
-
if (held) return {
|
|
405
|
-
...toRecord(held),
|
|
406
|
-
backend: "keyring"
|
|
407
|
-
};
|
|
408
|
-
} catch {}
|
|
433
|
+
const fromKeyring = toKeyringCredential(issuer);
|
|
409
434
|
const held = readFile()[issuer];
|
|
410
|
-
|
|
435
|
+
const fromFile = held === void 0 ? void 0 : {
|
|
411
436
|
...held,
|
|
412
437
|
backend: "file",
|
|
413
438
|
path: toCredentialPath()
|
|
414
439
|
};
|
|
440
|
+
if (!fromKeyring || !fromFile) return fromKeyring ?? fromFile;
|
|
441
|
+
return (fromFile.renewedAt ?? "") > (fromKeyring.renewedAt ?? "") ? fromFile : fromKeyring;
|
|
442
|
+
}
|
|
443
|
+
function toKeyringCredential(issuer) {
|
|
444
|
+
const keyring = toKeyring(issuer);
|
|
445
|
+
if (!keyring) return;
|
|
446
|
+
try {
|
|
447
|
+
const held = keyring.getPassword();
|
|
448
|
+
return held ? {
|
|
449
|
+
...toRecord(held),
|
|
450
|
+
backend: "keyring"
|
|
451
|
+
} : void 0;
|
|
452
|
+
} catch {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
415
455
|
}
|
|
416
456
|
/** toRecord reads a stored entry, which older versions wrote as the bare token. */
|
|
417
457
|
function toRecord(held) {
|
|
@@ -424,10 +464,17 @@ function toRecord(held) {
|
|
|
424
464
|
}
|
|
425
465
|
/** forget removes whatever is held for an issuer, in both places. */
|
|
426
466
|
function forget(issuer) {
|
|
467
|
+
forgetKeyring(issuer);
|
|
468
|
+
forgetFile(issuer);
|
|
469
|
+
}
|
|
470
|
+
function forgetKeyring(issuer) {
|
|
427
471
|
const keyring = toKeyring(issuer);
|
|
428
|
-
if (keyring)
|
|
472
|
+
if (!keyring) return;
|
|
473
|
+
try {
|
|
429
474
|
keyring.deletePassword();
|
|
430
475
|
} catch {}
|
|
476
|
+
}
|
|
477
|
+
function forgetFile(issuer) {
|
|
431
478
|
const held = readFile();
|
|
432
479
|
if (held[issuer] === void 0) return;
|
|
433
480
|
delete held[issuer];
|
|
@@ -474,6 +521,8 @@ const STALE_MS = 3e4;
|
|
|
474
521
|
const WAIT_MS$1 = 1e4;
|
|
475
522
|
const RETRY_MS = 25;
|
|
476
523
|
const DIRECTORY_MODE = 448;
|
|
524
|
+
/** What this process wrote into the lock, which is how it knows the lock is still its own. */
|
|
525
|
+
let heldBy;
|
|
477
526
|
/** toLockPath names the lock every process coordinates credential writes through. */
|
|
478
527
|
function toLockPath() {
|
|
479
528
|
return join(dirname(toCredentialPath()), "credentials.lock");
|
|
@@ -485,17 +534,23 @@ function tryAcquire() {
|
|
|
485
534
|
recursive: true,
|
|
486
535
|
mode: DIRECTORY_MODE
|
|
487
536
|
});
|
|
537
|
+
const mark = `${process.pid}:${randomUUID()}`;
|
|
488
538
|
try {
|
|
489
539
|
const handle = openSync(path, "wx");
|
|
490
|
-
writeSync(handle,
|
|
540
|
+
writeSync(handle, mark);
|
|
491
541
|
closeSync(handle);
|
|
542
|
+
heldBy = mark;
|
|
492
543
|
return true;
|
|
493
544
|
} catch {
|
|
494
|
-
return isStale(path) ? steal(path) : false;
|
|
545
|
+
return isStale(path) ? steal(path, mark) : false;
|
|
495
546
|
}
|
|
496
547
|
}
|
|
497
|
-
/** release lets the next process in. */
|
|
548
|
+
/** release lets the next process in, and only ever removes this process's own lock. */
|
|
498
549
|
function release$1() {
|
|
550
|
+
const mark = heldBy;
|
|
551
|
+
if (mark === void 0) return;
|
|
552
|
+
heldBy = void 0;
|
|
553
|
+
if (toMark(toLockPath()) !== mark) return;
|
|
499
554
|
rmSync(toLockPath(), { force: true });
|
|
500
555
|
}
|
|
501
556
|
/**
|
|
@@ -522,9 +577,29 @@ function isStale(path) {
|
|
|
522
577
|
return true;
|
|
523
578
|
}
|
|
524
579
|
}
|
|
525
|
-
|
|
580
|
+
/**
|
|
581
|
+
* steal takes over a lock whose holder is gone. Two processes can reach this at once, so
|
|
582
|
+
* the winner is whichever mark survives in the file, not whichever removed it.
|
|
583
|
+
*/
|
|
584
|
+
function steal(path, mark) {
|
|
526
585
|
rmSync(path, { force: true });
|
|
527
|
-
|
|
586
|
+
try {
|
|
587
|
+
const handle = openSync(path, "wx");
|
|
588
|
+
writeSync(handle, mark);
|
|
589
|
+
closeSync(handle);
|
|
590
|
+
} catch {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
if (toMark(path) !== mark) return false;
|
|
594
|
+
heldBy = mark;
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
function toMark(path) {
|
|
598
|
+
try {
|
|
599
|
+
return readFileSync(path, "utf8");
|
|
600
|
+
} catch {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
528
603
|
}
|
|
529
604
|
//#endregion
|
|
530
605
|
//#region src/auth/session.ts
|
|
@@ -567,9 +642,9 @@ async function toAccessToken(settings, refreshToken) {
|
|
|
567
642
|
return await withLock(async () => {
|
|
568
643
|
const latest = toCredential(settings.issuerUrl)?.refreshToken ?? refreshToken;
|
|
569
644
|
try {
|
|
570
|
-
const tokens = await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest);
|
|
645
|
+
const tokens = toDated(await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest));
|
|
571
646
|
held.set(settings.issuerUrl, tokens);
|
|
572
|
-
|
|
647
|
+
store(settings.issuerUrl, tokens, latest);
|
|
573
648
|
return tokens;
|
|
574
649
|
} catch (error) {
|
|
575
650
|
if (error instanceof GrantFailure && isDead(error.code)) {
|
|
@@ -580,14 +655,37 @@ async function toAccessToken(settings, refreshToken) {
|
|
|
580
655
|
}
|
|
581
656
|
});
|
|
582
657
|
}
|
|
583
|
-
/**
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
]);
|
|
658
|
+
/**
|
|
659
|
+
* invalid_grant is the only refusal that says anything about the refresh token itself.
|
|
660
|
+
* invalid_client and unauthorized_client describe the client registration, which is server
|
|
661
|
+
* configuration and a setting a person can mistype, so a good credential survives them.
|
|
662
|
+
*/
|
|
589
663
|
function isDead(code) {
|
|
590
|
-
return
|
|
664
|
+
return code === "invalid_grant";
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* toDated fills in when an access token expires. A token endpoint that states no expires_in
|
|
668
|
+
* would otherwise have this process refresh on every command, and every refresh spends a
|
|
669
|
+
* generation of the token family.
|
|
670
|
+
*/
|
|
671
|
+
function toDated(tokens) {
|
|
672
|
+
if (tokens.expiresAt !== void 0) return tokens;
|
|
673
|
+
return {
|
|
674
|
+
...tokens,
|
|
675
|
+
expiresAt: toClaims(tokens.accessToken).expiresAt
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* store writes what a rotation issued. The old token is spent either way, so failing to
|
|
680
|
+
* write the new one is worth saying out loud rather than failing a command that succeeded.
|
|
681
|
+
*/
|
|
682
|
+
function store(issuer, tokens, previous) {
|
|
683
|
+
if (!tokens.refreshToken || tokens.refreshToken === previous) return;
|
|
684
|
+
try {
|
|
685
|
+
keep(issuer, tokens.refreshToken, tokens.refreshExpiresAt);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
process.stderr.write(`warning: this sign in was renewed but could not be stored, so the next command will ask you to sign in again: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
688
|
+
}
|
|
591
689
|
}
|
|
592
690
|
/** forgetHeldTokens drops the access tokens this process is holding. */
|
|
593
691
|
function forgetHeldTokens() {
|
|
@@ -1197,6 +1295,7 @@ function toPrompt(url) {
|
|
|
1197
1295
|
fail = reject;
|
|
1198
1296
|
});
|
|
1199
1297
|
const input = process.stdin;
|
|
1298
|
+
pasted.catch(() => {});
|
|
1200
1299
|
if (!input.isTTY) return {
|
|
1201
1300
|
pasted,
|
|
1202
1301
|
close: () => {}
|
|
@@ -1312,9 +1411,13 @@ async function runLogin(input) {
|
|
|
1312
1411
|
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`);
|
|
1313
1412
|
const prompt = toPrompt(url);
|
|
1314
1413
|
if (process.stdin.isTTY) process.stderr.write("Press c to copy the URL, or paste the code or redirect URL here: ");
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1414
|
+
let callback;
|
|
1415
|
+
try {
|
|
1416
|
+
callback = await Promise.race([listener.callback, prompt.pasted]);
|
|
1417
|
+
} finally {
|
|
1418
|
+
listener.close();
|
|
1419
|
+
prompt.close();
|
|
1420
|
+
}
|
|
1318
1421
|
process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
|
|
1319
1422
|
if (callback.error) {
|
|
1320
1423
|
writeFailure(`sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
|
|
@@ -1360,7 +1463,15 @@ async function runDeviceLogin(input, metadata, scope) {
|
|
|
1360
1463
|
opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
|
|
1361
1464
|
].join("\n"));
|
|
1362
1465
|
const prompt = toPrompt(url);
|
|
1363
|
-
|
|
1466
|
+
try {
|
|
1467
|
+
return await toSignedIn(input, metadata, await Promise.race([toTokensFromDevice(metadata.token_endpoint, settings.clientId, device), prompt.pasted.then(toCancelled)]));
|
|
1468
|
+
} finally {
|
|
1469
|
+
prompt.close();
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
/** toCancelled ends a device sign in that the keyboard interrupted. */
|
|
1473
|
+
function toCancelled() {
|
|
1474
|
+
throw new Error("sign in was cancelled");
|
|
1364
1475
|
}
|
|
1365
1476
|
/** toSignedIn stores what a sign in issued, whichever flow issued it. */
|
|
1366
1477
|
async function toSignedIn(input, metadata, tokens) {
|
|
@@ -1635,6 +1746,13 @@ async function runMcp(input) {
|
|
|
1635
1746
|
}
|
|
1636
1747
|
//#endregion
|
|
1637
1748
|
//#region src/command/operation.ts
|
|
1749
|
+
/**
|
|
1750
|
+
* toEnum takes a value in any case, as the API does, and answers the spelling the document
|
|
1751
|
+
* lists, which is what a response always uses.
|
|
1752
|
+
*/
|
|
1753
|
+
function toEnum(values) {
|
|
1754
|
+
return z.string().transform((value) => values.find((allowed) => allowed.toLowerCase() === value.toLowerCase()) ?? value).pipe(z.enum(values));
|
|
1755
|
+
}
|
|
1638
1756
|
const UNSET_FLAG = {
|
|
1639
1757
|
name: "unset",
|
|
1640
1758
|
description: "A field to clear, named as its flag is, repeatable",
|
|
@@ -1741,7 +1859,9 @@ function toQuery(operation, flags) {
|
|
|
1741
1859
|
for (const flag of operation.queryFlags) {
|
|
1742
1860
|
const value = flags[toOptionKey(flag.name)];
|
|
1743
1861
|
if (value === void 0) continue;
|
|
1744
|
-
|
|
1862
|
+
const parsed = flag.schema.safeParse(value);
|
|
1863
|
+
const carried = parsed.success ? parsed.data : value;
|
|
1864
|
+
for (const entry of Array.isArray(carried) ? carried : [carried]) query.append(flag.queryName, String(entry));
|
|
1745
1865
|
}
|
|
1746
1866
|
return query;
|
|
1747
1867
|
}
|
|
@@ -1761,7 +1881,10 @@ async function toForm(upload, flags) {
|
|
|
1761
1881
|
}
|
|
1762
1882
|
for (const field of upload.fields) {
|
|
1763
1883
|
const value = flags[toOptionKey(field.name)];
|
|
1764
|
-
if (value !== void 0)
|
|
1884
|
+
if (value !== void 0) {
|
|
1885
|
+
const parsed = field.schema.safeParse(value);
|
|
1886
|
+
form.append(field.jsonPath[0] ?? field.name, String(parsed.success ? parsed.data : value));
|
|
1887
|
+
}
|
|
1765
1888
|
}
|
|
1766
1889
|
return form;
|
|
1767
1890
|
}
|
|
@@ -1847,21 +1970,21 @@ const surfaceCommands = [
|
|
|
1847
1970
|
queryName: "page",
|
|
1848
1971
|
description: "The page to return, starting at 1",
|
|
1849
1972
|
valueName: "number",
|
|
1850
|
-
schema: z.coerce.number()
|
|
1973
|
+
schema: z.coerce.number().int()
|
|
1851
1974
|
},
|
|
1852
1975
|
{
|
|
1853
1976
|
name: "limit",
|
|
1854
1977
|
queryName: "limit",
|
|
1855
1978
|
description: "The number of records per page, from 1 to 100",
|
|
1856
1979
|
valueName: "number",
|
|
1857
|
-
schema: z.coerce.number()
|
|
1980
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
1858
1981
|
},
|
|
1859
1982
|
{
|
|
1860
1983
|
name: "sort-by",
|
|
1861
1984
|
queryName: "sortBy",
|
|
1862
1985
|
description: "The field to sort by",
|
|
1863
1986
|
valueName: "value",
|
|
1864
|
-
schema:
|
|
1987
|
+
schema: toEnum([
|
|
1865
1988
|
"serial",
|
|
1866
1989
|
"project",
|
|
1867
1990
|
"item",
|
|
@@ -1875,14 +1998,14 @@ const surfaceCommands = [
|
|
|
1875
1998
|
queryName: "sortOrder",
|
|
1876
1999
|
description: "The sort direction",
|
|
1877
2000
|
valueName: "value",
|
|
1878
|
-
schema:
|
|
2001
|
+
schema: toEnum(["ASC", "DESC"])
|
|
1879
2002
|
},
|
|
1880
2003
|
{
|
|
1881
2004
|
name: "archived",
|
|
1882
2005
|
queryName: "archived",
|
|
1883
2006
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1884
2007
|
valueName: "value",
|
|
1885
|
-
schema:
|
|
2008
|
+
schema: toEnum([
|
|
1886
2009
|
"all",
|
|
1887
2010
|
"false",
|
|
1888
2011
|
"true"
|
|
@@ -1949,7 +2072,7 @@ const surfaceCommands = [
|
|
|
1949
2072
|
description: "The functional statuses to list, where SCRAPPED lists scrapped assets",
|
|
1950
2073
|
valueName: "value",
|
|
1951
2074
|
repeatable: true,
|
|
1952
|
-
schema: z.array(
|
|
2075
|
+
schema: z.array(toEnum([
|
|
1953
2076
|
"FUNCTIONAL",
|
|
1954
2077
|
"NEEDS_REVIEW",
|
|
1955
2078
|
"NON-FUNCTIONAL",
|
|
@@ -1962,7 +2085,7 @@ const surfaceCommands = [
|
|
|
1962
2085
|
description: "The transit statuses to list",
|
|
1963
2086
|
valueName: "value",
|
|
1964
2087
|
repeatable: true,
|
|
1965
|
-
schema: z.array(
|
|
2088
|
+
schema: z.array(toEnum([
|
|
1966
2089
|
"IN_TRANSIT",
|
|
1967
2090
|
"IN_TRANSIT_TO_FIELD",
|
|
1968
2091
|
"IN_TRANSIT_TO_INVENTORY",
|
|
@@ -2014,7 +2137,7 @@ const surfaceCommands = [
|
|
|
2014
2137
|
queryName: "scrapped",
|
|
2015
2138
|
description: "Whether to list unscrapped assets, scrapped assets, or all of them",
|
|
2016
2139
|
valueName: "value",
|
|
2017
|
-
schema:
|
|
2140
|
+
schema: toEnum([
|
|
2018
2141
|
"all",
|
|
2019
2142
|
"false",
|
|
2020
2143
|
"true"
|
|
@@ -2053,7 +2176,7 @@ const surfaceCommands = [
|
|
|
2053
2176
|
jsonPath: ["depreciationModel"],
|
|
2054
2177
|
description: "The method one unit is depreciated by, or null to clear it",
|
|
2055
2178
|
valueName: "value",
|
|
2056
|
-
schema:
|
|
2179
|
+
schema: toEnum([
|
|
2057
2180
|
"DOUBLE_DECLINING",
|
|
2058
2181
|
"STRAIGHT_LINE",
|
|
2059
2182
|
"SUM_YEAR",
|
|
@@ -2098,7 +2221,7 @@ const surfaceCommands = [
|
|
|
2098
2221
|
description: "The asset's starting functional status, FUNCTIONAL when absent, which cannot be SCRAPPED",
|
|
2099
2222
|
valueName: "value",
|
|
2100
2223
|
nullable: true,
|
|
2101
|
-
schema:
|
|
2224
|
+
schema: toEnum([
|
|
2102
2225
|
"FUNCTIONAL",
|
|
2103
2226
|
"NEEDS_REVIEW",
|
|
2104
2227
|
"NON-FUNCTIONAL",
|
|
@@ -2197,7 +2320,7 @@ const surfaceCommands = [
|
|
|
2197
2320
|
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
2198
2321
|
valueName: "number",
|
|
2199
2322
|
nullable: true,
|
|
2200
|
-
schema: z.coerce.number()
|
|
2323
|
+
schema: z.coerce.number().int()
|
|
2201
2324
|
}
|
|
2202
2325
|
]
|
|
2203
2326
|
}),
|
|
@@ -2311,7 +2434,7 @@ const surfaceCommands = [
|
|
|
2311
2434
|
description: "The asset's new functional status, which cannot be SCRAPPED because scrapping has its own endpoint",
|
|
2312
2435
|
valueName: "value",
|
|
2313
2436
|
nullable: true,
|
|
2314
|
-
schema:
|
|
2437
|
+
schema: toEnum([
|
|
2315
2438
|
"FUNCTIONAL",
|
|
2316
2439
|
"NEEDS_REVIEW",
|
|
2317
2440
|
"NON-FUNCTIONAL",
|
|
@@ -2393,7 +2516,7 @@ const surfaceCommands = [
|
|
|
2393
2516
|
jsonPath: ["depreciationModel"],
|
|
2394
2517
|
description: "The method one unit is depreciated by, or null to clear it",
|
|
2395
2518
|
valueName: "value",
|
|
2396
|
-
schema:
|
|
2519
|
+
schema: toEnum([
|
|
2397
2520
|
"DOUBLE_DECLINING",
|
|
2398
2521
|
"STRAIGHT_LINE",
|
|
2399
2522
|
"SUM_YEAR",
|
|
@@ -2493,7 +2616,7 @@ const surfaceCommands = [
|
|
|
2493
2616
|
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
2494
2617
|
valueName: "number",
|
|
2495
2618
|
nullable: true,
|
|
2496
|
-
schema: z.coerce.number()
|
|
2619
|
+
schema: z.coerce.number().int()
|
|
2497
2620
|
}
|
|
2498
2621
|
]
|
|
2499
2622
|
}), {
|
|
@@ -2549,7 +2672,7 @@ const surfaceCommands = [
|
|
|
2549
2672
|
description: "Whether the adjustment adds to the asset's cost basis or writes it down",
|
|
2550
2673
|
valueName: "value",
|
|
2551
2674
|
required: true,
|
|
2552
|
-
schema:
|
|
2675
|
+
schema: toEnum(["CAPITALIZATION", "IMPAIRMENT"])
|
|
2553
2676
|
},
|
|
2554
2677
|
{
|
|
2555
2678
|
name: "amount",
|
|
@@ -2581,7 +2704,7 @@ const surfaceCommands = [
|
|
|
2581
2704
|
description: "Why the adjustment was made, which must be one its adjustment type allows",
|
|
2582
2705
|
valueName: "value",
|
|
2583
2706
|
required: true,
|
|
2584
|
-
schema:
|
|
2707
|
+
schema: toEnum([
|
|
2585
2708
|
"ADDITION",
|
|
2586
2709
|
"BETTERMENT",
|
|
2587
2710
|
"DAMAGE",
|
|
@@ -3044,7 +3167,7 @@ const surfaceCommands = [
|
|
|
3044
3167
|
description: "Why the useful life was revised",
|
|
3045
3168
|
valueName: "value",
|
|
3046
3169
|
required: true,
|
|
3047
|
-
schema:
|
|
3170
|
+
schema: toEnum([
|
|
3048
3171
|
"CHANGE_IN_USE",
|
|
3049
3172
|
"DAMAGE",
|
|
3050
3173
|
"OBSOLESCENCE",
|
|
@@ -3060,7 +3183,7 @@ const surfaceCommands = [
|
|
|
3060
3183
|
description: "The asset's revised useful life in months",
|
|
3061
3184
|
valueName: "number",
|
|
3062
3185
|
required: true,
|
|
3063
|
-
schema: z.coerce.number()
|
|
3186
|
+
schema: z.coerce.number().int()
|
|
3064
3187
|
}
|
|
3065
3188
|
]
|
|
3066
3189
|
})]
|
|
@@ -3087,14 +3210,14 @@ const surfaceCommands = [
|
|
|
3087
3210
|
queryName: "page",
|
|
3088
3211
|
description: "The page to return, starting at 1",
|
|
3089
3212
|
valueName: "number",
|
|
3090
|
-
schema: z.coerce.number()
|
|
3213
|
+
schema: z.coerce.number().int()
|
|
3091
3214
|
},
|
|
3092
3215
|
{
|
|
3093
3216
|
name: "limit",
|
|
3094
3217
|
queryName: "limit",
|
|
3095
3218
|
description: "The number of records per page, from 1 to 100",
|
|
3096
3219
|
valueName: "number",
|
|
3097
|
-
schema: z.coerce.number()
|
|
3220
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
3098
3221
|
},
|
|
3099
3222
|
{
|
|
3100
3223
|
name: "sort-by",
|
|
@@ -3108,14 +3231,14 @@ const surfaceCommands = [
|
|
|
3108
3231
|
queryName: "sortOrder",
|
|
3109
3232
|
description: "The sort direction",
|
|
3110
3233
|
valueName: "value",
|
|
3111
|
-
schema:
|
|
3234
|
+
schema: toEnum(["ASC", "DESC"])
|
|
3112
3235
|
},
|
|
3113
3236
|
{
|
|
3114
3237
|
name: "archived",
|
|
3115
3238
|
queryName: "archived",
|
|
3116
3239
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
3117
3240
|
valueName: "value",
|
|
3118
|
-
schema:
|
|
3241
|
+
schema: toEnum([
|
|
3119
3242
|
"all",
|
|
3120
3243
|
"false",
|
|
3121
3244
|
"true"
|
|
@@ -3392,8 +3515,7 @@ const surfaceCommands = [
|
|
|
3392
3515
|
name: "is-public",
|
|
3393
3516
|
jsonPath: ["isPublic"],
|
|
3394
3517
|
description: "Whether any organization's API key may download the file, which is false unless sent as true",
|
|
3395
|
-
|
|
3396
|
-
schema: z.string()
|
|
3518
|
+
schema: z.boolean()
|
|
3397
3519
|
}
|
|
3398
3520
|
]
|
|
3399
3521
|
}
|
|
@@ -3437,7 +3559,7 @@ const surfaceCommands = [
|
|
|
3437
3559
|
queryName: "type",
|
|
3438
3560
|
description: "The item type to list, SERVICE, DEVICE, or BULK, or absent for every type",
|
|
3439
3561
|
valueName: "value",
|
|
3440
|
-
schema:
|
|
3562
|
+
schema: toEnum([
|
|
3441
3563
|
"BULK",
|
|
3442
3564
|
"DEVICE",
|
|
3443
3565
|
"SERVICE"
|
|
@@ -3455,21 +3577,21 @@ const surfaceCommands = [
|
|
|
3455
3577
|
queryName: "page",
|
|
3456
3578
|
description: "The page to return, starting at 1",
|
|
3457
3579
|
valueName: "number",
|
|
3458
|
-
schema: z.coerce.number()
|
|
3580
|
+
schema: z.coerce.number().int()
|
|
3459
3581
|
},
|
|
3460
3582
|
{
|
|
3461
3583
|
name: "limit",
|
|
3462
3584
|
queryName: "limit",
|
|
3463
3585
|
description: "The number of records per page, from 1 to 100",
|
|
3464
3586
|
valueName: "number",
|
|
3465
|
-
schema: z.coerce.number()
|
|
3587
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
3466
3588
|
},
|
|
3467
3589
|
{
|
|
3468
3590
|
name: "sort-by",
|
|
3469
3591
|
queryName: "sortBy",
|
|
3470
3592
|
description: "The field to sort by",
|
|
3471
3593
|
valueName: "value",
|
|
3472
|
-
schema:
|
|
3594
|
+
schema: toEnum([
|
|
3473
3595
|
"name",
|
|
3474
3596
|
"sku",
|
|
3475
3597
|
"type",
|
|
@@ -3481,14 +3603,14 @@ const surfaceCommands = [
|
|
|
3481
3603
|
queryName: "sortOrder",
|
|
3482
3604
|
description: "The sort direction",
|
|
3483
3605
|
valueName: "value",
|
|
3484
|
-
schema:
|
|
3606
|
+
schema: toEnum(["ASC", "DESC"])
|
|
3485
3607
|
},
|
|
3486
3608
|
{
|
|
3487
3609
|
name: "archived",
|
|
3488
3610
|
queryName: "archived",
|
|
3489
3611
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
3490
3612
|
valueName: "value",
|
|
3491
|
-
schema:
|
|
3613
|
+
schema: toEnum([
|
|
3492
3614
|
"all",
|
|
3493
3615
|
"false",
|
|
3494
3616
|
"true"
|
|
@@ -3549,7 +3671,7 @@ const surfaceCommands = [
|
|
|
3549
3671
|
description: "SERVICE for a non-physical item, DEVICE for a physical item tracked by serial number, or BULK for a part tracked by quantity",
|
|
3550
3672
|
valueName: "value",
|
|
3551
3673
|
required: true,
|
|
3552
|
-
schema:
|
|
3674
|
+
schema: toEnum([
|
|
3553
3675
|
"BULK",
|
|
3554
3676
|
"DEVICE",
|
|
3555
3677
|
"SERVICE"
|
|
@@ -3560,7 +3682,7 @@ const surfaceCommands = [
|
|
|
3560
3682
|
jsonPath: ["unitOfMeasure"],
|
|
3561
3683
|
description: "The unit a BULK item's quantities are counted in, which SERVICE and DEVICE items ignore",
|
|
3562
3684
|
valueName: "value",
|
|
3563
|
-
schema:
|
|
3685
|
+
schema: toEnum([
|
|
3564
3686
|
"BG",
|
|
3565
3687
|
"BO",
|
|
3566
3688
|
"BX",
|
|
@@ -3706,7 +3828,7 @@ const surfaceCommands = [
|
|
|
3706
3828
|
description: "The type to convert the item to, when the item's assets and inventory history allow the conversion",
|
|
3707
3829
|
valueName: "value",
|
|
3708
3830
|
nullable: true,
|
|
3709
|
-
schema:
|
|
3831
|
+
schema: toEnum([
|
|
3710
3832
|
"BULK",
|
|
3711
3833
|
"DEVICE",
|
|
3712
3834
|
"SERVICE"
|
|
@@ -3754,7 +3876,7 @@ const surfaceCommands = [
|
|
|
3754
3876
|
jsonPath: ["depreciationModel"],
|
|
3755
3877
|
description: "The method one unit is depreciated by, or null to clear it",
|
|
3756
3878
|
valueName: "value",
|
|
3757
|
-
schema:
|
|
3879
|
+
schema: toEnum([
|
|
3758
3880
|
"DOUBLE_DECLINING",
|
|
3759
3881
|
"STRAIGHT_LINE",
|
|
3760
3882
|
"SUM_YEAR",
|
|
@@ -3839,7 +3961,7 @@ const surfaceCommands = [
|
|
|
3839
3961
|
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
3840
3962
|
valueName: "number",
|
|
3841
3963
|
nullable: true,
|
|
3842
|
-
schema: z.coerce.number()
|
|
3964
|
+
schema: z.coerce.number().int()
|
|
3843
3965
|
}
|
|
3844
3966
|
]
|
|
3845
3967
|
})]
|
|
@@ -3870,7 +3992,7 @@ const surfaceCommands = [
|
|
|
3870
3992
|
description: "The kind of value the field holds",
|
|
3871
3993
|
valueName: "value",
|
|
3872
3994
|
required: true,
|
|
3873
|
-
schema:
|
|
3995
|
+
schema: toEnum([
|
|
3874
3996
|
"BOOLEAN",
|
|
3875
3997
|
"DATE",
|
|
3876
3998
|
"DATE_TIME",
|
|
@@ -3896,7 +4018,7 @@ const surfaceCommands = [
|
|
|
3896
4018
|
valueName: "number",
|
|
3897
4019
|
required: true,
|
|
3898
4020
|
nullable: true,
|
|
3899
|
-
schema: z.coerce.number()
|
|
4021
|
+
schema: z.coerce.number().int()
|
|
3900
4022
|
},
|
|
3901
4023
|
{
|
|
3902
4024
|
name: "section",
|
|
@@ -3905,7 +4027,7 @@ const surfaceCommands = [
|
|
|
3905
4027
|
valueName: "number",
|
|
3906
4028
|
required: true,
|
|
3907
4029
|
nullable: true,
|
|
3908
|
-
schema: z.coerce.number()
|
|
4030
|
+
schema: z.coerce.number().int()
|
|
3909
4031
|
}
|
|
3910
4032
|
]
|
|
3911
4033
|
}),
|
|
@@ -3976,21 +4098,21 @@ const surfaceCommands = [
|
|
|
3976
4098
|
queryName: "page",
|
|
3977
4099
|
description: "The page to return, starting at 1",
|
|
3978
4100
|
valueName: "number",
|
|
3979
|
-
schema: z.coerce.number()
|
|
4101
|
+
schema: z.coerce.number().int()
|
|
3980
4102
|
},
|
|
3981
4103
|
{
|
|
3982
4104
|
name: "limit",
|
|
3983
4105
|
queryName: "limit",
|
|
3984
4106
|
description: "The number of records per page, from 1 to 100",
|
|
3985
4107
|
valueName: "number",
|
|
3986
|
-
schema: z.coerce.number()
|
|
4108
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
3987
4109
|
},
|
|
3988
4110
|
{
|
|
3989
4111
|
name: "sort-by",
|
|
3990
4112
|
queryName: "sortBy",
|
|
3991
4113
|
description: "The field to sort by",
|
|
3992
4114
|
valueName: "value",
|
|
3993
|
-
schema:
|
|
4115
|
+
schema: toEnum([
|
|
3994
4116
|
"name",
|
|
3995
4117
|
"company",
|
|
3996
4118
|
"assetCount"
|
|
@@ -4001,14 +4123,14 @@ const surfaceCommands = [
|
|
|
4001
4123
|
queryName: "sortOrder",
|
|
4002
4124
|
description: "The sort direction",
|
|
4003
4125
|
valueName: "value",
|
|
4004
|
-
schema:
|
|
4126
|
+
schema: toEnum(["ASC", "DESC"])
|
|
4005
4127
|
},
|
|
4006
4128
|
{
|
|
4007
4129
|
name: "archived",
|
|
4008
4130
|
queryName: "archived",
|
|
4009
4131
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
4010
4132
|
valueName: "value",
|
|
4011
|
-
schema:
|
|
4133
|
+
schema: toEnum([
|
|
4012
4134
|
"all",
|
|
4013
4135
|
"false",
|
|
4014
4136
|
"true"
|
|
@@ -4019,7 +4141,7 @@ const surfaceCommands = [
|
|
|
4019
4141
|
queryName: "isTransient",
|
|
4020
4142
|
description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
|
|
4021
4143
|
valueName: "value",
|
|
4022
|
-
schema:
|
|
4144
|
+
schema: toEnum([
|
|
4023
4145
|
"all",
|
|
4024
4146
|
"false",
|
|
4025
4147
|
"true"
|
|
@@ -4184,7 +4306,7 @@ const surfaceCommands = [
|
|
|
4184
4306
|
jsonPath: ["type"],
|
|
4185
4307
|
description: "SITE for a site, or ZONE for a zone within a site",
|
|
4186
4308
|
valueName: "value",
|
|
4187
|
-
schema:
|
|
4309
|
+
schema: toEnum([
|
|
4188
4310
|
"SITE",
|
|
4189
4311
|
"UNKNOWN",
|
|
4190
4312
|
"ZONE"
|
|
@@ -4340,7 +4462,7 @@ const surfaceCommands = [
|
|
|
4340
4462
|
jsonPath: ["type"],
|
|
4341
4463
|
description: "SITE for a site, or ZONE for a zone within a site",
|
|
4342
4464
|
valueName: "value",
|
|
4343
|
-
schema:
|
|
4465
|
+
schema: toEnum([
|
|
4344
4466
|
"SITE",
|
|
4345
4467
|
"UNKNOWN",
|
|
4346
4468
|
"ZONE"
|
|
@@ -4370,7 +4492,7 @@ const surfaceCommands = [
|
|
|
4370
4492
|
queryName: "archived",
|
|
4371
4493
|
description: "Whether to return unarchived zones, archived zones, or all of them",
|
|
4372
4494
|
valueName: "value",
|
|
4373
|
-
schema:
|
|
4495
|
+
schema: toEnum([
|
|
4374
4496
|
"all",
|
|
4375
4497
|
"false",
|
|
4376
4498
|
"true"
|
|
@@ -4465,32 +4587,6 @@ const surfaceCommands = [
|
|
|
4465
4587
|
}
|
|
4466
4588
|
];
|
|
4467
4589
|
//#endregion
|
|
4468
|
-
//#region src/auth/jwt.ts
|
|
4469
|
-
/**
|
|
4470
|
-
* toClaims reads an access token's payload for display. Nothing here verifies the
|
|
4471
|
-
* signature, because the API is what decides whether a token is good.
|
|
4472
|
-
*/
|
|
4473
|
-
function toClaims(token) {
|
|
4474
|
-
const payload = token.split(".")[1];
|
|
4475
|
-
if (!payload) return {};
|
|
4476
|
-
try {
|
|
4477
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
4478
|
-
return {
|
|
4479
|
-
expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
|
|
4480
|
-
issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
|
|
4481
|
-
scopes: toScopes(decoded),
|
|
4482
|
-
subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
|
|
4483
|
-
};
|
|
4484
|
-
} catch {
|
|
4485
|
-
return {};
|
|
4486
|
-
}
|
|
4487
|
-
}
|
|
4488
|
-
/** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
|
|
4489
|
-
function toScopes(decoded) {
|
|
4490
|
-
if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
|
|
4491
|
-
return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
|
|
4492
|
-
}
|
|
4493
|
-
//#endregion
|
|
4494
4590
|
//#region src/system/host.ts
|
|
4495
4591
|
const BYTES_PER_GB = 1024 ** 3;
|
|
4496
4592
|
/** toDistribution reads the name a Linux distribution gives itself. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hardfin/cli",
|
|
3
|
-
"version": "0.0.2-dev.
|
|
3
|
+
"version": "0.0.2-dev.15",
|
|
4
4
|
"description": "Command line interface for the Hardfin API",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hardfin, Inc.",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^22.15.0",
|
|
49
|
+
"@vitest/coverage-v8": "^5.0.1",
|
|
49
50
|
"tsdown": "^0.23.0",
|
|
50
51
|
"typescript": "^5.9.0",
|
|
51
52
|
"unrun": "^0.3.1",
|