Files

62 lines
2.7 KiB
TypeScript

// lib/incarnation.ts — process-launch incarnation identity (DESIGN HAZARD).
//
// Mercer-measured hazard, carried by the NG-1 tasking: Pi documents
// PI_SESSION_ID as SESSION identity, not incarnation, and Mercer's
// environment carries no incarnation variable. Per L2-D01/D05 we therefore
// do NOT relabel PI_SESSION_ID as an incarnation — the R6 journal keys on
// the identity minted here.
//
// Resolution order (pure function of process state + injectable rng):
// 1. Launcher claim: MOSAIC_LAUNCH_INCARNATION, VALIDATED (NG-7 F2): the
// claim becomes a FILENAME COMPONENT in the journal and goal-state
// paths, so anything outside ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ is
// IGNORED (no path separators, no leading dot, no traversal shape) and
// resolution falls through — an invalid claim must never reach a path.
// 2. Process-global: globalThis.__mosaicCoreIncarnation — set on first
// mint. globalThis is per-process, so the identity survives extension
// /reload (module re-import) and session switches WITHIN the process.
// Deliberately NOT exported via process.env: child shells must not
// inherit a forgeable-looking claim.
// 3. Mint: rng() — DEFAULTS to crypto.randomUUID (NG-7 F1: the first cut
// required an injectable rng with no default, and both production
// call sites invoked incarnationIdentity() with no argument — a live
// startup throw the test suites never caught because every arm
// injected an rng or an explicit incarnationId).
//
// This function never reads PI_SESSION_ID. There is no code path here that
// could conflate session identity with launch identity.
import { randomUUID } from "node:crypto";
export const LAUNCH_CLAIM_ENV = "MOSAIC_LAUNCH_INCARNATION";
const GLOBAL_SLOT = "__mosaicCoreIncarnation";
/** A claim is usable only if it is a safe single path component. */
export const CLAIM_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
type Rng = () => string;
export function validClaim(claim: string | undefined): claim is string {
return typeof claim === "string" && CLAIM_RE.test(claim);
}
export function incarnationIdentity(rng: Rng = () => randomUUID()): string {
const claim = process.env[LAUNCH_CLAIM_ENV];
if (validClaim(claim)) {
return claim;
}
const g = globalThis as Record<string, unknown>;
const existing = g[GLOBAL_SLOT];
if (typeof existing === "string" && existing.length > 0) {
return existing;
}
const minted = rng();
g[GLOBAL_SLOT] = minted;
return minted;
}
/** Test/adapter helper: clear the process-global slot (never touches claims). */
export function resetIncarnationGlobal(): void {
delete (globalThis as Record<string, unknown>)[GLOBAL_SLOT];
}