paseo-agy-acp 2.2.0
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/CHANGELOG.md +296 -0
- package/LICENSE +201 -0
- package/README.md +571 -0
- package/README.zh-CN.md +540 -0
- package/assets/official-kernel-compat/rc01/paseo_model_compat.py +172 -0
- package/dist/ACP Connector/acp/session/paseo-context.d.ts +6 -0
- package/dist/ACP Connector/acp/session/paseo-context.js +83 -0
- package/dist/ACP Connector/acp/session/paseo-context.js.map +1 -0
- package/dist/ACP Connector/acp/session/turn-scheduler.d.ts +90 -0
- package/dist/ACP Connector/acp/session/turn-scheduler.js +247 -0
- package/dist/ACP Connector/acp/session/turn-scheduler.js.map +1 -0
- package/dist/ACP Connector/admission/dispatch-boundary.d.ts +19 -0
- package/dist/ACP Connector/admission/dispatch-boundary.js +16 -0
- package/dist/ACP Connector/admission/dispatch-boundary.js.map +1 -0
- package/dist/ACP Connector/admission/errors.d.ts +18 -0
- package/dist/ACP Connector/admission/errors.js +46 -0
- package/dist/ACP Connector/admission/errors.js.map +1 -0
- package/dist/ACP Connector/admission/owner-instance.d.ts +31 -0
- package/dist/ACP Connector/admission/owner-instance.js +105 -0
- package/dist/ACP Connector/admission/owner-instance.js.map +1 -0
- package/dist/ACP Connector/admission/runtime.d.ts +20 -0
- package/dist/ACP Connector/admission/runtime.js +157 -0
- package/dist/ACP Connector/admission/runtime.js.map +1 -0
- package/dist/ACP Connector/admission/startup-recovery.d.ts +22 -0
- package/dist/ACP Connector/admission/startup-recovery.js +129 -0
- package/dist/ACP Connector/admission/startup-recovery.js.map +1 -0
- package/dist/ACP Connector/admission/turn-coordinator.d.ts +57 -0
- package/dist/ACP Connector/admission/turn-coordinator.js +369 -0
- package/dist/ACP Connector/admission/turn-coordinator.js.map +1 -0
- package/dist/ACP Connector/agent.d.ts +4 -0
- package/dist/ACP Connector/agent.js +6 -0
- package/dist/ACP Connector/agent.js.map +1 -0
- package/dist/ACP Connector/main.d.ts +2 -0
- package/dist/ACP Connector/main.js +31 -0
- package/dist/ACP Connector/main.js.map +1 -0
- package/dist/ACP Connector/official-kernel/admission-fence.d.ts +10 -0
- package/dist/ACP Connector/official-kernel/admission-fence.js +52 -0
- package/dist/ACP Connector/official-kernel/admission-fence.js.map +1 -0
- package/dist/ACP Connector/official-kernel/context.d.ts +3 -0
- package/dist/ACP Connector/official-kernel/context.js +52 -0
- package/dist/ACP Connector/official-kernel/context.js.map +1 -0
- package/dist/ACP Connector/official-kernel/errors.d.ts +5 -0
- package/dist/ACP Connector/official-kernel/errors.js +39 -0
- package/dist/ACP Connector/official-kernel/errors.js.map +1 -0
- package/dist/ACP Connector/official-kernel/identity.d.ts +3 -0
- package/dist/ACP Connector/official-kernel/identity.js +17 -0
- package/dist/ACP Connector/official-kernel/identity.js.map +1 -0
- package/dist/ACP Connector/official-kernel/json-rpc.d.ts +34 -0
- package/dist/ACP Connector/official-kernel/json-rpc.js +26 -0
- package/dist/ACP Connector/official-kernel/json-rpc.js.map +1 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-lifecycle.d.ts +115 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-lifecycle.js +1904 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-lifecycle.js.map +1 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-pins.d.ts +14 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-pins.js +29 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-pins.js.map +1 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-rc01-recipe.d.ts +2 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-rc01-recipe.js +32 -0
- package/dist/ACP Connector/official-kernel/kernel-compat-rc01-recipe.js.map +1 -0
- package/dist/ACP Connector/official-kernel/kernel.d.ts +3 -0
- package/dist/ACP Connector/official-kernel/kernel.js +12 -0
- package/dist/ACP Connector/official-kernel/kernel.js.map +1 -0
- package/dist/ACP Connector/official-kernel/login.d.ts +1 -0
- package/dist/ACP Connector/official-kernel/login.js +73 -0
- package/dist/ACP Connector/official-kernel/login.js.map +1 -0
- package/dist/ACP Connector/official-kernel/mcp-rewrite.d.ts +1 -0
- package/dist/ACP Connector/official-kernel/mcp-rewrite.js +39 -0
- package/dist/ACP Connector/official-kernel/mcp-rewrite.js.map +1 -0
- package/dist/ACP Connector/official-kernel/mode-map.d.ts +5 -0
- package/dist/ACP Connector/official-kernel/mode-map.js +35 -0
- package/dist/ACP Connector/official-kernel/mode-map.js.map +1 -0
- package/dist/ACP Connector/official-kernel/ndjson.d.ts +3 -0
- package/dist/ACP Connector/official-kernel/ndjson.js +29 -0
- package/dist/ACP Connector/official-kernel/ndjson.js.map +1 -0
- package/dist/ACP Connector/official-kernel/proxy.d.ts +14 -0
- package/dist/ACP Connector/official-kernel/proxy.js +197 -0
- package/dist/ACP Connector/official-kernel/proxy.js.map +1 -0
- package/dist/ACP Connector/official-kernel/run.d.ts +8 -0
- package/dist/ACP Connector/official-kernel/run.js +22 -0
- package/dist/ACP Connector/official-kernel/run.js.map +1 -0
- package/dist/ACP Connector/official-kernel/spawn.d.ts +5 -0
- package/dist/ACP Connector/official-kernel/spawn.js +33 -0
- package/dist/ACP Connector/official-kernel/spawn.js.map +1 -0
- package/dist/Admission Controller/controller.d.ts +338 -0
- package/dist/Admission Controller/controller.js +2089 -0
- package/dist/Admission Controller/controller.js.map +1 -0
- package/dist/Admission Controller/key-derivation.d.ts +24 -0
- package/dist/Admission Controller/key-derivation.js +141 -0
- package/dist/Admission Controller/key-derivation.js.map +1 -0
- package/dist/Admission Controller/key-store.d.ts +8 -0
- package/dist/Admission Controller/key-store.js +280 -0
- package/dist/Admission Controller/key-store.js.map +1 -0
- package/dist/Admission Controller/policy-limits.d.ts +6 -0
- package/dist/Admission Controller/policy-limits.js +11 -0
- package/dist/Admission Controller/policy-limits.js.map +1 -0
- package/dist/Admission Controller/process-evidence.d.ts +111 -0
- package/dist/Admission Controller/process-evidence.js +549 -0
- package/dist/Admission Controller/process-evidence.js.map +1 -0
- package/dist/Admission Controller/runtime-config.d.ts +23 -0
- package/dist/Admission Controller/runtime-config.js +86 -0
- package/dist/Admission Controller/runtime-config.js.map +1 -0
- package/dist/Admission Controller/schema.d.ts +12 -0
- package/dist/Admission Controller/schema.js +422 -0
- package/dist/Admission Controller/schema.js.map +1 -0
- package/package.json +72 -0
- package/scripts/prepare-admission-state-dir.mjs +62 -0
- package/scripts/prepare-official-kernel-compat.mjs +387 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Tested production default. Override with AGY_ACP_ADMISSION_MAX_ACTIVE_TURNS. */
|
|
2
|
+
export const DEFAULT_ADMISSION_ACTIVE_TURNS = 8;
|
|
3
|
+
/** Tested production default. Override with AGY_ACP_ADMISSION_MAX_CONCURRENT_STARTS. */
|
|
4
|
+
export const DEFAULT_ADMISSION_CONCURRENT_STARTS = 8;
|
|
5
|
+
export function isAllowedAdmissionActiveTurns(value) {
|
|
6
|
+
return Number.isSafeInteger(value) && value >= 1;
|
|
7
|
+
}
|
|
8
|
+
export function isAllowedAdmissionConcurrentStarts(value) {
|
|
9
|
+
return Number.isSafeInteger(value) && value >= 1;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=policy-limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-limits.js","sourceRoot":"","sources":["../../Admission Controller/policy-limits.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,CAAC;AAChD,wFAAwF;AACxF,MAAM,CAAC,MAAM,mCAAmC,GAAG,CAAC,CAAC;AAErD,MAAM,UAAU,6BAA6B,CAAC,KAAa;IACzD,OAAO,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,kCAAkC,CAAC,KAAa;IAC9D,OAAO,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export interface LinuxProcessEvidenceReaders {
|
|
2
|
+
readFile(path: string): string;
|
|
3
|
+
readLink(path: string): string;
|
|
4
|
+
}
|
|
5
|
+
/** Immutable Linux evidence used to distinguish a PID from a later PID reuse. */
|
|
6
|
+
export interface LinuxProcessIdentity {
|
|
7
|
+
readonly bootId: string;
|
|
8
|
+
readonly pid: number;
|
|
9
|
+
readonly startTimeTicks: string;
|
|
10
|
+
readonly pidNamespaceInode: number;
|
|
11
|
+
readonly ppid: number;
|
|
12
|
+
readonly pgrp: number;
|
|
13
|
+
readonly session: number;
|
|
14
|
+
}
|
|
15
|
+
export interface LinuxProcessStatEvidence {
|
|
16
|
+
readonly pid: number;
|
|
17
|
+
readonly startTimeTicks: string;
|
|
18
|
+
readonly ppid: number;
|
|
19
|
+
readonly pgrp: number;
|
|
20
|
+
readonly session: number;
|
|
21
|
+
}
|
|
22
|
+
/** The durable recovery claim fields that a process proof must bind exactly. */
|
|
23
|
+
export interface LinuxPreDispatchProofBinding {
|
|
24
|
+
readonly requestId: string;
|
|
25
|
+
readonly leaseId: string;
|
|
26
|
+
readonly leaseGeneration: number;
|
|
27
|
+
readonly recoveryGeneration: number;
|
|
28
|
+
readonly claimantInstanceId: string;
|
|
29
|
+
}
|
|
30
|
+
/** The immutable local process record observed for a pre-dispatch proof. */
|
|
31
|
+
export interface LinuxPreDispatchProofSubject {
|
|
32
|
+
readonly ownerInstanceId: string;
|
|
33
|
+
readonly connectorCreatedAt: string;
|
|
34
|
+
readonly connector: LinuxProcessIdentity;
|
|
35
|
+
readonly child: LinuxProcessIdentity;
|
|
36
|
+
readonly promptChannel: "stdin" | "pty";
|
|
37
|
+
}
|
|
38
|
+
/** The signed payload for the only automatic pre-dispatch recovery path. */
|
|
39
|
+
export interface LinuxPreDispatchProofPayload {
|
|
40
|
+
readonly binding: LinuxPreDispatchProofBinding;
|
|
41
|
+
readonly subject: LinuxPreDispatchProofSubject;
|
|
42
|
+
readonly observedAt: number;
|
|
43
|
+
readonly owner: "gone";
|
|
44
|
+
readonly root: "gone";
|
|
45
|
+
readonly residue: "empty";
|
|
46
|
+
}
|
|
47
|
+
/** A proof producer needs only signing capability, never controller mutation. */
|
|
48
|
+
export interface LinuxPreDispatchProofSigner {
|
|
49
|
+
signPreDispatchProof(payload: LinuxPreDispatchProofPayload): string;
|
|
50
|
+
}
|
|
51
|
+
/** A recovery coordinator needs only verification capability. */
|
|
52
|
+
export interface LinuxPreDispatchProofVerifier {
|
|
53
|
+
verifyPreDispatchProof(payload: LinuxPreDispatchProofPayload, proofHmac: string): boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface LinuxPreDispatchProofAuthority extends LinuxPreDispatchProofSigner, LinuxPreDispatchProofVerifier {
|
|
56
|
+
close(): void;
|
|
57
|
+
}
|
|
58
|
+
/** An HMAC-authenticated, claim-bound observation of an empty pre-dispatch process tree. */
|
|
59
|
+
export interface LinuxPreDispatchTerminationProof extends LinuxPreDispatchProofPayload {
|
|
60
|
+
readonly proofHmac: string;
|
|
61
|
+
}
|
|
62
|
+
/** A conservative observation of a persisted process identity. */
|
|
63
|
+
export type LinuxProcessIdentityState = "same" | "gone" | "pid_reused" | "unverifiable";
|
|
64
|
+
export type LinuxProcessGroupState = "empty" | "present" | "unverifiable";
|
|
65
|
+
export declare class ProcessEvidenceError extends Error {
|
|
66
|
+
readonly kind: "process_gone" | "unverifiable";
|
|
67
|
+
constructor(message: string, kind?: "process_gone" | "unverifiable");
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The production readers are explicit so callers can inject a deterministic
|
|
71
|
+
* filesystem view in tests and never inspect a real process by accident.
|
|
72
|
+
*/
|
|
73
|
+
export declare const nativeLinuxProcessEvidenceReaders: LinuxProcessEvidenceReaders;
|
|
74
|
+
/**
|
|
75
|
+
* Construct the local proof authority from a caller-owned 32-byte key. The
|
|
76
|
+
* adapter receives only the signing half; the coordinator receives only the
|
|
77
|
+
* verification half, so process inspection cannot mutate durable state.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createLinuxPreDispatchProofAuthority(key: Buffer): LinuxPreDispatchProofAuthority;
|
|
80
|
+
/** Sign a normalized proof payload without giving the caller access to the signing key. */
|
|
81
|
+
export declare function issueLinuxPreDispatchTerminationProof(value: unknown, signer: unknown): LinuxPreDispatchTerminationProof;
|
|
82
|
+
/** Return a copied proof only when its shape and HMAC both verify. */
|
|
83
|
+
export declare function verifyLinuxPreDispatchTerminationProof(value: unknown, verifier: unknown): LinuxPreDispatchTerminationProof | null;
|
|
84
|
+
/** Validate and copy a claim binding before it becomes part of a signed proof. */
|
|
85
|
+
export declare function validateLinuxPreDispatchProofBinding(value: unknown): LinuxPreDispatchProofBinding | null;
|
|
86
|
+
/** Capture the Linux-only evidence needed to recognize one process instance. */
|
|
87
|
+
export declare function captureLinuxProcessIdentity(pid: number, readers?: LinuxProcessEvidenceReaders): LinuxProcessIdentity;
|
|
88
|
+
/**
|
|
89
|
+
* Re-observe a persisted process without reducing a reused PID to a liveness
|
|
90
|
+
* check. Only a direct ENOENT from the process-specific procfs paths is gone;
|
|
91
|
+
* malformed or inaccessible evidence remains unverifiable.
|
|
92
|
+
*/
|
|
93
|
+
export declare function observeLinuxProcessIdentity(expected: unknown, readers?: LinuxProcessEvidenceReaders): LinuxProcessIdentityState;
|
|
94
|
+
/**
|
|
95
|
+
* Inspect process-group residue without accepting PID existence as identity.
|
|
96
|
+
* Only a fully enumerable process list with no matching boot/ns/pgrp/session
|
|
97
|
+
* residue is empty; malformed rows and unreadable procfs fail closed.
|
|
98
|
+
*/
|
|
99
|
+
export declare function inspectLinuxProcessGroup(expected: unknown, processIds: readonly number[], readers?: LinuxProcessEvidenceReaders): LinuxProcessGroupState;
|
|
100
|
+
/**
|
|
101
|
+
* Parse the evidence-bearing fields in /proc/<pid>/stat.
|
|
102
|
+
*
|
|
103
|
+
* The comm field is not escaped by procfs, so its final parenthesis is the
|
|
104
|
+
* only safe delimiter: comm itself may contain both spaces and parentheses.
|
|
105
|
+
*/
|
|
106
|
+
export declare function parseLinuxProcessStat(value: unknown): LinuxProcessStatEvidence;
|
|
107
|
+
/**
|
|
108
|
+
* Return false rather than throwing when persisted evidence is incomplete,
|
|
109
|
+
* malformed, or differs in any identity field.
|
|
110
|
+
*/
|
|
111
|
+
export declare function isSameLinuxProcessIdentity(expected: unknown, observed: unknown): boolean;
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { readFileSync, readlinkSync } from "node:fs";
|
|
3
|
+
const BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id";
|
|
4
|
+
const MAX_PID = 2_147_483_647;
|
|
5
|
+
const MAX_PID_NAMESPACE_INODE = 4_294_967_295;
|
|
6
|
+
const MAX_START_TIME_TICKS = 18446744073709551615n;
|
|
7
|
+
const BOOT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
|
+
const POSITIVE_DECIMAL_PATTERN = /^[1-9][0-9]*$/;
|
|
9
|
+
const SIGNED_DECIMAL_PATTERN = /^-?[0-9]+$/;
|
|
10
|
+
const PROCESS_STATES = new Set(["R", "S", "D", "Z", "T", "t", "W", "X", "x", "K", "P", "I"]);
|
|
11
|
+
const MAX_IDENTIFIER_LENGTH = 256;
|
|
12
|
+
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
13
|
+
const ISO_UTC_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
14
|
+
const HMAC_PATTERN = /^[0-9a-f]{64}$/;
|
|
15
|
+
const PRE_DISPATCH_PROOF_DOMAIN = "paseo-agy-acp/linux-pre-dispatch-proof/v1";
|
|
16
|
+
export class ProcessEvidenceError extends Error {
|
|
17
|
+
kind;
|
|
18
|
+
constructor(message, kind = "unverifiable") {
|
|
19
|
+
super(`process evidence error: ${message}`);
|
|
20
|
+
this.name = "ProcessEvidenceError";
|
|
21
|
+
this.kind = kind;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The production readers are explicit so callers can inject a deterministic
|
|
26
|
+
* filesystem view in tests and never inspect a real process by accident.
|
|
27
|
+
*/
|
|
28
|
+
export const nativeLinuxProcessEvidenceReaders = Object.freeze({
|
|
29
|
+
readFile(path) {
|
|
30
|
+
return readFileSync(path, "utf8");
|
|
31
|
+
},
|
|
32
|
+
readLink(path) {
|
|
33
|
+
return readlinkSync(path, "utf8");
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Construct the local proof authority from a caller-owned 32-byte key. The
|
|
38
|
+
* adapter receives only the signing half; the coordinator receives only the
|
|
39
|
+
* verification half, so process inspection cannot mutate durable state.
|
|
40
|
+
*/
|
|
41
|
+
export function createLinuxPreDispatchProofAuthority(key) {
|
|
42
|
+
if (!Buffer.isBuffer(key) || key.length !== 32) {
|
|
43
|
+
throw new ProcessEvidenceError("pre-dispatch proof key must be exactly 32 bytes");
|
|
44
|
+
}
|
|
45
|
+
const secret = Buffer.from(key);
|
|
46
|
+
let closed = false;
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
signPreDispatchProof(value) {
|
|
49
|
+
if (closed)
|
|
50
|
+
throw new ProcessEvidenceError("pre-dispatch proof authority is closed");
|
|
51
|
+
const payload = normalizeLinuxPreDispatchProofPayload(value);
|
|
52
|
+
if (payload === null)
|
|
53
|
+
throw new ProcessEvidenceError("pre-dispatch proof payload is invalid");
|
|
54
|
+
return preDispatchProofHmac(secret, payload);
|
|
55
|
+
},
|
|
56
|
+
verifyPreDispatchProof(value, proofHmac) {
|
|
57
|
+
if (closed)
|
|
58
|
+
return false;
|
|
59
|
+
const payload = normalizeLinuxPreDispatchProofPayload(value);
|
|
60
|
+
if (payload === null || !isHmac(proofHmac))
|
|
61
|
+
return false;
|
|
62
|
+
return sameHmac(preDispatchProofHmac(secret, payload), proofHmac);
|
|
63
|
+
},
|
|
64
|
+
close() {
|
|
65
|
+
if (closed)
|
|
66
|
+
return;
|
|
67
|
+
closed = true;
|
|
68
|
+
secret.fill(0);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Sign a normalized proof payload without giving the caller access to the signing key. */
|
|
73
|
+
export function issueLinuxPreDispatchTerminationProof(value, signer) {
|
|
74
|
+
const payload = normalizeLinuxPreDispatchProofPayload(value);
|
|
75
|
+
if (payload === null || !isProofSigner(signer)) {
|
|
76
|
+
throw new ProcessEvidenceError("pre-dispatch proof inputs are invalid");
|
|
77
|
+
}
|
|
78
|
+
let proofHmac;
|
|
79
|
+
try {
|
|
80
|
+
proofHmac = signer.signPreDispatchProof(payload);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw new ProcessEvidenceError("pre-dispatch proof could not be signed");
|
|
84
|
+
}
|
|
85
|
+
if (!isHmac(proofHmac))
|
|
86
|
+
throw new ProcessEvidenceError("pre-dispatch proof signature is invalid");
|
|
87
|
+
return Object.freeze({ ...payload, proofHmac });
|
|
88
|
+
}
|
|
89
|
+
/** Return a copied proof only when its shape and HMAC both verify. */
|
|
90
|
+
export function verifyLinuxPreDispatchTerminationProof(value, verifier) {
|
|
91
|
+
const proof = normalizeLinuxPreDispatchTerminationProof(value);
|
|
92
|
+
if (proof === null || !isProofVerifier(verifier))
|
|
93
|
+
return null;
|
|
94
|
+
try {
|
|
95
|
+
return verifier.verifyPreDispatchProof(proofPayload(proof), proof.proofHmac) === true ? proof : null;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Validate and copy a claim binding before it becomes part of a signed proof. */
|
|
102
|
+
export function validateLinuxPreDispatchProofBinding(value) {
|
|
103
|
+
const record = exactRecord(value, [
|
|
104
|
+
"requestId",
|
|
105
|
+
"leaseId",
|
|
106
|
+
"leaseGeneration",
|
|
107
|
+
"recoveryGeneration",
|
|
108
|
+
"claimantInstanceId"
|
|
109
|
+
]);
|
|
110
|
+
if (record === null ||
|
|
111
|
+
!isIdentifier(record.requestId) ||
|
|
112
|
+
!isIdentifier(record.leaseId) ||
|
|
113
|
+
!isPositiveSafeInteger(record.leaseGeneration) ||
|
|
114
|
+
!isPositiveSafeInteger(record.recoveryGeneration) ||
|
|
115
|
+
!isIdentifier(record.claimantInstanceId)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
requestId: record.requestId,
|
|
120
|
+
leaseId: record.leaseId,
|
|
121
|
+
leaseGeneration: record.leaseGeneration,
|
|
122
|
+
recoveryGeneration: record.recoveryGeneration,
|
|
123
|
+
claimantInstanceId: record.claimantInstanceId
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** Capture the Linux-only evidence needed to recognize one process instance. */
|
|
127
|
+
export function captureLinuxProcessIdentity(pid, readers = nativeLinuxProcessEvidenceReaders) {
|
|
128
|
+
const expectedPid = requirePositiveNumber(pid, "pid", MAX_PID);
|
|
129
|
+
requireReaders(readers);
|
|
130
|
+
const bootId = parseBootId(readEvidence(() => readers.readFile(BOOT_ID_PATH)));
|
|
131
|
+
const stat = parseLinuxProcessStat(readEvidence(() => readers.readFile(`/proc/${expectedPid}/stat`), "process"));
|
|
132
|
+
if (stat.pid !== expectedPid)
|
|
133
|
+
throw new ProcessEvidenceError("stat PID does not match the requested PID");
|
|
134
|
+
const pidNamespaceInode = parsePidNamespaceInode(readEvidence(() => readers.readLink(`/proc/${expectedPid}/ns/pid`), "process"));
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
bootId,
|
|
137
|
+
pid: expectedPid,
|
|
138
|
+
startTimeTicks: stat.startTimeTicks,
|
|
139
|
+
pidNamespaceInode,
|
|
140
|
+
ppid: stat.ppid,
|
|
141
|
+
pgrp: stat.pgrp,
|
|
142
|
+
session: stat.session
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Re-observe a persisted process without reducing a reused PID to a liveness
|
|
147
|
+
* check. Only a direct ENOENT from the process-specific procfs paths is gone;
|
|
148
|
+
* malformed or inaccessible evidence remains unverifiable.
|
|
149
|
+
*/
|
|
150
|
+
export function observeLinuxProcessIdentity(expected, readers = nativeLinuxProcessEvidenceReaders) {
|
|
151
|
+
const normalizedExpected = normalizeIdentity(expected);
|
|
152
|
+
if (normalizedExpected === null)
|
|
153
|
+
return "unverifiable";
|
|
154
|
+
try {
|
|
155
|
+
const observed = captureLinuxProcessIdentity(normalizedExpected.pid, readers);
|
|
156
|
+
return isSameLinuxProcessIdentity(normalizedExpected, observed) ? "same" : "pid_reused";
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return error instanceof ProcessEvidenceError && error.kind === "process_gone" ? "gone" : "unverifiable";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Inspect process-group residue without accepting PID existence as identity.
|
|
164
|
+
* Only a fully enumerable process list with no matching boot/ns/pgrp/session
|
|
165
|
+
* residue is empty; malformed rows and unreadable procfs fail closed.
|
|
166
|
+
*/
|
|
167
|
+
export function inspectLinuxProcessGroup(expected, processIds, readers = nativeLinuxProcessEvidenceReaders) {
|
|
168
|
+
const normalizedExpected = normalizeIdentity(expected);
|
|
169
|
+
if (normalizedExpected === null || !Array.isArray(processIds))
|
|
170
|
+
return "unverifiable";
|
|
171
|
+
let unverifiable = false;
|
|
172
|
+
for (const pid of processIds) {
|
|
173
|
+
if (!isPositiveSafeInteger(pid) || pid > MAX_PID) {
|
|
174
|
+
unverifiable = true;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
let stat;
|
|
178
|
+
try {
|
|
179
|
+
stat = parseLinuxProcessStat(readEvidence(() => readers.readFile(`/proc/${pid}/stat`), "process"));
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (error instanceof ProcessEvidenceError && error.kind === "process_gone")
|
|
183
|
+
continue;
|
|
184
|
+
unverifiable = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (stat.pid !== pid) {
|
|
188
|
+
unverifiable = true;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (stat.pgrp !== normalizedExpected.pgrp || stat.session !== normalizedExpected.session) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
let observed;
|
|
195
|
+
try {
|
|
196
|
+
observed = captureLinuxProcessIdentity(pid, readers);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (error instanceof ProcessEvidenceError && error.kind === "process_gone")
|
|
200
|
+
continue;
|
|
201
|
+
unverifiable = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (observed.bootId === normalizedExpected.bootId &&
|
|
205
|
+
observed.pidNamespaceInode === normalizedExpected.pidNamespaceInode &&
|
|
206
|
+
observed.pgrp === normalizedExpected.pgrp &&
|
|
207
|
+
observed.session === normalizedExpected.session) {
|
|
208
|
+
return "present";
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return unverifiable ? "unverifiable" : "empty";
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Parse the evidence-bearing fields in /proc/<pid>/stat.
|
|
215
|
+
*
|
|
216
|
+
* The comm field is not escaped by procfs, so its final parenthesis is the
|
|
217
|
+
* only safe delimiter: comm itself may contain both spaces and parentheses.
|
|
218
|
+
*/
|
|
219
|
+
export function parseLinuxProcessStat(value) {
|
|
220
|
+
const stat = requireSingleLine(value, "stat");
|
|
221
|
+
const openingParenthesis = stat.indexOf(" (");
|
|
222
|
+
const closingParenthesis = stat.lastIndexOf(")");
|
|
223
|
+
if (openingParenthesis <= 0 || closingParenthesis < openingParenthesis + 2) {
|
|
224
|
+
throw new ProcessEvidenceError("stat record has an invalid comm delimiter");
|
|
225
|
+
}
|
|
226
|
+
const pid = parsePositiveDecimal(stat.slice(0, openingParenthesis), "stat PID", MAX_PID);
|
|
227
|
+
const trailingFields = stat.slice(closingParenthesis + 1);
|
|
228
|
+
if (!trailingFields.startsWith(" "))
|
|
229
|
+
throw new ProcessEvidenceError("stat record has no process state");
|
|
230
|
+
const fields = trailingFields.trim().split(/\s+/);
|
|
231
|
+
if (fields.length < 20 || !PROCESS_STATES.has(fields[0])) {
|
|
232
|
+
throw new ProcessEvidenceError("stat record is incomplete or has an invalid process state");
|
|
233
|
+
}
|
|
234
|
+
if (fields.slice(1).some((field) => !SIGNED_DECIMAL_PATTERN.test(field))) {
|
|
235
|
+
throw new ProcessEvidenceError("stat record has malformed numeric fields");
|
|
236
|
+
}
|
|
237
|
+
return Object.freeze({
|
|
238
|
+
pid,
|
|
239
|
+
ppid: parseNonNegativeDecimal(fields[1], "parent PID", MAX_PID),
|
|
240
|
+
pgrp: parseNonNegativeDecimal(fields[2], "process group", MAX_PID),
|
|
241
|
+
session: parseNonNegativeDecimal(fields[3], "session", MAX_PID),
|
|
242
|
+
startTimeTicks: parseStartTimeTicks(fields[19])
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Return false rather than throwing when persisted evidence is incomplete,
|
|
247
|
+
* malformed, or differs in any identity field.
|
|
248
|
+
*/
|
|
249
|
+
export function isSameLinuxProcessIdentity(expected, observed) {
|
|
250
|
+
const left = normalizeIdentity(expected);
|
|
251
|
+
const right = normalizeIdentity(observed);
|
|
252
|
+
if (left === null || right === null)
|
|
253
|
+
return false;
|
|
254
|
+
return (left.bootId === right.bootId &&
|
|
255
|
+
left.pid === right.pid &&
|
|
256
|
+
left.startTimeTicks === right.startTimeTicks &&
|
|
257
|
+
left.pidNamespaceInode === right.pidNamespaceInode &&
|
|
258
|
+
left.ppid === right.ppid &&
|
|
259
|
+
left.pgrp === right.pgrp &&
|
|
260
|
+
left.session === right.session);
|
|
261
|
+
}
|
|
262
|
+
function normalizeLinuxPreDispatchProofPayload(value) {
|
|
263
|
+
const record = exactRecord(value, ["binding", "subject", "observedAt", "owner", "root", "residue"]);
|
|
264
|
+
if (record === null ||
|
|
265
|
+
record.owner !== "gone" ||
|
|
266
|
+
record.root !== "gone" ||
|
|
267
|
+
record.residue !== "empty" ||
|
|
268
|
+
!isTimestamp(record.observedAt)) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const binding = validateLinuxPreDispatchProofBinding(record.binding);
|
|
272
|
+
const subject = normalizeLinuxPreDispatchProofSubject(record.subject);
|
|
273
|
+
return binding === null || subject === null
|
|
274
|
+
? null
|
|
275
|
+
: Object.freeze({
|
|
276
|
+
binding,
|
|
277
|
+
subject,
|
|
278
|
+
observedAt: record.observedAt,
|
|
279
|
+
owner: "gone",
|
|
280
|
+
root: "gone",
|
|
281
|
+
residue: "empty"
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function normalizeLinuxPreDispatchTerminationProof(value) {
|
|
285
|
+
const record = exactRecord(value, ["binding", "subject", "observedAt", "owner", "root", "residue", "proofHmac"]);
|
|
286
|
+
if (record === null || !isHmac(record.proofHmac))
|
|
287
|
+
return null;
|
|
288
|
+
const payload = normalizeLinuxPreDispatchProofPayload({
|
|
289
|
+
binding: record.binding,
|
|
290
|
+
subject: record.subject,
|
|
291
|
+
observedAt: record.observedAt,
|
|
292
|
+
owner: record.owner,
|
|
293
|
+
root: record.root,
|
|
294
|
+
residue: record.residue
|
|
295
|
+
});
|
|
296
|
+
return payload === null ? null : Object.freeze({ ...payload, proofHmac: record.proofHmac });
|
|
297
|
+
}
|
|
298
|
+
function normalizeLinuxPreDispatchProofSubject(value) {
|
|
299
|
+
const record = exactRecord(value, ["ownerInstanceId", "connectorCreatedAt", "connector", "child", "promptChannel"]);
|
|
300
|
+
if (record === null ||
|
|
301
|
+
!isIdentifier(record.ownerInstanceId) ||
|
|
302
|
+
!isCanonicalTimestamp(record.connectorCreatedAt) ||
|
|
303
|
+
(record.promptChannel !== "stdin" && record.promptChannel !== "pty")) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const connector = normalizeIdentity(record.connector);
|
|
307
|
+
const child = normalizeIdentity(record.child);
|
|
308
|
+
return connector === null || child === null
|
|
309
|
+
? null
|
|
310
|
+
: Object.freeze({
|
|
311
|
+
ownerInstanceId: record.ownerInstanceId,
|
|
312
|
+
connectorCreatedAt: record.connectorCreatedAt,
|
|
313
|
+
connector: Object.freeze(connector),
|
|
314
|
+
child: Object.freeze(child),
|
|
315
|
+
promptChannel: record.promptChannel
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function proofPayload(proof) {
|
|
319
|
+
return Object.freeze({
|
|
320
|
+
binding: proof.binding,
|
|
321
|
+
subject: proof.subject,
|
|
322
|
+
observedAt: proof.observedAt,
|
|
323
|
+
owner: proof.owner,
|
|
324
|
+
root: proof.root,
|
|
325
|
+
residue: proof.residue
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function preDispatchProofHmac(key, payload) {
|
|
329
|
+
return createHmac("sha256", key).update(canonicalPreDispatchProofPayload(payload), "utf8").digest("hex");
|
|
330
|
+
}
|
|
331
|
+
function canonicalPreDispatchProofPayload(payload) {
|
|
332
|
+
const binding = payload.binding;
|
|
333
|
+
const subject = payload.subject;
|
|
334
|
+
return JSON.stringify([
|
|
335
|
+
PRE_DISPATCH_PROOF_DOMAIN,
|
|
336
|
+
[
|
|
337
|
+
binding.requestId,
|
|
338
|
+
binding.leaseId,
|
|
339
|
+
binding.leaseGeneration,
|
|
340
|
+
binding.recoveryGeneration,
|
|
341
|
+
binding.claimantInstanceId
|
|
342
|
+
],
|
|
343
|
+
[
|
|
344
|
+
subject.ownerInstanceId,
|
|
345
|
+
subject.connectorCreatedAt,
|
|
346
|
+
subject.promptChannel,
|
|
347
|
+
processIdentityValues(subject.connector),
|
|
348
|
+
processIdentityValues(subject.child)
|
|
349
|
+
],
|
|
350
|
+
payload.observedAt,
|
|
351
|
+
payload.owner,
|
|
352
|
+
payload.root,
|
|
353
|
+
payload.residue
|
|
354
|
+
]);
|
|
355
|
+
}
|
|
356
|
+
function processIdentityValues(identity) {
|
|
357
|
+
return [
|
|
358
|
+
identity.bootId,
|
|
359
|
+
identity.pid,
|
|
360
|
+
identity.startTimeTicks,
|
|
361
|
+
identity.pidNamespaceInode,
|
|
362
|
+
identity.ppid,
|
|
363
|
+
identity.pgrp,
|
|
364
|
+
identity.session
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
function isProofSigner(value) {
|
|
368
|
+
return hasMethod(value, "signPreDispatchProof");
|
|
369
|
+
}
|
|
370
|
+
function isProofVerifier(value) {
|
|
371
|
+
return hasMethod(value, "verifyPreDispatchProof");
|
|
372
|
+
}
|
|
373
|
+
function hasMethod(value, name) {
|
|
374
|
+
if (typeof value !== "object" || value === null)
|
|
375
|
+
return false;
|
|
376
|
+
try {
|
|
377
|
+
return typeof value[name] === "function";
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function exactRecord(value, expectedKeys) {
|
|
384
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
385
|
+
return null;
|
|
386
|
+
try {
|
|
387
|
+
const prototype = Object.getPrototypeOf(value);
|
|
388
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
389
|
+
return null;
|
|
390
|
+
const names = Object.getOwnPropertyNames(value);
|
|
391
|
+
if (names.length !== expectedKeys.length || Object.getOwnPropertySymbols(value).length !== 0)
|
|
392
|
+
return null;
|
|
393
|
+
const record = Object.create(null);
|
|
394
|
+
for (const key of expectedKeys) {
|
|
395
|
+
if (!names.includes(key))
|
|
396
|
+
return null;
|
|
397
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
398
|
+
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor))
|
|
399
|
+
return null;
|
|
400
|
+
record[key] = descriptor.value;
|
|
401
|
+
}
|
|
402
|
+
return record;
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function isIdentifier(value) {
|
|
409
|
+
return (typeof value === "string" &&
|
|
410
|
+
value.length > 0 &&
|
|
411
|
+
value.length <= MAX_IDENTIFIER_LENGTH &&
|
|
412
|
+
value.trim() === value &&
|
|
413
|
+
!CONTROL_CHARACTER_PATTERN.test(value));
|
|
414
|
+
}
|
|
415
|
+
function isPositiveSafeInteger(value) {
|
|
416
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
417
|
+
}
|
|
418
|
+
function isTimestamp(value) {
|
|
419
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
420
|
+
}
|
|
421
|
+
function isCanonicalTimestamp(value) {
|
|
422
|
+
if (typeof value !== "string" || !ISO_UTC_TIMESTAMP_PATTERN.test(value))
|
|
423
|
+
return false;
|
|
424
|
+
try {
|
|
425
|
+
return new Date(value).toISOString() === value;
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function isHmac(value) {
|
|
432
|
+
return typeof value === "string" && HMAC_PATTERN.test(value);
|
|
433
|
+
}
|
|
434
|
+
function sameHmac(expected, candidate) {
|
|
435
|
+
if (!isHmac(expected) || !isHmac(candidate))
|
|
436
|
+
return false;
|
|
437
|
+
const left = Buffer.from(expected, "hex");
|
|
438
|
+
const right = Buffer.from(candidate, "hex");
|
|
439
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
440
|
+
}
|
|
441
|
+
function normalizeIdentity(value) {
|
|
442
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
443
|
+
return null;
|
|
444
|
+
const identity = value;
|
|
445
|
+
try {
|
|
446
|
+
return {
|
|
447
|
+
bootId: parseBootId(identity.bootId),
|
|
448
|
+
pid: requirePositiveNumber(identity.pid, "pid", MAX_PID),
|
|
449
|
+
startTimeTicks: parseStartTimeTicks(identity.startTimeTicks),
|
|
450
|
+
pidNamespaceInode: requirePositiveNumber(identity.pidNamespaceInode, "PID namespace inode", MAX_PID_NAMESPACE_INODE),
|
|
451
|
+
ppid: requirePositiveNumber(identity.ppid, "parent PID", MAX_PID),
|
|
452
|
+
pgrp: requirePositiveNumber(identity.pgrp, "process group", MAX_PID),
|
|
453
|
+
session: requirePositiveNumber(identity.session, "session", MAX_PID)
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function requireReaders(readers) {
|
|
461
|
+
if (typeof readers !== "object" ||
|
|
462
|
+
readers === null ||
|
|
463
|
+
typeof readers.readFile !== "function" ||
|
|
464
|
+
typeof readers.readLink !== "function") {
|
|
465
|
+
throw new ProcessEvidenceError("filesystem readers are invalid");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function readEvidence(read, subject = "system") {
|
|
469
|
+
try {
|
|
470
|
+
const value = read();
|
|
471
|
+
if (typeof value !== "string")
|
|
472
|
+
throw new Error("reader did not return text");
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
if (subject === "process" && isMissingProcessEvidence(error)) {
|
|
477
|
+
throw new ProcessEvidenceError("process evidence is absent", "process_gone");
|
|
478
|
+
}
|
|
479
|
+
throw new ProcessEvidenceError("required process evidence is unavailable");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function isMissingProcessEvidence(error) {
|
|
483
|
+
if (typeof error !== "object" || error === null)
|
|
484
|
+
return false;
|
|
485
|
+
try {
|
|
486
|
+
return error.code === "ENOENT";
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function parseBootId(value) {
|
|
493
|
+
const bootId = requireSingleLine(value, "boot ID");
|
|
494
|
+
if (!BOOT_ID_PATTERN.test(bootId) || /^0{8}-0{4}-0{4}-0{4}-0{12}$/i.test(bootId)) {
|
|
495
|
+
throw new ProcessEvidenceError("boot ID is malformed");
|
|
496
|
+
}
|
|
497
|
+
return bootId.toLowerCase();
|
|
498
|
+
}
|
|
499
|
+
function parsePidNamespaceInode(value) {
|
|
500
|
+
const target = requireSingleLine(value, "PID namespace");
|
|
501
|
+
const match = /^pid:\[([1-9][0-9]*)\]$/.exec(target);
|
|
502
|
+
if (match === null)
|
|
503
|
+
throw new ProcessEvidenceError("PID namespace is malformed");
|
|
504
|
+
return parsePositiveDecimal(match[1], "PID namespace inode", MAX_PID_NAMESPACE_INODE);
|
|
505
|
+
}
|
|
506
|
+
function parseStartTimeTicks(value) {
|
|
507
|
+
if (typeof value !== "string" || !POSITIVE_DECIMAL_PATTERN.test(value)) {
|
|
508
|
+
throw new ProcessEvidenceError("start time is malformed");
|
|
509
|
+
}
|
|
510
|
+
const ticks = BigInt(value);
|
|
511
|
+
if (ticks > MAX_START_TIME_TICKS)
|
|
512
|
+
throw new ProcessEvidenceError("start time is out of range");
|
|
513
|
+
return value;
|
|
514
|
+
}
|
|
515
|
+
function parsePositiveDecimal(value, name, maximum) {
|
|
516
|
+
if (!POSITIVE_DECIMAL_PATTERN.test(value))
|
|
517
|
+
throw new ProcessEvidenceError(`${name} is malformed`);
|
|
518
|
+
const parsed = Number(value);
|
|
519
|
+
if (!Number.isSafeInteger(parsed) || parsed > maximum) {
|
|
520
|
+
throw new ProcessEvidenceError(`${name} is out of range`);
|
|
521
|
+
}
|
|
522
|
+
return parsed;
|
|
523
|
+
}
|
|
524
|
+
function parseNonNegativeDecimal(value, name, maximum) {
|
|
525
|
+
if (!/^[0-9]+$/.test(value))
|
|
526
|
+
throw new ProcessEvidenceError(`${name} is malformed`);
|
|
527
|
+
const parsed = Number(value);
|
|
528
|
+
if (!Number.isSafeInteger(parsed) || parsed > maximum) {
|
|
529
|
+
throw new ProcessEvidenceError(`${name} is out of range`);
|
|
530
|
+
}
|
|
531
|
+
return parsed;
|
|
532
|
+
}
|
|
533
|
+
function requirePositiveNumber(value, name, maximum) {
|
|
534
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
535
|
+
throw new ProcessEvidenceError(`${name} must be a positive integer in range`);
|
|
536
|
+
}
|
|
537
|
+
return value;
|
|
538
|
+
}
|
|
539
|
+
function requireSingleLine(value, name) {
|
|
540
|
+
if (typeof value !== "string")
|
|
541
|
+
throw new ProcessEvidenceError(`${name} is unavailable`);
|
|
542
|
+
const withoutNewline = value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
543
|
+
const line = withoutNewline.endsWith("\r") ? withoutNewline.slice(0, -1) : withoutNewline;
|
|
544
|
+
if (line.length === 0 || line.includes("\n") || line.includes("\r")) {
|
|
545
|
+
throw new ProcessEvidenceError(`${name} is malformed`);
|
|
546
|
+
}
|
|
547
|
+
return line;
|
|
548
|
+
}
|
|
549
|
+
//# sourceMappingURL=process-evidence.js.map
|