/** * @module sensitive * @category Internal * * Internal mechanics for the sensitive-data foundation (#855 / epic #566). * The public surface (`sensitive(zodType)`) lives at `libs/act/src/sensitive.ts` * and re-exports `REDACTED` / `SHREDDED` from here; this module holds the * registry plus the helpers the orchestrator calls during commit, load, and * handler dispatch. * * - `_registry` — process-global `z.registry<{ sensitive: true }>()`. Public * `sensitive()` adds to it; the helpers in this module read it. * - `pii_fields(schema)` — walk a Zod schema's top-level shape, return the * keys marked via `sensitive(...)`. * - `pii_gate(event, fields, predicate, actor)` — produce the external * view: plaintext when authorized, `[REDACTED]` when not, `[SHREDDED]` * when the underlying pii column is null. * - `pii_strip(event, fields)` — remove sensitive keys entirely * before invoking projection / reaction handlers. * * @internal */ import { z } from "zod"; import type { Actor, Committed, Schemas } from "../types/index.js"; /** * Sentinel placed in `event.data[field]` when the caller isn't authorized to * see the sensitive field — either `.discloses(predicate)` returned `false`, * or no predicate was declared (framework default-deny). Recoverable: a * properly-authorized read returns the plaintext. * * Re-exported from `libs/act/src/sensitive.ts` as part of the public surface. */ export declare const REDACTED: "[REDACTED]"; /** * Sentinel placed in `event.data[field]` when the underlying PII payload has * been wiped via `Store.forget_pii(stream)` — the row's pii column is `NULL` * and the original plaintext is gone forever. Irrecoverable. * * Re-exported from `libs/act/src/sensitive.ts` as part of the public surface. */ export declare const SHREDDED: "[SHREDDED]"; /** * Process-global registry holding every Zod schema marked sensitive. Backed * by a `WeakMap`, so wrapper-created instances (`.optional()`, `.nullable()`, * `.default()`) that chain off a marked schema produce *new* schema instances * the registry doesn't track; the field walker handles those via unwrap. * * Exported so the public `sensitive(zodType)` wrapper can call `_registry.add`. * Underscore prefix marks "framework-private, don't touch from user code." * * @internal */ export declare const _registry: z.core.$ZodRegistry<{ sensitive: true; }, z.core.$ZodType>>; /** * Marker key stamped onto a schema's own `def`. Zod clones a schema on every * refinement (`.min()`, `.email()`, `.trim()`, `.describe()`, `.refine()`, * `.transform()`, …) via `{...def}`, which copies own symbol keys — so a * marker on the def survives the whole chain, while the `_registry` WeakMap * (keyed on the *instance*) does not (#1417). * * The registry is still populated and still consulted: it covers schemas * marked before this key existed, and it is the mechanism the public * `sensitive()` doc-comment describes. * * @internal */ export declare const _SENSITIVE: unique symbol; /** * Stamp the def-level marker. Called by the public `sensitive()` alongside * `_registry.add`. * * @internal */ export declare function _mark_sensitive(schema: z.ZodType): void; /** * True when the given schema was marked via `sensitive(...)`. * * Walks through Zod wrapper layers (`.optional()`, `.nullable()`, * `.default()`, `.readonly()`) by following `_def.innerType` until it reaches * a non-wrapper schema, then checks the registry. Wrappers create new schema * instances; the marker lives on the *inner* schema the user wrapped, so we * test that one. * * @internal */ export declare function is_pii(schema: z.ZodType): boolean; /** * Derive an event's sensitive fields, as the declared schema of each. * * Walks the top-level shape of a `z.object({...})` and keeps the keys whose * schema (after unwrapping optional/nullable/default wrappers) was marked via * `sensitive(...)`. Returns an empty object for non-object schemas or events * with no sensitive fields — the common-case zero-cost path. * * Only the top-level shape is walked. Sensitive fields nested inside a * `z.object` declared inside the event payload would require recursive * descent; that's deferred until a real callsite needs it. * * A union event has no top-level shape, so the options are walked and merged: * a key sensitive in any variant must be split, because the stored payload * could be that variant ([#1417](https://github.com/Rotorsoft/act-root/issues/1417)). * The first variant to declare a key wins, which only matters to a caller that * wants the schema rather than the name. * * Returning the schemas rather than just the names is what lets a caller do * something per field — the event builder asks each one whether it holds a * date, so the `pii` sidecar's dates can be revived like any other. * * @internal */ export declare function pii_schemas(schema: z.ZodType): Record; /** * The names of an event's sensitive fields — {@link pii_schemas} keyed. * * @internal — consumed by the registry's `sensitive_fields(event_name)` lookup, * and public through `types/schemas.ts`, where act-http's OpenAPI emitter uses * it to mark request-body properties `writeOnly`. */ export declare function pii_fields(schema: z.ZodType): readonly string[]; /** * Split an emitted event's `data` into `data` (non-sensitive) + `pii` * (sensitive) using the field list precomputed at build time. Used by the * State's `_pii_split` decorator just before `Store.commit`. * * Single forward pass over `Object.keys(validated)` — same shape as the * spread-and-delete-free implementation in slice 3, just hoisted out of the * orchestrator hot path so it's only invoked when the State actually has a * sensitive event. * * @internal */ export declare function pii_split>(emitted: { name: TName; data: TData; }, fields: readonly string[]): { name: TName; data: TData; pii: Record; }; /** * Build the **external view** of a committed event — the form returned by * `load()`, `query()`, `query_array()`, and the snapshot in `do()`'s reply. * * Only ever reached for events that declare sensitive fields — the sole caller * is {@link make_gate}, which the builder invokes exclusively for sensitive * events; non-sensitive events short-circuit to {@link IDENTITY_GATE} before * they get here. `fields` is therefore guaranteed non-empty (same contract as * {@link pii_strip}). * * - Event whose `pii` payload is null/undefined → substitute {@link SHREDDED} * for each declared field. Irrecoverable, so no predicate check. * - Event with a `pii` payload, predicate returns `true` → merge `pii` into * `data` (plaintext). * - Event with a `pii` payload, predicate returns `false` OR no predicate * declared (framework default-deny) → substitute {@link REDACTED} for each * declared field. * * @internal */ export declare function pii_gate(event: Committed, fields: readonly string[], predicate: ((event: any, actor: Actor) => boolean) | null, actor: Actor | undefined): Committed; /** * A prebuilt per-event read gate: given a committed event and the reading * actor, return the caller-visible form. This is the single gating primitive * the builder prebuilds for **every** read surface — both the actor-less * `query` / `query_array` (which pass no actor → default-deny) and the * actor-aware `load` / `do`-return view (which pass the reader). Non-sensitive * events use the shared {@link IDENTITY_GATE}; sensitive events use a redactor * built by {@link make_gate} that closes over the field list and the state's * disclosure predicate, so the read path never recomputes the sensitive-field * lookup nor allocates per event. * * The `actor` is optional so the actor-less surfaces can call `gate(event)`. * * @internal */ export type EventGate = (event: Committed, actor?: Actor) => Committed; /** * Shared zero-cost gate for every event with no `sensitive(...)` fields — a * single frozen reference the builder hands back for non-sensitive events, so * the common path is one `Map` miss and an identity call, no allocation. This * is the "by default, return the event" half of the prebuilt per-event gate. * * @internal */ export declare const IDENTITY_GATE: EventGate; /** * Prebuild a read gate for a sensitive event, capturing its field list and the * disclosure predicate once at build time. The returned closure defers to * {@link pii_gate} with the reading actor supplied per call: * * - `predicate = null` (the actor-less `query` surfaces, or a state that never * declared `.discloses`) → default-deny: declared fields come back * {@link REDACTED} (or {@link SHREDDED} once the pii column is forgotten). * - `predicate` set + an authorized actor → plaintext merged into `data`. * * Either way the isolated `pii` sidecar is dropped. The builder stores one gate * per sensitive event (per state for the load path; predicate-less for the * query path); non-sensitive events fall back to {@link IDENTITY_GATE}. * * @internal */ export declare function make_gate(fields: readonly string[], predicate: ((event: any, actor: Actor) => boolean) | null): EventGate; /** * Build the **handler view** — sensitive keys removed entirely from `data` * and the `pii` field dropped from the event. Used before invoking projection * handlers and reaction handlers, which never see PII by framework rule. * * Different from {@link pii_gate} (which substitutes {@link REDACTED} or * {@link SHREDDED}) — projection tables and reaction sinks shouldn't even * structurally observe the keys, so a handler that mistakenly writes * `event.data.email` into a column would get `undefined`, not a sentinel * string that looks like real data. The strictness is deliberate. * * Reactions that genuinely need PII (e.g. a welcome-email reaction reading * `email`) opt back in by explicitly calling `app.load(stream, { actor: * system_actor })` inside the handler — pulling PII through the gate at the * call site makes the security-relevant path visible in code review. * * @internal */ export declare function pii_strip(event: Committed, fields: readonly string[]): Committed; //# sourceMappingURL=sensitive.d.ts.map