@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
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Egress decisions, sealed as signed Avow effect receipts.
|
|
3
|
+
*
|
|
4
|
+
* The Egress Guard already gates whether redacted text may leave the device
|
|
5
|
+
* (see {@link file://./egress.ts}). This module makes that decision *provable*:
|
|
6
|
+
* every allow or deny is expressed as a Writ-style governed effect — an
|
|
7
|
+
* `EgressSubject` — and signed into a tamper-evident receipt via the shared
|
|
8
|
+
* `@edgeproc/avow` envelope. A holder of the signer's public key can later
|
|
9
|
+
* verify exactly which egress decisions were taken, without ever seeing the
|
|
10
|
+
* content.
|
|
11
|
+
*
|
|
12
|
+
* INVARIANT — no plaintext in a receipt: the subject stores only
|
|
13
|
+
* `args_digest = sha256(canonical({ redactedText }))`, never the text itself,
|
|
14
|
+
* and never the raw PII the vault holds. A receipt records *that* a decision
|
|
15
|
+
* happened over *this* content, not the content.
|
|
16
|
+
*/
|
|
17
|
+
import { type SignedReceipt } from "@edgeproc/avow";
|
|
18
|
+
export { contentHash } from "@edgeproc/avow";
|
|
19
|
+
/** Whether the guard let the redacted text leave the device, or refused it. */
|
|
20
|
+
export type EgressDecision = "allow" | "deny";
|
|
21
|
+
/**
|
|
22
|
+
* The governed effect: sending redacted text to a named provider was either
|
|
23
|
+
* allowed or denied. A type alias (not an interface) so it satisfies the
|
|
24
|
+
* `@edgeproc/avow` `JsonValue` constraint via TypeScript's implicit index
|
|
25
|
+
* signature — the subject IS a plain JSON object.
|
|
26
|
+
*/
|
|
27
|
+
export type EgressSubject = {
|
|
28
|
+
readonly action: "llm.egress";
|
|
29
|
+
readonly provider: string;
|
|
30
|
+
readonly args_digest: string;
|
|
31
|
+
readonly decision: EgressDecision;
|
|
32
|
+
readonly detector_version: string;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Version tag for the Tier-1 deterministic detector ruleset. Bumping the
|
|
36
|
+
* ruleset bumps this so a verifier can tell which detector version screened
|
|
37
|
+
* the content behind a receipt.
|
|
38
|
+
*/
|
|
39
|
+
export declare const DETECTOR_VERSION = "1";
|
|
40
|
+
/** What the caller knows at the egress decision point. */
|
|
41
|
+
export interface EgressSubjectInput {
|
|
42
|
+
readonly provider: string;
|
|
43
|
+
/** The redacted (already-scrubbed) text — hashed, never stored verbatim. */
|
|
44
|
+
readonly redactedText: string;
|
|
45
|
+
readonly decision: EgressDecision;
|
|
46
|
+
readonly detectorVersion?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Build the signed subject, digesting the redacted text and nothing else. */
|
|
49
|
+
export declare function buildEgressSubject(input: EgressSubjectInput): Promise<EgressSubject>;
|
|
50
|
+
/**
|
|
51
|
+
* How a guarded provider seals its decisions. Given to
|
|
52
|
+
* {@link file://./egress.ts guardedProvider}, every allow AND every fail-closed
|
|
53
|
+
* deny at that chokepoint is signed and handed to `onReceipt` — the guard
|
|
54
|
+
* cannot act silently once governed.
|
|
55
|
+
*/
|
|
56
|
+
export interface EgressGovernance {
|
|
57
|
+
/** Provider name recorded in each receipt (e.g. "openrouter"). */
|
|
58
|
+
readonly provider: string;
|
|
59
|
+
/** Ed25519 seed (hex) that signs the receipts. */
|
|
60
|
+
readonly seedHex: string;
|
|
61
|
+
/** Receives every sealed decision receipt. */
|
|
62
|
+
readonly onReceipt: (receipt: SignedReceipt<EgressSubject>) => void;
|
|
63
|
+
readonly detectorVersion?: string;
|
|
64
|
+
}
|
|
65
|
+
/** Seal an egress decision into a signed, pinned-key-verifiable receipt. */
|
|
66
|
+
export declare function sealEgressReceipt(input: EgressSubjectInput, seedHex: string): Promise<SignedReceipt<EgressSubject>>;
|
|
67
|
+
//# sourceMappingURL=egressReceipt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egressReceipt.d.ts","sourceRoot":"","sources":["../src/egressReceipt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAe,KAAK,aAAa,EAAe,MAAM,gBAAgB,CAAC;AAI9E,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,+EAA+E;AAC/E,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,MAAM,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,0DAA0D;AAC1D,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,8EAA8E;AAC9E,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,aAAa,CAAC,CAQxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,aAAa,CAAC,KAAK,IAAI,CAAC;IACpE,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;CACnC;AAED,4EAA4E;AAC5E,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,kBAAkB,EACzB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC,CAEvC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Egress decisions, sealed as signed Avow effect receipts.
|
|
3
|
+
*
|
|
4
|
+
* The Egress Guard already gates whether redacted text may leave the device
|
|
5
|
+
* (see {@link file://./egress.ts}). This module makes that decision *provable*:
|
|
6
|
+
* every allow or deny is expressed as a Writ-style governed effect — an
|
|
7
|
+
* `EgressSubject` — and signed into a tamper-evident receipt via the shared
|
|
8
|
+
* `@edgeproc/avow` envelope. A holder of the signer's public key can later
|
|
9
|
+
* verify exactly which egress decisions were taken, without ever seeing the
|
|
10
|
+
* content.
|
|
11
|
+
*
|
|
12
|
+
* INVARIANT — no plaintext in a receipt: the subject stores only
|
|
13
|
+
* `args_digest = sha256(canonical({ redactedText }))`, never the text itself,
|
|
14
|
+
* and never the raw PII the vault holds. A receipt records *that* a decision
|
|
15
|
+
* happened over *this* content, not the content.
|
|
16
|
+
*/
|
|
17
|
+
import { contentHash, signPayload } from "@edgeproc/avow";
|
|
18
|
+
// Re-exported so receipt verifiers can recompute `args_digest` with the SAME
|
|
19
|
+
// canonical hash the sealer used, without importing @edgeproc/avow directly.
|
|
20
|
+
export { contentHash } from "@edgeproc/avow";
|
|
21
|
+
/**
|
|
22
|
+
* Version tag for the Tier-1 deterministic detector ruleset. Bumping the
|
|
23
|
+
* ruleset bumps this so a verifier can tell which detector version screened
|
|
24
|
+
* the content behind a receipt.
|
|
25
|
+
*/
|
|
26
|
+
export const DETECTOR_VERSION = "1";
|
|
27
|
+
/** Build the signed subject, digesting the redacted text and nothing else. */
|
|
28
|
+
export async function buildEgressSubject(input) {
|
|
29
|
+
return {
|
|
30
|
+
action: "llm.egress",
|
|
31
|
+
provider: input.provider,
|
|
32
|
+
args_digest: await contentHash({ redactedText: input.redactedText }),
|
|
33
|
+
decision: input.decision,
|
|
34
|
+
detector_version: input.detectorVersion ?? DETECTOR_VERSION,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Seal an egress decision into a signed, pinned-key-verifiable receipt. */
|
|
38
|
+
export async function sealEgressReceipt(input, seedHex) {
|
|
39
|
+
return signPayload(await buildEgressSubject(input), seedHex);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=egressReceipt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"egressReceipt.js","sourceRoot":"","sources":["../src/egressReceipt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,WAAW,EAAsB,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE9E,6EAA6E;AAC7E,6EAA6E;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAmB7C;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAWpC,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAyB;IAEzB,OAAO;QACL,MAAM,EAAE,YAAY;QACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,WAAW,EAAE,MAAM,WAAW,CAAC,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC;QACpE,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,gBAAgB,EAAE,KAAK,CAAC,eAAe,IAAI,gBAAgB;KAC5D,CAAC;AACJ,CAAC;AAkBD,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,KAAyB,EACzB,OAAe;IAEf,OAAO,WAAW,CAAC,MAAM,kBAAkB,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;AAC/D,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failures of the privacy boundary. Every one of these is a FAIL-CLOSED
|
|
3
|
+
* signal: the safe reaction is to stop the send/restore, not to continue.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by {@link approve} when handed an object the redaction pipeline never
|
|
7
|
+
* minted. A hand-built "pending" could smuggle unreviewed raw text past the
|
|
8
|
+
* guard, so it is rejected outright.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ForgedPayloadError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Thrown by every provider adapter (via {@link assertApproved}) when handed a
|
|
15
|
+
* payload the egress guard never approved — e.g. a structurally identical
|
|
16
|
+
* object hand-built by the caller. The capability is checked by IDENTITY in a
|
|
17
|
+
* module-private registry, so it holds at runtime in plain JS, not only in the
|
|
18
|
+
* type system.
|
|
19
|
+
*/
|
|
20
|
+
export declare class UnapprovedPayloadError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown by {@link redactForEgress} when the input already contains text
|
|
25
|
+
* matching the placeholder grammar (`[TYPE_1]`). Such text is
|
|
26
|
+
* indistinguishable from a vault token after redaction, so rehydration would
|
|
27
|
+
* silently substitute vault values into text that never contained them —
|
|
28
|
+
* fail closed instead of weakening reversibility.
|
|
29
|
+
*/
|
|
30
|
+
export declare class PlaceholderCollisionError extends Error {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Thrown by {@link rehydrate} when the vault it was handed is not the vault
|
|
35
|
+
* the payload was redacted with. Restoring with the wrong vault silently
|
|
36
|
+
* yields wrong or missing values — fail closed instead.
|
|
37
|
+
*/
|
|
38
|
+
export declare class VaultMismatchError extends Error {
|
|
39
|
+
constructor(message: string);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Thrown by {@link rehydrate} when a placeholder-shaped token (`[TYPE_n]`) in
|
|
43
|
+
* the reply cannot be resolved by the bound vault. Under a bound vault an
|
|
44
|
+
* unresolvable token is an anomaly — the reply was redacted with a different
|
|
45
|
+
* vault, or a model invented a placeholder — so the safe reaction is to stop,
|
|
46
|
+
* not to leave a token that merely *looks* like redaction in place.
|
|
47
|
+
*/
|
|
48
|
+
export declare class UnresolvedPlaceholderError extends Error {
|
|
49
|
+
constructor(message: string);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Thrown by {@link redactForEgress} when a value the detector recognised (and
|
|
53
|
+
* wrote into the vault) still appears verbatim in the redacted output — e.g. a
|
|
54
|
+
* second, unlabelled copy of an account number that the label-gated rule only
|
|
55
|
+
* matched once. Emitting it would put a value the tool already knows is PII on
|
|
56
|
+
* the wire, so redaction fails closed instead of leaking it. The reversibility
|
|
57
|
+
* backstop is the human preview; this is the belt to that suspenders.
|
|
58
|
+
*/
|
|
59
|
+
export declare class ResidualValueError extends Error {
|
|
60
|
+
constructor(message: string);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;GAMG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;GAMG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;GAMG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed failures of the privacy boundary. Every one of these is a FAIL-CLOSED
|
|
3
|
+
* signal: the safe reaction is to stop the send/restore, not to continue.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by {@link approve} when handed an object the redaction pipeline never
|
|
7
|
+
* minted. A hand-built "pending" could smuggle unreviewed raw text past the
|
|
8
|
+
* guard, so it is rejected outright.
|
|
9
|
+
*/
|
|
10
|
+
export class ForgedPayloadError extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ForgedPayloadError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Thrown by every provider adapter (via {@link assertApproved}) when handed a
|
|
18
|
+
* payload the egress guard never approved — e.g. a structurally identical
|
|
19
|
+
* object hand-built by the caller. The capability is checked by IDENTITY in a
|
|
20
|
+
* module-private registry, so it holds at runtime in plain JS, not only in the
|
|
21
|
+
* type system.
|
|
22
|
+
*/
|
|
23
|
+
export class UnapprovedPayloadError extends Error {
|
|
24
|
+
constructor(message) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "UnapprovedPayloadError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Thrown by {@link redactForEgress} when the input already contains text
|
|
31
|
+
* matching the placeholder grammar (`[TYPE_1]`). Such text is
|
|
32
|
+
* indistinguishable from a vault token after redaction, so rehydration would
|
|
33
|
+
* silently substitute vault values into text that never contained them —
|
|
34
|
+
* fail closed instead of weakening reversibility.
|
|
35
|
+
*/
|
|
36
|
+
export class PlaceholderCollisionError extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "PlaceholderCollisionError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Thrown by {@link rehydrate} when the vault it was handed is not the vault
|
|
44
|
+
* the payload was redacted with. Restoring with the wrong vault silently
|
|
45
|
+
* yields wrong or missing values — fail closed instead.
|
|
46
|
+
*/
|
|
47
|
+
export class VaultMismatchError extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "VaultMismatchError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Thrown by {@link rehydrate} when a placeholder-shaped token (`[TYPE_n]`) in
|
|
55
|
+
* the reply cannot be resolved by the bound vault. Under a bound vault an
|
|
56
|
+
* unresolvable token is an anomaly — the reply was redacted with a different
|
|
57
|
+
* vault, or a model invented a placeholder — so the safe reaction is to stop,
|
|
58
|
+
* not to leave a token that merely *looks* like redaction in place.
|
|
59
|
+
*/
|
|
60
|
+
export class UnresolvedPlaceholderError extends Error {
|
|
61
|
+
constructor(message) {
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = "UnresolvedPlaceholderError";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Thrown by {@link redactForEgress} when a value the detector recognised (and
|
|
68
|
+
* wrote into the vault) still appears verbatim in the redacted output — e.g. a
|
|
69
|
+
* second, unlabelled copy of an account number that the label-gated rule only
|
|
70
|
+
* matched once. Emitting it would put a value the tool already knows is PII on
|
|
71
|
+
* the wire, so redaction fails closed instead of leaking it. The reversibility
|
|
72
|
+
* backstop is the human preview; this is the belt to that suspenders.
|
|
73
|
+
*/
|
|
74
|
+
export class ResidualValueError extends Error {
|
|
75
|
+
constructor(message) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = "ResidualValueError";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAC/C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,yBAA0B,SAAQ,KAAK;IAClD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,2BAA2B,CAAC;IAC1C,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IACnD,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;IAC3C,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@edgeproc/privacy-core` — a browser-side privacy boundary for LLM calls.
|
|
3
|
+
*
|
|
4
|
+
* Raw private text stays on-device; only policy-approved, redacted text can
|
|
5
|
+
* reach a provider; the model's reply is rehydrated locally. The load-bearing
|
|
6
|
+
* idea is the type-enforced Egress Guard: provider adapters accept ONLY a
|
|
7
|
+
* branded {@link RedactedPayload}, so handing raw text to a provider is a
|
|
8
|
+
* compile error.
|
|
9
|
+
*
|
|
10
|
+
* This barrel is the PRODUCTION surface. Test fixtures live behind the
|
|
11
|
+
* `@edgeproc/privacy-core/testing` subpath, never here.
|
|
12
|
+
*/
|
|
13
|
+
export { detect } from "./detect/detector.js";
|
|
14
|
+
export { approve, assertApproved, guardedProvider, type LlmProvider, type PendingRedaction, type RedactedPayload, unsafeBypass, } from "./egress.js";
|
|
15
|
+
export { buildEgressSubject, contentHash, DETECTOR_VERSION, type EgressDecision, type EgressGovernance, type EgressSubject, type EgressSubjectInput, sealEgressReceipt, } from "./egressReceipt.js";
|
|
16
|
+
export { ForgedPayloadError, PlaceholderCollisionError, ResidualValueError, UnapprovedPayloadError, UnresolvedPlaceholderError, VaultMismatchError, } from "./errors.js";
|
|
17
|
+
export { makeProvider, type ProviderConfig, type SelectedProvider, } from "./providers/factory.js";
|
|
18
|
+
export { NoLLMProvider } from "./providers/nollm.js";
|
|
19
|
+
export { type OpenRouterConfig, OpenRouterProvider, } from "./providers/openrouter.js";
|
|
20
|
+
export { redactForEgress } from "./redact.js";
|
|
21
|
+
export { rehydrate } from "./rehydrate.js";
|
|
22
|
+
export type { AuditEntry, AuditSink, EntityType, RedactedResponse, Span, VaultRef, } from "./types.js";
|
|
23
|
+
export { Vault } from "./vault.js";
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAE9C,OAAO,EACL,OAAO,EACP,cAAc,EACd,eAAe,EACf,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACtB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EACL,KAAK,gBAAgB,EACrB,kBAAkB,GACnB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,YAAY,EACV,UAAU,EACV,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,IAAI,EACJ,QAAQ,GACT,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@edgeproc/privacy-core` — a browser-side privacy boundary for LLM calls.
|
|
3
|
+
*
|
|
4
|
+
* Raw private text stays on-device; only policy-approved, redacted text can
|
|
5
|
+
* reach a provider; the model's reply is rehydrated locally. The load-bearing
|
|
6
|
+
* idea is the type-enforced Egress Guard: provider adapters accept ONLY a
|
|
7
|
+
* branded {@link RedactedPayload}, so handing raw text to a provider is a
|
|
8
|
+
* compile error.
|
|
9
|
+
*
|
|
10
|
+
* This barrel is the PRODUCTION surface. Test fixtures live behind the
|
|
11
|
+
* `@edgeproc/privacy-core/testing` subpath, never here.
|
|
12
|
+
*/
|
|
13
|
+
// Detection spine.
|
|
14
|
+
export { detect } from "./detect/detector.js";
|
|
15
|
+
// The egress boundary.
|
|
16
|
+
export { approve, assertApproved, guardedProvider, unsafeBypass, } from "./egress.js";
|
|
17
|
+
// Signed egress receipts (the Writ-style effect face).
|
|
18
|
+
export { buildEgressSubject, contentHash, DETECTOR_VERSION, sealEgressReceipt, } from "./egressReceipt.js";
|
|
19
|
+
// Typed fail-closed errors.
|
|
20
|
+
export { ForgedPayloadError, PlaceholderCollisionError, ResidualValueError, UnapprovedPayloadError, UnresolvedPlaceholderError, VaultMismatchError, } from "./errors.js";
|
|
21
|
+
export { makeProvider, } from "./providers/factory.js";
|
|
22
|
+
// Providers.
|
|
23
|
+
export { NoLLMProvider } from "./providers/nollm.js";
|
|
24
|
+
export { OpenRouterProvider, } from "./providers/openrouter.js";
|
|
25
|
+
// The reversible loop.
|
|
26
|
+
export { redactForEgress } from "./redact.js";
|
|
27
|
+
export { rehydrate } from "./rehydrate.js";
|
|
28
|
+
export { Vault } from "./vault.js";
|
|
29
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,mBAAmB;AACnB,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,uBAAuB;AACvB,OAAO,EACL,OAAO,EACP,cAAc,EACd,eAAe,EAIf,YAAY,GACb,MAAM,aAAa,CAAC;AACrB,uDAAuD;AACvD,OAAO,EACL,kBAAkB,EAClB,WAAW,EACX,gBAAgB,EAKhB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAC5B,4BAA4B;AAC5B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,EAC1B,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,YAAY,GAGb,MAAM,wBAAwB,CAAC;AAChC,aAAa;AACb,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAEL,kBAAkB,GACnB,MAAM,2BAA2B,CAAC;AACnC,uBAAuB;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAU3C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LlmProvider } from "../egress.js";
|
|
2
|
+
/** Config for {@link makeProvider}. Host apps source these from their own env. */
|
|
3
|
+
export interface ProviderConfig {
|
|
4
|
+
readonly apiKey?: string | undefined;
|
|
5
|
+
readonly model?: string | undefined;
|
|
6
|
+
readonly endpoint?: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
/** A live provider plus a label for the UI to show which one is selected. */
|
|
9
|
+
export interface SelectedProvider {
|
|
10
|
+
readonly provider: LlmProvider;
|
|
11
|
+
readonly label: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Pick a provider from config. With a non-empty API key, the OpenRouter path is
|
|
15
|
+
* selected; otherwise it falls back to the offline echo so the demo runs cold.
|
|
16
|
+
* The library stays env-agnostic — the host (e.g. the Vite demo) reads its own
|
|
17
|
+
* env and passes the values in.
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeProvider(config?: ProviderConfig): SelectedProvider;
|
|
20
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/providers/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,kFAAkF;AAClF,MAAM,WAAW,cAAc;IAG7B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAGpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAED,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAID;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,cAAmB,GAAG,gBAAgB,CAgB1E"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NoLLMProvider } from "./nollm.js";
|
|
2
|
+
import { OpenRouterProvider } from "./openrouter.js";
|
|
3
|
+
const DEFAULT_MODEL = "openai/gpt-4o-mini";
|
|
4
|
+
/**
|
|
5
|
+
* Pick a provider from config. With a non-empty API key, the OpenRouter path is
|
|
6
|
+
* selected; otherwise it falls back to the offline echo so the demo runs cold.
|
|
7
|
+
* The library stays env-agnostic — the host (e.g. the Vite demo) reads its own
|
|
8
|
+
* env and passes the values in.
|
|
9
|
+
*/
|
|
10
|
+
export function makeProvider(config = {}) {
|
|
11
|
+
const model = config.model ?? DEFAULT_MODEL;
|
|
12
|
+
if (config.apiKey?.trim()) {
|
|
13
|
+
return {
|
|
14
|
+
provider: new OpenRouterProvider({
|
|
15
|
+
apiKey: config.apiKey,
|
|
16
|
+
model,
|
|
17
|
+
endpoint: config.endpoint,
|
|
18
|
+
}),
|
|
19
|
+
label: `OpenRouter · ${model}`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
provider: new NoLLMProvider(),
|
|
24
|
+
label: "NoLLMProvider (offline echo)",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=factory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/providers/factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAmBrD,MAAM,aAAa,GAAG,oBAAoB,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAAM,GAAmB,EAAE;IACtD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,aAAa,CAAC;IAC5C,IAAI,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC;QAC1B,OAAO;YACL,QAAQ,EAAE,IAAI,kBAAkB,CAAC;gBAC/B,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,KAAK;gBACL,QAAQ,EAAE,MAAM,CAAC,QAAQ;aAC1B,CAAC;YACF,KAAK,EAAE,gBAAgB,KAAK,EAAE;SAC/B,CAAC;IACJ,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,IAAI,aAAa,EAAE;QAC7B,KAAK,EAAE,8BAA8B;KACtC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type LlmProvider, type RedactedPayload } from "../egress.js";
|
|
2
|
+
import type { RedactedResponse } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Offline fallback provider — runs with NO API key. It does NOT touch the
|
|
5
|
+
* network; it echoes a plausible analyst reply that references the same
|
|
6
|
+
* placeholders, so the rehydrate step has something to restore. This is what
|
|
7
|
+
* makes the demo runnable cold, with the wow-loop fully intact.
|
|
8
|
+
*/
|
|
9
|
+
export declare class NoLLMProvider implements LlmProvider {
|
|
10
|
+
complete(payload: RedactedPayload): Promise<RedactedResponse>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=nollm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nollm.d.ts","sourceRoot":"","sources":["../../src/providers/nollm.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;GAKG;AACH,qBAAa,aAAc,YAAW,WAAW;IACzC,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqBlE;CACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { assertApproved, } from "../egress.js";
|
|
2
|
+
/**
|
|
3
|
+
* Offline fallback provider — runs with NO API key. It does NOT touch the
|
|
4
|
+
* network; it echoes a plausible analyst reply that references the same
|
|
5
|
+
* placeholders, so the rehydrate step has something to restore. This is what
|
|
6
|
+
* makes the demo runnable cold, with the wow-loop fully intact.
|
|
7
|
+
*/
|
|
8
|
+
export class NoLLMProvider {
|
|
9
|
+
async complete(payload) {
|
|
10
|
+
// Runtime guard: even the offline echo refuses a forged payload.
|
|
11
|
+
assertApproved(payload);
|
|
12
|
+
const tokens = [...payload.redactedText.matchAll(/\[[A-Z]+_\d+\]/g)].map((m) => m[0]);
|
|
13
|
+
const list = tokens.length ? tokens.join(", ") : "no sensitive values";
|
|
14
|
+
const lines = [
|
|
15
|
+
"Summary (offline echo — no API key set):",
|
|
16
|
+
`I reviewed your statement. It referenced ${tokens.length} redacted value(s): ${list}.`,
|
|
17
|
+
];
|
|
18
|
+
// Only ever echo placeholders that ACTUALLY came from this payload — the
|
|
19
|
+
// vault can resolve those. Inventing a token the vault never minted would
|
|
20
|
+
// (correctly) trip rehydrate's fail-closed guard.
|
|
21
|
+
if (tokens[0]) {
|
|
22
|
+
lines.push(`The first flagged value, ${tokens[0]}, is the one to check.`);
|
|
23
|
+
}
|
|
24
|
+
lines.push("(Set OPENROUTER_API_KEY + VITE_USE_OPENROUTER=1 to call a real model via the dev proxy.)");
|
|
25
|
+
return { redactedText: lines.join("\n") };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=nollm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nollm.js","sourceRoot":"","sources":["../../src/providers/nollm.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,cAAc,CAAC;AAGtB;;;;;GAKG;AACH,MAAM,OAAO,aAAa;IACxB,KAAK,CAAC,QAAQ,CAAC,OAAwB;QACrC,iEAAiE;QACjE,cAAc,CAAC,OAAO,CAAC,CAAC;QACxB,MAAM,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC,CAAC,GAAG,CACtE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CACZ,CAAC;QACF,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,qBAAqB,CAAC;QACvE,MAAM,KAAK,GAAG;YACZ,0CAA0C;YAC1C,4CAA4C,MAAM,CAAC,MAAM,uBAAuB,IAAI,GAAG;SACxF,CAAC;QACF,yEAAyE;QACzE,0EAA0E;QAC1E,kDAAkD;QAClD,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YACd,KAAK,CAAC,IAAI,CAAC,4BAA4B,MAAM,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC;QAC5E,CAAC;QACD,KAAK,CAAC,IAAI,CACR,0FAA0F,CAC3F,CAAC;QACF,OAAO,EAAE,YAAY,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC5C,CAAC;CACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type LlmProvider, type RedactedPayload } from "../egress.js";
|
|
2
|
+
import type { RedactedResponse } from "../types.js";
|
|
3
|
+
export interface OpenRouterConfig {
|
|
4
|
+
readonly apiKey: string;
|
|
5
|
+
readonly model: string;
|
|
6
|
+
readonly endpoint?: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* OpenRouter (OpenAI-compatible chat/completions). It accepts ONLY a
|
|
10
|
+
* RedactedPayload, so by construction only placeholder text can ever be put on
|
|
11
|
+
* the wire — the raw values are never in scope here.
|
|
12
|
+
*/
|
|
13
|
+
export declare class OpenRouterProvider implements LlmProvider {
|
|
14
|
+
private readonly cfg;
|
|
15
|
+
constructor(cfg: OpenRouterConfig);
|
|
16
|
+
complete(payload: RedactedPayload): Promise<RedactedResponse>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=openrouter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openrouter.d.ts","sourceRoot":"","sources":["../../src/providers/openrouter.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAgBD;;;;GAIG;AACH,qBAAa,kBAAmB,YAAW,WAAW;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAhC,YAA6B,GAAG,EAAE,gBAAgB,EAAI;IAEhD,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAwBlE;CACF"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { assertApproved, } from "../egress.js";
|
|
2
|
+
const DEFAULT_ENDPOINT = "https://openrouter.ai/api/v1/chat/completions";
|
|
3
|
+
const SYSTEM_PROMPT = "You are a personal finance assistant. The user's text has had sensitive " +
|
|
4
|
+
"values replaced by typed placeholders like [CARD_1] or [NAME_2]. Reason " +
|
|
5
|
+
"about the placeholders as opaque tokens and reuse the SAME placeholders in " +
|
|
6
|
+
"your reply. Never invent real values.";
|
|
7
|
+
/**
|
|
8
|
+
* OpenRouter (OpenAI-compatible chat/completions). It accepts ONLY a
|
|
9
|
+
* RedactedPayload, so by construction only placeholder text can ever be put on
|
|
10
|
+
* the wire — the raw values are never in scope here.
|
|
11
|
+
*/
|
|
12
|
+
export class OpenRouterProvider {
|
|
13
|
+
cfg;
|
|
14
|
+
constructor(cfg) {
|
|
15
|
+
this.cfg = cfg;
|
|
16
|
+
}
|
|
17
|
+
async complete(payload) {
|
|
18
|
+
// Runtime guard: a structurally forged payload never reaches the wire.
|
|
19
|
+
assertApproved(payload);
|
|
20
|
+
const body = JSON.stringify({
|
|
21
|
+
model: this.cfg.model,
|
|
22
|
+
messages: [
|
|
23
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
24
|
+
{ role: "user", content: payload.redactedText },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
const res = await fetch(this.cfg.endpoint ?? DEFAULT_ENDPOINT, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"content-type": "application/json",
|
|
31
|
+
authorization: `Bearer ${this.cfg.apiKey}`,
|
|
32
|
+
},
|
|
33
|
+
body,
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new Error(`OpenRouter ${res.status}: ${await res.text()}`);
|
|
37
|
+
}
|
|
38
|
+
const json = (await res.json());
|
|
39
|
+
const content = json.choices?.[0]?.message?.content ?? "";
|
|
40
|
+
return { redactedText: content };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=openrouter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openrouter.js","sourceRoot":"","sources":["../../src/providers/openrouter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,cAAc,CAAC;AAStB,MAAM,gBAAgB,GAAG,+CAA+C,CAAC;AAEzE,MAAM,aAAa,GACjB,0EAA0E;IAC1E,0EAA0E;IAC1E,6EAA6E;IAC7E,uCAAuC,CAAC;AAQ1C;;;;GAIG;AACH,MAAM,OAAO,kBAAkB;IACA,GAAG;IAAhC,YAA6B,GAAqB;mBAArB,GAAG;IAAqB,CAAC;IAEtD,KAAK,CAAC,QAAQ,CAAC,OAAwB;QACrC,uEAAuE;QACvE,cAAc,CAAC,OAAO,CAAC,CAAC;QACxB,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC;YAC1B,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK;YACrB,QAAQ,EAAE;gBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,EAAE;gBAC1C,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,YAAY,EAAE;aAChD;SACF,CAAC,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,IAAI,gBAAgB,EAAE;YAC7D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE;aAC3C;YACD,IAAI;SACL,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CAAC,cAAc,GAAG,CAAC,MAAM,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACnE,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAmB,CAAC;QAClD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,CAAC;QAC1D,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;IACnC,CAAC;CACF"}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type PendingRedaction } from "./egress.js";
|
|
2
|
+
import type { AuditSink } from "./types.js";
|
|
3
|
+
import type { Vault } from "./vault.js";
|
|
4
|
+
/**
|
|
5
|
+
* THE ONLY legitimate constructor of a PendingRedaction.
|
|
6
|
+
*
|
|
7
|
+
* Detects PII, writes each raw value into the vault, and replaces it with a
|
|
8
|
+
* stable placeholder. The result is a review PROPOSAL — it is NOT sendable.
|
|
9
|
+
* Only the explicit `approve()` step turns it into the RedactedPayload
|
|
10
|
+
* capability a provider will accept, and that holds even when detection finds
|
|
11
|
+
* nothing: zero detections never auto-approve.
|
|
12
|
+
*/
|
|
13
|
+
export declare function redactForEgress(raw: string, vault: Vault, audit?: AuditSink): Promise<PendingRedaction>;
|
|
14
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE1E,OAAO,KAAK,EAAE,SAAS,EAAQ,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAoDxC;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,KAAK,EACZ,KAAK,CAAC,EAAE,SAAS,GAChB,OAAO,CAAC,gBAAgB,CAAC,CAyB3B"}
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { detect } from "./detect/detector.js";
|
|
2
|
+
import { mintPendingRedaction } from "./egress.js";
|
|
3
|
+
import { PlaceholderCollisionError, ResidualValueError } from "./errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* The placeholder grammar (same shape `rehydrate` restores). Input containing
|
|
6
|
+
* it would be indistinguishable from vault tokens after redaction.
|
|
7
|
+
*/
|
|
8
|
+
const PLACEHOLDER_SHAPE = /\[[A-Z]+_\d+\]/;
|
|
9
|
+
/** Escape a raw value so it can be embedded literally inside a RegExp. */
|
|
10
|
+
function escapeForRegExp(value) {
|
|
11
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Match the value only as a STANDALONE token: no alphanumeric character may sit
|
|
15
|
+
* immediately before or after it. This is the \b-style guard for values whose
|
|
16
|
+
* edges are digits/letters (every label-gated ACCOUNT/ROUTING value is), and it
|
|
17
|
+
* is what separates a real leak (`... 021000021` standalone) from a benign
|
|
18
|
+
* superstring (`0123456789A`, `1002003009`) that merely contains the digits.
|
|
19
|
+
* For a value whose own edge is a non-word char the adjacent guard is simply
|
|
20
|
+
* inert there, degrading to a raw-presence check — safe, since such a value
|
|
21
|
+
* cannot be a substring of a longer alphanumeric token anyway.
|
|
22
|
+
*/
|
|
23
|
+
function survivesAsToken(out, value) {
|
|
24
|
+
const standalone = new RegExp(`(?<![A-Za-z0-9])${escapeForRegExp(value)}(?![A-Za-z0-9])`);
|
|
25
|
+
return standalone.test(out);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Fail closed if any detected value survived as a standalone token in the
|
|
29
|
+
* redacted output. The span-by-span rebuild only removes the ranges the
|
|
30
|
+
* detector matched, so a value that recurs where the ruleset did not
|
|
31
|
+
* independently match it (a duplicate copy of a label-gated ACCOUNT/ROUTING
|
|
32
|
+
* number) can slip through. A detected value is one the tool already vaulted as
|
|
33
|
+
* PII; emitting a verbatim standalone copy would leak it, so refuse rather than
|
|
34
|
+
* send. A value that survives only as a substring of a longer alphanumeric
|
|
35
|
+
* token (a benign order id / tracking number) is NOT a leak and does not refuse.
|
|
36
|
+
*/
|
|
37
|
+
function assertNoResidual(out, spans) {
|
|
38
|
+
for (const s of spans) {
|
|
39
|
+
if (survivesAsToken(out, s.value)) {
|
|
40
|
+
throw new ResidualValueError(`a detected ${s.type} value survived redaction and would cross the wire ` +
|
|
41
|
+
"verbatim (a duplicate the ruleset matched only once) — refusing to " +
|
|
42
|
+
"emit a payload that leaks a value already known to be PII");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* THE ONLY legitimate constructor of a PendingRedaction.
|
|
48
|
+
*
|
|
49
|
+
* Detects PII, writes each raw value into the vault, and replaces it with a
|
|
50
|
+
* stable placeholder. The result is a review PROPOSAL — it is NOT sendable.
|
|
51
|
+
* Only the explicit `approve()` step turns it into the RedactedPayload
|
|
52
|
+
* capability a provider will accept, and that holds even when detection finds
|
|
53
|
+
* nothing: zero detections never auto-approve.
|
|
54
|
+
*/
|
|
55
|
+
export async function redactForEgress(raw, vault, audit) {
|
|
56
|
+
const collision = raw.match(PLACEHOLDER_SHAPE);
|
|
57
|
+
if (collision) {
|
|
58
|
+
throw new PlaceholderCollisionError(`input already contains placeholder-shaped text ("${collision[0]}") — ` +
|
|
59
|
+
"redacting it would make restore ambiguous, so the redaction is refused");
|
|
60
|
+
}
|
|
61
|
+
const spans = detect(raw);
|
|
62
|
+
// Rebuild text left-to-right, swapping each span for its vault token.
|
|
63
|
+
let out = "";
|
|
64
|
+
let cursor = 0;
|
|
65
|
+
const placeholders = [];
|
|
66
|
+
for (const s of spans) {
|
|
67
|
+
out += raw.slice(cursor, s.start);
|
|
68
|
+
const token = vault.tokenize(s.type, s.value);
|
|
69
|
+
out += token;
|
|
70
|
+
placeholders.push(token);
|
|
71
|
+
cursor = s.end;
|
|
72
|
+
}
|
|
73
|
+
out += raw.slice(cursor);
|
|
74
|
+
// Fail closed BEFORE auditing/minting: a redact that leaks is not a redact.
|
|
75
|
+
assertNoResidual(out, spans);
|
|
76
|
+
audit?.({ kind: "redact", at: Date.now(), placeholders });
|
|
77
|
+
return mintPendingRedaction(out, vault.ref, placeholders);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAyB,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAI5E;;;GAGG;AACH,MAAM,iBAAiB,GAAG,gBAAgB,CAAC;AAE3C,0EAA0E;AAC1E,SAAS,eAAe,CAAC,KAAa;IACpC,OAAO,KAAK,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,eAAe,CAAC,GAAW,EAAE,KAAa;IACjD,MAAM,UAAU,GAAG,IAAI,MAAM,CAC3B,mBAAmB,eAAe,CAAC,KAAK,CAAC,iBAAiB,CAC3D,CAAC;IACF,OAAO,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,gBAAgB,CAAC,GAAW,EAAE,KAAsB;IAC3D,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,eAAe,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,kBAAkB,CAC1B,cAAc,CAAC,CAAC,IAAI,qDAAqD;gBACvE,qEAAqE;gBACrE,2DAA2D,CAC9D,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,GAAW,EACX,KAAY,EACZ,KAAiB;IAEjB,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAC/C,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,IAAI,yBAAyB,CACjC,oDAAoD,SAAS,CAAC,CAAC,CAAC,OAAO;YACrE,wEAAwE,CAC3E,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,sEAAsE;IACtE,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;QAC9C,GAAG,IAAI,KAAK,CAAC;QACb,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC;IACjB,CAAC;IACD,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACzB,4EAA4E;IAC5E,gBAAgB,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAC7B,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;IAC1D,OAAO,oBAAoB,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;AAC5D,CAAC"}
|