shieldstack-ts 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/.dockerignore +9 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +61 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +35 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +27 -0
- package/.github/workflows/ci.yml +69 -0
- package/CHANGELOG.md +59 -0
- package/CONTRIBUTING.md +83 -0
- package/Dockerfile +45 -0
- package/LICENSE +21 -0
- package/README.md +277 -0
- package/SECURITY.md +42 -0
- package/demo.ts +41 -0
- package/docker-compose.yml +49 -0
- package/examples/demo/AGENTS.md +5 -0
- package/examples/demo/CLAUDE.md +1 -0
- package/examples/demo/README.md +36 -0
- package/examples/demo/eslint.config.mjs +18 -0
- package/examples/demo/next.config.ts +8 -0
- package/examples/demo/package-lock.json +6041 -0
- package/examples/demo/package.json +25 -0
- package/examples/demo/public/file.svg +1 -0
- package/examples/demo/public/globe.svg +1 -0
- package/examples/demo/public/next.svg +1 -0
- package/examples/demo/public/vercel.svg +1 -0
- package/examples/demo/public/window.svg +1 -0
- package/examples/demo/src/app/api/chat/route.ts +38 -0
- package/examples/demo/src/app/favicon.ico +0 -0
- package/examples/demo/src/app/globals.css +75 -0
- package/examples/demo/src/app/layout.tsx +30 -0
- package/examples/demo/src/app/page.module.css +142 -0
- package/examples/demo/src/app/page.tsx +162 -0
- package/examples/demo/tsconfig.json +34 -0
- package/package.json +44 -0
- package/src/adapters/express.ts +28 -0
- package/src/adapters/hono.ts +22 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/next.ts +26 -0
- package/src/budgeting/InMemoryStore.ts +26 -0
- package/src/budgeting/RedisStore.ts +41 -0
- package/src/budgeting/index.ts +5 -0
- package/src/budgeting/tokenLimiter.ts +60 -0
- package/src/budgeting/types.ts +10 -0
- package/src/core/ShieldStack.ts +119 -0
- package/src/index.ts +7 -0
- package/src/observability/index.ts +2 -0
- package/src/observability/logger.ts +62 -0
- package/src/sanitizers/index.ts +4 -0
- package/src/sanitizers/injection.ts +49 -0
- package/src/sanitizers/pii.ts +97 -0
- package/src/sanitizers/secrets.ts +49 -0
- package/src/streams/StreamSanitizer.ts +46 -0
- package/src/streams/index.ts +2 -0
- package/src/validation/index.ts +2 -0
- package/src/validation/zodValidator.ts +46 -0
- package/tests/injection.test.ts +23 -0
- package/tests/pii.test.ts +21 -0
- package/tests/redis.integration.ts +65 -0
- package/tests/redisStore.test.ts +107 -0
- package/tests/tokenLimiter.test.ts +27 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "demo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "eslint"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"next": "16.2.2",
|
|
13
|
+
"react": "19.2.4",
|
|
14
|
+
"react-dom": "19.2.4",
|
|
15
|
+
"shieldstack-ts": "file:../../"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^20",
|
|
19
|
+
"@types/react": "^19",
|
|
20
|
+
"@types/react-dom": "^19",
|
|
21
|
+
"eslint": "^9",
|
|
22
|
+
"eslint-config-next": "16.2.2",
|
|
23
|
+
"typescript": "^5"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ShieldStack, withShield } from 'shieldstack-ts';
|
|
2
|
+
|
|
3
|
+
const shield = new ShieldStack({
|
|
4
|
+
pii: { policy: 'redact', emails: true, creditCards: true, phoneNumbers: true },
|
|
5
|
+
injectionDetection: { threshold: 0.8 },
|
|
6
|
+
tokenLimiter: { maxTokens: 1000, windowMs: 60000 }
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const mockLlmResponse = [
|
|
10
|
+
"Sure! Here is the data you requested from our internal system:",
|
|
11
|
+
" The customer email is admin@enterprise.com",
|
|
12
|
+
" and their phone number is 1-800-555-0199.",
|
|
13
|
+
" Make sure you don't leak the AWS key: AKIAIOSFODNN7EXAMPLE",
|
|
14
|
+
" which has full production access. Is there anything else you need?"
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function createMockStream(): ReadableStream<Uint8Array> {
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
return new ReadableStream({
|
|
20
|
+
async start(controller) {
|
|
21
|
+
for (const chunk of mockLlmResponse) {
|
|
22
|
+
controller.enqueue(encoder.encode(chunk));
|
|
23
|
+
await new Promise(r => setTimeout(r, 200));
|
|
24
|
+
}
|
|
25
|
+
controller.close();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function chatHandler(req: Request) {
|
|
31
|
+
const stream = createMockStream();
|
|
32
|
+
|
|
33
|
+
return new Response(stream, {
|
|
34
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const POST = withShield(shield, chatHandler);
|
|
Binary file
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg-color: #f7f9fc;
|
|
3
|
+
--surface-color: #ffffff;
|
|
4
|
+
--text-active: #1f2937;
|
|
5
|
+
--text-muted: #6b7280;
|
|
6
|
+
--accent-color: #3b82f6; /* Trust blue */
|
|
7
|
+
--border-color: #e5e7eb;
|
|
8
|
+
--shadow-sm: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.05);
|
|
9
|
+
--shadow-lg: 0 20px 25px -5px rgb(0 0 0 / 0.05), 0 8px 10px -6px rgb(0 0 0 / 0.05);
|
|
10
|
+
--glass-bg: rgba(255, 255, 255, 0.85);
|
|
11
|
+
--glass-border: rgba(255, 255, 255, 0.4);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
html, body {
|
|
15
|
+
width: 100vw;
|
|
16
|
+
height: 100vh;
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
font-family: var(--font-geist-sans), 'Inter', sans-serif;
|
|
20
|
+
background-color: var(--bg-color);
|
|
21
|
+
color: var(--text-active);
|
|
22
|
+
-webkit-font-smoothing: antialiased;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
* {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.premium-container {
|
|
31
|
+
display: flex;
|
|
32
|
+
height: 100vh;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
background: radial-gradient(circle at top, #ffffff, #f7f9fc);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.glass-panel {
|
|
39
|
+
background: var(--glass-bg);
|
|
40
|
+
backdrop-filter: blur(12px);
|
|
41
|
+
-webkit-backdrop-filter: blur(12px);
|
|
42
|
+
border: 1px solid var(--glass-border);
|
|
43
|
+
box-shadow: var(--shadow-lg);
|
|
44
|
+
border-radius: 24px;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
transition: transform 0.2s ease-out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.button-premium {
|
|
52
|
+
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
|
53
|
+
color: white;
|
|
54
|
+
border: none;
|
|
55
|
+
padding: 12px 24px;
|
|
56
|
+
border-radius: 12px;
|
|
57
|
+
font-weight: 500;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
box-shadow: 0 4px 14px 0 rgba(59, 130, 246, 0.39);
|
|
60
|
+
transition: all 0.2s ease;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.button-premium:hover {
|
|
64
|
+
transform: translateY(-1px);
|
|
65
|
+
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.45);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.message-box {
|
|
69
|
+
animation: fadeIn 0.3s ease-out forwards;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@keyframes fadeIn {
|
|
73
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
74
|
+
to { opacity: 1; transform: translateY(0); }
|
|
75
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
const geistSans = Geist({
|
|
6
|
+
variable: "--font-geist-sans",
|
|
7
|
+
subsets: ["latin"],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const geistMono = Geist_Mono({
|
|
11
|
+
variable: "--font-geist-mono",
|
|
12
|
+
subsets: ["latin"],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const metadata: Metadata = {
|
|
16
|
+
title: "ShieldStack Trust Terminal",
|
|
17
|
+
description: "LLM security middleware demo",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function RootLayout({
|
|
21
|
+
children,
|
|
22
|
+
}: Readonly<{
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}>) {
|
|
25
|
+
return (
|
|
26
|
+
<html lang="en" className={`${geistSans.variable} ${geistMono.variable}`}>
|
|
27
|
+
<body>{children}</body>
|
|
28
|
+
</html>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
.page {
|
|
2
|
+
--background: #fafafa;
|
|
3
|
+
--foreground: #fff;
|
|
4
|
+
|
|
5
|
+
--text-primary: #000;
|
|
6
|
+
--text-secondary: #666;
|
|
7
|
+
|
|
8
|
+
--button-primary-hover: #383838;
|
|
9
|
+
--button-secondary-hover: #f2f2f2;
|
|
10
|
+
--button-secondary-border: #ebebeb;
|
|
11
|
+
|
|
12
|
+
display: flex;
|
|
13
|
+
flex: 1;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
font-family: var(--font-geist-sans);
|
|
18
|
+
background-color: var(--background);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.main {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex: 1;
|
|
24
|
+
width: 100%;
|
|
25
|
+
max-width: 800px;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
align-items: flex-start;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
background-color: var(--foreground);
|
|
30
|
+
padding: 120px 60px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.intro {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: column;
|
|
36
|
+
align-items: flex-start;
|
|
37
|
+
text-align: left;
|
|
38
|
+
gap: 24px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.intro h1 {
|
|
42
|
+
max-width: 320px;
|
|
43
|
+
font-size: 40px;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
line-height: 48px;
|
|
46
|
+
letter-spacing: -2.4px;
|
|
47
|
+
text-wrap: balance;
|
|
48
|
+
color: var(--text-primary);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.intro p {
|
|
52
|
+
max-width: 440px;
|
|
53
|
+
font-size: 18px;
|
|
54
|
+
line-height: 32px;
|
|
55
|
+
text-wrap: balance;
|
|
56
|
+
color: var(--text-secondary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.intro a {
|
|
60
|
+
font-weight: 500;
|
|
61
|
+
color: var(--text-primary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.ctas {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: row;
|
|
67
|
+
width: 100%;
|
|
68
|
+
max-width: 440px;
|
|
69
|
+
gap: 16px;
|
|
70
|
+
font-size: 14px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.ctas a {
|
|
74
|
+
display: flex;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
align-items: center;
|
|
77
|
+
height: 40px;
|
|
78
|
+
padding: 0 16px;
|
|
79
|
+
border-radius: 128px;
|
|
80
|
+
border: 1px solid transparent;
|
|
81
|
+
transition: 0.2s;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
width: fit-content;
|
|
84
|
+
font-weight: 500;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
a.primary {
|
|
88
|
+
background: var(--text-primary);
|
|
89
|
+
color: var(--background);
|
|
90
|
+
gap: 8px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
a.secondary {
|
|
94
|
+
border-color: var(--button-secondary-border);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Enable hover only on non-touch devices */
|
|
98
|
+
@media (hover: hover) and (pointer: fine) {
|
|
99
|
+
a.primary:hover {
|
|
100
|
+
background: var(--button-primary-hover);
|
|
101
|
+
border-color: transparent;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
a.secondary:hover {
|
|
105
|
+
background: var(--button-secondary-hover);
|
|
106
|
+
border-color: transparent;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (max-width: 600px) {
|
|
111
|
+
.main {
|
|
112
|
+
padding: 48px 24px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.intro {
|
|
116
|
+
gap: 16px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.intro h1 {
|
|
120
|
+
font-size: 32px;
|
|
121
|
+
line-height: 40px;
|
|
122
|
+
letter-spacing: -1.92px;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@media (prefers-color-scheme: dark) {
|
|
127
|
+
.logo {
|
|
128
|
+
filter: invert();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.page {
|
|
132
|
+
--background: #000;
|
|
133
|
+
--foreground: #000;
|
|
134
|
+
|
|
135
|
+
--text-primary: #ededed;
|
|
136
|
+
--text-secondary: #999;
|
|
137
|
+
|
|
138
|
+
--button-primary-hover: #ccc;
|
|
139
|
+
--button-secondary-hover: #1a1a1a;
|
|
140
|
+
--button-secondary-border: #1a1a1a;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function Home() {
|
|
6
|
+
const [messages, setMessages] = useState<{role: 'user' | 'assistant', content: string}[]>([]);
|
|
7
|
+
const [input, setInput] = useState('');
|
|
8
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
9
|
+
const [errorStatus, setErrorStatus] = useState<string | null>(null);
|
|
10
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
11
|
+
|
|
12
|
+
const scrollToBottom = () => {
|
|
13
|
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
scrollToBottom();
|
|
18
|
+
}, [messages]);
|
|
19
|
+
|
|
20
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
if (!input.trim() || isLoading) return;
|
|
23
|
+
|
|
24
|
+
const userMessage = input;
|
|
25
|
+
setInput('');
|
|
26
|
+
setMessages(prev => [...prev, { role: 'user', content: userMessage }]);
|
|
27
|
+
setErrorStatus(null);
|
|
28
|
+
setIsLoading(true);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch('/api/chat', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ message: userMessage })
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
let errJson = { error: 'Unknown Error' };
|
|
39
|
+
try { errJson = await response.json(); } catch(e) {}
|
|
40
|
+
setErrorStatus(`Shield Blocked: ${errJson.error}`);
|
|
41
|
+
setIsLoading(false);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setMessages(prev => [...prev, { role: 'assistant', content: '' }]);
|
|
46
|
+
|
|
47
|
+
const reader = response.body?.getReader();
|
|
48
|
+
const decoder = new TextDecoder();
|
|
49
|
+
|
|
50
|
+
if (reader) {
|
|
51
|
+
let done = false;
|
|
52
|
+
while (!done) {
|
|
53
|
+
const { value, done: doneReading } = await reader.read();
|
|
54
|
+
done = doneReading;
|
|
55
|
+
if (value) {
|
|
56
|
+
const chunkValue = decoder.decode(value, { stream: true });
|
|
57
|
+
setMessages(prev => {
|
|
58
|
+
const newMsgs = [...prev];
|
|
59
|
+
const lastIndex = newMsgs.length - 1;
|
|
60
|
+
newMsgs[lastIndex] = {
|
|
61
|
+
...newMsgs[lastIndex],
|
|
62
|
+
content: newMsgs[lastIndex].content + chunkValue
|
|
63
|
+
};
|
|
64
|
+
return newMsgs;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
setErrorStatus('Connection failed. Please ensure the backend is running.');
|
|
71
|
+
} finally {
|
|
72
|
+
setIsLoading(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<main className="premium-container">
|
|
78
|
+
<div className="glass-panel" style={{ width: '100%', maxWidth: '800px', height: '80vh' }}>
|
|
79
|
+
|
|
80
|
+
<header style={{ padding: '24px', borderBottom: '1px solid var(--border-color)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
81
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
82
|
+
<div style={{ width: '32px', height: '32px', borderRadius: '8px', background: 'var(--accent-color)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white', fontWeight: 'bold' }}>S</div>
|
|
83
|
+
<div>
|
|
84
|
+
<h1 style={{ fontSize: '1.25rem', fontWeight: 600, margin: 0, color: 'var(--text-active)' }}>ShieldStack Trust Terminal</h1>
|
|
85
|
+
<p style={{ fontSize: '0.875rem', color: 'var(--text-muted)', margin: 0 }}>Secure Edge Execution</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<div style={{ fontSize: '0.75rem', padding: '4px 8px', borderRadius: '12px', background: 'rgba(59, 130, 246, 0.1)', color: 'var(--accent-color)', fontWeight: 600 }}>Active Shield</div>
|
|
89
|
+
</header>
|
|
90
|
+
|
|
91
|
+
<div style={{ flex: 1, overflowY: 'auto', padding: '24px', display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
|
92
|
+
{messages.length === 0 && (
|
|
93
|
+
<div style={{ margin: 'auto', textAlign: 'center', color: 'var(--text-muted)' }}>
|
|
94
|
+
<p>Type a prompt to test the protection.</p>
|
|
95
|
+
<p style={{ fontSize: '0.875rem', marginTop: '8px' }}>E.g. Try sending fake PII, or typing "Ignore previous instructions"</p>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{messages.map((msg, i) => (
|
|
100
|
+
<div key={i} className="message-box" style={{
|
|
101
|
+
alignSelf: msg.role === 'user' ? 'flex-end' : 'flex-start',
|
|
102
|
+
background: msg.role === 'user' ? 'var(--accent-color)' : '#ffffff',
|
|
103
|
+
color: msg.role === 'user' ? 'white' : 'var(--text-active)',
|
|
104
|
+
padding: '12px 16px',
|
|
105
|
+
borderRadius: '12px',
|
|
106
|
+
maxWidth: '80%',
|
|
107
|
+
boxShadow: 'var(--shadow-sm)',
|
|
108
|
+
border: msg.role === 'user' ? 'none' : '1px solid var(--border-color)'
|
|
109
|
+
}}>
|
|
110
|
+
{msg.content}
|
|
111
|
+
</div>
|
|
112
|
+
))}
|
|
113
|
+
|
|
114
|
+
{errorStatus && (
|
|
115
|
+
<div className="message-box" style={{
|
|
116
|
+
alignSelf: 'center',
|
|
117
|
+
background: '#fee2e2',
|
|
118
|
+
color: '#991b1b',
|
|
119
|
+
padding: '12px 16px',
|
|
120
|
+
borderRadius: '8px',
|
|
121
|
+
fontSize: '0.875rem',
|
|
122
|
+
fontWeight: 500,
|
|
123
|
+
maxWidth: '90%',
|
|
124
|
+
border: '1px solid #fecaca'
|
|
125
|
+
}}>
|
|
126
|
+
⚠️ {errorStatus}
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
<div ref={messagesEndRef} />
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Input Area */}
|
|
133
|
+
<div style={{ padding: '24px', borderTop: '1px solid var(--border-color)', background: '#ffffff' }}>
|
|
134
|
+
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: '12px' }}>
|
|
135
|
+
<input
|
|
136
|
+
type="text"
|
|
137
|
+
value={input}
|
|
138
|
+
onChange={e => setInput(e.target.value)}
|
|
139
|
+
placeholder="Test the shield..."
|
|
140
|
+
disabled={isLoading}
|
|
141
|
+
style={{
|
|
142
|
+
flex: 1,
|
|
143
|
+
padding: '12px 16px',
|
|
144
|
+
borderRadius: '12px',
|
|
145
|
+
border: '1px solid var(--border-color)',
|
|
146
|
+
outline: 'none',
|
|
147
|
+
background: '#f9fafb',
|
|
148
|
+
fontFamily: 'inherit',
|
|
149
|
+
fontSize: '1rem',
|
|
150
|
+
color: 'var(--text-active)'
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
<button type="submit" className="button-premium" disabled={isLoading || !input.trim()}>
|
|
154
|
+
{isLoading ? 'Scanning...' : 'Send'}
|
|
155
|
+
</button>
|
|
156
|
+
</form>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
</div>
|
|
160
|
+
</main>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shieldstack-ts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High-performance TypeScript-native LLM security & observability middleware",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup",
|
|
17
|
+
"dev": "tsup --watch",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"lint": "eslint src/ --ext .ts",
|
|
21
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"author": "Ali Shuja <alishujawork@gmail.com>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"keywords": ["llm", "security", "middleware", "pii", "privacy", "openai", "typescript", "edge"],
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/ShujaSN/shieldstack-ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"zod": "^3.22.4"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.12.7",
|
|
36
|
+
"@typescript-eslint/eslint-plugin": "^7.6.0",
|
|
37
|
+
"@typescript-eslint/parser": "^7.6.0",
|
|
38
|
+
"eslint": "^8.57.0",
|
|
39
|
+
"prettier": "^3.2.5",
|
|
40
|
+
"tsup": "^8.1.0",
|
|
41
|
+
"typescript": "^5.4.5",
|
|
42
|
+
"vitest": "^1.5.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ShieldStack } from '../core/ShieldStack';
|
|
2
|
+
|
|
3
|
+
export function expressShield(shield: ShieldStack) {
|
|
4
|
+
return async (req: any, res: any, next: any) => {
|
|
5
|
+
try {
|
|
6
|
+
const identifier = req.ip || 'anonymous';
|
|
7
|
+
|
|
8
|
+
if (req.body) {
|
|
9
|
+
const payloadStr = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);
|
|
10
|
+
const safePayload = await shield.evaluateRequest(payloadStr, identifier);
|
|
11
|
+
|
|
12
|
+
if (typeof req.body === 'string') {
|
|
13
|
+
req.body = safePayload;
|
|
14
|
+
} else {
|
|
15
|
+
try {
|
|
16
|
+
req.body = JSON.parse(safePayload);
|
|
17
|
+
} catch(e) {
|
|
18
|
+
req.body = safePayload;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
next();
|
|
23
|
+
} catch (error: any) {
|
|
24
|
+
shield.logger.error('express_shield_blocked', error.message);
|
|
25
|
+
res.status(403).json({ error: error.message });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ShieldStack } from '../core/ShieldStack';
|
|
2
|
+
|
|
3
|
+
export function honoShield(shield: ShieldStack) {
|
|
4
|
+
return async (c: any, next: any) => {
|
|
5
|
+
try {
|
|
6
|
+
const req: Request = c.req.raw;
|
|
7
|
+
const identifier = c.req.header('cf-connecting-ip') || 'anonymous';
|
|
8
|
+
const text = await c.req.raw.clone().text();
|
|
9
|
+
if (text) await shield.evaluateRequest(text, identifier);
|
|
10
|
+
|
|
11
|
+
await next();
|
|
12
|
+
|
|
13
|
+
if (c.res && c.res.body instanceof ReadableStream) {
|
|
14
|
+
const streamSanitizer = shield.createStreamSanitizer();
|
|
15
|
+
c.res = new Response(c.res.body.pipeThrough(streamSanitizer), c.res);
|
|
16
|
+
}
|
|
17
|
+
} catch (error: any) {
|
|
18
|
+
shield.logger.error('hono_shield_blocked', error.message);
|
|
19
|
+
return c.json({ error: error.message }, 403);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ShieldStack } from '../core/ShieldStack';
|
|
2
|
+
|
|
3
|
+
export function withShield(shield: ShieldStack, handler: (req: Request, ...args: any[]) => Promise<Response>) {
|
|
4
|
+
return async (req: Request, ...args: any[]) => {
|
|
5
|
+
try {
|
|
6
|
+
const identifier = req.headers.get('x-forwarded-for') || 'anonymous';
|
|
7
|
+
|
|
8
|
+
if (req.clone().body) {
|
|
9
|
+
const text = await req.clone().text();
|
|
10
|
+
if (text) await shield.evaluateRequest(text, identifier);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const response = await handler(req, ...args);
|
|
14
|
+
|
|
15
|
+
if (response.body) {
|
|
16
|
+
const streamSanitizer = shield.createStreamSanitizer();
|
|
17
|
+
return new Response(response.body.pipeThrough(streamSanitizer), response);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return response;
|
|
21
|
+
} catch (error: any) {
|
|
22
|
+
shield.logger.error('next_shield_blocked', error.message);
|
|
23
|
+
return new Response(JSON.stringify({ error: error.message }), { status: 403, headers: { 'Content-Type': 'application/json' }});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|