@sekyuriti/attest 0.2.7 → 0.4.1
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/bin/attest.js +50 -6
- package/dist/index.d.mts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +154 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +153 -29
- package/dist/index.mjs.map +1 -1
- package/dist/middleware.d.mts +5 -3
- package/dist/middleware.d.ts +5 -3
- package/dist/middleware.js +151 -33
- package/dist/middleware.js.map +1 -1
- package/dist/middleware.mjs +151 -33
- package/dist/middleware.mjs.map +1 -1
- package/package.json +3 -5
package/bin/attest.js
CHANGED
|
@@ -17,6 +17,8 @@ const c = {
|
|
|
17
17
|
black: "\x1b[30m",
|
|
18
18
|
white: "\x1b[37m",
|
|
19
19
|
gray: "\x1b[90m",
|
|
20
|
+
green: "\x1b[32m",
|
|
21
|
+
yellow: "\x1b[33m",
|
|
20
22
|
inverse: "\x1b[7m",
|
|
21
23
|
};
|
|
22
24
|
|
|
@@ -208,7 +210,7 @@ function printHeader() {
|
|
|
208
210
|
logBold(" █▀▀ █▀▀ █▄▀ █▄█ █ █ █▀█ █ ▀█▀ █");
|
|
209
211
|
logBold(" ▄▄█ ██▄ █ █ █ █▄█ █▀▄ █ █ █");
|
|
210
212
|
log("");
|
|
211
|
-
logDim(" ATTEST CLI v0.
|
|
213
|
+
logDim(" ATTEST CLI v0.3.0");
|
|
212
214
|
log("");
|
|
213
215
|
}
|
|
214
216
|
|
|
@@ -352,11 +354,26 @@ ATTEST_SECRET_KEY=${apiKey}
|
|
|
352
354
|
// Check if there's already an import statement
|
|
353
355
|
const hasImports = /import\s/.test(middlewareContent);
|
|
354
356
|
if (hasImports) {
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
)
|
|
357
|
+
// Find all import lines and add after the last one
|
|
358
|
+
const lines = middlewareContent.split('\n');
|
|
359
|
+
let lastImportIndex = -1;
|
|
360
|
+
|
|
361
|
+
for (let i = 0; i < lines.length; i++) {
|
|
362
|
+
// Match import statements (including multiline imports)
|
|
363
|
+
if (lines[i].trim().startsWith('import ') ||
|
|
364
|
+
(lines[i].includes(' from ') && lines[i].includes(';'))) {
|
|
365
|
+
lastImportIndex = i;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (lastImportIndex !== -1) {
|
|
370
|
+
// Insert after the last import line
|
|
371
|
+
lines.splice(lastImportIndex + 1, 0, attestImport.trim());
|
|
372
|
+
middlewareContent = lines.join('\n');
|
|
373
|
+
} else {
|
|
374
|
+
// Fallback: add at beginning
|
|
375
|
+
middlewareContent = attestImport + middlewareContent;
|
|
376
|
+
}
|
|
360
377
|
} else {
|
|
361
378
|
// Add at the beginning
|
|
362
379
|
middlewareContent = attestImport + "\n" + middlewareContent;
|
|
@@ -471,6 +488,33 @@ export const config = {
|
|
|
471
488
|
log(` ${c.bold}✓${c.reset} ${step}`);
|
|
472
489
|
}
|
|
473
490
|
|
|
491
|
+
// Verify script endpoint is reachable
|
|
492
|
+
log("");
|
|
493
|
+
log(" Verifying endpoint...");
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
const scriptUrl = `https://sekyuriti.build/api/v2/attest/script/${publicKey}`;
|
|
497
|
+
const response = await fetch(scriptUrl, { method: "HEAD" });
|
|
498
|
+
|
|
499
|
+
if (response.ok) {
|
|
500
|
+
log(` ${c.bold}✓${c.reset} Script endpoint verified`);
|
|
501
|
+
} else if (response.status === 404) {
|
|
502
|
+
log(` ${c.yellow}⚠${c.reset} Script endpoint returned 404`);
|
|
503
|
+
log("");
|
|
504
|
+
log(" This may indicate:");
|
|
505
|
+
log(" • The project hasn't been fully provisioned yet");
|
|
506
|
+
log(" • ATTEST is not enabled for this project");
|
|
507
|
+
log("");
|
|
508
|
+
log(" Enable ATTEST in your project dashboard:");
|
|
509
|
+
log(" https://sekyuriti.build/dashboard");
|
|
510
|
+
} else {
|
|
511
|
+
log(` ${c.yellow}⚠${c.reset} Script endpoint returned ${response.status}`);
|
|
512
|
+
}
|
|
513
|
+
} catch (err) {
|
|
514
|
+
log(` ${c.yellow}⚠${c.reset} Could not verify endpoint (${err.message})`);
|
|
515
|
+
log(" Check your network connection and try again.");
|
|
516
|
+
}
|
|
517
|
+
|
|
474
518
|
log("");
|
|
475
519
|
log(" Documentation: https://sekyuriti.build/docs/attest");
|
|
476
520
|
log("");
|
package/dist/index.d.mts
CHANGED
|
@@ -18,6 +18,21 @@ interface AttestConfig {
|
|
|
18
18
|
* @default "open"
|
|
19
19
|
*/
|
|
20
20
|
failMode?: "open" | "closed";
|
|
21
|
+
/**
|
|
22
|
+
* Timeout in milliseconds for verification requests.
|
|
23
|
+
* @default 5000
|
|
24
|
+
*/
|
|
25
|
+
timeout?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Skip verification for localhost requests (for development).
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
allowLocalhost?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Enable debug logging.
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
debug?: boolean;
|
|
21
36
|
}
|
|
22
37
|
interface AttestResult {
|
|
23
38
|
/** Whether the request passed verification */
|
|
@@ -36,7 +51,12 @@ interface AttestResult {
|
|
|
36
51
|
limit: number;
|
|
37
52
|
percent: number;
|
|
38
53
|
};
|
|
54
|
+
/** Whether this was a bypass (localhost, etc.) */
|
|
55
|
+
bypass?: boolean;
|
|
56
|
+
/** Error code for programmatic handling */
|
|
57
|
+
errorCode?: AttestErrorCode;
|
|
39
58
|
}
|
|
59
|
+
type AttestErrorCode = "MISSING_HEADERS" | "INVALID_TIMESTAMP" | "INVALID_SIGNATURE" | "EXPIRED_TIMESTAMP" | "SERVICE_ERROR" | "SERVICE_TIMEOUT" | "SERVICE_UNAVAILABLE" | "RATE_LIMITED" | "INVALID_PROJECT" | "INVALID_API_KEY";
|
|
40
60
|
interface AttestHeaders {
|
|
41
61
|
timestamp: string | null;
|
|
42
62
|
signature: string | null;
|
|
@@ -61,7 +81,7 @@ declare function hasAttestHeaders(request: Request): boolean;
|
|
|
61
81
|
* export async function POST(request: Request) {
|
|
62
82
|
* const result = await verifyAttest(request, {
|
|
63
83
|
* projectId: process.env.ATTEST_PROJECT_ID!,
|
|
64
|
-
* apiKey: process.env.
|
|
84
|
+
* apiKey: process.env.ATTEST_SECRET_KEY!,
|
|
65
85
|
* });
|
|
66
86
|
*
|
|
67
87
|
* if (!result.attested) {
|
|
@@ -82,7 +102,7 @@ declare function verifyAttest(request: Request, config: AttestConfig): Promise<A
|
|
|
82
102
|
*
|
|
83
103
|
* const verify = createAttestVerifier({
|
|
84
104
|
* projectId: process.env.ATTEST_PROJECT_ID!,
|
|
85
|
-
* apiKey: process.env.
|
|
105
|
+
* apiKey: process.env.ATTEST_SECRET_KEY!,
|
|
86
106
|
* });
|
|
87
107
|
*
|
|
88
108
|
* export async function POST(request: Request) {
|
|
@@ -95,5 +115,12 @@ declare function verifyAttest(request: Request, config: AttestConfig): Promise<A
|
|
|
95
115
|
* ```
|
|
96
116
|
*/
|
|
97
117
|
declare function createAttestVerifier(config: AttestConfig): (request: Request) => Promise<AttestResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Quick check if request should be verified
|
|
120
|
+
* Use this for early bailout in middleware
|
|
121
|
+
*/
|
|
122
|
+
declare function shouldVerify(request: Request, config?: {
|
|
123
|
+
allowLocalhost?: boolean;
|
|
124
|
+
}): boolean;
|
|
98
125
|
|
|
99
|
-
export { type AttestConfig, type AttestHeaders, type AttestResult, createAttestVerifier, getAttestHeaders, hasAttestHeaders, verifyAttest };
|
|
126
|
+
export { type AttestConfig, type AttestErrorCode, type AttestHeaders, type AttestResult, createAttestVerifier, getAttestHeaders, hasAttestHeaders, shouldVerify, verifyAttest };
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,21 @@ interface AttestConfig {
|
|
|
18
18
|
* @default "open"
|
|
19
19
|
*/
|
|
20
20
|
failMode?: "open" | "closed";
|
|
21
|
+
/**
|
|
22
|
+
* Timeout in milliseconds for verification requests.
|
|
23
|
+
* @default 5000
|
|
24
|
+
*/
|
|
25
|
+
timeout?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Skip verification for localhost requests (for development).
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
allowLocalhost?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Enable debug logging.
|
|
33
|
+
* @default false
|
|
34
|
+
*/
|
|
35
|
+
debug?: boolean;
|
|
21
36
|
}
|
|
22
37
|
interface AttestResult {
|
|
23
38
|
/** Whether the request passed verification */
|
|
@@ -36,7 +51,12 @@ interface AttestResult {
|
|
|
36
51
|
limit: number;
|
|
37
52
|
percent: number;
|
|
38
53
|
};
|
|
54
|
+
/** Whether this was a bypass (localhost, etc.) */
|
|
55
|
+
bypass?: boolean;
|
|
56
|
+
/** Error code for programmatic handling */
|
|
57
|
+
errorCode?: AttestErrorCode;
|
|
39
58
|
}
|
|
59
|
+
type AttestErrorCode = "MISSING_HEADERS" | "INVALID_TIMESTAMP" | "INVALID_SIGNATURE" | "EXPIRED_TIMESTAMP" | "SERVICE_ERROR" | "SERVICE_TIMEOUT" | "SERVICE_UNAVAILABLE" | "RATE_LIMITED" | "INVALID_PROJECT" | "INVALID_API_KEY";
|
|
40
60
|
interface AttestHeaders {
|
|
41
61
|
timestamp: string | null;
|
|
42
62
|
signature: string | null;
|
|
@@ -61,7 +81,7 @@ declare function hasAttestHeaders(request: Request): boolean;
|
|
|
61
81
|
* export async function POST(request: Request) {
|
|
62
82
|
* const result = await verifyAttest(request, {
|
|
63
83
|
* projectId: process.env.ATTEST_PROJECT_ID!,
|
|
64
|
-
* apiKey: process.env.
|
|
84
|
+
* apiKey: process.env.ATTEST_SECRET_KEY!,
|
|
65
85
|
* });
|
|
66
86
|
*
|
|
67
87
|
* if (!result.attested) {
|
|
@@ -82,7 +102,7 @@ declare function verifyAttest(request: Request, config: AttestConfig): Promise<A
|
|
|
82
102
|
*
|
|
83
103
|
* const verify = createAttestVerifier({
|
|
84
104
|
* projectId: process.env.ATTEST_PROJECT_ID!,
|
|
85
|
-
* apiKey: process.env.
|
|
105
|
+
* apiKey: process.env.ATTEST_SECRET_KEY!,
|
|
86
106
|
* });
|
|
87
107
|
*
|
|
88
108
|
* export async function POST(request: Request) {
|
|
@@ -95,5 +115,12 @@ declare function verifyAttest(request: Request, config: AttestConfig): Promise<A
|
|
|
95
115
|
* ```
|
|
96
116
|
*/
|
|
97
117
|
declare function createAttestVerifier(config: AttestConfig): (request: Request) => Promise<AttestResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Quick check if request should be verified
|
|
120
|
+
* Use this for early bailout in middleware
|
|
121
|
+
*/
|
|
122
|
+
declare function shouldVerify(request: Request, config?: {
|
|
123
|
+
allowLocalhost?: boolean;
|
|
124
|
+
}): boolean;
|
|
98
125
|
|
|
99
|
-
export { type AttestConfig, type AttestHeaders, type AttestResult, createAttestVerifier, getAttestHeaders, hasAttestHeaders, verifyAttest };
|
|
126
|
+
export { type AttestConfig, type AttestErrorCode, type AttestHeaders, type AttestResult, createAttestVerifier, getAttestHeaders, hasAttestHeaders, shouldVerify, verifyAttest };
|
package/dist/index.js
CHANGED
|
@@ -23,10 +23,18 @@ __export(src_exports, {
|
|
|
23
23
|
createAttestVerifier: () => createAttestVerifier,
|
|
24
24
|
getAttestHeaders: () => getAttestHeaders,
|
|
25
25
|
hasAttestHeaders: () => hasAttestHeaders,
|
|
26
|
+
shouldVerify: () => shouldVerify,
|
|
26
27
|
verifyAttest: () => verifyAttest
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(src_exports);
|
|
29
30
|
var ATTEST_VERIFY_URL = "https://attest.sekyuriti.build/verify";
|
|
31
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
32
|
+
var LOCALHOST_IPS = [
|
|
33
|
+
"127.0.0.1",
|
|
34
|
+
"::1",
|
|
35
|
+
"localhost",
|
|
36
|
+
"::ffff:127.0.0.1"
|
|
37
|
+
];
|
|
30
38
|
function getAttestHeaders(request) {
|
|
31
39
|
return {
|
|
32
40
|
timestamp: request.headers.get("x-attest-timestamp"),
|
|
@@ -39,60 +47,177 @@ function hasAttestHeaders(request) {
|
|
|
39
47
|
const headers = getAttestHeaders(request);
|
|
40
48
|
return !!(headers.timestamp && headers.signature && headers.fingerprint);
|
|
41
49
|
}
|
|
50
|
+
function getClientIP(request) {
|
|
51
|
+
const forwardedFor = request.headers.get("x-forwarded-for");
|
|
52
|
+
if (forwardedFor) {
|
|
53
|
+
return forwardedFor.split(",")[0].trim();
|
|
54
|
+
}
|
|
55
|
+
const realIP = request.headers.get("x-real-ip");
|
|
56
|
+
if (realIP) return realIP;
|
|
57
|
+
const cfIP = request.headers.get("cf-connecting-ip");
|
|
58
|
+
if (cfIP) return cfIP;
|
|
59
|
+
const vercelIP = request.headers.get("x-vercel-forwarded-for");
|
|
60
|
+
if (vercelIP) return vercelIP.split(",")[0].trim();
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function isLocalhostIP(ip) {
|
|
64
|
+
if (!ip) return false;
|
|
65
|
+
return LOCALHOST_IPS.includes(ip);
|
|
66
|
+
}
|
|
67
|
+
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
...options,
|
|
73
|
+
signal: controller.signal
|
|
74
|
+
});
|
|
75
|
+
return response;
|
|
76
|
+
} finally {
|
|
77
|
+
clearTimeout(timeoutId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
42
80
|
async function verifyAttest(request, config) {
|
|
81
|
+
const {
|
|
82
|
+
projectId,
|
|
83
|
+
apiKey,
|
|
84
|
+
verifyUrl = ATTEST_VERIFY_URL,
|
|
85
|
+
failMode = "open",
|
|
86
|
+
timeout = DEFAULT_TIMEOUT_MS,
|
|
87
|
+
allowLocalhost = true,
|
|
88
|
+
debug = false
|
|
89
|
+
} = config;
|
|
90
|
+
const log = debug ? console.log.bind(console, "[@sekyuriti/attest]") : () => {
|
|
91
|
+
};
|
|
92
|
+
if (allowLocalhost) {
|
|
93
|
+
const clientIP = getClientIP(request);
|
|
94
|
+
if (isLocalhostIP(clientIP)) {
|
|
95
|
+
log("Bypassing verification for localhost IP:", clientIP);
|
|
96
|
+
return {
|
|
97
|
+
attested: true,
|
|
98
|
+
reason: "Localhost bypass",
|
|
99
|
+
bypass: true
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
43
103
|
const headers = getAttestHeaders(request);
|
|
44
104
|
if (!headers.timestamp || !headers.signature || !headers.fingerprint) {
|
|
105
|
+
log("Missing ATTEST headers");
|
|
45
106
|
return {
|
|
46
107
|
attested: false,
|
|
47
|
-
reason: "Missing ATTEST headers"
|
|
108
|
+
reason: "Missing ATTEST headers",
|
|
109
|
+
errorCode: "MISSING_HEADERS"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const timestamp = parseInt(headers.timestamp, 10);
|
|
113
|
+
if (isNaN(timestamp)) {
|
|
114
|
+
log("Invalid timestamp format");
|
|
115
|
+
return {
|
|
116
|
+
attested: false,
|
|
117
|
+
reason: "Invalid timestamp format",
|
|
118
|
+
errorCode: "INVALID_TIMESTAMP"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
const maxAge = 5 * 60 * 1e3;
|
|
123
|
+
if (now - timestamp > maxAge) {
|
|
124
|
+
log("Timestamp expired:", { timestamp, now, diff: now - timestamp });
|
|
125
|
+
return {
|
|
126
|
+
attested: false,
|
|
127
|
+
reason: "Request timestamp expired",
|
|
128
|
+
errorCode: "EXPIRED_TIMESTAMP"
|
|
48
129
|
};
|
|
49
130
|
}
|
|
50
131
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
132
|
+
log("Verifying with service...");
|
|
133
|
+
const response = await fetchWithTimeout(
|
|
134
|
+
verifyUrl,
|
|
135
|
+
{
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: {
|
|
138
|
+
"Content-Type": "application/json"
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
project_id: projectId,
|
|
142
|
+
api_key: apiKey,
|
|
143
|
+
method: request.method,
|
|
144
|
+
url: request.url,
|
|
145
|
+
timestamp,
|
|
146
|
+
signature: headers.signature,
|
|
147
|
+
fingerprint: headers.fingerprint
|
|
148
|
+
})
|
|
55
149
|
},
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
api_key: config.apiKey,
|
|
59
|
-
method: request.method,
|
|
60
|
-
url: request.url,
|
|
61
|
-
timestamp: parseInt(headers.timestamp, 10),
|
|
62
|
-
signature: headers.signature,
|
|
63
|
-
fingerprint: headers.fingerprint
|
|
64
|
-
})
|
|
65
|
-
});
|
|
150
|
+
timeout
|
|
151
|
+
);
|
|
66
152
|
if (!response.ok) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
153
|
+
const errorCode = mapHttpStatusToErrorCode(response.status);
|
|
154
|
+
log("Service returned error:", response.status, errorCode);
|
|
155
|
+
if (response.status === 401 || response.status === 403) {
|
|
156
|
+
return {
|
|
157
|
+
attested: false,
|
|
158
|
+
reason: `Verification failed: ${response.status}`,
|
|
159
|
+
errorCode
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return handleServiceError(failMode, `Service error: ${response.status}`, errorCode);
|
|
71
163
|
}
|
|
72
|
-
|
|
164
|
+
const result = await response.json();
|
|
165
|
+
log("Verification result:", result.attested);
|
|
166
|
+
return result;
|
|
73
167
|
} catch (error) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
attested: false,
|
|
79
|
-
reason: "Verification service unavailable (fail-closed mode)"
|
|
80
|
-
};
|
|
168
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
169
|
+
log("Verification timed out");
|
|
170
|
+
return handleServiceError(failMode, "Verification timed out", "SERVICE_TIMEOUT");
|
|
81
171
|
}
|
|
172
|
+
console.error("[@sekyuriti/attest] Verification failed:", error);
|
|
173
|
+
return handleServiceError(failMode, "Verification service unavailable", "SERVICE_UNAVAILABLE");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function mapHttpStatusToErrorCode(status) {
|
|
177
|
+
switch (status) {
|
|
178
|
+
case 401:
|
|
179
|
+
return "INVALID_API_KEY";
|
|
180
|
+
case 403:
|
|
181
|
+
return "INVALID_PROJECT";
|
|
182
|
+
case 429:
|
|
183
|
+
return "RATE_LIMITED";
|
|
184
|
+
default:
|
|
185
|
+
return "SERVICE_ERROR";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function handleServiceError(failMode, reason, errorCode) {
|
|
189
|
+
if (failMode === "closed") {
|
|
82
190
|
return {
|
|
83
|
-
attested:
|
|
84
|
-
reason:
|
|
191
|
+
attested: false,
|
|
192
|
+
reason: `${reason} (fail-closed mode)`,
|
|
193
|
+
errorCode
|
|
85
194
|
};
|
|
86
195
|
}
|
|
196
|
+
return {
|
|
197
|
+
attested: true,
|
|
198
|
+
reason: `${reason} (fail-open mode)`,
|
|
199
|
+
bypass: true,
|
|
200
|
+
errorCode
|
|
201
|
+
};
|
|
87
202
|
}
|
|
88
203
|
function createAttestVerifier(config) {
|
|
89
204
|
return (request) => verifyAttest(request, config);
|
|
90
205
|
}
|
|
206
|
+
function shouldVerify(request, config) {
|
|
207
|
+
if (config?.allowLocalhost !== false) {
|
|
208
|
+
const clientIP = getClientIP(request);
|
|
209
|
+
if (isLocalhostIP(clientIP)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return hasAttestHeaders(request);
|
|
214
|
+
}
|
|
91
215
|
// Annotate the CommonJS export names for ESM import in node:
|
|
92
216
|
0 && (module.exports = {
|
|
93
217
|
createAttestVerifier,
|
|
94
218
|
getAttestHeaders,
|
|
95
219
|
hasAttestHeaders,
|
|
220
|
+
shouldVerify,
|
|
96
221
|
verifyAttest
|
|
97
222
|
});
|
|
98
223
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +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://attest.sekyuriti.build/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 * Behavior when verification service is unavailable.\n * - \"open\": Allow requests through (default, maintains availability)\n * - \"closed\": Block requests (stricter security, may cause outages)\n * @default \"open\"\n */\n failMode?: \"open\" | \"closed\";\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 method: request.method,\n url: request.url,\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 console.error(\"[@sekyuriti/attest] Verification failed:\", error);\n\n // Configurable fail mode\n const failMode = config.failMode || \"open\";\n\n if (failMode === \"closed\") {\n // Fail closed - block requests when service is unavailable (stricter security)\n return {\n attested: false,\n reason: \"Verification service unavailable (fail-closed mode)\",\n };\n }\n\n // Fail open - allow requests through (default, maintains availability)\n return {\n attested: true,\n reason: \"Verification service unavailable (fail-open mode)\",\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;AA+CnB,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,QAAQ,QAAQ;AAAA,QAChB,KAAK,QAAQ;AAAA,QACb,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;AACd,YAAQ,MAAM,4CAA4C,KAAK;AAG/D,UAAM,WAAW,OAAO,YAAY;AAEpC,QAAI,aAAa,UAAU;AAEzB,aAAO;AAAA,QACL,UAAU;AAAA,QACV,QAAQ;AAAA,MACV;AAAA,IACF;AAGA,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":[]}
|
|
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://attest.sekyuriti.build/verify\";\n\n// Default timeout for verification requests (5 seconds)\nconst DEFAULT_TIMEOUT_MS = 5000;\n\n// Localhost IPs that bypass verification in dev mode\nconst LOCALHOST_IPS = [\n \"127.0.0.1\",\n \"::1\",\n \"localhost\",\n \"::ffff:127.0.0.1\",\n];\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 * Behavior when verification service is unavailable.\n * - \"open\": Allow requests through (default, maintains availability)\n * - \"closed\": Block requests (stricter security, may cause outages)\n * @default \"open\"\n */\n failMode?: \"open\" | \"closed\";\n /**\n * Timeout in milliseconds for verification requests.\n * @default 5000\n */\n timeout?: number;\n /**\n * Skip verification for localhost requests (for development).\n * @default true\n */\n allowLocalhost?: boolean;\n /**\n * Enable debug logging.\n * @default false\n */\n debug?: boolean;\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 /** Whether this was a bypass (localhost, etc.) */\n bypass?: boolean;\n /** Error code for programmatic handling */\n errorCode?: AttestErrorCode;\n}\n\nexport type AttestErrorCode =\n | \"MISSING_HEADERS\"\n | \"INVALID_TIMESTAMP\"\n | \"INVALID_SIGNATURE\"\n | \"EXPIRED_TIMESTAMP\"\n | \"SERVICE_ERROR\"\n | \"SERVICE_TIMEOUT\"\n | \"SERVICE_UNAVAILABLE\"\n | \"RATE_LIMITED\"\n | \"INVALID_PROJECT\"\n | \"INVALID_API_KEY\";\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 * Get client IP from request headers\n */\nfunction getClientIP(request: Request): string | null {\n // Try common headers (in order of preference)\n const forwardedFor = request.headers.get(\"x-forwarded-for\");\n if (forwardedFor) {\n return forwardedFor.split(\",\")[0].trim();\n }\n\n const realIP = request.headers.get(\"x-real-ip\");\n if (realIP) return realIP;\n\n // Cloudflare\n const cfIP = request.headers.get(\"cf-connecting-ip\");\n if (cfIP) return cfIP;\n\n // Vercel\n const vercelIP = request.headers.get(\"x-vercel-forwarded-for\");\n if (vercelIP) return vercelIP.split(\",\")[0].trim();\n\n return null;\n}\n\n/**\n * Check if IP is localhost\n */\nfunction isLocalhostIP(ip: string | null): boolean {\n if (!ip) return false;\n return LOCALHOST_IPS.includes(ip);\n}\n\n/**\n * Fetch with timeout\n */\nasync function fetchWithTimeout(\n url: string,\n options: RequestInit,\n timeoutMs: number\n): Promise<Response> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const response = await fetch(url, {\n ...options,\n signal: controller.signal,\n });\n return response;\n } finally {\n clearTimeout(timeoutId);\n }\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_SECRET_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 {\n projectId,\n apiKey,\n verifyUrl = ATTEST_VERIFY_URL,\n failMode = \"open\",\n timeout = DEFAULT_TIMEOUT_MS,\n allowLocalhost = true,\n debug = false,\n } = config;\n\n const log = debug ? console.log.bind(console, \"[@sekyuriti/attest]\") : () => {};\n\n // Check for localhost bypass\n if (allowLocalhost) {\n const clientIP = getClientIP(request);\n if (isLocalhostIP(clientIP)) {\n log(\"Bypassing verification for localhost IP:\", clientIP);\n return {\n attested: true,\n reason: \"Localhost bypass\",\n bypass: true,\n };\n }\n }\n\n const headers = getAttestHeaders(request);\n\n // If no ATTEST headers, request is not attested\n if (!headers.timestamp || !headers.signature || !headers.fingerprint) {\n log(\"Missing ATTEST headers\");\n return {\n attested: false,\n reason: \"Missing ATTEST headers\",\n errorCode: \"MISSING_HEADERS\",\n };\n }\n\n // Validate timestamp is a number and not too old (prevent replay attacks)\n const timestamp = parseInt(headers.timestamp, 10);\n if (isNaN(timestamp)) {\n log(\"Invalid timestamp format\");\n return {\n attested: false,\n reason: \"Invalid timestamp format\",\n errorCode: \"INVALID_TIMESTAMP\",\n };\n }\n\n // Check if timestamp is within reasonable range (5 minutes)\n const now = Date.now();\n const maxAge = 5 * 60 * 1000; // 5 minutes\n if (now - timestamp > maxAge) {\n log(\"Timestamp expired:\", { timestamp, now, diff: now - timestamp });\n return {\n attested: false,\n reason: \"Request timestamp expired\",\n errorCode: \"EXPIRED_TIMESTAMP\",\n };\n }\n\n try {\n log(\"Verifying with service...\");\n\n const response = await fetchWithTimeout(\n verifyUrl,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({\n project_id: projectId,\n api_key: apiKey,\n method: request.method,\n url: request.url,\n timestamp,\n signature: headers.signature,\n fingerprint: headers.fingerprint,\n }),\n },\n timeout\n );\n\n if (!response.ok) {\n const errorCode = mapHttpStatusToErrorCode(response.status);\n log(\"Service returned error:\", response.status, errorCode);\n\n // For auth errors, always fail (don't use failMode)\n if (response.status === 401 || response.status === 403) {\n return {\n attested: false,\n reason: `Verification failed: ${response.status}`,\n errorCode,\n };\n }\n\n // For other errors, use failMode\n return handleServiceError(failMode, `Service error: ${response.status}`, errorCode);\n }\n\n const result = (await response.json()) as AttestResult;\n log(\"Verification result:\", result.attested);\n return result;\n } catch (error) {\n // Handle timeout specifically\n if (error instanceof Error && error.name === \"AbortError\") {\n log(\"Verification timed out\");\n return handleServiceError(failMode, \"Verification timed out\", \"SERVICE_TIMEOUT\");\n }\n\n // Handle network errors\n console.error(\"[@sekyuriti/attest] Verification failed:\", error);\n return handleServiceError(failMode, \"Verification service unavailable\", \"SERVICE_UNAVAILABLE\");\n }\n}\n\n/**\n * Map HTTP status to error code\n */\nfunction mapHttpStatusToErrorCode(status: number): AttestErrorCode {\n switch (status) {\n case 401:\n return \"INVALID_API_KEY\";\n case 403:\n return \"INVALID_PROJECT\";\n case 429:\n return \"RATE_LIMITED\";\n default:\n return \"SERVICE_ERROR\";\n }\n}\n\n/**\n * Handle service errors based on failMode\n */\nfunction handleServiceError(\n failMode: \"open\" | \"closed\",\n reason: string,\n errorCode: AttestErrorCode\n): AttestResult {\n if (failMode === \"closed\") {\n return {\n attested: false,\n reason: `${reason} (fail-closed mode)`,\n errorCode,\n };\n }\n\n // Fail open - allow through but mark as bypass\n return {\n attested: true,\n reason: `${reason} (fail-open mode)`,\n bypass: true,\n errorCode,\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_SECRET_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\n/**\n * Quick check if request should be verified\n * Use this for early bailout in middleware\n */\nexport function shouldVerify(request: Request, config?: { allowLocalhost?: boolean }): boolean {\n // Check localhost bypass\n if (config?.allowLocalhost !== false) {\n const clientIP = getClientIP(request);\n if (isLocalhostIP(clientIP)) {\n return false;\n }\n }\n\n // Check if has headers\n return hasAttestHeaders(request);\n}\n"],"mappings":"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","names":[]}
|