@sekyuriti/attest 0.2.6 → 0.4.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/bin/attest.js CHANGED
@@ -17,6 +17,7 @@ const c = {
17
17
  black: "\x1b[30m",
18
18
  white: "\x1b[37m",
19
19
  gray: "\x1b[90m",
20
+ green: "\x1b[32m",
20
21
  inverse: "\x1b[7m",
21
22
  };
22
23
 
@@ -208,7 +209,7 @@ function printHeader() {
208
209
  logBold(" █▀▀ █▀▀ █▄▀ █▄█ █ █ █▀█ █ ▀█▀ █");
209
210
  logBold(" ▄▄█ ██▄ █ █ █ █▄█ █▀▄ █ █ █");
210
211
  log("");
211
- logDim(" ATTEST CLI v0.2.3");
212
+ logDim(" ATTEST CLI v0.3.0");
212
213
  log("");
213
214
  }
214
215
 
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,58 +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
- timestamp: parseInt(headers.timestamp, 10),
60
- signature: headers.signature,
61
- fingerprint: headers.fingerprint
62
- })
63
- });
150
+ timeout
151
+ );
64
152
  if (!response.ok) {
65
- return {
66
- attested: false,
67
- reason: `Verification service error: ${response.status}`
68
- };
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);
69
163
  }
