@sekyuriti/attest 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 +154 -0
- package/dist/index.d.mts +92 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +61 -0
- package/dist/index.mjs.map +1 -0
- package/dist/middleware.d.mts +75 -0
- package/dist/middleware.d.ts +75 -0
- package/dist/middleware.js +136 -0
- package/dist/middleware.js.map +1 -0
- package/dist/middleware.mjs +110 -0
- package/dist/middleware.mjs.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @sekyuriti/attest
|
|
2
|
+
|
|
3
|
+
API protection for Next.js applications. Verify that requests come from real browsers, not bots or scripts.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @sekyuriti/attest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Add the script to your frontend
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<script src="https://sekyuriti.build/api/v2/attest/script/YOUR_PROJECT_ID" defer></script>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or in Next.js:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
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
|
|
39
|
+
|
|
40
|
+
**Option A: Middleware (recommended)**
|
|
41
|
+
|
|
42
|
+
Protects all `/api/*` routes automatically:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// middleware.ts
|
|
46
|
+
import { createAttestMiddleware } from "@sekyuriti/attest/middleware";
|
|
47
|
+
|
|
48
|
+
export const middleware = createAttestMiddleware({
|
|
49
|
+
projectId: process.env.ATTEST_PROJECT_ID!,
|
|
50
|
+
apiKey: process.env.ATTEST_API_KEY!,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const config = {
|
|
54
|
+
matcher: "/api/:path*",
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Option B: Per-route verification**
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// app/api/protected/route.ts
|
|
62
|
+
import { verifyAttest } from "@sekyuriti/attest";
|
|
63
|
+
|
|
64
|
+
export async function POST(request: Request) {
|
|
65
|
+
const result = await verifyAttest(request, {
|
|
66
|
+
projectId: process.env.ATTEST_PROJECT_ID!,
|
|
67
|
+
apiKey: process.env.ATTEST_API_KEY!,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!result.attested) {
|
|
71
|
+
return Response.json({ error: "Not attested" }, { status: 403 });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle request...
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Environment Variables
|
|
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
|
+
});
|
|
97
|
+
|
|
98
|
+
// result.attested: boolean
|
|
99
|
+
// result.fingerprint: string (if attested)
|
|
100
|
+
// result.reason: string (if not attested)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `createAttestMiddleware(config)`
|
|
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
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `createAttestVerifier(config)`
|
|
124
|
+
|
|
125
|
+
Create a reusable verifier function.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const verify = createAttestVerifier({
|
|
129
|
+
projectId: process.env.ATTEST_PROJECT_ID!,
|
|
130
|
+
apiKey: process.env.ATTEST_API_KEY!,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Use in multiple routes
|
|
134
|
+
const result = await verify(request);
|
|
135
|
+
```
|
|
136
|
+
|
|
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
|
+
## Protection Features
|
|
145
|
+
|
|
146
|
+
- DevTools detection
|
|
147
|
+
- Bot/headless browser detection
|
|
148
|
+
- Request signing with HMAC-SHA256
|
|
149
|
+
- Browser fingerprinting
|
|
150
|
+
- Timestamp validation
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sekyuriti/attest
|
|
3
|
+
*
|
|
4
|
+
* API protection for Next.js applications.
|
|
5
|
+
* Verify that requests come from real browsers, not bots or scripts.
|
|
6
|
+
*/
|
|
7
|
+
interface AttestConfig {
|
|
8
|
+
/** Your ATTEST project ID (starts with ATST_) */
|
|
9
|
+
projectId: string;
|
|
10
|
+
/** Your ATTEST API key (keep this secret, server-side only) */
|
|
11
|
+
apiKey: string;
|
|
12
|
+
/** Custom verify URL (optional, for self-hosted) */
|
|
13
|
+
verifyUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
interface AttestResult {
|
|
16
|
+
/** Whether the request passed verification */
|
|
17
|
+
attested: boolean;
|
|
18
|
+
/** Browser fingerprint (if attested) */
|
|
19
|
+
fingerprint?: string;
|
|
20
|
+
/** Verification timestamp */
|
|
21
|
+
timestamp?: number;
|
|
22
|
+
/** Reason for failure (if not attested) */
|
|
23
|
+
reason?: string;
|
|
24
|
+
/** Warning message (e.g., approaching rate limit) */
|
|
25
|
+
warning?: string;
|
|
26
|
+
/** Current usage stats */
|
|
27
|
+
usage?: {
|
|
28
|
+
used: number;
|
|
29
|
+
limit: number;
|
|
30
|
+
percent: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
interface AttestHeaders {
|
|
34
|
+
timestamp: string | null;
|
|
35
|
+
signature: string | null;
|
|
36
|
+
fingerprint: string | null;
|
|
37
|
+
project: string | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract ATTEST headers from a request
|
|
41
|
+
*/
|
|
42
|
+
declare function getAttestHeaders(request: Request): AttestHeaders;
|
|
43
|
+
/**
|
|
44
|
+
* Check if a request has ATTEST headers
|
|
45
|
+
*/
|
|
46
|
+
declare function hasAttestHeaders(request: Request): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Verify a request with ATTEST
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { verifyAttest } from "@sekyuriti/attest";
|
|
53
|
+
*
|
|
54
|
+
* export async function POST(request: Request) {
|
|
55
|
+
* const result = await verifyAttest(request, {
|
|
56
|
+
* projectId: process.env.ATTEST_PROJECT_ID!,
|
|
57
|
+
* apiKey: process.env.ATTEST_API_KEY!,
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* if (!result.attested) {
|
|
61
|
+
* return Response.json({ error: "Not attested" }, { status: 403 });
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* // ... handle request
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
declare function verifyAttest(request: Request, config: AttestConfig): Promise<AttestResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Create a configured verifier function
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { createAttestVerifier } from "@sekyuriti/attest";
|
|
75
|
+
*
|
|
76
|
+
* const verify = createAttestVerifier({
|
|
77
|
+
* projectId: process.env.ATTEST_PROJECT_ID!,
|
|
78
|
+
* apiKey: process.env.ATTEST_API_KEY!,
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* export async function POST(request: Request) {
|
|
82
|
+
* const result = await verify(request);
|
|
83
|
+
* if (!result.attested) {
|
|
84
|
+
* return Response.json({ error: "Not attested" }, { status: 403 });
|
|
85
|
+
* }
|
|
86
|
+
* // ...
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
declare function createAttestVerifier(config: AttestConfig): (request: Request) => Promise<AttestResult>;
|
|
91
|
+
|
|
92
|
+
export { type AttestConfig, type AttestHeaders, type AttestResult, createAttestVerifier, getAttestHeaders, hasAttestHeaders, verifyAttest };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sekyuriti/attest
|
|
3
|
+
*
|
|
4
|
+
* API protection for Next.js applications.
|
|
5
|
+
* Verify that requests come from real browsers, not bots or scripts.
|
|
6
|
+
*/
|
|
7
|
+
interface AttestConfig {
|
|
8
|
+
/** Your ATTEST project ID (starts with ATST_) */
|
|
9
|
+
projectId: string;
|
|
10
|
+
/** Your ATTEST API key (keep this secret, server-side only) */
|
|
11
|
+
apiKey: string;
|
|
12
|
+
/** Custom verify URL (optional, for self-hosted) */
|
|
13
|
+
verifyUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
interface AttestResult {
|
|
16
|
+
/** Whether the request passed verification */
|
|
17
|
+
attested: boolean;
|
|
18
|
+
/** Browser fingerprint (if attested) */
|
|
19
|
+
fingerprint?: string;
|
|
20
|
+
/** Verification timestamp */
|
|
21
|
+
timestamp?: number;
|
|
22
|
+
/** Reason for failure (if not attested) */
|
|
23
|
+
reason?: string;
|
|
24
|
+
/** Warning message (e.g., approaching rate limit) */
|
|
25
|
+
warning?: string;
|
|
26
|
+
/** Current usage stats */
|
|
27
|
+
usage?: {
|
|
28
|
+
used: number;
|
|
29
|
+
limit: number;
|
|
30
|
+
percent: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
interface AttestHeaders {
|
|
34
|
+
timestamp: string | null;
|
|
35
|
+
signature: string | null;
|
|
36
|
+
fingerprint: string | null;
|
|
37
|
+
project: string | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extract ATTEST headers from a request
|
|
41
|
+
*/
|
|
42
|
+
declare function getAttestHeaders(request: Request): AttestHeaders;
|
|
43
|
+
/**
|
|
44
|
+
* Check if a request has ATTEST headers
|
|
45
|
+
*/
|
|
46
|
+
declare function hasAttestHeaders(request: Request): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Verify a request with ATTEST
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { verifyAttest } from "@sekyuriti/attest";
|
|
53
|
+
*
|
|
54
|
+
* export async function POST(request: Request) {
|
|
55
|
+
* const result = await verifyAttest(request, {
|
|
56
|
+
* projectId: process.env.ATTEST_PROJECT_ID!,
|
|
57
|
+
* apiKey: process.env.ATTEST_API_KEY!,
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* if (!result.attested) {
|
|
61
|
+
* return Response.json({ error: "Not attested" }, { status: 403 });
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* // ... handle request
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
declare function verifyAttest(request: Request, config: AttestConfig): Promise<AttestResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Create a configured verifier function
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { createAttestVerifier } from "@sekyuriti/attest";
|
|
75
|
+
*
|
|
76
|
+
* const verify = createAttestVerifier({
|
|
77
|
+
* projectId: process.env.ATTEST_PROJECT_ID!,
|
|
78
|
+
* apiKey: process.env.ATTEST_API_KEY!,
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* export async function POST(request: Request) {
|
|
82
|
+
* const result = await verify(request);
|
|
83
|
+
* if (!result.attested) {
|
|
84
|
+
* return Response.json({ error: "Not attested" }, { status: 403 });
|
|
85
|
+
* }
|
|
86
|
+
* // ...
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
declare function createAttestVerifier(config: AttestConfig): (request: Request) => Promise<AttestResult>;
|
|
91
|
+
|
|
92
|
+
export { type AttestConfig, type AttestHeaders, type AttestResult, createAttestVerifier, getAttestHeaders, hasAttestHeaders, verifyAttest };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
createAttestVerifier: () => createAttestVerifier,
|
|
24
|
+
getAttestHeaders: () => getAttestHeaders,
|
|
25
|
+
hasAttestHeaders: () => hasAttestHeaders,
|
|
26
|
+
verifyAttest: () => verifyAttest
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(src_exports);
|
|
29
|
+
var ATTEST_VERIFY_URL = "https://sekyuriti.build/api/v2/attest/verify";
|
|
30
|
+
function getAttestHeaders(request) {
|
|
31
|
+
return {
|
|
32
|
+
timestamp: request.headers.get("x-attest-timestamp"),
|
|
33
|
+
signature: request.headers.get("x-attest-signature"),
|
|
34
|
+
fingerprint: request.headers.get("x-attest-fingerprint"),
|
|
35
|
+
project: request.headers.get("x-attest-project")
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function hasAttestHeaders(request) {
|
|
39
|
+
const headers = getAttestHeaders(request);
|
|
40
|
+
return !!(headers.timestamp && headers.signature && headers.fingerprint);
|
|
41
|
+
}
|
|
42
|
+
async function verifyAttest(request, config) {
|
|
43
|
+
const headers = getAttestHeaders(request);
|
|
44
|
+
if (!headers.timestamp || !headers.signature || !headers.fingerprint) {
|
|
45
|
+
return {
|
|
46
|
+
attested: false,
|
|
47
|
+
reason: "Missing ATTEST headers"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(config.verifyUrl || ATTEST_VERIFY_URL, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/json"
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
project_id: config.projectId,
|
|
58
|
+
api_key: config.apiKey,
|
|
59
|
+
timestamp: parseInt(headers.timestamp, 10),
|
|
60
|
+
signature: headers.signature,
|
|
61
|
+
fingerprint: headers.fingerprint
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
return {
|
|
66
|
+
attested: false,
|
|
67
|
+
reason: `Verification service error: ${response.status}`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return await response.json();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("[@sekyuriti/attest] Verification failed:", error);
|
|
73
|
+
return {
|
|
74
|
+
attested: true,
|
|
75
|
+
reason: "Verification service unavailable"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function createAttestVerifier(config) {
|
|
80
|
+
return (request) => verifyAttest(request, config);
|
|
81
|
+
}
|
|
82
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
83
|
+
0 && (module.exports = {
|
|
84
|
+
createAttestVerifier,
|
|
85
|
+
getAttestHeaders,
|
|
86
|
+
hasAttestHeaders,
|
|
87
|
+
verifyAttest
|
|
88
|
+
});
|
|
89
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @sekyuriti/attest\n *\n * API protection for Next.js applications.\n * Verify that requests come from real browsers, not bots or scripts.\n */\n\nconst ATTEST_VERIFY_URL = \"https://sekyuriti.build/api/v2/attest/verify\";\n\nexport interface AttestConfig {\n /** Your ATTEST project ID (starts with ATST_) */\n projectId: string;\n /** Your ATTEST API key (keep this secret, server-side only) */\n apiKey: string;\n /** Custom verify URL (optional, for self-hosted) */\n verifyUrl?: string;\n}\n\nexport interface AttestResult {\n /** Whether the request passed verification */\n attested: boolean;\n /** Browser fingerprint (if attested) */\n fingerprint?: string;\n /** Verification timestamp */\n timestamp?: number;\n /** Reason for failure (if not attested) */\n reason?: string;\n /** Warning message (e.g., approaching rate limit) */\n warning?: string;\n /** Current usage stats */\n usage?: {\n used: number;\n limit: number;\n percent: number;\n };\n}\n\nexport interface AttestHeaders {\n timestamp: string | null;\n signature: string | null;\n fingerprint: string | null;\n project: string | null;\n}\n\n/**\n * Extract ATTEST headers from a request\n */\nexport function getAttestHeaders(request: Request): AttestHeaders {\n return {\n timestamp: request.headers.get(\"x-attest-timestamp\"),\n signature: request.headers.get(\"x-attest-signature\"),\n fingerprint: request.headers.get(\"x-attest-fingerprint\"),\n project: request.headers.get(\"x-attest-project\"),\n };\n}\n\n/**\n * Check if a request has ATTEST headers\n */\nexport function hasAttestHeaders(request: Request): boolean {\n const headers = getAttestHeaders(request);\n return !!(headers.timestamp && headers.signature && headers.fingerprint);\n}\n\n/**\n * Verify a request with ATTEST\n *\n * @example\n * ```ts\n * import { verifyAttest } from \"@sekyuriti/attest\";\n *\n * export async function POST(request: Request) {\n * const result = await verifyAttest(request, {\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * if (!result.attested) {\n * return Response.json({ error: \"Not attested\" }, { status: 403 });\n * }\n *\n * // ... handle request\n * }\n * ```\n */\nexport async function verifyAttest(\n request: Request,\n config: AttestConfig\n): Promise<AttestResult> {\n const headers = getAttestHeaders(request);\n\n // If no ATTEST headers, request is not attested\n if (!headers.timestamp || !headers.signature || !headers.fingerprint) {\n return {\n attested: false,\n reason: \"Missing ATTEST headers\",\n };\n }\n\n try {\n const response = await fetch(config.verifyUrl || ATTEST_VERIFY_URL, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n project_id: config.projectId,\n api_key: config.apiKey,\n timestamp: parseInt(headers.timestamp, 10),\n signature: headers.signature,\n fingerprint: headers.fingerprint,\n }),\n });\n\n if (!response.ok) {\n return {\n attested: false,\n reason: `Verification service error: ${response.status}`,\n };\n }\n\n return (await response.json()) as AttestResult;\n } catch (error) {\n // Fail open - don't break the app if ATTEST service is down\n console.error(\"[@sekyuriti/attest] Verification failed:\", error);\n return {\n attested: true,\n reason: \"Verification service unavailable\",\n };\n }\n}\n\n/**\n * Create a configured verifier function\n *\n * @example\n * ```ts\n * import { createAttestVerifier } from \"@sekyuriti/attest\";\n *\n * const verify = createAttestVerifier({\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * export async function POST(request: Request) {\n * const result = await verify(request);\n * if (!result.attested) {\n * return Response.json({ error: \"Not attested\" }, { status: 403 });\n * }\n * // ...\n * }\n * ```\n */\nexport function createAttestVerifier(config: AttestConfig) {\n return (request: Request) => verifyAttest(request, config);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,IAAM,oBAAoB;AAwCnB,SAAS,iBAAiB,SAAiC;AAChE,SAAO;AAAA,IACL,WAAW,QAAQ,QAAQ,IAAI,oBAAoB;AAAA,IACnD,WAAW,QAAQ,QAAQ,IAAI,oBAAoB;AAAA,IACnD,aAAa,QAAQ,QAAQ,IAAI,sBAAsB;AAAA,IACvD,SAAS,QAAQ,QAAQ,IAAI,kBAAkB;AAAA,EACjD;AACF;AAKO,SAAS,iBAAiB,SAA2B;AAC1D,QAAM,UAAU,iBAAiB,OAAO;AACxC,SAAO,CAAC,EAAE,QAAQ,aAAa,QAAQ,aAAa,QAAQ;AAC9D;AAuBA,eAAsB,aACpB,SACA,QACuB;AACvB,QAAM,UAAU,iBAAiB,OAAO;AAGxC,MAAI,CAAC,QAAQ,aAAa,CAAC,QAAQ,aAAa,CAAC,QAAQ,aAAa;AACpE,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,OAAO,aAAa,mBAAmB;AAAA,MAClE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,YAAY,OAAO;AAAA,QACnB,SAAS,OAAO;AAAA,QAChB,WAAW,SAAS,QAAQ,WAAW,EAAE;AAAA,QACzC,WAAW,QAAQ;AAAA,QACnB,aAAa,QAAQ;AAAA,MACvB,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,aAAO;AAAA,QACL,UAAU;AAAA,QACV,QAAQ,+BAA+B,SAAS,MAAM;AAAA,MACxD;AAAA,IACF;AAEA,WAAQ,MAAM,SAAS,KAAK;AAAA,EAC9B,SAAS,OAAO;AAEd,YAAQ,MAAM,4CAA4C,KAAK;AAC/D,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,IACV;AAAA,EACF;AACF;AAuBO,SAAS,qBAAqB,QAAsB;AACzD,SAAO,CAAC,YAAqB,aAAa,SAAS,MAAM;AAC3D;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var ATTEST_VERIFY_URL = "https://sekyuriti.build/api/v2/attest/verify";
|
|
3
|
+
function getAttestHeaders(request) {
|
|
4
|
+
return {
|
|
5
|
+
timestamp: request.headers.get("x-attest-timestamp"),
|
|
6
|
+
signature: request.headers.get("x-attest-signature"),
|
|
7
|
+
fingerprint: request.headers.get("x-attest-fingerprint"),
|
|
8
|
+
project: request.headers.get("x-attest-project")
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function hasAttestHeaders(request) {
|
|
12
|
+
const headers = getAttestHeaders(request);
|
|
13
|
+
return !!(headers.timestamp && headers.signature && headers.fingerprint);
|
|
14
|
+
}
|
|
15
|
+
async function verifyAttest(request, config) {
|
|
16
|
+
const headers = getAttestHeaders(request);
|
|
17
|
+
if (!headers.timestamp || !headers.signature || !headers.fingerprint) {
|
|
18
|
+
return {
|
|
19
|
+
attested: false,
|
|
20
|
+
reason: "Missing ATTEST headers"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(config.verifyUrl || ATTEST_VERIFY_URL, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json"
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
project_id: config.projectId,
|
|
31
|
+
api_key: config.apiKey,
|
|
32
|
+
timestamp: parseInt(headers.timestamp, 10),
|
|
33
|
+
signature: headers.signature,
|
|
34
|
+
fingerprint: headers.fingerprint
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
return {
|
|
39
|
+
attested: false,
|
|
40
|
+
reason: `Verification service error: ${response.status}`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return await response.json();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("[@sekyuriti/attest] Verification failed:", error);
|
|
46
|
+
return {
|
|
47
|
+
attested: true,
|
|
48
|
+
reason: "Verification service unavailable"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function createAttestVerifier(config) {
|
|
53
|
+
return (request) => verifyAttest(request, config);
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
createAttestVerifier,
|
|
57
|
+
getAttestHeaders,
|
|
58
|
+
hasAttestHeaders,
|
|
59
|
+
verifyAttest
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @sekyuriti/attest\n *\n * API protection for Next.js applications.\n * Verify that requests come from real browsers, not bots or scripts.\n */\n\nconst ATTEST_VERIFY_URL = \"https://sekyuriti.build/api/v2/attest/verify\";\n\nexport interface AttestConfig {\n /** Your ATTEST project ID (starts with ATST_) */\n projectId: string;\n /** Your ATTEST API key (keep this secret, server-side only) */\n apiKey: string;\n /** Custom verify URL (optional, for self-hosted) */\n verifyUrl?: string;\n}\n\nexport interface AttestResult {\n /** Whether the request passed verification */\n attested: boolean;\n /** Browser fingerprint (if attested) */\n fingerprint?: string;\n /** Verification timestamp */\n timestamp?: number;\n /** Reason for failure (if not attested) */\n reason?: string;\n /** Warning message (e.g., approaching rate limit) */\n warning?: string;\n /** Current usage stats */\n usage?: {\n used: number;\n limit: number;\n percent: number;\n };\n}\n\nexport interface AttestHeaders {\n timestamp: string | null;\n signature: string | null;\n fingerprint: string | null;\n project: string | null;\n}\n\n/**\n * Extract ATTEST headers from a request\n */\nexport function getAttestHeaders(request: Request): AttestHeaders {\n return {\n timestamp: request.headers.get(\"x-attest-timestamp\"),\n signature: request.headers.get(\"x-attest-signature\"),\n fingerprint: request.headers.get(\"x-attest-fingerprint\"),\n project: request.headers.get(\"x-attest-project\"),\n };\n}\n\n/**\n * Check if a request has ATTEST headers\n */\nexport function hasAttestHeaders(request: Request): boolean {\n const headers = getAttestHeaders(request);\n return !!(headers.timestamp && headers.signature && headers.fingerprint);\n}\n\n/**\n * Verify a request with ATTEST\n *\n * @example\n * ```ts\n * import { verifyAttest } from \"@sekyuriti/attest\";\n *\n * export async function POST(request: Request) {\n * const result = await verifyAttest(request, {\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * if (!result.attested) {\n * return Response.json({ error: \"Not attested\" }, { status: 403 });\n * }\n *\n * // ... handle request\n * }\n * ```\n */\nexport async function verifyAttest(\n request: Request,\n config: AttestConfig\n): Promise<AttestResult> {\n const headers = getAttestHeaders(request);\n\n // If no ATTEST headers, request is not attested\n if (!headers.timestamp || !headers.signature || !headers.fingerprint) {\n return {\n attested: false,\n reason: \"Missing ATTEST headers\",\n };\n }\n\n try {\n const response = await fetch(config.verifyUrl || ATTEST_VERIFY_URL, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n project_id: config.projectId,\n api_key: config.apiKey,\n timestamp: parseInt(headers.timestamp, 10),\n signature: headers.signature,\n fingerprint: headers.fingerprint,\n }),\n });\n\n if (!response.ok) {\n return {\n attested: false,\n reason: `Verification service error: ${response.status}`,\n };\n }\n\n return (await response.json()) as AttestResult;\n } catch (error) {\n // Fail open - don't break the app if ATTEST service is down\n console.error(\"[@sekyuriti/attest] Verification failed:\", error);\n return {\n attested: true,\n reason: \"Verification service unavailable\",\n };\n }\n}\n\n/**\n * Create a configured verifier function\n *\n * @example\n * ```ts\n * import { createAttestVerifier } from \"@sekyuriti/attest\";\n *\n * const verify = createAttestVerifier({\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * export async function POST(request: Request) {\n * const result = await verify(request);\n * if (!result.attested) {\n * return Response.json({ error: \"Not attested\" }, { status: 403 });\n * }\n * // ...\n * }\n * ```\n */\nexport function createAttestVerifier(config: AttestConfig) {\n return (request: Request) => verifyAttest(request, config);\n}\n"],"mappings":";AAOA,IAAM,oBAAoB;AAwCnB,SAAS,iBAAiB,SAAiC;AAChE,SAAO;AAAA,IACL,WAAW,QAAQ,QAAQ,IAAI,oBAAoB;AAAA,IACnD,WAAW,QAAQ,QAAQ,IAAI,oBAAoB;AAAA,IACnD,aAAa,QAAQ,QAAQ,IAAI,sBAAsB;AAAA,IACvD,SAAS,QAAQ,QAAQ,IAAI,kBAAkB;AAAA,EACjD;AACF;AAKO,SAAS,iBAAiB,SAA2B;AAC1D,QAAM,UAAU,iBAAiB,OAAO;AACxC,SAAO,CAAC,EAAE,QAAQ,aAAa,QAAQ,aAAa,QAAQ;AAC9D;AAuBA,eAAsB,aACpB,SACA,QACuB;AACvB,QAAM,UAAU,iBAAiB,OAAO;AAGxC,MAAI,CAAC,QAAQ,aAAa,CAAC,QAAQ,aAAa,CAAC,QAAQ,aAAa;AACpE,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,OAAO,aAAa,mBAAmB;AAAA,MAClE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU;AAAA,QACnB,YAAY,OAAO;AAAA,QACnB,SAAS,OAAO;AAAA,QAChB,WAAW,SAAS,QAAQ,WAAW,EAAE;AAAA,QACzC,WAAW,QAAQ;AAAA,QACnB,aAAa,QAAQ;AAAA,MACvB,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,aAAO;AAAA,QACL,UAAU;AAAA,QACV,QAAQ,+BAA+B,SAAS,MAAM;AAAA,MACxD;AAAA,IACF;AAEA,WAAQ,MAAM,SAAS,KAAK;AAAA,EAC9B,SAAS,OAAO;AAEd,YAAQ,MAAM,4CAA4C,KAAK;AAC/D,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,IACV;AAAA,EACF;AACF;AAuBO,SAAS,qBAAqB,QAAsB;AACzD,SAAO,CAAC,YAAqB,aAAa,SAAS,MAAM;AAC3D;","names":[]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { AttestConfig, AttestResult } from './index.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @sekyuriti/attest/middleware
|
|
6
|
+
*
|
|
7
|
+
* Next.js middleware for automatic ATTEST verification.
|
|
8
|
+
* Protects all matching routes with a single file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface AttestMiddlewareConfig extends AttestConfig {
|
|
12
|
+
/**
|
|
13
|
+
* Routes to protect (glob patterns)
|
|
14
|
+
* @default ["/api/*"]
|
|
15
|
+
*/
|
|
16
|
+
protectedRoutes?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Routes to exclude from protection (glob patterns)
|
|
19
|
+
* @default []
|
|
20
|
+
*/
|
|
21
|
+
excludeRoutes?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Allow requests without ATTEST headers (passthrough mode)
|
|
24
|
+
* Useful for gradual rollout
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
27
|
+
allowUnauthenticated?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Custom handler for blocked requests
|
|
30
|
+
*/
|
|
31
|
+
onBlocked?: (request: NextRequest, result: AttestResult) => Response | Promise<Response>;
|
|
32
|
+
/**
|
|
33
|
+
* Custom handler for allowed requests (for logging, etc.)
|
|
34
|
+
*/
|
|
35
|
+
onAllowed?: (request: NextRequest, result: AttestResult) => void | Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create ATTEST middleware for Next.js
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* // middleware.ts
|
|
43
|
+
* import { createAttestMiddleware } from "@sekyuriti/attest/middleware";
|
|
44
|
+
*
|
|
45
|
+
* export const middleware = createAttestMiddleware({
|
|
46
|
+
* projectId: process.env.ATTEST_PROJECT_ID!,
|
|
47
|
+
* apiKey: process.env.ATTEST_API_KEY!,
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* export const config = {
|
|
51
|
+
* matcher: "/api/:path*",
|
|
52
|
+
* };
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare function createAttestMiddleware(config: AttestMiddlewareConfig): (request: NextRequest) => Promise<Response>;
|
|
56
|
+
/**
|
|
57
|
+
* Simple middleware that uses environment variables
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* // middleware.ts
|
|
62
|
+
* export { attestMiddleware as middleware } from "@sekyuriti/attest/middleware";
|
|
63
|
+
*
|
|
64
|
+
* export const config = {
|
|
65
|
+
* matcher: "/api/:path*",
|
|
66
|
+
* };
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* Requires environment variables:
|
|
70
|
+
* - ATTEST_PROJECT_ID
|
|
71
|
+
* - ATTEST_API_KEY
|
|
72
|
+
*/
|
|
73
|
+
declare const attestMiddleware: (request: NextRequest) => Promise<Response>;
|
|
74
|
+
|
|
75
|
+
export { type AttestMiddlewareConfig, attestMiddleware, createAttestMiddleware };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { AttestConfig, AttestResult } from './index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @sekyuriti/attest/middleware
|
|
6
|
+
*
|
|
7
|
+
* Next.js middleware for automatic ATTEST verification.
|
|
8
|
+
* Protects all matching routes with a single file.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface AttestMiddlewareConfig extends AttestConfig {
|
|
12
|
+
/**
|
|
13
|
+
* Routes to protect (glob patterns)
|
|
14
|
+
* @default ["/api/*"]
|
|
15
|
+
*/
|
|
16
|
+
protectedRoutes?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Routes to exclude from protection (glob patterns)
|
|
19
|
+
* @default []
|
|
20
|
+
*/
|
|
21
|
+
excludeRoutes?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Allow requests without ATTEST headers (passthrough mode)
|
|
24
|
+
* Useful for gradual rollout
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
27
|
+
allowUnauthenticated?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Custom handler for blocked requests
|
|
30
|
+
*/
|
|
31
|
+
onBlocked?: (request: NextRequest, result: AttestResult) => Response | Promise<Response>;
|
|
32
|
+
/**
|
|
33
|
+
* Custom handler for allowed requests (for logging, etc.)
|
|
34
|
+
*/
|
|
35
|
+
onAllowed?: (request: NextRequest, result: AttestResult) => void | Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create ATTEST middleware for Next.js
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* // middleware.ts
|
|
43
|
+
* import { createAttestMiddleware } from "@sekyuriti/attest/middleware";
|
|
44
|
+
*
|
|
45
|
+
* export const middleware = createAttestMiddleware({
|
|
46
|
+
* projectId: process.env.ATTEST_PROJECT_ID!,
|
|
47
|
+
* apiKey: process.env.ATTEST_API_KEY!,
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* export const config = {
|
|
51
|
+
* matcher: "/api/:path*",
|
|
52
|
+
* };
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare function createAttestMiddleware(config: AttestMiddlewareConfig): (request: NextRequest) => Promise<Response>;
|
|
56
|
+
/**
|
|
57
|
+
* Simple middleware that uses environment variables
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* // middleware.ts
|
|
62
|
+
* export { attestMiddleware as middleware } from "@sekyuriti/attest/middleware";
|
|
63
|
+
*
|
|
64
|
+
* export const config = {
|
|
65
|
+
* matcher: "/api/:path*",
|
|
66
|
+
* };
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* Requires environment variables:
|
|
70
|
+
* - ATTEST_PROJECT_ID
|
|
71
|
+
* - ATTEST_API_KEY
|
|
72
|
+
*/
|
|
73
|
+
declare const attestMiddleware: (request: NextRequest) => Promise<Response>;
|
|
74
|
+
|
|
75
|
+
export { type AttestMiddlewareConfig, attestMiddleware, createAttestMiddleware };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/middleware.ts
|
|
21
|
+
var middleware_exports = {};
|
|
22
|
+
__export(middleware_exports, {
|
|
23
|
+
attestMiddleware: () => attestMiddleware,
|
|
24
|
+
createAttestMiddleware: () => createAttestMiddleware
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(middleware_exports);
|
|
27
|
+
var import_server = require("next/server");
|
|
28
|
+
|
|
29
|
+
// src/index.ts
|
|
30
|
+
var ATTEST_VERIFY_URL = "https://sekyuriti.build/api/v2/attest/verify";
|
|
31
|
+
function getAttestHeaders(request) {
|
|
32
|
+
return {
|
|
33
|
+
timestamp: request.headers.get("x-attest-timestamp"),
|
|
34
|
+
signature: request.headers.get("x-attest-signature"),
|
|
35
|
+
fingerprint: request.headers.get("x-attest-fingerprint"),
|
|
36
|
+
project: request.headers.get("x-attest-project")
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function verifyAttest(request, config) {
|
|
40
|
+
const headers = getAttestHeaders(request);
|
|
41
|
+
if (!headers.timestamp || !headers.signature || !headers.fingerprint) {
|
|
42
|
+
return {
|
|
43
|
+
attested: false,
|
|
44
|
+
reason: "Missing ATTEST headers"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(config.verifyUrl || ATTEST_VERIFY_URL, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: {
|
|
51
|
+
"Content-Type": "application/json"
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
project_id: config.projectId,
|
|
55
|
+
api_key: config.apiKey,
|
|
56
|
+
timestamp: parseInt(headers.timestamp, 10),
|
|
57
|
+
signature: headers.signature,
|
|
58
|
+
fingerprint: headers.fingerprint
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
return {
|
|
63
|
+
attested: false,
|
|
64
|
+
reason: `Verification service error: ${response.status}`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return await response.json();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error("[@sekyuriti/attest] Verification failed:", error);
|
|
70
|
+
return {
|
|
71
|
+
attested: true,
|
|
72
|
+
reason: "Verification service unavailable"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/middleware.ts
|
|
78
|
+
function matchesPattern(path, pattern) {
|
|
79
|
+
const regexPattern = pattern.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
80
|
+
return new RegExp(`^${regexPattern}$`).test(path);
|
|
81
|
+
}
|
|
82
|
+
function matchesAnyPattern(path, patterns) {
|
|
83
|
+
return patterns.some((pattern) => matchesPattern(path, pattern));
|
|
84
|
+
}
|
|
85
|
+
function createAttestMiddleware(config) {
|
|
86
|
+
const {
|
|
87
|
+
protectedRoutes = ["/api/*"],
|
|
88
|
+
excludeRoutes = [],
|
|
89
|
+
allowUnauthenticated = false,
|
|
90
|
+
onBlocked,
|
|
91
|
+
onAllowed,
|
|
92
|
+
...attestConfig
|
|
93
|
+
} = config;
|
|
94
|
+
return async function middleware(request) {
|
|
95
|
+
const path = request.nextUrl.pathname;
|
|
96
|
+
const isProtected = matchesAnyPattern(path, protectedRoutes);
|
|
97
|
+
const isExcluded = matchesAnyPattern(path, excludeRoutes);
|
|
98
|
+
if (!isProtected || isExcluded) {
|
|
99
|
+
return import_server.NextResponse.next();
|
|
100
|
+
}
|
|
101
|
+
const result = await verifyAttest(request, attestConfig);
|
|
102
|
+
if (!result.attested) {
|
|
103
|
+
if (allowUnauthenticated && result.reason === "Missing ATTEST headers") {
|
|
104
|
+
return import_server.NextResponse.next();
|
|
105
|
+
}
|
|
106
|
+
if (onBlocked) {
|
|
107
|
+
return onBlocked(request, result);
|
|
108
|
+
}
|
|
109
|
+
return import_server.NextResponse.json(
|
|
110
|
+
{
|
|
111
|
+
error: "Request not attested",
|
|
112
|
+
reason: result.reason
|
|
113
|
+
},
|
|
114
|
+
{ status: 403 }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (onAllowed) {
|
|
118
|
+
await onAllowed(request, result);
|
|
119
|
+
}
|
|
120
|
+
const response = import_server.NextResponse.next();
|
|
121
|
+
if (result.fingerprint) {
|
|
122
|
+
response.headers.set("x-attest-verified-fingerprint", result.fingerprint);
|
|
123
|
+
}
|
|
124
|
+
return response;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
var attestMiddleware = createAttestMiddleware({
|
|
128
|
+
projectId: process.env.ATTEST_PROJECT_ID || "",
|
|
129
|
+
apiKey: process.env.ATTEST_API_KEY || ""
|
|
130
|
+
});
|
|
131
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
132
|
+
0 && (module.exports = {
|
|
133
|
+
attestMiddleware,
|
|
134
|
+
createAttestMiddleware
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/index.ts"],"sourcesContent":["/**\n * @sekyuriti/attest/middleware\n *\n * Next.js middleware for automatic ATTEST verification.\n * Protects all matching routes with a single file.\n */\n\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\nimport { verifyAttest, type AttestConfig, type AttestResult } from \"./index\";\n\nexport interface AttestMiddlewareConfig extends AttestConfig {\n /**\n * Routes to protect (glob patterns)\n * @default [\"/api/*\"]\n */\n protectedRoutes?: string[];\n\n /**\n * Routes to exclude from protection (glob patterns)\n * @default []\n */\n excludeRoutes?: string[];\n\n /**\n * Allow requests without ATTEST headers (passthrough mode)\n * Useful for gradual rollout\n * @default false\n */\n allowUnauthenticated?: boolean;\n\n /**\n * Custom handler for blocked requests\n */\n onBlocked?: (request: NextRequest, result: AttestResult) => Response | Promise<Response>;\n\n /**\n * Custom handler for allowed requests (for logging, etc.)\n */\n onAllowed?: (request: NextRequest, result: AttestResult) => void | Promise<void>;\n}\n\n/**\n * Check if a path matches a glob pattern\n */\nfunction matchesPattern(path: string, pattern: string): boolean {\n // Convert glob to regex\n const regexPattern = pattern\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${regexPattern}$`).test(path);\n}\n\n/**\n * Check if a path matches any of the patterns\n */\nfunction matchesAnyPattern(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchesPattern(path, pattern));\n}\n\n/**\n * Create ATTEST middleware for Next.js\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createAttestMiddleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const middleware = createAttestMiddleware({\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n */\nexport function createAttestMiddleware(config: AttestMiddlewareConfig) {\n const {\n protectedRoutes = [\"/api/*\"],\n excludeRoutes = [],\n allowUnauthenticated = false,\n onBlocked,\n onAllowed,\n ...attestConfig\n } = config;\n\n return async function middleware(request: NextRequest) {\n const path = request.nextUrl.pathname;\n\n // Check if this path should be protected\n const isProtected = matchesAnyPattern(path, protectedRoutes);\n const isExcluded = matchesAnyPattern(path, excludeRoutes);\n\n if (!isProtected || isExcluded) {\n return NextResponse.next();\n }\n\n // Verify with ATTEST\n const result = await verifyAttest(request, attestConfig);\n\n if (!result.attested) {\n // Allow through if configured and no headers present\n if (allowUnauthenticated && result.reason === \"Missing ATTEST headers\") {\n return NextResponse.next();\n }\n\n // Custom blocked handler\n if (onBlocked) {\n return onBlocked(request, result);\n }\n\n // Default blocked response\n return NextResponse.json(\n {\n error: \"Request not attested\",\n reason: result.reason,\n },\n { status: 403 }\n );\n }\n\n // Call allowed handler if provided\n if (onAllowed) {\n await onAllowed(request, result);\n }\n\n // Add fingerprint to headers for downstream use\n const response = NextResponse.next();\n if (result.fingerprint) {\n response.headers.set(\"x-attest-verified-fingerprint\", result.fingerprint);\n }\n\n return response;\n };\n}\n\n/**\n * Simple middleware that uses environment variables\n *\n * @example\n * ```ts\n * // middleware.ts\n * export { attestMiddleware as middleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n *\n * Requires environment variables:\n * - ATTEST_PROJECT_ID\n * - ATTEST_API_KEY\n */\nexport const attestMiddleware = createAttestMiddleware({\n projectId: process.env.ATTEST_PROJECT_ID || \"\",\n apiKey: process.env.ATTEST_API_KEY || \"\",\n});\n","/**\n * @sekyuriti/attest\n *\n * API protection for Next.js applications.\n * Verify that requests come from real browsers, not bots or scripts.\n */\n\nconst ATTEST_VERIFY_URL = \"https://sekyuriti.build/api/v2/attest/verify\";\n\nexport interface AttestConfig {\n /** Your ATTEST project ID (starts with ATST_) */\n projectId: string;\n /** Your ATTEST API key (keep this secret, server-side only) */\n apiKey: string;\n /** Custom verify URL (optional, for self-hosted) */\n verifyUrl?: string;\n}\n\nexport interface AttestResult {\n /** Whether the request passed verification */\n attested: boolean;\n /** Browser fingerprint (if attested) */\n fingerprint?: string;\n /** Verification timestamp */\n timestamp?: number;\n /** Reason for failure (if not attested) */\n reason?: string;\n /** Warning message (e.g., approaching rate limit) */\n warning?: string;\n /** Current usage stats */\n usage?: {\n used: number;\n limit: number;\n percent: number;\n };\n}\n\nexport interface AttestHeaders {\n timestamp: string | null;\n signature: string | null;\n fingerprint: string | null;\n project: string | null;\n}\n\n/**\n * Extract ATTEST headers from a request\n */\nexport function getAttestHeaders(request: Request): AttestHeaders {\n return {\n timestamp: request.headers.get(\"x-attest-timestamp\"),\n signature: request.headers.get(\"x-attest-signature\"),\n fingerprint: request.headers.get(\"x-attest-fingerprint\"),\n project: request.headers.get(\"x-attest-project\"),\n };\n}\n\n/**\n * Check if a request has ATTEST headers\n */\nexport function hasAttestHeaders(request: Request): boolean {\n const headers = getAttestHeaders(request);\n return !!(headers.timestamp && headers.signature && headers.fingerprint);\n}\n\n/**\n * Verify a request with ATTEST\n *\n * @example\n * ```ts\n * import { verifyAttest } from \"@sekyuriti/attest\";\n *\n * export async function POST(request: Request) {\n * const result = await verifyAttest(request, {\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * if (!result.attested) {\n * return Response.json({ error: \"Not attested\" }, { status: 403 });\n * }\n *\n * // ... handle request\n * }\n * ```\n */\nexport async function verifyAttest(\n request: Request,\n config: AttestConfig\n): Promise<AttestResult> {\n const headers = getAttestHeaders(request);\n\n // If no ATTEST headers, request is not attested\n if (!headers.timestamp || !headers.signature || !headers.fingerprint) {\n return {\n attested: false,\n reason: \"Missing ATTEST headers\",\n };\n }\n\n try {\n const response = await fetch(config.verifyUrl || ATTEST_VERIFY_URL, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n project_id: config.projectId,\n api_key: config.apiKey,\n timestamp: parseInt(headers.timestamp, 10),\n signature: headers.signature,\n fingerprint: headers.fingerprint,\n }),\n });\n\n if (!response.ok) {\n return {\n attested: false,\n reason: `Verification service error: ${response.status}`,\n };\n }\n\n return (await response.json()) as AttestResult;\n } catch (error) {\n // Fail open - don't break the app if ATTEST service is down\n console.error(\"[@sekyuriti/attest] Verification failed:\", error);\n return {\n attested: true,\n reason: \"Verification service unavailable\",\n };\n }\n}\n\n/**\n * Create a configured verifier function\n *\n * @example\n * ```ts\n * import { createAttestVerifier } from \"@sekyuriti/attest\";\n *\n * const verify = createAttestVerifier({\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * export async function POST(request: Request) {\n * const result = await verify(request);\n * if (!result.attested) {\n * return Response.json({ error: \"Not attested\" }, { status: 403 });\n * }\n * // ...\n * }\n * ```\n */\nexport function createAttestVerifier(config: AttestConfig) {\n return (request: Request) => verifyAttest(request, config);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var ATTEST_VERIFY_URL = "https://sekyuriti.build/api/v2/attest/verify";
|
|
6
|
+
function getAttestHeaders(request) {
|
|
7
|
+
return {
|
|
8
|
+
timestamp: request.headers.get("x-attest-timestamp"),
|
|
9
|
+
signature: request.headers.get("x-attest-signature"),
|
|
10
|
+
fingerprint: request.headers.get("x-attest-fingerprint"),
|
|
11
|
+
project: request.headers.get("x-attest-project")
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async function verifyAttest(request, config) {
|
|
15
|
+
const headers = getAttestHeaders(request);
|
|
16
|
+
if (!headers.timestamp || !headers.signature || !headers.fingerprint) {
|
|
17
|
+
return {
|
|
18
|
+
attested: false,
|
|
19
|
+
reason: "Missing ATTEST headers"
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(config.verifyUrl || ATTEST_VERIFY_URL, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json"
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
project_id: config.projectId,
|
|
30
|
+
api_key: config.apiKey,
|
|
31
|
+
timestamp: parseInt(headers.timestamp, 10),
|
|
32
|
+
signature: headers.signature,
|
|
33
|
+
fingerprint: headers.fingerprint
|
|
34
|
+
})
|
|
35
|
+
});
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
return {
|
|
38
|
+
attested: false,
|
|
39
|
+
reason: `Verification service error: ${response.status}`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return await response.json();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("[@sekyuriti/attest] Verification failed:", error);
|
|
45
|
+
return {
|
|
46
|
+
attested: true,
|
|
47
|
+
reason: "Verification service unavailable"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/middleware.ts
|
|
53
|
+
function matchesPattern(path, pattern) {
|
|
54
|
+
const regexPattern = pattern.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
55
|
+
return new RegExp(`^${regexPattern}$`).test(path);
|
|
56
|
+
}
|
|
57
|
+
function matchesAnyPattern(path, patterns) {
|
|
58
|
+
return patterns.some((pattern) => matchesPattern(path, pattern));
|
|
59
|
+
}
|
|
60
|
+
function createAttestMiddleware(config) {
|
|
61
|
+
const {
|
|
62
|
+
protectedRoutes = ["/api/*"],
|
|
63
|
+
excludeRoutes = [],
|
|
64
|
+
allowUnauthenticated = false,
|
|
65
|
+
onBlocked,
|
|
66
|
+
onAllowed,
|
|
67
|
+
...attestConfig
|
|
68
|
+
} = config;
|
|
69
|
+
return async function middleware(request) {
|
|
70
|
+
const path = request.nextUrl.pathname;
|
|
71
|
+
const isProtected = matchesAnyPattern(path, protectedRoutes);
|
|
72
|
+
const isExcluded = matchesAnyPattern(path, excludeRoutes);
|
|
73
|
+
if (!isProtected || isExcluded) {
|
|
74
|
+
return NextResponse.next();
|
|
75
|
+
}
|
|
76
|
+
const result = await verifyAttest(request, attestConfig);
|
|
77
|
+
if (!result.attested) {
|
|
78
|
+
if (allowUnauthenticated && result.reason === "Missing ATTEST headers") {
|
|
79
|
+
return NextResponse.next();
|
|
80
|
+
}
|
|
81
|
+
if (onBlocked) {
|
|
82
|
+
return onBlocked(request, result);
|
|
83
|
+
}
|
|
84
|
+
return NextResponse.json(
|
|
85
|
+
{
|
|
86
|
+
error: "Request not attested",
|
|
87
|
+
reason: result.reason
|
|
88
|
+
},
|
|
89
|
+
{ status: 403 }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (onAllowed) {
|
|
93
|
+
await onAllowed(request, result);
|
|
94
|
+
}
|
|
95
|
+
const response = NextResponse.next();
|
|
96
|
+
if (result.fingerprint) {
|
|
97
|
+
response.headers.set("x-attest-verified-fingerprint", result.fingerprint);
|
|
98
|
+
}
|
|
99
|
+
return response;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
var attestMiddleware = createAttestMiddleware({
|
|
103
|
+
projectId: process.env.ATTEST_PROJECT_ID || "",
|
|
104
|
+
apiKey: process.env.ATTEST_API_KEY || ""
|
|
105
|
+
});
|
|
106
|
+
export {
|
|
107
|
+
attestMiddleware,
|
|
108
|
+
createAttestMiddleware
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=middleware.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/index.ts"],"sourcesContent":["/**\n * @sekyuriti/attest/middleware\n *\n * Next.js middleware for automatic ATTEST verification.\n * Protects all matching routes with a single file.\n */\n\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\nimport { verifyAttest, type AttestConfig, type AttestResult } from \"./index\";\n\nexport interface AttestMiddlewareConfig extends AttestConfig {\n /**\n * Routes to protect (glob patterns)\n * @default [\"/api/*\"]\n */\n protectedRoutes?: string[];\n\n /**\n * Routes to exclude from protection (glob patterns)\n * @default []\n */\n excludeRoutes?: string[];\n\n /**\n * Allow requests without ATTEST headers (passthrough mode)\n * Useful for gradual rollout\n * @default false\n */\n allowUnauthenticated?: boolean;\n\n /**\n * Custom handler for blocked requests\n */\n onBlocked?: (request: NextRequest, result: AttestResult) => Response | Promise<Response>;\n\n /**\n * Custom handler for allowed requests (for logging, etc.)\n */\n onAllowed?: (request: NextRequest, result: AttestResult) => void | Promise<void>;\n}\n\n/**\n * Check if a path matches a glob pattern\n */\nfunction matchesPattern(path: string, pattern: string): boolean {\n // Convert glob to regex\n const regexPattern = pattern\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${regexPattern}$`).test(path);\n}\n\n/**\n * Check if a path matches any of the patterns\n */\nfunction matchesAnyPattern(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchesPattern(path, pattern));\n}\n\n/**\n * Create ATTEST middleware for Next.js\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createAttestMiddleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const middleware = createAttestMiddleware({\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n */\nexport function createAttestMiddleware(config: AttestMiddlewareConfig) {\n const {\n protectedRoutes = [\"/api/*\"],\n excludeRoutes = [],\n allowUnauthenticated = false,\n onBlocked,\n onAllowed,\n ...attestConfig\n } = config;\n\n return async function middleware(request: NextRequest) {\n const path = request.nextUrl.pathname;\n\n // Check if this path should be protected\n const isProtected = matchesAnyPattern(path, protectedRoutes);\n const isExcluded = matchesAnyPattern(path, excludeRoutes);\n\n if (!isProtected || isExcluded) {\n return NextResponse.next();\n }\n\n // Verify with ATTEST\n const result = await verifyAttest(request, attestConfig);\n\n if (!result.attested) {\n // Allow through if configured and no headers present\n if (allowUnauthenticated && result.reason === \"Missing ATTEST headers\") {\n return NextResponse.next();\n }\n\n // Custom blocked handler\n if (onBlocked) {\n return onBlocked(request, result);\n }\n\n // Default blocked response\n return NextResponse.json(\n {\n error: \"Request not attested\",\n reason: result.reason,\n },\n { status: 403 }\n );\n }\n\n // Call allowed handler if provided\n if (onAllowed) {\n await onAllowed(request, result);\n }\n\n // Add fingerprint to headers for downstream use\n const response = NextResponse.next();\n if (result.fingerprint) {\n response.headers.set(\"x-attest-verified-fingerprint\", result.fingerprint);\n }\n\n return response;\n };\n}\n\n/**\n * Simple middleware that uses environment variables\n *\n * @example\n * ```ts\n * // middleware.ts\n * export { attestMiddleware as middleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n *\n * Requires environment variables:\n * - ATTEST_PROJECT_ID\n * - ATTEST_API_KEY\n */\nexport const attestMiddleware = createAttestMiddleware({\n projectId: process.env.ATTEST_PROJECT_ID || \"\",\n apiKey: process.env.ATTEST_API_KEY || \"\",\n});\n","/**\n * @sekyuriti/attest\n *\n * API protection for Next.js applications.\n * Verify that requests come from real browsers, not bots or scripts.\n */\n\nconst ATTEST_VERIFY_URL = \"https://sekyuriti.build/api/v2/attest/verify\";\n\nexport interface AttestConfig {\n /** Your ATTEST project ID (starts with ATST_) */\n projectId: string;\n /** Your ATTEST API key (keep this secret, server-side only) */\n apiKey: string;\n /** Custom verify URL (optional, for self-hosted) */\n verifyUrl?: string;\n}\n\nexport interface AttestResult {\n /** Whether the request passed verification */\n attested: boolean;\n /** Browser fingerprint (if attested) */\n fingerprint?: string;\n /** Verification timestamp */\n timestamp?: number;\n /** Reason for failure (if not attested) */\n reason?: string;\n /** Warning message (e.g., approaching rate limit) */\n warning?: string;\n /** Current usage stats */\n usage?: {\n used: number;\n limit: number;\n percent: number;\n };\n}\n\nexport interface AttestHeaders {\n timestamp: string | null;\n signature: string | null;\n fingerprint: string | null;\n project: string | null;\n}\n\n/**\n * Extract ATTEST headers from a request\n */\nexport function getAttestHeaders(request: Request): AttestHeaders {\n return {\n timestamp: request.headers.get(\"x-attest-timestamp\"),\n signature: request.headers.get(\"x-attest-signature\"),\n fingerprint: request.headers.get(\"x-attest-fingerprint\"),\n project: request.headers.get(\"x-attest-project\"),\n };\n}\n\n/**\n * Check if a request has ATTEST headers\n */\nexport function hasAttestHeaders(request: Request): boolean {\n const headers = getAttestHeaders(request);\n return !!(headers.timestamp && headers.signature && headers.fingerprint);\n}\n\n/**\n * Verify a request with ATTEST\n *\n * @example\n * ```ts\n * import { verifyAttest } from \"@sekyuriti/attest\";\n *\n * export async function POST(request: Request) {\n * const result = await verifyAttest(request, {\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * if (!result.attested) {\n * return Response.json({ error: \"Not attested\" }, { status: 403 });\n * }\n *\n * // ... handle request\n * }\n * ```\n */\nexport async function verifyAttest(\n request: Request,\n config: AttestConfig\n): Promise<AttestResult> {\n const headers = getAttestHeaders(request);\n\n // If no ATTEST headers, request is not attested\n if (!headers.timestamp || !headers.signature || !headers.fingerprint) {\n return {\n attested: false,\n reason: \"Missing ATTEST headers\",\n };\n }\n\n try {\n const response = await fetch(config.verifyUrl || ATTEST_VERIFY_URL, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n project_id: config.projectId,\n api_key: config.apiKey,\n timestamp: parseInt(headers.timestamp, 10),\n signature: headers.signature,\n fingerprint: headers.fingerprint,\n }),\n });\n\n if (!response.ok) {\n return {\n attested: false,\n reason: `Verification service error: ${response.status}`,\n };\n }\n\n return (await response.json()) as AttestResult;\n } catch (error) {\n // Fail open - don't break the app if ATTEST service is down\n console.error(\"[@sekyuriti/attest] Verification failed:\", error);\n return {\n attested: true,\n reason: \"Verification service unavailable\",\n };\n }\n}\n\n/**\n * Create a configured verifier function\n *\n * @example\n * ```ts\n * import { createAttestVerifier } from \"@sekyuriti/attest\";\n *\n * const verify = createAttestVerifier({\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * export async function POST(request: Request) {\n * const result = await verify(request);\n * if (!result.attested) {\n * return Response.json({ error: \"Not attested\" }, { status: 403 });\n * }\n * // ...\n * }\n * ```\n */\nexport function createAttestVerifier(config: AttestConfig) {\n return (request: Request) => verifyAttest(request, config);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sekyuriti/attest",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "API protection middleware for Next.js - verify requests with ATTEST",
|
|
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
|
+
"./middleware": {
|
|
15
|
+
"types": "./dist/middleware.d.ts",
|
|
16
|
+
"import": "./dist/middleware.mjs",
|
|
17
|
+
"require": "./dist/middleware.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup",
|
|
25
|
+
"dev": "tsup --watch",
|
|
26
|
+
"prepublishOnly": "npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"api-protection",
|
|
30
|
+
"bot-detection",
|
|
31
|
+
"security",
|
|
32
|
+
"nextjs",
|
|
33
|
+
"middleware",
|
|
34
|
+
"attest",
|
|
35
|
+
"sekyuriti"
|
|
36
|
+
],
|
|
37
|
+
"author": "SEKYURITI",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/sekyuriti/attest"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://sekyuriti.build/attest",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"next": ">=13.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"next": "^15.0.0",
|
|
49
|
+
"tsup": "^8.0.0",
|
|
50
|
+
"typescript": "^5.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|