@astrasyncai/adapter-edge 1.9.1 → 1.10.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 +47 -3
- package/dist/akamai.d.ts +92 -0
- package/dist/akamai.js +176 -0
- package/dist/akamai.js.map +1 -0
- package/dist/cloudflare.d.ts +10 -2
- package/dist/cloudflare.js +4 -3
- package/dist/cloudflare.js.map +1 -1
- package/package.json +10 -4
- package/templates/akamai/README.md +16 -0
- package/templates/akamai/bundle.json +4 -0
- package/templates/akamai/main.js +18 -0
- package/templates/cloudflare/package.json +15 -0
- package/templates/cloudflare/src/compose.ts +14 -0
- package/templates/cloudflare/src/worker.ts +5 -0
- package/templates/cloudflare/wrangler.toml +26 -0
- package/templates/fastly/fastly.toml +17 -0
- package/templates/fastly/package.json +9 -0
- package/templates/fastly/src/index.ts +15 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @astrasyncai/adapter-edge
|
|
2
2
|
|
|
3
3
|
**AstraSync Trusted Agent Gateway** — edge adapters for **Vercel Edge**,
|
|
4
|
-
**Cloudflare Workers**, and **
|
|
4
|
+
**Cloudflare Workers**, **Fastly Compute**, and **Akamai EdgeWorkers**. The gateway classifies and
|
|
5
5
|
verifies inbound AI-agent traffic in front of your existing site. Zero site
|
|
6
6
|
changes; **observe-only by default**.
|
|
7
7
|
|
|
@@ -18,7 +18,12 @@ only sniffs and forwards raw artifacts.
|
|
|
18
18
|
falls back to observe + pass-through. An unreachable backend — or a
|
|
19
19
|
misconfigured gateway — can never break your site.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
**Where it runs:** after your CDN's own firewall and bot rules. Requests
|
|
22
|
+
those rules block never reach the adapter. To see the agents your edge
|
|
23
|
+
blocks, connect its security logs on the endpoint page (**Edge logs**, see
|
|
24
|
+
<https://astrasync.ai/docs/edge-log-connectors>).
|
|
25
|
+
|
|
26
|
+
All the adapters share the same engine
|
|
22
27
|
(`@astrasyncai/verification-gateway/edge-core`) as the Lambda@Edge adapter
|
|
23
28
|
(`@astrasyncai/adapter-lambda`); use that package for CloudFront.
|
|
24
29
|
|
|
@@ -52,6 +57,18 @@ Deploy the worker on the routes you want covered. Pass traffic is forwarded
|
|
|
52
57
|
to your origin with `fetch()`; telemetry runs in `ctx.waitUntil`, off the
|
|
53
58
|
latency path.
|
|
54
59
|
|
|
60
|
+
Ready-made project: `templates/cloudflare` in this package (`wrangler.toml`,
|
|
61
|
+
worker, and `compose.ts`). If your zone **already runs a Worker on the
|
|
62
|
+
route** (Cloudflare allows one per route), pass your handler as `forward`.
|
|
63
|
+
The gateway runs first, and passed requests go to your code instead of
|
|
64
|
+
straight to the origin:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
export default {
|
|
68
|
+
fetch: createGatewayHandler({ forward: (req, env, ctx) => existing.fetch(req, env, ctx) }),
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
55
72
|
### Vercel Edge Middleware
|
|
56
73
|
|
|
57
74
|
This package does not import `next`, so the middleware returns either a
|
|
@@ -101,6 +118,30 @@ Optional geolocation enrichment (country/city/ASN) via the Fastly runtime
|
|
|
101
118
|
is behind the `geo: true` flag; the `fastly:geolocation` import is dynamic,
|
|
102
119
|
so the base build stays runtime-neutral.
|
|
103
120
|
|
|
121
|
+
### Akamai EdgeWorkers
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
// main.js
|
|
125
|
+
import { httpRequest } from 'http-request';
|
|
126
|
+
import { createOnClientRequest } from '@astrasyncai/adapter-edge/akamai';
|
|
127
|
+
|
|
128
|
+
export const onClientRequest = createOnClientRequest({
|
|
129
|
+
httpRequest,
|
|
130
|
+
counterpartyId: 'ASTRAE-…',
|
|
131
|
+
counterpartyUrl: 'https://www.example.com',
|
|
132
|
+
apiKey: '…',
|
|
133
|
+
apiBaseUrl: 'https://www.example.com/.astrasync/api',
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Runs in `onClientRequest`, after App & API Protector and Bot Manager.
|
|
138
|
+
Detection is header-only, because request bodies aren't readable at this stage.
|
|
139
|
+
The client IP comes from `True-Client-IP`, so enable that property behaviour.
|
|
140
|
+
Sub-requests go to a hostname on your property: add the rule in
|
|
141
|
+
`templates/akamai/README.md` that routes `/.astrasync/api/*` to `astrasync.ai`.
|
|
142
|
+
EdgeWorkers lacks `fetch`, so the adapter installs minimal shims over the
|
|
143
|
+
`httpRequest` you pass in.
|
|
144
|
+
|
|
104
145
|
## Configuration
|
|
105
146
|
|
|
106
147
|
| Option | Env var | Default | Notes |
|
|
@@ -141,7 +182,10 @@ platform contributes the connection-layer signals it can see:
|
|
|
141
182
|
| HTTP version | `request.cf.httpProtocol` | — | — | viewer headers |
|
|
142
183
|
| **JA3 / JA4** | `request.cf.botManagement` (Bot Management zones only) | `x-vercel-ja4-digest` (BotID; JA4 only) | — | **viewer headers (both, 2.2.0+)** |
|
|
143
184
|
| Header structure | — | — | — | viewer header-order/-count |
|
|
144
|
-
|
|
185
|
+
|
|
186
|
+
**Akamai EdgeWorkers:** client IP (`True-Client-IP`), country, region, city,
|
|
187
|
+
continent and postal code from `request.userLocation`. No TLS fingerprints.
|
|
188
|
+
| PoP/colo, TCP RTT | `request.cf.colo` / `.clientTcpRtt` | — | — | — |
|
|
145
189
|
|
|
146
190
|
Everything captured surfaces on the endpoint's traffic feed in the
|
|
147
191
|
AstraSync dashboard. For the cross-integration view (edge vs SDK-behind-CDN
|
package/dist/akamai.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { NormalizedRequest } from '@astrasyncai/verification-gateway/edge-core';
|
|
2
|
+
import { E as EdgeGatewayOptions } from './types-BhaNxFeN.js';
|
|
3
|
+
import '@astrasyncai/verification-gateway/edge-config';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AstraSync Trusted Agent Gateway — Akamai EdgeWorkers adapter (round 31).
|
|
7
|
+
*
|
|
8
|
+
* ```js
|
|
9
|
+
* // main.js (EdgeWorkers bundle)
|
|
10
|
+
* import { httpRequest } from 'http-request';
|
|
11
|
+
* import { createOnClientRequest } from '@astrasyncai/adapter-edge/akamai';
|
|
12
|
+
*
|
|
13
|
+
* export const onClientRequest = createOnClientRequest({
|
|
14
|
+
* httpRequest,
|
|
15
|
+
* counterpartyId: 'ASTRAE-…',
|
|
16
|
+
* counterpartyUrl: 'https://www.example.com',
|
|
17
|
+
* apiKey: '…', // from an EdgeKV item or a PMUSER variable at bundle time
|
|
18
|
+
* apiBaseUrl: 'https://www.example.com/.astrasync/api', // see below
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Runs in `onClientRequest`, after Akamai's App & API Protector / Bot
|
|
23
|
+
* Manager — requests those block never reach it (connect the Akamai SIEM
|
|
24
|
+
* stream as an edge-log connector to see them). v1 is header-only: the
|
|
25
|
+
* request body is not readable in `onClientRequest`, so body-borne commerce
|
|
26
|
+
* protocols are not detected at this edge (headers still are).
|
|
27
|
+
*
|
|
28
|
+
* EdgeWorkers has no `fetch` / `Headers` / `AbortSignal.timeout`; the
|
|
29
|
+
* adapter installs minimal versions backed by the `http-request` module you
|
|
30
|
+
* pass in, only where the runtime lacks them. Sub-requests may need to target
|
|
31
|
+
* a hostname served by your Akamai property: route a path such as
|
|
32
|
+
* `/.astrasync/api/*` to the `astrasync.ai` origin and point `apiBaseUrl`
|
|
33
|
+
* there.
|
|
34
|
+
*
|
|
35
|
+
* Observe never blocks; any internal error passes the request through.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** The EdgeWorkers `http-request` module's `httpRequest` (structural). */
|
|
39
|
+
type AkamaiHttpRequest = (url: string, options?: {
|
|
40
|
+
method?: string;
|
|
41
|
+
headers?: Record<string, string | string[]>;
|
|
42
|
+
body?: string;
|
|
43
|
+
timeout?: number;
|
|
44
|
+
}) => Promise<{
|
|
45
|
+
status: number;
|
|
46
|
+
ok?: boolean;
|
|
47
|
+
getHeader?(name: string): string[] | null | undefined;
|
|
48
|
+
text(): Promise<string>;
|
|
49
|
+
json?(): Promise<unknown>;
|
|
50
|
+
}>;
|
|
51
|
+
/** The subset of the EdgeWorkers `Request` the gateway uses (structural). */
|
|
52
|
+
interface AkamaiRequest {
|
|
53
|
+
method: string;
|
|
54
|
+
host: string;
|
|
55
|
+
path: string;
|
|
56
|
+
query?: string;
|
|
57
|
+
scheme?: string;
|
|
58
|
+
getHeaders(): Record<string, string[]>;
|
|
59
|
+
setHeader(name: string, value: string | string[]): void;
|
|
60
|
+
removeHeader(name: string): void;
|
|
61
|
+
respondWith(status: number, headers: Record<string, string | string[]>, body: string, denyReason?: string): void;
|
|
62
|
+
userLocation?: {
|
|
63
|
+
country?: string;
|
|
64
|
+
region?: string;
|
|
65
|
+
city?: string;
|
|
66
|
+
continent?: string;
|
|
67
|
+
zipCode?: string;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
interface AkamaiGatewayOptions extends EdgeGatewayOptions {
|
|
71
|
+
/** The EdgeWorkers `httpRequest` from `import { httpRequest } from 'http-request'`. */
|
|
72
|
+
httpRequest: AkamaiHttpRequest;
|
|
73
|
+
/** Header carrying the client IP. Default `true-client-ip` (enable it on the property). */
|
|
74
|
+
clientIpHeader?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Install fetch-API shims over `httpRequest` where the runtime lacks them.
|
|
78
|
+
* Exported for tests; idempotent; never replaces a native implementation.
|
|
79
|
+
*/
|
|
80
|
+
declare function installAkamaiFetchShims(httpRequest: AkamaiHttpRequest, timeoutMs?: number): void;
|
|
81
|
+
/** Normalise an EdgeWorkers request into the edge core's shape (header-only). */
|
|
82
|
+
declare function normalizeAkamaiRequest(request: AkamaiRequest, clientIpHeader?: string): NormalizedRequest;
|
|
83
|
+
/**
|
|
84
|
+
* Create the `onClientRequest` handler. A pass mutates the request in place
|
|
85
|
+
* (spoofable `x-astrasync-*` headers stripped, trust headers set) and lets
|
|
86
|
+
* Akamai forward it; an enforce-mode deny answers via `respondWith`.
|
|
87
|
+
* EdgeWorkers has no `waitUntil`, so telemetry is awaited under the
|
|
88
|
+
* gateway's beacon budget.
|
|
89
|
+
*/
|
|
90
|
+
declare function createOnClientRequest(options: AkamaiGatewayOptions): (request: AkamaiRequest) => Promise<void>;
|
|
91
|
+
|
|
92
|
+
export { type AkamaiGatewayOptions, type AkamaiHttpRequest, type AkamaiRequest, EdgeGatewayOptions, createOnClientRequest, installAkamaiFetchShims, normalizeAkamaiRequest };
|
package/dist/akamai.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// src/akamai.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
|
+
publicBaseUrl: options.publicBaseUrl ?? e.ASTRASYNC_PUBLIC_BASE_URL,
|
|
21
|
+
counterpartyId: options.counterpartyId ?? e.ASTRASYNC_COUNTERPARTY_ID,
|
|
22
|
+
counterpartyUrl: options.counterpartyUrl ?? e.ASTRASYNC_COUNTERPARTY_URL,
|
|
23
|
+
apiKey: options.apiKey ?? e.ASTRASYNC_API_KEY,
|
|
24
|
+
log: options.log ?? defaultLogger(logPrefix)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function isConfigured(resolved) {
|
|
28
|
+
if (resolved.counterpartyId && resolved.counterpartyUrl) return true;
|
|
29
|
+
resolved.log(
|
|
30
|
+
"error",
|
|
31
|
+
"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."
|
|
32
|
+
);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
async function loadConfig(resolved) {
|
|
36
|
+
try {
|
|
37
|
+
return await getEdgeConfig(
|
|
38
|
+
{ apiBaseUrl: resolved.apiBaseUrl, apiKey: resolved.apiKey },
|
|
39
|
+
resolved.counterpartyId,
|
|
40
|
+
{ ttlMs: resolved.configTtlMs }
|
|
41
|
+
);
|
|
42
|
+
} catch {
|
|
43
|
+
return resolved.fallbackConfig ?? DEFAULT_EDGE_CONFIG;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/akamai.ts
|
|
48
|
+
var MiniHeaders = class _MiniHeaders {
|
|
49
|
+
m = /* @__PURE__ */ new Map();
|
|
50
|
+
constructor(init) {
|
|
51
|
+
if (init instanceof _MiniHeaders) init.forEach((v, k) => this.set(k, v));
|
|
52
|
+
else if (Array.isArray(init)) for (const [k, v] of init) this.set(k, v);
|
|
53
|
+
else if (init) for (const k of Object.keys(init)) this.set(k, init[k]);
|
|
54
|
+
}
|
|
55
|
+
get(name) {
|
|
56
|
+
return this.m.get(name.toLowerCase()) ?? null;
|
|
57
|
+
}
|
|
58
|
+
set(name, value) {
|
|
59
|
+
this.m.set(name.toLowerCase(), String(value));
|
|
60
|
+
}
|
|
61
|
+
has(name) {
|
|
62
|
+
return this.m.has(name.toLowerCase());
|
|
63
|
+
}
|
|
64
|
+
delete(name) {
|
|
65
|
+
this.m.delete(name.toLowerCase());
|
|
66
|
+
}
|
|
67
|
+
forEach(fn) {
|
|
68
|
+
this.m.forEach((v, k) => fn(v, k));
|
|
69
|
+
}
|
|
70
|
+
toObject() {
|
|
71
|
+
return Object.fromEntries(this.m);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
function installAkamaiFetchShims(httpRequest, timeoutMs = 3e3) {
|
|
75
|
+
const g = globalThis;
|
|
76
|
+
if (typeof g.Headers === "undefined") g.Headers = MiniHeaders;
|
|
77
|
+
const Abort = g.AbortSignal;
|
|
78
|
+
if (!Abort) g.AbortSignal = { timeout: () => void 0 };
|
|
79
|
+
else if (typeof Abort.timeout !== "function") Abort.timeout = () => void 0;
|
|
80
|
+
if (typeof g.fetch === "function") return;
|
|
81
|
+
g.fetch = async (input, init = {}) => {
|
|
82
|
+
const url = typeof input === "string" ? input : String(input.url ?? input);
|
|
83
|
+
const headers = init.headers instanceof MiniHeaders ? init.headers.toObject() : Array.isArray(init.headers) ? Object.fromEntries(init.headers) : init.headers ?? {};
|
|
84
|
+
const res = await httpRequest(url, {
|
|
85
|
+
method: init.method ?? "GET",
|
|
86
|
+
headers,
|
|
87
|
+
...typeof init.body === "string" && { body: init.body },
|
|
88
|
+
timeout: timeoutMs
|
|
89
|
+
});
|
|
90
|
+
const text = await res.text();
|
|
91
|
+
return {
|
|
92
|
+
ok: res.ok ?? (res.status >= 200 && res.status < 300),
|
|
93
|
+
status: res.status,
|
|
94
|
+
headers: { get: (name) => res.getHeader?.(name)?.[0] ?? null },
|
|
95
|
+
text: async () => text,
|
|
96
|
+
json: async () => JSON.parse(text)
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function normalizeAkamaiRequest(request, clientIpHeader = "true-client-ip") {
|
|
101
|
+
const headers = {};
|
|
102
|
+
for (const [name, values] of Object.entries(request.getHeaders() ?? {})) {
|
|
103
|
+
headers[name.toLowerCase()] = Array.isArray(values) ? values.join(", ") : String(values);
|
|
104
|
+
}
|
|
105
|
+
const loc = request.userLocation ?? {};
|
|
106
|
+
const connection = {};
|
|
107
|
+
const clientIp = headers[clientIpHeader.toLowerCase()];
|
|
108
|
+
if (clientIp) connection.ip = clientIp;
|
|
109
|
+
if (loc.country) connection.country = loc.country;
|
|
110
|
+
if (loc.region) connection.region = loc.region;
|
|
111
|
+
if (loc.city) connection.city = loc.city;
|
|
112
|
+
if (loc.continent) connection.continent = loc.continent;
|
|
113
|
+
if (loc.zipCode) connection.postalCode = loc.zipCode;
|
|
114
|
+
return {
|
|
115
|
+
method: request.method,
|
|
116
|
+
uri: request.path,
|
|
117
|
+
querystring: request.query ?? "",
|
|
118
|
+
headers,
|
|
119
|
+
...clientIp && { clientIp },
|
|
120
|
+
...Object.keys(connection).length > 0 && { connection }
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function createOnClientRequest(options) {
|
|
124
|
+
return async function onClientRequest(request) {
|
|
125
|
+
const resolved = resolveOptions(options, void 0, "astrasync-edge/akamai");
|
|
126
|
+
try {
|
|
127
|
+
if (!isConfigured(resolved)) return;
|
|
128
|
+
installAkamaiFetchShims(options.httpRequest, resolved.budgets?.verifyEnforceMs ?? 3e3);
|
|
129
|
+
const config = await loadConfig(resolved);
|
|
130
|
+
const normalized = normalizeAkamaiRequest(request, options.clientIpHeader);
|
|
131
|
+
const { application } = await evaluateEdgeRequest(
|
|
132
|
+
{
|
|
133
|
+
apiBaseUrl: resolved.apiBaseUrl,
|
|
134
|
+
publicBaseUrl: resolved.publicBaseUrl,
|
|
135
|
+
counterpartyId: resolved.counterpartyId,
|
|
136
|
+
counterpartyUrl: resolved.counterpartyUrl,
|
|
137
|
+
budgets: resolved.budgets,
|
|
138
|
+
adapter: "akamai",
|
|
139
|
+
random: resolved.random,
|
|
140
|
+
log: resolved.log
|
|
141
|
+
},
|
|
142
|
+
normalized,
|
|
143
|
+
config,
|
|
144
|
+
resolved.apiKey
|
|
145
|
+
);
|
|
146
|
+
if (application.action === "respond") {
|
|
147
|
+
request.respondWith(
|
|
148
|
+
application.status,
|
|
149
|
+
{ "content-type": ["application/json"], ...application.headers },
|
|
150
|
+
JSON.stringify(application.bodyJson),
|
|
151
|
+
"astrasync-gateway"
|
|
152
|
+
);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
for (const prefix of application.stripHeaderPrefixes) {
|
|
156
|
+
const p = prefix.toLowerCase();
|
|
157
|
+
for (const name of Object.keys(normalized.headers)) {
|
|
158
|
+
if (name.startsWith(p)) request.removeHeader(name);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const [name, value] of Object.entries(application.injectHeaders)) {
|
|
162
|
+
request.setHeader(name, value);
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
resolved.log("error", "gateway error \u2014 passing request through", {
|
|
166
|
+
error: err instanceof Error ? err.message : String(err)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
createOnClientRequest,
|
|
173
|
+
installAkamaiFetchShims,
|
|
174
|
+
normalizeAkamaiRequest
|
|
175
|
+
};
|
|
176
|
+
//# sourceMappingURL=akamai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/akamai.ts","../src/config.ts"],"sourcesContent":["/**\n * AstraSync Trusted Agent Gateway — Akamai EdgeWorkers adapter (round 31).\n *\n * ```js\n * // main.js (EdgeWorkers bundle)\n * import { httpRequest } from 'http-request';\n * import { createOnClientRequest } from '@astrasyncai/adapter-edge/akamai';\n *\n * export const onClientRequest = createOnClientRequest({\n * httpRequest,\n * counterpartyId: 'ASTRAE-…',\n * counterpartyUrl: 'https://www.example.com',\n * apiKey: '…', // from an EdgeKV item or a PMUSER variable at bundle time\n * apiBaseUrl: 'https://www.example.com/.astrasync/api', // see below\n * });\n * ```\n *\n * Runs in `onClientRequest`, after Akamai's App & API Protector / Bot\n * Manager — requests those block never reach it (connect the Akamai SIEM\n * stream as an edge-log connector to see them). v1 is header-only: the\n * request body is not readable in `onClientRequest`, so body-borne commerce\n * protocols are not detected at this edge (headers still are).\n *\n * EdgeWorkers has no `fetch` / `Headers` / `AbortSignal.timeout`; the\n * adapter installs minimal versions backed by the `http-request` module you\n * pass in, only where the runtime lacks them. Sub-requests may need to target\n * a hostname served by your Akamai property: route a path such as\n * `/.astrasync/api/*` to the `astrasync.ai` origin and point `apiBaseUrl`\n * there.\n *\n * Observe never blocks; any internal error passes the request through.\n */\n\nimport { evaluateEdgeRequest } from '@astrasyncai/verification-gateway/edge-core';\nimport type { NormalizedRequest } from '@astrasyncai/verification-gateway/edge-core';\nimport { resolveOptions, isConfigured, loadConfig } from './config';\nimport type { EdgeGatewayOptions } from './types';\n\n/** The EdgeWorkers `http-request` module's `httpRequest` (structural). */\nexport type AkamaiHttpRequest = (\n url: string,\n options?: {\n method?: string;\n headers?: Record<string, string | string[]>;\n body?: string;\n timeout?: number;\n }\n) => Promise<{\n status: number;\n ok?: boolean;\n getHeader?(name: string): string[] | null | undefined;\n text(): Promise<string>;\n json?(): Promise<unknown>;\n}>;\n\n/** The subset of the EdgeWorkers `Request` the gateway uses (structural). */\nexport interface AkamaiRequest {\n method: string;\n host: string;\n path: string;\n query?: string;\n scheme?: string;\n getHeaders(): Record<string, string[]>;\n setHeader(name: string, value: string | string[]): void;\n removeHeader(name: string): void;\n respondWith(\n status: number,\n headers: Record<string, string | string[]>,\n body: string,\n denyReason?: string\n ): void;\n userLocation?: {\n country?: string;\n region?: string;\n city?: string;\n continent?: string;\n zipCode?: string;\n };\n}\n\nexport interface AkamaiGatewayOptions extends EdgeGatewayOptions {\n /** The EdgeWorkers `httpRequest` from `import { httpRequest } from 'http-request'`. */\n httpRequest: AkamaiHttpRequest;\n /** Header carrying the client IP. Default `true-client-ip` (enable it on the property). */\n clientIpHeader?: string;\n}\n\n/** Minimal `Headers` for runtimes without one (case-insensitive, string values). */\nclass MiniHeaders {\n private readonly m = new Map<string, string>();\n constructor(init?: Record<string, string> | Array<[string, string]> | MiniHeaders) {\n if (init instanceof MiniHeaders) init.forEach((v, k) => this.set(k, v));\n else if (Array.isArray(init)) for (const [k, v] of init) this.set(k, v);\n else if (init) for (const k of Object.keys(init)) this.set(k, init[k]);\n }\n get(name: string): string | null {\n return this.m.get(name.toLowerCase()) ?? null;\n }\n set(name: string, value: string): void {\n this.m.set(name.toLowerCase(), String(value));\n }\n has(name: string): boolean {\n return this.m.has(name.toLowerCase());\n }\n delete(name: string): void {\n this.m.delete(name.toLowerCase());\n }\n forEach(fn: (value: string, name: string) => void): void {\n this.m.forEach((v, k) => fn(v, k));\n }\n toObject(): Record<string, string> {\n return Object.fromEntries(this.m);\n }\n}\n\n/**\n * Install fetch-API shims over `httpRequest` where the runtime lacks them.\n * Exported for tests; idempotent; never replaces a native implementation.\n */\nexport function installAkamaiFetchShims(httpRequest: AkamaiHttpRequest, timeoutMs = 3000): void {\n const g = globalThis as Record<string, unknown>;\n if (typeof g.Headers === 'undefined') g.Headers = MiniHeaders;\n const Abort = g.AbortSignal as { timeout?: unknown } | undefined;\n if (!Abort) g.AbortSignal = { timeout: () => undefined };\n else if (typeof Abort.timeout !== 'function') Abort.timeout = () => undefined;\n if (typeof g.fetch === 'function') return;\n g.fetch = async (\n input: unknown,\n init: { method?: string; headers?: unknown; body?: unknown } = {}\n ) => {\n const url =\n typeof input === 'string' ? input : String((input as { url?: string }).url ?? input);\n const headers =\n init.headers instanceof MiniHeaders\n ? init.headers.toObject()\n : Array.isArray(init.headers)\n ? Object.fromEntries(init.headers as Array<[string, string]>)\n : ((init.headers as Record<string, string> | undefined) ?? {});\n const res = await httpRequest(url, {\n method: init.method ?? 'GET',\n headers,\n ...(typeof init.body === 'string' && { body: init.body }),\n timeout: timeoutMs,\n });\n const text = await res.text();\n return {\n ok: res.ok ?? (res.status >= 200 && res.status < 300),\n status: res.status,\n headers: { get: (name: string) => res.getHeader?.(name)?.[0] ?? null },\n text: async () => text,\n json: async () => JSON.parse(text),\n };\n };\n}\n\n/** Normalise an EdgeWorkers request into the edge core's shape (header-only). */\nexport function normalizeAkamaiRequest(\n request: AkamaiRequest,\n clientIpHeader = 'true-client-ip'\n): NormalizedRequest {\n const headers: Record<string, string> = {};\n for (const [name, values] of Object.entries(request.getHeaders() ?? {})) {\n headers[name.toLowerCase()] = Array.isArray(values) ? values.join(', ') : String(values);\n }\n const loc = request.userLocation ?? {};\n const connection: Record<string, string> = {};\n const clientIp = headers[clientIpHeader.toLowerCase()];\n if (clientIp) connection.ip = clientIp;\n if (loc.country) connection.country = loc.country;\n if (loc.region) connection.region = loc.region;\n if (loc.city) connection.city = loc.city;\n if (loc.continent) connection.continent = loc.continent;\n if (loc.zipCode) connection.postalCode = loc.zipCode;\n return {\n method: request.method,\n uri: request.path,\n querystring: request.query ?? '',\n headers,\n ...(clientIp && { clientIp }),\n ...(Object.keys(connection).length > 0 && { connection }),\n };\n}\n\n/**\n * Create the `onClientRequest` handler. A pass mutates the request in place\n * (spoofable `x-astrasync-*` headers stripped, trust headers set) and lets\n * Akamai forward it; an enforce-mode deny answers via `respondWith`.\n * EdgeWorkers has no `waitUntil`, so telemetry is awaited under the\n * gateway's beacon budget.\n */\nexport function createOnClientRequest(options: AkamaiGatewayOptions) {\n return async function onClientRequest(request: AkamaiRequest): Promise<void> {\n const resolved = resolveOptions(options, undefined, 'astrasync-edge/akamai');\n try {\n if (!isConfigured(resolved)) return;\n installAkamaiFetchShims(options.httpRequest, resolved.budgets?.verifyEnforceMs ?? 3000);\n const config = await loadConfig(resolved);\n const normalized = normalizeAkamaiRequest(request, options.clientIpHeader);\n const { application } = await evaluateEdgeRequest(\n {\n apiBaseUrl: resolved.apiBaseUrl,\n publicBaseUrl: resolved.publicBaseUrl,\n counterpartyId: resolved.counterpartyId,\n counterpartyUrl: resolved.counterpartyUrl,\n budgets: resolved.budgets,\n adapter: 'akamai',\n random: resolved.random,\n log: resolved.log,\n },\n normalized,\n config,\n resolved.apiKey\n );\n if (application.action === 'respond') {\n request.respondWith(\n application.status,\n { 'content-type': ['application/json'], ...application.headers },\n JSON.stringify(application.bodyJson),\n 'astrasync-gateway'\n );\n return;\n }\n for (const prefix of application.stripHeaderPrefixes) {\n const p = prefix.toLowerCase();\n for (const name of Object.keys(normalized.headers)) {\n if (name.startsWith(p)) request.removeHeader(name);\n }\n }\n for (const [name, value] of Object.entries(application.injectHeaders)) {\n request.setHeader(name, value);\n }\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 }\n };\n}\n\nexport type { EdgeGatewayOptions } from './types';\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 publicBaseUrl: options.publicBaseUrl ?? e.ASTRASYNC_PUBLIC_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"],"mappings":"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","names":[]}
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -24,6 +24,14 @@ interface CloudflareExecutionContext {
|
|
|
24
24
|
waitUntil(promise: Promise<unknown>): void;
|
|
25
25
|
}
|
|
26
26
|
type CloudflareEnv = Record<string, unknown>;
|
|
27
|
+
interface CloudflareGatewayOptions extends EdgeGatewayOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Round 31 — where pass traffic goes. Default `fetch` (your origin). Zones
|
|
30
|
+
* that already run a Worker on the route (one Worker per route) pass their
|
|
31
|
+
* existing handler here, so the gateway runs first and their logic second.
|
|
32
|
+
*/
|
|
33
|
+
forward?: (request: Request, env?: CloudflareEnv, ctx?: CloudflareExecutionContext) => Promise<Response>;
|
|
34
|
+
}
|
|
27
35
|
type CloudflareFetchHandler = (request: Request, env?: CloudflareEnv, ctx?: CloudflareExecutionContext) => Promise<Response>;
|
|
28
36
|
/**
|
|
29
37
|
* Connection-layer signals Cloudflare exposes on `request.cf` plus the
|
|
@@ -37,6 +45,6 @@ declare function collectCloudflareConnection(request: Request): Record<string, s
|
|
|
37
45
|
* machine-readable JSON response. Telemetry beacons are scheduled through
|
|
38
46
|
* `ctx.waitUntil`, so they never add request latency.
|
|
39
47
|
*/
|
|
40
|
-
declare function createGatewayHandler(options?:
|
|
48
|
+
declare function createGatewayHandler(options?: CloudflareGatewayOptions): CloudflareFetchHandler;
|
|
41
49
|
|
|
42
|
-
export { type CloudflareEnv, type CloudflareExecutionContext, type CloudflareFetchHandler, EdgeGatewayOptions, collectCloudflareConnection, createGatewayHandler };
|
|
50
|
+
export { type CloudflareEnv, type CloudflareExecutionContext, type CloudflareFetchHandler, type CloudflareGatewayOptions, EdgeGatewayOptions, collectCloudflareConnection, createGatewayHandler };
|
package/dist/cloudflare.js
CHANGED
|
@@ -154,9 +154,10 @@ function envStrings(env) {
|
|
|
154
154
|
}
|
|
155
155
|
function createGatewayHandler(options = {}) {
|
|
156
156
|
return async function fetchHandler(request, env, ctx) {
|
|
157
|
+
const forward = (r) => options.forward ? options.forward(r, env, ctx) : fetch(r);
|
|
157
158
|
const resolved = resolveOptions(options, envStrings(env), "astrasync-edge/cloudflare");
|
|
158
159
|
try {
|
|
159
|
-
if (!isConfigured(resolved)) return
|
|
160
|
+
if (!isConfigured(resolved)) return forward(request);
|
|
160
161
|
const config = await loadConfig(resolved);
|
|
161
162
|
const normalized = await normalizeWebRequest(request, {
|
|
162
163
|
maxBodyBytes: resolved.maxBodyBytes,
|
|
@@ -180,12 +181,12 @@ function createGatewayHandler(options = {}) {
|
|
|
180
181
|
resolved.apiKey
|
|
181
182
|
);
|
|
182
183
|
if (application.action === "respond") return toJsonResponse(application);
|
|
183
|
-
return
|
|
184
|
+
return forward(buildPassRequest(request, application));
|
|
184
185
|
} catch (err) {
|
|
185
186
|
resolved.log("error", "gateway error \u2014 passing request through", {
|
|
186
187
|
error: err instanceof Error ? err.message : String(err)
|
|
187
188
|
});
|
|
188
|
-
return
|
|
189
|
+
return forward(request);
|
|
189
190
|
}
|
|
190
191
|
};
|
|
191
192
|
}
|
package/dist/cloudflare.js.map
CHANGED
|
@@ -1 +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 publicBaseUrl: resolved.publicBaseUrl,\n counterpartyId: resolved.counterpartyId,\n counterpartyUrl: resolved.counterpartyUrl,\n budgets: resolved.budgets,\n adapter: 'cloudflare',\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 publicBaseUrl: options.publicBaseUrl ?? e.ASTRASYNC_PUBLIC_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":[]}
|
|
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 interface CloudflareGatewayOptions extends EdgeGatewayOptions {\n /**\n * Round 31 — where pass traffic goes. Default `fetch` (your origin). Zones\n * that already run a Worker on the route (one Worker per route) pass their\n * existing handler here, so the gateway runs first and their logic second.\n */\n forward?: (\n request: Request,\n env?: CloudflareEnv,\n ctx?: CloudflareExecutionContext\n ) => Promise<Response>;\n}\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(\n options: CloudflareGatewayOptions = {}\n): CloudflareFetchHandler {\n return async function fetchHandler(request, env, ctx) {\n const forward = (r: Request) => (options.forward ? options.forward(r, env, ctx) : fetch(r));\n const resolved = resolveOptions(options, envStrings(env), 'astrasync-edge/cloudflare');\n try {\n if (!isConfigured(resolved)) return forward(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 publicBaseUrl: resolved.publicBaseUrl,\n counterpartyId: resolved.counterpartyId,\n counterpartyUrl: resolved.counterpartyUrl,\n budgets: resolved.budgets,\n adapter: 'cloudflare',\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 forward(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 forward(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 publicBaseUrl: options.publicBaseUrl ?? e.ASTRASYNC_PUBLIC_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":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrasyncai/adapter-edge",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AstraSync Trusted Agent Gateway — edge adapters for Vercel Edge, Cloudflare Workers, and
|
|
3
|
+
"version": "1.10.0",
|
|
4
|
+
"description": "AstraSync Trusted Agent Gateway — edge adapters for Vercel Edge, Cloudflare Workers, Fastly Compute, and Akamai EdgeWorkers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -19,10 +19,15 @@
|
|
|
19
19
|
"./fastly": {
|
|
20
20
|
"types": "./dist/fastly.d.ts",
|
|
21
21
|
"import": "./dist/fastly.js"
|
|
22
|
+
},
|
|
23
|
+
"./akamai": {
|
|
24
|
+
"types": "./dist/akamai.d.ts",
|
|
25
|
+
"import": "./dist/akamai.js"
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
"files": [
|
|
25
29
|
"dist",
|
|
30
|
+
"templates",
|
|
26
31
|
"README.md"
|
|
27
32
|
],
|
|
28
33
|
"scripts": {
|
|
@@ -43,6 +48,7 @@
|
|
|
43
48
|
"cloudflare-workers",
|
|
44
49
|
"vercel-edge",
|
|
45
50
|
"fastly-compute",
|
|
51
|
+
"akamai-edgeworkers",
|
|
46
52
|
"agent-verification",
|
|
47
53
|
"bot-detection"
|
|
48
54
|
],
|
|
@@ -54,13 +60,13 @@
|
|
|
54
60
|
"directory": "packages/adapter-edge"
|
|
55
61
|
},
|
|
56
62
|
"dependencies": {
|
|
57
|
-
"@astrasyncai/verification-gateway": "^5.
|
|
63
|
+
"@astrasyncai/verification-gateway": "^5.22.0"
|
|
58
64
|
},
|
|
59
65
|
"devDependencies": {
|
|
60
66
|
"@types/node": "^20.10.0",
|
|
61
67
|
"eslint": "^8.56.0",
|
|
62
68
|
"tsup": "^8.0.1",
|
|
63
69
|
"typescript": "^5.3.3",
|
|
64
|
-
"vitest": "^4.1.
|
|
70
|
+
"vitest": "^4.1.12"
|
|
65
71
|
}
|
|
66
72
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# AstraSync gateway on Akamai EdgeWorkers
|
|
2
|
+
|
|
3
|
+
1. **Client IP header.** In Property Manager, enable the _True Client IP Header_
|
|
4
|
+
behaviour (`True-Client-IP`). The gateway reads the client IP from it.
|
|
5
|
+
2. **API route.** Add a property rule matching path `/.astrasync/api/*`. Give it the
|
|
6
|
+
origin `astrasync.ai` (HTTPS, forward host header = origin hostname). Rewrite
|
|
7
|
+
the path to strip the `/.astrasync` prefix. Caching: _no-store_.
|
|
8
|
+
3. **EdgeWorker.** Create an EdgeWorker ID, upload this bundle, and add the
|
|
9
|
+
_EdgeWorkers_ behaviour to the rule that covers your site.
|
|
10
|
+
4. **Where it runs.** Akamai's App & API Protector / Bot Manager run first.
|
|
11
|
+
Requests they deny never reach the EdgeWorker. To see those agents, stream
|
|
12
|
+
your security events to AstraSync: DataStream 2 → custom HTTPS endpoint, set
|
|
13
|
+
up on the endpoint page under _Edge logs_.
|
|
14
|
+
|
|
15
|
+
Limits in this version: header-only detection (EdgeWorkers can't read request
|
|
16
|
+
bodies in `onClientRequest`), and no TLS fingerprints.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// AstraSync Trusted Agent Gateway — Akamai EdgeWorkers (onClientRequest).
|
|
2
|
+
// Bundle with your bundler of choice (the adapter is ESM), then:
|
|
3
|
+
// akamai edgeworkers upload --bundle astrasync-gateway.tgz <ewId>
|
|
4
|
+
// akamai edgeworkers activate <ewId> STAGING <version>
|
|
5
|
+
import { httpRequest } from 'http-request';
|
|
6
|
+
import { createOnClientRequest } from '@astrasyncai/adapter-edge/akamai';
|
|
7
|
+
|
|
8
|
+
export const onClientRequest = createOnClientRequest({
|
|
9
|
+
httpRequest,
|
|
10
|
+
counterpartyId: 'ASTRAE-replace-me',
|
|
11
|
+
counterpartyUrl: 'https://www.example.com',
|
|
12
|
+
// Your merchant kya_… key. Keep it out of source: inject at bundle time
|
|
13
|
+
// or read it from EdgeKV before calling the gateway.
|
|
14
|
+
apiKey: 'kya_replace_me',
|
|
15
|
+
// Sub-requests must reach a hostname on your property: add the property
|
|
16
|
+
// rule in README.md that routes /.astrasync/api/* to astrasync.ai.
|
|
17
|
+
apiBaseUrl: 'https://www.example.com/.astrasync/api',
|
|
18
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "astrasync-gateway-worker",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"deploy": "wrangler deploy",
|
|
7
|
+
"dev": "wrangler dev"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@astrasyncai/adapter-edge": "^1.10.0"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"wrangler": "^4.0.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use this instead of worker.ts when your zone ALREADY runs a Worker on the
|
|
3
|
+
* same route (Cloudflare allows one Worker per route). The gateway runs
|
|
4
|
+
* first; requests it passes go to your existing handler instead of straight
|
|
5
|
+
* to the origin. Enforce-mode denials are answered before your code runs.
|
|
6
|
+
*/
|
|
7
|
+
import { createGatewayHandler } from '@astrasyncai/adapter-edge/cloudflare';
|
|
8
|
+
import existingWorker from './existing-worker'; // your current Worker module
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
fetch: createGatewayHandler({
|
|
12
|
+
forward: (request, env, ctx) => existingWorker.fetch(request, env, ctx),
|
|
13
|
+
}),
|
|
14
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createGatewayHandler } from '@astrasyncai/adapter-edge/cloudflare';
|
|
2
|
+
|
|
3
|
+
// Pass traffic goes to your origin unchanged (plus trust headers for verified
|
|
4
|
+
// agents); telemetry runs in ctx.waitUntil, off the latency path.
|
|
5
|
+
export default { fetch: createGatewayHandler() };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# AstraSync Trusted Agent Gateway — Cloudflare Worker
|
|
2
|
+
#
|
|
3
|
+
# 1. npm install && npx wrangler login
|
|
4
|
+
# 2. Set the two vars below (from your endpoint page in the AstraSync dashboard).
|
|
5
|
+
# 3. npx wrangler secret put ASTRASYNC_API_KEY (your merchant kya_… key)
|
|
6
|
+
# 4. Edit the route to your zone, then: npx wrangler deploy
|
|
7
|
+
#
|
|
8
|
+
# Observe mode is the default and never blocks. Switch to enforce, choose a
|
|
9
|
+
# check depth and set per-path rules on the endpoint's Edge verification
|
|
10
|
+
# panel — no redeploy. The Worker runs AFTER Cloudflare's WAF and bot
|
|
11
|
+
# rules: requests they block never reach it. To see those, connect your
|
|
12
|
+
# Cloudflare security events (Logpush → HTTP) on the endpoint page under
|
|
13
|
+
# Edge logs.
|
|
14
|
+
|
|
15
|
+
name = "astrasync-gateway"
|
|
16
|
+
main = "src/worker.ts"
|
|
17
|
+
compatibility_date = "2026-09-01"
|
|
18
|
+
|
|
19
|
+
# Cover the whole site; humans and static assets pass with no API call.
|
|
20
|
+
routes = [
|
|
21
|
+
{ pattern = "www.example.com/*", zone_name = "example.com" },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[vars]
|
|
25
|
+
ASTRASYNC_COUNTERPARTY_ID = "ASTRAE-replace-me"
|
|
26
|
+
ASTRASYNC_COUNTERPARTY_URL = "https://www.example.com"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# AstraSync Trusted Agent Gateway — Fastly Compute
|
|
2
|
+
#
|
|
3
|
+
# 1. npm install && fastly compute init --from=. (or add to your service)
|
|
4
|
+
# 2. Backends: "origin" → your origin; "astrasync" → astrasync.ai:443
|
|
5
|
+
# 3. Put your merchant kya_… key in a secret store named "astrasync"
|
|
6
|
+
# (key: api_key) and fill in the ids in src/index.ts.
|
|
7
|
+
# 4. fastly compute publish
|
|
8
|
+
#
|
|
9
|
+
# Runs after Fastly's Next-Gen WAF: requests it blocks never reach this
|
|
10
|
+
# service. Connect Fastly HTTPS logging on the endpoint page (Edge logs) to
|
|
11
|
+
# see them.
|
|
12
|
+
manifest_version = 3
|
|
13
|
+
name = "astrasync-gateway"
|
|
14
|
+
language = "javascript"
|
|
15
|
+
|
|
16
|
+
[scripts]
|
|
17
|
+
build = "npx js-compute-runtime src/index.ts bin/main.wasm"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/// <reference types="@fastly/js-compute" />
|
|
2
|
+
import { SecretStore } from 'fastly:secret-store';
|
|
3
|
+
import { createGatewayHandler } from '@astrasyncai/adapter-edge/fastly';
|
|
4
|
+
|
|
5
|
+
const apiKey = (await new SecretStore('astrasync').get('api_key'))?.plaintext();
|
|
6
|
+
|
|
7
|
+
const gateway = createGatewayHandler({
|
|
8
|
+
originBackend: 'origin',
|
|
9
|
+
counterpartyId: 'ASTRAE-replace-me',
|
|
10
|
+
counterpartyUrl: 'https://www.example.com',
|
|
11
|
+
...(apiKey && { apiKey }),
|
|
12
|
+
geo: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
addEventListener('fetch', (event) => event.respondWith(gateway(event)));
|