mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 20:21:50 +01:00
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>
60 lines
2.2 KiB
TypeScript
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;
|
|
}
|