@sekyuriti/attest 0.2.1 → 0.2.3
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 +38 -93
- package/bin/attest.js +110 -126
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,52 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
API protection for Next.js applications. Verify that requests come from real browsers, not bots or scripts.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
One command setup:
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
|
|
10
|
+
npx @sekyuriti/attest login
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
This will:
|
|
14
|
+
1. Open browser for authentication
|
|
15
|
+
2. Let you select your project
|
|
16
|
+
3. Auto-add environment variables to `.env.local`
|
|
17
|
+
4. Auto-inject the ATTEST script into `layout.tsx`
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
Done. Your API is protected.
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
<script src="https://sekyuriti.build/api/v2/attest/script/YOUR_PROJECT_ID" defer></script>
|
|
17
|
-
```
|
|
21
|
+
## What It Does
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// app/layout.tsx
|
|
23
|
-
export default function RootLayout({ children }) {
|
|
24
|
-
return (
|
|
25
|
-
<html>
|
|
26
|
-
<head>
|
|
27
|
-
<script
|
|
28
|
-
src={`https://sekyuriti.build/api/v2/attest/script/${process.env.NEXT_PUBLIC_ATTEST_PROJECT_ID}`}
|
|
29
|
-
defer
|
|
30
|
-
/>
|
|
31
|
-
</head>
|
|
32
|
-
<body>{children}</body>
|
|
33
|
-
</html>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### 2. Protect your API routes
|
|
23
|
+
- **Frontend script** automatically signs all `fetch()` and `XMLHttpRequest` calls
|
|
24
|
+
- **Backend verification** validates signatures with SEKYURITI's API
|
|
25
|
+
- **Bots and scripts** can't generate valid signatures without running in a real browser
|
|
39
26
|
|
|
40
|
-
|
|
27
|
+
## Optional: Middleware
|
|
41
28
|
|
|
42
|
-
|
|
29
|
+
Add server-side verification for all API routes:
|
|
43
30
|
|
|
44
31
|
```ts
|
|
45
32
|
// middleware.ts
|
|
46
33
|
import { createAttestMiddleware } from "@sekyuriti/attest/middleware";
|
|
47
34
|
|
|
48
35
|
export const middleware = createAttestMiddleware({
|
|
49
|
-
projectId: process.env.
|
|
50
|
-
apiKey: process.env.
|
|
36
|
+
projectId: process.env.NEXT_PUBLIC_ATTEST_KEY!,
|
|
37
|
+
apiKey: process.env.ATTEST_SECRET_KEY!,
|
|
51
38
|
});
|
|
52
39
|
|
|
53
40
|
export const config = {
|
|
@@ -55,7 +42,7 @@ export const config = {
|
|
|
55
42
|
};
|
|
56
43
|
```
|
|
57
44
|
|
|
58
|
-
|
|
45
|
+
## Optional: Per-Route Verification
|
|
59
46
|
|
|
60
47
|
```ts
|
|
61
48
|
// app/api/protected/route.ts
|
|
@@ -63,8 +50,8 @@ import { verifyAttest } from "@sekyuriti/attest";
|
|
|
63
50
|
|
|
64
51
|
export async function POST(request: Request) {
|
|
65
52
|
const result = await verifyAttest(request, {
|
|
66
|
-
projectId: process.env.
|
|
67
|
-
apiKey: process.env.
|
|
53
|
+
projectId: process.env.NEXT_PUBLIC_ATTEST_KEY!,
|
|
54
|
+
apiKey: process.env.ATTEST_SECRET_KEY!,
|
|
68
55
|
});
|
|
69
56
|
|
|
70
57
|
if (!result.attested) {
|
|
@@ -75,72 +62,26 @@ export async function POST(request: Request) {
|
|
|
75
62
|
}
|
|
76
63
|
```
|
|
77
64
|
|
|
78
|
-
##
|
|
79
|
-
|
|
80
|
-
```env
|
|
81
|
-
ATTEST_PROJECT_ID=ATST_xxxxxxxxxxxx
|
|
82
|
-
ATTEST_API_KEY=sk_xxxxxxxxxxxx
|
|
83
|
-
NEXT_PUBLIC_ATTEST_PROJECT_ID=ATST_xxxxxxxxxxxx
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## API Reference
|
|
87
|
-
|
|
88
|
-
### `verifyAttest(request, config)`
|
|
89
|
-
|
|
90
|
-
Verify a single request.
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
const result = await verifyAttest(request, {
|
|
94
|
-
projectId: "ATST_xxx",
|
|
95
|
-
apiKey: "sk_xxx",
|
|
96
|
-
});
|
|
65
|
+
## CLI Commands
|
|
97
66
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Create middleware for automatic verification.
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
const middleware = createAttestMiddleware({
|
|
109
|
-
projectId: "ATST_xxx",
|
|
110
|
-
apiKey: "sk_xxx",
|
|
111
|
-
|
|
112
|
-
// Optional settings
|
|
113
|
-
protectedRoutes: ["/api/*"], // Routes to protect
|
|
114
|
-
excludeRoutes: ["/api/health"], // Routes to skip
|
|
115
|
-
allowUnauthenticated: false, // Allow requests without headers
|
|
116
|
-
|
|
117
|
-
// Custom handlers
|
|
118
|
-
onBlocked: (req, result) => Response.json({ error: result.reason }, { status: 403 }),
|
|
119
|
-
onAllowed: (req, result) => console.log("Verified:", result.fingerprint),
|
|
120
|
-
});
|
|
67
|
+
```bash
|
|
68
|
+
attest login # Authenticate and setup project
|
|
69
|
+
attest logout # Sign out
|
|
70
|
+
attest status # Show account and usage info
|
|
71
|
+
attest init # Re-run setup in current project
|
|
72
|
+
attest whoami # Print current user email
|
|
73
|
+
attest help # Show help
|
|
121
74
|
```
|
|
122
75
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
Create a reusable verifier function.
|
|
76
|
+
## Environment Variables
|
|
126
77
|
|
|
127
|
-
|
|
128
|
-
const verify = createAttestVerifier({
|
|
129
|
-
projectId: process.env.ATTEST_PROJECT_ID!,
|
|
130
|
-
apiKey: process.env.ATTEST_API_KEY!,
|
|
131
|
-
});
|
|
78
|
+
Auto-generated by `attest login`:
|
|
132
79
|
|
|
133
|
-
|
|
134
|
-
|
|
80
|
+
```env
|
|
81
|
+
NEXT_PUBLIC_ATTEST_KEY=your_public_key
|
|
82
|
+
ATTEST_SECRET_KEY=your_secret_key
|
|
135
83
|
```
|
|
136
84
|
|
|
137
|
-
## How It Works
|
|
138
|
-
|
|
139
|
-
1. **Frontend script** automatically signs all `fetch()` and `XMLHttpRequest` calls
|
|
140
|
-
2. **Signatures** are added as headers: `X-Attest-Timestamp`, `X-Attest-Signature`, `X-Attest-Fingerprint`
|
|
141
|
-
3. **Backend verification** validates signatures with SEKYURITI's API
|
|
142
|
-
4. **Bots and scripts** can't generate valid signatures without running in a real browser
|
|
143
|
-
|
|
144
85
|
## Protection Features
|
|
145
86
|
|
|
146
87
|
- DevTools detection
|
|
@@ -149,6 +90,10 @@ const result = await verify(request);
|
|
|
149
90
|
- Browser fingerprinting
|
|
150
91
|
- Timestamp validation
|
|
151
92
|
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
https://sekyuriti.build/docs/attest
|
|
96
|
+
|
|
152
97
|
## License
|
|
153
98
|
|
|
154
99
|
MIT
|
package/bin/attest.js
CHANGED
|
@@ -139,7 +139,109 @@ function printHeader() {
|
|
|
139
139
|
logBold(" █▀▀ █▀▀ █▄▀ █▄█ █ █ █▀█ █ ▀█▀ █");
|
|
140
140
|
logBold(" ▄▄█ ██▄ █ █ █ █▄█ █▀▄ █ █ █");
|
|
141
141
|
log("");
|
|
142
|
-
logDim(" ATTEST CLI v0.2.
|
|
142
|
+
logDim(" ATTEST CLI v0.2.1");
|
|
143
|
+
log("");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
147
|
+
// INIT HELPER (used by both login and init commands)
|
|
148
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
149
|
+
|
|
150
|
+
async function runInit(publicKey, apiKey) {
|
|
151
|
+
let steps = [];
|
|
152
|
+
|
|
153
|
+
// Step 1: Add .env variables
|
|
154
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
155
|
+
const envLocalPath = path.join(process.cwd(), ".env.local");
|
|
156
|
+
let targetEnvPath = envLocalPath;
|
|
157
|
+
|
|
158
|
+
if (fs.existsSync(envLocalPath)) {
|
|
159
|
+
targetEnvPath = envLocalPath;
|
|
160
|
+
} else if (fs.existsSync(envPath)) {
|
|
161
|
+
targetEnvPath = envPath;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const envVars = `
|
|
165
|
+
# ATTEST Configuration
|
|
166
|
+
NEXT_PUBLIC_ATTEST_KEY=${publicKey}
|
|
167
|
+
ATTEST_SECRET_KEY=${apiKey}
|
|
168
|
+
`.trim();
|
|
169
|
+
|
|
170
|
+
if (fs.existsSync(targetEnvPath)) {
|
|
171
|
+
let content = fs.readFileSync(targetEnvPath, "utf-8");
|
|
172
|
+
if (!content.includes("ATTEST_KEY")) {
|
|
173
|
+
content += "\n\n" + envVars + "\n";
|
|
174
|
+
fs.writeFileSync(targetEnvPath, content);
|
|
175
|
+
steps.push(`Added ATTEST config to ${path.basename(targetEnvPath)}`);
|
|
176
|
+
} else {
|
|
177
|
+
steps.push(`ATTEST config already in ${path.basename(targetEnvPath)}`);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
fs.writeFileSync(envLocalPath, envVars + "\n");
|
|
181
|
+
steps.push("Created .env.local with ATTEST config");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Step 2: Find and update layout.tsx
|
|
185
|
+
const layoutPaths = [
|
|
186
|
+
path.join(process.cwd(), "src/app/layout.tsx"),
|
|
187
|
+
path.join(process.cwd(), "app/layout.tsx"),
|
|
188
|
+
path.join(process.cwd(), "src/app/layout.js"),
|
|
189
|
+
path.join(process.cwd(), "app/layout.js"),
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
let layoutPath = null;
|
|
193
|
+
for (const p of layoutPaths) {
|
|
194
|
+
if (fs.existsSync(p)) {
|
|
195
|
+
layoutPath = p;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const scriptTag = `<Script
|
|
201
|
+
src="https://sekyuriti.build/api/v2/attest/script/${publicKey}"
|
|
202
|
+
strategy="beforeInteractive"
|
|
203
|
+
/>`;
|
|
204
|
+
|
|
205
|
+
if (layoutPath) {
|
|
206
|
+
let layoutContent = fs.readFileSync(layoutPath, "utf-8");
|
|
207
|
+
|
|
208
|
+
if (layoutContent.includes("sekyuriti.build/api/v2/attest/script")) {
|
|
209
|
+
steps.push("ATTEST script already in layout");
|
|
210
|
+
} else {
|
|
211
|
+
// Check if Script is already imported
|
|
212
|
+
const hasScriptImport = layoutContent.includes("from 'next/script'") || layoutContent.includes('from "next/script"');
|
|
213
|
+
|
|
214
|
+
if (!hasScriptImport) {
|
|
215
|
+
// Add Script import after the first import line
|
|
216
|
+
layoutContent = layoutContent.replace(
|
|
217
|
+
/(import .+ from ['"][^'"]+['"];?\n)/,
|
|
218
|
+
`$1import Script from "next/script";\n`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Add script tag after <body> or <body className=...>
|
|
223
|
+
if (layoutContent.includes("<body")) {
|
|
224
|
+
layoutContent = layoutContent.replace(
|
|
225
|
+
/(<body[^>]*>)/,
|
|
226
|
+
`$1\n ${scriptTag}`
|
|
227
|
+
);
|
|
228
|
+
fs.writeFileSync(layoutPath, layoutContent);
|
|
229
|
+
steps.push(`Added ATTEST script to ${path.basename(layoutPath)}`);
|
|
230
|
+
} else {
|
|
231
|
+
steps.push("Could not find <body> tag in layout");
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
steps.push("No layout.tsx found - add script manually");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Print results
|
|
239
|
+
for (const step of steps) {
|
|
240
|
+
log(` ${c.bold}✓${c.reset} ${step}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
log("");
|
|
244
|
+
log(" Documentation: https://sekyuriti.build/docs/attest");
|
|
143
245
|
log("");
|
|
144
246
|
}
|
|
145
247
|
|
|
@@ -211,6 +313,11 @@ async function cmdLogin() {
|
|
|
211
313
|
apiKey: data.apiKey,
|
|
212
314
|
});
|
|
213
315
|
|
|
316
|
+
// Auto-run init after successful login
|
|
317
|
+
log(" Setting up your project...");
|
|
318
|
+
log("");
|
|
319
|
+
await runInit(data.publicKey, data.apiKey);
|
|
320
|
+
|
|
214
321
|
return;
|
|
215
322
|
}
|
|
216
323
|
}
|
|
@@ -334,132 +441,9 @@ async function cmdInit() {
|
|
|
334
441
|
return;
|
|
335
442
|
}
|
|
336
443
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// Step 1: Add .env variables
|
|
340
|
-
const envPath = path.join(process.cwd(), ".env");
|
|
341
|
-
const envLocalPath = path.join(process.cwd(), ".env.local");
|
|
342
|
-
let targetEnvPath = envLocalPath;
|
|
343
|
-
|
|
344
|
-
if (fs.existsSync(envLocalPath)) {
|
|
345
|
-
targetEnvPath = envLocalPath;
|
|
346
|
-
} else if (fs.existsSync(envPath)) {
|
|
347
|
-
targetEnvPath = envPath;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
const envVars = `
|
|
351
|
-
# ATTEST Configuration
|
|
352
|
-
NEXT_PUBLIC_ATTEST_KEY=${config.publicKey}
|
|
353
|
-
ATTEST_SECRET_KEY=${config.apiKey}
|
|
354
|
-
`.trim();
|
|
355
|
-
|
|
356
|
-
if (fs.existsSync(targetEnvPath)) {
|
|
357
|
-
let content = fs.readFileSync(targetEnvPath, "utf-8");
|
|
358
|
-
if (!content.includes("ATTEST_KEY")) {
|
|
359
|
-
content += "\n\n" + envVars + "\n";
|
|
360
|
-
fs.writeFileSync(targetEnvPath, content);
|
|
361
|
-
steps.push(`Added ATTEST config to ${path.basename(targetEnvPath)}`);
|
|
362
|
-
} else {
|
|
363
|
-
steps.push(`ATTEST config already in ${path.basename(targetEnvPath)}`);
|
|
364
|
-
}
|
|
365
|
-
} else {
|
|
366
|
-
fs.writeFileSync(envLocalPath, envVars + "\n");
|
|
367
|
-
steps.push("Created .env.local with ATTEST config");
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// Step 2: Find and update layout.tsx
|
|
371
|
-
const layoutPaths = [
|
|
372
|
-
path.join(process.cwd(), "src/app/layout.tsx"),
|
|
373
|
-
path.join(process.cwd(), "app/layout.tsx"),
|
|
374
|
-
path.join(process.cwd(), "src/app/layout.js"),
|
|
375
|
-
path.join(process.cwd(), "app/layout.js"),
|
|
376
|
-
];
|
|
377
|
-
|
|
378
|
-
let layoutPath = null;
|
|
379
|
-
for (const p of layoutPaths) {
|
|
380
|
-
if (fs.existsSync(p)) {
|
|
381
|
-
layoutPath = p;
|
|
382
|
-
break;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const scriptTag = `<Script
|
|
387
|
-
src="https://sekyuriti.build/api/v2/attest/script/${config.publicKey}"
|
|
388
|
-
strategy="beforeInteractive"
|
|
389
|
-
/>`;
|
|
390
|
-
|
|
391
|
-
if (layoutPath) {
|
|
392
|
-
let layoutContent = fs.readFileSync(layoutPath, "utf-8");
|
|
393
|
-
|
|
394
|
-
if (layoutContent.includes("sekyuriti.build/api/v2/attest/script")) {
|
|
395
|
-
steps.push("ATTEST script already in layout");
|
|
396
|
-
} else {
|
|
397
|
-
// Check if Script is already imported
|
|
398
|
-
const hasScriptImport = layoutContent.includes("from 'next/script'") || layoutContent.includes('from "next/script"');
|
|
399
|
-
|
|
400
|
-
if (!hasScriptImport) {
|
|
401
|
-
// Add Script import after the first import line
|
|
402
|
-
layoutContent = layoutContent.replace(
|
|
403
|
-
/(import .+ from ['"][^'"]+['"];?\n)/,
|
|
404
|
-
`$1import Script from "next/script";\n`
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Add script tag after <body> or <body className=...>
|
|
409
|
-
if (layoutContent.includes("<body")) {
|
|
410
|
-
layoutContent = layoutContent.replace(
|
|
411
|
-
/(<body[^>]*>)/,
|
|
412
|
-
`$1\n ${scriptTag}`
|
|
413
|
-
);
|
|
414
|
-
fs.writeFileSync(layoutPath, layoutContent);
|
|
415
|
-
steps.push(`Added ATTEST script to ${path.basename(layoutPath)}`);
|
|
416
|
-
} else {
|
|
417
|
-
steps.push("Could not find <body> tag in layout");
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
} else {
|
|
421
|
-
steps.push("No layout.tsx found - add script manually");
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Step 3: Check for middleware.ts and offer to add verification
|
|
425
|
-
const middlewarePaths = [
|
|
426
|
-
path.join(process.cwd(), "middleware.ts"),
|
|
427
|
-
path.join(process.cwd(), "src/middleware.ts"),
|
|
428
|
-
path.join(process.cwd(), "middleware.js"),
|
|
429
|
-
path.join(process.cwd(), "src/middleware.js"),
|
|
430
|
-
];
|
|
431
|
-
|
|
432
|
-
let hasMiddleware = false;
|
|
433
|
-
for (const p of middlewarePaths) {
|
|
434
|
-
if (fs.existsSync(p)) {
|
|
435
|
-
hasMiddleware = true;
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Print results
|
|
441
|
-
log(" SETUP COMPLETE");
|
|
442
|
-
log("");
|
|
443
|
-
|
|
444
|
-
for (const step of steps) {
|
|
445
|
-
log(` ${c.bold}✓${c.reset} ${step}`);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
log("");
|
|
449
|
-
|
|
450
|
-
if (!hasMiddleware) {
|
|
451
|
-
log(" Optional: Add middleware for server-side verification:");
|
|
452
|
-
log("");
|
|
453
|
-
log(` ${c.dim}// middleware.ts${c.reset}`);
|
|
454
|
-
log(` ${c.dim}import { createAttestMiddleware } from '@sekyuriti/attest/middleware';${c.reset}`);
|
|
455
|
-
log(` ${c.dim}export default createAttestMiddleware({${c.reset}`);
|
|
456
|
-
log(` ${c.dim} protectedPaths: ['/api/'],${c.reset}`);
|
|
457
|
-
log(` ${c.dim}});${c.reset}`);
|
|
458
|
-
log("");
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
log(" Documentation: https://sekyuriti.build/docs/attest");
|
|
444
|
+
log(" Setting up ATTEST...");
|
|
462
445
|
log("");
|
|
446
|
+
await runInit(config.publicKey, config.apiKey);
|
|
463
447
|
}
|
|
464
448
|
|
|
465
449
|
// ═══════════════════════════════════════════════════════════════════
|