@sekyuriti/attest 0.2.4 → 0.2.5
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/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/middleware.js +1 -1
- package/dist/middleware.js.map +1 -1
- package/dist/middleware.mjs +1 -1
- package/dist/middleware.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ __export(src_exports, {
|
|
|
26
26
|
verifyAttest: () => verifyAttest
|
|
27
27
|
});
|
|
28
28
|
module.exports = __toCommonJS(src_exports);
|
|
29
|
-
var ATTEST_VERIFY_URL = "https://
|
|
29
|
+
var ATTEST_VERIFY_URL = "https://sekyuriti.build/api/v2/attest/verify";
|
|
30
30
|
function getAttestHeaders(request) {
|
|
31
31
|
return {
|
|
32
32
|
timestamp: request.headers.get("x-attest-timestamp"),
|
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://
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @sekyuriti/attest\n *\n * API protection for Next.js applications.\n * Verify that requests come from real browsers, not bots or scripts.\n */\n\nconst ATTEST_VERIFY_URL = \"https://sekyuriti.build/api/v2/attest/verify\";\n\nexport interface AttestConfig {\n /** Your ATTEST project ID (starts with ATST_) */\n projectId: string;\n /** Your ATTEST API key (keep this secret, server-side only) */\n apiKey: string;\n /** Custom verify URL (optional, for self-hosted) */\n verifyUrl?: string;\n /**\n * 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":[]}
|
package/dist/index.mjs
CHANGED
package/dist/index.mjs.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://
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @sekyuriti/attest\n *\n * API protection for Next.js applications.\n * Verify that requests come from real browsers, not bots or scripts.\n */\n\nconst ATTEST_VERIFY_URL = \"https://sekyuriti.build/api/v2/attest/verify\";\n\nexport interface AttestConfig {\n /** Your ATTEST project ID (starts with ATST_) */\n projectId: string;\n /** Your ATTEST API key (keep this secret, server-side only) */\n apiKey: string;\n /** Custom verify URL (optional, for self-hosted) */\n verifyUrl?: string;\n /**\n * 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":";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":[]}
|
package/dist/middleware.js
CHANGED
|
@@ -27,7 +27,7 @@ module.exports = __toCommonJS(middleware_exports);
|
|
|
27
27
|
var import_server = require("next/server");
|
|
28
28
|
|
|
29
29
|
// src/index.ts
|
|
30
|
-
var ATTEST_VERIFY_URL = "https://
|
|
30
|
+
var ATTEST_VERIFY_URL = "https://sekyuriti.build/api/v2/attest/verify";
|
|
31
31
|
function getAttestHeaders(request) {
|
|
32
32
|
return {
|
|
33
33
|
timestamp: request.headers.get("x-attest-timestamp"),
|
package/dist/middleware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware.ts","../src/index.ts"],"sourcesContent":["/**\n * @sekyuriti/attest/middleware\n *\n * Next.js middleware for automatic ATTEST verification.\n * Protects all matching routes with a single file.\n */\n\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\nimport { verifyAttest, type AttestConfig, type AttestResult } from \"./index\";\n\nexport interface AttestMiddlewareConfig extends AttestConfig {\n /**\n * Routes to protect (glob patterns)\n * @default [\"/api/*\"]\n */\n protectedRoutes?: string[];\n\n /**\n * Routes to exclude from protection (glob patterns)\n * @default []\n */\n excludeRoutes?: string[];\n\n /**\n * IP addresses to whitelist (bypass ATTEST verification)\n * Supports exact IPs and CIDR notation\n * @example [\"127.0.0.1\", \"10.0.0.0/8\", \"192.168.1.0/24\"]\n */\n whitelistIPs?: string[];\n\n /**\n * Webhook secrets - if request has matching header, bypass ATTEST\n * Useful for Stripe, GitHub, etc. webhooks\n * @example { \"x-webhook-secret\": \"whsec_xxx\", \"x-github-event\": \"*\" }\n */\n whitelistHeaders?: Record<string, string>;\n\n /**\n * Internal service tokens - bypass ATTEST if Authorization header matches\n * @example [\"Bearer internal_service_token_xxx\"]\n */\n whitelistTokens?: string[];\n\n /**\n * User agents to whitelist (for health checks, monitoring)\n * @example [\"kube-probe/*\", \"Prometheus/*\"]\n */\n whitelistUserAgents?: string[];\n\n /**\n * Allow requests without ATTEST headers (passthrough mode)\n * Useful for gradual rollout\n * @default false\n */\n allowUnauthenticated?: boolean;\n\n /**\n * Custom handler for blocked requests\n */\n onBlocked?: (request: NextRequest, result: AttestResult) => Response | Promise<Response>;\n\n /**\n * Custom handler for allowed requests (for logging, etc.)\n */\n onAllowed?: (request: NextRequest, result: AttestResult) => void | Promise<void>;\n\n /**\n * Custom whitelist function for advanced use cases\n * Return true to bypass ATTEST verification\n */\n customWhitelist?: (request: NextRequest) => boolean | Promise<boolean>;\n}\n\n/**\n * Check if a path matches a glob pattern\n */\nfunction matchesPattern(path: string, pattern: string): boolean {\n // Convert glob to regex\n const regexPattern = pattern\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${regexPattern}$`).test(path);\n}\n\n/**\n * Check if a path matches any of the patterns\n */\nfunction matchesAnyPattern(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchesPattern(path, pattern));\n}\n\n/**\n * Parse CIDR notation and check if IP is in range\n */\nfunction ipInCIDR(ip: string, cidr: string): boolean {\n // Handle exact IP match\n if (!cidr.includes(\"/\")) {\n return ip === cidr;\n }\n\n const [range, bits] = cidr.split(\"/\");\n const mask = parseInt(bits, 10);\n\n // Convert IP to number\n const ipToNum = (ipStr: string): number => {\n const parts = ipStr.split(\".\").map(Number);\n return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];\n };\n\n const ipNum = ipToNum(ip);\n const rangeNum = ipToNum(range);\n const maskNum = ~((1 << (32 - mask)) - 1);\n\n return (ipNum & maskNum) === (rangeNum & maskNum);\n}\n\n/**\n * Check if IP is whitelisted\n */\nfunction isIPWhitelisted(ip: string | null, whitelist: string[]): boolean {\n if (!ip || whitelist.length === 0) return false;\n return whitelist.some((cidr) => ipInCIDR(ip, cidr));\n}\n\n/**\n * Get client IP from request\n */\nfunction getClientIP(request: NextRequest): string | null {\n // Try common headers first (for proxied requests)\n const forwardedFor = request.headers.get(\"x-forwarded-for\");\n if (forwardedFor) {\n // Take the first IP (client IP)\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 request has whitelisted headers\n */\nfunction hasWhitelistedHeaders(\n request: NextRequest,\n whitelist: Record<string, string>\n): boolean {\n for (const [header, expectedValue] of Object.entries(whitelist)) {\n const actualValue = request.headers.get(header);\n if (actualValue) {\n // \"*\" means any value is accepted (header presence check)\n if (expectedValue === \"*\") return true;\n // Exact match\n if (actualValue === expectedValue) return true;\n }\n }\n return false;\n}\n\n/**\n * Check if request has whitelisted authorization token\n */\nfunction hasWhitelistedToken(request: NextRequest, tokens: string[]): boolean {\n const authHeader = request.headers.get(\"authorization\");\n if (!authHeader) return false;\n return tokens.includes(authHeader);\n}\n\n/**\n * Check if user agent is whitelisted\n */\nfunction hasWhitelistedUserAgent(request: NextRequest, patterns: string[]): boolean {\n const userAgent = request.headers.get(\"user-agent\");\n if (!userAgent) return false;\n return patterns.some((pattern) => matchesPattern(userAgent, pattern));\n}\n\n/**\n * Create ATTEST middleware for Next.js\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createAttestMiddleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const middleware = createAttestMiddleware({\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n */\nexport function createAttestMiddleware(config: AttestMiddlewareConfig) {\n const {\n protectedRoutes = [\"/api/*\"],\n excludeRoutes = [],\n whitelistIPs = [],\n whitelistHeaders = {},\n whitelistTokens = [],\n whitelistUserAgents = [],\n allowUnauthenticated = false,\n onBlocked,\n onAllowed,\n customWhitelist,\n ...attestConfig\n } = config;\n\n return async function middleware(request: NextRequest) {\n const path = request.nextUrl.pathname;\n\n // Check if this path should be protected\n const isProtected = matchesAnyPattern(path, protectedRoutes);\n const isExcluded = matchesAnyPattern(path, excludeRoutes);\n\n if (!isProtected || isExcluded) {\n return NextResponse.next();\n }\n\n // === WHITELIST CHECKS (bypass ATTEST) ===\n\n // 1. IP whitelist (internal services, VPN, etc.)\n if (whitelistIPs.length > 0) {\n const clientIP = getClientIP(request);\n if (isIPWhitelisted(clientIP, whitelistIPs)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"ip-whitelist\");\n return response;\n }\n }\n\n // 2. Header whitelist (webhooks from Stripe, GitHub, etc.)\n if (Object.keys(whitelistHeaders).length > 0) {\n if (hasWhitelistedHeaders(request, whitelistHeaders)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"header-whitelist\");\n return response;\n }\n }\n\n // 3. Token whitelist (service-to-service auth)\n if (whitelistTokens.length > 0) {\n if (hasWhitelistedToken(request, whitelistTokens)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"token-whitelist\");\n return response;\n }\n }\n\n // 4. User agent whitelist (health checks, monitoring)\n if (whitelistUserAgents.length > 0) {\n if (hasWhitelistedUserAgent(request, whitelistUserAgents)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"useragent-whitelist\");\n return response;\n }\n }\n\n // 5. Custom whitelist function\n if (customWhitelist) {\n const shouldBypass = await customWhitelist(request);\n if (shouldBypass) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"custom-whitelist\");\n return response;\n }\n }\n\n // === ATTEST VERIFICATION ===\n\n // Verify with ATTEST\n const result = await verifyAttest(request, attestConfig);\n\n if (!result.attested) {\n // Allow through if configured and no headers present\n if (allowUnauthenticated && result.reason === \"Missing ATTEST headers\") {\n return NextResponse.next();\n }\n\n // Custom blocked handler\n if (onBlocked) {\n return onBlocked(request, result);\n }\n\n // Default blocked response\n return NextResponse.json(\n {\n error: \"Request not attested\",\n reason: result.reason,\n },\n { status: 403 }\n );\n }\n\n // Call allowed handler if provided\n if (onAllowed) {\n await onAllowed(request, result);\n }\n\n // Add fingerprint to headers for downstream use\n const response = NextResponse.next();\n if (result.fingerprint) {\n response.headers.set(\"x-attest-verified-fingerprint\", result.fingerprint);\n }\n\n return response;\n };\n}\n\n/**\n * Simple middleware that uses environment variables\n *\n * @example\n * ```ts\n * // middleware.ts\n * export { attestMiddleware as middleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n *\n * Requires environment variables:\n * - ATTEST_PROJECT_ID\n * - ATTEST_API_KEY\n */\nexport const attestMiddleware = createAttestMiddleware({\n projectId: process.env.ATTEST_PROJECT_ID || \"\",\n apiKey: process.env.ATTEST_API_KEY || \"\",\n});\n","/**\n * @sekyuriti/attest\n *\n * API protection for Next.js applications.\n * Verify that requests come from real browsers, not bots or scripts.\n */\n\nconst ATTEST_VERIFY_URL = \"https://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":"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","names":["response"]}
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/index.ts"],"sourcesContent":["/**\n * @sekyuriti/attest/middleware\n *\n * Next.js middleware for automatic ATTEST verification.\n * Protects all matching routes with a single file.\n */\n\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\nimport { verifyAttest, type AttestConfig, type AttestResult } from \"./index\";\n\nexport interface AttestMiddlewareConfig extends AttestConfig {\n /**\n * Routes to protect (glob patterns)\n * @default [\"/api/*\"]\n */\n protectedRoutes?: string[];\n\n /**\n * Routes to exclude from protection (glob patterns)\n * @default []\n */\n excludeRoutes?: string[];\n\n /**\n * IP addresses to whitelist (bypass ATTEST verification)\n * Supports exact IPs and CIDR notation\n * @example [\"127.0.0.1\", \"10.0.0.0/8\", \"192.168.1.0/24\"]\n */\n whitelistIPs?: string[];\n\n /**\n * Webhook secrets - if request has matching header, bypass ATTEST\n * Useful for Stripe, GitHub, etc. webhooks\n * @example { \"x-webhook-secret\": \"whsec_xxx\", \"x-github-event\": \"*\" }\n */\n whitelistHeaders?: Record<string, string>;\n\n /**\n * Internal service tokens - bypass ATTEST if Authorization header matches\n * @example [\"Bearer internal_service_token_xxx\"]\n */\n whitelistTokens?: string[];\n\n /**\n * User agents to whitelist (for health checks, monitoring)\n * @example [\"kube-probe/*\", \"Prometheus/*\"]\n */\n whitelistUserAgents?: string[];\n\n /**\n * Allow requests without ATTEST headers (passthrough mode)\n * Useful for gradual rollout\n * @default false\n */\n allowUnauthenticated?: boolean;\n\n /**\n * Custom handler for blocked requests\n */\n onBlocked?: (request: NextRequest, result: AttestResult) => Response | Promise<Response>;\n\n /**\n * Custom handler for allowed requests (for logging, etc.)\n */\n onAllowed?: (request: NextRequest, result: AttestResult) => void | Promise<void>;\n\n /**\n * Custom whitelist function for advanced use cases\n * Return true to bypass ATTEST verification\n */\n customWhitelist?: (request: NextRequest) => boolean | Promise<boolean>;\n}\n\n/**\n * Check if a path matches a glob pattern\n */\nfunction matchesPattern(path: string, pattern: string): boolean {\n // Convert glob to regex\n const regexPattern = pattern\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${regexPattern}$`).test(path);\n}\n\n/**\n * Check if a path matches any of the patterns\n */\nfunction matchesAnyPattern(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchesPattern(path, pattern));\n}\n\n/**\n * Parse CIDR notation and check if IP is in range\n */\nfunction ipInCIDR(ip: string, cidr: string): boolean {\n // Handle exact IP match\n if (!cidr.includes(\"/\")) {\n return ip === cidr;\n }\n\n const [range, bits] = cidr.split(\"/\");\n const mask = parseInt(bits, 10);\n\n // Convert IP to number\n const ipToNum = (ipStr: string): number => {\n const parts = ipStr.split(\".\").map(Number);\n return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];\n };\n\n const ipNum = ipToNum(ip);\n const rangeNum = ipToNum(range);\n const maskNum = ~((1 << (32 - mask)) - 1);\n\n return (ipNum & maskNum) === (rangeNum & maskNum);\n}\n\n/**\n * Check if IP is whitelisted\n */\nfunction isIPWhitelisted(ip: string | null, whitelist: string[]): boolean {\n if (!ip || whitelist.length === 0) return false;\n return whitelist.some((cidr) => ipInCIDR(ip, cidr));\n}\n\n/**\n * Get client IP from request\n */\nfunction getClientIP(request: NextRequest): string | null {\n // Try common headers first (for proxied requests)\n const forwardedFor = request.headers.get(\"x-forwarded-for\");\n if (forwardedFor) {\n // Take the first IP (client IP)\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 request has whitelisted headers\n */\nfunction hasWhitelistedHeaders(\n request: NextRequest,\n whitelist: Record<string, string>\n): boolean {\n for (const [header, expectedValue] of Object.entries(whitelist)) {\n const actualValue = request.headers.get(header);\n if (actualValue) {\n // \"*\" means any value is accepted (header presence check)\n if (expectedValue === \"*\") return true;\n // Exact match\n if (actualValue === expectedValue) return true;\n }\n }\n return false;\n}\n\n/**\n * Check if request has whitelisted authorization token\n */\nfunction hasWhitelistedToken(request: NextRequest, tokens: string[]): boolean {\n const authHeader = request.headers.get(\"authorization\");\n if (!authHeader) return false;\n return tokens.includes(authHeader);\n}\n\n/**\n * Check if user agent is whitelisted\n */\nfunction hasWhitelistedUserAgent(request: NextRequest, patterns: string[]): boolean {\n const userAgent = request.headers.get(\"user-agent\");\n if (!userAgent) return false;\n return patterns.some((pattern) => matchesPattern(userAgent, pattern));\n}\n\n/**\n * Create ATTEST middleware for Next.js\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createAttestMiddleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const middleware = createAttestMiddleware({\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n */\nexport function createAttestMiddleware(config: AttestMiddlewareConfig) {\n const {\n protectedRoutes = [\"/api/*\"],\n excludeRoutes = [],\n whitelistIPs = [],\n whitelistHeaders = {},\n whitelistTokens = [],\n whitelistUserAgents = [],\n allowUnauthenticated = false,\n onBlocked,\n onAllowed,\n customWhitelist,\n ...attestConfig\n } = config;\n\n return async function middleware(request: NextRequest) {\n const path = request.nextUrl.pathname;\n\n // Check if this path should be protected\n const isProtected = matchesAnyPattern(path, protectedRoutes);\n const isExcluded = matchesAnyPattern(path, excludeRoutes);\n\n if (!isProtected || isExcluded) {\n return NextResponse.next();\n }\n\n // === WHITELIST CHECKS (bypass ATTEST) ===\n\n // 1. IP whitelist (internal services, VPN, etc.)\n if (whitelistIPs.length > 0) {\n const clientIP = getClientIP(request);\n if (isIPWhitelisted(clientIP, whitelistIPs)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"ip-whitelist\");\n return response;\n }\n }\n\n // 2. Header whitelist (webhooks from Stripe, GitHub, etc.)\n if (Object.keys(whitelistHeaders).length > 0) {\n if (hasWhitelistedHeaders(request, whitelistHeaders)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"header-whitelist\");\n return response;\n }\n }\n\n // 3. Token whitelist (service-to-service auth)\n if (whitelistTokens.length > 0) {\n if (hasWhitelistedToken(request, whitelistTokens)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"token-whitelist\");\n return response;\n }\n }\n\n // 4. User agent whitelist (health checks, monitoring)\n if (whitelistUserAgents.length > 0) {\n if (hasWhitelistedUserAgent(request, whitelistUserAgents)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"useragent-whitelist\");\n return response;\n }\n }\n\n // 5. Custom whitelist function\n if (customWhitelist) {\n const shouldBypass = await customWhitelist(request);\n if (shouldBypass) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"custom-whitelist\");\n return response;\n }\n }\n\n // === ATTEST VERIFICATION ===\n\n // Verify with ATTEST\n const result = await verifyAttest(request, attestConfig);\n\n if (!result.attested) {\n // Allow through if configured and no headers present\n if (allowUnauthenticated && result.reason === \"Missing ATTEST headers\") {\n return NextResponse.next();\n }\n\n // Custom blocked handler\n if (onBlocked) {\n return onBlocked(request, result);\n }\n\n // Default blocked response\n return NextResponse.json(\n {\n error: \"Request not attested\",\n reason: result.reason,\n },\n { status: 403 }\n );\n }\n\n // Call allowed handler if provided\n if (onAllowed) {\n await onAllowed(request, result);\n }\n\n // Add fingerprint to headers for downstream use\n const response = NextResponse.next();\n if (result.fingerprint) {\n response.headers.set(\"x-attest-verified-fingerprint\", result.fingerprint);\n }\n\n return response;\n };\n}\n\n/**\n * Simple middleware that uses environment variables\n *\n * @example\n * ```ts\n * // middleware.ts\n * export { attestMiddleware as middleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n *\n * Requires environment variables:\n * - ATTEST_PROJECT_ID\n * - ATTEST_API_KEY\n */\nexport const attestMiddleware = createAttestMiddleware({\n projectId: process.env.ATTEST_PROJECT_ID || \"\",\n apiKey: process.env.ATTEST_API_KEY || \"\",\n});\n","/**\n * @sekyuriti/attest\n *\n * API protection for Next.js applications.\n * Verify that requests come from real browsers, not bots or scripts.\n */\n\nconst ATTEST_VERIFY_URL = \"https://sekyuriti.build/api/v2/attest/verify\";\n\nexport interface AttestConfig {\n /** Your ATTEST project ID (starts with ATST_) */\n projectId: string;\n /** Your ATTEST API key (keep this secret, server-side only) */\n apiKey: string;\n /** Custom verify URL (optional, for self-hosted) */\n verifyUrl?: string;\n /**\n * 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":"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","names":["response"]}
|
package/dist/middleware.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
|
-
var ATTEST_VERIFY_URL = "https://
|
|
5
|
+
var ATTEST_VERIFY_URL = "https://sekyuriti.build/api/v2/attest/verify";
|
|
6
6
|
function getAttestHeaders(request) {
|
|
7
7
|
return {
|
|
8
8
|
timestamp: request.headers.get("x-attest-timestamp"),
|
package/dist/middleware.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware.ts","../src/index.ts"],"sourcesContent":["/**\n * @sekyuriti/attest/middleware\n *\n * Next.js middleware for automatic ATTEST verification.\n * Protects all matching routes with a single file.\n */\n\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\nimport { verifyAttest, type AttestConfig, type AttestResult } from \"./index\";\n\nexport interface AttestMiddlewareConfig extends AttestConfig {\n /**\n * Routes to protect (glob patterns)\n * @default [\"/api/*\"]\n */\n protectedRoutes?: string[];\n\n /**\n * Routes to exclude from protection (glob patterns)\n * @default []\n */\n excludeRoutes?: string[];\n\n /**\n * IP addresses to whitelist (bypass ATTEST verification)\n * Supports exact IPs and CIDR notation\n * @example [\"127.0.0.1\", \"10.0.0.0/8\", \"192.168.1.0/24\"]\n */\n whitelistIPs?: string[];\n\n /**\n * Webhook secrets - if request has matching header, bypass ATTEST\n * Useful for Stripe, GitHub, etc. webhooks\n * @example { \"x-webhook-secret\": \"whsec_xxx\", \"x-github-event\": \"*\" }\n */\n whitelistHeaders?: Record<string, string>;\n\n /**\n * Internal service tokens - bypass ATTEST if Authorization header matches\n * @example [\"Bearer internal_service_token_xxx\"]\n */\n whitelistTokens?: string[];\n\n /**\n * User agents to whitelist (for health checks, monitoring)\n * @example [\"kube-probe/*\", \"Prometheus/*\"]\n */\n whitelistUserAgents?: string[];\n\n /**\n * Allow requests without ATTEST headers (passthrough mode)\n * Useful for gradual rollout\n * @default false\n */\n allowUnauthenticated?: boolean;\n\n /**\n * Custom handler for blocked requests\n */\n onBlocked?: (request: NextRequest, result: AttestResult) => Response | Promise<Response>;\n\n /**\n * Custom handler for allowed requests (for logging, etc.)\n */\n onAllowed?: (request: NextRequest, result: AttestResult) => void | Promise<void>;\n\n /**\n * Custom whitelist function for advanced use cases\n * Return true to bypass ATTEST verification\n */\n customWhitelist?: (request: NextRequest) => boolean | Promise<boolean>;\n}\n\n/**\n * Check if a path matches a glob pattern\n */\nfunction matchesPattern(path: string, pattern: string): boolean {\n // Convert glob to regex\n const regexPattern = pattern\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${regexPattern}$`).test(path);\n}\n\n/**\n * Check if a path matches any of the patterns\n */\nfunction matchesAnyPattern(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchesPattern(path, pattern));\n}\n\n/**\n * Parse CIDR notation and check if IP is in range\n */\nfunction ipInCIDR(ip: string, cidr: string): boolean {\n // Handle exact IP match\n if (!cidr.includes(\"/\")) {\n return ip === cidr;\n }\n\n const [range, bits] = cidr.split(\"/\");\n const mask = parseInt(bits, 10);\n\n // Convert IP to number\n const ipToNum = (ipStr: string): number => {\n const parts = ipStr.split(\".\").map(Number);\n return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];\n };\n\n const ipNum = ipToNum(ip);\n const rangeNum = ipToNum(range);\n const maskNum = ~((1 << (32 - mask)) - 1);\n\n return (ipNum & maskNum) === (rangeNum & maskNum);\n}\n\n/**\n * Check if IP is whitelisted\n */\nfunction isIPWhitelisted(ip: string | null, whitelist: string[]): boolean {\n if (!ip || whitelist.length === 0) return false;\n return whitelist.some((cidr) => ipInCIDR(ip, cidr));\n}\n\n/**\n * Get client IP from request\n */\nfunction getClientIP(request: NextRequest): string | null {\n // Try common headers first (for proxied requests)\n const forwardedFor = request.headers.get(\"x-forwarded-for\");\n if (forwardedFor) {\n // Take the first IP (client IP)\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 request has whitelisted headers\n */\nfunction hasWhitelistedHeaders(\n request: NextRequest,\n whitelist: Record<string, string>\n): boolean {\n for (const [header, expectedValue] of Object.entries(whitelist)) {\n const actualValue = request.headers.get(header);\n if (actualValue) {\n // \"*\" means any value is accepted (header presence check)\n if (expectedValue === \"*\") return true;\n // Exact match\n if (actualValue === expectedValue) return true;\n }\n }\n return false;\n}\n\n/**\n * Check if request has whitelisted authorization token\n */\nfunction hasWhitelistedToken(request: NextRequest, tokens: string[]): boolean {\n const authHeader = request.headers.get(\"authorization\");\n if (!authHeader) return false;\n return tokens.includes(authHeader);\n}\n\n/**\n * Check if user agent is whitelisted\n */\nfunction hasWhitelistedUserAgent(request: NextRequest, patterns: string[]): boolean {\n const userAgent = request.headers.get(\"user-agent\");\n if (!userAgent) return false;\n return patterns.some((pattern) => matchesPattern(userAgent, pattern));\n}\n\n/**\n * Create ATTEST middleware for Next.js\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createAttestMiddleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const middleware = createAttestMiddleware({\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n */\nexport function createAttestMiddleware(config: AttestMiddlewareConfig) {\n const {\n protectedRoutes = [\"/api/*\"],\n excludeRoutes = [],\n whitelistIPs = [],\n whitelistHeaders = {},\n whitelistTokens = [],\n whitelistUserAgents = [],\n allowUnauthenticated = false,\n onBlocked,\n onAllowed,\n customWhitelist,\n ...attestConfig\n } = config;\n\n return async function middleware(request: NextRequest) {\n const path = request.nextUrl.pathname;\n\n // Check if this path should be protected\n const isProtected = matchesAnyPattern(path, protectedRoutes);\n const isExcluded = matchesAnyPattern(path, excludeRoutes);\n\n if (!isProtected || isExcluded) {\n return NextResponse.next();\n }\n\n // === WHITELIST CHECKS (bypass ATTEST) ===\n\n // 1. IP whitelist (internal services, VPN, etc.)\n if (whitelistIPs.length > 0) {\n const clientIP = getClientIP(request);\n if (isIPWhitelisted(clientIP, whitelistIPs)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"ip-whitelist\");\n return response;\n }\n }\n\n // 2. Header whitelist (webhooks from Stripe, GitHub, etc.)\n if (Object.keys(whitelistHeaders).length > 0) {\n if (hasWhitelistedHeaders(request, whitelistHeaders)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"header-whitelist\");\n return response;\n }\n }\n\n // 3. Token whitelist (service-to-service auth)\n if (whitelistTokens.length > 0) {\n if (hasWhitelistedToken(request, whitelistTokens)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"token-whitelist\");\n return response;\n }\n }\n\n // 4. User agent whitelist (health checks, monitoring)\n if (whitelistUserAgents.length > 0) {\n if (hasWhitelistedUserAgent(request, whitelistUserAgents)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"useragent-whitelist\");\n return response;\n }\n }\n\n // 5. Custom whitelist function\n if (customWhitelist) {\n const shouldBypass = await customWhitelist(request);\n if (shouldBypass) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"custom-whitelist\");\n return response;\n }\n }\n\n // === ATTEST VERIFICATION ===\n\n // Verify with ATTEST\n const result = await verifyAttest(request, attestConfig);\n\n if (!result.attested) {\n // Allow through if configured and no headers present\n if (allowUnauthenticated && result.reason === \"Missing ATTEST headers\") {\n return NextResponse.next();\n }\n\n // Custom blocked handler\n if (onBlocked) {\n return onBlocked(request, result);\n }\n\n // Default blocked response\n return NextResponse.json(\n {\n error: \"Request not attested\",\n reason: result.reason,\n },\n { status: 403 }\n );\n }\n\n // Call allowed handler if provided\n if (onAllowed) {\n await onAllowed(request, result);\n }\n\n // Add fingerprint to headers for downstream use\n const response = NextResponse.next();\n if (result.fingerprint) {\n response.headers.set(\"x-attest-verified-fingerprint\", result.fingerprint);\n }\n\n return response;\n };\n}\n\n/**\n * Simple middleware that uses environment variables\n *\n * @example\n * ```ts\n * // middleware.ts\n * export { attestMiddleware as middleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n *\n * Requires environment variables:\n * - ATTEST_PROJECT_ID\n * - ATTEST_API_KEY\n */\nexport const attestMiddleware = createAttestMiddleware({\n projectId: process.env.ATTEST_PROJECT_ID || \"\",\n apiKey: process.env.ATTEST_API_KEY || \"\",\n});\n","/**\n * @sekyuriti/attest\n *\n * API protection for Next.js applications.\n * Verify that requests come from real browsers, not bots or scripts.\n */\n\nconst ATTEST_VERIFY_URL = \"https://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":"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","names":["response"]}
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/index.ts"],"sourcesContent":["/**\n * @sekyuriti/attest/middleware\n *\n * Next.js middleware for automatic ATTEST verification.\n * Protects all matching routes with a single file.\n */\n\nimport { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\nimport { verifyAttest, type AttestConfig, type AttestResult } from \"./index\";\n\nexport interface AttestMiddlewareConfig extends AttestConfig {\n /**\n * Routes to protect (glob patterns)\n * @default [\"/api/*\"]\n */\n protectedRoutes?: string[];\n\n /**\n * Routes to exclude from protection (glob patterns)\n * @default []\n */\n excludeRoutes?: string[];\n\n /**\n * IP addresses to whitelist (bypass ATTEST verification)\n * Supports exact IPs and CIDR notation\n * @example [\"127.0.0.1\", \"10.0.0.0/8\", \"192.168.1.0/24\"]\n */\n whitelistIPs?: string[];\n\n /**\n * Webhook secrets - if request has matching header, bypass ATTEST\n * Useful for Stripe, GitHub, etc. webhooks\n * @example { \"x-webhook-secret\": \"whsec_xxx\", \"x-github-event\": \"*\" }\n */\n whitelistHeaders?: Record<string, string>;\n\n /**\n * Internal service tokens - bypass ATTEST if Authorization header matches\n * @example [\"Bearer internal_service_token_xxx\"]\n */\n whitelistTokens?: string[];\n\n /**\n * User agents to whitelist (for health checks, monitoring)\n * @example [\"kube-probe/*\", \"Prometheus/*\"]\n */\n whitelistUserAgents?: string[];\n\n /**\n * Allow requests without ATTEST headers (passthrough mode)\n * Useful for gradual rollout\n * @default false\n */\n allowUnauthenticated?: boolean;\n\n /**\n * Custom handler for blocked requests\n */\n onBlocked?: (request: NextRequest, result: AttestResult) => Response | Promise<Response>;\n\n /**\n * Custom handler for allowed requests (for logging, etc.)\n */\n onAllowed?: (request: NextRequest, result: AttestResult) => void | Promise<void>;\n\n /**\n * Custom whitelist function for advanced use cases\n * Return true to bypass ATTEST verification\n */\n customWhitelist?: (request: NextRequest) => boolean | Promise<boolean>;\n}\n\n/**\n * Check if a path matches a glob pattern\n */\nfunction matchesPattern(path: string, pattern: string): boolean {\n // Convert glob to regex\n const regexPattern = pattern\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${regexPattern}$`).test(path);\n}\n\n/**\n * Check if a path matches any of the patterns\n */\nfunction matchesAnyPattern(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchesPattern(path, pattern));\n}\n\n/**\n * Parse CIDR notation and check if IP is in range\n */\nfunction ipInCIDR(ip: string, cidr: string): boolean {\n // Handle exact IP match\n if (!cidr.includes(\"/\")) {\n return ip === cidr;\n }\n\n const [range, bits] = cidr.split(\"/\");\n const mask = parseInt(bits, 10);\n\n // Convert IP to number\n const ipToNum = (ipStr: string): number => {\n const parts = ipStr.split(\".\").map(Number);\n return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];\n };\n\n const ipNum = ipToNum(ip);\n const rangeNum = ipToNum(range);\n const maskNum = ~((1 << (32 - mask)) - 1);\n\n return (ipNum & maskNum) === (rangeNum & maskNum);\n}\n\n/**\n * Check if IP is whitelisted\n */\nfunction isIPWhitelisted(ip: string | null, whitelist: string[]): boolean {\n if (!ip || whitelist.length === 0) return false;\n return whitelist.some((cidr) => ipInCIDR(ip, cidr));\n}\n\n/**\n * Get client IP from request\n */\nfunction getClientIP(request: NextRequest): string | null {\n // Try common headers first (for proxied requests)\n const forwardedFor = request.headers.get(\"x-forwarded-for\");\n if (forwardedFor) {\n // Take the first IP (client IP)\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 request has whitelisted headers\n */\nfunction hasWhitelistedHeaders(\n request: NextRequest,\n whitelist: Record<string, string>\n): boolean {\n for (const [header, expectedValue] of Object.entries(whitelist)) {\n const actualValue = request.headers.get(header);\n if (actualValue) {\n // \"*\" means any value is accepted (header presence check)\n if (expectedValue === \"*\") return true;\n // Exact match\n if (actualValue === expectedValue) return true;\n }\n }\n return false;\n}\n\n/**\n * Check if request has whitelisted authorization token\n */\nfunction hasWhitelistedToken(request: NextRequest, tokens: string[]): boolean {\n const authHeader = request.headers.get(\"authorization\");\n if (!authHeader) return false;\n return tokens.includes(authHeader);\n}\n\n/**\n * Check if user agent is whitelisted\n */\nfunction hasWhitelistedUserAgent(request: NextRequest, patterns: string[]): boolean {\n const userAgent = request.headers.get(\"user-agent\");\n if (!userAgent) return false;\n return patterns.some((pattern) => matchesPattern(userAgent, pattern));\n}\n\n/**\n * Create ATTEST middleware for Next.js\n *\n * @example\n * ```ts\n * // middleware.ts\n * import { createAttestMiddleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const middleware = createAttestMiddleware({\n * projectId: process.env.ATTEST_PROJECT_ID!,\n * apiKey: process.env.ATTEST_API_KEY!,\n * });\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n */\nexport function createAttestMiddleware(config: AttestMiddlewareConfig) {\n const {\n protectedRoutes = [\"/api/*\"],\n excludeRoutes = [],\n whitelistIPs = [],\n whitelistHeaders = {},\n whitelistTokens = [],\n whitelistUserAgents = [],\n allowUnauthenticated = false,\n onBlocked,\n onAllowed,\n customWhitelist,\n ...attestConfig\n } = config;\n\n return async function middleware(request: NextRequest) {\n const path = request.nextUrl.pathname;\n\n // Check if this path should be protected\n const isProtected = matchesAnyPattern(path, protectedRoutes);\n const isExcluded = matchesAnyPattern(path, excludeRoutes);\n\n if (!isProtected || isExcluded) {\n return NextResponse.next();\n }\n\n // === WHITELIST CHECKS (bypass ATTEST) ===\n\n // 1. IP whitelist (internal services, VPN, etc.)\n if (whitelistIPs.length > 0) {\n const clientIP = getClientIP(request);\n if (isIPWhitelisted(clientIP, whitelistIPs)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"ip-whitelist\");\n return response;\n }\n }\n\n // 2. Header whitelist (webhooks from Stripe, GitHub, etc.)\n if (Object.keys(whitelistHeaders).length > 0) {\n if (hasWhitelistedHeaders(request, whitelistHeaders)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"header-whitelist\");\n return response;\n }\n }\n\n // 3. Token whitelist (service-to-service auth)\n if (whitelistTokens.length > 0) {\n if (hasWhitelistedToken(request, whitelistTokens)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"token-whitelist\");\n return response;\n }\n }\n\n // 4. User agent whitelist (health checks, monitoring)\n if (whitelistUserAgents.length > 0) {\n if (hasWhitelistedUserAgent(request, whitelistUserAgents)) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"useragent-whitelist\");\n return response;\n }\n }\n\n // 5. Custom whitelist function\n if (customWhitelist) {\n const shouldBypass = await customWhitelist(request);\n if (shouldBypass) {\n const response = NextResponse.next();\n response.headers.set(\"x-attest-bypass\", \"custom-whitelist\");\n return response;\n }\n }\n\n // === ATTEST VERIFICATION ===\n\n // Verify with ATTEST\n const result = await verifyAttest(request, attestConfig);\n\n if (!result.attested) {\n // Allow through if configured and no headers present\n if (allowUnauthenticated && result.reason === \"Missing ATTEST headers\") {\n return NextResponse.next();\n }\n\n // Custom blocked handler\n if (onBlocked) {\n return onBlocked(request, result);\n }\n\n // Default blocked response\n return NextResponse.json(\n {\n error: \"Request not attested\",\n reason: result.reason,\n },\n { status: 403 }\n );\n }\n\n // Call allowed handler if provided\n if (onAllowed) {\n await onAllowed(request, result);\n }\n\n // Add fingerprint to headers for downstream use\n const response = NextResponse.next();\n if (result.fingerprint) {\n response.headers.set(\"x-attest-verified-fingerprint\", result.fingerprint);\n }\n\n return response;\n };\n}\n\n/**\n * Simple middleware that uses environment variables\n *\n * @example\n * ```ts\n * // middleware.ts\n * export { attestMiddleware as middleware } from \"@sekyuriti/attest/middleware\";\n *\n * export const config = {\n * matcher: \"/api/:path*\",\n * };\n * ```\n *\n * Requires environment variables:\n * - ATTEST_PROJECT_ID\n * - ATTEST_API_KEY\n */\nexport const attestMiddleware = createAttestMiddleware({\n projectId: process.env.ATTEST_PROJECT_ID || \"\",\n apiKey: process.env.ATTEST_API_KEY || \"\",\n});\n","/**\n * @sekyuriti/attest\n *\n * API protection for Next.js applications.\n * Verify that requests come from real browsers, not bots or scripts.\n */\n\nconst ATTEST_VERIFY_URL = \"https://sekyuriti.build/api/v2/attest/verify\";\n\nexport interface AttestConfig {\n /** Your ATTEST project ID (starts with ATST_) */\n projectId: string;\n /** Your ATTEST API key (keep this secret, server-side only) */\n apiKey: string;\n /** Custom verify URL (optional, for self-hosted) */\n verifyUrl?: string;\n /**\n * 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":";AAOA,SAAS,oBAAoB;;;ACA7B,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;AA+BA,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;;;ADxEA,SAAS,eAAe,MAAc,SAA0B;AAE9D,QAAM,eAAe,QAClB,QAAQ,OAAO,IAAI,EACnB,QAAQ,OAAO,GAAG;AACrB,SAAO,IAAI,OAAO,IAAI,YAAY,GAAG,EAAE,KAAK,IAAI;AAClD;AAKA,SAAS,kBAAkB,MAAc,UAA6B;AACpE,SAAO,SAAS,KAAK,CAAC,YAAY,eAAe,MAAM,OAAO,CAAC;AACjE;AAKA,SAAS,SAAS,IAAY,MAAuB;AAEnD,MAAI,CAAC,KAAK,SAAS,GAAG,GAAG;AACvB,WAAO,OAAO;AAAA,EAChB;AAEA,QAAM,CAAC,OAAO,IAAI,IAAI,KAAK,MAAM,GAAG;AACpC,QAAM,OAAO,SAAS,MAAM,EAAE;AAG9B,QAAM,UAAU,CAAC,UAA0B;AACzC,UAAM,QAAQ,MAAM,MAAM,GAAG,EAAE,IAAI,MAAM;AACzC,WAAQ,MAAM,CAAC,KAAK,KAAO,MAAM,CAAC,KAAK,KAAO,MAAM,CAAC,KAAK,IAAK,MAAM,CAAC;AAAA,EACxE;AAEA,QAAM,QAAQ,QAAQ,EAAE;AACxB,QAAM,WAAW,QAAQ,KAAK;AAC9B,QAAM,UAAU,GAAG,KAAM,KAAK,QAAS;AAEvC,UAAQ,QAAQ,cAAc,WAAW;AAC3C;AAKA,SAAS,gBAAgB,IAAmB,WAA8B;AACxE,MAAI,CAAC,MAAM,UAAU,WAAW,EAAG,QAAO;AAC1C,SAAO,UAAU,KAAK,CAAC,SAAS,SAAS,IAAI,IAAI,CAAC;AACpD;AAKA,SAAS,YAAY,SAAqC;AAExD,QAAM,eAAe,QAAQ,QAAQ,IAAI,iBAAiB;AAC1D,MAAI,cAAc;AAEhB,WAAO,aAAa,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAAA,EACzC;AAEA,QAAM,SAAS,QAAQ,QAAQ,IAAI,WAAW;AAC9C,MAAI,OAAQ,QAAO;AAGnB,QAAM,OAAO,QAAQ,QAAQ,IAAI,kBAAkB;AACnD,MAAI,KAAM,QAAO;AAGjB,QAAM,WAAW,QAAQ,QAAQ,IAAI,wBAAwB;AAC7D,MAAI,SAAU,QAAO,SAAS,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK;AAEjD,SAAO;AACT;AAKA,SAAS,sBACP,SACA,WACS;AACT,aAAW,CAAC,QAAQ,aAAa,KAAK,OAAO,QAAQ,SAAS,GAAG;AAC/D,UAAM,cAAc,QAAQ,QAAQ,IAAI,MAAM;AAC9C,QAAI,aAAa;AAEf,UAAI,kBAAkB,IAAK,QAAO;AAElC,UAAI,gBAAgB,cAAe,QAAO;AAAA,IAC5C;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,oBAAoB,SAAsB,QAA2B;AAC5E,QAAM,aAAa,QAAQ,QAAQ,IAAI,eAAe;AACtD,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO,OAAO,SAAS,UAAU;AACnC;AAKA,SAAS,wBAAwB,SAAsB,UAA6B;AAClF,QAAM,YAAY,QAAQ,QAAQ,IAAI,YAAY;AAClD,MAAI,CAAC,UAAW,QAAO;AACvB,SAAO,SAAS,KAAK,CAAC,YAAY,eAAe,WAAW,OAAO,CAAC;AACtE;AAoBO,SAAS,uBAAuB,QAAgC;AACrE,QAAM;AAAA,IACJ,kBAAkB,CAAC,QAAQ;AAAA,IAC3B,gBAAgB,CAAC;AAAA,IACjB,eAAe,CAAC;AAAA,IAChB,mBAAmB,CAAC;AAAA,IACpB,kBAAkB,CAAC;AAAA,IACnB,sBAAsB,CAAC;AAAA,IACvB,uBAAuB;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AAEJ,SAAO,eAAe,WAAW,SAAsB;AACrD,UAAM,OAAO,QAAQ,QAAQ;AAG7B,UAAM,cAAc,kBAAkB,MAAM,eAAe;AAC3D,UAAM,aAAa,kBAAkB,MAAM,aAAa;AAExD,QAAI,CAAC,eAAe,YAAY;AAC9B,aAAO,aAAa,KAAK;AAAA,IAC3B;AAKA,QAAI,aAAa,SAAS,GAAG;AAC3B,YAAM,WAAW,YAAY,OAAO;AACpC,UAAI,gBAAgB,UAAU,YAAY,GAAG;AAC3C,cAAMA,YAAW,aAAa,KAAK;AACnC,QAAAA,UAAS,QAAQ,IAAI,mBAAmB,cAAc;AACtD,eAAOA;AAAA,MACT;AAAA,IACF;AAGA,QAAI,OAAO,KAAK,gBAAgB,EAAE,SAAS,GAAG;AAC5C,UAAI,sBAAsB,SAAS,gBAAgB,GAAG;AACpD,cAAMA,YAAW,aAAa,KAAK;AACnC,QAAAA,UAAS,QAAQ,IAAI,mBAAmB,kBAAkB;AAC1D,eAAOA;AAAA,MACT;AAAA,IACF;AAGA,QAAI,gBAAgB,SAAS,GAAG;AAC9B,UAAI,oBAAoB,SAAS,eAAe,GAAG;AACjD,cAAMA,YAAW,aAAa,KAAK;AACnC,QAAAA,UAAS,QAAQ,IAAI,mBAAmB,iBAAiB;AACzD,eAAOA;AAAA,MACT;AAAA,IACF;AAGA,QAAI,oBAAoB,SAAS,GAAG;AAClC,UAAI,wBAAwB,SAAS,mBAAmB,GAAG;AACzD,cAAMA,YAAW,aAAa,KAAK;AACnC,QAAAA,UAAS,QAAQ,IAAI,mBAAmB,qBAAqB;AAC7D,eAAOA;AAAA,MACT;AAAA,IACF;AAGA,QAAI,iBAAiB;AACnB,YAAM,eAAe,MAAM,gBAAgB,OAAO;AAClD,UAAI,cAAc;AAChB,cAAMA,YAAW,aAAa,KAAK;AACnC,QAAAA,UAAS,QAAQ,IAAI,mBAAmB,kBAAkB;AAC1D,eAAOA;AAAA,MACT;AAAA,IACF;AAKA,UAAM,SAAS,MAAM,aAAa,SAAS,YAAY;AAEvD,QAAI,CAAC,OAAO,UAAU;AAEpB,UAAI,wBAAwB,OAAO,WAAW,0BAA0B;AACtE,eAAO,aAAa,KAAK;AAAA,MAC3B;AAGA,UAAI,WAAW;AACb,eAAO,UAAU,SAAS,MAAM;AAAA,MAClC;AAGA,aAAO,aAAa;AAAA,QAClB;AAAA,UACE,OAAO;AAAA,UACP,QAAQ,OAAO;AAAA,QACjB;AAAA,QACA,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AAGA,QAAI,WAAW;AACb,YAAM,UAAU,SAAS,MAAM;AAAA,IACjC;AAGA,UAAM,WAAW,aAAa,KAAK;AACnC,QAAI,OAAO,aAAa;AACtB,eAAS,QAAQ,IAAI,iCAAiC,OAAO,WAAW;AAAA,IAC1E;AAEA,WAAO;AAAA,EACT;AACF;AAmBO,IAAM,mBAAmB,uBAAuB;AAAA,EACrD,WAAW,QAAQ,IAAI,qBAAqB;AAAA,EAC5C,QAAQ,QAAQ,IAAI,kBAAkB;AACxC,CAAC;","names":["response"]}
|