Files
PaperClipAI/packages/tailscale-https-broker/src/integers.ts
T
DottaandClaude Opus 4.8 ec7adcaf3d feat(runtime-exposure): least-privilege Tailscale HTTPS broker + shared contract (PAP-17051)
Foundation for opt-in `tailscale_https` exposure of managed branch runtimes,
implementing the PAP-17049 plan under the PAP-17050 threat-model verdict.

Broker (packages/tailscale-https-broker): root/dedicated-account host broker
exposing only idempotent list/expose/remove over a Unix socket. Implements the
six required verdict controls: SO_PEERCRED + UID/GID allowlist + unguessable
broker-issued lease handles (req #1); dedicated port allowlist + /proc listener
ownership/loopback proof before mutation (req #2); per-mutation transaction
(strict-parse serve JSON -> verify absence/exact-lease -> verify :443 -> one
fixed per-port op -> reread -> require only-intended-changed + :443 identical
-> atomic root-owned registry commit, quarantine-on-ambiguity) (req #3);
execve/shell=false argv built only from canonical integers + fixed tokens,
pinned CLI schema (req #4); versioned length-bounded strict-JSON protocol
(req #5); bounded append-only audit events (req #6). Deny-by-default for
Funnel/cert/Services/reset/set-config/path-handlers/:443/reserved ports.

Shared contract (packages/shared): single source of truth for exposure config,
lifecycle state (pending/ready/failed/cleanup_pending/removed), listener
ownership, and status, with strict zod validators and deterministic
app/HMR port-pair + URL derivation so DB/server/UI/runtime cannot drift.

Tests: broker 57/57, shared runtime-exposure 13/13, both typecheck clean.
Server/DB/UI lifecycle integration + docs land in follow-on commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 19:23:17 +00:00

60 lines
2.2 KiB
TypeScript

/**
* Canonical integer parsing for the broker. Commands and Serve mutations are
* generated only from integers that pass these checks, never from raw strings,
* URLs, or unvalidated JSON (PAP-17050 verdict requirement #4).
*/
/** Ports are constrained to the valid TCP range. */
export const MIN_PORT = 1;
export const MAX_PORT = 65535;
/**
* Assert a JSON-decoded value is a canonical, safe, in-range port number.
* Rejects non-numbers, non-integers, NaN/Infinity, negatives, and out-of-range
* values. Returns the number so call sites read as validated.
*/
export function assertCanonicalPort(value: unknown): number {
if (typeof value !== "number") {
throw new Error("port must be a JSON number");
}
if (!Number.isInteger(value)) {
throw new Error("port must be an integer");
}
if (value < MIN_PORT || value > MAX_PORT) {
throw new Error(`port out of range [${MIN_PORT}, ${MAX_PORT}]`);
}
return value;
}
/**
* Parse an integer from an untrusted STRING with strict canonical rules.
* Rejects: empty, whitespace, signs, leading zeros (non-canonical), decimals,
* exponents, thousands separators, Unicode digits, and anything that does not
* round-trip exactly back to its canonical decimal form. Used at any boundary
* where a numeric value could arrive as text.
*/
export function parseCanonicalIntegerString(raw: unknown): number {
if (typeof raw !== "string") {
throw new Error("expected a string integer");
}
// Only ASCII digits, at least one, no sign, no whitespace. `^[0-9]+$` with a
// JS regex still matches only ASCII 0-9 (it does not match Unicode digits
// unless the `u` + property-escape form is used), which is what we want.
if (!/^[0-9]+$/.test(raw)) {
throw new Error("integer contains non-canonical characters");
}
// Reject non-canonical leading zeros ("007", "00").
if (raw.length > 1 && raw[0] === "0") {
throw new Error("integer has non-canonical leading zero");
}
const parsed = Number(raw);
if (!Number.isSafeInteger(parsed)) {
throw new Error("integer overflow / not a safe integer");
}
// Round-trip guard against any residual ambiguity.
if (String(parsed) !== raw) {
throw new Error("integer did not round-trip canonically");
}
return parsed;
}