70
- return await response.json();
164
+ const result = await response.json();
165
+ log("Verification result:", result.attested);
166
+ return result;
71
167
  } catch (error) {
72
- console.error("[@sekyuriti/attest] Verification failed:", error);
73
- const failMode = config.failMode || "open";
74
- if (failMode === "closed") {
75
- return {
76
- attested: false,
77
- reason: "Verification service unavailable (fail-closed mode)"
78
- };
168
+ if (error instanceof Error && error.name === "AbortError") {
169
+ log("Verification timed out");
170
+ return handleServiceError(failMode, "Verification timed out", "SERVICE_TIMEOUT");
79
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") {
80
190
  return {
81
- attested: true,
82
- reason: "Verification service unavailable (fail-open mode)"
191
+ attested: false,
192
+ reason: `${reason} (fail-closed mode)`,
193
+ errorCode
83
194
  };
84
195
  }
196
+ return {
197
+ attested: true,
198
+ reason: `${reason} (fail-open mode)`,
199
+ bypass: true,
200
+ errorCode
201
+ };
85
202
  }
86
203
  function createAttestVerifier(config) {
87
204
  return (request) => verifyAttest(request, config);
88
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
+ }
89
215
  // Annotate the CommonJS export names for ESM import in node:
90
216
  0 && (module.exports = {
91
217
  createAttestVerifier,
92
218
  getAttestHeaders,
93
219
  hasAttestHeaders,
220
+ shouldVerify,
94
221
  verifyAttest
95
222
  });
96
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 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,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":[]}
package/dist/index.mjs CHANGED
@@ -1,5 +1,12 @@
1
1
  // src/index.ts
2
2
  var ATTEST_VERIFY_URL = "https://attest.sekyuriti.build/verify";
3
+ var DEFAULT_TIMEOUT_MS = 5e3;
4
+ var LOCALHOST_IPS = [
5
+ "127.0.0.1",
6
+ "::1",
7
+ "localhost",
8
+ "::ffff:127.0.0.1"
9
+ ];
3
10
  function getAttestHeaders(request) {
4
11
  return {
5
12
  timestamp: request.headers.get("x-attest-timestamp"),
@@ -12,57 +19,176 @@ function hasAttestHeaders(request) {
12
19
  const headers = getAttestHeaders(request);
13
20
  return !!(headers.timestamp && headers.signature && headers.fingerprint);
14
21
  }
22
+ function getClientIP(request) {
23
+ const forwardedFor = request.headers.get("x-forwarded-for");
24
+ if (forwardedFor) {
25
+ return forwardedFor.split(",")[0].trim();
26
+ }
27
+ const realIP = request.headers.get("x-real-ip");
28
+ if (realIP) return realIP;
29
+ const cfIP = request.headers.get("cf-connecting-ip");
30
+ if (cfIP) return cfIP;
31
+ const vercelIP = request.headers.get("x-vercel-forwarded-for");
32
+ if (vercelIP) return vercelIP.split(",")[0].trim();
33
+ return null;
34
+ }
35
+ function isLocalhostIP(ip) {
36
+ if (!ip) return false;
37
+ return LOCALHOST_IPS.includes(ip);
38
+ }
39
+ async function fetchWithTimeout(url, options, timeoutMs) {
40
+ const controller = new AbortController();
41
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
42
+ try {
43
+ const response = await fetch(url, {
44
+ ...options,
45
+ signal: controller.signal
46
+ });
47
+ return response;
48
+ } finally {
49
+ clearTimeout(timeoutId);
50
+ }
51
+ }
15
52
  async function verifyAttest(request, config) {
53
+ const {
54
+ projectId,
55
+ apiKey,
56
+ verifyUrl = ATTEST_VERIFY_URL,
57
+ failMode = "open",
58
+ timeout = DEFAULT_TIMEOUT_MS,
59
+ allowLocalhost = true,
60
+ debug = false
61
+ } = config;
62
+ const log = debug ? console.log.bind(console, "[@sekyuriti/attest]") : () => {
63
+ };
64
+ if (allowLocalhost) {
65
+ const clientIP = getClientIP(request);
66
+ if (isLocalhostIP(clientIP)) {
67
+ log("Bypassing verification for localhost IP:", clientIP);
68
+ return {
69
+ attested: true,
70
+ reason: "Localhost bypass",
71
+ bypass: true
72
+ };
73
+ }
74
+ }
16
75
  const headers = getAttestHeaders(request);
17
76
  if (!headers.timestamp || !headers.signature || !headers.fingerprint) {
77
+ log("Missing ATTEST headers");
18
78
  return {
19
79
  attested: false,
20
- reason: "Missing ATTEST headers"
80
+ reason: "Missing ATTEST headers",
81
+ errorCode: "MISSING_HEADERS"
82
+ };
83
+ }
84
+ const timestamp = parseInt(headers.timestamp, 10);
85
+ if (isNaN(timestamp)) {
86
+ log("Invalid timestamp format");
87
+ return {
88
+ attested: false,
89
+ reason: "Invalid timestamp format",
90
+ errorCode: "INVALID_TIMESTAMP"
91
+ };
92
+ }
93
+ const now = Date.now();
94
+ const maxAge = 5 * 60 * 1e3;
95
+ if (now - timestamp > maxAge) {
96
+ log("Timestamp expired:", { timestamp, now, diff: now - timestamp });
97
+ return {
98
+ attested: false,
99
+ reason: "Request timestamp expired",
100
+ errorCode: "EXPIRED_TIMESTAMP"
21
101
  };
22
102
  }
23
103
  try {
24
- const response = await fetch(config.verifyUrl || ATTEST_VERIFY_URL, {
25
- method: "POST",
26
- headers: {
27
- "Content-Type": "application/json"
104
+ log("Verifying with service...");
105
+ const response = await fetchWithTimeout(
106
+ verifyUrl,
107
+ {
108
+ method: "POST",
109
+ headers: {
110
+ "Content-Type": "application/json"
111
+ },
112
+ body: JSON.stringify({
113
+ project_id: projectId,
114
+ api_key: apiKey,
115
+ method: request.method,
116
+ url: request.url,
117
+ timestamp,
118
+ signature: headers.signature,
119
+ fingerprint: headers.fingerprint
120
+ })
28
121
  },
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
- });
122
+ timeout
123
+ );
37
124
  if (!response.ok) {
38
- return {
39
- attested: false,
40
- reason: `Verification service error: ${response.status}`
41
- };
125
+ const errorCode = mapHttpStatusToErrorCode(response.status);
126
+ log("Service returned error:", response.status, errorCode);
127
+ if (response.status === 401 || response.status === 403) {
128
+ return {
129
+ attested: false,
130
+ reason: `Verification failed: ${response.status}`,
131
+ errorCode
132
+ };
133
+ }
134
+ return handleServiceError(failMode, `Service error: ${response.status}`, errorCode);
42
135
  }
43
- return await response.json();
136
+ const result = await response.json();
137
+ log("Verification result:", result.attested);
138
+ return result;
44
139
  } catch (error) {
45
- console.error("[@sekyuriti/attest] Verification failed:", error);
46
- const failMode = config.failMode || "open";
47
- if (failMode === "closed") {
48
- return {
49
- attested: false,
50
- reason: "Verification service unavailable (fail-closed mode)"
51
- };
140
+ if (error instanceof Error && error.name === "AbortError") {
141
+ log("Verification timed out");
142
+ return handleServiceError(failMode, "Verification timed out", "SERVICE_TIMEOUT");
52
143
  }
144
+ console.error("[@sekyuriti/attest] Verification failed:", error);
145
+ return handleServiceError(failMode, "Verification service unavailable", "SERVICE_UNAVAILABLE");
146
+ }
147
+ }
148
+ function mapHttpStatusToErrorCode(status) {
149
+ switch (status) {
150
+ case 401:
151
+ return "INVALID_API_KEY";
152
+ case 403:
153
+ return "INVALID_PROJECT";
154
+ case 429:
155
+ return "RATE_LIMITED";
156
+ default:
157
+ return "SERVICE_ERROR";
158
+ }
159
+ }
160
+ function handleServiceError(failMode, reason, errorCode) {
161
+ if (failMode === "closed") {
53
162
  return {
54
- attested: true,
55
- reason: "Verification service unavailable (fail-open mode)"
163
+ attested: false,
164
+ reason: `${reason} (fail-closed mode)`,
165
+ errorCode
56
166
  };
57
167
  }
168
+ return {
169
+ attested: true,
170
+ reason: `${reason} (fail-open mode)`,
171
+ bypass: true,
172
+ errorCode
173
+ };
58
174
  }
59
175
  function createAttestVerifier(config) {
60
176
  return (request) => verifyAttest(request, config);
61
177
  }
178
+ function shouldVerify(request, config) {
179
+ if (config?.allowLocalhost !== false) {
180
+ const clientIP = getClientIP(request);
181
+ if (isLocalhostIP(clientIP)) {
182
+ return false;
183
+ }
184
+ }
185
+ return hasAttestHeaders(request);
186
+ }
62
187
  export {
63
188
  createAttestVerifier,
64
189
  getAttestHeaders,
65
190
  hasAttestHeaders,
191
+ shouldVerify,
66
192
  verifyAttest
67
193
  };
68
194
  //# sourceMappingURL=index.mjs.map