@atlasauth/pca-doctor 0.0.0-stage → 0.4.2
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/LICENSE +21 -0
- package/README.md +46 -2
- package/dist/caveats.d.ts +24 -0
- package/dist/caveats.js +136 -0
- package/dist/caveats.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +131 -0
- package/dist/cli.js.map +1 -0
- package/dist/clock.d.ts +28 -0
- package/dist/clock.js +77 -0
- package/dist/clock.js.map +1 -0
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.js +104 -0
- package/dist/doctor.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts +21 -0
- package/dist/proxy.js +89 -0
- package/dist/proxy.js.map +1 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.js +112 -0
- package/dist/store.js.map +1 -0
- package/dist/suites.d.ts +5 -0
- package/dist/suites.js +62 -0
- package/dist/suites.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +51 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Atlas Authorization
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @atlasauth/pca-doctor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Checks the assumptions a Proof-Carrying Authority deployment depends on, against the live deployment, and prints pass /
|
|
4
|
+
warn / fail with a remediation for each check. The protocol can be correct while the deployment around it is not: a
|
|
5
|
+
skewed clock, a replay store that is not atomic or not durable, a proxy configuration that trusts forged forwarding
|
|
6
|
+
headers, or a suite / post-quantum pin policy that accepts more than intended.
|
|
7
|
+
|
|
8
|
+
> Status: **0.4.2, unaudited.**
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm i -D @atlasauth/pca-doctor
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Run it
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx pca-doctor --url https://api.example.com
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Check groups: `clock`, `store`, `proxy`, `suites`, `caveats` (select with `--only a,b` or `--skip a,b`).
|
|
23
|
+
|
|
24
|
+
- **clock**: skew against a reference clock (`--reference`, default an HTTPS endpoint; `--max-skew-ms`, default 60000).
|
|
25
|
+
- **store**: atomicity and durability of your replay store, using an adapter module in the
|
|
26
|
+
[`@atlasauth/pca-store-conformance`](https://www.npmjs.com/package/@atlasauth/pca-store-conformance) interface
|
|
27
|
+
(`--store ./my-adapter.js`, `--store-option k=v`).
|
|
28
|
+
- **proxy**: the trusted-proxy settings against the real number of hops; it sends forged forwarding headers to see whether
|
|
29
|
+
the deployment believes them (`--no-probe` to skip sending them).
|
|
30
|
+
- **suites**: the allowed signature suites and post-quantum key pins.
|
|
31
|
+
- **caveats**: that the installed `@atlasauth/pca` enforces root caveats (`--pca-dir` points at the deployed application).
|
|
32
|
+
|
|
33
|
+
Configuration can also come from a JSON or JS file (`--config`) or the environment (`PCA_DOCTOR_URL`,
|
|
34
|
+
`TRUSTED_PROXY_COUNT`, `PCA_ALLOWED_SUITES`, `PCA_PQ_PINS`, ...); flags override both. `--strict` makes warnings fail,
|
|
35
|
+
`--json` prints machine-readable output. Run `npx pca-doctor --help` for every option.
|
|
36
|
+
|
|
37
|
+
## In CI
|
|
38
|
+
|
|
39
|
+
Run it against a staging deployment after each deploy, with `--strict --json`, and fail the pipeline on a non-zero exit.
|
|
40
|
+
|
|
41
|
+
## Programmatic use
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { runDoctor } from '@atlasauth/pca-doctor';
|
|
45
|
+
|
|
46
|
+
const results = await runDoctor({ url: 'https://api.example.com' });
|
|
47
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CheckResult, DoctorConfig } from './types';
|
|
2
|
+
/** The parts of @atlasauth/pca the probe uses (typed loosely: any version may be loaded). */
|
|
3
|
+
export type PcaLike = Record<string, any>;
|
|
4
|
+
export interface LoadedPca {
|
|
5
|
+
module: PcaLike;
|
|
6
|
+
version: string;
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
/** Load @atlasauth/pca as resolved from `dir` (the deployed application), or the doctor's own copy. */
|
|
10
|
+
export declare function loadPca(dir?: string): LoadedPca;
|
|
11
|
+
/** -1 / 0 / 1 for a < b / = / > over major.minor.patch (pre-release tags ignored); NaN parts compare as -1. */
|
|
12
|
+
export declare function compareVersion(a: string, b: string): number;
|
|
13
|
+
export declare const ROOT_CAVEAT_MIN_VERSION = "0.4.1";
|
|
14
|
+
export declare const CAVEAT_REMEDIATION = "Upgrade @atlasauth/pca to >= 0.4.1 in the deployed application (root caveats outside the envelope are enforced from 0.4.1), redeploy, and re-run pca-doctor with --pca-dir pointing at the application.";
|
|
15
|
+
/** Decide a payment of `amount` USD under a root grant carrying `rootCaveat`; returns the decision. */
|
|
16
|
+
export declare function probeDecision(m: PcaLike, rootCaveat: Record<string, unknown>, amount: number, now?: number): {
|
|
17
|
+
allowed: boolean;
|
|
18
|
+
reasons: string[];
|
|
19
|
+
};
|
|
20
|
+
export declare function checkRootCaveats(cfg: DoctorConfig, deps?: {
|
|
21
|
+
loadPca?: (dir?: string) => LoadedPca;
|
|
22
|
+
}): CheckResult;
|
|
23
|
+
export declare const DEFAULT_REQUIRED: string[];
|
|
24
|
+
export declare function checkEnforcement(cfg: DoctorConfig): CheckResult;
|
package/dist/caveats.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_REQUIRED = exports.CAVEAT_REMEDIATION = exports.ROOT_CAVEAT_MIN_VERSION = void 0;
|
|
4
|
+
exports.loadPca = loadPca;
|
|
5
|
+
exports.compareVersion = compareVersion;
|
|
6
|
+
exports.probeDecision = probeDecision;
|
|
7
|
+
exports.checkRootCaveats = checkRootCaveats;
|
|
8
|
+
exports.checkEnforcement = checkEnforcement;
|
|
9
|
+
/**
|
|
10
|
+
* Root-caveat enforcement (0.4.1 security fix). There is NO switch that turns it off in 0.4.1: decide() / verifyPCActn /
|
|
11
|
+
* requirePCA always evaluate the caveats on the root grant. What can leave a deployment without it is an OLDER library
|
|
12
|
+
* (< 0.4.1, where root caveats outside the envelope were signed but ignored) or a verifier that skips enforcement. So:
|
|
13
|
+
* - the installed @atlasauth/pca version (the deployment's own, via verifier.pcaDir) must be >= 0.4.1;
|
|
14
|
+
* - a functional probe through THAT library: a root max_spend of 100 must deny 150 and allow 50, and an unknown root
|
|
15
|
+
* caveat type must deny (fail closed);
|
|
16
|
+
* - requirePCA must not run with insecureAllowUnenforced, and a custom `require` list should keep the defaults.
|
|
17
|
+
*/
|
|
18
|
+
const node_fs_1 = require("node:fs");
|
|
19
|
+
const node_path_1 = require("node:path");
|
|
20
|
+
/** Load @atlasauth/pca as resolved from `dir` (the deployed application), or the doctor's own copy. */
|
|
21
|
+
function loadPca(dir) {
|
|
22
|
+
// With `paths`, require.resolve falls back to the GLOBAL folders (NODE_PATH, which pnpm's bin shims set to the store's
|
|
23
|
+
// hoisted node_modules, and ~/.node_modules) when nothing under `dir` has the package, and would silently probe some
|
|
24
|
+
// other copy (e.g. the doctor's own) instead of the application's. So `dir` must itself install it: the nearest
|
|
25
|
+
// ancestor holding node_modules/@atlasauth/pca is the resolution base, and none is an error.
|
|
26
|
+
let base;
|
|
27
|
+
if (dir) {
|
|
28
|
+
for (let d = (0, node_path_1.resolve)(dir);; d = (0, node_path_1.dirname)(d)) {
|
|
29
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(d, 'node_modules', '@atlasauth', 'pca', 'package.json'))) {
|
|
30
|
+
base = d;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
if ((0, node_path_1.dirname)(d) === d)
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
if (base === undefined)
|
|
37
|
+
throw new Error(`@atlasauth/pca is not installed in ${dir} or any parent node_modules`);
|
|
38
|
+
}
|
|
39
|
+
const opts = base ? { paths: [base] } : undefined;
|
|
40
|
+
const pkgPath = require.resolve('@atlasauth/pca/package.json', opts);
|
|
41
|
+
const main = require.resolve('@atlasauth/pca', opts);
|
|
42
|
+
return { module: require(main), version: String(require(pkgPath).version), path: (0, node_path_1.dirname)(pkgPath) };
|
|
43
|
+
}
|
|
44
|
+
/** -1 / 0 / 1 for a < b / = / > over major.minor.patch (pre-release tags ignored); NaN parts compare as -1. */
|
|
45
|
+
function compareVersion(a, b) {
|
|
46
|
+
const pa = a.split(/[.+-]/).slice(0, 3).map(Number);
|
|
47
|
+
const pb = b.split(/[.+-]/).slice(0, 3).map(Number);
|
|
48
|
+
for (let i = 0; i < 3; i++) {
|
|
49
|
+
const x = Number.isFinite(pa[i]) ? pa[i] : -1;
|
|
50
|
+
const y = Number.isFinite(pb[i]) ? pb[i] : -1;
|
|
51
|
+
if (x !== y)
|
|
52
|
+
return x < y ? -1 : 1;
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
exports.ROOT_CAVEAT_MIN_VERSION = '0.4.1';
|
|
57
|
+
exports.CAVEAT_REMEDIATION = 'Upgrade @atlasauth/pca to >= 0.4.1 in the deployed application (root caveats outside the envelope are enforced from 0.4.1), redeploy, and re-run pca-doctor with --pca-dir pointing at the application.';
|
|
58
|
+
/** Decide a payment of `amount` USD under a root grant carrying `rootCaveat`; returns the decision. */
|
|
59
|
+
function probeDecision(m, rootCaveat, amount, now = Date.now()) {
|
|
60
|
+
const principal = m.generateKeyPair();
|
|
61
|
+
const pPub = m.encodeKey(principal.publicKey);
|
|
62
|
+
const { grant: g0 } = m.mintGrant({
|
|
63
|
+
principalSecret: principal.secretKey,
|
|
64
|
+
principalPublic: pPub,
|
|
65
|
+
holder: pPub,
|
|
66
|
+
goal: 'pca-doctor root caveat probe',
|
|
67
|
+
envelope: { predicates: [{ verb: 'pay', resource: 'invoice:*' }], caveats: [{ type: 'expires', at: now + 3_600_000 }], agent_binding: {}, risk_policy: m.DEFAULT_RISK_POLICY },
|
|
68
|
+
});
|
|
69
|
+
const grant = m.mintRoot({ principalSecret: principal.secretKey, principalPublic: pPub, holder: pPub, caveats: [...g0.caveats, rootCaveat] });
|
|
70
|
+
const agent = m.generateKeyPair();
|
|
71
|
+
const task = m.delegate(grant, m.encodeKey(agent.publicKey), [], principal.secretKey);
|
|
72
|
+
const d = m.decide({
|
|
73
|
+
grant,
|
|
74
|
+
chain: [grant, task],
|
|
75
|
+
action: { action: { verb: 'pay', resource: 'invoice:42', params: { amount, currency: 'usd' }, reversibility_class: 'reversible' } },
|
|
76
|
+
risk: { reversibility: 1, blastRadius: 0, confidence: 1, semanticDistance: 0, taint: 0 },
|
|
77
|
+
budget: { B: 1e9, tau: now, asOf: now },
|
|
78
|
+
now,
|
|
79
|
+
});
|
|
80
|
+
return { allowed: d.releaseGuardianShare === true, reasons: Array.isArray(d.reasons) ? d.reasons.map(String) : [] };
|
|
81
|
+
}
|
|
82
|
+
function checkRootCaveats(cfg, deps = {}) {
|
|
83
|
+
const id = 'caveats.root';
|
|
84
|
+
let pca;
|
|
85
|
+
try {
|
|
86
|
+
pca = (deps.loadPca ?? loadPca)(cfg.verifier?.pcaDir);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
return { id, status: 'fail', detail: `cannot load @atlasauth/pca${cfg.verifier?.pcaDir ? ` from ${cfg.verifier.pcaDir}` : ''}: ${e.message}`, remediation: exports.CAVEAT_REMEDIATION };
|
|
90
|
+
}
|
|
91
|
+
const where = `@atlasauth/pca ${pca.version} at ${pca.path}`;
|
|
92
|
+
if (compareVersion(pca.version, exports.ROOT_CAVEAT_MIN_VERSION) < 0) {
|
|
93
|
+
return { id, status: 'fail', detail: `${where}: root caveats outside the envelope are NOT enforced before ${exports.ROOT_CAVEAT_MIN_VERSION} (legacy behaviour)`, remediation: exports.CAVEAT_REMEDIATION };
|
|
94
|
+
}
|
|
95
|
+
let over, under, unknown;
|
|
96
|
+
try {
|
|
97
|
+
over = probeDecision(pca.module, { type: 'max_spend', usd: 100 }, 150);
|
|
98
|
+
under = probeDecision(pca.module, { type: 'max_spend', usd: 100 }, 50);
|
|
99
|
+
unknown = probeDecision(pca.module, { type: 'pca-doctor-unknown-caveat' }, 50);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
return { id, status: 'fail', detail: `${where}: the enforcement probe could not run: ${e.message}`, remediation: exports.CAVEAT_REMEDIATION };
|
|
103
|
+
}
|
|
104
|
+
const observed = { version: pca.version, over, under, unknown };
|
|
105
|
+
if (over.allowed || unknown.allowed) {
|
|
106
|
+
return {
|
|
107
|
+
id,
|
|
108
|
+
status: 'fail',
|
|
109
|
+
detail: `${where}: a root caveat was NOT enforced (${over.allowed ? 'max_spend 100 allowed a 150 payment' : 'an unknown root caveat type was allowed'})`,
|
|
110
|
+
remediation: exports.CAVEAT_REMEDIATION,
|
|
111
|
+
observed,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (!under.allowed) {
|
|
115
|
+
return { id, status: 'warn', detail: `${where}: inconclusive: the control payment within the root max_spend was denied too (${under.reasons.join('; ')})`, observed };
|
|
116
|
+
}
|
|
117
|
+
return { id, status: 'pass', detail: `${where}: root caveats enforced (max_spend 100 denies 150, allows 50; an unknown root caveat type denies)`, observed };
|
|
118
|
+
}
|
|
119
|
+
exports.DEFAULT_REQUIRED = ['counter', 'revocation', 'plan_root_authorized', 'audience', 'validity', 'grant_ref_bound'];
|
|
120
|
+
function checkEnforcement(cfg) {
|
|
121
|
+
const id = 'verifier.enforcement';
|
|
122
|
+
const v = cfg.verifier;
|
|
123
|
+
if (!v)
|
|
124
|
+
return { id, status: 'skip', detail: 'no verifier configuration given (config verifier.*)' };
|
|
125
|
+
if (v.insecureAllowUnenforced === true) {
|
|
126
|
+
return { id, status: 'fail', detail: 'requirePCA runs with insecureAllowUnenforced: required checks that are not enforced do not deny', remediation: 'Remove insecureAllowUnenforced and supply the stores the required checks need (counter / revocation / budget).' };
|
|
127
|
+
}
|
|
128
|
+
if (v.require !== undefined) {
|
|
129
|
+
const missing = exports.DEFAULT_REQUIRED.filter((c) => !v.require.includes(c));
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
return { id, status: 'warn', detail: `the require list drops default check(s): ${missing.join(', ')}`, remediation: `Keep every default required check (${exports.DEFAULT_REQUIRED.join(', ')}) and add to it.` };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { id, status: 'pass', detail: v.require ? `require = ${v.require.join(', ')}` : 'default required checks; insecureAllowUnenforced off' };
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=caveats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"caveats.js","sourceRoot":"","sources":["../src/caveats.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** pca-doctor: check a live PCA deployment's assumptions. Exit 1 on any fail (and on warn with --strict), 2 on usage errors. */
|
|
3
|
+
import { type CheckGroup } from './doctor';
|
|
4
|
+
import type { DoctorConfig } from './types';
|
|
5
|
+
export declare const USAGE: string;
|
|
6
|
+
export interface DoctorArgs {
|
|
7
|
+
config?: string;
|
|
8
|
+
flags: DoctorConfig;
|
|
9
|
+
only: CheckGroup[];
|
|
10
|
+
json: boolean;
|
|
11
|
+
strict: boolean;
|
|
12
|
+
help: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function parseDoctorArgs(argv: readonly string[]): DoctorArgs;
|
|
15
|
+
export declare function main(argv: readonly string[], env?: Record<string, string | undefined>, print?: (s: string) => void, printErr?: (s: string) => void): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.USAGE = void 0;
|
|
5
|
+
exports.parseDoctorArgs = parseDoctorArgs;
|
|
6
|
+
exports.main = main;
|
|
7
|
+
/** pca-doctor: check a live PCA deployment's assumptions. Exit 1 on any fail (and on warn with --strict), 2 on usage errors. */
|
|
8
|
+
const doctor_1 = require("./doctor");
|
|
9
|
+
exports.USAGE = `pca-doctor [--url <deployment>] [options]
|
|
10
|
+
|
|
11
|
+
--url URL the live deployment (base URL of the API that verifies PCActns)
|
|
12
|
+
--config FILE JSON or JS config (see the pca-doctor guide); env and flags override it
|
|
13
|
+
--store MODULE replay-store adapter module (store conformance kit interface) for the atomicity / durability checks
|
|
14
|
+
--store-option k=v passed to the adapter factory (repeatable)
|
|
15
|
+
--reference URL|local clock reference (default https://www.google.com/generate_204)
|
|
16
|
+
--max-skew-ms N clock fail threshold (default 60000)
|
|
17
|
+
--trusted-proxy-count N, --proxy-header H, --proxy-hops N the deployment's proxy settings and the real hop count
|
|
18
|
+
--pca-dir DIR the deployed application's directory (its installed @atlasauth/pca is probed)
|
|
19
|
+
--only a,b | --skip a,b check groups: ${doctor_1.CHECK_GROUPS.join(', ')}
|
|
20
|
+
--no-probe do not send forged forwarding headers to the deployment
|
|
21
|
+
--strict warnings fail too
|
|
22
|
+
--json machine-readable output
|
|
23
|
+
|
|
24
|
+
Env: PCA_DOCTOR_URL, PCA_DOCTOR_REFERENCE_URL, PCA_DOCTOR_STORE, TRUSTED_PROXY_COUNT, TRUSTED_PROXY_HEADER, PCA_TRUSTED_PROXY_HOPS,
|
|
25
|
+
PCA_ALLOWED_SUITES (comma list), PCA_PQ_PINS (JSON), PCA_ALLOW_UNBOUND_PQ.`;
|
|
26
|
+
const groups = (flag, v) => {
|
|
27
|
+
const g = (v ?? '').split(',').filter(Boolean);
|
|
28
|
+
const bad = g.filter((x) => !doctor_1.CHECK_GROUPS.includes(x));
|
|
29
|
+
if (bad.length || g.length === 0)
|
|
30
|
+
throw new Error(`${flag}: unknown check group(s) ${bad.join(', ') || '(none given)'}`);
|
|
31
|
+
return g;
|
|
32
|
+
};
|
|
33
|
+
const num = (flag, v) => {
|
|
34
|
+
const n = Number(v);
|
|
35
|
+
if (v === undefined || v === '' || !Number.isFinite(n))
|
|
36
|
+
throw new Error(`${flag} needs a number`);
|
|
37
|
+
return n;
|
|
38
|
+
};
|
|
39
|
+
function parseDoctorArgs(argv) {
|
|
40
|
+
const a = { flags: {}, only: [...doctor_1.CHECK_GROUPS], json: false, strict: false, help: false };
|
|
41
|
+
const f = a.flags;
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const k = argv[i];
|
|
44
|
+
const next = () => argv[++i];
|
|
45
|
+
switch (k) {
|
|
46
|
+
case '--url':
|
|
47
|
+
f.url = next();
|
|
48
|
+
break;
|
|
49
|
+
case '--config':
|
|
50
|
+
a.config = next();
|
|
51
|
+
break;
|
|
52
|
+
case '--store':
|
|
53
|
+
f.store = { ...f.store, adapter: next() };
|
|
54
|
+
break;
|
|
55
|
+
case '--store-option': {
|
|
56
|
+
const kv = next() ?? '';
|
|
57
|
+
const eq = kv.indexOf('=');
|
|
58
|
+
if (eq < 1)
|
|
59
|
+
throw new Error('--store-option needs k=v');
|
|
60
|
+
f.store = { ...f.store, options: { ...f.store?.options, [kv.slice(0, eq)]: kv.slice(eq + 1) } };
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case '--reference':
|
|
64
|
+
f.clock = { ...f.clock, referenceUrl: next() };
|
|
65
|
+
break;
|
|
66
|
+
case '--max-skew-ms':
|
|
67
|
+
f.clock = { ...f.clock, maxSkewMs: num(k, next()) };
|
|
68
|
+
break;
|
|
69
|
+
case '--trusted-proxy-count':
|
|
70
|
+
f.proxy = { ...f.proxy, trustedProxyCount: num(k, next()) };
|
|
71
|
+
break;
|
|
72
|
+
case '--proxy-header':
|
|
73
|
+
f.proxy = { ...f.proxy, header: next() };
|
|
74
|
+
break;
|
|
75
|
+
case '--proxy-hops':
|
|
76
|
+
f.proxy = { ...f.proxy, hops: num(k, next()) };
|
|
77
|
+
break;
|
|
78
|
+
case '--no-probe':
|
|
79
|
+
f.proxy = { ...f.proxy, probe: false };
|
|
80
|
+
break;
|
|
81
|
+
case '--pca-dir':
|
|
82
|
+
f.verifier = { ...f.verifier, pcaDir: next() };
|
|
83
|
+
break;
|
|
84
|
+
case '--only':
|
|
85
|
+
a.only = groups(k, next());
|
|
86
|
+
break;
|
|
87
|
+
case '--skip': {
|
|
88
|
+
const s = groups(k, next());
|
|
89
|
+
a.only = a.only.filter((g) => !s.includes(g));
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case '--strict':
|
|
93
|
+
a.strict = true;
|
|
94
|
+
break;
|
|
95
|
+
case '--json':
|
|
96
|
+
a.json = true;
|
|
97
|
+
break;
|
|
98
|
+
case '-h':
|
|
99
|
+
case '--help':
|
|
100
|
+
a.help = true;
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
throw new Error(`unknown argument ${k}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return a;
|
|
107
|
+
}
|
|
108
|
+
async function main(argv, env = process.env, print = console.log, printErr = console.error) {
|
|
109
|
+
let a;
|
|
110
|
+
let cfg;
|
|
111
|
+
try {
|
|
112
|
+
a = parseDoctorArgs(argv);
|
|
113
|
+
if (a.help) {
|
|
114
|
+
print(exports.USAGE);
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
cfg = (0, doctor_1.mergeConfig)(a.config ? await (0, doctor_1.loadConfigFile)(a.config) : {}, (0, doctor_1.configFromEnv)(env), a.flags);
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
printErr(`${e.message}\n\n${exports.USAGE}`);
|
|
121
|
+
return 2;
|
|
122
|
+
}
|
|
123
|
+
const results = await (0, doctor_1.runDoctor)(cfg, a.only);
|
|
124
|
+
const verdict = (0, doctor_1.verdictOf)(results, a.strict);
|
|
125
|
+
print(a.json ? JSON.stringify({ target: cfg.url ?? null, verdict, results }, null, 2) : (0, doctor_1.formatResults)(results, cfg.url, a.strict));
|
|
126
|
+
return verdict === 'fail' ? 1 : 0;
|
|
127
|
+
}
|
|
128
|
+
// Stryker disable next-line all: process entry glue
|
|
129
|
+
if (require.main === module)
|
|
130
|
+
void main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
|
|
131
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CheckResult, DoctorConfig } from './types';
|
|
2
|
+
export type FetchLike = (url: string, init?: {
|
|
3
|
+
method?: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
redirect?: 'manual';
|
|
7
|
+
}) => Promise<{
|
|
8
|
+
status: number;
|
|
9
|
+
headers: {
|
|
10
|
+
get(name: string): string | null;
|
|
11
|
+
};
|
|
12
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
13
|
+
}>;
|
|
14
|
+
export declare const DEFAULT_REFERENCE_URL = "https://www.google.com/generate_204";
|
|
15
|
+
export interface DateReading {
|
|
16
|
+
/** Remote clock minus local clock (ms). */
|
|
17
|
+
offsetMs: number;
|
|
18
|
+
uncertaintyMs: number;
|
|
19
|
+
}
|
|
20
|
+
/** One reading of a server's clock relative to `now()` from its Date header. */
|
|
21
|
+
export declare function readDate(url: string, fetchImpl: FetchLike, now: () => number, timeoutMs: number): Promise<DateReading | {
|
|
22
|
+
error: string;
|
|
23
|
+
}>;
|
|
24
|
+
export declare const CLOCK_REMEDIATION = "Synchronise every verifier, issuer and replay-store host with NTP (chrony or systemd-timesyncd) and alert on drift. PCA tolerates at most 60 s between any two clocks (PCACTN_MAX_SKEW_MS); replay retention is exp + skew, so a clock outside that bound can free a nonce early or reject fresh actions.";
|
|
25
|
+
export declare function checkClock(cfg: DoctorConfig, deps: {
|
|
26
|
+
fetch: FetchLike;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
}): Promise<CheckResult>;
|
package/dist/clock.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLOCK_REMEDIATION = exports.DEFAULT_REFERENCE_URL = void 0;
|
|
4
|
+
exports.readDate = readDate;
|
|
5
|
+
exports.checkClock = checkClock;
|
|
6
|
+
/**
|
|
7
|
+
* Clock skew of the deployment against a reference, from HTTP `Date` headers. `Date` has one-second resolution and is
|
|
8
|
+
* truncated, so each reading carries +-0.5 s plus half the round trip; the verdict uses that uncertainty: FAIL only when
|
|
9
|
+
* the skew is beyond the limit even at the favourable end of the interval, WARN when it is above the warning fraction or
|
|
10
|
+
* could be beyond the limit.
|
|
11
|
+
*/
|
|
12
|
+
const pca_1 = require("@atlasauth/pca");
|
|
13
|
+
exports.DEFAULT_REFERENCE_URL = 'https://www.google.com/generate_204';
|
|
14
|
+
/** One reading of a server's clock relative to `now()` from its Date header. */
|
|
15
|
+
async function readDate(url, fetchImpl, now, timeoutMs) {
|
|
16
|
+
const t0 = now();
|
|
17
|
+
let res;
|
|
18
|
+
try {
|
|
19
|
+
res = await fetchImpl(url, { method: 'HEAD', redirect: 'manual', signal: AbortSignal.timeout(timeoutMs) });
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return { error: `request failed: ${e.message}` };
|
|
23
|
+
}
|
|
24
|
+
const t1 = now();
|
|
25
|
+
const date = res.headers.get('date');
|
|
26
|
+
const d = date === null ? NaN : Date.parse(date);
|
|
27
|
+
if (!Number.isFinite(d))
|
|
28
|
+
return { error: date === null ? 'no Date header' : `unparseable Date header '${date}'` };
|
|
29
|
+
return { offsetMs: d + 500 - (t0 + t1) / 2, uncertaintyMs: 500 + (t1 - t0) / 2 };
|
|
30
|
+
}
|
|
31
|
+
const s = (ms) => `${ms >= 0 ? '+' : '-'}${(Math.abs(ms) / 1000).toFixed(1)} s`;
|
|
32
|
+
exports.CLOCK_REMEDIATION = 'Synchronise every verifier, issuer and replay-store host with NTP (chrony or systemd-timesyncd) and alert on drift. PCA tolerates at most 60 s between any two clocks (PCACTN_MAX_SKEW_MS); replay retention is exp + skew, so a clock outside that bound can free a nonce early or reject fresh actions.';
|
|
33
|
+
async function checkClock(cfg, deps) {
|
|
34
|
+
const now = deps.now ?? Date.now;
|
|
35
|
+
if (!cfg.url)
|
|
36
|
+
return { id: 'clock', status: 'skip', detail: 'no deployment URL given (--url)' };
|
|
37
|
+
const max = cfg.clock?.maxSkewMs ?? pca_1.PCACTN_MAX_SKEW_MS;
|
|
38
|
+
const warnAt = max * (cfg.clock?.warnFraction ?? 0.25);
|
|
39
|
+
const timeout = cfg.clock?.timeoutMs ?? 10_000;
|
|
40
|
+
const dep = await readDate(cfg.url, deps.fetch, now, timeout);
|
|
41
|
+
if ('error' in dep) {
|
|
42
|
+
return {
|
|
43
|
+
id: 'clock',
|
|
44
|
+
status: 'warn',
|
|
45
|
+
detail: `cannot observe the deployment's clock at ${cfg.url}: ${dep.error}`,
|
|
46
|
+
remediation: `Make the deployment answer with a Date header (RFC 9110 requires one from an origin with a clock), or check its clock on the host. ${exports.CLOCK_REMEDIATION}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const refUrl = cfg.clock?.referenceUrl ?? exports.DEFAULT_REFERENCE_URL;
|
|
50
|
+
let ref = { offsetMs: 0, uncertaintyMs: 0 };
|
|
51
|
+
let refName = "this machine's clock";
|
|
52
|
+
let refNote = '';
|
|
53
|
+
if (refUrl !== 'local') {
|
|
54
|
+
const r = await readDate(refUrl, deps.fetch, now, timeout);
|
|
55
|
+
if ('error' in r)
|
|
56
|
+
refNote = ` (reference ${refUrl} unusable: ${r.error}; compared with this machine's clock instead)`;
|
|
57
|
+
else {
|
|
58
|
+
ref = r;
|
|
59
|
+
refName = refUrl;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const skew = dep.offsetMs - ref.offsetMs;
|
|
63
|
+
const unc = dep.uncertaintyMs + ref.uncertaintyMs;
|
|
64
|
+
const lower = Math.abs(skew) - unc;
|
|
65
|
+
const upper = Math.abs(skew) + unc;
|
|
66
|
+
const status = lower > max ? 'fail' : Math.abs(skew) > warnAt || upper > max ? 'warn' : 'pass';
|
|
67
|
+
const detail = `deployment clock ${s(skew)} relative to ${refName}${refNote} (+-${(unc / 1000).toFixed(1)} s: Date has 1 s resolution, plus round trips); ` +
|
|
68
|
+
`fail beyond ${max / 1000} s, warn above ${warnAt / 1000} s. The Date header is written by whatever answers the request (a CDN or proxy may set it).`;
|
|
69
|
+
return {
|
|
70
|
+
id: 'clock',
|
|
71
|
+
status,
|
|
72
|
+
detail,
|
|
73
|
+
...(status === 'pass' ? {} : { remediation: exports.CLOCK_REMEDIATION }),
|
|
74
|
+
observed: { skewMs: skew, uncertaintyMs: unc, maxSkewMs: max, warnAtMs: warnAt, reference: refName },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=clock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.js","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"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"}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type LoadedPca } from './caveats';
|
|
2
|
+
import { type FetchLike } from './clock';
|
|
3
|
+
import type { CheckResult, DoctorConfig } from './types';
|
|
4
|
+
export declare const CHECK_GROUPS: readonly ["clock", "store", "proxy", "suites", "caveats"];
|
|
5
|
+
export type CheckGroup = (typeof CHECK_GROUPS)[number];
|
|
6
|
+
export interface DoctorDeps {
|
|
7
|
+
fetch?: FetchLike;
|
|
8
|
+
now?: () => number;
|
|
9
|
+
loadPca?: (dir?: string) => LoadedPca;
|
|
10
|
+
}
|
|
11
|
+
export declare function runDoctor(cfg: DoctorConfig, only?: readonly CheckGroup[], deps?: DoctorDeps): Promise<CheckResult[]>;
|
|
12
|
+
export type Verdict = 'pass' | 'warn' | 'fail';
|
|
13
|
+
/** fail if any check failed; warn if any warned (fail under --strict); else pass. */
|
|
14
|
+
export declare function verdictOf(results: readonly CheckResult[], strict?: boolean): Verdict;
|
|
15
|
+
export declare function formatResults(results: readonly CheckResult[], target: string | undefined, strict?: boolean): string;
|
|
16
|
+
/** The deployment's own environment variable names (as the API and the guides use them). */
|
|
17
|
+
export declare function configFromEnv(env: Record<string, string | undefined>): DoctorConfig;
|
|
18
|
+
/** Two-level merge: later sources win per field inside each section. */
|
|
19
|
+
export declare function mergeConfig(...cs: DoctorConfig[]): DoctorConfig;
|
|
20
|
+
/** A JSON file, or a JS module whose default export is the config object. */
|
|
21
|
+
export declare function loadConfigFile(path: string): Promise<DoctorConfig>;
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CHECK_GROUPS = void 0;
|
|
4
|
+
exports.runDoctor = runDoctor;
|
|
5
|
+
exports.verdictOf = verdictOf;
|
|
6
|
+
exports.formatResults = formatResults;
|
|
7
|
+
exports.configFromEnv = configFromEnv;
|
|
8
|
+
exports.mergeConfig = mergeConfig;
|
|
9
|
+
exports.loadConfigFile = loadConfigFile;
|
|
10
|
+
/** Run every check, merge configuration (file < env < flags), and format the results. */
|
|
11
|
+
const node_fs_1 = require("node:fs");
|
|
12
|
+
const node_path_1 = require("node:path");
|
|
13
|
+
const node_url_1 = require("node:url");
|
|
14
|
+
const caveats_1 = require("./caveats");
|
|
15
|
+
const clock_1 = require("./clock");
|
|
16
|
+
const proxy_1 = require("./proxy");
|
|
17
|
+
const store_1 = require("./store");
|
|
18
|
+
const suites_1 = require("./suites");
|
|
19
|
+
exports.CHECK_GROUPS = ['clock', 'store', 'proxy', 'suites', 'caveats'];
|
|
20
|
+
async function runDoctor(cfg, only = exports.CHECK_GROUPS, deps = {}) {
|
|
21
|
+
const fetchImpl = deps.fetch ?? globalThis.fetch;
|
|
22
|
+
const out = [];
|
|
23
|
+
if (only.includes('clock'))
|
|
24
|
+
out.push(await (0, clock_1.checkClock)(cfg, { fetch: fetchImpl, now: deps.now }));
|
|
25
|
+
if (only.includes('store'))
|
|
26
|
+
out.push(...(await (0, store_1.checkStore)(cfg)));
|
|
27
|
+
if (only.includes('proxy'))
|
|
28
|
+
out.push((0, proxy_1.checkProxyConfig)(cfg), await (0, proxy_1.checkProxyProbe)(cfg, { fetch: fetchImpl }));
|
|
29
|
+
if (only.includes('suites'))
|
|
30
|
+
out.push((0, suites_1.checkAllowedSuites)(cfg), (0, suites_1.checkPqBinding)(cfg));
|
|
31
|
+
if (only.includes('caveats'))
|
|
32
|
+
out.push((0, caveats_1.checkRootCaveats)(cfg, { loadPca: deps.loadPca }), (0, caveats_1.checkEnforcement)(cfg));
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
/** fail if any check failed; warn if any warned (fail under --strict); else pass. */
|
|
36
|
+
function verdictOf(results, strict = false) {
|
|
37
|
+
if (results.some((r) => r.status === 'fail'))
|
|
38
|
+
return 'fail';
|
|
39
|
+
if (results.some((r) => r.status === 'warn'))
|
|
40
|
+
return strict ? 'fail' : 'warn';
|
|
41
|
+
return 'pass';
|
|
42
|
+
}
|
|
43
|
+
function formatResults(results, target, strict = false) {
|
|
44
|
+
const lines = [`pca-doctor${target ? `: ${target}` : ''}`];
|
|
45
|
+
for (const r of results) {
|
|
46
|
+
lines.push(` ${r.status.toUpperCase().padEnd(4)} ${r.id.padEnd(20)} ${r.detail}`);
|
|
47
|
+
if (r.remediation)
|
|
48
|
+
lines.push(` ${''.padEnd(4)} ${''.padEnd(20)} fix: ${r.remediation}`);
|
|
49
|
+
}
|
|
50
|
+
const n = (s) => results.filter((r) => r.status === s).length;
|
|
51
|
+
lines.push(`Summary: ${n('pass')} pass, ${n('warn')} warn, ${n('fail')} fail, ${n('skip')} skip -> ${verdictOf(results, strict).toUpperCase()}`);
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
const bool = (v) => (v === undefined ? undefined : /^(1|true|yes|on)$/i.test(v));
|
|
55
|
+
const int = (v) => (v === undefined || v === '' ? undefined : Number(v));
|
|
56
|
+
/** The deployment's own environment variable names (as the API and the guides use them). */
|
|
57
|
+
function configFromEnv(env) {
|
|
58
|
+
const c = {};
|
|
59
|
+
if (env.PCA_DOCTOR_URL)
|
|
60
|
+
c.url = env.PCA_DOCTOR_URL;
|
|
61
|
+
if (env.PCA_DOCTOR_REFERENCE_URL)
|
|
62
|
+
c.clock = { referenceUrl: env.PCA_DOCTOR_REFERENCE_URL };
|
|
63
|
+
if (env.PCA_DOCTOR_STORE)
|
|
64
|
+
c.store = { adapter: env.PCA_DOCTOR_STORE };
|
|
65
|
+
const proxy = {};
|
|
66
|
+
if (env.TRUSTED_PROXY_COUNT !== undefined)
|
|
67
|
+
proxy.trustedProxyCount = int(env.TRUSTED_PROXY_COUNT);
|
|
68
|
+
if (env.TRUSTED_PROXY_HEADER)
|
|
69
|
+
proxy.header = env.TRUSTED_PROXY_HEADER;
|
|
70
|
+
if (env.PCA_TRUSTED_PROXY_HOPS !== undefined)
|
|
71
|
+
proxy.hops = int(env.PCA_TRUSTED_PROXY_HOPS);
|
|
72
|
+
if (Object.keys(proxy).length)
|
|
73
|
+
c.proxy = proxy;
|
|
74
|
+
const v = {};
|
|
75
|
+
if (env.PCA_ALLOWED_SUITES !== undefined)
|
|
76
|
+
v.allowedSuites = env.PCA_ALLOWED_SUITES.split(',').map((s) => s.trim()).filter(Boolean);
|
|
77
|
+
if (env.PCA_PQ_PINS)
|
|
78
|
+
v.pqPins = JSON.parse(env.PCA_PQ_PINS);
|
|
79
|
+
if (env.PCA_ALLOW_UNBOUND_PQ !== undefined)
|
|
80
|
+
v.allowUnboundPq = bool(env.PCA_ALLOW_UNBOUND_PQ);
|
|
81
|
+
if (Object.keys(v).length)
|
|
82
|
+
c.verifier = v;
|
|
83
|
+
return c;
|
|
84
|
+
}
|
|
85
|
+
/** Two-level merge: later sources win per field inside each section. */
|
|
86
|
+
function mergeConfig(...cs) {
|
|
87
|
+
const out = {};
|
|
88
|
+
for (const c of cs) {
|
|
89
|
+
for (const [k, val] of Object.entries(c)) {
|
|
90
|
+
if (val === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
out[k] = val && typeof val === 'object' && !Array.isArray(val) ? { ...(out[k] ?? {}), ...val } : val;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/** A JSON file, or a JS module whose default export is the config object. */
|
|
98
|
+
async function loadConfigFile(path) {
|
|
99
|
+
if (path.endsWith('.json'))
|
|
100
|
+
return JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
101
|
+
const m = (await import((0, node_url_1.pathToFileURL)((0, node_path_1.resolve)(path)).href));
|
|
102
|
+
return (m.default ?? m);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":";;;AAoBA,8BASC;AAKD,8BAIC;AAED,sCASC;AAMD,sCAgBC;AAGD,kCASC;AAGD,wCAIC;AA1FD,yFAAyF;AACzF,qCAAuC;AACvC,yCAAoC;AACpC,uCAAyC;AACzC,uCAA+E;AAC/E,mCAAqD;AACrD,mCAA4D;AAC5D,mCAAqC;AACrC,qCAA8D;AAGjD,QAAA,YAAY,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAU,CAAC;AAS/E,KAAK,UAAU,SAAS,CAAC,GAAiB,EAAE,OAA8B,oBAAY,EAAE,OAAmB,EAAE;IAClH,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,IAAK,UAAU,CAAC,KAA8B,CAAC;IAC3E,MAAM,GAAG,GAAkB,EAAE,CAAC;IAC9B,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,MAAM,IAAA,kBAAU,EAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACjG,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAA,kBAAU,EAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACjE,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,IAAA,wBAAgB,EAAC,GAAG,CAAC,EAAE,MAAM,IAAA,uBAAe,EAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IAC9G,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,IAAA,2BAAkB,EAAC,GAAG,CAAC,EAAE,IAAA,uBAAc,EAAC,GAAG,CAAC,CAAC,CAAC;IACpF,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,GAAG,CAAC,IAAI,CAAC,IAAA,0BAAgB,EAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,IAAA,0BAAgB,EAAC,GAAG,CAAC,CAAC,CAAC;IAChH,OAAO,GAAG,CAAC;AACb,CAAC;AAID,qFAAqF;AACrF,SAAgB,SAAS,CAAC,OAA+B,EAAE,MAAM,GAAG,KAAK;IACvE,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAC5D,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IAC9E,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAgB,aAAa,CAAC,OAA+B,EAAE,MAA0B,EAAE,MAAM,GAAG,KAAK;IACvG,MAAM,KAAK,GAAG,CAAC,aAAa,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC3D,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QACpF,IAAI,CAAC,CAAC,WAAW;YAAE,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7F,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,CAAwB,EAAU,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7F,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,YAAY,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;IACjJ,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,CAAqB,EAAuB,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1H,MAAM,GAAG,GAAG,CAAC,CAAqB,EAAsB,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAEjH,4FAA4F;AAC5F,SAAgB,aAAa,CAAC,GAAuC;IACnE,MAAM,CAAC,GAAiB,EAAE,CAAC;IAC3B,IAAI,GAAG,CAAC,cAAc;QAAE,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,cAAc,CAAC;IACnD,IAAI,GAAG,CAAC,wBAAwB;QAAE,CAAC,CAAC,KAAK,GAAG,EAAE,YAAY,EAAE,GAAG,CAAC,wBAAwB,EAAE,CAAC;IAC3F,IAAI,GAAG,CAAC,gBAAgB;QAAE,CAAC,CAAC,KAAK,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,gBAAgB,EAAE,CAAC;IACtE,MAAM,KAAK,GAAuC,EAAE,CAAC;IACrD,IAAI,GAAG,CAAC,mBAAmB,KAAK,SAAS;QAAE,KAAK,CAAC,iBAAiB,GAAG,GAAG,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAClG,IAAI,GAAG,CAAC,oBAAoB;QAAE,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,oBAAmC,CAAC;IACrF,IAAI,GAAG,CAAC,sBAAsB,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IAC3F,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM;QAAE,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC;IAC/C,MAAM,CAAC,GAA0C,EAAE,CAAC;IACpD,IAAI,GAAG,CAAC,kBAAkB,KAAK,SAAS;QAAE,CAAC,CAAC,aAAa,GAAG,GAAG,CAAC,kBAAkB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACnI,IAAI,GAAG,CAAC,WAAW;QAAE,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAA2B,CAAC;IACtF,IAAI,GAAG,CAAC,oBAAoB,KAAK,SAAS;QAAE,CAAC,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAC9F,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM;QAAE,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;IAC1C,OAAO,CAAC,CAAC;AACX,CAAC;AAED,wEAAwE;AACxE,SAAgB,WAAW,CAAC,GAAG,EAAkB;IAC/C,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QACnB,KAAK,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,IAAI,GAAG,KAAK,SAAS;gBAAE,SAAS;YAChC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAE,GAAG,CAAC,CAAC,CAAY,IAAI,EAAE,CAAC,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;QACnH,CAAC;IACH,CAAC;IACD,OAAO,GAAmB,CAAC;AAC7B,CAAC;AAED,6EAA6E;AACtE,KAAK,UAAU,cAAc,CAAC,IAAY;IAC/C,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,IAAA,sBAAY,EAAC,IAAI,EAAE,MAAM,CAAC,CAAiB,CAAC;IAC1F,MAAM,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,IAAA,wBAAa,EAAC,IAAA,mBAAO,EAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAA+B,CAAC;IAC1F,OAAO,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAiB,CAAC;AAC1C,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** @atlasauth/pca-doctor: check a live PCA deployment's assumptions (see the pca-doctor guide). */
|
|
2
|
+
export * from './types';
|
|
3
|
+
export * from './clock';
|
|
4
|
+
export * from './store';
|
|
5
|
+
export * from './proxy';
|
|
6
|
+
export * from './suites';
|
|
7
|
+
export * from './caveats';
|
|
8
|
+
export * from './doctor';
|
|
9
|
+
export { main, parseDoctorArgs, USAGE } from './cli';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.USAGE = exports.parseDoctorArgs = exports.main = void 0;
|
|
18
|
+
/** @atlasauth/pca-doctor: check a live PCA deployment's assumptions (see the pca-doctor guide). */
|
|
19
|
+
__exportStar(require("./types"), exports);
|
|
20
|
+
__exportStar(require("./clock"), exports);
|
|
21
|
+
__exportStar(require("./store"), exports);
|
|
22
|
+
__exportStar(require("./proxy"), exports);
|
|
23
|
+
__exportStar(require("./suites"), exports);
|
|
24
|
+
__exportStar(require("./caveats"), exports);
|
|
25
|
+
__exportStar(require("./doctor"), exports);
|
|
26
|
+
var cli_1 = require("./cli");
|
|
27
|
+
Object.defineProperty(exports, "main", { enumerable: true, get: function () { return cli_1.main; } });
|
|
28
|
+
Object.defineProperty(exports, "parseDoctorArgs", { enumerable: true, get: function () { return cli_1.parseDoctorArgs; } });
|
|
29
|
+
Object.defineProperty(exports, "USAGE", { enumerable: true, get: function () { return cli_1.USAGE; } });
|
|
30
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,mGAAmG;AACnG,0CAAwB;AACxB,0CAAwB;AACxB,0CAAwB;AACxB,0CAAwB;AACxB,2CAAyB;AACzB,4CAA0B;AAC1B,2CAAyB;AACzB,6BAAqD;AAA5C,2FAAA,IAAI,OAAA;AAAE,sGAAA,eAAe,OAAA;AAAE,4FAAA,KAAK,OAAA"}
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TRUSTED_PROXY_COUNT / TRUSTED_PROXY_HEADER sanity. Two parts:
|
|
3
|
+
* - config: the stated values against the stated number of appending proxies (`hops`);
|
|
4
|
+
* - live probe: forged forwarding headers sent to a public rate-limited path, watching `RateLimit-Remaining`. If a forged
|
|
5
|
+
* request lands in a FRESH bucket while plain requests keep draining one bucket, the deployment derives the client
|
|
6
|
+
* address from client-written entries (count too large, or a proxy that passes the header through untouched).
|
|
7
|
+
* What cannot be observed from one vantage point: a count that is too LOW (every client merged into the proxy's bucket
|
|
8
|
+
* looks the same as one client), the socket-address fallback, and anything the deployment does not reflect in headers.
|
|
9
|
+
*/
|
|
10
|
+
import type { FetchLike } from './clock';
|
|
11
|
+
import type { CheckResult, DoctorConfig } from './types';
|
|
12
|
+
export declare const PROXY_REMEDIATION = "Set TRUSTED_PROXY_COUNT to exactly the number of reverse proxies in front of the API that APPEND to the trusted header (1 behind one load balancer / ingress; 0 when clients connect directly), and TRUSTED_PROXY_HEADER to the header they write. Too high lets a client pick its own rate-limit bucket with extra X-Forwarded-For entries; too low merges every client into the proxy's bucket.";
|
|
13
|
+
export declare function checkProxyConfig(cfg: DoctorConfig): CheckResult;
|
|
14
|
+
/** A random documentation address (RFC 5737 TEST-NET-2 / -3): never a real client. */
|
|
15
|
+
export declare function testNetIp(rand?: () => number): string;
|
|
16
|
+
/** Forged values for all three headers at once (the server trusts at most one of them). */
|
|
17
|
+
export declare function forgedHeaders(rand?: () => number): Record<string, string>;
|
|
18
|
+
export declare function checkProxyProbe(cfg: DoctorConfig, deps: {
|
|
19
|
+
fetch: FetchLike;
|
|
20
|
+
rand?: () => number;
|
|
21
|
+
}): Promise<CheckResult>;
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROXY_REMEDIATION = void 0;
|
|
4
|
+
exports.checkProxyConfig = checkProxyConfig;
|
|
5
|
+
exports.testNetIp = testNetIp;
|
|
6
|
+
exports.forgedHeaders = forgedHeaders;
|
|
7
|
+
exports.checkProxyProbe = checkProxyProbe;
|
|
8
|
+
exports.PROXY_REMEDIATION = 'Set TRUSTED_PROXY_COUNT to exactly the number of reverse proxies in front of the API that APPEND to the trusted header (1 behind one load balancer / ingress; 0 when clients connect directly), and TRUSTED_PROXY_HEADER to the header they write. Too high lets a client pick its own rate-limit bucket with extra X-Forwarded-For entries; too low merges every client into the proxy\'s bucket.';
|
|
9
|
+
function checkProxyConfig(cfg) {
|
|
10
|
+
const p = cfg.proxy;
|
|
11
|
+
const n = p?.trustedProxyCount;
|
|
12
|
+
if (n === undefined)
|
|
13
|
+
return { id: 'proxy.config', status: 'skip', detail: 'TRUSTED_PROXY_COUNT not given (config proxy.trustedProxyCount or env TRUSTED_PROXY_COUNT)' };
|
|
14
|
+
const header = p?.header ?? 'x-forwarded-for';
|
|
15
|
+
const fail = (detail) => ({ id: 'proxy.config', status: 'fail', detail, remediation: exports.PROXY_REMEDIATION });
|
|
16
|
+
if (!Number.isInteger(n) || n < 0 || n > 16)
|
|
17
|
+
return fail(`TRUSTED_PROXY_COUNT=${String(n)} is not an integer in 0..16`);
|
|
18
|
+
if (header === 'x-real-ip' && n === 0)
|
|
19
|
+
return fail('TRUSTED_PROXY_HEADER=x-real-ip needs TRUSTED_PROXY_COUNT >= 1 (with 0 every forwarding header is ignored)');
|
|
20
|
+
if (p?.hops === undefined) {
|
|
21
|
+
return {
|
|
22
|
+
id: 'proxy.config',
|
|
23
|
+
status: n === 0 ? 'pass' : 'warn',
|
|
24
|
+
detail: `TRUSTED_PROXY_COUNT=${n} (${header}); the real number of appending proxies was not stated (proxy.hops / PCA_TRUSTED_PROXY_HOPS), so it cannot be compared`,
|
|
25
|
+
...(n === 0 ? {} : { remediation: exports.PROXY_REMEDIATION }),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (n > p.hops)
|
|
29
|
+
return fail(`TRUSTED_PROXY_COUNT=${n} is larger than the ${p.hops} proxies in front of the API: a client chooses its own address (and rate-limit bucket) with extra ${header} entries`);
|
|
30
|
+
if (n < p.hops) {
|
|
31
|
+
return { id: 'proxy.config', status: 'warn', detail: `TRUSTED_PROXY_COUNT=${n} is smaller than the ${p.hops} proxies in front of the API: clients are keyed by a proxy's address and share its bucket`, remediation: exports.PROXY_REMEDIATION };
|
|
32
|
+
}
|
|
33
|
+
return { id: 'proxy.config', status: 'pass', detail: `TRUSTED_PROXY_COUNT=${n} (${header}) matches the stated ${p.hops} appending proxies` };
|
|
34
|
+
}
|
|
35
|
+
/** A random documentation address (RFC 5737 TEST-NET-2 / -3): never a real client. */
|
|
36
|
+
function testNetIp(rand = Math.random) {
|
|
37
|
+
return `${rand() < 0.5 ? '198.51.100' : '203.0.113'}.${1 + Math.floor(rand() * 254)}`;
|
|
38
|
+
}
|
|
39
|
+
/** Forged values for all three headers at once (the server trusts at most one of them). */
|
|
40
|
+
function forgedHeaders(rand = Math.random) {
|
|
41
|
+
const chain = Array.from({ length: 17 }, () => testNetIp(rand));
|
|
42
|
+
return { 'x-forwarded-for': chain.join(', '), forwarded: chain.map((ip) => `for=${ip}`).join(', '), 'x-real-ip': testNetIp(rand) };
|
|
43
|
+
}
|
|
44
|
+
async function remaining(fetchImpl, url, headers, timeoutMs) {
|
|
45
|
+
const r = await fetchImpl(url, { headers, signal: AbortSignal.timeout(timeoutMs) });
|
|
46
|
+
await r.arrayBuffer().catch(() => undefined);
|
|
47
|
+
const v = r.headers.get('ratelimit-remaining');
|
|
48
|
+
const n = v === null ? NaN : Number(v);
|
|
49
|
+
return Number.isFinite(n) ? n : undefined;
|
|
50
|
+
}
|
|
51
|
+
async function checkProxyProbe(cfg, deps) {
|
|
52
|
+
const id = 'proxy.probe';
|
|
53
|
+
if (!cfg.url)
|
|
54
|
+
return { id, status: 'skip', detail: 'no deployment URL given (--url)' };
|
|
55
|
+
if (cfg.proxy?.probe === false)
|
|
56
|
+
return { id, status: 'skip', detail: 'disabled (--no-probe)' };
|
|
57
|
+
const url = new URL(cfg.proxy?.probePath ?? '/v1/pca/suites', cfg.url).href;
|
|
58
|
+
const t = cfg.clock?.timeoutMs ?? 10_000;
|
|
59
|
+
let seq;
|
|
60
|
+
try {
|
|
61
|
+
const a = await remaining(deps.fetch, url, {}, t);
|
|
62
|
+
const b = await remaining(deps.fetch, url, {}, t);
|
|
63
|
+
const f = await remaining(deps.fetch, url, forgedHeaders(deps.rand), t);
|
|
64
|
+
const c = await remaining(deps.fetch, url, {}, t);
|
|
65
|
+
seq = [a, b, f, c];
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return { id, status: 'warn', detail: `probe request to ${url} failed: ${e.message}`, remediation: 'Set proxy.probePath to a public, rate-limited path of the deployment, or run with --no-probe.' };
|
|
69
|
+
}
|
|
70
|
+
const observed = { url, remaining: seq };
|
|
71
|
+
if (seq.some((x) => x === undefined)) {
|
|
72
|
+
return { id, status: 'skip', detail: `not observable remotely: ${url} does not return a RateLimit-Remaining header on every response`, observed };
|
|
73
|
+
}
|
|
74
|
+
const [a, b, f, c] = seq;
|
|
75
|
+
if (!(b < a && c < b)) {
|
|
76
|
+
return { id, status: 'warn', detail: `inconclusive: the plain requests' bucket did not drain monotonically (${a}, ${b}, then ${c}): a window rollover, a shared bucket or concurrent traffic. Re-run.`, observed };
|
|
77
|
+
}
|
|
78
|
+
if (f > b) {
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
status: 'fail',
|
|
82
|
+
detail: `a request with FORGED forwarding headers got a fresh rate-limit bucket (remaining ${f}, while the plain requests' bucket was at ${b}): the deployment trusts client-written forwarding entries`,
|
|
83
|
+
remediation: exports.PROXY_REMEDIATION,
|
|
84
|
+
observed,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return { id, status: 'pass', detail: `forged X-Forwarded-For / Forwarded / X-Real-IP did not move the client to another bucket (remaining ${a}, ${b}, forged ${f}, ${c}). A count that is too LOW is not observable from one client.`, observed };
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay / counter store checks through the conformance kit's adapter interface: a REAL concurrent conflicting write
|
|
3
|
+
* (many clients consume one fresh key at once: exactly one may win; counters race on one stream) and durability (write,
|
|
4
|
+
* close, restart when the adapter can, read back). Short by design; the full kit (`pca-store-conformance`) goes further.
|
|
5
|
+
*/
|
|
6
|
+
import { loadAdapter as kitLoadAdapter } from '@atlasauth/pca-store-conformance';
|
|
7
|
+
import type { CheckResult, DoctorConfig } from './types';
|
|
8
|
+
export declare const ATOMICITY_REMEDIATION = "Make consumeOnce / advance one atomic operation on the shared store, e.g. Postgres INSERT .. ON CONFLICT (key) DO UPDATE .. WHERE until < $now RETURNING and UPDATE .. WHERE counter < $n RETURNING, Redis SET key NX PX or a Lua compare-and-set; never read, then write. Then run the full kit: pca-store-conformance --adapter <your adapter>.";
|
|
9
|
+
export declare const DURABILITY_REMEDIATION = "Keep consumed keys and counters in a store with synchronous, persistent commits (Postgres synchronous_commit=on; Redis appendfsync always and maxmemory-policy noeviction), shared by every verifier instance. An in-memory store is only correct for a single process whose lifetime covers the action lifetime.";
|
|
10
|
+
export declare function checkStore(cfg: DoctorConfig, deps?: {
|
|
11
|
+
loadAdapter?: typeof kitLoadAdapter;
|
|
12
|
+
}): Promise<CheckResult[]>;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DURABILITY_REMEDIATION = exports.ATOMICITY_REMEDIATION = void 0;
|
|
4
|
+
exports.checkStore = checkStore;
|
|
5
|
+
/**
|
|
6
|
+
* Replay / counter store checks through the conformance kit's adapter interface: a REAL concurrent conflicting write
|
|
7
|
+
* (many clients consume one fresh key at once: exactly one may win; counters race on one stream) and durability (write,
|
|
8
|
+
* close, restart when the adapter can, read back). Short by design; the full kit (`pca-store-conformance`) goes further.
|
|
9
|
+
*/
|
|
10
|
+
const pca_store_conformance_1 = require("@atlasauth/pca-store-conformance");
|
|
11
|
+
const OP = { consume: 'consumeOnce', advance: 'advance' };
|
|
12
|
+
exports.ATOMICITY_REMEDIATION = 'Make consumeOnce / advance one atomic operation on the shared store, e.g. Postgres INSERT .. ON CONFLICT (key) DO UPDATE .. WHERE until < $now RETURNING and UPDATE .. WHERE counter < $n RETURNING, Redis SET key NX PX or a Lua compare-and-set; never read, then write. Then run the full kit: pca-store-conformance --adapter <your adapter>.';
|
|
13
|
+
exports.DURABILITY_REMEDIATION = 'Keep consumed keys and counters in a store with synchronous, persistent commits (Postgres synchronous_commit=on; Redis appendfsync always and maxmemory-policy noeviction), shared by every verifier instance. An in-memory store is only correct for a single process whose lifetime covers the action lifetime.';
|
|
14
|
+
function firstCounterexample(h) {
|
|
15
|
+
const v = h.violations[0];
|
|
16
|
+
return v ? ` First violating key ${v.key}: ${(0, pca_store_conformance_1.formatOps)(v.counterexample ?? []).join(' | ')}` : '';
|
|
17
|
+
}
|
|
18
|
+
async function checkStore(cfg, deps = {}) {
|
|
19
|
+
const spec = cfg.store?.adapter;
|
|
20
|
+
if (!spec) {
|
|
21
|
+
const detail = 'no store adapter given (--store ./adapter.mjs); the replay store cannot be exercised remotely';
|
|
22
|
+
return [
|
|
23
|
+
{ id: 'store.atomicity', status: 'skip', detail },
|
|
24
|
+
{ id: 'store.durability', status: 'skip', detail },
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
let adapter;
|
|
28
|
+
try {
|
|
29
|
+
adapter = await (deps.loadAdapter ?? pca_store_conformance_1.loadAdapter)(spec, cfg.store?.options ?? {});
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
return [{ id: 'store.atomicity', status: 'fail', detail: `cannot load the store adapter ${spec}: ${e.message}`, remediation: 'Point --store at a module exporting a store adapter (see the store conformance kit guide).' }];
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const kinds = await (0, pca_store_conformance_1.detectKinds)(adapter);
|
|
36
|
+
return [await atomicity(adapter, kinds, cfg), await durability(adapter, kinds)];
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return [{ id: 'store.atomicity', status: 'fail', detail: `the store adapter failed: ${e.message}`, remediation: exports.ATOMICITY_REMEDIATION }];
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
await adapter.teardown?.();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function atomicity(adapter, kinds, cfg) {
|
|
46
|
+
const clients = cfg.store?.concurrency ?? 32;
|
|
47
|
+
const rounds = cfg.store?.rounds ?? 10;
|
|
48
|
+
const parts = [];
|
|
49
|
+
const observed = {};
|
|
50
|
+
let status = 'pass';
|
|
51
|
+
let cx = '';
|
|
52
|
+
for (const kind of kinds) {
|
|
53
|
+
const w = await (0, pca_store_conformance_1.runBurst)(adapter, kind, { clients, rounds, values: 8 });
|
|
54
|
+
const h = (0, pca_store_conformance_1.checkHistory)(w.ops);
|
|
55
|
+
const winners = new Set(w.ops.filter((o) => o.res === 'true').map((o) => o.key)).size;
|
|
56
|
+
observed[kind] = { ops: h.ops, keys: h.keys, outcomes: h.outcomes, violations: h.violations.length, unresolved: h.unresolved.length };
|
|
57
|
+
const definite = h.outcomes.true + h.outcomes.false;
|
|
58
|
+
if (h.violations.length > 0) {
|
|
59
|
+
status = 'fail';
|
|
60
|
+
cx ||= firstCounterexample(h);
|
|
61
|
+
parts.push(`${OP[kind]}: ${h.violations.length} of ${rounds} same-key rounds NOT linearizable (${clients} concurrent clients)`);
|
|
62
|
+
}
|
|
63
|
+
else if (definite === 0 || h.unresolved.length > 0) {
|
|
64
|
+
if (status === 'pass')
|
|
65
|
+
status = 'warn';
|
|
66
|
+
parts.push(`${OP[kind]}: inconclusive (${w.errors} of ${h.ops} calls failed or timed out${h.unresolved.length ? `, ${h.unresolved.length} key(s) undecided` : ''})`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
parts.push(`${OP[kind]}: ${rounds} rounds of ${clients} concurrent conflicting calls linearizable${kind === 'consume' ? ` (${winners} of ${rounds} rounds had exactly one TRUE)` : ''}${w.errors ? `, ${w.errors} calls failed closed` : ''}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
id: 'store.atomicity',
|
|
74
|
+
status,
|
|
75
|
+
detail: `${adapter.name}: ${parts.join('; ')}.${cx}`,
|
|
76
|
+
...(status === 'pass' ? {} : { remediation: exports.ATOMICITY_REMEDIATION }),
|
|
77
|
+
observed,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function durability(adapter, kinds) {
|
|
81
|
+
if (adapter.capabilities?.durable === false) {
|
|
82
|
+
return { id: 'store.durability', status: 'warn', detail: `${adapter.name} declares itself non-durable: a restart forgets consumed nonces`, remediation: exports.DURABILITY_REMEDIATION };
|
|
83
|
+
}
|
|
84
|
+
const parts = [];
|
|
85
|
+
let status = 'pass';
|
|
86
|
+
let cx = '';
|
|
87
|
+
for (const kind of kinds) {
|
|
88
|
+
const w = await (0, pca_store_conformance_1.runDurability)(adapter, kind);
|
|
89
|
+
const h = (0, pca_store_conformance_1.checkHistory)(w.ops);
|
|
90
|
+
if (h.violations.length > 0) {
|
|
91
|
+
status = 'fail';
|
|
92
|
+
cx ||= firstCounterexample(h);
|
|
93
|
+
parts.push(`${OP[kind]}: state LOST across close/restart`);
|
|
94
|
+
}
|
|
95
|
+
else if (h.outcomes.true + h.outcomes.false < w.ops.length) {
|
|
96
|
+
if (status === 'pass')
|
|
97
|
+
status = 'warn';
|
|
98
|
+
parts.push(`${OP[kind]}: inconclusive (${w.errors} calls failed)`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
parts.push(`${OP[kind]}: survived`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const how = adapter.restart ? 'write, close, restart() the backend, read back' : 'write, close, re-open (no restart() hook: the backend itself was not restarted)';
|
|
105
|
+
return {
|
|
106
|
+
id: 'store.durability',
|
|
107
|
+
status,
|
|
108
|
+
detail: `${adapter.name} (${how}): ${parts.join('; ')}.${cx}`,
|
|
109
|
+
...(status === 'pass' ? {} : { remediation: exports.DURABILITY_REMEDIATION }),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
package/dist/suites.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CheckResult, DoctorConfig } from './types';
|
|
2
|
+
export declare const SUITES_REMEDIATION = "Set the verifier's allowedSuites to exactly the suites your issuers sign with (names from SIG_SUITES), e.g. ['ed25519'] or ['hybrid-ed25519-ml-dsa-65'].";
|
|
3
|
+
export declare const PQ_REMEDIATION = "Never set allowUnboundPq. For pure-PQ or hybrid roots, configure pqPins (issuer Ed25519 identity -> PQ public key, from pqPinFor) or verify against your stored root grant (trustedRoot; requirePCA / verifyPCActnCore do this).";
|
|
4
|
+
export declare function checkAllowedSuites(cfg: DoctorConfig, registry?: Readonly<Record<string, unknown>>): CheckResult;
|
|
5
|
+
export declare function checkPqBinding(cfg: DoctorConfig): CheckResult;
|
package/dist/suites.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PQ_REMEDIATION = exports.SUITES_REMEDIATION = void 0;
|
|
4
|
+
exports.checkAllowedSuites = checkAllowedSuites;
|
|
5
|
+
exports.checkPqBinding = checkPqBinding;
|
|
6
|
+
/** allowedSuites / pqPins / allowUnboundPq sanity against the library's closed suite registry. */
|
|
7
|
+
const pca_1 = require("@atlasauth/pca");
|
|
8
|
+
const B64U = /^[A-Za-z0-9_-]+$/;
|
|
9
|
+
const ED25519_ID = /^[A-Za-z0-9_-]{43}$/;
|
|
10
|
+
exports.SUITES_REMEDIATION = "Set the verifier's allowedSuites to exactly the suites your issuers sign with (names from SIG_SUITES), e.g. ['ed25519'] or ['hybrid-ed25519-ml-dsa-65'].";
|
|
11
|
+
exports.PQ_REMEDIATION = 'Never set allowUnboundPq. For pure-PQ or hybrid roots, configure pqPins (issuer Ed25519 identity -> PQ public key, from pqPinFor) or verify against your stored root grant (trustedRoot; requirePCA / verifyPCActnCore do this).';
|
|
12
|
+
const isPq = (alg) => !!pca_1.SIG_SUITES[alg]?.needsPqPk;
|
|
13
|
+
function checkAllowedSuites(cfg, registry = pca_1.SIG_SUITES) {
|
|
14
|
+
const id = 'suites.allowed';
|
|
15
|
+
const v = cfg.verifier;
|
|
16
|
+
if (!v)
|
|
17
|
+
return { id, status: 'skip', detail: 'no verifier configuration given (config verifier.*)' };
|
|
18
|
+
const list = v.allowedSuites;
|
|
19
|
+
const known = Object.keys(registry);
|
|
20
|
+
if (list === undefined) {
|
|
21
|
+
return { id, status: 'warn', detail: `allowedSuites is unset: every registered suite is accepted (${known.length}: ${known.join(', ')})`, remediation: exports.SUITES_REMEDIATION };
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
24
|
+
return { id, status: 'fail', detail: 'allowedSuites is empty or not a list: no PCActn can verify', remediation: exports.SUITES_REMEDIATION };
|
|
25
|
+
}
|
|
26
|
+
const unknown = list.filter((s) => typeof s !== 'string' || !Object.prototype.hasOwnProperty.call(registry, s));
|
|
27
|
+
if (unknown.length > 0) {
|
|
28
|
+
return { id, status: 'fail', detail: `allowedSuites names suite(s) outside the registry: ${unknown.map(String).join(', ')} (known: ${known.join(', ')})`, remediation: exports.SUITES_REMEDIATION };
|
|
29
|
+
}
|
|
30
|
+
return { id, status: 'pass', detail: `allowedSuites = ${list.join(', ')}` };
|
|
31
|
+
}
|
|
32
|
+
function checkPqBinding(cfg) {
|
|
33
|
+
const id = 'suites.pq-binding';
|
|
34
|
+
const v = cfg.verifier;
|
|
35
|
+
if (!v)
|
|
36
|
+
return { id, status: 'skip', detail: 'no verifier configuration given (config verifier.*)' };
|
|
37
|
+
const fail = (detail) => ({ id, status: 'fail', detail, remediation: exports.PQ_REMEDIATION });
|
|
38
|
+
if (v.allowUnboundPq === true)
|
|
39
|
+
return fail('allowUnboundPq is set: PQ keys are read from the artifact, so a hybrid gives no protection against an Ed25519 forger and a pure-PQ hop can be signed by any key');
|
|
40
|
+
const pins = v.pqPins;
|
|
41
|
+
if (pins !== undefined) {
|
|
42
|
+
if (!pins || typeof pins !== 'object' || Array.isArray(pins))
|
|
43
|
+
return fail('pqPins is not an object of { ed25519 identity: pq public key }');
|
|
44
|
+
const bad = Object.entries(pins).filter(([k, val]) => !ED25519_ID.test(k) || typeof val !== 'string' || !B64U.test(val));
|
|
45
|
+
if (bad.length > 0)
|
|
46
|
+
return fail(`pqPins has malformed entries (key must be a 43-char base64url Ed25519 identity, value a base64url PQ key): ${bad.map(([k]) => k).join(', ')}`);
|
|
47
|
+
}
|
|
48
|
+
const pq = (v.allowedSuites ?? Object.keys(pca_1.SIG_SUITES)).filter(isPq);
|
|
49
|
+
if (pq.length === 0)
|
|
50
|
+
return { id, status: 'pass', detail: 'no post-quantum suite is accepted (Ed25519 only: no PQ binding needed, and no PQ protection)' };
|
|
51
|
+
const pinned = pins !== undefined && Object.keys(pins).length > 0;
|
|
52
|
+
if (!pinned && v.trustedRoot !== true) {
|
|
53
|
+
return {
|
|
54
|
+
id,
|
|
55
|
+
status: 'warn',
|
|
56
|
+
detail: `post-quantum suite(s) accepted (${pq.join(', ')}) with neither pqPins nor a stored trustedRoot: a non-Ed25519 root is refused (fail closed) or its PQ key is not pinned`,
|
|
57
|
+
remediation: exports.PQ_REMEDIATION,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { id, status: 'pass', detail: `PQ suites (${pq.join(', ')}) are bound: ${pinned ? `${Object.keys(pins).length} pin(s)` : 'stored trustedRoot'}; allowUnboundPq off` };
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=suites.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suites.js","sourceRoot":"","sources":["../src/suites.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** pca-doctor result and configuration types. */
|
|
2
|
+
export type CheckStatus = 'pass' | 'warn' | 'fail' | 'skip';
|
|
3
|
+
export interface CheckResult {
|
|
4
|
+
/** Stable id, e.g. `clock`, `store.atomicity`. */
|
|
5
|
+
id: string;
|
|
6
|
+
status: CheckStatus;
|
|
7
|
+
/** What was observed (or why nothing could be). */
|
|
8
|
+
detail: string;
|
|
9
|
+
/** What to do about a warn / fail. */
|
|
10
|
+
remediation?: string;
|
|
11
|
+
/** Raw observations for --json. */
|
|
12
|
+
observed?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
export type ProxyHeader = 'x-forwarded-for' | 'forwarded' | 'x-real-ip';
|
|
15
|
+
export interface DoctorConfig {
|
|
16
|
+
/** Base URL of the live deployment (the API that verifies PCActns). */
|
|
17
|
+
url?: string;
|
|
18
|
+
clock?: {
|
|
19
|
+
/** HTTPS URL whose Date header is the reference, or 'local' for this machine's clock. Default https://www.google.com/generate_204. */
|
|
20
|
+
referenceUrl?: string;
|
|
21
|
+
/** Fail beyond this skew (default PCACTN_MAX_SKEW_MS = 60 s). */
|
|
22
|
+
maxSkewMs?: number;
|
|
23
|
+
/** Warn beyond this fraction of maxSkewMs (default 0.25). */
|
|
24
|
+
warnFraction?: number;
|
|
25
|
+
/** Per-request timeout (default 10 s). */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
};
|
|
28
|
+
store?: {
|
|
29
|
+
/** Adapter module path or builtin:<name> (the store conformance kit's adapter interface). */
|
|
30
|
+
adapter?: string;
|
|
31
|
+
options?: Record<string, string>;
|
|
32
|
+
/** Concurrent clients for the conflicting-write probe (default 32). */
|
|
33
|
+
concurrency?: number;
|
|
34
|
+
/** Same-key rounds (default 10). */
|
|
35
|
+
rounds?: number;
|
|
36
|
+
};
|
|
37
|
+
proxy?: {
|
|
38
|
+
/** The deployment's TRUSTED_PROXY_COUNT. */
|
|
39
|
+
trustedProxyCount?: number;
|
|
40
|
+
/** The deployment's TRUSTED_PROXY_HEADER (default x-forwarded-for). */
|
|
41
|
+
header?: ProxyHeader;
|
|
42
|
+
/** How many reverse proxies really sit in front of the API and append to the header (operator's statement). */
|
|
43
|
+
hops?: number;
|
|
44
|
+
/** Send forged forwarding headers to the live URL and watch the rate-limit bucket (default true when url is set). */
|
|
45
|
+
probe?: boolean;
|
|
46
|
+
/** A public, rate-limited path on the deployment (default /v1/pca/suites). */
|
|
47
|
+
probePath?: string;
|
|
48
|
+
};
|
|
49
|
+
verifier?: {
|
|
50
|
+
/** The verifier's allowedSuites option (absent = unset). */
|
|
51
|
+
allowedSuites?: string[];
|
|
52
|
+
/** The verifier's pqPins (issuer Ed25519 identity -> PQ public key, base64url). */
|
|
53
|
+
pqPins?: Record<string, string>;
|
|
54
|
+
/** The verifier supplies its stored root grant as trustedRoot (verifyPCActnCore / requirePCA do). */
|
|
55
|
+
trustedRoot?: boolean;
|
|
56
|
+
allowUnboundPq?: boolean;
|
|
57
|
+
/** requirePCA's insecureAllowUnenforced. */
|
|
58
|
+
insecureAllowUnenforced?: boolean;
|
|
59
|
+
/** requirePCA's `require` list (absent = the defaults). */
|
|
60
|
+
require?: string[];
|
|
61
|
+
/** Directory of the deployed application, to load ITS installed @atlasauth/pca (default: the doctor's own). */
|
|
62
|
+
pcaDir?: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA,iDAAiD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlasauth/pca-doctor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "pca-doctor: check a live PCA deployment's assumptions (clock skew, replay-store atomicity and durability, trusted-proxy config, suite / PQ-pin policy, root-caveat enforcement) and print pass / warn / fail with a remediation per check.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"doctor",
|
|
7
|
+
"deployment check",
|
|
8
|
+
"proof-carrying authority",
|
|
9
|
+
"pca"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"homepage": "https://github.com/Atlas-Authorization/pca#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/Atlas-Authorization/pca/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Atlas-Authorization/pca.git",
|
|
19
|
+
"directory": "packages/pca-doctor"
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"bin": {
|
|
24
|
+
"pca-doctor": "./dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@atlasauth/pca": "0.4.2",
|
|
35
|
+
"@atlasauth/pca-store-conformance": "0.4.2"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"typescript": "^5.6.3",
|
|
39
|
+
"vitest": "^2.1.4"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -p tsconfig.build.json",
|
|
51
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
52
|
+
"test": "vitest run"
|
|
53
|
+
}
|
|
6
54
|
}
|