@fungi.computer/veil 0.2.1 → 0.3.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 +69 -0
- package/dist/model-credentials.d.ts +8 -7
- package/dist/model-credentials.js +15 -12
- package/dist/provider-egress.d.ts +16 -5
- package/dist/provider-egress.js +194 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,75 @@ Workerd's `startTls({ expectedServerHostname })` verifies the certificate
|
|
|
84
84
|
against that name; it reports a verification failure on the first read, which
|
|
85
85
|
the door types as `unavailable`.
|
|
86
86
|
|
|
87
|
+
## Platform snapshot egress (private, not enabled by this change)
|
|
88
|
+
|
|
89
|
+
`configuredProviderEgress(env)` (from `./gatekeeper`) also owns the platform
|
|
90
|
+
snapshot routes below. Like the existing Hub search caller
|
|
91
|
+
(`apps/hub/src/agent-web-search.ts`, `configuredWebSearch`), a caller obtains
|
|
92
|
+
`baseUrl(route)` and passes credential-free requests through
|
|
93
|
+
`providerFetch(egress, route)`. Veil alone reads the bindings, pins the
|
|
94
|
+
destination and attaches the credential. The new holdings callers are composed
|
|
95
|
+
later; these routes neither mount Hub endpoints nor schedule jobs.
|
|
96
|
+
|
|
97
|
+
| Route | Destination / allowed calls | Private bindings |
|
|
98
|
+
| --------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| `solana-rpc` | Helius mainnet, exactly `https://mainnet.helius-rpc.com/`, POST | `SOLANA_RPC_URL` (that credential-free root), `SOLANA_RPC_API_KEY` (Worker secret) |
|
|
100
|
+
| `solana-rpc-fallback` | One configured QuickNode Solana mainnet endpoint, exactly its HTTPS root, POST | `SOLANA_RPC_FALLBACK_URL` (`https://<endpoint>.solana-mainnet.quiknode.pro/`), `SOLANA_RPC_FALLBACK_API_KEY` (Worker secret) |
|
|
101
|
+
| `sanctions-screen` | GET exactly `https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/SDN.XML` | `SANCTIONS_SDN_URL` (that exact URL, opt-in) |
|
|
102
|
+
|
|
103
|
+
The RPC routes accept only single JSON-RPC 2.0 calls to `getAccountInfo`,
|
|
104
|
+
`getMultipleAccounts`, `getProgramAccounts`, `getTokenSupply` and `getSlot`:
|
|
105
|
+
mint/program-owner boot checks, token and lock account reads, supply and
|
|
106
|
+
finalized-slot reconciliation. Transactions, simulation, airdrops, arbitrary
|
|
107
|
+
methods and batches are refused. The holdings owner still owns finality, params,
|
|
108
|
+
maintained account decoders and reconciliation. QuickNode is the proposal's
|
|
109
|
+
second-provider choice, not an automatic retry: its caller must apply the same
|
|
110
|
+
snapshot checks on fallback.
|
|
111
|
+
|
|
112
|
+
Helius receives `?api-key=<secret>`; QuickNode receives `/<secret>/`. Neither
|
|
113
|
+
credential-bearing URL is returned from `baseUrl`. Caller headers, queries and
|
|
114
|
+
alternate paths are not forwarded. Keep keys in the existing private
|
|
115
|
+
Worker-secret custody, never Team storage, public config, telemetry or logs. The
|
|
116
|
+
final transport sees the authorized URL and must not log it. Missing/invalid
|
|
117
|
+
bindings refuse `unavailable` before fetch; other destinations or methods refuse
|
|
118
|
+
`denied`. RPC redirects and non-2xx responses refuse `unavailable`; no upstream
|
|
119
|
+
error body, headers or URL is returned. Successful bodies are bounded to 64 MiB
|
|
120
|
+
and refused on a credential echo (raw, JSON-escaped or percent-encoded); RPC
|
|
121
|
+
request bodies are bounded to 64 KiB. Transport failures, including aborts,
|
|
122
|
+
expose only fixed `VeilError` codes. The existing streaming model/search routes
|
|
123
|
+
and model-key rules are unchanged.
|
|
124
|
+
|
|
125
|
+
The sanctions GET alone may manually follow **one HTTP 302** from the exact OFAC
|
|
126
|
+
export to
|
|
127
|
+
`https://wc2h-sls-prod-public-published.s3.us-gov-west-1.amazonaws.com`, with a
|
|
128
|
+
path beginning `/Published/` and ending `/SDN.XML`. OFAC's signed query is
|
|
129
|
+
retained; caller headers/credentials and upstream cookies are not forwarded. The
|
|
130
|
+
Location string is capped at 8,192 code units, must have no userinfo or
|
|
131
|
+
fragment, and cannot change HTTPS or port 443. Veil cancels the redirect body
|
|
132
|
+
before the second GET, uses the original abort signal (including any caller
|
|
133
|
+
deadline, never restarted) and keeps the same 64 MiB download bound. Another
|
|
134
|
+
host/path, a missing/invalid Location, a second redirect or any other redirect
|
|
135
|
+
status refuses `unavailable`. Direct entry at S3 and non-GET sanctions requests
|
|
136
|
+
remain refused.
|
|
137
|
+
|
|
138
|
+
**Sanctions choice:** use the official OFAC SDN XML list, not the Chainalysis
|
|
139
|
+
free API. Read-only confirmation on 2026-10-31 found actual
|
|
140
|
+
`Digital Currency Address - SOL` identifiers in the official export (including
|
|
141
|
+
ID 194579), establishing Solana-address coverage without assuming a vendor's
|
|
142
|
+
chain support. OFAC's public list requires **no credential**, so there is no
|
|
143
|
+
`SANCTIONS_API_KEY` binding or invented anonymous-key fallback. F5 must parse
|
|
144
|
+
the XML through a maintained decoder, match the SOL identifiers, and fail closed
|
|
145
|
+
on an unavailable or unusable list; this door does not claim to screen persons,
|
|
146
|
+
countries or unlisted addresses.
|
|
147
|
+
|
|
148
|
+
Provider contract sources:
|
|
149
|
+
[Helius RPC](https://www.helius.dev/docs/rpc/overview),
|
|
150
|
+
[QuickNode endpoints](https://www.quicknode.com/docs/solana/endpoints),
|
|
151
|
+
[OFAC data formats](https://ofac.treasury.gov/specially-designated-nationals-list-data-formats-data-schemas)
|
|
152
|
+
and the exact official export above. Unit fixtures prove custody and route
|
|
153
|
+
policy, not provisioned keys, paid-plan large-snapshot support, live RPC
|
|
154
|
+
reconciliation or live screening; F4/F5 own those opt-in proofs.
|
|
155
|
+
|
|
87
156
|
## Model credentials and failure handling
|
|
88
157
|
|
|
89
158
|
`src/model-credentials.ts` selects which key class may serve an inference
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
* provider login policy: the local runtime, the TUI login registry and the Hub
|
|
4
4
|
* read it; none of them decides visibility on its own.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* hosted
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* is forbidden on both hosts.
|
|
6
|
+
* Offer a person's own Claude or ChatGPT subscription locally, never on
|
|
7
|
+
* hosted Fungi. A subscription is always one person's: never Team-scoped,
|
|
8
|
+
* shared or pooled. `subscriptionKillSwitch` can turn off a provider if it
|
|
9
|
+
* objects or blocks tokens. Hosted BYOK uses API keys. The retired Codex
|
|
10
|
+
* provider is forbidden on both hosts.
|
|
12
11
|
* See wiki/3-resources/decisions/adr-byok-model-keys-v1.md.
|
|
13
12
|
*/
|
|
14
13
|
/** The owner's own machine, or Fungi's servers acting for a Team. */
|
|
@@ -69,7 +68,9 @@ export declare const modelCredentialPolicy: Readonly<{
|
|
|
69
68
|
status: "allowed";
|
|
70
69
|
}>;
|
|
71
70
|
hosted: Readonly<{
|
|
72
|
-
status: "
|
|
71
|
+
status: "forbidden";
|
|
72
|
+
reason: string;
|
|
73
|
+
source: string;
|
|
73
74
|
}>;
|
|
74
75
|
};
|
|
75
76
|
};
|
|
@@ -3,17 +3,21 @@
|
|
|
3
3
|
* provider login policy: the local runtime, the TUI login registry and the Hub
|
|
4
4
|
* read it; none of them decides visibility on its own.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* hosted
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* is forbidden on both hosts.
|
|
6
|
+
* Offer a person's own Claude or ChatGPT subscription locally, never on
|
|
7
|
+
* hosted Fungi. A subscription is always one person's: never Team-scoped,
|
|
8
|
+
* shared or pooled. `subscriptionKillSwitch` can turn off a provider if it
|
|
9
|
+
* objects or blocks tokens. Hosted BYOK uses API keys. The retired Codex
|
|
10
|
+
* provider is forbidden on both hosts.
|
|
12
11
|
* See wiki/3-resources/decisions/adr-byok-model-keys-v1.md.
|
|
13
12
|
*/
|
|
14
13
|
const allowed = { status: "allowed" };
|
|
15
14
|
const unreviewed = { status: "unreviewed" };
|
|
16
15
|
const notOffered = {
|
|
16
|
+
status: "forbidden",
|
|
17
|
+
reason: "Fungi does not offer this credential method on this host",
|
|
18
|
+
source: "wiki/3-resources/decisions/adr-byok-model-keys-v1.md",
|
|
19
|
+
};
|
|
20
|
+
const providerNotOffered = {
|
|
17
21
|
status: "forbidden",
|
|
18
22
|
reason: "The provider does not offer this credential method",
|
|
19
23
|
source: "provider documentation",
|
|
@@ -25,7 +29,7 @@ const retiredCodex = {
|
|
|
25
29
|
};
|
|
26
30
|
const apiKeyOnly = {
|
|
27
31
|
api_key: { local: allowed, hosted: allowed },
|
|
28
|
-
subscription: { local:
|
|
32
|
+
subscription: { local: providerNotOffered, hosted: providerNotOffered },
|
|
29
33
|
};
|
|
30
34
|
/**
|
|
31
35
|
* Reviewed providers, keyed by Pi provider id. `openai-compatible` is a
|
|
@@ -38,9 +42,8 @@ export const modelCredentialPolicy = Object.freeze({
|
|
|
38
42
|
},
|
|
39
43
|
anthropic: {
|
|
40
44
|
api_key: { local: allowed, hosted: allowed },
|
|
41
|
-
// Local: Pi's Claude Pro/Max OAuth
|
|
42
|
-
|
|
43
|
-
subscription: { local: allowed, hosted: allowed },
|
|
45
|
+
// Local: Pi's Claude Pro/Max OAuth. Hosted: no Claude subscription login.
|
|
46
|
+
subscription: { local: allowed, hosted: notOffered },
|
|
44
47
|
},
|
|
45
48
|
"openai-codex": {
|
|
46
49
|
api_key: { local: retiredCodex, hosted: retiredCodex },
|
|
@@ -52,8 +55,8 @@ export const modelCredentialPolicy = Object.freeze({
|
|
|
52
55
|
local: allowed,
|
|
53
56
|
hosted: {
|
|
54
57
|
status: "forbidden",
|
|
55
|
-
reason: "ChatGPT
|
|
56
|
-
source: "
|
|
58
|
+
reason: "Fungi does not offer ChatGPT subscription sign-in on hosted Fungi; approval alone would not enable it",
|
|
59
|
+
source: "wiki/3-resources/decisions/adr-byok-model-keys-v1.md",
|
|
57
60
|
},
|
|
58
61
|
},
|
|
59
62
|
},
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
* `/parallel` route, with `x-api-key`.
|
|
15
15
|
* - `ai-gateway-logs`: Cloudflare's API log read used for usage
|
|
16
16
|
* reconciliation, `Authorization: Bearer` with the gateway token.
|
|
17
|
+
* - `solana-rpc` / `solana-rpc-fallback`: finalized snapshot reads through
|
|
18
|
+
* Helius / QuickNode, with private query / path credentials.
|
|
19
|
+
* - `sanctions-screen`: the official OFAC SDN XML export, without a key.
|
|
17
20
|
*/
|
|
18
21
|
/** Private Worker bindings read only here; never Team storage or responses. */
|
|
19
22
|
export type ProviderKeyBindings = Readonly<{
|
|
@@ -24,8 +27,16 @@ export type ProviderKeyBindings = Readonly<{
|
|
|
24
27
|
/** Cloudflare account that owns the gateway; routes are scoped to it. */
|
|
25
28
|
AI_GATEWAY_ACCOUNT_ID?: string;
|
|
26
29
|
AI_GATEWAY_ID?: string;
|
|
30
|
+
/** Credential-free Helius mainnet root; the key is a separate Worker secret. */
|
|
31
|
+
SOLANA_RPC_URL?: string;
|
|
32
|
+
SOLANA_RPC_API_KEY?: string;
|
|
33
|
+
/** Credential-free QuickNode Solana mainnet root; token is a Worker secret. */
|
|
34
|
+
SOLANA_RPC_FALLBACK_URL?: string;
|
|
35
|
+
SOLANA_RPC_FALLBACK_API_KEY?: string;
|
|
36
|
+
/** Opt-in to the pinned public OFAC SDN XML export. No API key is needed. */
|
|
37
|
+
SANCTIONS_SDN_URL?: string;
|
|
27
38
|
}>;
|
|
28
|
-
export type ProviderRoute = "camel" | "ai-gateway" | "parallel" | "ai-gateway-logs";
|
|
39
|
+
export type ProviderRoute = "camel" | "ai-gateway" | "parallel" | "ai-gateway-logs" | "solana-rpc" | "solana-rpc-fallback" | "sanctions-screen";
|
|
29
40
|
/**
|
|
30
41
|
* The provider-key door. `fetch` is the transport the authorized request is
|
|
31
42
|
* finally sent on (a test double, or the Hub's telemetry-wrapped fetch).
|
|
@@ -33,16 +44,16 @@ export type ProviderRoute = "camel" | "ai-gateway" | "parallel" | "ai-gateway-lo
|
|
|
33
44
|
export declare function createProviderEgress(env: ProviderKeyBindings, options?: Readonly<{
|
|
34
45
|
fetch?: typeof globalThis.fetch;
|
|
35
46
|
}>): {
|
|
36
|
-
/** Whether
|
|
47
|
+
/** Whether required custody/configuration is present; reveals no value. */
|
|
37
48
|
credentialed(route: ProviderRoute): boolean;
|
|
38
49
|
/** The route's public base URL, or `null` while it is not configured. */
|
|
39
50
|
baseUrl(route: ProviderRoute): string | null;
|
|
40
51
|
/**
|
|
41
52
|
* Send a credential-free request to a route. Refuses (`denied`) any
|
|
42
53
|
* destination outside the route and (`unavailable`) a route with no
|
|
43
|
-
* credential.
|
|
44
|
-
*
|
|
45
|
-
*
|
|
54
|
+
* credential. Credentialed routes never follow redirects (workerd has no
|
|
55
|
+
* `error` mode; `manual` hands the 3xx back instead). Only the anonymous
|
|
56
|
+
* sanctions GET may follow one pinned OFAC government-S3 302.
|
|
46
57
|
*/
|
|
47
58
|
fetch(route: ProviderRoute, input: string | URL | Request, init?: RequestInit): Promise<Response>;
|
|
48
59
|
};
|
package/dist/provider-egress.js
CHANGED
|
@@ -1,8 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { VeilError } from "./index.js";
|
|
2
3
|
const CAMEL_BASE = "https://stream.camelai.com/v1";
|
|
3
4
|
const PARALLEL_BASE = "https://api.parallel.ai";
|
|
4
5
|
const GATEWAY_ORIGIN = "https://gateway.ai.cloudflare.com";
|
|
5
6
|
const CLOUDFLARE_API_ORIGIN = "https://api.cloudflare.com";
|
|
7
|
+
const HELIUS_BASE = "https://mainnet.helius-rpc.com/";
|
|
8
|
+
const OFAC_SDN = "https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/SDN.XML";
|
|
9
|
+
const OFAC_DOWNLOAD_ORIGIN = "https://wc2h-sls-prod-public-published.s3.us-gov-west-1.amazonaws.com";
|
|
10
|
+
// Single read-only JSON-RPC calls used by the finalized holdings/lock reader.
|
|
11
|
+
// Zod owns JSON envelope decoding; the holdings owner owns params and results.
|
|
12
|
+
const snapshotCall = z
|
|
13
|
+
.object({
|
|
14
|
+
jsonrpc: z.literal("2.0"),
|
|
15
|
+
id: z.union([z.string(), z.number()]),
|
|
16
|
+
method: z.enum([
|
|
17
|
+
"getAccountInfo",
|
|
18
|
+
"getMultipleAccounts",
|
|
19
|
+
"getProgramAccounts",
|
|
20
|
+
"getTokenSupply",
|
|
21
|
+
"getSlot",
|
|
22
|
+
]),
|
|
23
|
+
params: z.array(z.unknown()).optional(),
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
function snapshotRoute(route) {
|
|
27
|
+
return (route === "solana-rpc" ||
|
|
28
|
+
route === "solana-rpc-fallback" ||
|
|
29
|
+
route === "sanctions-screen");
|
|
30
|
+
}
|
|
6
31
|
const present = (value) => {
|
|
7
32
|
const trimmed = value?.trim();
|
|
8
33
|
return trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
@@ -18,6 +43,34 @@ function gatewayScope(env) {
|
|
|
18
43
|
/** The destination and credential for a route; `undefined` if either is missing. */
|
|
19
44
|
function target(env, route) {
|
|
20
45
|
switch (route) {
|
|
46
|
+
case "solana-rpc":
|
|
47
|
+
case "solana-rpc-fallback": {
|
|
48
|
+
const primary = route === "solana-rpc";
|
|
49
|
+
const raw = present(primary ? env.SOLANA_RPC_URL : env.SOLANA_RPC_FALLBACK_URL);
|
|
50
|
+
const credential = present(primary ? env.SOLANA_RPC_API_KEY : env.SOLANA_RPC_FALLBACK_API_KEY);
|
|
51
|
+
if (raw === undefined || credential === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (!URL.canParse(raw))
|
|
54
|
+
return undefined;
|
|
55
|
+
const base = new URL(raw);
|
|
56
|
+
if (base.protocol !== "https:" ||
|
|
57
|
+
base.username ||
|
|
58
|
+
base.password ||
|
|
59
|
+
base.port ||
|
|
60
|
+
base.pathname !== "/" ||
|
|
61
|
+
base.search ||
|
|
62
|
+
base.hash)
|
|
63
|
+
return undefined;
|
|
64
|
+
if (primary
|
|
65
|
+
? base.href !== HELIUS_BASE
|
|
66
|
+
: !/^[a-z0-9-]+\.solana-mainnet\.quiknode\.pro$/u.test(base.hostname))
|
|
67
|
+
return undefined;
|
|
68
|
+
return { base, credential };
|
|
69
|
+
}
|
|
70
|
+
case "sanctions-screen":
|
|
71
|
+
return present(env.SANCTIONS_SDN_URL) === OFAC_SDN
|
|
72
|
+
? { base: new URL(OFAC_SDN) }
|
|
73
|
+
: undefined;
|
|
21
74
|
case "camel": {
|
|
22
75
|
const credential = present(env.CAMEL_API_KEY);
|
|
23
76
|
return credential === undefined
|
|
@@ -67,6 +120,8 @@ function within(url, base) {
|
|
|
67
120
|
}
|
|
68
121
|
/** Replace whatever credential the caller sent with this route's own. */
|
|
69
122
|
function authorize(route, headers, credential) {
|
|
123
|
+
if (credential === undefined)
|
|
124
|
+
throw new VeilError("unavailable");
|
|
70
125
|
for (const name of [
|
|
71
126
|
"authorization",
|
|
72
127
|
"x-api-key",
|
|
@@ -75,6 +130,11 @@ function authorize(route, headers, credential) {
|
|
|
75
130
|
])
|
|
76
131
|
headers.delete(name);
|
|
77
132
|
switch (route) {
|
|
133
|
+
case "solana-rpc":
|
|
134
|
+
case "solana-rpc-fallback":
|
|
135
|
+
case "sanctions-screen":
|
|
136
|
+
// These doors construct their own headers and URL below.
|
|
137
|
+
return;
|
|
78
138
|
case "camel":
|
|
79
139
|
case "ai-gateway-logs":
|
|
80
140
|
headers.set("authorization", `Bearer ${credential}`);
|
|
@@ -87,6 +147,130 @@ function authorize(route, headers, credential) {
|
|
|
87
147
|
return;
|
|
88
148
|
}
|
|
89
149
|
}
|
|
150
|
+
/** A bounded body, before JSON decoding or returning a provider response. */
|
|
151
|
+
async function boundedText(message, limit) {
|
|
152
|
+
const reader = message.body?.getReader();
|
|
153
|
+
if (reader === undefined)
|
|
154
|
+
return "";
|
|
155
|
+
const chunks = [];
|
|
156
|
+
let size = 0;
|
|
157
|
+
try {
|
|
158
|
+
for (;;) {
|
|
159
|
+
const part = await reader.read();
|
|
160
|
+
if (part.done)
|
|
161
|
+
break;
|
|
162
|
+
const chunk = part.value;
|
|
163
|
+
if (!(chunk instanceof Uint8Array))
|
|
164
|
+
throw new VeilError("unavailable");
|
|
165
|
+
size += chunk.byteLength;
|
|
166
|
+
if (size > limit)
|
|
167
|
+
throw new VeilError("unavailable");
|
|
168
|
+
chunks.push(chunk);
|
|
169
|
+
}
|
|
170
|
+
return await new Blob(chunks).text();
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
try {
|
|
174
|
+
await reader.cancel();
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
reader.releaseLock();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Fixed, non-streaming snapshot doors never return upstream headers or URLs. */
|
|
182
|
+
async function fetchSnapshot(route, resolved, request, transport) {
|
|
183
|
+
try {
|
|
184
|
+
const url = new URL(request.url);
|
|
185
|
+
if (url.href !== resolved.base.href)
|
|
186
|
+
throw new VeilError("denied");
|
|
187
|
+
const screening = route === "sanctions-screen";
|
|
188
|
+
if (request.method !== (screening ? "GET" : "POST"))
|
|
189
|
+
throw new VeilError("denied");
|
|
190
|
+
let body;
|
|
191
|
+
if (!screening) {
|
|
192
|
+
body = await boundedText(request, 64 * 1024);
|
|
193
|
+
let envelope;
|
|
194
|
+
try {
|
|
195
|
+
envelope = JSON.parse(body);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
throw new VeilError("denied");
|
|
199
|
+
}
|
|
200
|
+
if (!snapshotCall.safeParse(envelope).success)
|
|
201
|
+
throw new VeilError("denied");
|
|
202
|
+
}
|
|
203
|
+
const destination = new URL(resolved.base);
|
|
204
|
+
const key = resolved.credential;
|
|
205
|
+
if (!screening) {
|
|
206
|
+
if (key === undefined)
|
|
207
|
+
throw new VeilError("unavailable");
|
|
208
|
+
if (route === "solana-rpc")
|
|
209
|
+
destination.searchParams.set("api-key", key);
|
|
210
|
+
else
|
|
211
|
+
destination.pathname = `/${encodeURIComponent(key)}/`;
|
|
212
|
+
}
|
|
213
|
+
const init = {
|
|
214
|
+
method: request.method,
|
|
215
|
+
headers: screening
|
|
216
|
+
? { accept: "application/xml" }
|
|
217
|
+
: {
|
|
218
|
+
accept: "application/json",
|
|
219
|
+
"content-type": "application/json",
|
|
220
|
+
},
|
|
221
|
+
signal: request.signal,
|
|
222
|
+
redirect: "manual",
|
|
223
|
+
};
|
|
224
|
+
if (body !== undefined)
|
|
225
|
+
init.body = body;
|
|
226
|
+
let response = await transport(new Request(destination, init));
|
|
227
|
+
// OFAC publishes a signed S3 download URL. Only this anonymous GET door
|
|
228
|
+
// may follow that one observed 302; the next response must succeed.
|
|
229
|
+
if (screening && response.status === 302) {
|
|
230
|
+
const location = response.headers.get("location");
|
|
231
|
+
await response.body?.cancel();
|
|
232
|
+
if (location === null ||
|
|
233
|
+
location.length > 8192 ||
|
|
234
|
+
!URL.canParse(location))
|
|
235
|
+
throw new VeilError("unavailable");
|
|
236
|
+
const download = new URL(location);
|
|
237
|
+
if (download.origin !== OFAC_DOWNLOAD_ORIGIN ||
|
|
238
|
+
download.username !== "" ||
|
|
239
|
+
download.password !== "" ||
|
|
240
|
+
!download.pathname.startsWith("/Published/") ||
|
|
241
|
+
!download.pathname.endsWith("/SDN.XML") ||
|
|
242
|
+
download.hash !== "")
|
|
243
|
+
throw new VeilError("unavailable");
|
|
244
|
+
request.signal.throwIfAborted();
|
|
245
|
+
response = await transport(new Request(download, {
|
|
246
|
+
method: "GET",
|
|
247
|
+
headers: { accept: "application/xml" },
|
|
248
|
+
signal: request.signal,
|
|
249
|
+
redirect: "manual",
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
await response.body?.cancel();
|
|
254
|
+
throw new VeilError("unavailable");
|
|
255
|
+
}
|
|
256
|
+
const text = await boundedText(response, 64 * 1024 * 1024);
|
|
257
|
+
if (key !== undefined &&
|
|
258
|
+
[key, encodeURIComponent(key), JSON.stringify(key).slice(1, -1)].some((echo) => text.includes(echo)))
|
|
259
|
+
throw new VeilError("unavailable");
|
|
260
|
+
return new Response(text, {
|
|
261
|
+
status: response.status,
|
|
262
|
+
headers: {
|
|
263
|
+
"content-type": screening ? "application/xml" : "application/json",
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
// Even aborted transports can include a credential-bearing URL in errors.
|
|
269
|
+
if (error instanceof VeilError)
|
|
270
|
+
throw error;
|
|
271
|
+
throw new VeilError("unavailable");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
90
274
|
/**
|
|
91
275
|
* The provider-key door. `fetch` is the transport the authorized request is
|
|
92
276
|
* finally sent on (a test double, or the Hub's telemetry-wrapped fetch).
|
|
@@ -94,9 +278,13 @@ function authorize(route, headers, credential) {
|
|
|
94
278
|
export function createProviderEgress(env, options = {}) {
|
|
95
279
|
const transport = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
96
280
|
return {
|
|
97
|
-
/** Whether
|
|
281
|
+
/** Whether required custody/configuration is present; reveals no value. */
|
|
98
282
|
credentialed(route) {
|
|
99
283
|
switch (route) {
|
|
284
|
+
case "solana-rpc":
|
|
285
|
+
case "solana-rpc-fallback":
|
|
286
|
+
case "sanctions-screen":
|
|
287
|
+
return target(env, route) !== undefined;
|
|
100
288
|
case "camel":
|
|
101
289
|
return present(env.CAMEL_API_KEY) !== undefined;
|
|
102
290
|
case "parallel":
|
|
@@ -116,9 +304,9 @@ export function createProviderEgress(env, options = {}) {
|
|
|
116
304
|
/**
|
|
117
305
|
* Send a credential-free request to a route. Refuses (`denied`) any
|
|
118
306
|
* destination outside the route and (`unavailable`) a route with no
|
|
119
|
-
* credential.
|
|
120
|
-
*
|
|
121
|
-
*
|
|
307
|
+
* credential. Credentialed routes never follow redirects (workerd has no
|
|
308
|
+
* `error` mode; `manual` hands the 3xx back instead). Only the anonymous
|
|
309
|
+
* sanctions GET may follow one pinned OFAC government-S3 302.
|
|
122
310
|
*/
|
|
123
311
|
async fetch(route, input, init) {
|
|
124
312
|
const resolved = target(env, route);
|
|
@@ -134,6 +322,8 @@ export function createProviderEgress(env, options = {}) {
|
|
|
134
322
|
}
|
|
135
323
|
if (!within(new URL(request.url), resolved.base))
|
|
136
324
|
throw new VeilError("denied");
|
|
325
|
+
if (snapshotRoute(route))
|
|
326
|
+
return fetchSnapshot(route, resolved, request, transport);
|
|
137
327
|
const headers = new Headers(request.headers);
|
|
138
328
|
authorize(route, headers, resolved.credential);
|
|
139
329
|
try {
|