@edgeproc/privacy-core 0.2.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.
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/detect/checksums.d.ts +5 -0
- package/dist/detect/checksums.d.ts.map +1 -0
- package/dist/detect/checksums.js +37 -0
- package/dist/detect/checksums.js.map +1 -0
- package/dist/detect/detector.d.ts +12 -0
- package/dist/detect/detector.d.ts.map +1 -0
- package/dist/detect/detector.js +57 -0
- package/dist/detect/detector.js.map +1 -0
- package/dist/detect/patterns.d.ts +22 -0
- package/dist/detect/patterns.d.ts.map +1 -0
- package/dist/detect/patterns.js +40 -0
- package/dist/detect/patterns.js.map +1 -0
- package/dist/egress.d.ts +85 -0
- package/dist/egress.d.ts.map +1 -0
- package/dist/egress.js +115 -0
- package/dist/egress.js.map +1 -0
- package/dist/egressReceipt.d.ts +67 -0
- package/dist/egressReceipt.d.ts.map +1 -0
- package/dist/egressReceipt.js +41 -0
- package/dist/egressReceipt.js.map +1 -0
- package/dist/errors.d.ts +62 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +80 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/factory.d.ts +20 -0
- package/dist/providers/factory.d.ts.map +1 -0
- package/dist/providers/factory.js +27 -0
- package/dist/providers/factory.js.map +1 -0
- package/dist/providers/nollm.d.ts +12 -0
- package/dist/providers/nollm.d.ts.map +1 -0
- package/dist/providers/nollm.js +28 -0
- package/dist/providers/nollm.js.map +1 -0
- package/dist/providers/openrouter.d.ts +18 -0
- package/dist/providers/openrouter.d.ts.map +1 -0
- package/dist/providers/openrouter.js +43 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/redact.d.ts +14 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +79 -0
- package/dist/redact.js.map +1 -0
- package/dist/rehydrate.d.ts +21 -0
- package/dist/rehydrate.d.ts.map +1 -0
- package/dist/rehydrate.js +36 -0
- package/dist/rehydrate.js.map +1 -0
- package/dist/testing.d.ts +14 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +31 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/vault.d.ts +21 -0
- package/dist/vault.d.ts.map +1 -0
- package/dist/vault.js +38 -0
- package/dist/vault.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Harish Seshadri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# @edgeproc/privacy-core
|
|
2
|
+
|
|
3
|
+
A browser-side privacy boundary for LLM calls. Raw private text stays on your
|
|
4
|
+
device; only **policy-approved, redacted** text is allowed to reach an external
|
|
5
|
+
LLM; the model's reply is **rehydrated locally** so you see real values again.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## TL;DR
|
|
10
|
+
|
|
11
|
+
- **What it is** — a TypeScript library that redacts sensitive text *before* it
|
|
12
|
+
leaves the browser, swapping each detected value for a typed placeholder
|
|
13
|
+
(`[CARD_1]`, `[NAME_2]`, `[AMOUNT_1]`), then restores the real values locally
|
|
14
|
+
when the model replies. You see and approve the exact text that will be sent.
|
|
15
|
+
- **Why it works** — you can't send raw text by accident. The only way to hand
|
|
16
|
+
text to the model runs through the redaction step, and you see and approve the
|
|
17
|
+
exact placeholders-only version before anything is sent. If code tries to skip
|
|
18
|
+
that and send raw text, it won't even build — the safety is baked into the code
|
|
19
|
+
itself, not a rule you have to remember. (The mechanism is in
|
|
20
|
+
[Under the hood](#under-the-hood-for-developers).)
|
|
21
|
+
- **Why it exists** — people paste bank statements, medical notes, and contracts
|
|
22
|
+
into chatbots every day, and all of it leaves the device in the clear. This
|
|
23
|
+
makes the boundary *visible and approvable* instead of invisible.
|
|
24
|
+
- **Status** — `0.1.0`, graduated from a proven spike. Deterministic detection
|
|
25
|
+
spine + reversible **in-memory** vault + Egress Guard + redact→rehydrate loop +
|
|
26
|
+
runnable demo. Unpublished (pre-registry). The encrypted vault and contextual
|
|
27
|
+
NER are deliberately **deferred** — see [Roadmap](#roadmap).
|
|
28
|
+
|
|
29
|
+
## Quickstart — one command, see the proof
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Node >= 22.13 and pnpm. Then, from the repo root:
|
|
33
|
+
pnpm install && pnpm demo
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Open <http://localhost:5173>. A synthetic bank statement is pre-loaded. You will
|
|
37
|
+
see, top to bottom:
|
|
38
|
+
|
|
39
|
+
1. **The raw text** you'd normally paste into a chatbot.
|
|
40
|
+
2. **The redaction set + the exact wire payload** — every real value replaced by
|
|
41
|
+
a placeholder. Open your browser's network tab to confirm: only this redacted
|
|
42
|
+
text would leave the device.
|
|
43
|
+
3. Click **Send**. By default it uses the offline echo provider (runs cold). To
|
|
44
|
+
call a real model, set `OPENROUTER_API_KEY` + `VITE_USE_OPENROUTER=1` (copy
|
|
45
|
+
`.env.example` → `examples/demo/.env`): the demo then routes through a
|
|
46
|
+
same-origin **dev proxy** that injects the key server-side, so it never
|
|
47
|
+
reaches the browser. Either way, **only placeholders cross the wire**.
|
|
48
|
+
4. **The rehydrated answer** — placeholders swapped back to real values locally,
|
|
49
|
+
values that never left your machine.
|
|
50
|
+
|
|
51
|
+
Prefer to see the guarantee enforced headlessly? `pnpm test:e2e` drives the loop
|
|
52
|
+
in real chromium, intercepts the request, and asserts only placeholders cross the
|
|
53
|
+
wire (the screenshot lands in the gitignored `test-results/`).
|
|
54
|
+
|
|
55
|
+
## Use it as a library
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import {
|
|
59
|
+
approve,
|
|
60
|
+
makeProvider,
|
|
61
|
+
redactForEgress,
|
|
62
|
+
rehydrate,
|
|
63
|
+
Vault,
|
|
64
|
+
} from "@edgeproc/privacy-core";
|
|
65
|
+
|
|
66
|
+
const vault = new Vault();
|
|
67
|
+
const { provider } = makeProvider({ apiKey: process.env.OPENROUTER_API_KEY });
|
|
68
|
+
|
|
69
|
+
// detect + vault-write + brand — produces a PendingRedaction: a review PROPOSAL,
|
|
70
|
+
// not yet sendable (no provider will accept it).
|
|
71
|
+
const pending = await redactForEgress(rawStatement, vault);
|
|
72
|
+
|
|
73
|
+
// The explicit review step turns the proposal into a sendable RedactedPayload.
|
|
74
|
+
// The audit sink is REQUIRED, so every approval is observable.
|
|
75
|
+
const payload = approve(pending, (entry) => console.log(entry));
|
|
76
|
+
|
|
77
|
+
// provider.complete accepts ONLY a RedactedPayload — raw text won't compile.
|
|
78
|
+
const response = await provider.complete(payload);
|
|
79
|
+
|
|
80
|
+
// restore real values locally, on-device — bound to the payload's vault.
|
|
81
|
+
const answer = rehydrate(response.redactedText, vault, payload.vaultRef);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## The honest hard truth (read this first, it is not a footnote)
|
|
85
|
+
|
|
86
|
+
Two limits define what this can and cannot promise. Stating them up front is the
|
|
87
|
+
point — over-claiming privacy is worse than claiming none.
|
|
88
|
+
|
|
89
|
+
- **Detection recall *is* the product. Anything the detector misses leaks
|
|
90
|
+
silently.** No redactor catches everything: regex misses oddly-formatted
|
|
91
|
+
values, NER misses unusual names, novel PII types are invisible to both. So v0
|
|
92
|
+
does **not** promise "we redact everything." The v0 guarantee is scoped to
|
|
93
|
+
**user-confirmed redaction**: the human reviews the proposed redaction set in
|
|
94
|
+
the preview and approves it before send. Misses are caught by a person, not
|
|
95
|
+
promised away by the tool. The guard's job is to make the boundary *visible and
|
|
96
|
+
approvable*, not to claim perfect detection.
|
|
97
|
+
|
|
98
|
+
- **Redaction is not anonymization.** Even with every literal identifier removed,
|
|
99
|
+
the *structure* still leaks a behavioral fingerprint. "`$482.10` + the word
|
|
100
|
+
*insurance* + *early January*" can re-identify a person even though no name,
|
|
101
|
+
card, or SSN survived. Privacy-core reduces direct identifier leakage; it does
|
|
102
|
+
**not** make data anonymous, and it does not defend against a determined
|
|
103
|
+
re-identification attack. Generalization modes (amount bucketing, date
|
|
104
|
+
coarsening) that would start to address this are explicitly **deferred**.
|
|
105
|
+
|
|
106
|
+
If those two limits are unacceptable for a use case, this is the wrong tool.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Under the hood (for developers)
|
|
111
|
+
|
|
112
|
+
The "it won't even build" guarantee is a **type-enforced Egress Guard**, not a
|
|
113
|
+
convention. Provider adapters accept *only* a branded `RedactedPayload`, whose
|
|
114
|
+
sole constructor is the redaction pipeline (`redactForEgress` → `approve`). A
|
|
115
|
+
raw `string` is not assignable to that type, so **handing raw text to a provider
|
|
116
|
+
is a compile error** — not a discipline you have to remember. `pnpm build` (tsc)
|
|
117
|
+
proves it on every build.
|
|
118
|
+
|
|
119
|
+
The same guarantee is re-checked at runtime so a cast can't defeat it: every
|
|
120
|
+
approved payload is registered by identity, and `assertApproved()` rejects a
|
|
121
|
+
hand-forged or spread-cloned look-alike before any network call is made.
|
|
122
|
+
|
|
123
|
+
## Architecture — maps 1:1 to `src/`
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
src/
|
|
127
|
+
├── index.ts # public API barrel — the production surface, nothing else
|
|
128
|
+
├── types.ts # shared domain types (EntityType, Span, AuditEntry, …)
|
|
129
|
+
├── egress.ts # the moat: branded RedactedPayload + LlmProvider + unsafeBypass
|
|
130
|
+
├── redact.ts # redactForEgress — the ONLY legitimate payload constructor
|
|
131
|
+
├── rehydrate.ts # local restore of real values after the reply
|
|
132
|
+
├── vault.ts # Vault — reversible token<->value map (in-memory, v0)
|
|
133
|
+
├── detect/
|
|
134
|
+
│ ├── detector.ts # detect() — merges patterns + dictionaries, drops overlaps
|
|
135
|
+
│ ├── patterns.ts # the deterministic ruleset (generic + finance packs)
|
|
136
|
+
│ └── checksums.ts # Luhn (cards) + IBAN mod-97
|
|
137
|
+
├── providers/
|
|
138
|
+
│ ├── factory.ts # makeProvider — picks OpenRouter or the offline echo
|
|
139
|
+
│ ├── nollm.ts # NoLLMProvider — offline echo, runs with no API key
|
|
140
|
+
│ └── openrouter.ts # OpenRouterProvider — OpenAI-compatible chat/completions
|
|
141
|
+
└── testing.ts # SYNTHETIC_STATEMENT fixture — via the ./testing subpath,
|
|
142
|
+
# NEVER from the main barrel
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The flow is the portfolio's deterministic-core law: a fast, deterministic
|
|
146
|
+
detection **spine** (`detect`) plus a bounded, optional inference adapter (the
|
|
147
|
+
NER tier — deferred). The net-new product IP is the reversible loop + vault +
|
|
148
|
+
the type-enforced Egress Guard.
|
|
149
|
+
|
|
150
|
+
### Public API
|
|
151
|
+
|
|
152
|
+
Everything `src/index.ts` exports, and nothing more:
|
|
153
|
+
|
|
154
|
+
| Export | Kind | Role |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `detect` | fn | deterministic PII span detection |
|
|
157
|
+
| `redactForEgress` | fn | detect → vault-write → brand → a `PendingRedaction` proposal |
|
|
158
|
+
| `approve` | fn | explicit review step → mints the sendable payload (audit sink required) |
|
|
159
|
+
| `rehydrate` | fn | restore real values locally from placeholders |
|
|
160
|
+
| `Vault` | class | reversible token↔value map |
|
|
161
|
+
| `RedactedPayload` | type | the branded egress type |
|
|
162
|
+
| `LlmProvider` | interface | provider contract — accepts only `RedactedPayload` |
|
|
163
|
+
| `NoLLMProvider` | class | offline echo provider |
|
|
164
|
+
| `OpenRouterProvider` | class | OpenAI-compatible provider |
|
|
165
|
+
| `makeProvider` | fn | env-driven provider selector |
|
|
166
|
+
| `guardedProvider` | fn | wrap a provider so the runtime egress guard runs at one chokepoint |
|
|
167
|
+
| `unsafeBypass` | fn | the explicit, audited escape hatch |
|
|
168
|
+
|
|
169
|
+
The brand factory (`mintPendingRedaction`) and the `SYNTHETIC_STATEMENT` fixture
|
|
170
|
+
are intentionally **not** on the front door — a payload can be earned, not
|
|
171
|
+
forged, and a fixture is never shipped by accident.
|
|
172
|
+
|
|
173
|
+
## Roadmap (explicitly deferred — not shipped in 0.1.0)
|
|
174
|
+
|
|
175
|
+
Labeled so no one mistakes it for current scope:
|
|
176
|
+
|
|
177
|
+
- **Encrypted IndexedDB vault** (AES-GCM + passphrase KDF). v0 uses an
|
|
178
|
+
**in-memory** vault that clears on reload — plainly stated, by design.
|
|
179
|
+
- **Contextual NER adapter** (names/merchants/locations regex misses) — depends
|
|
180
|
+
on the not-yet-extracted `@edgeproc/browser` runtime; off by default.
|
|
181
|
+
- **Audit-log persistence** (the audit sink is wired today; durable storage is
|
|
182
|
+
deferred).
|
|
183
|
+
- **More domain packs** (medical, legal, HR, identity) beyond the lifted generic
|
|
184
|
+
+ finance set, and **generalization / anonymization** modes.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT. Recognizer patterns are ported from Microsoft Presidio (MIT); the
|
|
189
|
+
redact/rehydrate vault design follows LLM Guard's `Anonymize`/`Vault` (MIT),
|
|
190
|
+
reimplemented here in TypeScript.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checksums.d.ts","sourceRoot":"","sources":["../../src/detect/checksums.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAejD;AAWD,mCAAmC;AACnC,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQ9C"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Luhn check for card numbers (digits only). */
|
|
2
|
+
export function luhnValid(digits) {
|
|
3
|
+
const d = digits.replace(/\D/g, "");
|
|
4
|
+
if (d.length < 13 || d.length > 19)
|
|
5
|
+
return false;
|
|
6
|
+
let sum = 0;
|
|
7
|
+
let dbl = false;
|
|
8
|
+
for (let i = d.length - 1; i >= 0; i--) {
|
|
9
|
+
let n = d.charCodeAt(i) - 48;
|
|
10
|
+
if (dbl) {
|
|
11
|
+
n *= 2;
|
|
12
|
+
if (n > 9)
|
|
13
|
+
n -= 9;
|
|
14
|
+
}
|
|
15
|
+
sum += n;
|
|
16
|
+
dbl = !dbl;
|
|
17
|
+
}
|
|
18
|
+
return sum % 10 === 0;
|
|
19
|
+
}
|
|
20
|
+
/** Compute n mod 97 over a decimal string too large for Number. */
|
|
21
|
+
function mod97(numeric) {
|
|
22
|
+
let remainder = 0;
|
|
23
|
+
for (const ch of numeric) {
|
|
24
|
+
remainder = (remainder * 10 + (ch.charCodeAt(0) - 48)) % 97;
|
|
25
|
+
}
|
|
26
|
+
return remainder;
|
|
27
|
+
}
|
|
28
|
+
/** ISO 13616 IBAN mod-97 check. */
|
|
29
|
+
export function ibanValid(raw) {
|
|
30
|
+
const s = raw.replace(/\s+/g, "").toUpperCase();
|
|
31
|
+
if (!/^[A-Z]{2}\d{2}[A-Z0-9]{1,30}$/.test(s))
|
|
32
|
+
return false;
|
|
33
|
+
const rearranged = s.slice(4) + s.slice(0, 4);
|
|
34
|
+
const expanded = rearranged.replace(/[A-Z]/g, (c) => String(c.charCodeAt(0) - 55));
|
|
35
|
+
return mod97(expanded) === 1;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=checksums.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checksums.js","sourceRoot":"","sources":["../../src/detect/checksums.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,MAAM,UAAU,SAAS,CAAC,MAAc;IACtC,MAAM,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACpC,IAAI,CAAC,CAAC,MAAM,GAAG,EAAE,IAAI,CAAC,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,KAAK,CAAC;IACjD,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,GAAG,GAAG,KAAK,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,IAAI,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QAC7B,IAAI,GAAG,EAAE,CAAC;YACR,CAAC,IAAI,CAAC,CAAC;YACP,IAAI,CAAC,GAAG,CAAC;gBAAE,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;QACD,GAAG,IAAI,CAAC,CAAC;QACT,GAAG,GAAG,CAAC,GAAG,CAAC;IACb,CAAC;IACD,OAAO,GAAG,GAAG,EAAE,KAAK,CAAC,CAAC;AACxB,CAAC;AAED,mEAAmE;AACnE,SAAS,KAAK,CAAC,OAAe;IAC5B,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;QACzB,SAAS,GAAG,CAAC,SAAS,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;IAC9D,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAChD,IAAI,CAAC,+BAA+B,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC3D,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAClD,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAC7B,CAAC;IACF,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Span } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detect all PII spans deterministically: structured patterns + checksums plus
|
|
4
|
+
* the finance/name dictionaries. Overlaps are dropped; the result is sorted by
|
|
5
|
+
* start offset and non-overlapping.
|
|
6
|
+
*
|
|
7
|
+
* Recall is the product: anything not matched here leaks unless a human catches
|
|
8
|
+
* it in the preview. The contextual NER tier that would widen recall is
|
|
9
|
+
* deliberately deferred (see Roadmap).
|
|
10
|
+
*/
|
|
11
|
+
export declare function detect(text: string): Span[];
|
|
12
|
+
//# sourceMappingURL=detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../../src/detect/detector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,IAAI,EAAE,MAAM,aAAa,CAAC;AAiDpD;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,CAO3C"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { MERCHANTS, NAMES, RULES } from "./patterns.js";
|
|
2
|
+
function dictSpans(text, type, dict) {
|
|
3
|
+
const out = [];
|
|
4
|
+
for (const term of dict) {
|
|
5
|
+
let from = 0;
|
|
6
|
+
let idx = text.indexOf(term, from);
|
|
7
|
+
while (idx !== -1) {
|
|
8
|
+
out.push({ type, value: term, start: idx, end: idx + term.length });
|
|
9
|
+
from = idx + term.length;
|
|
10
|
+
idx = text.indexOf(term, from);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
function regexSpans(text, rule) {
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const m of text.matchAll(rule.re)) {
|
|
18
|
+
// If the rule has a capture group, that group is the value; else whole match.
|
|
19
|
+
const value = m[1] ?? m[0];
|
|
20
|
+
if (rule.accept && !rule.accept(value))
|
|
21
|
+
continue;
|
|
22
|
+
const start = m.index + m[0].indexOf(value);
|
|
23
|
+
out.push({ type: rule.type, value, start, end: start + value.length });
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/** Drop overlapping spans (earlier/longer wins), keeping a sorted, disjoint set. */
|
|
28
|
+
function dropOverlaps(spans) {
|
|
29
|
+
const sorted = [...spans].sort((a, b) => a.start - b.start || b.end - b.start - (a.end - a.start));
|
|
30
|
+
const kept = [];
|
|
31
|
+
let lastEnd = -1;
|
|
32
|
+
for (const s of sorted) {
|
|
33
|
+
if (s.start >= lastEnd) {
|
|
34
|
+
kept.push(s);
|
|
35
|
+
lastEnd = s.end;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return kept;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Detect all PII spans deterministically: structured patterns + checksums plus
|
|
42
|
+
* the finance/name dictionaries. Overlaps are dropped; the result is sorted by
|
|
43
|
+
* start offset and non-overlapping.
|
|
44
|
+
*
|
|
45
|
+
* Recall is the product: anything not matched here leaks unless a human catches
|
|
46
|
+
* it in the preview. The contextual NER tier that would widen recall is
|
|
47
|
+
* deliberately deferred (see Roadmap).
|
|
48
|
+
*/
|
|
49
|
+
export function detect(text) {
|
|
50
|
+
const all = [
|
|
51
|
+
...RULES.flatMap((r) => regexSpans(text, r)),
|
|
52
|
+
...dictSpans(text, "MERCHANT", MERCHANTS),
|
|
53
|
+
...dictSpans(text, "NAME", NAMES),
|
|
54
|
+
];
|
|
55
|
+
return dropOverlaps(all);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detector.js","sourceRoot":"","sources":["../../src/detect/detector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAa,MAAM,eAAe,CAAC;AAEnE,SAAS,SAAS,CAChB,IAAY,EACZ,IAAgB,EAChB,IAAuB;IAEvB,MAAM,GAAG,GAAW,EAAE,CAAC;IACvB,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACnC,OAAO,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;YAClB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YACpE,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC;YACzB,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,UAAU,CAAC,IAAY,EAAE,IAAU;IAC1C,MAAM,GAAG,GAAW,EAAE,CAAC;IACvB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;QACvC,8EAA8E;QAC9E,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,SAAS;QACjD,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC5C,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,oFAAoF;AACpF,SAAS,YAAY,CAAC,KAAsB;IAC1C,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAC5B,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,CACnE,CAAC;IACF,MAAM,IAAI,GAAW,EAAE,CAAC;IACxB,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC;IACjB,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,CAAC,KAAK,IAAI,OAAO,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACb,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC;QAClB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,MAAM,GAAG,GAAG;QACV,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC5C,GAAG,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC;QACzC,GAAG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC;KAClC,CAAC;IACF,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EntityType } from "../types.js";
|
|
2
|
+
/** A regex recognizer, optionally gated by a checksum/structure accept-test. */
|
|
3
|
+
export interface Rule {
|
|
4
|
+
readonly type: EntityType;
|
|
5
|
+
readonly re: RegExp;
|
|
6
|
+
readonly accept?: (match: string) => boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The Tier-1 deterministic ruleset (generic + finance packs, v0 scope).
|
|
10
|
+
* Patterns + checksums are ported from Microsoft Presidio's recognizer set;
|
|
11
|
+
* we reuse the durable, well-tested structure, not Presidio's Python runtime.
|
|
12
|
+
*/
|
|
13
|
+
export declare const RULES: readonly Rule[];
|
|
14
|
+
/** The finance pack's merchant name set (deterministic dictionary lookup). */
|
|
15
|
+
export declare const MERCHANTS: readonly string[];
|
|
16
|
+
/**
|
|
17
|
+
* A tiny known-names dictionary. Real name coverage is the job of the deferred
|
|
18
|
+
* NER adapter (see Roadmap); this keeps the deterministic spine self-contained
|
|
19
|
+
* and the wow-demo runnable with zero download.
|
|
20
|
+
*/
|
|
21
|
+
export declare const NAMES: readonly string[];
|
|
22
|
+
//# sourceMappingURL=patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.d.ts","sourceRoot":"","sources":["../../src/detect/patterns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,gFAAgF;AAChF,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CAC9C;AAED;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAAE,SAAS,IAAI,EAcvB,CAAC;AAEX,8EAA8E;AAC9E,eAAO,MAAM,SAAS,EAAE,SAAS,MAAM,EAM7B,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,KAAK,EAAE,SAAS,MAAM,EAIzB,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ibanValid, luhnValid } from "./checksums.js";
|
|
2
|
+
/**
|
|
3
|
+
* The Tier-1 deterministic ruleset (generic + finance packs, v0 scope).
|
|
4
|
+
* Patterns + checksums are ported from Microsoft Presidio's recognizer set;
|
|
5
|
+
* we reuse the durable, well-tested structure, not Presidio's Python runtime.
|
|
6
|
+
*/
|
|
7
|
+
export const RULES = [
|
|
8
|
+
{ type: "EMAIL", re: /\b[\w.+-]+@[\w-]+\.[\w.-]+\b/g },
|
|
9
|
+
{
|
|
10
|
+
type: "IBAN",
|
|
11
|
+
re: /\b[A-Z]{2}\d{2}(?:\s?[A-Z0-9]{2,4})+\b/g,
|
|
12
|
+
accept: ibanValid,
|
|
13
|
+
},
|
|
14
|
+
{ type: "CARD", re: /\b(?:\d[ -]?){13,19}\b/g, accept: luhnValid },
|
|
15
|
+
{ type: "SSN", re: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
16
|
+
{ type: "ROUTING", re: /\bRouting number:\s*(\d{9})\b/g },
|
|
17
|
+
{ type: "ACCOUNT", re: /\bAccount number:\s*(\d{9,12})\b/g },
|
|
18
|
+
{ type: "PHONE", re: /\(\d{3}\)\s?\d{3}-\d{4}\b/g },
|
|
19
|
+
{ type: "AMOUNT", re: /\$\d{1,3}(?:,\d{3})*(?:\.\d{2})?\b/g },
|
|
20
|
+
{ type: "DATE", re: /\b\d{2}\/\d{2}\/\d{4}\b/g },
|
|
21
|
+
];
|
|
22
|
+
/** The finance pack's merchant name set (deterministic dictionary lookup). */
|
|
23
|
+
export const MERCHANTS = [
|
|
24
|
+
"Whole Foods",
|
|
25
|
+
"Starbucks",
|
|
26
|
+
"Amazon",
|
|
27
|
+
"Walmart",
|
|
28
|
+
"Costco",
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* A tiny known-names dictionary. Real name coverage is the job of the deferred
|
|
32
|
+
* NER adapter (see Roadmap); this keeps the deterministic spine self-contained
|
|
33
|
+
* and the wow-demo runnable with zero download.
|
|
34
|
+
*/
|
|
35
|
+
export const NAMES = [
|
|
36
|
+
"Ada Lovelace",
|
|
37
|
+
"Grace Hopper",
|
|
38
|
+
"Alan Turing",
|
|
39
|
+
];
|
|
40
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/detect/patterns.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAStD;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAoB;IACpC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,+BAA+B,EAAE;IACtD;QACE,IAAI,EAAE,MAAM;QACZ,EAAE,EAAE,yCAAyC;QAC7C,MAAM,EAAE,SAAS;KAClB;IACD,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,yBAAyB,EAAE,MAAM,EAAE,SAAS,EAAE;IAClE,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,wBAAwB,EAAE;IAC7C,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,gCAAgC,EAAE;IACzD,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,mCAAmC,EAAE;IAC5D,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,4BAA4B,EAAE;IACnD,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,qCAAqC,EAAE;IAC7D,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,0BAA0B,EAAE;CACxC,CAAC;AAEX,8EAA8E;AAC9E,MAAM,CAAC,MAAM,SAAS,GAAsB;IAC1C,aAAa;IACb,WAAW;IACX,QAAQ;IACR,SAAS;IACT,QAAQ;CACA,CAAC;AAEX;;;;GAIG;AACH,MAAM,CAAC,MAAM,KAAK,GAAsB;IACtC,cAAc;IACd,cAAc;IACd,aAAa;CACL,CAAC"}
|
package/dist/egress.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type EgressGovernance } from "./egressReceipt.js";
|
|
2
|
+
import type { AuditSink, VaultRef } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The egress boundary, expressed as a capability earned in two explicit steps:
|
|
5
|
+
*
|
|
6
|
+
* 1. `redactForEgress` mints a {@link PendingRedaction} — a review PROPOSAL.
|
|
7
|
+
* It is not sendable; no provider accepts it. Zero detections do NOT relax
|
|
8
|
+
* this: an empty redaction set still requires the explicit review step,
|
|
9
|
+
* because "the detector found nothing" is not the same as "a reviewer
|
|
10
|
+
* approved this content to leave the device".
|
|
11
|
+
* 2. `approve` — the explicit review action — converts a pipeline-minted
|
|
12
|
+
* pending into a {@link RedactedPayload}, the sendable capability.
|
|
13
|
+
*
|
|
14
|
+
* Both types carry a compile-time phantom brand (a raw `string` or hand-rolled
|
|
15
|
+
* object literal is not assignable), and every value minted here is registered
|
|
16
|
+
* in a module-private WeakSet so the capability also holds at RUNTIME in plain
|
|
17
|
+
* JS — a structurally identical forgery is rejected by {@link approve} and by
|
|
18
|
+
* every provider adapter (via {@link assertApproved}).
|
|
19
|
+
*/
|
|
20
|
+
declare const brand: unique symbol;
|
|
21
|
+
interface Branded<B extends string> {
|
|
22
|
+
readonly [brand]: B;
|
|
23
|
+
}
|
|
24
|
+
/** A redaction proposal awaiting explicit review. NOT sendable. */
|
|
25
|
+
export type PendingRedaction = Branded<"PendingRedaction"> & {
|
|
26
|
+
readonly redactedText: string;
|
|
27
|
+
readonly vaultRef: VaultRef;
|
|
28
|
+
/** The placeholder tokens the reviewer is being asked to approve. */
|
|
29
|
+
readonly placeholders: readonly string[];
|
|
30
|
+
};
|
|
31
|
+
/** The sendable capability: reviewed, redacted content — never raw content. */
|
|
32
|
+
export type RedactedPayload = Branded<"RedactedPayload"> & {
|
|
33
|
+
readonly redactedText: string;
|
|
34
|
+
readonly vaultRef: VaultRef;
|
|
35
|
+
readonly approvedAt: number;
|
|
36
|
+
};
|
|
37
|
+
/** The egress boundary: providers accept ONLY an approved RedactedPayload. */
|
|
38
|
+
export interface LlmProvider {
|
|
39
|
+
complete(payload: RedactedPayload): Promise<import("./types.js").RedactedResponse>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Runtime half of the egress guard. Every provider adapter MUST call this
|
|
43
|
+
* before doing anything with a payload; it rejects any object that was not
|
|
44
|
+
* minted by {@link approve} or {@link unsafeBypass} — TypeScript branding
|
|
45
|
+
* alone cannot stop a plain-JS caller, this check does.
|
|
46
|
+
*/
|
|
47
|
+
export declare function assertApproved(payload: RedactedPayload): void;
|
|
48
|
+
/**
|
|
49
|
+
* Internal factory used by the redact pipeline. Intentionally NOT re-exported
|
|
50
|
+
* from the public barrel — a pending is earned from detection, not built.
|
|
51
|
+
*/
|
|
52
|
+
export declare function mintPendingRedaction(redactedText: string, vaultRef: VaultRef, placeholders: readonly string[]): PendingRedaction;
|
|
53
|
+
/**
|
|
54
|
+
* THE explicit approval step — the only way a PendingRedaction becomes
|
|
55
|
+
* sendable. Approval is a deliberate human/reviewer action, never a side
|
|
56
|
+
* effect: even a zero-detection result passes through here. It refuses any
|
|
57
|
+
* object the pipeline did not mint (a hand-built "pending" would smuggle
|
|
58
|
+
* unreviewed text past the guard).
|
|
59
|
+
*
|
|
60
|
+
* The `audit` sink is REQUIRED — an approval that no one can observe is not
|
|
61
|
+
* an approval this boundary will grant. (Same stance as {@link unsafeBypass}.)
|
|
62
|
+
*/
|
|
63
|
+
export declare function approve(pending: PendingRedaction, audit: AuditSink): RedactedPayload;
|
|
64
|
+
/**
|
|
65
|
+
* Wrap any {@link LlmProvider} so the runtime egress guard runs at ONE
|
|
66
|
+
* chokepoint before the inner provider ever sees the payload. TypeScript's
|
|
67
|
+
* brand stops a raw string at compile time, but a hand-rolled or third-party
|
|
68
|
+
* provider could still forget to call {@link assertApproved} at runtime — this
|
|
69
|
+
* wrapper makes that impossible for any provider obtained through it.
|
|
70
|
+
*
|
|
71
|
+
* With `governance`, the chokepoint additionally seals EVERY decision — each
|
|
72
|
+
* allow and each fail-closed deny — as a signed Avow effect receipt (see
|
|
73
|
+
* {@link file://./egressReceipt.ts}). The deny receipt digests the redacted
|
|
74
|
+
* text that was ATTEMPTED, and only its hash; nothing reaches the inner
|
|
75
|
+
* provider on a deny either way.
|
|
76
|
+
*/
|
|
77
|
+
export declare function guardedProvider(inner: LlmProvider, governance?: EgressGovernance): LlmProvider;
|
|
78
|
+
/**
|
|
79
|
+
* The explicit, named, audited escape hatch — the ONLY way raw text can be
|
|
80
|
+
* made sendable without going through detection + review. It cannot be silent:
|
|
81
|
+
* it always emits an audit entry recording the human-supplied reason.
|
|
82
|
+
*/
|
|
83
|
+
export declare function unsafeBypass(raw: string, reason: string, audit: AuditSink): RedactedPayload;
|
|
84
|
+
export {};
|
|
85
|
+
//# sourceMappingURL=egress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress.d.ts","sourceRoot":"","sources":["../src/egress.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,CAAC,MAAM,KAAK,EAAE,OAAO,MAAM,CAAC;AAEnC,UAAU,OAAO,CAAC,CAAC,SAAS,MAAM;IAChC,QAAQ,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;CACrB;AAED,mEAAmE;AACnE,MAAM,MAAM,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC,GAAG;IAC3D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C,CAAC;AAEF,+EAA+E;AAC/E,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC,GAAG;IACzD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CACN,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,OAAO,YAAY,EAAE,gBAAgB,CAAC,CAAC;CACnD;AAYD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAO7D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,SAAS,MAAM,EAAE,GAC9B,gBAAgB,CAIlB;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CACrB,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,SAAS,GACf,eAAe,CAmBjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,WAAW,EAClB,UAAU,CAAC,EAAE,gBAAgB,GAC5B,WAAW,CAab;AAuBD;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,GACf,eAAe,CASjB"}
|
package/dist/egress.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { sealEgressReceipt, } from "./egressReceipt.js";
|
|
2
|
+
import { ForgedPayloadError, UnapprovedPayloadError } from "./errors.js";
|
|
3
|
+
/** Module-private: every pending the redaction pipeline actually minted. */
|
|
4
|
+
const mintedPending = new WeakSet();
|
|
5
|
+
/**
|
|
6
|
+
* Module-private: every payload the guard actually approved. A WeakSet keyed
|
|
7
|
+
* by object IDENTITY — callers cannot reproduce membership by building a
|
|
8
|
+
* structurally identical object, and entries vanish with the payloads.
|
|
9
|
+
*/
|
|
10
|
+
const approvedPayloads = new WeakSet();
|
|
11
|
+
/**
|
|
12
|
+
* Runtime half of the egress guard. Every provider adapter MUST call this
|
|
13
|
+
* before doing anything with a payload; it rejects any object that was not
|
|
14
|
+
* minted by {@link approve} or {@link unsafeBypass} — TypeScript branding
|
|
15
|
+
* alone cannot stop a plain-JS caller, this check does.
|
|
16
|
+
*/
|
|
17
|
+
export function assertApproved(payload) {
|
|
18
|
+
if (!approvedPayloads.has(payload)) {
|
|
19
|
+
throw new UnapprovedPayloadError("payload was not approved by the egress guard — providers only accept " +
|
|
20
|
+
"the capability minted by approve() (or the audited unsafeBypass)");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Internal factory used by the redact pipeline. Intentionally NOT re-exported
|
|
25
|
+
* from the public barrel — a pending is earned from detection, not built.
|
|
26
|
+
*/
|
|
27
|
+
export function mintPendingRedaction(redactedText, vaultRef, placeholders) {
|
|
28
|
+
const pending = Object.freeze({ redactedText, vaultRef, placeholders });
|
|
29
|
+
mintedPending.add(pending);
|
|
30
|
+
return pending;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* THE explicit approval step — the only way a PendingRedaction becomes
|
|
34
|
+
* sendable. Approval is a deliberate human/reviewer action, never a side
|
|
35
|
+
* effect: even a zero-detection result passes through here. It refuses any
|
|
36
|
+
* object the pipeline did not mint (a hand-built "pending" would smuggle
|
|
37
|
+
* unreviewed text past the guard).
|
|
38
|
+
*
|
|
39
|
+
* The `audit` sink is REQUIRED — an approval that no one can observe is not
|
|
40
|
+
* an approval this boundary will grant. (Same stance as {@link unsafeBypass}.)
|
|
41
|
+
*/
|
|
42
|
+
export function approve(pending, audit) {
|
|
43
|
+
if (!mintedPending.has(pending)) {
|
|
44
|
+
throw new ForgedPayloadError("approve() only accepts a PendingRedaction minted by redactForEgress — " +
|
|
45
|
+
"hand-built objects are rejected so unreviewed text cannot be approved");
|
|
46
|
+
}
|
|
47
|
+
const payload = Object.freeze({
|
|
48
|
+
redactedText: pending.redactedText,
|
|
49
|
+
vaultRef: pending.vaultRef,
|
|
50
|
+
approvedAt: Date.now(),
|
|
51
|
+
});
|
|
52
|
+
approvedPayloads.add(payload);
|
|
53
|
+
audit({
|
|
54
|
+
kind: "approve",
|
|
55
|
+
at: payload.approvedAt,
|
|
56
|
+
placeholders: pending.placeholders,
|
|
57
|
+
});
|
|
58
|
+
return payload;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Wrap any {@link LlmProvider} so the runtime egress guard runs at ONE
|
|
62
|
+
* chokepoint before the inner provider ever sees the payload. TypeScript's
|
|
63
|
+
* brand stops a raw string at compile time, but a hand-rolled or third-party
|
|
64
|
+
* provider could still forget to call {@link assertApproved} at runtime — this
|
|
65
|
+
* wrapper makes that impossible for any provider obtained through it.
|
|
66
|
+
*
|
|
67
|
+
* With `governance`, the chokepoint additionally seals EVERY decision — each
|
|
68
|
+
* allow and each fail-closed deny — as a signed Avow effect receipt (see
|
|
69
|
+
* {@link file://./egressReceipt.ts}). The deny receipt digests the redacted
|
|
70
|
+
* text that was ATTEMPTED, and only its hash; nothing reaches the inner
|
|
71
|
+
* provider on a deny either way.
|
|
72
|
+
*/
|
|
73
|
+
export function guardedProvider(inner, governance) {
|
|
74
|
+
return {
|
|
75
|
+
async complete(payload) {
|
|
76
|
+
try {
|
|
77
|
+
assertApproved(payload);
|
|
78
|
+
}
|
|
79
|
+
catch (denied) {
|
|
80
|
+
await sealDecision(governance, payload.redactedText, "deny");
|
|
81
|
+
throw denied;
|
|
82
|
+
}
|
|
83
|
+
await sealDecision(governance, payload.redactedText, "allow");
|
|
84
|
+
return inner.complete(payload);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Seal one guard decision and hand it to the governance sink, if governed. */
|
|
89
|
+
async function sealDecision(governance, redactedText, decision) {
|
|
90
|
+
if (!governance)
|
|
91
|
+
return;
|
|
92
|
+
const { provider, seedHex, onReceipt, detectorVersion } = governance;
|
|
93
|
+
onReceipt(await sealEgressReceipt({
|
|
94
|
+
provider,
|
|
95
|
+
redactedText,
|
|
96
|
+
decision,
|
|
97
|
+
...(detectorVersion === undefined ? {} : { detectorVersion }),
|
|
98
|
+
}, seedHex));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The explicit, named, audited escape hatch — the ONLY way raw text can be
|
|
102
|
+
* made sendable without going through detection + review. It cannot be silent:
|
|
103
|
+
* it always emits an audit entry recording the human-supplied reason.
|
|
104
|
+
*/
|
|
105
|
+
export function unsafeBypass(raw, reason, audit) {
|
|
106
|
+
audit({ kind: "unsafe-bypass", at: Date.now(), reason });
|
|
107
|
+
const payload = Object.freeze({
|
|
108
|
+
redactedText: raw,
|
|
109
|
+
vaultRef: { id: "unsafe-no-vault" },
|
|
110
|
+
approvedAt: Date.now(),
|
|
111
|
+
});
|
|
112
|
+
approvedPayloads.add(payload);
|
|
113
|
+
return payload;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=egress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egress.js","sourceRoot":"","sources":["../src/egress.ts"],"names":[],"mappings":"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"}
|