@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 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.2.3");
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
- // Add after last import
356
- middlewareContent = middlewareContent.replace(
357
- /(import[^;]+;[\s\S]*?)\n\n/,
358
- `$1\n${attestImport}\n`
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.ATTEST_API_KEY!,
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.ATTEST_API_KEY!,
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.ATTEST_API_KEY!,
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.ATTEST_API_KEY!,
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
- const response = await fetch(config.verifyUrl || ATTEST_VERIFY_URL, {
52
- method: "POST",
53
- headers: {
54
- "Content-Type": "application/json"
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
- body: JSON.stringify({
57
- project_id: config.projectId,
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
- return {
68
- attested: false,
69
- reason: `Verification service error: ${response.status}`
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
- return await response.json();
164
+ const result = await response.json();
165
+ log("Verification result:", result.attested);
166
+ return result;
73
167
  } catch (error) {
74
- console.error("[@sekyuriti/attest] Verification failed:", error);
75
- const failMode = config.failMode || "open";
76
- if (failMode === "closed") {
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: true,
84
- reason: "Verification service unavailable (fail-open mode)"
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":[]}