@attestly/compliance-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/index.d.mts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +212 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +209 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @attestly/compliance-core
|
|
2
|
+
|
|
3
|
+
A zero-latency, Web Standard-based SDK that strictly enforces AI compliance at the route boundary, *before* your API executes.
|
|
4
|
+
|
|
5
|
+
Built specifically to shift **EU AI Act Compliance**, **Model Version Locking**, and **PII Scrubbing** to the absolute left of your development cycle, without adding any overhead to your production environment.
|
|
6
|
+
|
|
7
|
+
## 🚀 Key Value Propositions
|
|
8
|
+
|
|
9
|
+
- **Zero-Dependency Core**: The SDK is incredibly lightweight and natively relies on Web Standard `Request` and `Response` objects. No heavy AST parsers or backend-bloat.
|
|
10
|
+
- **Zero-Latency (TTFB Protected)**: Validation runs entirely synchronously in milliseconds. In production, the wrapper acts as a near-zero-cost pass-through unless explicitly configured to intercept.
|
|
11
|
+
- **EU AI Act Shift-Left**: Catches Prohibited Practices (Article 5), auto-escalates High-Risk domains (like Law Enforcement or Biometrics), and strictly enforces required metadata traceability according to EU regulations.
|
|
12
|
+
- **Vercel AI SDK First**: Out-of-the-box support for streaming UIs. It inspects the prompt, scrubs PII, and returns the native `DataStreamResponse` totally untouched so your Lighthouse scores remain pinned at 100.
|
|
13
|
+
|
|
14
|
+
## 📦 Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @attestly/compliance-core
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 🛠Setup & Initialization
|
|
21
|
+
|
|
22
|
+
Instead of hand-coding your manifest, use our interactive CLI to intelligently scan your local codebase and generate a compliant `ai-manifest.json`.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @attestly/cli init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The CLI will:
|
|
29
|
+
1. Scan your project for AI model strings (e.g., `gpt-4o`, `claude-3-opus`).
|
|
30
|
+
2. Ask you to classify your primary System Domain under the EU AI Act.
|
|
31
|
+
3. Automatically escalate the risk category if you select high-risk domains.
|
|
32
|
+
4. Output your locked-down `ai-manifest.json`.
|
|
33
|
+
|
|
34
|
+
## 💻 Usage
|
|
35
|
+
|
|
36
|
+
Wrap your existing API routes in Next.js, Remix, or Hono.
|
|
37
|
+
|
|
38
|
+
### Standard API Route
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { withAttestlyCompliance } from '@attestly/compliance-core';
|
|
42
|
+
import manifest from '../../ai-manifest.json';
|
|
43
|
+
|
|
44
|
+
async function handler(req: Request) {
|
|
45
|
+
// Your standard LLM logic here...
|
|
46
|
+
return new Response(JSON.stringify({ success: true }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const POST = withAttestlyCompliance(handler, manifest);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Streaming API Route (Vercel AI SDK)
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { withAttestlyStream } from '@attestly/compliance-core';
|
|
56
|
+
import { streamText } from 'ai';
|
|
57
|
+
import { openai } from '@ai-sdk/openai';
|
|
58
|
+
import manifest from '../../ai-manifest.json';
|
|
59
|
+
|
|
60
|
+
async function handler(req: Request) {
|
|
61
|
+
const result = await streamText({
|
|
62
|
+
model: openai('gpt-4-0613'),
|
|
63
|
+
prompt: 'Hello world',
|
|
64
|
+
});
|
|
65
|
+
return result.toDataStreamResponse();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const POST = withAttestlyStream(handler, manifest);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## 📊 Attestly Studio (Local Telemetry)
|
|
72
|
+
|
|
73
|
+
You don't need to deploy to test your compliance barriers! Spin up the Attestly Studio right inside your terminal to see a real-time, local dashboard of your AI traffic.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx @attestly/cli studio
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This launches a local dashboard at `http://localhost:5050`.
|
|
80
|
+
|
|
81
|
+
- **Real-Time Feed**: Watch your local API requests stream in.
|
|
82
|
+
- **Inspector**: Click on a request to see the raw payload, the PII-scrubbed output, and any triggered EU AI Act violations.
|
|
83
|
+
- **Handoff**: Click the "Generate Trust Center" button to seamlessly hand off your local manifest to the Attestly SaaS platform to generate a public, SOC2-ready Trust Center!
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
interface AiManifest {
|
|
2
|
+
/**
|
|
3
|
+
* Allowed AI models. Must be version-locked (e.g., 'gpt-4-0613' instead of 'gpt-4').
|
|
4
|
+
*/
|
|
5
|
+
allowedModels: string[];
|
|
6
|
+
/**
|
|
7
|
+
* Required metadata tags for traceability. These keys must be present in the request payload or headers.
|
|
8
|
+
*/
|
|
9
|
+
requiredMetadata: string[];
|
|
10
|
+
/**
|
|
11
|
+
* PII scrubbing configuration.
|
|
12
|
+
*/
|
|
13
|
+
piiScrubbing: {
|
|
14
|
+
level: 'off' | 'standard' | 'strict';
|
|
15
|
+
redactString?: string;
|
|
16
|
+
customRegex?: string[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* EU AI Act Risk Category classification.
|
|
20
|
+
*/
|
|
21
|
+
euRiskCategory: 'minimal' | 'limited' | 'high' | 'gpai';
|
|
22
|
+
/**
|
|
23
|
+
* Use case context domain under the EU AI Act.
|
|
24
|
+
*/
|
|
25
|
+
systemDomain: 'general' | 'law-enforcement' | 'education' | 'employment' | 'critical-infrastructure' | 'biometrics';
|
|
26
|
+
/**
|
|
27
|
+
* Flag for explicit prohibited practices under Article 5 of the EU AI Act.
|
|
28
|
+
*/
|
|
29
|
+
prohibitedPractices?: {
|
|
30
|
+
realTimeBiometrics?: boolean;
|
|
31
|
+
socialScoring?: boolean;
|
|
32
|
+
emotionRecognition?: boolean;
|
|
33
|
+
manipulativeAI?: boolean;
|
|
34
|
+
untargetedScraping?: boolean;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type RequestHandler = (req: Request, ...args: any[]) => Promise<Response> | Response;
|
|
39
|
+
/**
|
|
40
|
+
* A higher-order function that wraps a standard Web API Request handler
|
|
41
|
+
* to enforce AI compliance according to the provided manifest.
|
|
42
|
+
*/
|
|
43
|
+
declare function withAttestlyCompliance(handler: RequestHandler, manifest: AiManifest): RequestHandler;
|
|
44
|
+
|
|
45
|
+
type StreamHandler = (req: Request, ...args: any[]) => Promise<Response> | Response;
|
|
46
|
+
/**
|
|
47
|
+
* A specialized wrapper for the Vercel AI SDK streams.
|
|
48
|
+
* Performs a synchronous compliance check upfront and returns the native stream untouched.
|
|
49
|
+
*/
|
|
50
|
+
declare function withAttestlyStream(handler: StreamHandler, manifest: AiManifest): StreamHandler;
|
|
51
|
+
|
|
52
|
+
export { type AiManifest, type RequestHandler, type StreamHandler, withAttestlyCompliance, withAttestlyStream };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
interface AiManifest {
|
|
2
|
+
/**
|
|
3
|
+
* Allowed AI models. Must be version-locked (e.g., 'gpt-4-0613' instead of 'gpt-4').
|
|
4
|
+
*/
|
|
5
|
+
allowedModels: string[];
|
|
6
|
+
/**
|
|
7
|
+
* Required metadata tags for traceability. These keys must be present in the request payload or headers.
|
|
8
|
+
*/
|
|
9
|
+
requiredMetadata: string[];
|
|
10
|
+
/**
|
|
11
|
+
* PII scrubbing configuration.
|
|
12
|
+
*/
|
|
13
|
+
piiScrubbing: {
|
|
14
|
+
level: 'off' | 'standard' | 'strict';
|
|
15
|
+
redactString?: string;
|
|
16
|
+
customRegex?: string[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* EU AI Act Risk Category classification.
|
|
20
|
+
*/
|
|
21
|
+
euRiskCategory: 'minimal' | 'limited' | 'high' | 'gpai';
|
|
22
|
+
/**
|
|
23
|
+
* Use case context domain under the EU AI Act.
|
|
24
|
+
*/
|
|
25
|
+
systemDomain: 'general' | 'law-enforcement' | 'education' | 'employment' | 'critical-infrastructure' | 'biometrics';
|
|
26
|
+
/**
|
|
27
|
+
* Flag for explicit prohibited practices under Article 5 of the EU AI Act.
|
|
28
|
+
*/
|
|
29
|
+
prohibitedPractices?: {
|
|
30
|
+
realTimeBiometrics?: boolean;
|
|
31
|
+
socialScoring?: boolean;
|
|
32
|
+
emotionRecognition?: boolean;
|
|
33
|
+
manipulativeAI?: boolean;
|
|
34
|
+
untargetedScraping?: boolean;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type RequestHandler = (req: Request, ...args: any[]) => Promise<Response> | Response;
|
|
39
|
+
/**
|
|
40
|
+
* A higher-order function that wraps a standard Web API Request handler
|
|
41
|
+
* to enforce AI compliance according to the provided manifest.
|
|
42
|
+
*/
|
|
43
|
+
declare function withAttestlyCompliance(handler: RequestHandler, manifest: AiManifest): RequestHandler;
|
|
44
|
+
|
|
45
|
+
type StreamHandler = (req: Request, ...args: any[]) => Promise<Response> | Response;
|
|
46
|
+
/**
|
|
47
|
+
* A specialized wrapper for the Vercel AI SDK streams.
|
|
48
|
+
* Performs a synchronous compliance check upfront and returns the native stream untouched.
|
|
49
|
+
*/
|
|
50
|
+
declare function withAttestlyStream(handler: StreamHandler, manifest: AiManifest): StreamHandler;
|
|
51
|
+
|
|
52
|
+
export { type AiManifest, type RequestHandler, type StreamHandler, withAttestlyCompliance, withAttestlyStream };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/utils/errors.ts
|
|
4
|
+
function createComplianceError(errors) {
|
|
5
|
+
const primaryCode = errors.length > 0 ? errors[0].code : "BLOCK_ENFORCED";
|
|
6
|
+
return new Response(
|
|
7
|
+
JSON.stringify({
|
|
8
|
+
error: "AttestlyComplianceViolation",
|
|
9
|
+
code: primaryCode,
|
|
10
|
+
details: errors
|
|
11
|
+
}),
|
|
12
|
+
{
|
|
13
|
+
status: 400,
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
"X-Attestly-Blocked": "true"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/utils/validator.ts
|
|
23
|
+
function validateManifest(payload, headers, manifest) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
if (payload && payload.model) {
|
|
26
|
+
const model = String(payload.model);
|
|
27
|
+
if (!manifest.allowedModels.includes(model)) {
|
|
28
|
+
errors.push({
|
|
29
|
+
code: "UNAUTHORIZED_MODEL",
|
|
30
|
+
message: `Model '${model}' is not in the allowedModels list.`
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const versionRegex = /-[0-9]+(-[0-9]+)*$/;
|
|
34
|
+
if (!versionRegex.test(model)) {
|
|
35
|
+
errors.push({
|
|
36
|
+
code: "INVALID_MODEL_VERSION",
|
|
37
|
+
message: `Model '${model}' must contain a strict version or date suffix (e.g., 'gpt-4-0613').`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const requiredKey of manifest.requiredMetadata) {
|
|
42
|
+
const hasInPayload = payload && payload[requiredKey] !== void 0;
|
|
43
|
+
const hasInHeader = headers.has(requiredKey) || headers.has(`x-${requiredKey}`);
|
|
44
|
+
if (!hasInPayload && !hasInHeader) {
|
|
45
|
+
errors.push({
|
|
46
|
+
code: "MISSING_REQUIRED_METADATA",
|
|
47
|
+
message: `Missing required metadata: '${requiredKey}'.`
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (manifest.prohibitedPractices) {
|
|
52
|
+
const practices = Object.entries(manifest.prohibitedPractices);
|
|
53
|
+
for (const [practice, isEnabled] of practices) {
|
|
54
|
+
if (isEnabled === true) {
|
|
55
|
+
errors.push({
|
|
56
|
+
code: "EU_PROHIBITED_PRACTICE",
|
|
57
|
+
message: `Under Article 5 of the EU AI Act, the practice '${practice}' is prohibited. Execution blocked.`
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (manifest.systemDomain === "law-enforcement" || manifest.systemDomain === "biometrics") {
|
|
63
|
+
if (manifest.euRiskCategory === "minimal" || manifest.euRiskCategory === "limited") {
|
|
64
|
+
errors.push({
|
|
65
|
+
code: "EU_RISK_MISCLASSIFICATION",
|
|
66
|
+
message: `Under Annex III of the EU AI Act, ${manifest.systemDomain} systems are automatically high-risk. Cannot be classified as ${manifest.euRiskCategory}.`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (manifest.euRiskCategory === "high") {
|
|
71
|
+
const hasSession = payload && payload.sessionId !== void 0 || headers.has("sessionId") || headers.has("x-sessionId");
|
|
72
|
+
const hasUserId = payload && payload.userId !== void 0 || headers.has("userId") || headers.has("x-userId");
|
|
73
|
+
const hasPurpose = payload && payload.purpose !== void 0 || headers.has("purpose") || headers.has("x-purpose");
|
|
74
|
+
if (!hasSession && !hasUserId && !hasPurpose) {
|
|
75
|
+
errors.push({
|
|
76
|
+
code: "MISSING_HIGH_RISK_TRACEABILITY",
|
|
77
|
+
message: "Under the EU AI Act, High-Risk systems require strict logging traceability. Missing metadata like sessionId, userId, or purpose."
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
valid: errors.length === 0,
|
|
83
|
+
errors
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/utils/scrubber.ts
|
|
88
|
+
var STANDARD_PII_REGEX = [
|
|
89
|
+
// Emails
|
|
90
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
91
|
+
// US SSN
|
|
92
|
+
/\b\d{3}-\d{2}-\d{4}\b/g,
|
|
93
|
+
// 16-digit Credit Cards (simple)
|
|
94
|
+
/\b(?:\d[ -]*?){13,16}\b/g,
|
|
95
|
+
// Generic API Keys (e.g., sk-..., AKIA...)
|
|
96
|
+
/\b(sk-[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16})\b/g
|
|
97
|
+
];
|
|
98
|
+
function scrubPayload(text, config) {
|
|
99
|
+
if (config.level === "off") {
|
|
100
|
+
return text;
|
|
101
|
+
}
|
|
102
|
+
if (config.level === "strict") {
|
|
103
|
+
throw new Error("NotImplemented: Strict PII scrubbing is reserved for the Attestly backend ML engine.");
|
|
104
|
+
}
|
|
105
|
+
let scrubbed = text;
|
|
106
|
+
const redactString = config.redactString ?? "[REDACTED]";
|
|
107
|
+
if (config.level === "standard") {
|
|
108
|
+
for (const regex of STANDARD_PII_REGEX) {
|
|
109
|
+
scrubbed = scrubbed.replace(regex, redactString);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (config.customRegex && config.customRegex.length > 0) {
|
|
113
|
+
for (const pattern of config.customRegex) {
|
|
114
|
+
try {
|
|
115
|
+
const regex = new RegExp(pattern, "g");
|
|
116
|
+
scrubbed = scrubbed.replace(regex, redactString);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.warn("Invalid custom regex pattern in PII scrubber:", pattern);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return scrubbed;
|
|
123
|
+
}
|
|
124
|
+
function scrubObject(obj, config) {
|
|
125
|
+
if (config.level === "off") return obj;
|
|
126
|
+
if (!obj) return obj;
|
|
127
|
+
if (typeof obj === "string") {
|
|
128
|
+
return scrubPayload(obj, config);
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(obj)) {
|
|
131
|
+
return obj.map((item) => scrubObject(item, config));
|
|
132
|
+
}
|
|
133
|
+
if (typeof obj === "object") {
|
|
134
|
+
const scrubbedObj = {};
|
|
135
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
136
|
+
scrubbedObj[key] = scrubObject(value, config);
|
|
137
|
+
}
|
|
138
|
+
return scrubbedObj;
|
|
139
|
+
}
|
|
140
|
+
return obj;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/middleware/withCompliance.ts
|
|
144
|
+
function withAttestlyCompliance(handler, manifest) {
|
|
145
|
+
return async (req, ...args) => {
|
|
146
|
+
let clonedReq = req.clone();
|
|
147
|
+
let payload = null;
|
|
148
|
+
try {
|
|
149
|
+
if (clonedReq.headers.get("content-type")?.includes("application/json")) {
|
|
150
|
+
payload = await clonedReq.json();
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
}
|
|
154
|
+
if (process.env.NODE_ENV === "development") {
|
|
155
|
+
const { valid, errors } = validateManifest(payload, req.headers, manifest);
|
|
156
|
+
if (!valid) {
|
|
157
|
+
return createComplianceError(errors);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (payload && manifest.piiScrubbing.level !== "off") {
|
|
161
|
+
const scrubbedPayload = scrubObject(payload, manifest.piiScrubbing);
|
|
162
|
+
const newReq = new Request(req.url, {
|
|
163
|
+
method: req.method,
|
|
164
|
+
headers: req.headers,
|
|
165
|
+
body: JSON.stringify(scrubbedPayload),
|
|
166
|
+
// @ts-ignore - some environments require duplex for streaming requests
|
|
167
|
+
duplex: "half"
|
|
168
|
+
});
|
|
169
|
+
return handler(newReq, ...args);
|
|
170
|
+
}
|
|
171
|
+
return handler(req, ...args);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/middleware/withStream.ts
|
|
176
|
+
function withAttestlyStream(handler, manifest) {
|
|
177
|
+
return async (req, ...args) => {
|
|
178
|
+
let clonedReq = req.clone();
|
|
179
|
+
let payload = null;
|
|
180
|
+
try {
|
|
181
|
+
if (clonedReq.headers.get("content-type")?.includes("application/json")) {
|
|
182
|
+
payload = await clonedReq.json();
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
}
|
|
186
|
+
if (process.env.NODE_ENV === "development") {
|
|
187
|
+
const { valid, errors } = validateManifest(payload, req.headers, manifest);
|
|
188
|
+
if (!valid) {
|
|
189
|
+
const errorMessages = errors.map((e) => `[${e.code}] ${e.message}`).join("\n");
|
|
190
|
+
throw new Error(`Attestly Compliance Stream Check Failed:
|
|
191
|
+
${errorMessages}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (payload && manifest.piiScrubbing.level !== "off") {
|
|
195
|
+
const scrubbedPayload = scrubObject(payload, manifest.piiScrubbing);
|
|
196
|
+
const newReq = new Request(req.url, {
|
|
197
|
+
method: req.method,
|
|
198
|
+
headers: req.headers,
|
|
199
|
+
body: JSON.stringify(scrubbedPayload),
|
|
200
|
+
// @ts-ignore
|
|
201
|
+
duplex: "half"
|
|
202
|
+
});
|
|
203
|
+
return handler(newReq, ...args);
|
|
204
|
+
}
|
|
205
|
+
return handler(req, ...args);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
exports.withAttestlyCompliance = withAttestlyCompliance;
|
|
210
|
+
exports.withAttestlyStream = withAttestlyStream;
|
|
211
|
+
//# sourceMappingURL=index.js.map
|
|
212
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/errors.ts","../src/utils/validator.ts","../src/utils/scrubber.ts","../src/middleware/withCompliance.ts","../src/middleware/withStream.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { ValidationError } from './validator';\r\n\r\nexport function createComplianceError(errors: ValidationError[]): Response {\r\n // Use the code from the first error as the primary telemetry code, \r\n // or default to BLOCK_ENFORCED if none exists.\r\n const primaryCode = errors.length > 0 ? errors[0].code : 'BLOCK_ENFORCED';\r\n \r\n return new Response(\r\n JSON.stringify({\r\n error: 'AttestlyComplianceViolation',\r\n code: primaryCode,\r\n details: errors\r\n }),\r\n {\r\n status: 400,\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'X-Attestly-Blocked': 'true'\r\n }\r\n }\r\n );\r\n}\r\n","import { AiManifest } from '../types';\r\n\r\nexport interface ValidationError {\r\n code: string;\r\n message: string;\r\n}\r\n\r\nexport function validateManifest(\r\n payload: any,\r\n headers: Headers,\r\n manifest: AiManifest\r\n): { valid: boolean; errors: ValidationError[] } {\r\n const errors: ValidationError[] = [];\r\n\r\n // ---------------------------------------------------------\r\n // Existing Rules\r\n // ---------------------------------------------------------\r\n\r\n // Rule 1: Strict Model Locking\r\n if (payload && payload.model) {\r\n const model = String(payload.model);\r\n \r\n // Check against allowedModels\r\n if (!manifest.allowedModels.includes(model)) {\r\n errors.push({\r\n code: 'UNAUTHORIZED_MODEL',\r\n message: `Model '${model}' is not in the allowedModels list.`\r\n });\r\n }\r\n\r\n // Enforce version/date suffixes regex\r\n const versionRegex = /-[0-9]+(-[0-9]+)*$/;\r\n if (!versionRegex.test(model)) {\r\n errors.push({\r\n code: 'INVALID_MODEL_VERSION',\r\n message: `Model '${model}' must contain a strict version or date suffix (e.g., 'gpt-4-0613').`\r\n });\r\n }\r\n }\r\n\r\n // Rule 2: Traceability\r\n for (const requiredKey of manifest.requiredMetadata) {\r\n const hasInPayload = payload && payload[requiredKey] !== undefined;\r\n const hasInHeader = headers.has(requiredKey) || headers.has(`x-${requiredKey}`);\r\n\r\n if (!hasInPayload && !hasInHeader) {\r\n errors.push({\r\n code: 'MISSING_REQUIRED_METADATA',\r\n message: `Missing required metadata: '${requiredKey}'.`\r\n });\r\n }\r\n }\r\n\r\n // ---------------------------------------------------------\r\n // EU AI Act Rules\r\n // ---------------------------------------------------------\r\n\r\n // Rule A (The Kill Switch): Prohibited Practices\r\n if (manifest.prohibitedPractices) {\r\n const practices = Object.entries(manifest.prohibitedPractices);\r\n for (const [practice, isEnabled] of practices) {\r\n if (isEnabled === true) {\r\n errors.push({\r\n code: 'EU_PROHIBITED_PRACTICE',\r\n message: `Under Article 5 of the EU AI Act, the practice '${practice}' is prohibited. Execution blocked.`\r\n });\r\n }\r\n }\r\n }\r\n\r\n // Rule B (Law Enforcement Escalation): Auto-classify high-risk domains\r\n if (manifest.systemDomain === 'law-enforcement' || manifest.systemDomain === 'biometrics') {\r\n if (manifest.euRiskCategory === 'minimal' || manifest.euRiskCategory === 'limited') {\r\n errors.push({\r\n code: 'EU_RISK_MISCLASSIFICATION',\r\n message: `Under Annex III of the EU AI Act, ${manifest.systemDomain} systems are automatically high-risk. Cannot be classified as ${manifest.euRiskCategory}.`\r\n });\r\n }\r\n }\r\n\r\n // Rule C (Conditional Traceability): High-Risk requires strict logging\r\n if (manifest.euRiskCategory === 'high') {\r\n const hasSession = (payload && payload.sessionId !== undefined) || headers.has('sessionId') || headers.has('x-sessionId');\r\n const hasUserId = (payload && payload.userId !== undefined) || headers.has('userId') || headers.has('x-userId');\r\n const hasPurpose = (payload && payload.purpose !== undefined) || headers.has('purpose') || headers.has('x-purpose');\r\n\r\n // We can check if these are in requiredMetadata or explicitly check payload/headers\r\n // Assuming 'sessionId' and 'userId' or 'purpose' are representative traces.\r\n // The instructions say: \"If missing, push an error explaining that High-Risk systems require strict logging\"\r\n // Let's enforce that at least some core traces exist or check requiredMetadata.\r\n // Given the prompt: \"strictly verify that the payload or headers contain traceability metadata (e.g., sessionId, userId). If missing...\"\r\n if (!hasSession && !hasUserId && !hasPurpose) {\r\n errors.push({\r\n code: 'MISSING_HIGH_RISK_TRACEABILITY',\r\n message: 'Under the EU AI Act, High-Risk systems require strict logging traceability. Missing metadata like sessionId, userId, or purpose.'\r\n });\r\n }\r\n }\r\n\r\n return {\r\n valid: errors.length === 0,\r\n errors\r\n };\r\n}\r\n","import { AiManifest } from '../types';\r\n\r\nconst STANDARD_PII_REGEX = [\r\n // Emails\r\n /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g,\r\n // US SSN\r\n /\\b\\d{3}-\\d{2}-\\d{4}\\b/g,\r\n // 16-digit Credit Cards (simple)\r\n /\\b(?:\\d[ -]*?){13,16}\\b/g,\r\n // Generic API Keys (e.g., sk-..., AKIA...)\r\n /\\b(sk-[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16})\\b/g\r\n];\r\n\r\nexport function scrubPayload(\r\n text: string,\r\n config: AiManifest['piiScrubbing']\r\n): string {\r\n if (config.level === 'off') {\r\n return text;\r\n }\r\n\r\n if (config.level === 'strict') {\r\n throw new Error('NotImplemented: Strict PII scrubbing is reserved for the Attestly backend ML engine.');\r\n }\r\n\r\n let scrubbed = text;\r\n const redactString = config.redactString ?? '[REDACTED]';\r\n\r\n if (config.level === 'standard') {\r\n for (const regex of STANDARD_PII_REGEX) {\r\n scrubbed = scrubbed.replace(regex, redactString);\r\n }\r\n }\r\n\r\n if (config.customRegex && config.customRegex.length > 0) {\r\n for (const pattern of config.customRegex) {\r\n try {\r\n const regex = new RegExp(pattern, 'g');\r\n scrubbed = scrubbed.replace(regex, redactString);\r\n } catch (e) {\r\n // Ignore invalid regex to prevent crashing the edge function\r\n console.warn('Invalid custom regex pattern in PII scrubber:', pattern);\r\n }\r\n }\r\n }\r\n\r\n return scrubbed;\r\n}\r\n\r\nexport function scrubObject(\r\n obj: any,\r\n config: AiManifest['piiScrubbing']\r\n): any {\r\n if (config.level === 'off') return obj;\r\n if (!obj) return obj;\r\n\r\n if (typeof obj === 'string') {\r\n return scrubPayload(obj, config);\r\n }\r\n\r\n if (Array.isArray(obj)) {\r\n return obj.map(item => scrubObject(item, config));\r\n }\r\n\r\n if (typeof obj === 'object') {\r\n const scrubbedObj: any = {};\r\n for (const [key, value] of Object.entries(obj)) {\r\n scrubbedObj[key] = scrubObject(value, config);\r\n }\r\n return scrubbedObj;\r\n }\r\n\r\n return obj;\r\n}\r\n","import { AiManifest } from '../types';\r\nimport { createComplianceError } from '../utils/errors';\r\nimport { validateManifest } from '../utils/validator';\r\nimport { scrubObject } from '../utils/scrubber';\r\n\r\nexport type RequestHandler = (req: Request, ...args: any[]) => Promise<Response> | Response;\r\n\r\n/**\r\n * A higher-order function that wraps a standard Web API Request handler\r\n * to enforce AI compliance according to the provided manifest.\r\n */\r\nexport function withAttestlyCompliance(\r\n handler: RequestHandler,\r\n manifest: AiManifest\r\n): RequestHandler {\r\n return async (req: Request, ...args: any[]) => {\r\n // Clone the request since we might need to read the body and pass the request down\r\n // But Request bodies can only be read once.\r\n // To be perfectly framework agnostic and non-intrusive, we only check JSON payloads.\r\n let clonedReq = req.clone();\r\n let payload = null;\r\n\r\n try {\r\n if (clonedReq.headers.get('content-type')?.includes('application/json')) {\r\n payload = await clonedReq.json();\r\n }\r\n } catch (e) {\r\n // Ignore if body is not readable/json\r\n }\r\n\r\n if (process.env.NODE_ENV === 'development') {\r\n const { valid, errors } = validateManifest(payload, req.headers, manifest);\r\n\r\n if (!valid) {\r\n return createComplianceError(errors);\r\n }\r\n }\r\n\r\n // Scrub payload if needed before passing it down.\r\n // To do this completely transparently, we'd have to construct a new Request.\r\n // However, the instructions say \"run scrubPayload on the prompt/messages array to ensure data is clean before it hits the underlying AI client.\"\r\n // In a middleware, modifying the `Request` object requires instantiating a new Request.\r\n if (payload && manifest.piiScrubbing.level !== 'off') {\r\n const scrubbedPayload = scrubObject(payload, manifest.piiScrubbing);\r\n const newReq = new Request(req.url, {\r\n method: req.method,\r\n headers: req.headers,\r\n body: JSON.stringify(scrubbedPayload),\r\n // @ts-ignore - some environments require duplex for streaming requests\r\n duplex: 'half'\r\n });\r\n return handler(newReq, ...args);\r\n }\r\n\r\n // Pass through to the original handler if no scrubbing was needed\r\n return handler(req, ...args);\r\n };\r\n}\r\n","import { AiManifest } from '../types';\r\nimport { validateManifest } from '../utils/validator';\r\nimport { scrubObject } from '../utils/scrubber';\r\n\r\nexport type StreamHandler = (req: Request, ...args: any[]) => Promise<Response> | Response;\r\n\r\n/**\r\n * A specialized wrapper for the Vercel AI SDK streams.\r\n * Performs a synchronous compliance check upfront and returns the native stream untouched.\r\n */\r\nexport function withAttestlyStream(\r\n handler: StreamHandler,\r\n manifest: AiManifest\r\n): StreamHandler {\r\n return async (req: Request, ...args: any[]) => {\r\n let clonedReq = req.clone();\r\n let payload = null;\r\n\r\n try {\r\n if (clonedReq.headers.get('content-type')?.includes('application/json')) {\r\n payload = await clonedReq.json();\r\n }\r\n } catch (e) {\r\n // Ignore if body is not readable/json\r\n }\r\n\r\n if (process.env.NODE_ENV === 'development') {\r\n const { valid, errors } = validateManifest(payload, req.headers, manifest);\r\n\r\n if (!valid) {\r\n const errorMessages = errors.map(e => `[${e.code}] ${e.message}`).join('\\n');\r\n throw new Error(`Attestly Compliance Stream Check Failed: \\n${errorMessages}`);\r\n }\r\n }\r\n\r\n if (payload && manifest.piiScrubbing.level !== 'off') {\r\n const scrubbedPayload = scrubObject(payload, manifest.piiScrubbing);\r\n const newReq = new Request(req.url, {\r\n method: req.method,\r\n headers: req.headers,\r\n body: JSON.stringify(scrubbedPayload),\r\n // @ts-ignore\r\n duplex: 'half'\r\n });\r\n return handler(newReq, ...args);\r\n }\r\n\r\n // Pass through the DataStreamResponse untouched\r\n return handler(req, ...args);\r\n };\r\n}\r\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// src/utils/errors.ts
|
|
2
|
+
function createComplianceError(errors) {
|
|
3
|
+
const primaryCode = errors.length > 0 ? errors[0].code : "BLOCK_ENFORCED";
|
|
4
|
+
return new Response(
|
|
5
|
+
JSON.stringify({
|
|
6
|
+
error: "AttestlyComplianceViolation",
|
|
7
|
+
code: primaryCode,
|
|
8
|
+
details: errors
|
|
9
|
+
}),
|
|
10
|
+
{
|
|
11
|
+
status: 400,
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
"X-Attestly-Blocked": "true"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// src/utils/validator.ts
|
|
21
|
+
function validateManifest(payload, headers, manifest) {
|
|
22
|
+
const errors = [];
|
|
23
|
+
if (payload && payload.model) {
|
|
24
|
+
const model = String(payload.model);
|
|
25
|
+
if (!manifest.allowedModels.includes(model)) {
|
|
26
|
+
errors.push({
|
|
27
|
+
code: "UNAUTHORIZED_MODEL",
|
|
28
|
+
message: `Model '${model}' is not in the allowedModels list.`
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const versionRegex = /-[0-9]+(-[0-9]+)*$/;
|
|
32
|
+
if (!versionRegex.test(model)) {
|
|
33
|
+
errors.push({
|
|
34
|
+
code: "INVALID_MODEL_VERSION",
|
|
35
|
+
message: `Model '${model}' must contain a strict version or date suffix (e.g., 'gpt-4-0613').`
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const requiredKey of manifest.requiredMetadata) {
|
|
40
|
+
const hasInPayload = payload && payload[requiredKey] !== void 0;
|
|
41
|
+
const hasInHeader = headers.has(requiredKey) || headers.has(`x-${requiredKey}`);
|
|
42
|
+
if (!hasInPayload && !hasInHeader) {
|
|
43
|
+
errors.push({
|
|
44
|
+
code: "MISSING_REQUIRED_METADATA",
|
|
45
|
+
message: `Missing required metadata: '${requiredKey}'.`
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (manifest.prohibitedPractices) {
|
|
50
|
+
const practices = Object.entries(manifest.prohibitedPractices);
|
|
51
|
+
for (const [practice, isEnabled] of practices) {
|
|
52
|
+
if (isEnabled === true) {
|
|
53
|
+
errors.push({
|
|
54
|
+
code: "EU_PROHIBITED_PRACTICE",
|
|
55
|
+
message: `Under Article 5 of the EU AI Act, the practice '${practice}' is prohibited. Execution blocked.`
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (manifest.systemDomain === "law-enforcement" || manifest.systemDomain === "biometrics") {
|
|
61
|
+
if (manifest.euRiskCategory === "minimal" || manifest.euRiskCategory === "limited") {
|
|
62
|
+
errors.push({
|
|
63
|
+
code: "EU_RISK_MISCLASSIFICATION",
|
|
64
|
+
message: `Under Annex III of the EU AI Act, ${manifest.systemDomain} systems are automatically high-risk. Cannot be classified as ${manifest.euRiskCategory}.`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (manifest.euRiskCategory === "high") {
|
|
69
|
+
const hasSession = payload && payload.sessionId !== void 0 || headers.has("sessionId") || headers.has("x-sessionId");
|
|
70
|
+
const hasUserId = payload && payload.userId !== void 0 || headers.has("userId") || headers.has("x-userId");
|
|
71
|
+
const hasPurpose = payload && payload.purpose !== void 0 || headers.has("purpose") || headers.has("x-purpose");
|
|
72
|
+
if (!hasSession && !hasUserId && !hasPurpose) {
|
|
73
|
+
errors.push({
|
|
74
|
+
code: "MISSING_HIGH_RISK_TRACEABILITY",
|
|
75
|
+
message: "Under the EU AI Act, High-Risk systems require strict logging traceability. Missing metadata like sessionId, userId, or purpose."
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
valid: errors.length === 0,
|
|
81
|
+
errors
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/utils/scrubber.ts
|
|
86
|
+
var STANDARD_PII_REGEX = [
|
|
87
|
+
// Emails
|
|
88
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
89
|
+
// US SSN
|
|
90
|
+
/\b\d{3}-\d{2}-\d{4}\b/g,
|
|
91
|
+
// 16-digit Credit Cards (simple)
|
|
92
|
+
/\b(?:\d[ -]*?){13,16}\b/g,
|
|
93
|
+
// Generic API Keys (e.g., sk-..., AKIA...)
|
|
94
|
+
/\b(sk-[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16})\b/g
|
|
95
|
+
];
|
|
96
|
+
function scrubPayload(text, config) {
|
|
97
|
+
if (config.level === "off") {
|
|
98
|
+
return text;
|
|
99
|
+
}
|
|
100
|
+
if (config.level === "strict") {
|
|
101
|
+
throw new Error("NotImplemented: Strict PII scrubbing is reserved for the Attestly backend ML engine.");
|
|
102
|
+
}
|
|
103
|
+
let scrubbed = text;
|
|
104
|
+
const redactString = config.redactString ?? "[REDACTED]";
|
|
105
|
+
if (config.level === "standard") {
|
|
106
|
+
for (const regex of STANDARD_PII_REGEX) {
|
|
107
|
+
scrubbed = scrubbed.replace(regex, redactString);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (config.customRegex && config.customRegex.length > 0) {
|
|
111
|
+
for (const pattern of config.customRegex) {
|
|
112
|
+
try {
|
|
113
|
+
const regex = new RegExp(pattern, "g");
|
|
114
|
+
scrubbed = scrubbed.replace(regex, redactString);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.warn("Invalid custom regex pattern in PII scrubber:", pattern);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return scrubbed;
|
|
121
|
+
}
|
|
122
|
+
function scrubObject(obj, config) {
|
|
123
|
+
if (config.level === "off") return obj;
|
|
124
|
+
if (!obj) return obj;
|
|
125
|
+
if (typeof obj === "string") {
|
|
126
|
+
return scrubPayload(obj, config);
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(obj)) {
|
|
129
|
+
return obj.map((item) => scrubObject(item, config));
|
|
130
|
+
}
|
|
131
|
+
if (typeof obj === "object") {
|
|
132
|
+
const scrubbedObj = {};
|
|
133
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
134
|
+
scrubbedObj[key] = scrubObject(value, config);
|
|
135
|
+
}
|
|
136
|
+
return scrubbedObj;
|
|
137
|
+
}
|
|
138
|
+
return obj;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/middleware/withCompliance.ts
|
|
142
|
+
function withAttestlyCompliance(handler, manifest) {
|
|
143
|
+
return async (req, ...args) => {
|
|
144
|
+
let clonedReq = req.clone();
|
|
145
|
+
let payload = null;
|
|
146
|
+
try {
|
|
147
|
+
if (clonedReq.headers.get("content-type")?.includes("application/json")) {
|
|
148
|
+
payload = await clonedReq.json();
|
|
149
|
+
}
|
|
150
|
+
} catch (e) {
|
|
151
|
+
}
|
|
152
|
+
if (process.env.NODE_ENV === "development") {
|
|
153
|
+
const { valid, errors } = validateManifest(payload, req.headers, manifest);
|
|
154
|
+
if (!valid) {
|
|
155
|
+
return createComplianceError(errors);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (payload && manifest.piiScrubbing.level !== "off") {
|
|
159
|
+
const scrubbedPayload = scrubObject(payload, manifest.piiScrubbing);
|
|
160
|
+
const newReq = new Request(req.url, {
|
|
161
|
+
method: req.method,
|
|
162
|
+
headers: req.headers,
|
|
163
|
+
body: JSON.stringify(scrubbedPayload),
|
|
164
|
+
// @ts-ignore - some environments require duplex for streaming requests
|
|
165
|
+
duplex: "half"
|
|
166
|
+
});
|
|
167
|
+
return handler(newReq, ...args);
|
|
168
|
+
}
|
|
169
|
+
return handler(req, ...args);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/middleware/withStream.ts
|
|
174
|
+
function withAttestlyStream(handler, manifest) {
|
|
175
|
+
return async (req, ...args) => {
|
|
176
|
+
let clonedReq = req.clone();
|
|
177
|
+
let payload = null;
|
|
178
|
+
try {
|
|
179
|
+
if (clonedReq.headers.get("content-type")?.includes("application/json")) {
|
|
180
|
+
payload = await clonedReq.json();
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
}
|
|
184
|
+
if (process.env.NODE_ENV === "development") {
|
|
185
|
+
const { valid, errors } = validateManifest(payload, req.headers, manifest);
|
|
186
|
+
if (!valid) {
|
|
187
|
+
const errorMessages = errors.map((e) => `[${e.code}] ${e.message}`).join("\n");
|
|
188
|
+
throw new Error(`Attestly Compliance Stream Check Failed:
|
|
189
|
+
${errorMessages}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (payload && manifest.piiScrubbing.level !== "off") {
|
|
193
|
+
const scrubbedPayload = scrubObject(payload, manifest.piiScrubbing);
|
|
194
|
+
const newReq = new Request(req.url, {
|
|
195
|
+
method: req.method,
|
|
196
|
+
headers: req.headers,
|
|
197
|
+
body: JSON.stringify(scrubbedPayload),
|
|
198
|
+
// @ts-ignore
|
|
199
|
+
duplex: "half"
|
|
200
|
+
});
|
|
201
|
+
return handler(newReq, ...args);
|
|
202
|
+
}
|
|
203
|
+
return handler(req, ...args);
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export { withAttestlyCompliance, withAttestlyStream };
|
|
208
|
+
//# sourceMappingURL=index.mjs.map
|
|
209
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/errors.ts","../src/utils/validator.ts","../src/utils/scrubber.ts","../src/middleware/withCompliance.ts","../src/middleware/withStream.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { ValidationError } from './validator';\r\n\r\nexport function createComplianceError(errors: ValidationError[]): Response {\r\n // Use the code from the first error as the primary telemetry code, \r\n // or default to BLOCK_ENFORCED if none exists.\r\n const primaryCode = errors.length > 0 ? errors[0].code : 'BLOCK_ENFORCED';\r\n \r\n return new Response(\r\n JSON.stringify({\r\n error: 'AttestlyComplianceViolation',\r\n code: primaryCode,\r\n details: errors\r\n }),\r\n {\r\n status: 400,\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'X-Attestly-Blocked': 'true'\r\n }\r\n }\r\n );\r\n}\r\n","import { AiManifest } from '../types';\r\n\r\nexport interface ValidationError {\r\n code: string;\r\n message: string;\r\n}\r\n\r\nexport function validateManifest(\r\n payload: any,\r\n headers: Headers,\r\n manifest: AiManifest\r\n): { valid: boolean; errors: ValidationError[] } {\r\n const errors: ValidationError[] = [];\r\n\r\n // ---------------------------------------------------------\r\n // Existing Rules\r\n // ---------------------------------------------------------\r\n\r\n // Rule 1: Strict Model Locking\r\n if (payload && payload.model) {\r\n const model = String(payload.model);\r\n \r\n // Check against allowedModels\r\n if (!manifest.allowedModels.includes(model)) {\r\n errors.push({\r\n code: 'UNAUTHORIZED_MODEL',\r\n message: `Model '${model}' is not in the allowedModels list.`\r\n });\r\n }\r\n\r\n // Enforce version/date suffixes regex\r\n const versionRegex = /-[0-9]+(-[0-9]+)*$/;\r\n if (!versionRegex.test(model)) {\r\n errors.push({\r\n code: 'INVALID_MODEL_VERSION',\r\n message: `Model '${model}' must contain a strict version or date suffix (e.g., 'gpt-4-0613').`\r\n });\r\n }\r\n }\r\n\r\n // Rule 2: Traceability\r\n for (const requiredKey of manifest.requiredMetadata) {\r\n const hasInPayload = payload && payload[requiredKey] !== undefined;\r\n const hasInHeader = headers.has(requiredKey) || headers.has(`x-${requiredKey}`);\r\n\r\n if (!hasInPayload && !hasInHeader) {\r\n errors.push({\r\n code: 'MISSING_REQUIRED_METADATA',\r\n message: `Missing required metadata: '${requiredKey}'.`\r\n });\r\n }\r\n }\r\n\r\n // ---------------------------------------------------------\r\n // EU AI Act Rules\r\n // ---------------------------------------------------------\r\n\r\n // Rule A (The Kill Switch): Prohibited Practices\r\n if (manifest.prohibitedPractices) {\r\n const practices = Object.entries(manifest.prohibitedPractices);\r\n for (const [practice, isEnabled] of practices) {\r\n if (isEnabled === true) {\r\n errors.push({\r\n code: 'EU_PROHIBITED_PRACTICE',\r\n message: `Under Article 5 of the EU AI Act, the practice '${practice}' is prohibited. Execution blocked.`\r\n });\r\n }\r\n }\r\n }\r\n\r\n // Rule B (Law Enforcement Escalation): Auto-classify high-risk domains\r\n if (manifest.systemDomain === 'law-enforcement' || manifest.systemDomain === 'biometrics') {\r\n if (manifest.euRiskCategory === 'minimal' || manifest.euRiskCategory === 'limited') {\r\n errors.push({\r\n code: 'EU_RISK_MISCLASSIFICATION',\r\n message: `Under Annex III of the EU AI Act, ${manifest.systemDomain} systems are automatically high-risk. Cannot be classified as ${manifest.euRiskCategory}.`\r\n });\r\n }\r\n }\r\n\r\n // Rule C (Conditional Traceability): High-Risk requires strict logging\r\n if (manifest.euRiskCategory === 'high') {\r\n const hasSession = (payload && payload.sessionId !== undefined) || headers.has('sessionId') || headers.has('x-sessionId');\r\n const hasUserId = (payload && payload.userId !== undefined) || headers.has('userId') || headers.has('x-userId');\r\n const hasPurpose = (payload && payload.purpose !== undefined) || headers.has('purpose') || headers.has('x-purpose');\r\n\r\n // We can check if these are in requiredMetadata or explicitly check payload/headers\r\n // Assuming 'sessionId' and 'userId' or 'purpose' are representative traces.\r\n // The instructions say: \"If missing, push an error explaining that High-Risk systems require strict logging\"\r\n // Let's enforce that at least some core traces exist or check requiredMetadata.\r\n // Given the prompt: \"strictly verify that the payload or headers contain traceability metadata (e.g., sessionId, userId). If missing...\"\r\n if (!hasSession && !hasUserId && !hasPurpose) {\r\n errors.push({\r\n code: 'MISSING_HIGH_RISK_TRACEABILITY',\r\n message: 'Under the EU AI Act, High-Risk systems require strict logging traceability. Missing metadata like sessionId, userId, or purpose.'\r\n });\r\n }\r\n }\r\n\r\n return {\r\n valid: errors.length === 0,\r\n errors\r\n };\r\n}\r\n","import { AiManifest } from '../types';\r\n\r\nconst STANDARD_PII_REGEX = [\r\n // Emails\r\n /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}/g,\r\n // US SSN\r\n /\\b\\d{3}-\\d{2}-\\d{4}\\b/g,\r\n // 16-digit Credit Cards (simple)\r\n /\\b(?:\\d[ -]*?){13,16}\\b/g,\r\n // Generic API Keys (e.g., sk-..., AKIA...)\r\n /\\b(sk-[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16})\\b/g\r\n];\r\n\r\nexport function scrubPayload(\r\n text: string,\r\n config: AiManifest['piiScrubbing']\r\n): string {\r\n if (config.level === 'off') {\r\n return text;\r\n }\r\n\r\n if (config.level === 'strict') {\r\n throw new Error('NotImplemented: Strict PII scrubbing is reserved for the Attestly backend ML engine.');\r\n }\r\n\r\n let scrubbed = text;\r\n const redactString = config.redactString ?? '[REDACTED]';\r\n\r\n if (config.level === 'standard') {\r\n for (const regex of STANDARD_PII_REGEX) {\r\n scrubbed = scrubbed.replace(regex, redactString);\r\n }\r\n }\r\n\r\n if (config.customRegex && config.customRegex.length > 0) {\r\n for (const pattern of config.customRegex) {\r\n try {\r\n const regex = new RegExp(pattern, 'g');\r\n scrubbed = scrubbed.replace(regex, redactString);\r\n } catch (e) {\r\n // Ignore invalid regex to prevent crashing the edge function\r\n console.warn('Invalid custom regex pattern in PII scrubber:', pattern);\r\n }\r\n }\r\n }\r\n\r\n return scrubbed;\r\n}\r\n\r\nexport function scrubObject(\r\n obj: any,\r\n config: AiManifest['piiScrubbing']\r\n): any {\r\n if (config.level === 'off') return obj;\r\n if (!obj) return obj;\r\n\r\n if (typeof obj === 'string') {\r\n return scrubPayload(obj, config);\r\n }\r\n\r\n if (Array.isArray(obj)) {\r\n return obj.map(item => scrubObject(item, config));\r\n }\r\n\r\n if (typeof obj === 'object') {\r\n const scrubbedObj: any = {};\r\n for (const [key, value] of Object.entries(obj)) {\r\n scrubbedObj[key] = scrubObject(value, config);\r\n }\r\n return scrubbedObj;\r\n }\r\n\r\n return obj;\r\n}\r\n","import { AiManifest } from '../types';\r\nimport { createComplianceError } from '../utils/errors';\r\nimport { validateManifest } from '../utils/validator';\r\nimport { scrubObject } from '../utils/scrubber';\r\n\r\nexport type RequestHandler = (req: Request, ...args: any[]) => Promise<Response> | Response;\r\n\r\n/**\r\n * A higher-order function that wraps a standard Web API Request handler\r\n * to enforce AI compliance according to the provided manifest.\r\n */\r\nexport function withAttestlyCompliance(\r\n handler: RequestHandler,\r\n manifest: AiManifest\r\n): RequestHandler {\r\n return async (req: Request, ...args: any[]) => {\r\n // Clone the request since we might need to read the body and pass the request down\r\n // But Request bodies can only be read once.\r\n // To be perfectly framework agnostic and non-intrusive, we only check JSON payloads.\r\n let clonedReq = req.clone();\r\n let payload = null;\r\n\r\n try {\r\n if (clonedReq.headers.get('content-type')?.includes('application/json')) {\r\n payload = await clonedReq.json();\r\n }\r\n } catch (e) {\r\n // Ignore if body is not readable/json\r\n }\r\n\r\n if (process.env.NODE_ENV === 'development') {\r\n const { valid, errors } = validateManifest(payload, req.headers, manifest);\r\n\r\n if (!valid) {\r\n return createComplianceError(errors);\r\n }\r\n }\r\n\r\n // Scrub payload if needed before passing it down.\r\n // To do this completely transparently, we'd have to construct a new Request.\r\n // However, the instructions say \"run scrubPayload on the prompt/messages array to ensure data is clean before it hits the underlying AI client.\"\r\n // In a middleware, modifying the `Request` object requires instantiating a new Request.\r\n if (payload && manifest.piiScrubbing.level !== 'off') {\r\n const scrubbedPayload = scrubObject(payload, manifest.piiScrubbing);\r\n const newReq = new Request(req.url, {\r\n method: req.method,\r\n headers: req.headers,\r\n body: JSON.stringify(scrubbedPayload),\r\n // @ts-ignore - some environments require duplex for streaming requests\r\n duplex: 'half'\r\n });\r\n return handler(newReq, ...args);\r\n }\r\n\r\n // Pass through to the original handler if no scrubbing was needed\r\n return handler(req, ...args);\r\n };\r\n}\r\n","import { AiManifest } from '../types';\r\nimport { validateManifest } from '../utils/validator';\r\nimport { scrubObject } from '../utils/scrubber';\r\n\r\nexport type StreamHandler = (req: Request, ...args: any[]) => Promise<Response> | Response;\r\n\r\n/**\r\n * A specialized wrapper for the Vercel AI SDK streams.\r\n * Performs a synchronous compliance check upfront and returns the native stream untouched.\r\n */\r\nexport function withAttestlyStream(\r\n handler: StreamHandler,\r\n manifest: AiManifest\r\n): StreamHandler {\r\n return async (req: Request, ...args: any[]) => {\r\n let clonedReq = req.clone();\r\n let payload = null;\r\n\r\n try {\r\n if (clonedReq.headers.get('content-type')?.includes('application/json')) {\r\n payload = await clonedReq.json();\r\n }\r\n } catch (e) {\r\n // Ignore if body is not readable/json\r\n }\r\n\r\n if (process.env.NODE_ENV === 'development') {\r\n const { valid, errors } = validateManifest(payload, req.headers, manifest);\r\n\r\n if (!valid) {\r\n const errorMessages = errors.map(e => `[${e.code}] ${e.message}`).join('\\n');\r\n throw new Error(`Attestly Compliance Stream Check Failed: \\n${errorMessages}`);\r\n }\r\n }\r\n\r\n if (payload && manifest.piiScrubbing.level !== 'off') {\r\n const scrubbedPayload = scrubObject(payload, manifest.piiScrubbing);\r\n const newReq = new Request(req.url, {\r\n method: req.method,\r\n headers: req.headers,\r\n body: JSON.stringify(scrubbedPayload),\r\n // @ts-ignore\r\n duplex: 'half'\r\n });\r\n return handler(newReq, ...args);\r\n }\r\n\r\n // Pass through the DataStreamResponse untouched\r\n return handler(req, ...args);\r\n };\r\n}\r\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@attestly/compliance-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-latency, Web Standard-based SDK that enforces AI compliance",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"dev": "tsup --watch"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"tsup": "^8.0.2",
|
|
29
|
+
"typescript": "^5.4.5"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
}
|
|
34
|
+
}
|