@bounded-sh/observe 0.1.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.
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Escort (enforce) config for a route (SPEC §4.2 Mode A, T9). Present only on
3
+ * routes a promoted policy has escorted. When set, the interceptor makes ONE
4
+ * verdict round-trip BEFORE the call fires; the call still fires from the
5
+ * customer's own process (payload never transits Bounded).
6
+ */
7
+ export interface EscortConfig {
8
+ /** Full URL the interceptor POSTs the candidate intent to for a verdict,
9
+ * e.g. "https://acme-dev.bounded.sh/api/verdict". Sensor-key authed. */
10
+ verdictUrl: string;
11
+ /** Full URL for the fire-and-forget settle report. Defaults to verdictUrl's
12
+ * sibling "/settle". */
13
+ settleUrl?: string;
14
+ /** Enforcement collection (execcfg key), e.g. "stripe_refunds". */
15
+ collection: string;
16
+ /** Rail request-body field -> intent field, e.g. {charge:"chargeId",
17
+ * amount:"amountCents"}. Extracted from the request the app ALREADY built. */
18
+ fieldMap: Record<string, string>;
19
+ /** Local behavior when the verdict endpoint is unreachable (U18). Default
20
+ * "closed" (immediate clean local error); "open" = proceed + flag. */
21
+ failMode: "closed" | "open";
22
+ /** Informational only — the server owns the real approval routing decision. */
23
+ approvalAbove?: {
24
+ field: string;
25
+ limit: number;
26
+ };
27
+ /** Verdict round-trip timeout, ms (default 3000). */
28
+ timeoutMs?: number;
29
+ }
30
+ /**
31
+ * Wanted-identity attribution for a route: an org may WANT a field like
32
+ * customer_email for per-user limits (per-actor enforcement + rollups). The
33
+ * value flows through the ATTRIBUTION channel (event.onBehalfOf) — NEVER the
34
+ * telemetry capture lists. Hashed by default IN-PROCESS (the raw value never
35
+ * leaves); raw values ride only on an explicit owner opt-in (hash:false).
36
+ */
37
+ export interface IdentityFieldConfig {
38
+ /** Request-body field (dot path) holding the per-user principal, e.g. "customer_email". */
39
+ source: string;
40
+ /** The only supported target: the envelope's onBehalfOf attribution slot. */
41
+ as: "onBehalfOf";
42
+ /** true (DEFAULT) ⇒ onBehalfOf = "sha256:<hex(sha256(value))>", computed
43
+ * in-process. false = EXPLICIT owner opt-in to raw values. */
44
+ hash: boolean;
45
+ }
46
+ export interface RecognizerEntry {
47
+ /** Exact destination host (post-alias), e.g. "api.stripe.com". */
48
+ host: string;
49
+ /** Uppercase method. */
50
+ method: string;
51
+ /** Templated path this entry matches exactly, e.g. "/v1/charges/{id}/refunds". */
52
+ path: string;
53
+ /**
54
+ * JSON-RPC operation this entry matches (crypto rails). When set, the entry
55
+ * matches host + POST + the request body's JSON-RPC `method === rpcMethod`
56
+ * INSTEAD of the exact path — every RPC call POSTs to one path ("/") with the
57
+ * operation in the body, so path alone cannot tell `sendTransaction` from
58
+ * `getBalance`. Keyed separately (`${host}|rpc|${rpcMethod}`); entries WITHOUT
59
+ * rpcMethod keep matching by host+method+path exactly as before. */
60
+ rpcMethod?: string;
61
+ rail: string;
62
+ action: string;
63
+ /** SAFE request-body fields to recognize (dot paths). L2 denylist still applies. */
64
+ requestFields?: string[];
65
+ /** SAFE response-JSON fields to recognize (dot paths). L2 denylist still applies. */
66
+ responseFields?: string[];
67
+ /** Wanted identity → onBehalfOf. Exempt from the capture denylist ONLY in
68
+ * this slot; it can never appear in requestFields/responseFields (entries
69
+ * that try are rejected by validateManifest — belt and braces). */
70
+ identityField?: IdentityFieldConfig;
71
+ /** Escort (enforce) config — set only on promoted/escorted routes. */
72
+ escort?: EscortConfig;
73
+ }
74
+ /** A compiled, resolved escort (settleUrl + rail + action filled in). */
75
+ export interface CompiledEscort extends EscortConfig {
76
+ settleUrl: string;
77
+ rail: string;
78
+ action: string;
79
+ }
80
+ export interface RouteMatcher {
81
+ host: string;
82
+ method?: string;
83
+ /** Matches when the templated path starts with this prefix. */
84
+ pathPrefix?: string;
85
+ }
86
+ export interface ObserveManifest {
87
+ /** Registry version stamped into rec.registryVersion. */
88
+ version: string;
89
+ /** Content-hashed policy version (stamped on events when present). */
90
+ policyVersion?: string;
91
+ recognizers: RecognizerEntry[];
92
+ /** Routes forced into exact per-minute counter aggregation. */
93
+ counters: RouteMatcher[];
94
+ /** Routes never reported at all (capture policy `mute`). */
95
+ mutes?: RouteMatcher[];
96
+ /** TODO: signed-manifest verification (see header comment). Ignored for now. */
97
+ signature?: string;
98
+ }
99
+ /** Built-in default manifest: metadata-only + curated safe-field recognizers.
100
+ * SAFE fields only: amounts (cents), opaque IDs, model names, token counts. */
101
+ export declare const BUILTIN_MANIFEST: ObserveManifest;
102
+ /** Env-derived escort pin parameters (register preload, BOUNDED_* env). Kept
103
+ * on ObserveConfig so the pin is re-applied over EVERY manifest swap — a
104
+ * fetched manifest can never silently drop the pinned enforcement. */
105
+ export interface EscortPin {
106
+ verdictUrl: string;
107
+ collection: string;
108
+ failMode: "closed" | "open";
109
+ timeoutMs?: number;
110
+ }
111
+ /**
112
+ * Pin an escort block onto the Stripe-refund recognizer (api.stripe.com POST
113
+ * /v1/refunds). Everything else stays OBSERVE. Applied over the built-in at
114
+ * boot (register preload) AND re-applied over every fetched manifest swap.
115
+ * Idempotent, fetched-wins:
116
+ * - If the entry ALREADY carries an escort block (server-side promotion in a
117
+ * fetched manifest), that block wins and the env pin is skipped.
118
+ * - If the entry is MISSING (a fetched manifest dropped the route), it is
119
+ * restored from the built-in recognizer WITH the pin — enforcement armed
120
+ * via BOUNDED_VERDICT_URL can never be silently switched off by a swap.
121
+ * The "+escort" version suffix is appended only when the pin was applied, and
122
+ * never doubled. counters/mutes/policyVersion of the base are preserved.
123
+ */
124
+ export declare function escortedManifest(base: ObserveManifest, verdictUrl: string, collection: string, failMode: "closed" | "open", timeoutMs?: number): ObserveManifest;
125
+ /** Apply an (optional) env escort pin over a manifest. No pin = passthrough. */
126
+ export declare function withEscortPin(base: ObserveManifest, pin: EscortPin | undefined): ObserveManifest;
127
+ /** Compiled manifest index for O(1)-ish hot-path decisions. */
128
+ export declare class CompiledManifest {
129
+ readonly manifest: ObserveManifest;
130
+ readonly source: "builtin" | "override" | "fetched";
131
+ /** host|METHOD|templatedPath -> entry (non-rpc entries only). */
132
+ private readonly exact;
133
+ /** host|rpc|rpcMethod -> entry (crypto JSON-RPC entries; matched on the
134
+ * request body's `method`, never the path). */
135
+ private readonly rpc;
136
+ /** Hosts carrying any rpcMethod recognizer (gates the JSON-RPC body parse). */
137
+ private readonly rpcHosts;
138
+ /** Hosts with any recognizer (request-body capture allowed). */
139
+ readonly captureHosts: Set<string>;
140
+ /** host|METHOD|templatedPath entries whose responseFields need a body read. */
141
+ private readonly respCapture;
142
+ /** host|METHOD|templatedPath -> compiled escort (non-GET/HEAD routes only). */
143
+ private readonly escorts;
144
+ /** True if ANY route is escorted — the hot-path gate for escort mode. When
145
+ * false (the observe-only case) the interceptor does zero extra work. */
146
+ readonly hasEscorts: boolean;
147
+ constructor(manifest: ObserveManifest, source: "builtin" | "override" | "fetched");
148
+ get version(): string;
149
+ recognize(host: string, method: string, pathTemplate: string): RecognizerEntry | undefined;
150
+ /** True if the host carries any crypto JSON-RPC recognizer — the cheap Set
151
+ * check that decides whether to parse the request body for its `method`. */
152
+ hasRpcHost(host: string): boolean;
153
+ /** Recognize a crypto JSON-RPC call by host + the body's `method`. */
154
+ recognizeRpc(host: string, rpcMethod: string): RecognizerEntry | undefined;
155
+ wantsResponseBody(host: string, method: string, pathTemplate: string): boolean;
156
+ /** Compiled escort for a route, or undefined. Reads are never escorted. */
157
+ escort(host: string, method: string, pathTemplate: string): CompiledEscort | undefined;
158
+ private matchList;
159
+ isCounterRoute(host: string, method: string, pathTemplate: string): boolean;
160
+ isMuted(host: string, method: string, pathTemplate: string): boolean;
161
+ }
162
+ /**
163
+ * Structurally validate an untrusted manifest object. Returns a sanitized
164
+ * ObserveManifest or null (=> caller keeps the current manifest; fail-safe).
165
+ * L2: recognizer field lists are filtered through the hard PII denylist —
166
+ * a bad manifest cannot widen capture into PII-named fields.
167
+ */
168
+ export declare function validateManifest(raw: unknown): ObserveManifest | null;