@astrasyncai/adapter-edge 0.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # @astrasyncai/adapter-edge
2
+
3
+ **AstraSync Trusted Agent Gateway** — edge adapters for **Vercel Edge**,
4
+ **Cloudflare Workers**, and **Fastly Compute**. The gateway classifies and
5
+ verifies inbound AI-agent traffic in front of your existing site. Zero site
6
+ changes; **observe-only by default**.
7
+
8
+ Every request is classified into a visibility tier — verified AstraSync
9
+ agents, identified-but-unregistered agents (payment-rail credentials such as
10
+ Visa Intelligent Commerce, Mastercard Agent Pay, Visa TAP, Web Bot Auth,
11
+ ACP, UCP, AP2, MPP, x402, or platform-agent UAs like Claude and ChatGPT),
12
+ anonymous bots, and humans. Humans and static assets are untouched — zero
13
+ API calls, zero added latency. Cryptographic verification of rail
14
+ credentials happens **server-side** at AstraSync's verify-access; the edge
15
+ only sniffs and forwards raw artifacts.
16
+
17
+ **Fail-open guarantee:** if the gateway can't reach AstraSync it always
18
+ falls back to observe + pass-through. An unreachable backend — or a
19
+ misconfigured gateway — can never break your site.
20
+
21
+ All three adapters share the same engine
22
+ (`@astrasyncai/verification-gateway/edge-core`) as the Lambda@Edge adapter
23
+ (`@astrasyncai/adapter-lambda`); use that package for CloudFront.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install @astrasyncai/adapter-edge
29
+ ```
30
+
31
+ ## Quickstart
32
+
33
+ ### Cloudflare Workers
34
+
35
+ ```ts
36
+ // src/worker.ts
37
+ import { createGatewayHandler } from '@astrasyncai/adapter-edge/cloudflare';
38
+
39
+ export default { fetch: createGatewayHandler() };
40
+ ```
41
+
42
+ Set the environment bindings on the worker (`wrangler.toml` vars +
43
+ `wrangler secret put ASTRASYNC_API_KEY`):
44
+
45
+ ```toml
46
+ [vars]
47
+ ASTRASYNC_COUNTERPARTY_ID = "ASTRAE-…"
48
+ ASTRASYNC_COUNTERPARTY_URL = "https://example.shop"
49
+ ```
50
+
51
+ Deploy the worker on the routes you want covered. Pass traffic is forwarded
52
+ to your origin with `fetch()`; telemetry runs in `ctx.waitUntil`, off the
53
+ latency path.
54
+
55
+ ### Vercel Edge Middleware
56
+
57
+ This package does not import `next`, so the middleware returns either a
58
+ `Response` (return it as-is) or a **pass signal** you apply with the
59
+ provided helper — a two-line wrapper:
60
+
61
+ ```ts
62
+ // middleware.ts
63
+ import { NextResponse, type NextRequest } from 'next/server';
64
+ import { createGatewayMiddleware, applyGatewayPass } from '@astrasyncai/adapter-edge/vercel';
65
+
66
+ const gateway = createGatewayMiddleware();
67
+
68
+ export async function middleware(request: NextRequest) {
69
+ const result = await gateway(request);
70
+ if (result instanceof Response) return result;
71
+ return NextResponse.next({ request: { headers: applyGatewayPass(request.headers, result) } });
72
+ }
73
+ ```
74
+
75
+ Configuration comes from `process.env` (`ASTRASYNC_COUNTERPARTY_ID`,
76
+ `ASTRASYNC_COUNTERPARTY_URL`, `ASTRASYNC_API_KEY`) or explicit options.
77
+ Alternatively pass a `next` factory option to get back a plain
78
+ `Response | undefined` middleware.
79
+
80
+ ### Fastly Compute (JavaScript)
81
+
82
+ Fastly requires every `fetch` to name a **configured backend**, so
83
+ `originBackend` is required — create a backend for your origin (and one
84
+ for `astrasync.ai` so verification calls can leave the service):
85
+
86
+ ```ts
87
+ // src/index.ts
88
+ import { createGatewayHandler } from '@astrasyncai/adapter-edge/fastly';
89
+
90
+ const gateway = createGatewayHandler({
91
+ originBackend: 'origin',
92
+ counterpartyId: 'ASTRAE-…',
93
+ counterpartyUrl: 'https://example.shop',
94
+ apiKey: '…', // e.g. from a Fastly secret store lookup at startup
95
+ });
96
+
97
+ addEventListener('fetch', (event) => event.respondWith(gateway(event)));
98
+ ```
99
+
100
+ Optional geolocation enrichment (country/city/ASN) via the Fastly runtime
101
+ is behind the `geo: true` flag; the `fastly:geolocation` import is dynamic,
102
+ so the base build stays runtime-neutral.
103
+
104
+ ## Configuration
105
+
106
+ | Option | Env var | Default | Notes |
107
+ | ----------------- | ---------------------------- | -------------------------- | ------------------------------------------------------------------------- |
108
+ | `counterpartyId` | `ASTRASYNC_COUNTERPARTY_ID` | — | Your property's `ASTRAE-…` endpoint id. Missing ⇒ fail-open pass-through. |
109
+ | `counterpartyUrl` | `ASTRASYNC_COUNTERPARTY_URL` | — | Canonical public URL of the property. |
110
+ | `apiKey` | `ASTRASYNC_API_KEY` | — | Merchant `kya_*` key. Observe mode degrades gracefully without it. |
111
+ | `apiBaseUrl` | `ASTRASYNC_API_BASE_URL` | `https://astrasync.ai/api` | |
112
+ | `configTtlMs` | — | 60 000 | Dashboard edge-config refresh interval. |
113
+ | `budgets` | — | 300 / 1500 / 3000 ms | Beacon / observe-verify / enforce-verify time budgets. |
114
+ | `maxBodyBytes` | — | 1 MiB | Body read cap for commerce-protocol detection. |
115
+ | `fallbackConfig` | — | observe/classify | Used before the first config fetch succeeds. |
116
+ | `originBackend` | — | — | **Fastly only, required.** Named backend for origin traffic. |
117
+ | `geo` | — | `false` | **Fastly only.** Opt-in `fastly:geolocation` enrichment. |
118
+ | `next` | — | — | **Vercel only.** Continue-factory for pass outcomes. |
119
+
120
+ Mode (`observe`/`enforce`), check depth (`classify`/`authenticate`/
121
+ `authorize`), per-path rules, and bot-telemetry sampling are configured on
122
+ the endpoint's **Edge verification** panel in the AstraSync dashboard — no
123
+ redeploys. The gateway refreshes its config every ~60s, and configs stale
124
+ for more than 24h automatically degrade enforce back to observe.
125
+
126
+ ## Capture matrix
127
+
128
+ Beyond the sanitized header capture (identical on every platform — secrets
129
+ stripped, credential headers reduced to a safe format prefix), each
130
+ platform contributes the connection-layer signals it can see:
131
+
132
+ | Signal | Cloudflare Workers | Vercel Edge | Fastly Compute | (Lambda@Edge) |
133
+ | ------------------ | ------------------------------------------------------ | --------------------------------------- | ----------------------- | --------------------------------- |
134
+ | Client IP | `CF-Connecting-IP` | `x-real-ip` | `Fastly-Client-IP` | viewer address |
135
+ | Country | `request.cf.country` | `x-vercel-ip-country` | `geo` flag | viewer headers |
136
+ | Region/City | `request.cf.regionCode` / `.city` | `x-vercel-ip-country-region` / `-city` | `geo` flag | viewer headers |
137
+ | Continent | `request.cf.continent` | `x-vercel-ip-continent` | — | — |
138
+ | Time zone | `request.cf.timezone` | `x-vercel-ip-timezone` | `geo` flag (UTC offset) | viewer headers |
139
+ | ASN / AS org | `request.cf.asn` / `.asOrganization` | — | `geo` flag | viewer headers (ASN only) |
140
+ | TLS version/cipher | `request.cf.tlsVersion` / `.tlsCipher` | — | — | viewer headers (ver+cipher) |
141
+ | HTTP version | `request.cf.httpProtocol` | — | — | viewer headers |
142
+ | **JA3 / JA4** | `request.cf.botManagement` (Bot Management zones only) | `x-vercel-ja4-digest` (BotID; JA4 only) | — | **viewer headers (both, 2.2.0+)** |
143
+ | Header structure | — | — | — | viewer header-order/-count |
144
+ | PoP/colo, TCP RTT | `request.cf.colo` / `.clientTcpRtt` | — | — | — |
145
+
146
+ Everything captured surfaces on the endpoint's traffic feed in the
147
+ AstraSync dashboard. For the cross-integration view (edge vs SDK-behind-CDN
148
+ vs platform-managed CDN) see the canonical capture matrix in the
149
+ [`@astrasyncai/verification-gateway` README](https://www.npmjs.com/package/@astrasyncai/verification-gateway).
150
+ CloudFront (adapter-lambda) is currently the only platform yielding **both**
151
+ JA3 and JA4 unconditionally — if fingerprint-based spoof resistance is the
152
+ deciding factor for your CDN choice, that's the row to read.
153
+
154
+ ## Enforce mode
155
+
156
+ Requests that fail verification on enforced paths get a machine-readable
157
+ `403` with registration guidance; human-approval flows return `202` with
158
+ step-up polling info. Allowed agent requests reach your origin with trust
159
+ attestation headers (`X-AstraSync-Trust-Score`, `X-AstraSync-Agent-Id`,
160
+ `X-AstraSync-Developer-Id`, `X-AstraSync-Permissions`) — inbound spoofs of
161
+ `x-astrasync-*` headers are stripped in both modes.
162
+
163
+ ## License
164
+
165
+ MIT © AstraSync
@@ -0,0 +1,42 @@
1
+ import { E as EdgeGatewayOptions } from './types-7lIOikBV.js';
2
+ export { G as GatewayPass } from './types-7lIOikBV.js';
3
+ import '@astrasyncai/verification-gateway/edge-config';
4
+ import '@astrasyncai/verification-gateway/edge-core';
5
+
6
+ /**
7
+ * AstraSync Trusted Agent Gateway — Cloudflare Workers adapter.
8
+ *
9
+ * ```ts
10
+ * import { createGatewayHandler } from '@astrasyncai/adapter-edge/cloudflare';
11
+ *
12
+ * export default { fetch: createGatewayHandler() };
13
+ * ```
14
+ *
15
+ * Configuration comes from the worker's environment bindings
16
+ * (`ASTRASYNC_API_KEY`, `ASTRASYNC_COUNTERPARTY_ID`,
17
+ * `ASTRASYNC_COUNTERPARTY_URL`, optional `ASTRASYNC_API_BASE_URL`) or from
18
+ * explicit options, which win. The handler NEVER blocks traffic outside
19
+ * enforce mode and fails open on any internal error.
20
+ */
21
+
22
+ /** Structural subset of the Workers `ExecutionContext`. */
23
+ interface CloudflareExecutionContext {
24
+ waitUntil(promise: Promise<unknown>): void;
25
+ }
26
+ type CloudflareEnv = Record<string, unknown>;
27
+ type CloudflareFetchHandler = (request: Request, env?: CloudflareEnv, ctx?: CloudflareExecutionContext) => Promise<Response>;
28
+ /**
29
+ * Connection-layer signals Cloudflare exposes on `request.cf` plus the
30
+ * `CF-Connecting-IP` header. Absent values are simply omitted.
31
+ */
32
+ declare function collectCloudflareConnection(request: Request): Record<string, string>;
33
+ /**
34
+ * Create the worker `fetch` handler. Pass traffic is forwarded to the
35
+ * origin with `fetch(mutatedRequest)` (spoofable trust headers stripped,
36
+ * attestation headers injected); enforce-mode denials short-circuit with a
37
+ * machine-readable JSON response. Telemetry beacons are scheduled through
38
+ * `ctx.waitUntil`, so they never add request latency.
39
+ */
40
+ declare function createGatewayHandler(options?: EdgeGatewayOptions): CloudflareFetchHandler;
41
+
42
+ export { type CloudflareEnv, type CloudflareExecutionContext, type CloudflareFetchHandler, EdgeGatewayOptions, collectCloudflareConnection, createGatewayHandler };
@@ -0,0 +1,193 @@
1
+ // src/cloudflare.ts
2
+ import { evaluateEdgeRequest } from "@astrasyncai/verification-gateway/edge-core";
3
+
4
+ // src/config.ts
5
+ import {
6
+ getEdgeConfig,
7
+ DEFAULT_EDGE_CONFIG
8
+ } from "@astrasyncai/verification-gateway/edge-config";
9
+ var DEFAULT_API_BASE_URL = "https://astrasync.ai/api";
10
+ function defaultLogger(prefix) {
11
+ return (level, message, data) => {
12
+ console[level === "info" ? "log" : level](`[${prefix}] ${message}`, data ?? "");
13
+ };
14
+ }
15
+ function resolveOptions(options, env, logPrefix) {
16
+ const e = env ?? {};
17
+ return {
18
+ ...options,
19
+ apiBaseUrl: options.apiBaseUrl ?? e.ASTRASYNC_API_BASE_URL ?? DEFAULT_API_BASE_URL,
20
+ counterpartyId: options.counterpartyId ?? e.ASTRASYNC_COUNTERPARTY_ID,
21
+ counterpartyUrl: options.counterpartyUrl ?? e.ASTRASYNC_COUNTERPARTY_URL,
22
+ apiKey: options.apiKey ?? e.ASTRASYNC_API_KEY,
23
+ log: options.log ?? defaultLogger(logPrefix)
24
+ };
25
+ }
26
+ function isConfigured(resolved) {
27
+ if (resolved.counterpartyId && resolved.counterpartyUrl) return true;
28
+ resolved.log(
29
+ "error",
30
+ "counterpartyId/counterpartyUrl are not configured \u2014 passing all traffic through. Set the ASTRASYNC_COUNTERPARTY_ID / ASTRASYNC_COUNTERPARTY_URL environment variables or pass them as options."
31
+ );
32
+ return false;
33
+ }
34
+ async function loadConfig(resolved) {
35
+ try {
36
+ return await getEdgeConfig(
37
+ { apiBaseUrl: resolved.apiBaseUrl, apiKey: resolved.apiKey },
38
+ resolved.counterpartyId,
39
+ { ttlMs: resolved.configTtlMs }
40
+ );
41
+ } catch {
42
+ return resolved.fallbackConfig ?? DEFAULT_EDGE_CONFIG;
43
+ }
44
+ }
45
+
46
+ // src/web.ts
47
+ var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
48
+ async function readBodyCapped(request, cap) {
49
+ if (request.method === "GET" || request.method === "HEAD") return { truncated: false };
50
+ const body = request.clone().body;
51
+ if (!body) return { truncated: false };
52
+ const reader = body.getReader();
53
+ const chunks = [];
54
+ let total = 0;
55
+ let truncated = false;
56
+ for (; ; ) {
57
+ const { done, value } = await reader.read();
58
+ if (done) break;
59
+ if (total + value.byteLength > cap) {
60
+ chunks.push(value.subarray(0, cap - total));
61
+ total = cap;
62
+ truncated = true;
63
+ void reader.cancel().catch(() => {
64
+ });
65
+ break;
66
+ }
67
+ chunks.push(value);
68
+ total += value.byteLength;
69
+ }
70
+ if (total === 0 && !truncated) return { truncated: false };
71
+ const joined = new Uint8Array(total);
72
+ let offset = 0;
73
+ for (const chunk of chunks) {
74
+ joined.set(chunk, offset);
75
+ offset += chunk.byteLength;
76
+ }
77
+ return { text: new TextDecoder().decode(joined), truncated };
78
+ }
79
+ async function normalizeWebRequest(request, options = {}) {
80
+ const url = new URL(request.url);
81
+ const headers = {};
82
+ request.headers.forEach((value, name) => {
83
+ headers[name] = value;
84
+ });
85
+ const body = await readBodyCapped(request, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
86
+ const connection = options.connection ?? {};
87
+ return {
88
+ method: request.method,
89
+ uri: url.pathname,
90
+ querystring: url.search.startsWith("?") ? url.search.slice(1) : url.search,
91
+ headers,
92
+ ...body.text !== void 0 && { bodyText: body.text },
93
+ ...body.truncated && { bodyTruncated: true },
94
+ ...options.clientIp && { clientIp: options.clientIp },
95
+ ...Object.keys(connection).length > 0 && { connection }
96
+ };
97
+ }
98
+ function applyGatewayPass(headers, pass) {
99
+ const out = new Headers(headers);
100
+ for (const prefix of pass.stripHeaderPrefixes) {
101
+ const p = prefix.toLowerCase();
102
+ for (const name of [...out.keys()]) {
103
+ if (name.startsWith(p)) out.delete(name);
104
+ }
105
+ }
106
+ for (const [name, value] of Object.entries(pass.injectHeaders)) {
107
+ out.set(name, value);
108
+ }
109
+ return out;
110
+ }
111
+ function buildPassRequest(request, pass) {
112
+ if (pass.stripHeaderPrefixes.length === 0 && Object.keys(pass.injectHeaders).length === 0) {
113
+ return request;
114
+ }
115
+ return new Request(request, { headers: applyGatewayPass(request.headers, pass) });
116
+ }
117
+ function toJsonResponse(application) {
118
+ return new Response(JSON.stringify(application.bodyJson), {
119
+ status: application.status,
120
+ headers: { "content-type": "application/json", ...application.headers }
121
+ });
122
+ }
123
+
124
+ // src/cloudflare.ts
125
+ function collectCloudflareConnection(request) {
126
+ const cf = request.cf;
127
+ const out = {};
128
+ const ip = request.headers.get("cf-connecting-ip");
129
+ if (ip) out.ip = ip;
130
+ if (!cf) return out;
131
+ if (cf.country) out.country = cf.country;
132
+ if (cf.city) out.city = cf.city;
133
+ if (cf.asn !== void 0 && cf.asn !== null && cf.asn !== "") out.asn = String(cf.asn);
134
+ if (cf.asOrganization) out.asOrg = cf.asOrganization;
135
+ if (cf.tlsVersion) out.tlsVersion = cf.tlsVersion;
136
+ if (cf.tlsCipher) out.tlsCipher = cf.tlsCipher;
137
+ if (cf.httpProtocol) out.httpVersion = cf.httpProtocol;
138
+ if (cf.colo) out.colo = cf.colo;
139
+ if (cf.timezone) out.timeZone = cf.timezone;
140
+ if (cf.regionCode) out.region = cf.regionCode;
141
+ if (cf.continent) out.continent = cf.continent;
142
+ if (typeof cf.clientTcpRtt === "number") out.clientTcpRtt = String(cf.clientTcpRtt);
143
+ if (cf.botManagement?.ja3Hash) out.tlsFingerprint = cf.botManagement.ja3Hash;
144
+ if (cf.botManagement?.ja4) out.ja4 = cf.botManagement.ja4;
145
+ return out;
146
+ }
147
+ function envStrings(env) {
148
+ const out = {};
149
+ for (const [key, value] of Object.entries(env ?? {})) {
150
+ if (typeof value === "string") out[key] = value;
151
+ }
152
+ return out;
153
+ }
154
+ function createGatewayHandler(options = {}) {
155
+ return async function fetchHandler(request, env, ctx) {
156
+ const resolved = resolveOptions(options, envStrings(env), "astrasync-edge/cloudflare");
157
+ try {
158
+ if (!isConfigured(resolved)) return fetch(request);
159
+ const config = await loadConfig(resolved);
160
+ const normalized = await normalizeWebRequest(request, {
161
+ maxBodyBytes: resolved.maxBodyBytes,
162
+ clientIp: request.headers.get("cf-connecting-ip") ?? void 0,
163
+ connection: collectCloudflareConnection(request)
164
+ });
165
+ const { application } = await evaluateEdgeRequest(
166
+ {
167
+ apiBaseUrl: resolved.apiBaseUrl,
168
+ counterpartyId: resolved.counterpartyId,
169
+ counterpartyUrl: resolved.counterpartyUrl,
170
+ budgets: resolved.budgets,
171
+ random: resolved.random,
172
+ log: resolved.log,
173
+ ...ctx && { waitUntil: (p) => ctx.waitUntil(p) }
174
+ },
175
+ normalized,
176
+ config,
177
+ resolved.apiKey
178
+ );
179
+ if (application.action === "respond") return toJsonResponse(application);
180
+ return fetch(buildPassRequest(request, application));
181
+ } catch (err) {
182
+ resolved.log("error", "gateway error \u2014 passing request through", {
183
+ error: err instanceof Error ? err.message : String(err)
184
+ });
185
+ return fetch(request);
186
+ }
187
+ };
188
+ }
189
+ export {
190
+ collectCloudflareConnection,
191
+ createGatewayHandler
192
+ };
193
+ //# sourceMappingURL=cloudflare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cloudflare.ts","../src/config.ts","../src/web.ts"],"sourcesContent":["/**\n * AstraSync Trusted Agent Gateway — Cloudflare Workers adapter.\n *\n * ```ts\n * import { createGatewayHandler } from '@astrasyncai/adapter-edge/cloudflare';\n *\n * export default { fetch: createGatewayHandler() };\n * ```\n *\n * Configuration comes from the worker's environment bindings\n * (`ASTRASYNC_API_KEY`, `ASTRASYNC_COUNTERPARTY_ID`,\n * `ASTRASYNC_COUNTERPARTY_URL`, optional `ASTRASYNC_API_BASE_URL`) or from\n * explicit options, which win. The handler NEVER blocks traffic outside\n * enforce mode and fails open on any internal error.\n */\n\nimport { evaluateEdgeRequest } from '@astrasyncai/verification-gateway/edge-core';\nimport { resolveOptions, isConfigured, loadConfig } from './config';\nimport type { EdgeGatewayOptions } from './types';\nimport { normalizeWebRequest, buildPassRequest, toJsonResponse } from './web';\n\nexport type { EdgeGatewayOptions, GatewayPass } from './types';\n\n/** The subset of `request.cf` connection properties the gateway captures. */\ninterface CfConnectionProperties {\n country?: string;\n asn?: number | string;\n asOrganization?: string;\n tlsVersion?: string;\n tlsCipher?: string;\n httpProtocol?: string;\n city?: string;\n colo?: string;\n timezone?: string;\n regionCode?: string;\n continent?: string;\n clientTcpRtt?: number;\n /** Present only on zones with Bot Management entitlement. */\n botManagement?: {\n ja3Hash?: string;\n ja4?: string;\n };\n}\n\n/** Structural subset of the Workers `ExecutionContext`. */\nexport interface CloudflareExecutionContext {\n waitUntil(promise: Promise<unknown>): void;\n}\n\nexport type CloudflareEnv = Record<string, unknown>;\n\nexport type CloudflareFetchHandler = (\n request: Request,\n env?: CloudflareEnv,\n ctx?: CloudflareExecutionContext\n) => Promise<Response>;\n\n/**\n * Connection-layer signals Cloudflare exposes on `request.cf` plus the\n * `CF-Connecting-IP` header. Absent values are simply omitted.\n */\nexport function collectCloudflareConnection(request: Request): Record<string, string> {\n const cf = (request as Request & { cf?: CfConnectionProperties }).cf;\n const out: Record<string, string> = {};\n const ip = request.headers.get('cf-connecting-ip');\n if (ip) out.ip = ip;\n if (!cf) return out;\n if (cf.country) out.country = cf.country;\n if (cf.city) out.city = cf.city;\n if (cf.asn !== undefined && cf.asn !== null && cf.asn !== '') out.asn = String(cf.asn);\n if (cf.asOrganization) out.asOrg = cf.asOrganization;\n if (cf.tlsVersion) out.tlsVersion = cf.tlsVersion;\n if (cf.tlsCipher) out.tlsCipher = cf.tlsCipher;\n if (cf.httpProtocol) out.httpVersion = cf.httpProtocol;\n if (cf.colo) out.colo = cf.colo;\n if (cf.timezone) out.timeZone = cf.timezone;\n if (cf.regionCode) out.region = cf.regionCode;\n if (cf.continent) out.continent = cf.continent;\n if (typeof cf.clientTcpRtt === 'number') out.clientTcpRtt = String(cf.clientTcpRtt);\n // JA3/JA4 ride request.cf only on Bot Management zones — harmless elsewhere.\n if (cf.botManagement?.ja3Hash) out.tlsFingerprint = cf.botManagement.ja3Hash;\n if (cf.botManagement?.ja4) out.ja4 = cf.botManagement.ja4;\n return out;\n}\n\nfunction envStrings(env: CloudflareEnv | undefined): Record<string, string | undefined> {\n const out: Record<string, string | undefined> = {};\n for (const [key, value] of Object.entries(env ?? {})) {\n if (typeof value === 'string') out[key] = value;\n }\n return out;\n}\n\n/**\n * Create the worker `fetch` handler. Pass traffic is forwarded to the\n * origin with `fetch(mutatedRequest)` (spoofable trust headers stripped,\n * attestation headers injected); enforce-mode denials short-circuit with a\n * machine-readable JSON response. Telemetry beacons are scheduled through\n * `ctx.waitUntil`, so they never add request latency.\n */\nexport function createGatewayHandler(options: EdgeGatewayOptions = {}): CloudflareFetchHandler {\n return async function fetchHandler(request, env, ctx) {\n const resolved = resolveOptions(options, envStrings(env), 'astrasync-edge/cloudflare');\n try {\n if (!isConfigured(resolved)) return fetch(request);\n\n const config = await loadConfig(resolved);\n const normalized = await normalizeWebRequest(request, {\n maxBodyBytes: resolved.maxBodyBytes,\n clientIp: request.headers.get('cf-connecting-ip') ?? undefined,\n connection: collectCloudflareConnection(request),\n });\n\n const { application } = await evaluateEdgeRequest(\n {\n apiBaseUrl: resolved.apiBaseUrl,\n counterpartyId: resolved.counterpartyId,\n counterpartyUrl: resolved.counterpartyUrl,\n budgets: resolved.budgets,\n random: resolved.random,\n log: resolved.log,\n ...(ctx && { waitUntil: (p: Promise<unknown>) => ctx.waitUntil(p) }),\n },\n normalized,\n config,\n resolved.apiKey\n );\n\n if (application.action === 'respond') return toJsonResponse(application);\n return fetch(buildPassRequest(request, application));\n } catch (err) {\n // Fail-open, unconditionally: the gateway must never take the site down.\n resolved.log('error', 'gateway error — passing request through', {\n error: err instanceof Error ? err.message : String(err),\n });\n return fetch(request);\n }\n };\n}\n","/**\n * Option/environment resolution and dashboard EdgeConfig loading shared by\n * the platform adapters.\n */\n\nimport {\n getEdgeConfig,\n DEFAULT_EDGE_CONFIG,\n type EdgeConfig,\n} from '@astrasyncai/verification-gateway/edge-config';\nimport type { EdgeLogger } from '@astrasyncai/verification-gateway/edge-core';\nimport type { EdgeGatewayOptions } from './types';\n\nexport const DEFAULT_API_BASE_URL = 'https://astrasync.ai/api';\n\nexport interface ResolvedGatewayOptions extends EdgeGatewayOptions {\n apiBaseUrl: string;\n log: EdgeLogger;\n}\n\nexport function defaultLogger(prefix: string): EdgeLogger {\n return (level, message, data) => {\n console[level === 'info' ? 'log' : level](`[${prefix}] ${message}`, data ?? '');\n };\n}\n\n/**\n * Merge explicit options with runtime environment variables (options win)\n * and apply defaults. `env` is the platform's environment map — Cloudflare\n * passes the worker `env` binding object, Vercel passes `process.env`,\n * Fastly Compute has no ambient env map (configure via options).\n */\nexport function resolveOptions(\n options: EdgeGatewayOptions,\n env: Record<string, string | undefined> | undefined,\n logPrefix: string\n): ResolvedGatewayOptions {\n const e = env ?? {};\n return {\n ...options,\n apiBaseUrl: options.apiBaseUrl ?? e.ASTRASYNC_API_BASE_URL ?? DEFAULT_API_BASE_URL,\n counterpartyId: options.counterpartyId ?? e.ASTRASYNC_COUNTERPARTY_ID,\n counterpartyUrl: options.counterpartyUrl ?? e.ASTRASYNC_COUNTERPARTY_URL,\n apiKey: options.apiKey ?? e.ASTRASYNC_API_KEY,\n log: options.log ?? defaultLogger(logPrefix),\n };\n}\n\n/**\n * Missing identity means the gateway cannot evaluate anything — it fails\n * OPEN (all traffic passes untouched) and says why, once per request.\n */\nexport function isConfigured(\n resolved: ResolvedGatewayOptions\n): resolved is ResolvedGatewayOptions & { counterpartyId: string; counterpartyUrl: string } {\n if (resolved.counterpartyId && resolved.counterpartyUrl) return true;\n resolved.log(\n 'error',\n 'counterpartyId/counterpartyUrl are not configured — passing all traffic through. ' +\n 'Set the ASTRASYNC_COUNTERPARTY_ID / ASTRASYNC_COUNTERPARTY_URL environment variables ' +\n 'or pass them as options.'\n );\n return false;\n}\n\n/**\n * Load the effective EdgeConfig. Delegates caching/fallback/degradation to\n * the SDK (60s TTL, stale-while-revalidate, enforce→observe degradation on\n * prolonged staleness) and never throws.\n */\nexport async function loadConfig(\n resolved: ResolvedGatewayOptions & { counterpartyId: string }\n): Promise<EdgeConfig> {\n try {\n return await getEdgeConfig(\n { apiBaseUrl: resolved.apiBaseUrl, apiKey: resolved.apiKey },\n resolved.counterpartyId,\n { ttlMs: resolved.configTtlMs }\n );\n } catch {\n return resolved.fallbackConfig ?? DEFAULT_EDGE_CONFIG;\n }\n}\n","/**\n * Shared web-standard platform I/O: converts a fetch-API `Request` into\n * the edge core's `NormalizedRequest`, and applies the core's abstract\n * outcome application back onto fetch-API primitives.\n */\n\nimport type {\n NormalizedRequest,\n OutcomeApplication,\n} from '@astrasyncai/verification-gateway/edge-core';\nimport type { GatewayPass } from './types';\n\n/** Default request-body read cap for commerce-protocol detection. */\nexport const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;\n\nexport interface WebNormalizeOptions {\n /** Body read cap in bytes. Default {@link DEFAULT_MAX_BODY_BYTES}. */\n maxBodyBytes?: number;\n /** Client IP as the platform reports it. */\n clientIp?: string;\n /** Connection-layer metadata the platform collected (country, asn, …). */\n connection?: Record<string, string>;\n}\n\nasync function readBodyCapped(\n request: Request,\n cap: number\n): Promise<{ text?: string; truncated: boolean }> {\n if (request.method === 'GET' || request.method === 'HEAD') return { truncated: false };\n const body = request.clone().body;\n if (!body) return { truncated: false };\n\n const reader = body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n let truncated = false;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (total + value.byteLength > cap) {\n chunks.push(value.subarray(0, cap - total));\n total = cap;\n truncated = true;\n // Fire-and-forget: cancelling one branch of a cloned (tee'd) body can\n // only settle once the other branch is consumed — never await it.\n void reader.cancel().catch(() => {});\n break;\n }\n chunks.push(value);\n total += value.byteLength;\n }\n if (total === 0 && !truncated) return { truncated: false };\n\n const joined = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n joined.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return { text: new TextDecoder().decode(joined), truncated };\n}\n\n/**\n * Normalize a fetch-API `Request` into the edge core's request shape.\n * Header names are already lowercased (and multi-valued headers joined\n * with `\", \"`) by the `Headers` class. The body is read from a CLONE — the\n * original request stays forwardable to the origin — and capped at\n * `maxBodyBytes` (detection sees a flagged, truncated prefix beyond it).\n */\nexport async function normalizeWebRequest(\n request: Request,\n options: WebNormalizeOptions = {}\n): Promise<NormalizedRequest> {\n const url = new URL(request.url);\n const headers: Record<string, string> = {};\n request.headers.forEach((value, name) => {\n headers[name] = value;\n });\n const body = await readBodyCapped(request, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);\n const connection = options.connection ?? {};\n return {\n method: request.method,\n uri: url.pathname,\n querystring: url.search.startsWith('?') ? url.search.slice(1) : url.search,\n headers,\n ...(body.text !== undefined && { bodyText: body.text }),\n ...(body.truncated && { bodyTruncated: true }),\n ...(options.clientIp && { clientIp: options.clientIp }),\n ...(Object.keys(connection).length > 0 && { connection }),\n };\n}\n\n/**\n * Apply a pass application to a header set: strip every header matching\n * one of the spoofable prefixes, then set the injected trust headers.\n * Returns a NEW `Headers` instance; the input is not mutated.\n */\nexport function applyGatewayPass(headers: Headers, pass: GatewayPass): Headers {\n const out = new Headers(headers);\n for (const prefix of pass.stripHeaderPrefixes) {\n const p = prefix.toLowerCase();\n for (const name of [...out.keys()]) {\n if (name.startsWith(p)) out.delete(name);\n }\n }\n for (const [name, value] of Object.entries(pass.injectHeaders)) {\n out.set(name, value);\n }\n return out;\n}\n\n/**\n * Build the origin-bound request for a pass application: the same request\n * with spoofable headers stripped and trust headers injected.\n */\nexport function buildPassRequest(request: Request, pass: GatewayPass): Request {\n if (pass.stripHeaderPrefixes.length === 0 && Object.keys(pass.injectHeaders).length === 0) {\n return request;\n }\n return new Request(request, { headers: applyGatewayPass(request.headers, pass) });\n}\n\n/** Build the short-circuit JSON response for a respond application. */\nexport function toJsonResponse(\n application: Extract<OutcomeApplication, { action: 'respond' }>\n): Response {\n return new Response(JSON.stringify(application.bodyJson), {\n status: application.status,\n headers: { 'content-type': 'application/json', ...application.headers },\n });\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,63 @@
1
+ import { E as EdgeGatewayOptions } from './types-7lIOikBV.js';
2
+ export { G as GatewayPass } from './types-7lIOikBV.js';
3
+ import '@astrasyncai/verification-gateway/edge-config';
4
+ import '@astrasyncai/verification-gateway/edge-core';
5
+
6
+ /**
7
+ * AstraSync Trusted Agent Gateway — Fastly Compute (JavaScript) adapter.
8
+ *
9
+ * ```ts
10
+ * import { createGatewayHandler } from '@astrasyncai/adapter-edge/fastly';
11
+ *
12
+ * const gateway = createGatewayHandler({
13
+ * originBackend: 'origin', // the backend name configured on your service
14
+ * counterpartyId: 'ASTRAE-…',
15
+ * counterpartyUrl: 'https://example.shop',
16
+ * apiKey: '…',
17
+ * });
18
+ *
19
+ * addEventListener('fetch', (event) => event.respondWith(gateway(event)));
20
+ * ```
21
+ *
22
+ * Fastly Compute requires every `fetch` to name a configured backend, so
23
+ * `originBackend` is REQUIRED (the AstraSync API must also be reachable —
24
+ * see the README for the api backend setup). Fastly has no ambient
25
+ * environment map; configure via options.
26
+ *
27
+ * Geolocation enrichment (`fastly:geolocation`) is available behind the
28
+ * opt-in `geo` flag: the import is dynamic, so the base build carries no
29
+ * `fastly:*` imports and runs in any test runner.
30
+ */
31
+
32
+ interface FastlyGatewayOptions extends EdgeGatewayOptions {
33
+ /**
34
+ * REQUIRED. The name of the backend (as configured on the Fastly
35
+ * service) that origin-bound traffic is forwarded to — Fastly Compute
36
+ * fetches must name a backend explicitly.
37
+ */
38
+ originBackend: string;
39
+ /**
40
+ * Opt-in geolocation enrichment via the `fastly:geolocation` runtime
41
+ * module (country/city/ASN on the connection capture). Off by default so
42
+ * the base build has no `fastly:*` imports.
43
+ */
44
+ geo?: boolean;
45
+ }
46
+ /** Structural subset of the Fastly Compute fetch event. */
47
+ interface FastlyFetchEvent {
48
+ request: Request;
49
+ client?: {
50
+ address?: string;
51
+ };
52
+ waitUntil?(promise: Promise<unknown>): void;
53
+ }
54
+ type FastlyGatewayHandler = (event: FastlyFetchEvent) => Promise<Response>;
55
+ /**
56
+ * Create the fetch-event handler. Pass traffic is forwarded with
57
+ * `fetch(mutatedRequest, { backend: originBackend })`; enforce-mode
58
+ * denials short-circuit with a machine-readable JSON response. Telemetry
59
+ * beacons are scheduled through `event.waitUntil` when available.
60
+ */
61
+ declare function createGatewayHandler(options: FastlyGatewayOptions): FastlyGatewayHandler;
62
+
63
+ export { EdgeGatewayOptions, type FastlyFetchEvent, type FastlyGatewayHandler, type FastlyGatewayOptions, createGatewayHandler };