@g8r-security/agent-shield-sdk 0.1.0 → 0.1.2
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/README.md +21 -15
- package/dist/index.d.mts +61 -11
- package/dist/index.d.ts +61 -11
- package/dist/index.js +56 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +52 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,11 +11,12 @@ This is a **TypeScript source package** — no compile step. Consumers reference
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import { AgentShield } from '@g8r-security/agent-shield-sdk';
|
|
14
|
+
import { AgentShield, tenantId } from '@g8r-security/agent-shield-sdk';
|
|
15
15
|
|
|
16
16
|
const shield = new AgentShield({
|
|
17
17
|
consoleUrl: 'https://shield.yourcompany.com',
|
|
18
18
|
apiKey: 'sk-shield-...',
|
|
19
|
+
tenantId: tenantId('acme-corp'),
|
|
19
20
|
agentId: 'enterprise-assistant',
|
|
20
21
|
department: 'Finance',
|
|
21
22
|
userId: 'usr_FIN_042',
|
|
@@ -75,27 +76,32 @@ import { redactSensitiveData } from '@g8r-security/agent-shield-sdk';
|
|
|
75
76
|
const { redacted, tokensReplaced } = redactSensitiveData(input);
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
##
|
|
79
|
+
## Redaction Layer
|
|
79
80
|
|
|
80
|
-
Sensitive data is detected and replaced **locally** before
|
|
81
|
+
Sensitive data is detected and replaced **locally** before the prompt leaves the process — on both the policy-check and audit-log paths, so the gateway never receives recognized raw secrets.
|
|
82
|
+
|
|
83
|
+
> ⚠️ **Best-effort, not exhaustive.** Redaction is pattern- and entropy-based. It catches the formats listed below, but it **cannot** catch every secret or PII shape — unstructured PII (names, addresses), free-form secrets below the entropy threshold, or novel token formats may pass through. Treat this as one layer of defense-in-depth, not a compliance guarantee, and keep downstream controls and human review in place.
|
|
81
84
|
|
|
82
85
|
### Detection Patterns
|
|
83
86
|
|
|
84
|
-
| Pattern | Label |
|
|
85
|
-
|
|
86
|
-
| BIP-32 extended keys (`xpub`, `xprv`, `ypub`, …) | `[REDACTED:BIP32_KEY]` |
|
|
87
|
-
| WIF private keys (Base58, starts with `5`, `K`, or `L`) | `[REDACTED:WIF_KEY]` |
|
|
88
|
-
| 256-bit hex keys (64 hex chars, optional `0x` prefix) | `[REDACTED:HEX_KEY]` |
|
|
89
|
-
| PEM private / public key blocks | `[REDACTED:PEM_KEY]` |
|
|
90
|
-
| `custodial-id:…` | `[REDACTED:CUSTODIAL_ID]`
|
|
91
|
-
|
|
|
92
|
-
| `
|
|
93
|
-
|
|
|
94
|
-
|
|
|
87
|
+
| Pattern | Label |
|
|
88
|
+
|---|---|
|
|
89
|
+
| BIP-32 extended keys (`xpub`, `xprv`, `ypub`, …) | `[REDACTED:BIP32_KEY]` |
|
|
90
|
+
| WIF private keys (Base58, starts with `5`, `K`, or `L`) | `[REDACTED:WIF_KEY]` |
|
|
91
|
+
| 256-bit hex keys (64 hex chars, optional `0x` prefix) | `[REDACTED:HEX_KEY]` |
|
|
92
|
+
| PEM private / public key blocks | `[REDACTED:PEM_KEY]` |
|
|
93
|
+
| `custodial-id:…` / `cust-{digits}` / `wallet-id:…` / `vault-id:…` | `[REDACTED:CUSTODIAL_ID]` etc. |
|
|
94
|
+
| Card numbers (13–19 digits, Luhn-validated) | `[REDACTED:CARD]` |
|
|
95
|
+
| US SSNs (`123-45-6789`) | `[REDACTED:SSN]` |
|
|
96
|
+
| Email addresses | `[REDACTED:EMAIL]` |
|
|
97
|
+
| Phone numbers (separated, e.g. `415-555-0199`) | `[REDACTED:PHONE]` |
|
|
98
|
+
| High Shannon entropy strings (≥4.5 bits/char, ≥32 chars) | `[REDACTED:HIGH_ENTROPY]` |
|
|
99
|
+
|
|
100
|
+
These map to controls such as **GDPR Art. 32** (security of processing) and **PCI-DSS** PAN handling by reducing sensitive-data exposure — they *support* those controls rather than satisfy them on their own.
|
|
95
101
|
|
|
96
102
|
### Shannon Entropy Detection
|
|
97
103
|
|
|
98
|
-
Any token that is 32+ characters with Shannon entropy ≥ 4.5 bits/char is caught as a generic high-entropy secret. This
|
|
104
|
+
Any token that is 32+ characters with Shannon entropy ≥ 4.5 bits/char is caught as a generic high-entropy secret. This is a best-effort catch for many API keys and tokens that don't match a known format — but secrets shorter than 32 chars or below the entropy threshold will not be caught.
|
|
99
105
|
|
|
100
106
|
```
|
|
101
107
|
H = -Σ p(c) × log₂(p(c)) for each unique character c
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Branded id types + constructors used by the SDK.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the id types/helpers the SDK actually uses are copied here.
|
|
4
|
+
* Self-contained in the SDK so the published `@g8r-security/agent-shield-sdk`
|
|
5
|
+
* package has no dependency on internal packages. Only the id types/helpers the
|
|
6
|
+
* SDK actually uses are included here.
|
|
8
7
|
*/
|
|
9
8
|
/**
|
|
10
9
|
* Identifies a single tenant in the multi-tenant governance plane.
|
|
@@ -17,21 +16,68 @@ type TenantId = string & {
|
|
|
17
16
|
type RequestId = string & {
|
|
18
17
|
readonly __brand: 'RequestId';
|
|
19
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Cast a string to TenantId. Use at boundaries (config, env, request body).
|
|
21
|
+
* Enforces the charset / length contract: 1-64 chars of `[a-z0-9-]`,
|
|
22
|
+
* starting with `[a-z0-9]` — catches programmatic misuse (config typos,
|
|
23
|
+
* hard-coded slugs that drift).
|
|
24
|
+
*/
|
|
25
|
+
declare function tenantId(s: string): TenantId;
|
|
26
|
+
/** Generate a fresh request ID. Prefers crypto.randomUUID where available. */
|
|
27
|
+
declare function newRequestId(): RequestId;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Local-First Sensitive Data Redaction
|
|
31
|
+
*
|
|
32
|
+
* Best-effort redaction of common secret and PII formats BEFORE prompts reach
|
|
33
|
+
* the policy gateway. Covers cryptographic key formats, custodial identifiers,
|
|
34
|
+
* high-entropy strings, and common PII (card numbers validated via Luhn, US
|
|
35
|
+
* SSNs, email addresses, and phone numbers).
|
|
36
|
+
*
|
|
37
|
+
* IMPORTANT — this is a defense-in-depth layer, NOT a guarantee of completeness.
|
|
38
|
+
* Pattern- and entropy-based redaction cannot catch every secret or PII shape
|
|
39
|
+
* (e.g. unstructured PII, names, novel token formats, or values below the
|
|
40
|
+
* entropy threshold). Do not rely on it as a sole control; keep downstream
|
|
41
|
+
* safeguards and human review in place.
|
|
42
|
+
*
|
|
43
|
+
* It helps *support* (does not by itself satisfy) controls such as GDPR Art. 32
|
|
44
|
+
* and PCI-DSS PAN-handling by reducing sensitive-data exposure to the gateway.
|
|
45
|
+
*/
|
|
46
|
+
interface RedactionResult {
|
|
47
|
+
/** The input with all sensitive tokens replaced by placeholder strings. */
|
|
48
|
+
redacted: string;
|
|
49
|
+
/** The original sensitive token strings that were replaced. */
|
|
50
|
+
tokensReplaced: string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Redact sensitive data from a prompt string before it reaches the gateway.
|
|
54
|
+
*
|
|
55
|
+
* Processing order (important — PEM first to avoid splitting on inner patterns):
|
|
56
|
+
* 1. PEM private/public key blocks
|
|
57
|
+
* 2. BIP-32 extended keys
|
|
58
|
+
* 3. WIF private keys
|
|
59
|
+
* 4. Raw hex 256-bit keys
|
|
60
|
+
* 5. Custodial IDs (all four variants)
|
|
61
|
+
* 6. PII — card numbers (Luhn-validated), SSNs, emails, phone numbers
|
|
62
|
+
* 7. High-entropy string catch-all
|
|
63
|
+
*/
|
|
64
|
+
declare function redactSensitiveData(input: string): RedactionResult;
|
|
20
65
|
|
|
21
66
|
/**
|
|
22
67
|
* G8R Agent Shield SDK
|
|
23
68
|
*
|
|
24
69
|
* Lightweight TypeScript client that wraps LLM calls with policy enforcement.
|
|
25
|
-
* Automatically intercepts prompts, applies local-first
|
|
70
|
+
* Automatically intercepts prompts, applies best-effort local-first redaction,
|
|
26
71
|
* checks them against the G8R policy engine, and logs all activity to the
|
|
27
72
|
* Agent Shield Console.
|
|
28
73
|
*
|
|
29
74
|
* Usage:
|
|
30
|
-
* import { AgentShield } from '@g8r-security/agent-shield-sdk';
|
|
75
|
+
* import { AgentShield, tenantId } from '@g8r-security/agent-shield-sdk';
|
|
31
76
|
*
|
|
32
77
|
* const shield = new AgentShield({
|
|
33
78
|
* consoleUrl: 'https://shield.yourcompany.com',
|
|
34
79
|
* apiKey: 'sk-shield-...',
|
|
80
|
+
* tenantId: tenantId('acme-corp'),
|
|
35
81
|
* department: 'Finance',
|
|
36
82
|
* userId: 'usr_FIN_042',
|
|
37
83
|
* aiModel: 'GPT-4o',
|
|
@@ -84,7 +130,7 @@ interface PolicyCheckResult {
|
|
|
84
130
|
/**
|
|
85
131
|
* Tokens that were redacted from the prompt before it reached the gateway.
|
|
86
132
|
* Undefined when no tokens were redacted (clean prompt).
|
|
87
|
-
* Populated by the
|
|
133
|
+
* Populated by the local-first redaction layer.
|
|
88
134
|
*/
|
|
89
135
|
redactedTokens?: string[];
|
|
90
136
|
}
|
|
@@ -99,9 +145,10 @@ declare class AgentShield {
|
|
|
99
145
|
/**
|
|
100
146
|
* Check a prompt against the policy engine before sending to the LLM.
|
|
101
147
|
*
|
|
102
|
-
* Applies local-first
|
|
103
|
-
*
|
|
104
|
-
* the
|
|
148
|
+
* Applies best-effort local-first redaction before sending to the gateway, so
|
|
149
|
+
* recognized signing keys, custodial IDs, common PII, and high-entropy secrets
|
|
150
|
+
* are stripped before the prompt leaves the process. Redaction is one layer of
|
|
151
|
+
* defense, not a guarantee that every secret is caught (see redaction.ts).
|
|
105
152
|
*
|
|
106
153
|
* Returns the policy decision without executing the LLM call.
|
|
107
154
|
*/
|
|
@@ -130,6 +177,9 @@ declare class AgentShield {
|
|
|
130
177
|
wrap<T>(llmCallFactory: () => Promise<T>, prompt: string): Promise<T>;
|
|
131
178
|
/**
|
|
132
179
|
* Log an interaction to the Agent Shield Console.
|
|
180
|
+
*
|
|
181
|
+
* Redacts the prompt before transmitting: the audit trail must not store or
|
|
182
|
+
* carry raw secrets/PII, and this is an egress point just like /check.
|
|
133
183
|
*/
|
|
134
184
|
private log;
|
|
135
185
|
}
|
|
@@ -143,4 +193,4 @@ declare class ShieldBlockedError extends Error {
|
|
|
143
193
|
constructor(reason: string, violatedRule: string | null, complianceMappings: PolicyCheckResult['complianceMappings'], sessionRevoked?: boolean);
|
|
144
194
|
}
|
|
145
195
|
|
|
146
|
-
export { AgentShield, type PolicyCheckResult, ShieldBlockedError, type ShieldConfig, type ShieldLogEntry };
|
|
196
|
+
export { AgentShield, type PolicyCheckResult, type RedactionResult, type RequestId, ShieldBlockedError, type ShieldConfig, type ShieldLogEntry, type TenantId, newRequestId, redactSensitiveData, tenantId };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Branded id types + constructors used by the SDK.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the id types/helpers the SDK actually uses are copied here.
|
|
4
|
+
* Self-contained in the SDK so the published `@g8r-security/agent-shield-sdk`
|
|
5
|
+
* package has no dependency on internal packages. Only the id types/helpers the
|
|
6
|
+
* SDK actually uses are included here.
|
|
8
7
|
*/
|
|
9
8
|
/**
|
|
10
9
|
* Identifies a single tenant in the multi-tenant governance plane.
|
|
@@ -17,21 +16,68 @@ type TenantId = string & {
|
|
|
17
16
|
type RequestId = string & {
|
|
18
17
|
readonly __brand: 'RequestId';
|
|
19
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Cast a string to TenantId. Use at boundaries (config, env, request body).
|
|
21
|
+
* Enforces the charset / length contract: 1-64 chars of `[a-z0-9-]`,
|
|
22
|
+
* starting with `[a-z0-9]` — catches programmatic misuse (config typos,
|
|
23
|
+
* hard-coded slugs that drift).
|
|
24
|
+
*/
|
|
25
|
+
declare function tenantId(s: string): TenantId;
|
|
26
|
+
/** Generate a fresh request ID. Prefers crypto.randomUUID where available. */
|
|
27
|
+
declare function newRequestId(): RequestId;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Local-First Sensitive Data Redaction
|
|
31
|
+
*
|
|
32
|
+
* Best-effort redaction of common secret and PII formats BEFORE prompts reach
|
|
33
|
+
* the policy gateway. Covers cryptographic key formats, custodial identifiers,
|
|
34
|
+
* high-entropy strings, and common PII (card numbers validated via Luhn, US
|
|
35
|
+
* SSNs, email addresses, and phone numbers).
|
|
36
|
+
*
|
|
37
|
+
* IMPORTANT — this is a defense-in-depth layer, NOT a guarantee of completeness.
|
|
38
|
+
* Pattern- and entropy-based redaction cannot catch every secret or PII shape
|
|
39
|
+
* (e.g. unstructured PII, names, novel token formats, or values below the
|
|
40
|
+
* entropy threshold). Do not rely on it as a sole control; keep downstream
|
|
41
|
+
* safeguards and human review in place.
|
|
42
|
+
*
|
|
43
|
+
* It helps *support* (does not by itself satisfy) controls such as GDPR Art. 32
|
|
44
|
+
* and PCI-DSS PAN-handling by reducing sensitive-data exposure to the gateway.
|
|
45
|
+
*/
|
|
46
|
+
interface RedactionResult {
|
|
47
|
+
/** The input with all sensitive tokens replaced by placeholder strings. */
|
|
48
|
+
redacted: string;
|
|
49
|
+
/** The original sensitive token strings that were replaced. */
|
|
50
|
+
tokensReplaced: string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Redact sensitive data from a prompt string before it reaches the gateway.
|
|
54
|
+
*
|
|
55
|
+
* Processing order (important — PEM first to avoid splitting on inner patterns):
|
|
56
|
+
* 1. PEM private/public key blocks
|
|
57
|
+
* 2. BIP-32 extended keys
|
|
58
|
+
* 3. WIF private keys
|
|
59
|
+
* 4. Raw hex 256-bit keys
|
|
60
|
+
* 5. Custodial IDs (all four variants)
|
|
61
|
+
* 6. PII — card numbers (Luhn-validated), SSNs, emails, phone numbers
|
|
62
|
+
* 7. High-entropy string catch-all
|
|
63
|
+
*/
|
|
64
|
+
declare function redactSensitiveData(input: string): RedactionResult;
|
|
20
65
|
|
|
21
66
|
/**
|
|
22
67
|
* G8R Agent Shield SDK
|
|
23
68
|
*
|
|
24
69
|
* Lightweight TypeScript client that wraps LLM calls with policy enforcement.
|
|
25
|
-
* Automatically intercepts prompts, applies local-first
|
|
70
|
+
* Automatically intercepts prompts, applies best-effort local-first redaction,
|
|
26
71
|
* checks them against the G8R policy engine, and logs all activity to the
|
|
27
72
|
* Agent Shield Console.
|
|
28
73
|
*
|
|
29
74
|
* Usage:
|
|
30
|
-
* import { AgentShield } from '@g8r-security/agent-shield-sdk';
|
|
75
|
+
* import { AgentShield, tenantId } from '@g8r-security/agent-shield-sdk';
|
|
31
76
|
*
|
|
32
77
|
* const shield = new AgentShield({
|
|
33
78
|
* consoleUrl: 'https://shield.yourcompany.com',
|
|
34
79
|
* apiKey: 'sk-shield-...',
|
|
80
|
+
* tenantId: tenantId('acme-corp'),
|
|
35
81
|
* department: 'Finance',
|
|
36
82
|
* userId: 'usr_FIN_042',
|
|
37
83
|
* aiModel: 'GPT-4o',
|
|
@@ -84,7 +130,7 @@ interface PolicyCheckResult {
|
|
|
84
130
|
/**
|
|
85
131
|
* Tokens that were redacted from the prompt before it reached the gateway.
|
|
86
132
|
* Undefined when no tokens were redacted (clean prompt).
|
|
87
|
-
* Populated by the
|
|
133
|
+
* Populated by the local-first redaction layer.
|
|
88
134
|
*/
|
|
89
135
|
redactedTokens?: string[];
|
|
90
136
|
}
|
|
@@ -99,9 +145,10 @@ declare class AgentShield {
|
|
|
99
145
|
/**
|
|
100
146
|
* Check a prompt against the policy engine before sending to the LLM.
|
|
101
147
|
*
|
|
102
|
-
* Applies local-first
|
|
103
|
-
*
|
|
104
|
-
* the
|
|
148
|
+
* Applies best-effort local-first redaction before sending to the gateway, so
|
|
149
|
+
* recognized signing keys, custodial IDs, common PII, and high-entropy secrets
|
|
150
|
+
* are stripped before the prompt leaves the process. Redaction is one layer of
|
|
151
|
+
* defense, not a guarantee that every secret is caught (see redaction.ts).
|
|
105
152
|
*
|
|
106
153
|
* Returns the policy decision without executing the LLM call.
|
|
107
154
|
*/
|
|
@@ -130,6 +177,9 @@ declare class AgentShield {
|
|
|
130
177
|
wrap<T>(llmCallFactory: () => Promise<T>, prompt: string): Promise<T>;
|
|
131
178
|
/**
|
|
132
179
|
* Log an interaction to the Agent Shield Console.
|
|
180
|
+
*
|
|
181
|
+
* Redacts the prompt before transmitting: the audit trail must not store or
|
|
182
|
+
* carry raw secrets/PII, and this is an egress point just like /check.
|
|
133
183
|
*/
|
|
134
184
|
private log;
|
|
135
185
|
}
|
|
@@ -143,4 +193,4 @@ declare class ShieldBlockedError extends Error {
|
|
|
143
193
|
constructor(reason: string, violatedRule: string | null, complianceMappings: PolicyCheckResult['complianceMappings'], sessionRevoked?: boolean);
|
|
144
194
|
}
|
|
145
195
|
|
|
146
|
-
export { AgentShield, type PolicyCheckResult, ShieldBlockedError, type ShieldConfig, type ShieldLogEntry };
|
|
196
|
+
export { AgentShield, type PolicyCheckResult, type RedactionResult, type RequestId, ShieldBlockedError, type ShieldConfig, type ShieldLogEntry, type TenantId, newRequestId, redactSensitiveData, tenantId };
|
package/dist/index.js
CHANGED
|
@@ -21,12 +21,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AgentShield: () => AgentShield,
|
|
24
|
-
ShieldBlockedError: () => ShieldBlockedError
|
|
24
|
+
ShieldBlockedError: () => ShieldBlockedError,
|
|
25
|
+
newRequestId: () => newRequestId,
|
|
26
|
+
redactSensitiveData: () => redactSensitiveData,
|
|
27
|
+
tenantId: () => tenantId
|
|
25
28
|
});
|
|
26
29
|
module.exports = __toCommonJS(index_exports);
|
|
27
30
|
var import_ts_pattern = require("ts-pattern");
|
|
28
31
|
|
|
29
32
|
// src/ids.ts
|
|
33
|
+
var TENANT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
34
|
+
function tenantId(s) {
|
|
35
|
+
if (!s) throw new Error("tenantId cannot be empty");
|
|
36
|
+
if (!TENANT_ID_PATTERN.test(s)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`tenantId must be 1-64 chars, [a-z0-9-], starting with [a-z0-9] (got ${JSON.stringify(s)})`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return s;
|
|
42
|
+
}
|
|
30
43
|
function newRequestId() {
|
|
31
44
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
32
45
|
return crypto.randomUUID();
|
|
@@ -88,6 +101,24 @@ var CUSTODIAL_ID_PATTERN = /\bcustodial-id:[A-Za-z0-9_-]+\b/g;
|
|
|
88
101
|
var CUST_PATTERN = /\bcust-\d+\b/gi;
|
|
89
102
|
var WALLET_ID_PATTERN = /\bwallet-id:[A-Za-z0-9_-]+\b/g;
|
|
90
103
|
var VAULT_ID_PATTERN = /\bvault-id:[A-Za-z0-9_-]+\b/g;
|
|
104
|
+
var EMAIL_PATTERN = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
|
|
105
|
+
var SSN_PATTERN = /\b\d{3}[ -]\d{2}[ -]\d{4}\b/g;
|
|
106
|
+
var PHONE_PATTERN = /\b(?:\+?\d{1,3}[ .-]?)?\(?\d{3}\)?[ .-]\d{3}[ .-]\d{4}\b/g;
|
|
107
|
+
var CARD_CANDIDATE_PATTERN = /\b\d(?:[ -]?\d){12,18}\b/g;
|
|
108
|
+
function luhnValid(digits) {
|
|
109
|
+
let sum = 0;
|
|
110
|
+
let double = false;
|
|
111
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
112
|
+
let d = digits.charCodeAt(i) - 48;
|
|
113
|
+
if (double) {
|
|
114
|
+
d *= 2;
|
|
115
|
+
if (d > 9) d -= 9;
|
|
116
|
+
}
|
|
117
|
+
sum += d;
|
|
118
|
+
double = !double;
|
|
119
|
+
}
|
|
120
|
+
return sum % 10 === 0;
|
|
121
|
+
}
|
|
91
122
|
var ENTROPY_THRESHOLD = 4.5;
|
|
92
123
|
var ENTROPY_MIN_LENGTH = 32;
|
|
93
124
|
function extractHighEntropyTokens(input) {
|
|
@@ -113,6 +144,17 @@ function redactSensitiveData(input) {
|
|
|
113
144
|
replaceAll(CUST_PATTERN, "CUST_ID");
|
|
114
145
|
replaceAll(WALLET_ID_PATTERN, "WALLET_ID");
|
|
115
146
|
replaceAll(VAULT_ID_PATTERN, "VAULT_ID");
|
|
147
|
+
redacted = redacted.replace(CARD_CANDIDATE_PATTERN, (match2) => {
|
|
148
|
+
const digits = match2.replace(/\D/g, "");
|
|
149
|
+
if (digits.length >= 13 && digits.length <= 19 && luhnValid(digits)) {
|
|
150
|
+
tokensReplaced.push(match2);
|
|
151
|
+
return "[REDACTED:CARD]";
|
|
152
|
+
}
|
|
153
|
+
return match2;
|
|
154
|
+
});
|
|
155
|
+
replaceAll(SSN_PATTERN, "SSN");
|
|
156
|
+
replaceAll(EMAIL_PATTERN, "EMAIL");
|
|
157
|
+
replaceAll(PHONE_PATTERN, "PHONE");
|
|
116
158
|
const highEntropyTokens = extractHighEntropyTokens(redacted);
|
|
117
159
|
for (const token of highEntropyTokens) {
|
|
118
160
|
if (!redacted.includes(token)) continue;
|
|
@@ -130,9 +172,10 @@ var AgentShield = class {
|
|
|
130
172
|
/**
|
|
131
173
|
* Check a prompt against the policy engine before sending to the LLM.
|
|
132
174
|
*
|
|
133
|
-
* Applies local-first
|
|
134
|
-
*
|
|
135
|
-
* the
|
|
175
|
+
* Applies best-effort local-first redaction before sending to the gateway, so
|
|
176
|
+
* recognized signing keys, custodial IDs, common PII, and high-entropy secrets
|
|
177
|
+
* are stripped before the prompt leaves the process. Redaction is one layer of
|
|
178
|
+
* defense, not a guarantee that every secret is caught (see redaction.ts).
|
|
136
179
|
*
|
|
137
180
|
* Returns the policy decision without executing the LLM call.
|
|
138
181
|
*/
|
|
@@ -211,12 +254,16 @@ var AgentShield = class {
|
|
|
211
254
|
}
|
|
212
255
|
/**
|
|
213
256
|
* Log an interaction to the Agent Shield Console.
|
|
257
|
+
*
|
|
258
|
+
* Redacts the prompt before transmitting: the audit trail must not store or
|
|
259
|
+
* carry raw secrets/PII, and this is an egress point just like /check.
|
|
214
260
|
*/
|
|
215
261
|
async log(input, result, requestId = newRequestId()) {
|
|
216
262
|
const scopedLog = log.child({
|
|
217
263
|
tenant_id: this.config.tenantId,
|
|
218
264
|
request_id: requestId
|
|
219
265
|
});
|
|
266
|
+
const { redacted } = redactSensitiveData(input);
|
|
220
267
|
const res = await fetch(`${this.config.consoleUrl}/api/sdk/v1/log`, {
|
|
221
268
|
method: "POST",
|
|
222
269
|
headers: {
|
|
@@ -224,7 +271,7 @@ var AgentShield = class {
|
|
|
224
271
|
Authorization: `Bearer ${this.config.apiKey}`
|
|
225
272
|
},
|
|
226
273
|
body: JSON.stringify({
|
|
227
|
-
input,
|
|
274
|
+
input: redacted,
|
|
228
275
|
tenantId: this.config.tenantId,
|
|
229
276
|
requestId,
|
|
230
277
|
userId: this.config.userId,
|
|
@@ -257,6 +304,9 @@ var ShieldBlockedError = class extends Error {
|
|
|
257
304
|
// Annotate the CommonJS export names for ESM import in node:
|
|
258
305
|
0 && (module.exports = {
|
|
259
306
|
AgentShield,
|
|
260
|
-
ShieldBlockedError
|
|
307
|
+
ShieldBlockedError,
|
|
308
|
+
newRequestId,
|
|
309
|
+
redactSensitiveData,
|
|
310
|
+
tenantId
|
|
261
311
|
});
|
|
262
312
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/ids.ts","../src/logger.ts","../src/redaction.ts"],"sourcesContent":["/**\n * G8R Agent Shield SDK\n *\n * Lightweight TypeScript client that wraps LLM calls with policy enforcement.\n * Automatically intercepts prompts, applies local-first VPC redaction (BitGo),\n * checks them against the G8R policy engine, and logs all activity to the\n * Agent Shield Console.\n *\n * Usage:\n * import { AgentShield } from '@g8r-security/agent-shield-sdk';\n *\n * const shield = new AgentShield({\n * consoleUrl: 'https://shield.yourcompany.com',\n * apiKey: 'sk-shield-...',\n * department: 'Finance',\n * userId: 'usr_FIN_042',\n * aiModel: 'GPT-4o',\n * });\n *\n * // Wrap any LLM call — the factory function is only invoked if the policy allows it\n * const result = await shield.wrap(\n * () => openai.chat.completions.create({\n * model: 'gpt-4o',\n * messages: [{ role: 'user', content: 'Summarize Q1 earnings' }],\n * }),\n * 'Summarize Q1 earnings'\n * );\n */\n\nimport { match } from 'ts-pattern';\nimport { newRequestId, type RequestId, type TenantId } from './ids';\nimport { log } from './logger';\nimport { redactSensitiveData } from './redaction';\n\nexport interface ShieldConfig {\n /** URL of the G8R Agent Shield Console */\n consoleUrl: string;\n /** API key for authentication */\n apiKey: string;\n /** Tenant this SDK instance operates on behalf of. Required. */\n tenantId: TenantId;\n /** Department of the calling user */\n department: string;\n /** User identifier */\n userId: string;\n /** AI model being called */\n aiModel: string;\n /** Optional: agent identifier (defaults to \"sdk-client\") */\n agentId?: string;\n /** Optional: employee display name (defaults to userId) */\n employeeName?: string;\n}\n\nexport interface PolicyCheckResult {\n decision: 'allowed' | 'blocked' | 'escalated';\n reason: string;\n violatedRule: string | null;\n requiresApproval: boolean;\n /**\n * Set to true when a kill-switch rule fires (e.g. unauthorized partner data\n * access). Consumers should tear down the agent session in response.\n */\n sessionRevoked?: boolean;\n complianceMappings: Array<{\n regulation: string;\n controlId: string;\n controlName: string;\n description: string;\n }>;\n /**\n * Tokens that were redacted from the prompt before it reached the gateway.\n * Undefined when no tokens were redacted (clean prompt).\n * Populated by the BitGo VPC local-first redaction layer.\n */\n redactedTokens?: string[];\n}\n\nexport interface ShieldLogEntry {\n id: string;\n decision: string;\n timestamp: string;\n}\n\nexport class AgentShield {\n private config: ShieldConfig;\n\n constructor(config: ShieldConfig) {\n this.config = config;\n }\n\n /**\n * Check a prompt against the policy engine before sending to the LLM.\n *\n * Applies local-first VPC redaction (BitGo) before sending to the gateway,\n * ensuring signing keys, custodial IDs, and high-entropy secrets never leave\n * the local process in plaintext.\n *\n * Returns the policy decision without executing the LLM call.\n */\n async check(\n prompt: string,\n requestId: RequestId = newRequestId()\n ): Promise<PolicyCheckResult> {\n // Step 1: Local-first redaction — strip signing keys and custodial IDs\n // before the prompt reaches the remote gateway (BitGo VPC masking).\n const { redacted, tokensReplaced } = redactSensitiveData(prompt);\n\n // `requestId` is generated per-call by default, but `wrap()` passes its\n // own value so /check and /log share a single correlation id end-to-end\n // (C2). Build a scoped logger bound to tenantId + requestId so any\n // failure path emits lines with that context automatically.\n const scopedLog = log.child({\n tenant_id: this.config.tenantId,\n request_id: requestId,\n });\n\n const res = await fetch(`${this.config.consoleUrl}/api/sdk/v1/check`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify({\n input: redacted, // send the redacted version — never the raw prompt\n tenantId: this.config.tenantId,\n requestId,\n userId: this.config.userId,\n department: this.config.department,\n aiModel: this.config.aiModel,\n agentId: this.config.agentId ?? 'sdk-client',\n }),\n });\n\n if (!res.ok) {\n scopedLog.error('Shield policy check failed', { status: res.status });\n throw new Error(`Shield policy check failed: ${res.status}`);\n }\n\n const result = await res.json();\n return {\n ...result,\n ...(tokensReplaced.length > 0 ? { redactedTokens: tokensReplaced } : {}),\n };\n }\n\n /**\n * Wrap an LLM call with policy enforcement.\n *\n * The `llmCallFactory` is a function that creates the LLM promise.\n * It is only invoked if the policy engine allows the action.\n * This prevents the LLM call from executing before the policy check completes.\n *\n * @param llmCallFactory - A function that returns the LLM call promise\n * @param prompt - The prompt text to evaluate against the policy engine\n * @returns The result of the LLM call if allowed\n * @throws ShieldBlockedError if the policy engine blocks the action\n *\n * @example\n * const result = await shield.wrap(\n * () => openai.chat.completions.create({\n * model: 'gpt-4o',\n * messages: [{ role: 'user', content: prompt }],\n * }),\n * prompt\n * );\n */\n async wrap<T>(llmCallFactory: () => Promise<T>, prompt: string): Promise<T> {\n // Generate a single requestId for this wrap() invocation and thread it\n // through both /check and /log so the two server-side log lines can be\n // joined end-to-end (C2). Without this, each call would mint its own id\n // and the audit trail would lose the policy→action linkage.\n const requestId = newRequestId();\n\n // Step 1: Check the prompt against the policy engine (includes redaction)\n const policyResult = await this.check(prompt, requestId);\n\n // Step 2: Log the attempt regardless of decision\n await this.log(prompt, policyResult, requestId);\n\n // Step 3: Enforce the decision. Exhaustive match — adding a fourth\n // decision value will fail TypeScript compilation here until handled.\n match(policyResult.decision)\n .with('blocked', () => {\n throw new ShieldBlockedError(\n policyResult.reason,\n policyResult.violatedRule,\n policyResult.complianceMappings,\n policyResult.sessionRevoked ?? false\n );\n })\n .with('escalated', () => {\n // In production, this would await human approval via webhook\n log.warn('Action escalated for human review', {\n reason: policyResult.reason,\n });\n })\n .with('allowed', () => {\n /* fall through to invoke the LLM */\n })\n .exhaustive();\n\n // Step 4: Only now invoke the LLM call — after policy check passed\n return llmCallFactory();\n }\n\n /**\n * Log an interaction to the Agent Shield Console.\n */\n private async log(\n input: string,\n result: PolicyCheckResult,\n requestId: RequestId = newRequestId()\n ): Promise<ShieldLogEntry> {\n const scopedLog = log.child({\n tenant_id: this.config.tenantId,\n request_id: requestId,\n });\n\n const res = await fetch(`${this.config.consoleUrl}/api/sdk/v1/log`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify({\n input,\n tenantId: this.config.tenantId,\n requestId,\n userId: this.config.userId,\n department: this.config.department,\n aiModel: this.config.aiModel,\n agentId: this.config.agentId ?? 'sdk-client',\n employeeName: this.config.employeeName ?? this.config.userId,\n decision: result.decision,\n reason: result.reason,\n violatedRule: result.violatedRule,\n requiresApproval: result.requiresApproval,\n complianceMappings: result.complianceMappings,\n }),\n });\n\n if (!res.ok) {\n scopedLog.error('Failed to log interaction', { status: res.status });\n }\n\n return res.json();\n }\n}\n\n/**\n * Error thrown when the policy engine blocks an LLM call.\n */\nexport class ShieldBlockedError extends Error {\n public readonly violatedRule: string | null;\n public readonly complianceMappings: PolicyCheckResult['complianceMappings'];\n public readonly sessionRevoked: boolean;\n\n constructor(\n reason: string,\n violatedRule: string | null,\n complianceMappings: PolicyCheckResult['complianceMappings'],\n sessionRevoked: boolean = false\n ) {\n super(`[G8R Shield BLOCKED] ${reason}`);\n this.name = 'ShieldBlockedError';\n this.violatedRule = violatedRule;\n this.complianceMappings = complianceMappings;\n this.sessionRevoked = sessionRevoked;\n }\n}\n","/**\n * Branded id types + constructors used by the SDK.\n *\n * Vendored from the engine's `types.ts` so the published\n * `@g8r-security/agent-shield-sdk` package carries no `@g8r-security/core` dependency — the\n * engine is private IP and must not be a public-package dependency. Only\n * the id types/helpers the SDK actually uses are copied here.\n */\n\n/**\n * Identifies a single tenant in the multi-tenant governance plane.\n * Branded so a raw string can't be passed where a TenantId is expected.\n */\nexport type TenantId = string & { readonly __brand: 'TenantId' };\n\n/** Per-request correlation ID. Generated client-side by the SDK. */\nexport type RequestId = string & { readonly __brand: 'RequestId' };\n\nconst TENANT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;\n\n/**\n * Cast a string to TenantId. Use at boundaries (config, env, request body).\n * Enforces the charset / length contract: 1-64 chars of `[a-z0-9-]`,\n * starting with `[a-z0-9]` — catches programmatic misuse (config typos,\n * hard-coded slugs that drift).\n */\nexport function tenantId(s: string): TenantId {\n if (!s) throw new Error('tenantId cannot be empty');\n if (!TENANT_ID_PATTERN.test(s)) {\n throw new Error(\n `tenantId must be 1-64 chars, [a-z0-9-], starting with [a-z0-9] (got ${JSON.stringify(s)})`\n );\n }\n return s as TenantId;\n}\n\n/** Generate a fresh request ID. Prefers crypto.randomUUID where available. */\nexport function newRequestId(): RequestId {\n // crypto.randomUUID is available in Node 19+ and all modern browsers.\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID() as RequestId;\n }\n // Fallback: timestamp + Math.random (best-effort, not crypto-strong).\n return `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` as RequestId;\n}\n","/**\n * G8R structured JSON logger.\n *\n * Tiny, dependency-free logger that emits one JSON object per line. Designed\n * for governance contexts where each log line must carry `tenant_id` and\n * `request_id` so downstream pipelines can route audit trails per-tenant.\n *\n * Vendored into the SDK (a verbatim copy of the engine's logger) so the\n * published `@g8r-security/agent-shield-sdk` package carries no `@g8r-security/core`\n * dependency — the engine is private IP and must not be a public-package\n * dependency.\n *\n * Use the default `log` singleton for app-level chatter; call `createLogger`\n * (or `.child()`) when you need per-request bindings injected.\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport interface LogContext {\n tenant_id?: string;\n request_id?: string;\n [key: string]: unknown;\n}\n\nexport interface Logger {\n debug(msg: string, ctx?: LogContext): void;\n info(msg: string, ctx?: LogContext): void;\n warn(msg: string, ctx?: LogContext): void;\n error(msg: string, ctx?: LogContext): void;\n child(ctx: LogContext): Logger;\n}\n\nexport interface LoggerOptions {\n /** Defaults to 'info'. Anything below is dropped. */\n level?: LogLevel;\n /** Defaults to console.log. Override for testing or transport injection. */\n sink?: (line: string) => void;\n /** Context merged into every log line. */\n bindings?: LogContext;\n}\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n};\n\nexport function createLogger(opts: LoggerOptions = {}): Logger {\n const minLevel = LEVEL_ORDER[opts.level ?? 'info'];\n const sink = opts.sink ?? ((line: string) => console.log(line));\n const bindings = opts.bindings ?? {};\n\n function emit(level: LogLevel, msg: string, ctx?: LogContext): void {\n if (LEVEL_ORDER[level] < minLevel) return;\n const payload = {\n level,\n msg,\n ts: new Date().toISOString(),\n ...bindings,\n ...ctx,\n };\n sink(JSON.stringify(payload));\n }\n\n return {\n debug: (msg, ctx) => emit('debug', msg, ctx),\n info: (msg, ctx) => emit('info', msg, ctx),\n warn: (msg, ctx) => emit('warn', msg, ctx),\n error: (msg, ctx) => emit('error', msg, ctx),\n child: (extra) => createLogger({ ...opts, bindings: { ...bindings, ...extra } }),\n };\n}\n\n/** Default singleton — convenient for apps that don't need DI. */\nexport const log = createLogger();\n","/**\n * BitGo VPC Sensitive Data Redaction\n *\n * Redacts cryptographic keys, custodial identifiers, and high-entropy strings\n * BEFORE sending prompts to the G8R policy gateway (local-first redaction layer).\n *\n * Compliance:\n * - GDPR Art. 32: Security of Processing — appropriate technical measures\n * - PCI-DSS 3.4: Render PAN/sensitive data unreadable wherever stored or transmitted\n */\n\nexport interface RedactionResult {\n /** The input with all sensitive tokens replaced by placeholder strings. */\n redacted: string;\n /** The original sensitive token strings that were replaced. */\n tokensReplaced: string[];\n}\n\n/**\n * Shannon entropy: calculates bits per character.\n * High entropy (> 4.5) indicates likely cryptographic material.\n */\nfunction shannonEntropy(str: string): number {\n const freq = new Map<string, number>();\n for (const ch of str) {\n freq.set(ch, (freq.get(ch) ?? 0) + 1);\n }\n let entropy = 0;\n const len = str.length;\n for (const count of freq.values()) {\n const p = count / len;\n entropy -= p * Math.log2(p);\n }\n return entropy;\n}\n\n// ── Signing Key Patterns ─────────────────────────────────────────────────────\n\n/** BIP-32 extended public/private keys: xpub, xprv, ypub, zpub, zprv, etc. */\nconst BIP32_PATTERN = /\\b[xyz](?:pub|prv)[a-zA-Z0-9]{99,111}\\b/g;\n\n/**\n * WIF (Wallet Import Format) private keys.\n * Base58Check encoded, starts with 5 (uncompressed) or K/L (compressed), 51–52 chars.\n */\nconst WIF_PATTERN = /\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b/g;\n\n/**\n * Raw hex 256-bit keys — exactly 64 hex characters, optionally 0x-prefixed.\n * Matches Ethereum private keys, secp256k1 scalars, etc.\n */\nconst HEX_KEY_PATTERN = /\\b(?:0x)?[0-9a-fA-F]{64}\\b/g;\n\n/** PEM-encoded private or public key blocks (multi-line). */\nconst PEM_PATTERN =\n /-----BEGIN (?:RSA |EC |OPENSSH )?(?:PRIVATE|PUBLIC) KEY-----[\\s\\S]*?-----END (?:RSA |EC |OPENSSH )?(?:PRIVATE|PUBLIC) KEY-----/g;\n\n// ── Custodial ID Patterns ─────────────────────────────────────────────────────\n\n/** BitGo custodial-id format: `custodial-id:abc123xyz` */\nconst CUSTODIAL_ID_PATTERN = /\\bcustodial-id:[A-Za-z0-9_-]+\\b/g;\n\n/** Short custodial references: `cust-98765` */\nconst CUST_PATTERN = /\\bcust-\\d+\\b/gi;\n\n/** Wallet identifiers: `wallet-id:wlt-abc999` */\nconst WALLET_ID_PATTERN = /\\bwallet-id:[A-Za-z0-9_-]+\\b/g;\n\n/** Vault identifiers: `vault-id:v-secure-001` */\nconst VAULT_ID_PATTERN = /\\bvault-id:[A-Za-z0-9_-]+\\b/g;\n\n// ── Entropy Detection Constants ───────────────────────────────────────────────\n\n/** Minimum Shannon entropy (bits/char) to flag a token as likely cryptographic material. */\nconst ENTROPY_THRESHOLD = 4.5;\n\n/** Minimum token length to apply entropy analysis (shorter strings are too ambiguous). */\nconst ENTROPY_MIN_LENGTH = 32;\n\n/**\n * Find tokens in the string that exceed the entropy threshold.\n * Splits on whitespace and common delimiters to isolate candidates.\n */\nfunction extractHighEntropyTokens(input: string): string[] {\n const candidates = input.split(/[\\s,;:\"'`(){}[\\]<>]+/).filter(Boolean);\n return candidates.filter(\n (token) => token.length >= ENTROPY_MIN_LENGTH && shannonEntropy(token) >= ENTROPY_THRESHOLD\n );\n}\n\n/**\n * Redact sensitive data from a prompt string before it reaches the gateway.\n *\n * Processing order (important — PEM first to avoid splitting on inner patterns):\n * 1. PEM private/public key blocks\n * 2. BIP-32 extended keys\n * 3. WIF private keys\n * 4. Raw hex 256-bit keys\n * 5. Custodial IDs (all four variants)\n * 6. High-entropy string catch-all\n */\nexport function redactSensitiveData(input: string): RedactionResult {\n const tokensReplaced: string[] = [];\n let redacted = input;\n\n function replaceAll(pattern: RegExp, label: string): void {\n redacted = redacted.replace(pattern, (match) => {\n tokensReplaced.push(match);\n return `[REDACTED:${label}]`;\n });\n }\n\n replaceAll(PEM_PATTERN, 'PEM_KEY');\n replaceAll(BIP32_PATTERN, 'BIP32_KEY');\n replaceAll(WIF_PATTERN, 'WIF_KEY');\n replaceAll(HEX_KEY_PATTERN, 'HEX_KEY');\n replaceAll(CUSTODIAL_ID_PATTERN, 'CUSTODIAL_ID');\n replaceAll(CUST_PATTERN, 'CUST_ID');\n replaceAll(WALLET_ID_PATTERN, 'WALLET_ID');\n replaceAll(VAULT_ID_PATTERN, 'VAULT_ID');\n\n // High-entropy catch-all — runs on the already-redacted string to avoid double-replacing.\n const highEntropyTokens = extractHighEntropyTokens(redacted);\n for (const token of highEntropyTokens) {\n if (!redacted.includes(token)) continue;\n tokensReplaced.push(token);\n redacted = redacted.split(token).join('[REDACTED:HIGH_ENTROPY]');\n }\n\n return { redacted, tokensReplaced };\n}\n"],"mappings":"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","names":["match"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/ids.ts","../src/logger.ts","../src/redaction.ts"],"sourcesContent":["/**\n * G8R Agent Shield SDK\n *\n * Lightweight TypeScript client that wraps LLM calls with policy enforcement.\n * Automatically intercepts prompts, applies best-effort local-first redaction,\n * checks them against the G8R policy engine, and logs all activity to the\n * Agent Shield Console.\n *\n * Usage:\n * import { AgentShield, tenantId } from '@g8r-security/agent-shield-sdk';\n *\n * const shield = new AgentShield({\n * consoleUrl: 'https://shield.yourcompany.com',\n * apiKey: 'sk-shield-...',\n * tenantId: tenantId('acme-corp'),\n * department: 'Finance',\n * userId: 'usr_FIN_042',\n * aiModel: 'GPT-4o',\n * });\n *\n * // Wrap any LLM call — the factory function is only invoked if the policy allows it\n * const result = await shield.wrap(\n * () => openai.chat.completions.create({\n * model: 'gpt-4o',\n * messages: [{ role: 'user', content: 'Summarize Q1 earnings' }],\n * }),\n * 'Summarize Q1 earnings'\n * );\n */\n\nimport { match } from 'ts-pattern';\nimport { newRequestId, type RequestId, type TenantId } from './ids';\nimport { log } from './logger';\nimport { redactSensitiveData } from './redaction';\n\n// ── Public API re-exports ────────────────────────────────────────────────────\n// Consumers need `tenantId()` to construct the branded TenantId required by\n// ShieldConfig, and `redactSensitiveData` is documented as a public helper.\nexport { tenantId, newRequestId } from './ids';\nexport type { TenantId, RequestId } from './ids';\nexport { redactSensitiveData } from './redaction';\nexport type { RedactionResult } from './redaction';\n\nexport interface ShieldConfig {\n /** URL of the G8R Agent Shield Console */\n consoleUrl: string;\n /** API key for authentication */\n apiKey: string;\n /** Tenant this SDK instance operates on behalf of. Required. */\n tenantId: TenantId;\n /** Department of the calling user */\n department: string;\n /** User identifier */\n userId: string;\n /** AI model being called */\n aiModel: string;\n /** Optional: agent identifier (defaults to \"sdk-client\") */\n agentId?: string;\n /** Optional: employee display name (defaults to userId) */\n employeeName?: string;\n}\n\nexport interface PolicyCheckResult {\n decision: 'allowed' | 'blocked' | 'escalated';\n reason: string;\n violatedRule: string | null;\n requiresApproval: boolean;\n /**\n * Set to true when a kill-switch rule fires (e.g. unauthorized partner data\n * access). Consumers should tear down the agent session in response.\n */\n sessionRevoked?: boolean;\n complianceMappings: Array<{\n regulation: string;\n controlId: string;\n controlName: string;\n description: string;\n }>;\n /**\n * Tokens that were redacted from the prompt before it reached the gateway.\n * Undefined when no tokens were redacted (clean prompt).\n * Populated by the local-first redaction layer.\n */\n redactedTokens?: string[];\n}\n\nexport interface ShieldLogEntry {\n id: string;\n decision: string;\n timestamp: string;\n}\n\nexport class AgentShield {\n private config: ShieldConfig;\n\n constructor(config: ShieldConfig) {\n this.config = config;\n }\n\n /**\n * Check a prompt against the policy engine before sending to the LLM.\n *\n * Applies best-effort local-first redaction before sending to the gateway, so\n * recognized signing keys, custodial IDs, common PII, and high-entropy secrets\n * are stripped before the prompt leaves the process. Redaction is one layer of\n * defense, not a guarantee that every secret is caught (see redaction.ts).\n *\n * Returns the policy decision without executing the LLM call.\n */\n async check(\n prompt: string,\n requestId: RequestId = newRequestId()\n ): Promise<PolicyCheckResult> {\n // Step 1: Local-first redaction — strip recognized secrets and PII\n // before the prompt reaches the remote gateway.\n const { redacted, tokensReplaced } = redactSensitiveData(prompt);\n\n // `requestId` is generated per-call by default, but `wrap()` passes its\n // own value so /check and /log share a single correlation id end-to-end\n // (C2). Build a scoped logger bound to tenantId + requestId so any\n // failure path emits lines with that context automatically.\n const scopedLog = log.child({\n tenant_id: this.config.tenantId,\n request_id: requestId,\n });\n\n const res = await fetch(`${this.config.consoleUrl}/api/sdk/v1/check`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify({\n input: redacted, // send the redacted version — never the raw prompt\n tenantId: this.config.tenantId,\n requestId,\n userId: this.config.userId,\n department: this.config.department,\n aiModel: this.config.aiModel,\n agentId: this.config.agentId ?? 'sdk-client',\n }),\n });\n\n if (!res.ok) {\n scopedLog.error('Shield policy check failed', { status: res.status });\n throw new Error(`Shield policy check failed: ${res.status}`);\n }\n\n const result = await res.json();\n return {\n ...result,\n ...(tokensReplaced.length > 0 ? { redactedTokens: tokensReplaced } : {}),\n };\n }\n\n /**\n * Wrap an LLM call with policy enforcement.\n *\n * The `llmCallFactory` is a function that creates the LLM promise.\n * It is only invoked if the policy engine allows the action.\n * This prevents the LLM call from executing before the policy check completes.\n *\n * @param llmCallFactory - A function that returns the LLM call promise\n * @param prompt - The prompt text to evaluate against the policy engine\n * @returns The result of the LLM call if allowed\n * @throws ShieldBlockedError if the policy engine blocks the action\n *\n * @example\n * const result = await shield.wrap(\n * () => openai.chat.completions.create({\n * model: 'gpt-4o',\n * messages: [{ role: 'user', content: prompt }],\n * }),\n * prompt\n * );\n */\n async wrap<T>(llmCallFactory: () => Promise<T>, prompt: string): Promise<T> {\n // Generate a single requestId for this wrap() invocation and thread it\n // through both /check and /log so the two server-side log lines can be\n // joined end-to-end (C2). Without this, each call would mint its own id\n // and the audit trail would lose the policy→action linkage.\n const requestId = newRequestId();\n\n // Step 1: Check the prompt against the policy engine (includes redaction)\n const policyResult = await this.check(prompt, requestId);\n\n // Step 2: Log the attempt regardless of decision. log() redacts before\n // transmitting, so the audit-log path never leaks raw secrets either.\n await this.log(prompt, policyResult, requestId);\n\n // Step 3: Enforce the decision. Exhaustive match — adding a fourth\n // decision value will fail TypeScript compilation here until handled.\n match(policyResult.decision)\n .with('blocked', () => {\n throw new ShieldBlockedError(\n policyResult.reason,\n policyResult.violatedRule,\n policyResult.complianceMappings,\n policyResult.sessionRevoked ?? false\n );\n })\n .with('escalated', () => {\n // In production, this would await human approval via webhook\n log.warn('Action escalated for human review', {\n reason: policyResult.reason,\n });\n })\n .with('allowed', () => {\n /* fall through to invoke the LLM */\n })\n .exhaustive();\n\n // Step 4: Only now invoke the LLM call — after policy check passed\n return llmCallFactory();\n }\n\n /**\n * Log an interaction to the Agent Shield Console.\n *\n * Redacts the prompt before transmitting: the audit trail must not store or\n * carry raw secrets/PII, and this is an egress point just like /check.\n */\n private async log(\n input: string,\n result: PolicyCheckResult,\n requestId: RequestId = newRequestId()\n ): Promise<ShieldLogEntry> {\n const scopedLog = log.child({\n tenant_id: this.config.tenantId,\n request_id: requestId,\n });\n\n // Redact at the egress boundary — never send the raw prompt to /log.\n const { redacted } = redactSensitiveData(input);\n\n const res = await fetch(`${this.config.consoleUrl}/api/sdk/v1/log`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify({\n input: redacted,\n tenantId: this.config.tenantId,\n requestId,\n userId: this.config.userId,\n department: this.config.department,\n aiModel: this.config.aiModel,\n agentId: this.config.agentId ?? 'sdk-client',\n employeeName: this.config.employeeName ?? this.config.userId,\n decision: result.decision,\n reason: result.reason,\n violatedRule: result.violatedRule,\n requiresApproval: result.requiresApproval,\n complianceMappings: result.complianceMappings,\n }),\n });\n\n if (!res.ok) {\n scopedLog.error('Failed to log interaction', { status: res.status });\n }\n\n return res.json();\n }\n}\n\n/**\n * Error thrown when the policy engine blocks an LLM call.\n */\nexport class ShieldBlockedError extends Error {\n public readonly violatedRule: string | null;\n public readonly complianceMappings: PolicyCheckResult['complianceMappings'];\n public readonly sessionRevoked: boolean;\n\n constructor(\n reason: string,\n violatedRule: string | null,\n complianceMappings: PolicyCheckResult['complianceMappings'],\n sessionRevoked: boolean = false\n ) {\n super(`[G8R Shield BLOCKED] ${reason}`);\n this.name = 'ShieldBlockedError';\n this.violatedRule = violatedRule;\n this.complianceMappings = complianceMappings;\n this.sessionRevoked = sessionRevoked;\n }\n}\n","/**\n * Branded id types + constructors used by the SDK.\n *\n * Self-contained in the SDK so the published `@g8r-security/agent-shield-sdk`\n * package has no dependency on internal packages. Only the id types/helpers the\n * SDK actually uses are included here.\n */\n\n/**\n * Identifies a single tenant in the multi-tenant governance plane.\n * Branded so a raw string can't be passed where a TenantId is expected.\n */\nexport type TenantId = string & { readonly __brand: 'TenantId' };\n\n/** Per-request correlation ID. Generated client-side by the SDK. */\nexport type RequestId = string & { readonly __brand: 'RequestId' };\n\nconst TENANT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;\n\n/**\n * Cast a string to TenantId. Use at boundaries (config, env, request body).\n * Enforces the charset / length contract: 1-64 chars of `[a-z0-9-]`,\n * starting with `[a-z0-9]` — catches programmatic misuse (config typos,\n * hard-coded slugs that drift).\n */\nexport function tenantId(s: string): TenantId {\n if (!s) throw new Error('tenantId cannot be empty');\n if (!TENANT_ID_PATTERN.test(s)) {\n throw new Error(\n `tenantId must be 1-64 chars, [a-z0-9-], starting with [a-z0-9] (got ${JSON.stringify(s)})`\n );\n }\n return s as TenantId;\n}\n\n/** Generate a fresh request ID. Prefers crypto.randomUUID where available. */\nexport function newRequestId(): RequestId {\n // crypto.randomUUID is available in Node 19+ and all modern browsers.\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID() as RequestId;\n }\n // Fallback: timestamp + Math.random (best-effort, not crypto-strong).\n return `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` as RequestId;\n}\n","/**\n * G8R structured JSON logger.\n *\n * Tiny, dependency-free logger that emits one JSON object per line. Designed\n * for governance contexts where each log line must carry `tenant_id` and\n * `request_id` so downstream pipelines can route audit trails per-tenant.\n *\n * Self-contained in the SDK so the published `@g8r-security/agent-shield-sdk`\n * package has no dependency on internal packages.\n *\n * Use the default `log` singleton for app-level chatter; call `createLogger`\n * (or `.child()`) when you need per-request bindings injected.\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport interface LogContext {\n tenant_id?: string;\n request_id?: string;\n [key: string]: unknown;\n}\n\nexport interface Logger {\n debug(msg: string, ctx?: LogContext): void;\n info(msg: string, ctx?: LogContext): void;\n warn(msg: string, ctx?: LogContext): void;\n error(msg: string, ctx?: LogContext): void;\n child(ctx: LogContext): Logger;\n}\n\nexport interface LoggerOptions {\n /** Defaults to 'info'. Anything below is dropped. */\n level?: LogLevel;\n /** Defaults to console.log. Override for testing or transport injection. */\n sink?: (line: string) => void;\n /** Context merged into every log line. */\n bindings?: LogContext;\n}\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n};\n\nexport function createLogger(opts: LoggerOptions = {}): Logger {\n const minLevel = LEVEL_ORDER[opts.level ?? 'info'];\n const sink = opts.sink ?? ((line: string) => console.log(line));\n const bindings = opts.bindings ?? {};\n\n function emit(level: LogLevel, msg: string, ctx?: LogContext): void {\n if (LEVEL_ORDER[level] < minLevel) return;\n const payload = {\n level,\n msg,\n ts: new Date().toISOString(),\n ...bindings,\n ...ctx,\n };\n sink(JSON.stringify(payload));\n }\n\n return {\n debug: (msg, ctx) => emit('debug', msg, ctx),\n info: (msg, ctx) => emit('info', msg, ctx),\n warn: (msg, ctx) => emit('warn', msg, ctx),\n error: (msg, ctx) => emit('error', msg, ctx),\n child: (extra) => createLogger({ ...opts, bindings: { ...bindings, ...extra } }),\n };\n}\n\n/** Default singleton — convenient for apps that don't need DI. */\nexport const log = createLogger();\n","/**\n * Local-First Sensitive Data Redaction\n *\n * Best-effort redaction of common secret and PII formats BEFORE prompts reach\n * the policy gateway. Covers cryptographic key formats, custodial identifiers,\n * high-entropy strings, and common PII (card numbers validated via Luhn, US\n * SSNs, email addresses, and phone numbers).\n *\n * IMPORTANT — this is a defense-in-depth layer, NOT a guarantee of completeness.\n * Pattern- and entropy-based redaction cannot catch every secret or PII shape\n * (e.g. unstructured PII, names, novel token formats, or values below the\n * entropy threshold). Do not rely on it as a sole control; keep downstream\n * safeguards and human review in place.\n *\n * It helps *support* (does not by itself satisfy) controls such as GDPR Art. 32\n * and PCI-DSS PAN-handling by reducing sensitive-data exposure to the gateway.\n */\n\nexport interface RedactionResult {\n /** The input with all sensitive tokens replaced by placeholder strings. */\n redacted: string;\n /** The original sensitive token strings that were replaced. */\n tokensReplaced: string[];\n}\n\n/**\n * Shannon entropy: calculates bits per character.\n * High entropy (> 4.5) indicates likely cryptographic material.\n */\nfunction shannonEntropy(str: string): number {\n const freq = new Map<string, number>();\n for (const ch of str) {\n freq.set(ch, (freq.get(ch) ?? 0) + 1);\n }\n let entropy = 0;\n const len = str.length;\n for (const count of freq.values()) {\n const p = count / len;\n entropy -= p * Math.log2(p);\n }\n return entropy;\n}\n\n// ── Signing Key Patterns ─────────────────────────────────────────────────────\n\n/** BIP-32 extended public/private keys: xpub, xprv, ypub, zpub, zprv, etc. */\nconst BIP32_PATTERN = /\\b[xyz](?:pub|prv)[a-zA-Z0-9]{99,111}\\b/g;\n\n/**\n * WIF (Wallet Import Format) private keys.\n * Base58Check encoded, starts with 5 (uncompressed) or K/L (compressed), 51–52 chars.\n */\nconst WIF_PATTERN = /\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b/g;\n\n/**\n * Raw hex 256-bit keys — exactly 64 hex characters, optionally 0x-prefixed.\n * Matches Ethereum private keys, secp256k1 scalars, etc.\n */\nconst HEX_KEY_PATTERN = /\\b(?:0x)?[0-9a-fA-F]{64}\\b/g;\n\n/** PEM-encoded private or public key blocks (multi-line). */\nconst PEM_PATTERN =\n /-----BEGIN (?:RSA |EC |OPENSSH )?(?:PRIVATE|PUBLIC) KEY-----[\\s\\S]*?-----END (?:RSA |EC |OPENSSH )?(?:PRIVATE|PUBLIC) KEY-----/g;\n\n// ── Custodial ID Patterns ─────────────────────────────────────────────────────\n\n/** Custodial-id format: `custodial-id:abc123xyz` */\nconst CUSTODIAL_ID_PATTERN = /\\bcustodial-id:[A-Za-z0-9_-]+\\b/g;\n\n/** Short custodial references: `cust-98765` */\nconst CUST_PATTERN = /\\bcust-\\d+\\b/gi;\n\n/** Wallet identifiers: `wallet-id:wlt-abc999` */\nconst WALLET_ID_PATTERN = /\\bwallet-id:[A-Za-z0-9_-]+\\b/g;\n\n/** Vault identifiers: `vault-id:v-secure-001` */\nconst VAULT_ID_PATTERN = /\\bvault-id:[A-Za-z0-9_-]+\\b/g;\n\n// ── PII Patterns (best-effort) ────────────────────────────────────────────────\n\n/** Email addresses. */\nconst EMAIL_PATTERN = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g;\n\n/** US Social Security Numbers in dashed or spaced form, e.g. `123-45-6789`. */\nconst SSN_PATTERN = /\\b\\d{3}[ -]\\d{2}[ -]\\d{4}\\b/g;\n\n/**\n * Phone numbers with explicit separators (avoids matching arbitrary digit runs).\n * Optional country code, then 3-3-4 with space/dot/hyphen between groups.\n */\nconst PHONE_PATTERN = /\\b(?:\\+?\\d{1,3}[ .-]?)?\\(?\\d{3}\\)?[ .-]\\d{3}[ .-]\\d{4}\\b/g;\n\n/**\n * Candidate card-number runs: 13–19 digits with optional single space/hyphen\n * separators. Validated with Luhn before redacting to avoid false positives on\n * arbitrary long numbers (order/invoice IDs, etc.).\n */\nconst CARD_CANDIDATE_PATTERN = /\\b\\d(?:[ -]?\\d){12,18}\\b/g;\n\n/**\n * Luhn checksum — used to gate card-number redaction so we only mask digit runs\n * that actually pass the check digit, not every long number.\n */\nfunction luhnValid(digits: string): boolean {\n let sum = 0;\n let double = false;\n for (let i = digits.length - 1; i >= 0; i--) {\n let d = digits.charCodeAt(i) - 48;\n if (double) {\n d *= 2;\n if (d > 9) d -= 9;\n }\n sum += d;\n double = !double;\n }\n return sum % 10 === 0;\n}\n\n// ── Entropy Detection Constants ───────────────────────────────────────────────\n\n/** Minimum Shannon entropy (bits/char) to flag a token as likely cryptographic material. */\nconst ENTROPY_THRESHOLD = 4.5;\n\n/** Minimum token length to apply entropy analysis (shorter strings are too ambiguous). */\nconst ENTROPY_MIN_LENGTH = 32;\n\n/**\n * Find tokens in the string that exceed the entropy threshold.\n * Splits on whitespace and common delimiters to isolate candidates.\n */\nfunction extractHighEntropyTokens(input: string): string[] {\n const candidates = input.split(/[\\s,;:\"'`(){}[\\]<>]+/).filter(Boolean);\n return candidates.filter(\n (token) => token.length >= ENTROPY_MIN_LENGTH && shannonEntropy(token) >= ENTROPY_THRESHOLD\n );\n}\n\n/**\n * Redact sensitive data from a prompt string before it reaches the gateway.\n *\n * Processing order (important — PEM first to avoid splitting on inner patterns):\n * 1. PEM private/public key blocks\n * 2. BIP-32 extended keys\n * 3. WIF private keys\n * 4. Raw hex 256-bit keys\n * 5. Custodial IDs (all four variants)\n * 6. PII — card numbers (Luhn-validated), SSNs, emails, phone numbers\n * 7. High-entropy string catch-all\n */\nexport function redactSensitiveData(input: string): RedactionResult {\n const tokensReplaced: string[] = [];\n let redacted = input;\n\n function replaceAll(pattern: RegExp, label: string): void {\n redacted = redacted.replace(pattern, (match) => {\n tokensReplaced.push(match);\n return `[REDACTED:${label}]`;\n });\n }\n\n replaceAll(PEM_PATTERN, 'PEM_KEY');\n replaceAll(BIP32_PATTERN, 'BIP32_KEY');\n replaceAll(WIF_PATTERN, 'WIF_KEY');\n replaceAll(HEX_KEY_PATTERN, 'HEX_KEY');\n replaceAll(CUSTODIAL_ID_PATTERN, 'CUSTODIAL_ID');\n replaceAll(CUST_PATTERN, 'CUST_ID');\n replaceAll(WALLET_ID_PATTERN, 'WALLET_ID');\n replaceAll(VAULT_ID_PATTERN, 'VAULT_ID');\n\n // PII (best-effort). Card numbers first, gated by Luhn so we don't mask every\n // long digit run; then structured SSN / email / phone formats.\n redacted = redacted.replace(CARD_CANDIDATE_PATTERN, (match) => {\n const digits = match.replace(/\\D/g, '');\n if (digits.length >= 13 && digits.length <= 19 && luhnValid(digits)) {\n tokensReplaced.push(match);\n return '[REDACTED:CARD]';\n }\n return match;\n });\n replaceAll(SSN_PATTERN, 'SSN');\n replaceAll(EMAIL_PATTERN, 'EMAIL');\n replaceAll(PHONE_PATTERN, 'PHONE');\n\n // High-entropy catch-all — runs on the already-redacted string to avoid double-replacing.\n const highEntropyTokens = extractHighEntropyTokens(redacted);\n for (const token of highEntropyTokens) {\n if (!redacted.includes(token)) continue;\n tokensReplaced.push(token);\n redacted = redacted.split(token).join('[REDACTED:HIGH_ENTROPY]');\n }\n\n return { redacted, tokensReplaced };\n}\n"],"mappings":"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","names":["match"]}
|
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
import { match } from "ts-pattern";
|
|
3
3
|
|
|
4
4
|
// src/ids.ts
|
|
5
|
+
var TENANT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
6
|
+
function tenantId(s) {
|
|
7
|
+
if (!s) throw new Error("tenantId cannot be empty");
|
|
8
|
+
if (!TENANT_ID_PATTERN.test(s)) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`tenantId must be 1-64 chars, [a-z0-9-], starting with [a-z0-9] (got ${JSON.stringify(s)})`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
return s;
|
|
14
|
+
}
|
|
5
15
|
function newRequestId() {
|
|
6
16
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
7
17
|
return crypto.randomUUID();
|
|
@@ -63,6 +73,24 @@ var CUSTODIAL_ID_PATTERN = /\bcustodial-id:[A-Za-z0-9_-]+\b/g;
|
|
|
63
73
|
var CUST_PATTERN = /\bcust-\d+\b/gi;
|
|
64
74
|
var WALLET_ID_PATTERN = /\bwallet-id:[A-Za-z0-9_-]+\b/g;
|
|
65
75
|
var VAULT_ID_PATTERN = /\bvault-id:[A-Za-z0-9_-]+\b/g;
|
|
76
|
+
var EMAIL_PATTERN = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
|
|
77
|
+
var SSN_PATTERN = /\b\d{3}[ -]\d{2}[ -]\d{4}\b/g;
|
|
78
|
+
var PHONE_PATTERN = /\b(?:\+?\d{1,3}[ .-]?)?\(?\d{3}\)?[ .-]\d{3}[ .-]\d{4}\b/g;
|
|
79
|
+
var CARD_CANDIDATE_PATTERN = /\b\d(?:[ -]?\d){12,18}\b/g;
|
|
80
|
+
function luhnValid(digits) {
|
|
81
|
+
let sum = 0;
|
|
82
|
+
let double = false;
|
|
83
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
84
|
+
let d = digits.charCodeAt(i) - 48;
|
|
85
|
+
if (double) {
|
|
86
|
+
d *= 2;
|
|
87
|
+
if (d > 9) d -= 9;
|
|
88
|
+
}
|
|
89
|
+
sum += d;
|
|
90
|
+
double = !double;
|
|
91
|
+
}
|
|
92
|
+
return sum % 10 === 0;
|
|
93
|
+
}
|
|
66
94
|
var ENTROPY_THRESHOLD = 4.5;
|
|
67
95
|
var ENTROPY_MIN_LENGTH = 32;
|
|
68
96
|
function extractHighEntropyTokens(input) {
|
|
@@ -88,6 +116,17 @@ function redactSensitiveData(input) {
|
|
|
88
116
|
replaceAll(CUST_PATTERN, "CUST_ID");
|
|
89
117
|
replaceAll(WALLET_ID_PATTERN, "WALLET_ID");
|
|
90
118
|
replaceAll(VAULT_ID_PATTERN, "VAULT_ID");
|
|
119
|
+
redacted = redacted.replace(CARD_CANDIDATE_PATTERN, (match2) => {
|
|
120
|
+
const digits = match2.replace(/\D/g, "");
|
|
121
|
+
if (digits.length >= 13 && digits.length <= 19 && luhnValid(digits)) {
|
|
122
|
+
tokensReplaced.push(match2);
|
|
123
|
+
return "[REDACTED:CARD]";
|
|
124
|
+
}
|
|
125
|
+
return match2;
|
|
126
|
+
});
|
|
127
|
+
replaceAll(SSN_PATTERN, "SSN");
|
|
128
|
+
replaceAll(EMAIL_PATTERN, "EMAIL");
|
|
129
|
+
replaceAll(PHONE_PATTERN, "PHONE");
|
|
91
130
|
const highEntropyTokens = extractHighEntropyTokens(redacted);
|
|
92
131
|
for (const token of highEntropyTokens) {
|
|
93
132
|
if (!redacted.includes(token)) continue;
|
|
@@ -105,9 +144,10 @@ var AgentShield = class {
|
|
|
105
144
|
/**
|
|
106
145
|
* Check a prompt against the policy engine before sending to the LLM.
|
|
107
146
|
*
|
|
108
|
-
* Applies local-first
|
|
109
|
-
*
|
|
110
|
-
* the
|
|
147
|
+
* Applies best-effort local-first redaction before sending to the gateway, so
|
|
148
|
+
* recognized signing keys, custodial IDs, common PII, and high-entropy secrets
|
|
149
|
+
* are stripped before the prompt leaves the process. Redaction is one layer of
|
|
150
|
+
* defense, not a guarantee that every secret is caught (see redaction.ts).
|
|
111
151
|
*
|
|
112
152
|
* Returns the policy decision without executing the LLM call.
|
|
113
153
|
*/
|
|
@@ -186,12 +226,16 @@ var AgentShield = class {
|
|
|
186
226
|
}
|
|
187
227
|
/**
|
|
188
228
|
* Log an interaction to the Agent Shield Console.
|
|
229
|
+
*
|
|
230
|
+
* Redacts the prompt before transmitting: the audit trail must not store or
|
|
231
|
+
* carry raw secrets/PII, and this is an egress point just like /check.
|
|
189
232
|
*/
|
|
190
233
|
async log(input, result, requestId = newRequestId()) {
|
|
191
234
|
const scopedLog = log.child({
|
|
192
235
|
tenant_id: this.config.tenantId,
|
|
193
236
|
request_id: requestId
|
|
194
237
|
});
|
|
238
|
+
const { redacted } = redactSensitiveData(input);
|
|
195
239
|
const res = await fetch(`${this.config.consoleUrl}/api/sdk/v1/log`, {
|
|
196
240
|
method: "POST",
|
|
197
241
|
headers: {
|
|
@@ -199,7 +243,7 @@ var AgentShield = class {
|
|
|
199
243
|
Authorization: `Bearer ${this.config.apiKey}`
|
|
200
244
|
},
|
|
201
245
|
body: JSON.stringify({
|
|
202
|
-
input,
|
|
246
|
+
input: redacted,
|
|
203
247
|
tenantId: this.config.tenantId,
|
|
204
248
|
requestId,
|
|
205
249
|
userId: this.config.userId,
|
|
@@ -231,6 +275,9 @@ var ShieldBlockedError = class extends Error {
|
|
|
231
275
|
};
|
|
232
276
|
export {
|
|
233
277
|
AgentShield,
|
|
234
|
-
ShieldBlockedError
|
|
278
|
+
ShieldBlockedError,
|
|
279
|
+
newRequestId,
|
|
280
|
+
redactSensitiveData,
|
|
281
|
+
tenantId
|
|
235
282
|
};
|
|
236
283
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/ids.ts","../src/logger.ts","../src/redaction.ts"],"sourcesContent":["/**\n * G8R Agent Shield SDK\n *\n * Lightweight TypeScript client that wraps LLM calls with policy enforcement.\n * Automatically intercepts prompts, applies local-first VPC redaction (BitGo),\n * checks them against the G8R policy engine, and logs all activity to the\n * Agent Shield Console.\n *\n * Usage:\n * import { AgentShield } from '@g8r-security/agent-shield-sdk';\n *\n * const shield = new AgentShield({\n * consoleUrl: 'https://shield.yourcompany.com',\n * apiKey: 'sk-shield-...',\n * department: 'Finance',\n * userId: 'usr_FIN_042',\n * aiModel: 'GPT-4o',\n * });\n *\n * // Wrap any LLM call — the factory function is only invoked if the policy allows it\n * const result = await shield.wrap(\n * () => openai.chat.completions.create({\n * model: 'gpt-4o',\n * messages: [{ role: 'user', content: 'Summarize Q1 earnings' }],\n * }),\n * 'Summarize Q1 earnings'\n * );\n */\n\nimport { match } from 'ts-pattern';\nimport { newRequestId, type RequestId, type TenantId } from './ids';\nimport { log } from './logger';\nimport { redactSensitiveData } from './redaction';\n\nexport interface ShieldConfig {\n /** URL of the G8R Agent Shield Console */\n consoleUrl: string;\n /** API key for authentication */\n apiKey: string;\n /** Tenant this SDK instance operates on behalf of. Required. */\n tenantId: TenantId;\n /** Department of the calling user */\n department: string;\n /** User identifier */\n userId: string;\n /** AI model being called */\n aiModel: string;\n /** Optional: agent identifier (defaults to \"sdk-client\") */\n agentId?: string;\n /** Optional: employee display name (defaults to userId) */\n employeeName?: string;\n}\n\nexport interface PolicyCheckResult {\n decision: 'allowed' | 'blocked' | 'escalated';\n reason: string;\n violatedRule: string | null;\n requiresApproval: boolean;\n /**\n * Set to true when a kill-switch rule fires (e.g. unauthorized partner data\n * access). Consumers should tear down the agent session in response.\n */\n sessionRevoked?: boolean;\n complianceMappings: Array<{\n regulation: string;\n controlId: string;\n controlName: string;\n description: string;\n }>;\n /**\n * Tokens that were redacted from the prompt before it reached the gateway.\n * Undefined when no tokens were redacted (clean prompt).\n * Populated by the BitGo VPC local-first redaction layer.\n */\n redactedTokens?: string[];\n}\n\nexport interface ShieldLogEntry {\n id: string;\n decision: string;\n timestamp: string;\n}\n\nexport class AgentShield {\n private config: ShieldConfig;\n\n constructor(config: ShieldConfig) {\n this.config = config;\n }\n\n /**\n * Check a prompt against the policy engine before sending to the LLM.\n *\n * Applies local-first VPC redaction (BitGo) before sending to the gateway,\n * ensuring signing keys, custodial IDs, and high-entropy secrets never leave\n * the local process in plaintext.\n *\n * Returns the policy decision without executing the LLM call.\n */\n async check(\n prompt: string,\n requestId: RequestId = newRequestId()\n ): Promise<PolicyCheckResult> {\n // Step 1: Local-first redaction — strip signing keys and custodial IDs\n // before the prompt reaches the remote gateway (BitGo VPC masking).\n const { redacted, tokensReplaced } = redactSensitiveData(prompt);\n\n // `requestId` is generated per-call by default, but `wrap()` passes its\n // own value so /check and /log share a single correlation id end-to-end\n // (C2). Build a scoped logger bound to tenantId + requestId so any\n // failure path emits lines with that context automatically.\n const scopedLog = log.child({\n tenant_id: this.config.tenantId,\n request_id: requestId,\n });\n\n const res = await fetch(`${this.config.consoleUrl}/api/sdk/v1/check`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify({\n input: redacted, // send the redacted version — never the raw prompt\n tenantId: this.config.tenantId,\n requestId,\n userId: this.config.userId,\n department: this.config.department,\n aiModel: this.config.aiModel,\n agentId: this.config.agentId ?? 'sdk-client',\n }),\n });\n\n if (!res.ok) {\n scopedLog.error('Shield policy check failed', { status: res.status });\n throw new Error(`Shield policy check failed: ${res.status}`);\n }\n\n const result = await res.json();\n return {\n ...result,\n ...(tokensReplaced.length > 0 ? { redactedTokens: tokensReplaced } : {}),\n };\n }\n\n /**\n * Wrap an LLM call with policy enforcement.\n *\n * The `llmCallFactory` is a function that creates the LLM promise.\n * It is only invoked if the policy engine allows the action.\n * This prevents the LLM call from executing before the policy check completes.\n *\n * @param llmCallFactory - A function that returns the LLM call promise\n * @param prompt - The prompt text to evaluate against the policy engine\n * @returns The result of the LLM call if allowed\n * @throws ShieldBlockedError if the policy engine blocks the action\n *\n * @example\n * const result = await shield.wrap(\n * () => openai.chat.completions.create({\n * model: 'gpt-4o',\n * messages: [{ role: 'user', content: prompt }],\n * }),\n * prompt\n * );\n */\n async wrap<T>(llmCallFactory: () => Promise<T>, prompt: string): Promise<T> {\n // Generate a single requestId for this wrap() invocation and thread it\n // through both /check and /log so the two server-side log lines can be\n // joined end-to-end (C2). Without this, each call would mint its own id\n // and the audit trail would lose the policy→action linkage.\n const requestId = newRequestId();\n\n // Step 1: Check the prompt against the policy engine (includes redaction)\n const policyResult = await this.check(prompt, requestId);\n\n // Step 2: Log the attempt regardless of decision\n await this.log(prompt, policyResult, requestId);\n\n // Step 3: Enforce the decision. Exhaustive match — adding a fourth\n // decision value will fail TypeScript compilation here until handled.\n match(policyResult.decision)\n .with('blocked', () => {\n throw new ShieldBlockedError(\n policyResult.reason,\n policyResult.violatedRule,\n policyResult.complianceMappings,\n policyResult.sessionRevoked ?? false\n );\n })\n .with('escalated', () => {\n // In production, this would await human approval via webhook\n log.warn('Action escalated for human review', {\n reason: policyResult.reason,\n });\n })\n .with('allowed', () => {\n /* fall through to invoke the LLM */\n })\n .exhaustive();\n\n // Step 4: Only now invoke the LLM call — after policy check passed\n return llmCallFactory();\n }\n\n /**\n * Log an interaction to the Agent Shield Console.\n */\n private async log(\n input: string,\n result: PolicyCheckResult,\n requestId: RequestId = newRequestId()\n ): Promise<ShieldLogEntry> {\n const scopedLog = log.child({\n tenant_id: this.config.tenantId,\n request_id: requestId,\n });\n\n const res = await fetch(`${this.config.consoleUrl}/api/sdk/v1/log`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify({\n input,\n tenantId: this.config.tenantId,\n requestId,\n userId: this.config.userId,\n department: this.config.department,\n aiModel: this.config.aiModel,\n agentId: this.config.agentId ?? 'sdk-client',\n employeeName: this.config.employeeName ?? this.config.userId,\n decision: result.decision,\n reason: result.reason,\n violatedRule: result.violatedRule,\n requiresApproval: result.requiresApproval,\n complianceMappings: result.complianceMappings,\n }),\n });\n\n if (!res.ok) {\n scopedLog.error('Failed to log interaction', { status: res.status });\n }\n\n return res.json();\n }\n}\n\n/**\n * Error thrown when the policy engine blocks an LLM call.\n */\nexport class ShieldBlockedError extends Error {\n public readonly violatedRule: string | null;\n public readonly complianceMappings: PolicyCheckResult['complianceMappings'];\n public readonly sessionRevoked: boolean;\n\n constructor(\n reason: string,\n violatedRule: string | null,\n complianceMappings: PolicyCheckResult['complianceMappings'],\n sessionRevoked: boolean = false\n ) {\n super(`[G8R Shield BLOCKED] ${reason}`);\n this.name = 'ShieldBlockedError';\n this.violatedRule = violatedRule;\n this.complianceMappings = complianceMappings;\n this.sessionRevoked = sessionRevoked;\n }\n}\n","/**\n * Branded id types + constructors used by the SDK.\n *\n * Vendored from the engine's `types.ts` so the published\n * `@g8r-security/agent-shield-sdk` package carries no `@g8r-security/core` dependency — the\n * engine is private IP and must not be a public-package dependency. Only\n * the id types/helpers the SDK actually uses are copied here.\n */\n\n/**\n * Identifies a single tenant in the multi-tenant governance plane.\n * Branded so a raw string can't be passed where a TenantId is expected.\n */\nexport type TenantId = string & { readonly __brand: 'TenantId' };\n\n/** Per-request correlation ID. Generated client-side by the SDK. */\nexport type RequestId = string & { readonly __brand: 'RequestId' };\n\nconst TENANT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;\n\n/**\n * Cast a string to TenantId. Use at boundaries (config, env, request body).\n * Enforces the charset / length contract: 1-64 chars of `[a-z0-9-]`,\n * starting with `[a-z0-9]` — catches programmatic misuse (config typos,\n * hard-coded slugs that drift).\n */\nexport function tenantId(s: string): TenantId {\n if (!s) throw new Error('tenantId cannot be empty');\n if (!TENANT_ID_PATTERN.test(s)) {\n throw new Error(\n `tenantId must be 1-64 chars, [a-z0-9-], starting with [a-z0-9] (got ${JSON.stringify(s)})`\n );\n }\n return s as TenantId;\n}\n\n/** Generate a fresh request ID. Prefers crypto.randomUUID where available. */\nexport function newRequestId(): RequestId {\n // crypto.randomUUID is available in Node 19+ and all modern browsers.\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID() as RequestId;\n }\n // Fallback: timestamp + Math.random (best-effort, not crypto-strong).\n return `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` as RequestId;\n}\n","/**\n * G8R structured JSON logger.\n *\n * Tiny, dependency-free logger that emits one JSON object per line. Designed\n * for governance contexts where each log line must carry `tenant_id` and\n * `request_id` so downstream pipelines can route audit trails per-tenant.\n *\n * Vendored into the SDK (a verbatim copy of the engine's logger) so the\n * published `@g8r-security/agent-shield-sdk` package carries no `@g8r-security/core`\n * dependency — the engine is private IP and must not be a public-package\n * dependency.\n *\n * Use the default `log` singleton for app-level chatter; call `createLogger`\n * (or `.child()`) when you need per-request bindings injected.\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport interface LogContext {\n tenant_id?: string;\n request_id?: string;\n [key: string]: unknown;\n}\n\nexport interface Logger {\n debug(msg: string, ctx?: LogContext): void;\n info(msg: string, ctx?: LogContext): void;\n warn(msg: string, ctx?: LogContext): void;\n error(msg: string, ctx?: LogContext): void;\n child(ctx: LogContext): Logger;\n}\n\nexport interface LoggerOptions {\n /** Defaults to 'info'. Anything below is dropped. */\n level?: LogLevel;\n /** Defaults to console.log. Override for testing or transport injection. */\n sink?: (line: string) => void;\n /** Context merged into every log line. */\n bindings?: LogContext;\n}\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n};\n\nexport function createLogger(opts: LoggerOptions = {}): Logger {\n const minLevel = LEVEL_ORDER[opts.level ?? 'info'];\n const sink = opts.sink ?? ((line: string) => console.log(line));\n const bindings = opts.bindings ?? {};\n\n function emit(level: LogLevel, msg: string, ctx?: LogContext): void {\n if (LEVEL_ORDER[level] < minLevel) return;\n const payload = {\n level,\n msg,\n ts: new Date().toISOString(),\n ...bindings,\n ...ctx,\n };\n sink(JSON.stringify(payload));\n }\n\n return {\n debug: (msg, ctx) => emit('debug', msg, ctx),\n info: (msg, ctx) => emit('info', msg, ctx),\n warn: (msg, ctx) => emit('warn', msg, ctx),\n error: (msg, ctx) => emit('error', msg, ctx),\n child: (extra) => createLogger({ ...opts, bindings: { ...bindings, ...extra } }),\n };\n}\n\n/** Default singleton — convenient for apps that don't need DI. */\nexport const log = createLogger();\n","/**\n * BitGo VPC Sensitive Data Redaction\n *\n * Redacts cryptographic keys, custodial identifiers, and high-entropy strings\n * BEFORE sending prompts to the G8R policy gateway (local-first redaction layer).\n *\n * Compliance:\n * - GDPR Art. 32: Security of Processing — appropriate technical measures\n * - PCI-DSS 3.4: Render PAN/sensitive data unreadable wherever stored or transmitted\n */\n\nexport interface RedactionResult {\n /** The input with all sensitive tokens replaced by placeholder strings. */\n redacted: string;\n /** The original sensitive token strings that were replaced. */\n tokensReplaced: string[];\n}\n\n/**\n * Shannon entropy: calculates bits per character.\n * High entropy (> 4.5) indicates likely cryptographic material.\n */\nfunction shannonEntropy(str: string): number {\n const freq = new Map<string, number>();\n for (const ch of str) {\n freq.set(ch, (freq.get(ch) ?? 0) + 1);\n }\n let entropy = 0;\n const len = str.length;\n for (const count of freq.values()) {\n const p = count / len;\n entropy -= p * Math.log2(p);\n }\n return entropy;\n}\n\n// ── Signing Key Patterns ─────────────────────────────────────────────────────\n\n/** BIP-32 extended public/private keys: xpub, xprv, ypub, zpub, zprv, etc. */\nconst BIP32_PATTERN = /\\b[xyz](?:pub|prv)[a-zA-Z0-9]{99,111}\\b/g;\n\n/**\n * WIF (Wallet Import Format) private keys.\n * Base58Check encoded, starts with 5 (uncompressed) or K/L (compressed), 51–52 chars.\n */\nconst WIF_PATTERN = /\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b/g;\n\n/**\n * Raw hex 256-bit keys — exactly 64 hex characters, optionally 0x-prefixed.\n * Matches Ethereum private keys, secp256k1 scalars, etc.\n */\nconst HEX_KEY_PATTERN = /\\b(?:0x)?[0-9a-fA-F]{64}\\b/g;\n\n/** PEM-encoded private or public key blocks (multi-line). */\nconst PEM_PATTERN =\n /-----BEGIN (?:RSA |EC |OPENSSH )?(?:PRIVATE|PUBLIC) KEY-----[\\s\\S]*?-----END (?:RSA |EC |OPENSSH )?(?:PRIVATE|PUBLIC) KEY-----/g;\n\n// ── Custodial ID Patterns ─────────────────────────────────────────────────────\n\n/** BitGo custodial-id format: `custodial-id:abc123xyz` */\nconst CUSTODIAL_ID_PATTERN = /\\bcustodial-id:[A-Za-z0-9_-]+\\b/g;\n\n/** Short custodial references: `cust-98765` */\nconst CUST_PATTERN = /\\bcust-\\d+\\b/gi;\n\n/** Wallet identifiers: `wallet-id:wlt-abc999` */\nconst WALLET_ID_PATTERN = /\\bwallet-id:[A-Za-z0-9_-]+\\b/g;\n\n/** Vault identifiers: `vault-id:v-secure-001` */\nconst VAULT_ID_PATTERN = /\\bvault-id:[A-Za-z0-9_-]+\\b/g;\n\n// ── Entropy Detection Constants ───────────────────────────────────────────────\n\n/** Minimum Shannon entropy (bits/char) to flag a token as likely cryptographic material. */\nconst ENTROPY_THRESHOLD = 4.5;\n\n/** Minimum token length to apply entropy analysis (shorter strings are too ambiguous). */\nconst ENTROPY_MIN_LENGTH = 32;\n\n/**\n * Find tokens in the string that exceed the entropy threshold.\n * Splits on whitespace and common delimiters to isolate candidates.\n */\nfunction extractHighEntropyTokens(input: string): string[] {\n const candidates = input.split(/[\\s,;:\"'`(){}[\\]<>]+/).filter(Boolean);\n return candidates.filter(\n (token) => token.length >= ENTROPY_MIN_LENGTH && shannonEntropy(token) >= ENTROPY_THRESHOLD\n );\n}\n\n/**\n * Redact sensitive data from a prompt string before it reaches the gateway.\n *\n * Processing order (important — PEM first to avoid splitting on inner patterns):\n * 1. PEM private/public key blocks\n * 2. BIP-32 extended keys\n * 3. WIF private keys\n * 4. Raw hex 256-bit keys\n * 5. Custodial IDs (all four variants)\n * 6. High-entropy string catch-all\n */\nexport function redactSensitiveData(input: string): RedactionResult {\n const tokensReplaced: string[] = [];\n let redacted = input;\n\n function replaceAll(pattern: RegExp, label: string): void {\n redacted = redacted.replace(pattern, (match) => {\n tokensReplaced.push(match);\n return `[REDACTED:${label}]`;\n });\n }\n\n replaceAll(PEM_PATTERN, 'PEM_KEY');\n replaceAll(BIP32_PATTERN, 'BIP32_KEY');\n replaceAll(WIF_PATTERN, 'WIF_KEY');\n replaceAll(HEX_KEY_PATTERN, 'HEX_KEY');\n replaceAll(CUSTODIAL_ID_PATTERN, 'CUSTODIAL_ID');\n replaceAll(CUST_PATTERN, 'CUST_ID');\n replaceAll(WALLET_ID_PATTERN, 'WALLET_ID');\n replaceAll(VAULT_ID_PATTERN, 'VAULT_ID');\n\n // High-entropy catch-all — runs on the already-redacted string to avoid double-replacing.\n const highEntropyTokens = extractHighEntropyTokens(redacted);\n for (const token of highEntropyTokens) {\n if (!redacted.includes(token)) continue;\n tokensReplaced.push(token);\n redacted = redacted.split(token).join('[REDACTED:HIGH_ENTROPY]');\n }\n\n return { redacted, tokensReplaced };\n}\n"],"mappings":"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","names":["match"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/ids.ts","../src/logger.ts","../src/redaction.ts"],"sourcesContent":["/**\n * G8R Agent Shield SDK\n *\n * Lightweight TypeScript client that wraps LLM calls with policy enforcement.\n * Automatically intercepts prompts, applies best-effort local-first redaction,\n * checks them against the G8R policy engine, and logs all activity to the\n * Agent Shield Console.\n *\n * Usage:\n * import { AgentShield, tenantId } from '@g8r-security/agent-shield-sdk';\n *\n * const shield = new AgentShield({\n * consoleUrl: 'https://shield.yourcompany.com',\n * apiKey: 'sk-shield-...',\n * tenantId: tenantId('acme-corp'),\n * department: 'Finance',\n * userId: 'usr_FIN_042',\n * aiModel: 'GPT-4o',\n * });\n *\n * // Wrap any LLM call — the factory function is only invoked if the policy allows it\n * const result = await shield.wrap(\n * () => openai.chat.completions.create({\n * model: 'gpt-4o',\n * messages: [{ role: 'user', content: 'Summarize Q1 earnings' }],\n * }),\n * 'Summarize Q1 earnings'\n * );\n */\n\nimport { match } from 'ts-pattern';\nimport { newRequestId, type RequestId, type TenantId } from './ids';\nimport { log } from './logger';\nimport { redactSensitiveData } from './redaction';\n\n// ── Public API re-exports ────────────────────────────────────────────────────\n// Consumers need `tenantId()` to construct the branded TenantId required by\n// ShieldConfig, and `redactSensitiveData` is documented as a public helper.\nexport { tenantId, newRequestId } from './ids';\nexport type { TenantId, RequestId } from './ids';\nexport { redactSensitiveData } from './redaction';\nexport type { RedactionResult } from './redaction';\n\nexport interface ShieldConfig {\n /** URL of the G8R Agent Shield Console */\n consoleUrl: string;\n /** API key for authentication */\n apiKey: string;\n /** Tenant this SDK instance operates on behalf of. Required. */\n tenantId: TenantId;\n /** Department of the calling user */\n department: string;\n /** User identifier */\n userId: string;\n /** AI model being called */\n aiModel: string;\n /** Optional: agent identifier (defaults to \"sdk-client\") */\n agentId?: string;\n /** Optional: employee display name (defaults to userId) */\n employeeName?: string;\n}\n\nexport interface PolicyCheckResult {\n decision: 'allowed' | 'blocked' | 'escalated';\n reason: string;\n violatedRule: string | null;\n requiresApproval: boolean;\n /**\n * Set to true when a kill-switch rule fires (e.g. unauthorized partner data\n * access). Consumers should tear down the agent session in response.\n */\n sessionRevoked?: boolean;\n complianceMappings: Array<{\n regulation: string;\n controlId: string;\n controlName: string;\n description: string;\n }>;\n /**\n * Tokens that were redacted from the prompt before it reached the gateway.\n * Undefined when no tokens were redacted (clean prompt).\n * Populated by the local-first redaction layer.\n */\n redactedTokens?: string[];\n}\n\nexport interface ShieldLogEntry {\n id: string;\n decision: string;\n timestamp: string;\n}\n\nexport class AgentShield {\n private config: ShieldConfig;\n\n constructor(config: ShieldConfig) {\n this.config = config;\n }\n\n /**\n * Check a prompt against the policy engine before sending to the LLM.\n *\n * Applies best-effort local-first redaction before sending to the gateway, so\n * recognized signing keys, custodial IDs, common PII, and high-entropy secrets\n * are stripped before the prompt leaves the process. Redaction is one layer of\n * defense, not a guarantee that every secret is caught (see redaction.ts).\n *\n * Returns the policy decision without executing the LLM call.\n */\n async check(\n prompt: string,\n requestId: RequestId = newRequestId()\n ): Promise<PolicyCheckResult> {\n // Step 1: Local-first redaction — strip recognized secrets and PII\n // before the prompt reaches the remote gateway.\n const { redacted, tokensReplaced } = redactSensitiveData(prompt);\n\n // `requestId` is generated per-call by default, but `wrap()` passes its\n // own value so /check and /log share a single correlation id end-to-end\n // (C2). Build a scoped logger bound to tenantId + requestId so any\n // failure path emits lines with that context automatically.\n const scopedLog = log.child({\n tenant_id: this.config.tenantId,\n request_id: requestId,\n });\n\n const res = await fetch(`${this.config.consoleUrl}/api/sdk/v1/check`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify({\n input: redacted, // send the redacted version — never the raw prompt\n tenantId: this.config.tenantId,\n requestId,\n userId: this.config.userId,\n department: this.config.department,\n aiModel: this.config.aiModel,\n agentId: this.config.agentId ?? 'sdk-client',\n }),\n });\n\n if (!res.ok) {\n scopedLog.error('Shield policy check failed', { status: res.status });\n throw new Error(`Shield policy check failed: ${res.status}`);\n }\n\n const result = await res.json();\n return {\n ...result,\n ...(tokensReplaced.length > 0 ? { redactedTokens: tokensReplaced } : {}),\n };\n }\n\n /**\n * Wrap an LLM call with policy enforcement.\n *\n * The `llmCallFactory` is a function that creates the LLM promise.\n * It is only invoked if the policy engine allows the action.\n * This prevents the LLM call from executing before the policy check completes.\n *\n * @param llmCallFactory - A function that returns the LLM call promise\n * @param prompt - The prompt text to evaluate against the policy engine\n * @returns The result of the LLM call if allowed\n * @throws ShieldBlockedError if the policy engine blocks the action\n *\n * @example\n * const result = await shield.wrap(\n * () => openai.chat.completions.create({\n * model: 'gpt-4o',\n * messages: [{ role: 'user', content: prompt }],\n * }),\n * prompt\n * );\n */\n async wrap<T>(llmCallFactory: () => Promise<T>, prompt: string): Promise<T> {\n // Generate a single requestId for this wrap() invocation and thread it\n // through both /check and /log so the two server-side log lines can be\n // joined end-to-end (C2). Without this, each call would mint its own id\n // and the audit trail would lose the policy→action linkage.\n const requestId = newRequestId();\n\n // Step 1: Check the prompt against the policy engine (includes redaction)\n const policyResult = await this.check(prompt, requestId);\n\n // Step 2: Log the attempt regardless of decision. log() redacts before\n // transmitting, so the audit-log path never leaks raw secrets either.\n await this.log(prompt, policyResult, requestId);\n\n // Step 3: Enforce the decision. Exhaustive match — adding a fourth\n // decision value will fail TypeScript compilation here until handled.\n match(policyResult.decision)\n .with('blocked', () => {\n throw new ShieldBlockedError(\n policyResult.reason,\n policyResult.violatedRule,\n policyResult.complianceMappings,\n policyResult.sessionRevoked ?? false\n );\n })\n .with('escalated', () => {\n // In production, this would await human approval via webhook\n log.warn('Action escalated for human review', {\n reason: policyResult.reason,\n });\n })\n .with('allowed', () => {\n /* fall through to invoke the LLM */\n })\n .exhaustive();\n\n // Step 4: Only now invoke the LLM call — after policy check passed\n return llmCallFactory();\n }\n\n /**\n * Log an interaction to the Agent Shield Console.\n *\n * Redacts the prompt before transmitting: the audit trail must not store or\n * carry raw secrets/PII, and this is an egress point just like /check.\n */\n private async log(\n input: string,\n result: PolicyCheckResult,\n requestId: RequestId = newRequestId()\n ): Promise<ShieldLogEntry> {\n const scopedLog = log.child({\n tenant_id: this.config.tenantId,\n request_id: requestId,\n });\n\n // Redact at the egress boundary — never send the raw prompt to /log.\n const { redacted } = redactSensitiveData(input);\n\n const res = await fetch(`${this.config.consoleUrl}/api/sdk/v1/log`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify({\n input: redacted,\n tenantId: this.config.tenantId,\n requestId,\n userId: this.config.userId,\n department: this.config.department,\n aiModel: this.config.aiModel,\n agentId: this.config.agentId ?? 'sdk-client',\n employeeName: this.config.employeeName ?? this.config.userId,\n decision: result.decision,\n reason: result.reason,\n violatedRule: result.violatedRule,\n requiresApproval: result.requiresApproval,\n complianceMappings: result.complianceMappings,\n }),\n });\n\n if (!res.ok) {\n scopedLog.error('Failed to log interaction', { status: res.status });\n }\n\n return res.json();\n }\n}\n\n/**\n * Error thrown when the policy engine blocks an LLM call.\n */\nexport class ShieldBlockedError extends Error {\n public readonly violatedRule: string | null;\n public readonly complianceMappings: PolicyCheckResult['complianceMappings'];\n public readonly sessionRevoked: boolean;\n\n constructor(\n reason: string,\n violatedRule: string | null,\n complianceMappings: PolicyCheckResult['complianceMappings'],\n sessionRevoked: boolean = false\n ) {\n super(`[G8R Shield BLOCKED] ${reason}`);\n this.name = 'ShieldBlockedError';\n this.violatedRule = violatedRule;\n this.complianceMappings = complianceMappings;\n this.sessionRevoked = sessionRevoked;\n }\n}\n","/**\n * Branded id types + constructors used by the SDK.\n *\n * Self-contained in the SDK so the published `@g8r-security/agent-shield-sdk`\n * package has no dependency on internal packages. Only the id types/helpers the\n * SDK actually uses are included here.\n */\n\n/**\n * Identifies a single tenant in the multi-tenant governance plane.\n * Branded so a raw string can't be passed where a TenantId is expected.\n */\nexport type TenantId = string & { readonly __brand: 'TenantId' };\n\n/** Per-request correlation ID. Generated client-side by the SDK. */\nexport type RequestId = string & { readonly __brand: 'RequestId' };\n\nconst TENANT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;\n\n/**\n * Cast a string to TenantId. Use at boundaries (config, env, request body).\n * Enforces the charset / length contract: 1-64 chars of `[a-z0-9-]`,\n * starting with `[a-z0-9]` — catches programmatic misuse (config typos,\n * hard-coded slugs that drift).\n */\nexport function tenantId(s: string): TenantId {\n if (!s) throw new Error('tenantId cannot be empty');\n if (!TENANT_ID_PATTERN.test(s)) {\n throw new Error(\n `tenantId must be 1-64 chars, [a-z0-9-], starting with [a-z0-9] (got ${JSON.stringify(s)})`\n );\n }\n return s as TenantId;\n}\n\n/** Generate a fresh request ID. Prefers crypto.randomUUID where available. */\nexport function newRequestId(): RequestId {\n // crypto.randomUUID is available in Node 19+ and all modern browsers.\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID() as RequestId;\n }\n // Fallback: timestamp + Math.random (best-effort, not crypto-strong).\n return `req-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` as RequestId;\n}\n","/**\n * G8R structured JSON logger.\n *\n * Tiny, dependency-free logger that emits one JSON object per line. Designed\n * for governance contexts where each log line must carry `tenant_id` and\n * `request_id` so downstream pipelines can route audit trails per-tenant.\n *\n * Self-contained in the SDK so the published `@g8r-security/agent-shield-sdk`\n * package has no dependency on internal packages.\n *\n * Use the default `log` singleton for app-level chatter; call `createLogger`\n * (or `.child()`) when you need per-request bindings injected.\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport interface LogContext {\n tenant_id?: string;\n request_id?: string;\n [key: string]: unknown;\n}\n\nexport interface Logger {\n debug(msg: string, ctx?: LogContext): void;\n info(msg: string, ctx?: LogContext): void;\n warn(msg: string, ctx?: LogContext): void;\n error(msg: string, ctx?: LogContext): void;\n child(ctx: LogContext): Logger;\n}\n\nexport interface LoggerOptions {\n /** Defaults to 'info'. Anything below is dropped. */\n level?: LogLevel;\n /** Defaults to console.log. Override for testing or transport injection. */\n sink?: (line: string) => void;\n /** Context merged into every log line. */\n bindings?: LogContext;\n}\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n};\n\nexport function createLogger(opts: LoggerOptions = {}): Logger {\n const minLevel = LEVEL_ORDER[opts.level ?? 'info'];\n const sink = opts.sink ?? ((line: string) => console.log(line));\n const bindings = opts.bindings ?? {};\n\n function emit(level: LogLevel, msg: string, ctx?: LogContext): void {\n if (LEVEL_ORDER[level] < minLevel) return;\n const payload = {\n level,\n msg,\n ts: new Date().toISOString(),\n ...bindings,\n ...ctx,\n };\n sink(JSON.stringify(payload));\n }\n\n return {\n debug: (msg, ctx) => emit('debug', msg, ctx),\n info: (msg, ctx) => emit('info', msg, ctx),\n warn: (msg, ctx) => emit('warn', msg, ctx),\n error: (msg, ctx) => emit('error', msg, ctx),\n child: (extra) => createLogger({ ...opts, bindings: { ...bindings, ...extra } }),\n };\n}\n\n/** Default singleton — convenient for apps that don't need DI. */\nexport const log = createLogger();\n","/**\n * Local-First Sensitive Data Redaction\n *\n * Best-effort redaction of common secret and PII formats BEFORE prompts reach\n * the policy gateway. Covers cryptographic key formats, custodial identifiers,\n * high-entropy strings, and common PII (card numbers validated via Luhn, US\n * SSNs, email addresses, and phone numbers).\n *\n * IMPORTANT — this is a defense-in-depth layer, NOT a guarantee of completeness.\n * Pattern- and entropy-based redaction cannot catch every secret or PII shape\n * (e.g. unstructured PII, names, novel token formats, or values below the\n * entropy threshold). Do not rely on it as a sole control; keep downstream\n * safeguards and human review in place.\n *\n * It helps *support* (does not by itself satisfy) controls such as GDPR Art. 32\n * and PCI-DSS PAN-handling by reducing sensitive-data exposure to the gateway.\n */\n\nexport interface RedactionResult {\n /** The input with all sensitive tokens replaced by placeholder strings. */\n redacted: string;\n /** The original sensitive token strings that were replaced. */\n tokensReplaced: string[];\n}\n\n/**\n * Shannon entropy: calculates bits per character.\n * High entropy (> 4.5) indicates likely cryptographic material.\n */\nfunction shannonEntropy(str: string): number {\n const freq = new Map<string, number>();\n for (const ch of str) {\n freq.set(ch, (freq.get(ch) ?? 0) + 1);\n }\n let entropy = 0;\n const len = str.length;\n for (const count of freq.values()) {\n const p = count / len;\n entropy -= p * Math.log2(p);\n }\n return entropy;\n}\n\n// ── Signing Key Patterns ─────────────────────────────────────────────────────\n\n/** BIP-32 extended public/private keys: xpub, xprv, ypub, zpub, zprv, etc. */\nconst BIP32_PATTERN = /\\b[xyz](?:pub|prv)[a-zA-Z0-9]{99,111}\\b/g;\n\n/**\n * WIF (Wallet Import Format) private keys.\n * Base58Check encoded, starts with 5 (uncompressed) or K/L (compressed), 51–52 chars.\n */\nconst WIF_PATTERN = /\\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\\b/g;\n\n/**\n * Raw hex 256-bit keys — exactly 64 hex characters, optionally 0x-prefixed.\n * Matches Ethereum private keys, secp256k1 scalars, etc.\n */\nconst HEX_KEY_PATTERN = /\\b(?:0x)?[0-9a-fA-F]{64}\\b/g;\n\n/** PEM-encoded private or public key blocks (multi-line). */\nconst PEM_PATTERN =\n /-----BEGIN (?:RSA |EC |OPENSSH )?(?:PRIVATE|PUBLIC) KEY-----[\\s\\S]*?-----END (?:RSA |EC |OPENSSH )?(?:PRIVATE|PUBLIC) KEY-----/g;\n\n// ── Custodial ID Patterns ─────────────────────────────────────────────────────\n\n/** Custodial-id format: `custodial-id:abc123xyz` */\nconst CUSTODIAL_ID_PATTERN = /\\bcustodial-id:[A-Za-z0-9_-]+\\b/g;\n\n/** Short custodial references: `cust-98765` */\nconst CUST_PATTERN = /\\bcust-\\d+\\b/gi;\n\n/** Wallet identifiers: `wallet-id:wlt-abc999` */\nconst WALLET_ID_PATTERN = /\\bwallet-id:[A-Za-z0-9_-]+\\b/g;\n\n/** Vault identifiers: `vault-id:v-secure-001` */\nconst VAULT_ID_PATTERN = /\\bvault-id:[A-Za-z0-9_-]+\\b/g;\n\n// ── PII Patterns (best-effort) ────────────────────────────────────────────────\n\n/** Email addresses. */\nconst EMAIL_PATTERN = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g;\n\n/** US Social Security Numbers in dashed or spaced form, e.g. `123-45-6789`. */\nconst SSN_PATTERN = /\\b\\d{3}[ -]\\d{2}[ -]\\d{4}\\b/g;\n\n/**\n * Phone numbers with explicit separators (avoids matching arbitrary digit runs).\n * Optional country code, then 3-3-4 with space/dot/hyphen between groups.\n */\nconst PHONE_PATTERN = /\\b(?:\\+?\\d{1,3}[ .-]?)?\\(?\\d{3}\\)?[ .-]\\d{3}[ .-]\\d{4}\\b/g;\n\n/**\n * Candidate card-number runs: 13–19 digits with optional single space/hyphen\n * separators. Validated with Luhn before redacting to avoid false positives on\n * arbitrary long numbers (order/invoice IDs, etc.).\n */\nconst CARD_CANDIDATE_PATTERN = /\\b\\d(?:[ -]?\\d){12,18}\\b/g;\n\n/**\n * Luhn checksum — used to gate card-number redaction so we only mask digit runs\n * that actually pass the check digit, not every long number.\n */\nfunction luhnValid(digits: string): boolean {\n let sum = 0;\n let double = false;\n for (let i = digits.length - 1; i >= 0; i--) {\n let d = digits.charCodeAt(i) - 48;\n if (double) {\n d *= 2;\n if (d > 9) d -= 9;\n }\n sum += d;\n double = !double;\n }\n return sum % 10 === 0;\n}\n\n// ── Entropy Detection Constants ───────────────────────────────────────────────\n\n/** Minimum Shannon entropy (bits/char) to flag a token as likely cryptographic material. */\nconst ENTROPY_THRESHOLD = 4.5;\n\n/** Minimum token length to apply entropy analysis (shorter strings are too ambiguous). */\nconst ENTROPY_MIN_LENGTH = 32;\n\n/**\n * Find tokens in the string that exceed the entropy threshold.\n * Splits on whitespace and common delimiters to isolate candidates.\n */\nfunction extractHighEntropyTokens(input: string): string[] {\n const candidates = input.split(/[\\s,;:\"'`(){}[\\]<>]+/).filter(Boolean);\n return candidates.filter(\n (token) => token.length >= ENTROPY_MIN_LENGTH && shannonEntropy(token) >= ENTROPY_THRESHOLD\n );\n}\n\n/**\n * Redact sensitive data from a prompt string before it reaches the gateway.\n *\n * Processing order (important — PEM first to avoid splitting on inner patterns):\n * 1. PEM private/public key blocks\n * 2. BIP-32 extended keys\n * 3. WIF private keys\n * 4. Raw hex 256-bit keys\n * 5. Custodial IDs (all four variants)\n * 6. PII — card numbers (Luhn-validated), SSNs, emails, phone numbers\n * 7. High-entropy string catch-all\n */\nexport function redactSensitiveData(input: string): RedactionResult {\n const tokensReplaced: string[] = [];\n let redacted = input;\n\n function replaceAll(pattern: RegExp, label: string): void {\n redacted = redacted.replace(pattern, (match) => {\n tokensReplaced.push(match);\n return `[REDACTED:${label}]`;\n });\n }\n\n replaceAll(PEM_PATTERN, 'PEM_KEY');\n replaceAll(BIP32_PATTERN, 'BIP32_KEY');\n replaceAll(WIF_PATTERN, 'WIF_KEY');\n replaceAll(HEX_KEY_PATTERN, 'HEX_KEY');\n replaceAll(CUSTODIAL_ID_PATTERN, 'CUSTODIAL_ID');\n replaceAll(CUST_PATTERN, 'CUST_ID');\n replaceAll(WALLET_ID_PATTERN, 'WALLET_ID');\n replaceAll(VAULT_ID_PATTERN, 'VAULT_ID');\n\n // PII (best-effort). Card numbers first, gated by Luhn so we don't mask every\n // long digit run; then structured SSN / email / phone formats.\n redacted = redacted.replace(CARD_CANDIDATE_PATTERN, (match) => {\n const digits = match.replace(/\\D/g, '');\n if (digits.length >= 13 && digits.length <= 19 && luhnValid(digits)) {\n tokensReplaced.push(match);\n return '[REDACTED:CARD]';\n }\n return match;\n });\n replaceAll(SSN_PATTERN, 'SSN');\n replaceAll(EMAIL_PATTERN, 'EMAIL');\n replaceAll(PHONE_PATTERN, 'PHONE');\n\n // High-entropy catch-all — runs on the already-redacted string to avoid double-replacing.\n const highEntropyTokens = extractHighEntropyTokens(redacted);\n for (const token of highEntropyTokens) {\n if (!redacted.includes(token)) continue;\n tokensReplaced.push(token);\n redacted = redacted.split(token).join('[REDACTED:HIGH_ENTROPY]');\n }\n\n return { redacted, tokensReplaced };\n}\n"],"mappings":";AA8BA,SAAS,aAAa;;;ACbtB,IAAM,oBAAoB;AAQnB,SAAS,SAAS,GAAqB;AAC5C,MAAI,CAAC,EAAG,OAAM,IAAI,MAAM,0BAA0B;AAClD,MAAI,CAAC,kBAAkB,KAAK,CAAC,GAAG;AAC9B,UAAM,IAAI;AAAA,MACR,uEAAuE,KAAK,UAAU,CAAC,CAAC;AAAA,IAC1F;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,eAA0B;AAExC,MAAI,OAAO,WAAW,eAAe,OAAO,OAAO,eAAe,YAAY;AAC5E,WAAO,OAAO,WAAW;AAAA,EAC3B;AAEA,SAAO,OAAO,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAClF;;;ACJA,IAAM,cAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AACT;AAEO,SAAS,aAAa,OAAsB,CAAC,GAAW;AAC7D,QAAM,WAAW,YAAY,KAAK,SAAS,MAAM;AACjD,QAAM,OAAO,KAAK,SAAS,CAAC,SAAiB,QAAQ,IAAI,IAAI;AAC7D,QAAM,WAAW,KAAK,YAAY,CAAC;AAEnC,WAAS,KAAK,OAAiB,KAAa,KAAwB;AAClE,QAAI,YAAY,KAAK,IAAI,SAAU;AACnC,UAAM,UAAU;AAAA,MACd;AAAA,MACA;AAAA,MACA,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,MAC3B,GAAG;AAAA,MACH,GAAG;AAAA,IACL;AACA,SAAK,KAAK,UAAU,OAAO,CAAC;AAAA,EAC9B;AAEA,SAAO;AAAA,IACL,OAAO,CAAC,KAAK,QAAQ,KAAK,SAAS,KAAK,GAAG;AAAA,IAC3C,MAAM,CAAC,KAAK,QAAQ,KAAK,QAAQ,KAAK,GAAG;AAAA,IACzC,MAAM,CAAC,KAAK,QAAQ,KAAK,QAAQ,KAAK,GAAG;AAAA,IACzC,OAAO,CAAC,KAAK,QAAQ,KAAK,SAAS,KAAK,GAAG;AAAA,IAC3C,OAAO,CAAC,UAAU,aAAa,EAAE,GAAG,MAAM,UAAU,EAAE,GAAG,UAAU,GAAG,MAAM,EAAE,CAAC;AAAA,EACjF;AACF;AAGO,IAAM,MAAM,aAAa;;;AC5ChC,SAAS,eAAe,KAAqB;AAC3C,QAAM,OAAO,oBAAI,IAAoB;AACrC,aAAW,MAAM,KAAK;AACpB,SAAK,IAAI,KAAK,KAAK,IAAI,EAAE,KAAK,KAAK,CAAC;AAAA,EACtC;AACA,MAAI,UAAU;AACd,QAAM,MAAM,IAAI;AAChB,aAAW,SAAS,KAAK,OAAO,GAAG;AACjC,UAAM,IAAI,QAAQ;AAClB,eAAW,IAAI,KAAK,KAAK,CAAC;AAAA,EAC5B;AACA,SAAO;AACT;AAKA,IAAM,gBAAgB;AAMtB,IAAM,cAAc;AAMpB,IAAM,kBAAkB;AAGxB,IAAM,cACJ;AAKF,IAAM,uBAAuB;AAG7B,IAAM,eAAe;AAGrB,IAAM,oBAAoB;AAG1B,IAAM,mBAAmB;AAKzB,IAAM,gBAAgB;AAGtB,IAAM,cAAc;AAMpB,IAAM,gBAAgB;AAOtB,IAAM,yBAAyB;AAM/B,SAAS,UAAU,QAAyB;AAC1C,MAAI,MAAM;AACV,MAAI,SAAS;AACb,WAAS,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;AAC3C,QAAI,IAAI,OAAO,WAAW,CAAC,IAAI;AAC/B,QAAI,QAAQ;AACV,WAAK;AACL,UAAI,IAAI,EAAG,MAAK;AAAA,IAClB;AACA,WAAO;AACP,aAAS,CAAC;AAAA,EACZ;AACA,SAAO,MAAM,OAAO;AACtB;AAKA,IAAM,oBAAoB;AAG1B,IAAM,qBAAqB;AAM3B,SAAS,yBAAyB,OAAyB;AACzD,QAAM,aAAa,MAAM,MAAM,sBAAsB,EAAE,OAAO,OAAO;AACrE,SAAO,WAAW;AAAA,IAChB,CAAC,UAAU,MAAM,UAAU,sBAAsB,eAAe,KAAK,KAAK;AAAA,EAC5E;AACF;AAcO,SAAS,oBAAoB,OAAgC;AAClE,QAAM,iBAA2B,CAAC;AAClC,MAAI,WAAW;AAEf,WAAS,WAAW,SAAiB,OAAqB;AACxD,eAAW,SAAS,QAAQ,SAAS,CAACA,WAAU;AAC9C,qBAAe,KAAKA,MAAK;AACzB,aAAO,aAAa,KAAK;AAAA,IAC3B,CAAC;AAAA,EACH;AAEA,aAAW,aAAa,SAAS;AACjC,aAAW,eAAe,WAAW;AACrC,aAAW,aAAa,SAAS;AACjC,aAAW,iBAAiB,SAAS;AACrC,aAAW,sBAAsB,cAAc;AAC/C,aAAW,cAAc,SAAS;AAClC,aAAW,mBAAmB,WAAW;AACzC,aAAW,kBAAkB,UAAU;AAIvC,aAAW,SAAS,QAAQ,wBAAwB,CAACA,WAAU;AAC7D,UAAM,SAASA,OAAM,QAAQ,OAAO,EAAE;AACtC,QAAI,OAAO,UAAU,MAAM,OAAO,UAAU,MAAM,UAAU,MAAM,GAAG;AACnE,qBAAe,KAAKA,MAAK;AACzB,aAAO;AAAA,IACT;AACA,WAAOA;AAAA,EACT,CAAC;AACD,aAAW,aAAa,KAAK;AAC7B,aAAW,eAAe,OAAO;AACjC,aAAW,eAAe,OAAO;AAGjC,QAAM,oBAAoB,yBAAyB,QAAQ;AAC3D,aAAW,SAAS,mBAAmB;AACrC,QAAI,CAAC,SAAS,SAAS,KAAK,EAAG;AAC/B,mBAAe,KAAK,KAAK;AACzB,eAAW,SAAS,MAAM,KAAK,EAAE,KAAK,yBAAyB;AAAA,EACjE;AAEA,SAAO,EAAE,UAAU,eAAe;AACpC;;;AHpGO,IAAM,cAAN,MAAkB;AAAA,EAGvB,YAAY,QAAsB;AAChC,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,MACJ,QACA,YAAuB,aAAa,GACR;AAG5B,UAAM,EAAE,UAAU,eAAe,IAAI,oBAAoB,MAAM;AAM/D,UAAM,YAAY,IAAI,MAAM;AAAA,MAC1B,WAAW,KAAK,OAAO;AAAA,MACvB,YAAY;AAAA,IACd,CAAC;AAED,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,UAAU,qBAAqB;AAAA,MACpE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,KAAK,OAAO,MAAM;AAAA,MAC7C;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,OAAO;AAAA;AAAA,QACP,UAAU,KAAK,OAAO;AAAA,QACtB;AAAA,QACA,QAAQ,KAAK,OAAO;AAAA,QACpB,YAAY,KAAK,OAAO;AAAA,QACxB,SAAS,KAAK,OAAO;AAAA,QACrB,SAAS,KAAK,OAAO,WAAW;AAAA,MAClC,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,gBAAU,MAAM,8BAA8B,EAAE,QAAQ,IAAI,OAAO,CAAC;AACpE,YAAM,IAAI,MAAM,+BAA+B,IAAI,MAAM,EAAE;AAAA,IAC7D;AAEA,UAAM,SAAS,MAAM,IAAI,KAAK;AAC9B,WAAO;AAAA,MACL,GAAG;AAAA,MACH,GAAI,eAAe,SAAS,IAAI,EAAE,gBAAgB,eAAe,IAAI,CAAC;AAAA,IACxE;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBA,MAAM,KAAQ,gBAAkC,QAA4B;AAK1E,UAAM,YAAY,aAAa;AAG/B,UAAM,eAAe,MAAM,KAAK,MAAM,QAAQ,SAAS;AAIvD,UAAM,KAAK,IAAI,QAAQ,cAAc,SAAS;AAI9C,UAAM,aAAa,QAAQ,EACxB,KAAK,WAAW,MAAM;AACrB,YAAM,IAAI;AAAA,QACR,aAAa;AAAA,QACb,aAAa;AAAA,QACb,aAAa;AAAA,QACb,aAAa,kBAAkB;AAAA,MACjC;AAAA,IACF,CAAC,EACA,KAAK,aAAa,MAAM;AAEvB,UAAI,KAAK,qCAAqC;AAAA,QAC5C,QAAQ,aAAa;AAAA,MACvB,CAAC;AAAA,IACH,CAAC,EACA,KAAK,WAAW,MAAM;AAAA,IAEvB,CAAC,EACA,WAAW;AAGd,WAAO,eAAe;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,IACZ,OACA,QACA,YAAuB,aAAa,GACX;AACzB,UAAM,YAAY,IAAI,MAAM;AAAA,MAC1B,WAAW,KAAK,OAAO;AAAA,MACvB,YAAY;AAAA,IACd,CAAC;AAGD,UAAM,EAAE,SAAS,IAAI,oBAAoB,KAAK;AAE9C,UAAM,MAAM,MAAM,MAAM,GAAG,KAAK,OAAO,UAAU,mBAAmB;AAAA,MAClE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,KAAK,OAAO,MAAM;AAAA,MAC7C;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,OAAO;AAAA,QACP,UAAU,KAAK,OAAO;AAAA,QACtB;AAAA,QACA,QAAQ,KAAK,OAAO;AAAA,QACpB,YAAY,KAAK,OAAO;AAAA,QACxB,SAAS,KAAK,OAAO;AAAA,QACrB,SAAS,KAAK,OAAO,WAAW;AAAA,QAChC,cAAc,KAAK,OAAO,gBAAgB,KAAK,OAAO;AAAA,QACtD,UAAU,OAAO;AAAA,QACjB,QAAQ,OAAO;AAAA,QACf,cAAc,OAAO;AAAA,QACrB,kBAAkB,OAAO;AAAA,QACzB,oBAAoB,OAAO;AAAA,MAC7B,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,IAAI,IAAI;AACX,gBAAU,MAAM,6BAA6B,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IACrE;AAEA,WAAO,IAAI,KAAK;AAAA,EAClB;AACF;AAKO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAK5C,YACE,QACA,cACA,oBACA,iBAA0B,OAC1B;AACA,UAAM,wBAAwB,MAAM,EAAE;AACtC,SAAK,OAAO;AACZ,SAAK,eAAe;AACpB,SAAK,qBAAqB;AAC1B,SAAK,iBAAiB;AAAA,EACxB;AACF;","names":["match"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@g8r-security/agent-shield-sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "TypeScript client for G8R Agent Shield — wrap LLM and agent calls with policy enforcement, local-first redaction, and audit logging.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|