@hardfin/cli 0.0.2-dev.13 → 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/README.md +3 -3
- package/dist/cli.js +228 -112
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -285,9 +285,9 @@ Nothing secret is printed. An API key and a refresh token are each reported as a
|
|
|
285
285
|
`sha256:` fingerprint, which identifies a credential across two machines without disclosing
|
|
286
286
|
it.
|
|
287
287
|
|
|
288
|
-
`refreshExpiresAt` is
|
|
289
|
-
|
|
290
|
-
|
|
288
|
+
`refreshExpiresAt` is what the server said when it issued the token. A sign in stored before
|
|
289
|
+
the server reported one falls back to Hardfin's 90-day rule, and `refreshExpiryIsEstimated`
|
|
290
|
+
says which of the two you are looking at.
|
|
291
291
|
|
|
292
292
|
## Local configuration
|
|
293
293
|
|
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({
|
|
@@ -293,6 +319,7 @@ const TokenResponse = z.looseObject({
|
|
|
293
319
|
token_type: z.string(),
|
|
294
320
|
expires_in: z.number().optional(),
|
|
295
321
|
refresh_token: z.string().optional(),
|
|
322
|
+
refresh_token_expires_in: z.number().optional(),
|
|
296
323
|
scope: z.string().optional()
|
|
297
324
|
});
|
|
298
325
|
/** GrantFailure is a token request the authorization server refused. */
|
|
@@ -351,6 +378,7 @@ async function toTokens(url, form) {
|
|
|
351
378
|
return {
|
|
352
379
|
accessToken: parsed.data.access_token,
|
|
353
380
|
refreshToken: parsed.data.refresh_token,
|
|
381
|
+
refreshExpiresAt: parsed.data.refresh_token_expires_in === void 0 ? void 0 : Date.now() + parsed.data.refresh_token_expires_in * 1e3,
|
|
354
382
|
scope: parsed.data.scope,
|
|
355
383
|
expiresAt: parsed.data.expires_in === void 0 ? void 0 : Date.now() + parsed.data.expires_in * 1e3
|
|
356
384
|
};
|
|
@@ -375,40 +403,55 @@ function toCredentialPath() {
|
|
|
375
403
|
* family lifetime runs from. Callers hold the credential lock, which is what makes a write
|
|
376
404
|
* from another process merge rather than disappear.
|
|
377
405
|
*/
|
|
378
|
-
function keep(issuer, refreshToken) {
|
|
406
|
+
function keep(issuer, refreshToken, expiresAt) {
|
|
379
407
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
380
408
|
const record = {
|
|
381
409
|
refreshToken,
|
|
382
410
|
signedInAt: toCredential(issuer)?.signedInAt ?? now,
|
|
383
|
-
renewedAt: now
|
|
411
|
+
renewedAt: now,
|
|
412
|
+
expiresAt: expiresAt === void 0 ? void 0 : new Date(expiresAt).toISOString()
|
|
384
413
|
};
|
|
385
414
|
const keyring = toKeyring(issuer);
|
|
386
415
|
if (keyring) try {
|
|
387
416
|
keyring.setPassword(JSON.stringify(record));
|
|
417
|
+
forgetFile(issuer);
|
|
388
418
|
return "keyring";
|
|
389
419
|
} catch {}
|
|
390
420
|
writeFile({
|
|
391
421
|
...readFile(),
|
|
392
422
|
[issuer]: record
|
|
393
423
|
});
|
|
424
|
+
forgetKeyring(issuer);
|
|
394
425
|
return "file";
|
|
395
426
|
}
|
|
396
|
-
/**
|
|
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
|
+
*/
|
|
397
432
|
function toCredential(issuer) {
|
|
398
|
-
const
|
|
399
|
-
if (keyring) try {
|
|
400
|
-
const held = keyring.getPassword();
|
|
401
|
-
if (held) return {
|
|
402
|
-
...toRecord(held),
|
|
403
|
-
backend: "keyring"
|
|
404
|
-
};
|
|
405
|
-
} catch {}
|
|
433
|
+
const fromKeyring = toKeyringCredential(issuer);
|
|
406
434
|
const held = readFile()[issuer];
|
|
407
|
-
|
|
435
|
+
const fromFile = held === void 0 ? void 0 : {
|
|
408
436
|
...held,
|
|
409
437
|
backend: "file",
|
|
410
438
|
path: toCredentialPath()
|
|
411
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
|
+
}
|
|
412
455
|
}
|
|
413
456
|
/** toRecord reads a stored entry, which older versions wrote as the bare token. */
|
|
414
457
|
function toRecord(held) {
|
|
@@ -421,10 +464,17 @@ function toRecord(held) {
|
|
|
421
464
|
}
|
|
422
465
|
/** forget removes whatever is held for an issuer, in both places. */
|
|
423
466
|
function forget(issuer) {
|
|
467
|
+
forgetKeyring(issuer);
|
|
468
|
+
forgetFile(issuer);
|
|
469
|
+
}
|
|
470
|
+
function forgetKeyring(issuer) {
|
|
424
471
|
const keyring = toKeyring(issuer);
|
|
425
|
-
if (keyring)
|
|
472
|
+
if (!keyring) return;
|
|
473
|
+
try {
|
|
426
474
|
keyring.deletePassword();
|
|
427
475
|
} catch {}
|
|
476
|
+
}
|
|
477
|
+
function forgetFile(issuer) {
|
|
428
478
|
const held = readFile();
|
|
429
479
|
if (held[issuer] === void 0) return;
|
|
430
480
|
delete held[issuer];
|
|
@@ -471,6 +521,8 @@ const STALE_MS = 3e4;
|
|
|
471
521
|
const WAIT_MS$1 = 1e4;
|
|
472
522
|
const RETRY_MS = 25;
|
|
473
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;
|
|
474
526
|
/** toLockPath names the lock every process coordinates credential writes through. */
|
|
475
527
|
function toLockPath() {
|
|
476
528
|
return join(dirname(toCredentialPath()), "credentials.lock");
|
|
@@ -482,17 +534,23 @@ function tryAcquire() {
|
|
|
482
534
|
recursive: true,
|
|
483
535
|
mode: DIRECTORY_MODE
|
|
484
536
|
});
|
|
537
|
+
const mark = `${process.pid}:${randomUUID()}`;
|
|
485
538
|
try {
|
|
486
539
|
const handle = openSync(path, "wx");
|
|
487
|
-
writeSync(handle,
|
|
540
|
+
writeSync(handle, mark);
|
|
488
541
|
closeSync(handle);
|
|
542
|
+
heldBy = mark;
|
|
489
543
|
return true;
|
|
490
544
|
} catch {
|
|
491
|
-
return isStale(path) ? steal(path) : false;
|
|
545
|
+
return isStale(path) ? steal(path, mark) : false;
|
|
492
546
|
}
|
|
493
547
|
}
|
|
494
|
-
/** release lets the next process in. */
|
|
548
|
+
/** release lets the next process in, and only ever removes this process's own lock. */
|
|
495
549
|
function release$1() {
|
|
550
|
+
const mark = heldBy;
|
|
551
|
+
if (mark === void 0) return;
|
|
552
|
+
heldBy = void 0;
|
|
553
|
+
if (toMark(toLockPath()) !== mark) return;
|
|
496
554
|
rmSync(toLockPath(), { force: true });
|
|
497
555
|
}
|
|
498
556
|
/**
|
|
@@ -519,9 +577,29 @@ function isStale(path) {
|
|
|
519
577
|
return true;
|
|
520
578
|
}
|
|
521
579
|
}
|
|
522
|
-
|
|
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) {
|
|
523
585
|
rmSync(path, { force: true });
|
|
524
|
-
|
|
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
|
+
}
|
|
525
603
|
}
|
|
526
604
|
//#endregion
|
|
527
605
|
//#region src/auth/session.ts
|
|
@@ -564,9 +642,9 @@ async function toAccessToken(settings, refreshToken) {
|
|
|
564
642
|
return await withLock(async () => {
|
|
565
643
|
const latest = toCredential(settings.issuerUrl)?.refreshToken ?? refreshToken;
|
|
566
644
|
try {
|
|
567
|
-
const tokens = await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest);
|
|
645
|
+
const tokens = toDated(await toTokensFromRefresh(metadata.token_endpoint, settings.clientId, latest));
|
|
568
646
|
held.set(settings.issuerUrl, tokens);
|
|
569
|
-
|
|
647
|
+
store(settings.issuerUrl, tokens, latest);
|
|
570
648
|
return tokens;
|
|
571
649
|
} catch (error) {
|
|
572
650
|
if (error instanceof GrantFailure && isDead(error.code)) {
|
|
@@ -577,14 +655,37 @@ async function toAccessToken(settings, refreshToken) {
|
|
|
577
655
|
}
|
|
578
656
|
});
|
|
579
657
|
}
|
|
580
|
-
/**
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
]);
|
|
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
|
+
*/
|
|
586
663
|
function isDead(code) {
|
|
587
|
-
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
|
+
}
|
|
588
689
|
}
|
|
589
690
|
/** forgetHeldTokens drops the access tokens this process is holding. */
|
|
590
691
|
function forgetHeldTokens() {
|
|
@@ -1194,6 +1295,7 @@ function toPrompt(url) {
|
|
|
1194
1295
|
fail = reject;
|
|
1195
1296
|
});
|
|
1196
1297
|
const input = process.stdin;
|
|
1298
|
+
pasted.catch(() => {});
|
|
1197
1299
|
if (!input.isTTY) return {
|
|
1198
1300
|
pasted,
|
|
1199
1301
|
close: () => {}
|
|
@@ -1309,9 +1411,13 @@ async function runLogin(input) {
|
|
|
1309
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`);
|
|
1310
1412
|
const prompt = toPrompt(url);
|
|
1311
1413
|
if (process.stdin.isTTY) process.stderr.write("Press c to copy the URL, or paste the code or redirect URL here: ");
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1414
|
+
let callback;
|
|
1415
|
+
try {
|
|
1416
|
+
callback = await Promise.race([listener.callback, prompt.pasted]);
|
|
1417
|
+
} finally {
|
|
1418
|
+
listener.close();
|
|
1419
|
+
prompt.close();
|
|
1420
|
+
}
|
|
1315
1421
|
process.stderr.write(callback.error ? "\n" : "\nApproved, finishing the sign in\n");
|
|
1316
1422
|
if (callback.error) {
|
|
1317
1423
|
writeFailure(`sign in was refused: ${callback.error}${callback.errorDescription ? `, ${callback.errorDescription}` : ""}`, input.isJSON);
|
|
@@ -1357,7 +1463,15 @@ async function runDeviceLogin(input, metadata, scope) {
|
|
|
1357
1463
|
opened ? "Opened your browser there. Waiting for approval\n" : "Open that page on any machine, and enter the code. Waiting for approval\n"
|
|
1358
1464
|
].join("\n"));
|
|
1359
1465
|
const prompt = toPrompt(url);
|
|
1360
|
-
|
|
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");
|
|
1361
1475
|
}
|
|
1362
1476
|
/** toSignedIn stores what a sign in issued, whichever flow issued it. */
|
|
1363
1477
|
async function toSignedIn(input, metadata, tokens) {
|
|
@@ -1366,7 +1480,7 @@ async function toSignedIn(input, metadata, tokens) {
|
|
|
1366
1480
|
writeFailure("the authorization server issued no refresh token, so this sign in cannot be kept", input.isJSON);
|
|
1367
1481
|
return ExitCode.ERROR;
|
|
1368
1482
|
}
|
|
1369
|
-
const backend = await withLock(() => keep(input.resolved.settings.issuerUrl, refreshToken));
|
|
1483
|
+
const backend = await withLock(() => keep(input.resolved.settings.issuerUrl, refreshToken, tokens.refreshExpiresAt));
|
|
1370
1484
|
if (input.isJSON) {
|
|
1371
1485
|
writeData({
|
|
1372
1486
|
signedIn: true,
|
|
@@ -1632,6 +1746,13 @@ async function runMcp(input) {
|
|
|
1632
1746
|
}
|
|
1633
1747
|
//#endregion
|
|
1634
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
|
+
}
|
|
1635
1756
|
const UNSET_FLAG = {
|
|
1636
1757
|
name: "unset",
|
|
1637
1758
|
description: "A field to clear, named as its flag is, repeatable",
|
|
@@ -1738,7 +1859,9 @@ function toQuery(operation, flags) {
|
|
|
1738
1859
|
for (const flag of operation.queryFlags) {
|
|
1739
1860
|
const value = flags[toOptionKey(flag.name)];
|
|
1740
1861
|
if (value === void 0) continue;
|
|
1741
|
-
|
|
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));
|
|
1742
1865
|
}
|
|
1743
1866
|
return query;
|
|
1744
1867
|
}
|
|
@@ -1758,7 +1881,10 @@ async function toForm(upload, flags) {
|
|
|
1758
1881
|
}
|
|
1759
1882
|
for (const field of upload.fields) {
|
|
1760
1883
|
const value = flags[toOptionKey(field.name)];
|
|
1761
|
-
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
|
+
}
|
|
1762
1888
|
}
|
|
1763
1889
|
return form;
|
|
1764
1890
|
}
|
|
@@ -1844,21 +1970,21 @@ const surfaceCommands = [
|
|
|
1844
1970
|
queryName: "page",
|
|
1845
1971
|
description: "The page to return, starting at 1",
|
|
1846
1972
|
valueName: "number",
|
|
1847
|
-
schema: z.coerce.number()
|
|
1973
|
+
schema: z.coerce.number().int()
|
|
1848
1974
|
},
|
|
1849
1975
|
{
|
|
1850
1976
|
name: "limit",
|
|
1851
1977
|
queryName: "limit",
|
|
1852
1978
|
description: "The number of records per page, from 1 to 100",
|
|
1853
1979
|
valueName: "number",
|
|
1854
|
-
schema: z.coerce.number()
|
|
1980
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
1855
1981
|
},
|
|
1856
1982
|
{
|
|
1857
1983
|
name: "sort-by",
|
|
1858
1984
|
queryName: "sortBy",
|
|
1859
1985
|
description: "The field to sort by",
|
|
1860
1986
|
valueName: "value",
|
|
1861
|
-
schema:
|
|
1987
|
+
schema: toEnum([
|
|
1862
1988
|
"serial",
|
|
1863
1989
|
"project",
|
|
1864
1990
|
"item",
|
|
@@ -1872,14 +1998,14 @@ const surfaceCommands = [
|
|
|
1872
1998
|
queryName: "sortOrder",
|
|
1873
1999
|
description: "The sort direction",
|
|
1874
2000
|
valueName: "value",
|
|
1875
|
-
schema:
|
|
2001
|
+
schema: toEnum(["ASC", "DESC"])
|
|
1876
2002
|
},
|
|
1877
2003
|
{
|
|
1878
2004
|
name: "archived",
|
|
1879
2005
|
queryName: "archived",
|
|
1880
2006
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
1881
2007
|
valueName: "value",
|
|
1882
|
-
schema:
|
|
2008
|
+
schema: toEnum([
|
|
1883
2009
|
"all",
|
|
1884
2010
|
"false",
|
|
1885
2011
|
"true"
|
|
@@ -1946,7 +2072,7 @@ const surfaceCommands = [
|
|
|
1946
2072
|
description: "The functional statuses to list, where SCRAPPED lists scrapped assets",
|
|
1947
2073
|
valueName: "value",
|
|
1948
2074
|
repeatable: true,
|
|
1949
|
-
schema: z.array(
|
|
2075
|
+
schema: z.array(toEnum([
|
|
1950
2076
|
"FUNCTIONAL",
|
|
1951
2077
|
"NEEDS_REVIEW",
|
|
1952
2078
|
"NON-FUNCTIONAL",
|
|
@@ -1959,7 +2085,7 @@ const surfaceCommands = [
|
|
|
1959
2085
|
description: "The transit statuses to list",
|
|
1960
2086
|
valueName: "value",
|
|
1961
2087
|
repeatable: true,
|
|
1962
|
-
schema: z.array(
|
|
2088
|
+
schema: z.array(toEnum([
|
|
1963
2089
|
"IN_TRANSIT",
|
|
1964
2090
|
"IN_TRANSIT_TO_FIELD",
|
|
1965
2091
|
"IN_TRANSIT_TO_INVENTORY",
|
|
@@ -2011,7 +2137,7 @@ const surfaceCommands = [
|
|
|
2011
2137
|
queryName: "scrapped",
|
|
2012
2138
|
description: "Whether to list unscrapped assets, scrapped assets, or all of them",
|
|
2013
2139
|
valueName: "value",
|
|
2014
|
-
schema:
|
|
2140
|
+
schema: toEnum([
|
|
2015
2141
|
"all",
|
|
2016
2142
|
"false",
|
|
2017
2143
|
"true"
|
|
@@ -2050,7 +2176,7 @@ const surfaceCommands = [
|
|
|
2050
2176
|
jsonPath: ["depreciationModel"],
|
|
2051
2177
|
description: "The method one unit is depreciated by, or null to clear it",
|
|
2052
2178
|
valueName: "value",
|
|
2053
|
-
schema:
|
|
2179
|
+
schema: toEnum([
|
|
2054
2180
|
"DOUBLE_DECLINING",
|
|
2055
2181
|
"STRAIGHT_LINE",
|
|
2056
2182
|
"SUM_YEAR",
|
|
@@ -2095,7 +2221,7 @@ const surfaceCommands = [
|
|
|
2095
2221
|
description: "The asset's starting functional status, FUNCTIONAL when absent, which cannot be SCRAPPED",
|
|
2096
2222
|
valueName: "value",
|
|
2097
2223
|
nullable: true,
|
|
2098
|
-
schema:
|
|
2224
|
+
schema: toEnum([
|
|
2099
2225
|
"FUNCTIONAL",
|
|
2100
2226
|
"NEEDS_REVIEW",
|
|
2101
2227
|
"NON-FUNCTIONAL",
|
|
@@ -2194,7 +2320,7 @@ const surfaceCommands = [
|
|
|
2194
2320
|
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
2195
2321
|
valueName: "number",
|
|
2196
2322
|
nullable: true,
|
|
2197
|
-
schema: z.coerce.number()
|
|
2323
|
+
schema: z.coerce.number().int()
|
|
2198
2324
|
}
|
|
2199
2325
|
]
|
|
2200
2326
|
}),
|
|
@@ -2308,7 +2434,7 @@ const surfaceCommands = [
|
|
|
2308
2434
|
description: "The asset's new functional status, which cannot be SCRAPPED because scrapping has its own endpoint",
|
|
2309
2435
|
valueName: "value",
|
|
2310
2436
|
nullable: true,
|
|
2311
|
-
schema:
|
|
2437
|
+
schema: toEnum([
|
|
2312
2438
|
"FUNCTIONAL",
|
|
2313
2439
|
"NEEDS_REVIEW",
|
|
2314
2440
|
"NON-FUNCTIONAL",
|
|
@@ -2390,7 +2516,7 @@ const surfaceCommands = [
|
|
|
2390
2516
|
jsonPath: ["depreciationModel"],
|
|
2391
2517
|
description: "The method one unit is depreciated by, or null to clear it",
|
|
2392
2518
|
valueName: "value",
|
|
2393
|
-
schema:
|
|
2519
|
+
schema: toEnum([
|
|
2394
2520
|
"DOUBLE_DECLINING",
|
|
2395
2521
|
"STRAIGHT_LINE",
|
|
2396
2522
|
"SUM_YEAR",
|
|
@@ -2490,7 +2616,7 @@ const surfaceCommands = [
|
|
|
2490
2616
|
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
2491
2617
|
valueName: "number",
|
|
2492
2618
|
nullable: true,
|
|
2493
|
-
schema: z.coerce.number()
|
|
2619
|
+
schema: z.coerce.number().int()
|
|
2494
2620
|
}
|
|
2495
2621
|
]
|
|
2496
2622
|
}), {
|
|
@@ -2546,7 +2672,7 @@ const surfaceCommands = [
|
|
|
2546
2672
|
description: "Whether the adjustment adds to the asset's cost basis or writes it down",
|
|
2547
2673
|
valueName: "value",
|
|
2548
2674
|
required: true,
|
|
2549
|
-
schema:
|
|
2675
|
+
schema: toEnum(["CAPITALIZATION", "IMPAIRMENT"])
|
|
2550
2676
|
},
|
|
2551
2677
|
{
|
|
2552
2678
|
name: "amount",
|
|
@@ -2578,7 +2704,7 @@ const surfaceCommands = [
|
|
|
2578
2704
|
description: "Why the adjustment was made, which must be one its adjustment type allows",
|
|
2579
2705
|
valueName: "value",
|
|
2580
2706
|
required: true,
|
|
2581
|
-
schema:
|
|
2707
|
+
schema: toEnum([
|
|
2582
2708
|
"ADDITION",
|
|
2583
2709
|
"BETTERMENT",
|
|
2584
2710
|
"DAMAGE",
|
|
@@ -3041,7 +3167,7 @@ const surfaceCommands = [
|
|
|
3041
3167
|
description: "Why the useful life was revised",
|
|
3042
3168
|
valueName: "value",
|
|
3043
3169
|
required: true,
|
|
3044
|
-
schema:
|
|
3170
|
+
schema: toEnum([
|
|
3045
3171
|
"CHANGE_IN_USE",
|
|
3046
3172
|
"DAMAGE",
|
|
3047
3173
|
"OBSOLESCENCE",
|
|
@@ -3057,7 +3183,7 @@ const surfaceCommands = [
|
|
|
3057
3183
|
description: "The asset's revised useful life in months",
|
|
3058
3184
|
valueName: "number",
|
|
3059
3185
|
required: true,
|
|
3060
|
-
schema: z.coerce.number()
|
|
3186
|
+
schema: z.coerce.number().int()
|
|
3061
3187
|
}
|
|
3062
3188
|
]
|
|
3063
3189
|
})]
|
|
@@ -3084,14 +3210,14 @@ const surfaceCommands = [
|
|
|
3084
3210
|
queryName: "page",
|
|
3085
3211
|
description: "The page to return, starting at 1",
|
|
3086
3212
|
valueName: "number",
|
|
3087
|
-
schema: z.coerce.number()
|
|
3213
|
+
schema: z.coerce.number().int()
|
|
3088
3214
|
},
|
|
3089
3215
|
{
|
|
3090
3216
|
name: "limit",
|
|
3091
3217
|
queryName: "limit",
|
|
3092
3218
|
description: "The number of records per page, from 1 to 100",
|
|
3093
3219
|
valueName: "number",
|
|
3094
|
-
schema: z.coerce.number()
|
|
3220
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
3095
3221
|
},
|
|
3096
3222
|
{
|
|
3097
3223
|
name: "sort-by",
|
|
@@ -3105,14 +3231,14 @@ const surfaceCommands = [
|
|
|
3105
3231
|
queryName: "sortOrder",
|
|
3106
3232
|
description: "The sort direction",
|
|
3107
3233
|
valueName: "value",
|
|
3108
|
-
schema:
|
|
3234
|
+
schema: toEnum(["ASC", "DESC"])
|
|
3109
3235
|
},
|
|
3110
3236
|
{
|
|
3111
3237
|
name: "archived",
|
|
3112
3238
|
queryName: "archived",
|
|
3113
3239
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
3114
3240
|
valueName: "value",
|
|
3115
|
-
schema:
|
|
3241
|
+
schema: toEnum([
|
|
3116
3242
|
"all",
|
|
3117
3243
|
"false",
|
|
3118
3244
|
"true"
|
|
@@ -3389,8 +3515,7 @@ const surfaceCommands = [
|
|
|
3389
3515
|
name: "is-public",
|
|
3390
3516
|
jsonPath: ["isPublic"],
|
|
3391
3517
|
description: "Whether any organization's API key may download the file, which is false unless sent as true",
|
|
3392
|
-
|
|
3393
|
-
schema: z.string()
|
|
3518
|
+
schema: z.boolean()
|
|
3394
3519
|
}
|
|
3395
3520
|
]
|
|
3396
3521
|
}
|
|
@@ -3434,7 +3559,7 @@ const surfaceCommands = [
|
|
|
3434
3559
|
queryName: "type",
|
|
3435
3560
|
description: "The item type to list, SERVICE, DEVICE, or BULK, or absent for every type",
|
|
3436
3561
|
valueName: "value",
|
|
3437
|
-
schema:
|
|
3562
|
+
schema: toEnum([
|
|
3438
3563
|
"BULK",
|
|
3439
3564
|
"DEVICE",
|
|
3440
3565
|
"SERVICE"
|
|
@@ -3452,21 +3577,21 @@ const surfaceCommands = [
|
|
|
3452
3577
|
queryName: "page",
|
|
3453
3578
|
description: "The page to return, starting at 1",
|
|
3454
3579
|
valueName: "number",
|
|
3455
|
-
schema: z.coerce.number()
|
|
3580
|
+
schema: z.coerce.number().int()
|
|
3456
3581
|
},
|
|
3457
3582
|
{
|
|
3458
3583
|
name: "limit",
|
|
3459
3584
|
queryName: "limit",
|
|
3460
3585
|
description: "The number of records per page, from 1 to 100",
|
|
3461
3586
|
valueName: "number",
|
|
3462
|
-
schema: z.coerce.number()
|
|
3587
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
3463
3588
|
},
|
|
3464
3589
|
{
|
|
3465
3590
|
name: "sort-by",
|
|
3466
3591
|
queryName: "sortBy",
|
|
3467
3592
|
description: "The field to sort by",
|
|
3468
3593
|
valueName: "value",
|
|
3469
|
-
schema:
|
|
3594
|
+
schema: toEnum([
|
|
3470
3595
|
"name",
|
|
3471
3596
|
"sku",
|
|
3472
3597
|
"type",
|
|
@@ -3478,14 +3603,14 @@ const surfaceCommands = [
|
|
|
3478
3603
|
queryName: "sortOrder",
|
|
3479
3604
|
description: "The sort direction",
|
|
3480
3605
|
valueName: "value",
|
|
3481
|
-
schema:
|
|
3606
|
+
schema: toEnum(["ASC", "DESC"])
|
|
3482
3607
|
},
|
|
3483
3608
|
{
|
|
3484
3609
|
name: "archived",
|
|
3485
3610
|
queryName: "archived",
|
|
3486
3611
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
3487
3612
|
valueName: "value",
|
|
3488
|
-
schema:
|
|
3613
|
+
schema: toEnum([
|
|
3489
3614
|
"all",
|
|
3490
3615
|
"false",
|
|
3491
3616
|
"true"
|
|
@@ -3546,7 +3671,7 @@ const surfaceCommands = [
|
|
|
3546
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",
|
|
3547
3672
|
valueName: "value",
|
|
3548
3673
|
required: true,
|
|
3549
|
-
schema:
|
|
3674
|
+
schema: toEnum([
|
|
3550
3675
|
"BULK",
|
|
3551
3676
|
"DEVICE",
|
|
3552
3677
|
"SERVICE"
|
|
@@ -3557,7 +3682,7 @@ const surfaceCommands = [
|
|
|
3557
3682
|
jsonPath: ["unitOfMeasure"],
|
|
3558
3683
|
description: "The unit a BULK item's quantities are counted in, which SERVICE and DEVICE items ignore",
|
|
3559
3684
|
valueName: "value",
|
|
3560
|
-
schema:
|
|
3685
|
+
schema: toEnum([
|
|
3561
3686
|
"BG",
|
|
3562
3687
|
"BO",
|
|
3563
3688
|
"BX",
|
|
@@ -3703,7 +3828,7 @@ const surfaceCommands = [
|
|
|
3703
3828
|
description: "The type to convert the item to, when the item's assets and inventory history allow the conversion",
|
|
3704
3829
|
valueName: "value",
|
|
3705
3830
|
nullable: true,
|
|
3706
|
-
schema:
|
|
3831
|
+
schema: toEnum([
|
|
3707
3832
|
"BULK",
|
|
3708
3833
|
"DEVICE",
|
|
3709
3834
|
"SERVICE"
|
|
@@ -3751,7 +3876,7 @@ const surfaceCommands = [
|
|
|
3751
3876
|
jsonPath: ["depreciationModel"],
|
|
3752
3877
|
description: "The method one unit is depreciated by, or null to clear it",
|
|
3753
3878
|
valueName: "value",
|
|
3754
|
-
schema:
|
|
3879
|
+
schema: toEnum([
|
|
3755
3880
|
"DOUBLE_DECLINING",
|
|
3756
3881
|
"STRAIGHT_LINE",
|
|
3757
3882
|
"SUM_YEAR",
|
|
@@ -3836,7 +3961,7 @@ const surfaceCommands = [
|
|
|
3836
3961
|
description: "The number of months one unit is depreciated over, or null to clear it",
|
|
3837
3962
|
valueName: "number",
|
|
3838
3963
|
nullable: true,
|
|
3839
|
-
schema: z.coerce.number()
|
|
3964
|
+
schema: z.coerce.number().int()
|
|
3840
3965
|
}
|
|
3841
3966
|
]
|
|
3842
3967
|
})]
|
|
@@ -3867,7 +3992,7 @@ const surfaceCommands = [
|
|
|
3867
3992
|
description: "The kind of value the field holds",
|
|
3868
3993
|
valueName: "value",
|
|
3869
3994
|
required: true,
|
|
3870
|
-
schema:
|
|
3995
|
+
schema: toEnum([
|
|
3871
3996
|
"BOOLEAN",
|
|
3872
3997
|
"DATE",
|
|
3873
3998
|
"DATE_TIME",
|
|
@@ -3893,7 +4018,7 @@ const surfaceCommands = [
|
|
|
3893
4018
|
valueName: "number",
|
|
3894
4019
|
required: true,
|
|
3895
4020
|
nullable: true,
|
|
3896
|
-
schema: z.coerce.number()
|
|
4021
|
+
schema: z.coerce.number().int()
|
|
3897
4022
|
},
|
|
3898
4023
|
{
|
|
3899
4024
|
name: "section",
|
|
@@ -3902,7 +4027,7 @@ const surfaceCommands = [
|
|
|
3902
4027
|
valueName: "number",
|
|
3903
4028
|
required: true,
|
|
3904
4029
|
nullable: true,
|
|
3905
|
-
schema: z.coerce.number()
|
|
4030
|
+
schema: z.coerce.number().int()
|
|
3906
4031
|
}
|
|
3907
4032
|
]
|
|
3908
4033
|
}),
|
|
@@ -3973,21 +4098,21 @@ const surfaceCommands = [
|
|
|
3973
4098
|
queryName: "page",
|
|
3974
4099
|
description: "The page to return, starting at 1",
|
|
3975
4100
|
valueName: "number",
|
|
3976
|
-
schema: z.coerce.number()
|
|
4101
|
+
schema: z.coerce.number().int()
|
|
3977
4102
|
},
|
|
3978
4103
|
{
|
|
3979
4104
|
name: "limit",
|
|
3980
4105
|
queryName: "limit",
|
|
3981
4106
|
description: "The number of records per page, from 1 to 100",
|
|
3982
4107
|
valueName: "number",
|
|
3983
|
-
schema: z.coerce.number()
|
|
4108
|
+
schema: z.coerce.number().int().min(1).max(100)
|
|
3984
4109
|
},
|
|
3985
4110
|
{
|
|
3986
4111
|
name: "sort-by",
|
|
3987
4112
|
queryName: "sortBy",
|
|
3988
4113
|
description: "The field to sort by",
|
|
3989
4114
|
valueName: "value",
|
|
3990
|
-
schema:
|
|
4115
|
+
schema: toEnum([
|
|
3991
4116
|
"name",
|
|
3992
4117
|
"company",
|
|
3993
4118
|
"assetCount"
|
|
@@ -3998,14 +4123,14 @@ const surfaceCommands = [
|
|
|
3998
4123
|
queryName: "sortOrder",
|
|
3999
4124
|
description: "The sort direction",
|
|
4000
4125
|
valueName: "value",
|
|
4001
|
-
schema:
|
|
4126
|
+
schema: toEnum(["ASC", "DESC"])
|
|
4002
4127
|
},
|
|
4003
4128
|
{
|
|
4004
4129
|
name: "archived",
|
|
4005
4130
|
queryName: "archived",
|
|
4006
4131
|
description: "Whether to return unarchived records, archived records, or all of them",
|
|
4007
4132
|
valueName: "value",
|
|
4008
|
-
schema:
|
|
4133
|
+
schema: toEnum([
|
|
4009
4134
|
"all",
|
|
4010
4135
|
"false",
|
|
4011
4136
|
"true"
|
|
@@ -4016,7 +4141,7 @@ const surfaceCommands = [
|
|
|
4016
4141
|
queryName: "isTransient",
|
|
4017
4142
|
description: "Whether to return permanent locations (false), transient locations (true), or both (all)",
|
|
4018
4143
|
valueName: "value",
|
|
4019
|
-
schema:
|
|
4144
|
+
schema: toEnum([
|
|
4020
4145
|
"all",
|
|
4021
4146
|
"false",
|
|
4022
4147
|
"true"
|
|
@@ -4181,7 +4306,7 @@ const surfaceCommands = [
|
|
|
4181
4306
|
jsonPath: ["type"],
|
|
4182
4307
|
description: "SITE for a site, or ZONE for a zone within a site",
|
|
4183
4308
|
valueName: "value",
|
|
4184
|
-
schema:
|
|
4309
|
+
schema: toEnum([
|
|
4185
4310
|
"SITE",
|
|
4186
4311
|
"UNKNOWN",
|
|
4187
4312
|
"ZONE"
|
|
@@ -4337,7 +4462,7 @@ const surfaceCommands = [
|
|
|
4337
4462
|
jsonPath: ["type"],
|
|
4338
4463
|
description: "SITE for a site, or ZONE for a zone within a site",
|
|
4339
4464
|
valueName: "value",
|
|
4340
|
-
schema:
|
|
4465
|
+
schema: toEnum([
|
|
4341
4466
|
"SITE",
|
|
4342
4467
|
"UNKNOWN",
|
|
4343
4468
|
"ZONE"
|
|
@@ -4367,7 +4492,7 @@ const surfaceCommands = [
|
|
|
4367
4492
|
queryName: "archived",
|
|
4368
4493
|
description: "Whether to return unarchived zones, archived zones, or all of them",
|
|
4369
4494
|
valueName: "value",
|
|
4370
|
-
schema:
|
|
4495
|
+
schema: toEnum([
|
|
4371
4496
|
"all",
|
|
4372
4497
|
"false",
|
|
4373
4498
|
"true"
|
|
@@ -4378,6 +4503,23 @@ const surfaceCommands = [
|
|
|
4378
4503
|
}
|
|
4379
4504
|
]
|
|
4380
4505
|
},
|
|
4506
|
+
{
|
|
4507
|
+
name: "token",
|
|
4508
|
+
summary: "Token commands",
|
|
4509
|
+
arguments: [],
|
|
4510
|
+
flags: [],
|
|
4511
|
+
examples: [],
|
|
4512
|
+
subcommands: [defineOperation({
|
|
4513
|
+
name: "get",
|
|
4514
|
+
summary: "Report the credential this CLI is calling with",
|
|
4515
|
+
example: "hardfin token get",
|
|
4516
|
+
method: "GET",
|
|
4517
|
+
path: "/token",
|
|
4518
|
+
pathParameters: [],
|
|
4519
|
+
queryFlags: [],
|
|
4520
|
+
bodyFlags: []
|
|
4521
|
+
})]
|
|
4522
|
+
},
|
|
4381
4523
|
{
|
|
4382
4524
|
name: "url-link",
|
|
4383
4525
|
summary: "URL link commands",
|
|
@@ -4445,32 +4587,6 @@ const surfaceCommands = [
|
|
|
4445
4587
|
}
|
|
4446
4588
|
];
|
|
4447
4589
|
//#endregion
|
|
4448
|
-
//#region src/auth/jwt.ts
|
|
4449
|
-
/**
|
|
4450
|
-
* toClaims reads an access token's payload for display. Nothing here verifies the
|
|
4451
|
-
* signature, because the API is what decides whether a token is good.
|
|
4452
|
-
*/
|
|
4453
|
-
function toClaims(token) {
|
|
4454
|
-
const payload = token.split(".")[1];
|
|
4455
|
-
if (!payload) return {};
|
|
4456
|
-
try {
|
|
4457
|
-
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
4458
|
-
return {
|
|
4459
|
-
expiresAt: typeof decoded["exp"] === "number" ? decoded["exp"] * 1e3 : void 0,
|
|
4460
|
-
issuedAt: typeof decoded["iat"] === "number" ? decoded["iat"] * 1e3 : void 0,
|
|
4461
|
-
scopes: toScopes(decoded),
|
|
4462
|
-
subject: typeof decoded["sub"] === "string" ? decoded["sub"] : void 0
|
|
4463
|
-
};
|
|
4464
|
-
} catch {
|
|
4465
|
-
return {};
|
|
4466
|
-
}
|
|
4467
|
-
}
|
|
4468
|
-
/** toScopes reads scp, which is where Hardfin puts an access token's scopes. */
|
|
4469
|
-
function toScopes(decoded) {
|
|
4470
|
-
if (Array.isArray(decoded["scp"])) return decoded["scp"].map(String);
|
|
4471
|
-
return typeof decoded["scope"] === "string" ? decoded["scope"].split(" ") : void 0;
|
|
4472
|
-
}
|
|
4473
|
-
//#endregion
|
|
4474
4590
|
//#region src/system/host.ts
|
|
4475
4591
|
const BYTES_PER_GB = 1024 ** 3;
|
|
4476
4592
|
/** toDistribution reads the name a Linux distribution gives itself. */
|
|
@@ -4514,8 +4630,8 @@ function toFile(path) {
|
|
|
4514
4630
|
//#endregion
|
|
4515
4631
|
//#region src/command/status.ts
|
|
4516
4632
|
/**
|
|
4517
|
-
*
|
|
4518
|
-
*
|
|
4633
|
+
* What the CLI falls back to for an older sign in, stored before the server began reporting
|
|
4634
|
+
* when a refresh token expires. Hardfin's own rule, and an estimate rather than a fact.
|
|
4519
4635
|
*/
|
|
4520
4636
|
const REFRESH_SLIDING_DAYS = 90;
|
|
4521
4637
|
const statusCommand = defineCommand({
|
|
@@ -4619,8 +4735,8 @@ function toCredentialReport(apiKey, stored) {
|
|
|
4619
4735
|
fingerprint: toFingerprint(stored.refreshToken),
|
|
4620
4736
|
signedInAt: stored.signedInAt ?? null,
|
|
4621
4737
|
renewedAt: stored.renewedAt ?? null,
|
|
4622
|
-
refreshExpiresAt: toRefreshExpiry(stored.renewedAt),
|
|
4623
|
-
refreshExpiryIsEstimated:
|
|
4738
|
+
refreshExpiresAt: stored.expiresAt ?? toRefreshExpiry(stored.renewedAt),
|
|
4739
|
+
refreshExpiryIsEstimated: stored.expiresAt === void 0
|
|
4624
4740
|
};
|
|
4625
4741
|
}
|
|
4626
4742
|
function toServerReport(metadata) {
|
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",
|