@fungi.computer/veil 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +171 -2
- package/dist/best-effort-cleanup.d.ts +2 -0
- package/dist/best-effort-cleanup.js +5 -0
- package/dist/event-verifiers.d.ts +80 -0
- package/dist/event-verifiers.js +168 -0
- package/dist/gatekeeper.d.ts +202 -0
- package/dist/gatekeeper.js +231 -0
- package/dist/index.d.ts +254 -0
- package/dist/index.js +267 -0
- package/dist/model-credentials.d.ts +226 -0
- package/dist/model-credentials.js +133 -0
- package/dist/model-key-grants.d.ts +56 -0
- package/dist/model-key-grants.js +107 -0
- package/dist/model-key-network.d.ts +33 -0
- package/dist/model-key-network.js +202 -0
- package/dist/model-keys.d.ts +42 -0
- package/dist/model-keys.js +115 -0
- package/dist/nango-custody-http.d.ts +16 -0
- package/dist/nango-custody-http.js +93 -0
- package/dist/nango-git.d.ts +19 -0
- package/dist/nango-git.js +125 -0
- package/dist/nango-model-keys.d.ts +13 -0
- package/dist/nango-model-keys.js +48 -0
- package/dist/nango-world-seat.d.ts +14 -0
- package/dist/nango-world-seat.js +47 -0
- package/dist/nango.d.ts +16 -0
- package/dist/nango.js +300 -0
- package/dist/openapi-contract.d.ts +64 -0
- package/dist/openapi-contract.js +11 -0
- package/dist/openapi-effect.d.ts +9 -0
- package/dist/openapi-effect.js +284 -0
- package/dist/openapi-http.d.ts +2 -0
- package/dist/openapi-http.js +37 -0
- package/dist/openapi-secrets.d.ts +6 -0
- package/dist/openapi-secrets.js +80 -0
- package/dist/openapi.d.ts +5 -0
- package/dist/openapi.js +24 -0
- package/dist/owned-infrastructure.d.ts +13 -0
- package/dist/owned-infrastructure.js +17 -0
- package/dist/provider-egress.d.ts +51 -0
- package/dist/provider-egress.js +155 -0
- package/dist/public-egress-policy.d.ts +13 -0
- package/dist/public-egress-policy.js +101 -0
- package/dist/public-fetch.d.ts +90 -0
- package/dist/public-fetch.js +347 -0
- package/dist/webhook-trigger-secrets.d.ts +23 -0
- package/dist/webhook-trigger-secrets.js +53 -0
- package/dist/world-seat.d.ts +40 -0
- package/dist/world-seat.js +82 -0
- package/package.json +140 -3
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { VeilError } from "./index.js";
|
|
2
|
+
const CAMEL_BASE = "https://stream.camelai.com/v1";
|
|
3
|
+
const PARALLEL_BASE = "https://api.parallel.ai";
|
|
4
|
+
const GATEWAY_ORIGIN = "https://gateway.ai.cloudflare.com";
|
|
5
|
+
const CLOUDFLARE_API_ORIGIN = "https://api.cloudflare.com";
|
|
6
|
+
const present = (value) => {
|
|
7
|
+
const trimmed = value?.trim();
|
|
8
|
+
return trimmed === undefined || trimmed === "" ? undefined : trimmed;
|
|
9
|
+
};
|
|
10
|
+
/** The one AI Gateway origin-and-path scope, or `undefined` when unnamed. */
|
|
11
|
+
function gatewayScope(env) {
|
|
12
|
+
const account = present(env.AI_GATEWAY_ACCOUNT_ID);
|
|
13
|
+
const gateway = present(env.AI_GATEWAY_ID);
|
|
14
|
+
if (account === undefined || gateway === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
return `${GATEWAY_ORIGIN}/v1/${encodeURIComponent(account)}/${encodeURIComponent(gateway)}`;
|
|
17
|
+
}
|
|
18
|
+
/** The destination and credential for a route; `undefined` if either is missing. */
|
|
19
|
+
function target(env, route) {
|
|
20
|
+
switch (route) {
|
|
21
|
+
case "camel": {
|
|
22
|
+
const credential = present(env.CAMEL_API_KEY);
|
|
23
|
+
return credential === undefined
|
|
24
|
+
? undefined
|
|
25
|
+
: { base: new URL(CAMEL_BASE), credential };
|
|
26
|
+
}
|
|
27
|
+
case "ai-gateway": {
|
|
28
|
+
const credential = present(env.AI_GATEWAY_AUTH_TOKEN);
|
|
29
|
+
const scope = gatewayScope(env);
|
|
30
|
+
return credential === undefined || scope === undefined
|
|
31
|
+
? undefined
|
|
32
|
+
: { base: new URL(`${scope}/compat`), credential };
|
|
33
|
+
}
|
|
34
|
+
case "parallel": {
|
|
35
|
+
const credential = present(env.PARALLEL_API_KEY);
|
|
36
|
+
if (credential === undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
const scope = gatewayScope(env);
|
|
39
|
+
return {
|
|
40
|
+
base: new URL(scope === undefined ? PARALLEL_BASE : `${scope}/parallel`),
|
|
41
|
+
credential,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
case "ai-gateway-logs": {
|
|
45
|
+
const credential = present(env.AI_GATEWAY_AUTH_TOKEN);
|
|
46
|
+
const account = present(env.AI_GATEWAY_ACCOUNT_ID);
|
|
47
|
+
const gateway = present(env.AI_GATEWAY_ID);
|
|
48
|
+
return credential === undefined ||
|
|
49
|
+
account === undefined ||
|
|
50
|
+
gateway === undefined
|
|
51
|
+
? undefined
|
|
52
|
+
: {
|
|
53
|
+
base: new URL(`${CLOUDFLARE_API_ORIGIN}/client/v4/accounts/${encodeURIComponent(account)}/ai-gateway/gateways/${encodeURIComponent(gateway)}/logs`),
|
|
54
|
+
credential,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** `url` is the base itself or lies beneath it, on the same https origin. */
|
|
60
|
+
function within(url, base) {
|
|
61
|
+
if (url.protocol !== "https:" || url.origin !== base.origin)
|
|
62
|
+
return false;
|
|
63
|
+
if (url.username !== "" || url.password !== "")
|
|
64
|
+
return false;
|
|
65
|
+
const root = base.pathname.replace(/\/+$/u, "");
|
|
66
|
+
return url.pathname === root || url.pathname.startsWith(`${root}/`);
|
|
67
|
+
}
|
|
68
|
+
/** Replace whatever credential the caller sent with this route's own. */
|
|
69
|
+
function authorize(route, headers, credential) {
|
|
70
|
+
for (const name of [
|
|
71
|
+
"authorization",
|
|
72
|
+
"x-api-key",
|
|
73
|
+
"cf-aig-authorization",
|
|
74
|
+
"proxy-authorization",
|
|
75
|
+
])
|
|
76
|
+
headers.delete(name);
|
|
77
|
+
switch (route) {
|
|
78
|
+
case "camel":
|
|
79
|
+
case "ai-gateway-logs":
|
|
80
|
+
headers.set("authorization", `Bearer ${credential}`);
|
|
81
|
+
return;
|
|
82
|
+
case "ai-gateway":
|
|
83
|
+
headers.set("cf-aig-authorization", `Bearer ${credential}`);
|
|
84
|
+
return;
|
|
85
|
+
case "parallel":
|
|
86
|
+
headers.set("x-api-key", credential);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The provider-key door. `fetch` is the transport the authorized request is
|
|
92
|
+
* finally sent on (a test double, or the Hub's telemetry-wrapped fetch).
|
|
93
|
+
*/
|
|
94
|
+
export function createProviderEgress(env, options = {}) {
|
|
95
|
+
const transport = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
96
|
+
return {
|
|
97
|
+
/** Whether this route's credential is configured; reveals no value. */
|
|
98
|
+
credentialed(route) {
|
|
99
|
+
switch (route) {
|
|
100
|
+
case "camel":
|
|
101
|
+
return present(env.CAMEL_API_KEY) !== undefined;
|
|
102
|
+
case "parallel":
|
|
103
|
+
return present(env.PARALLEL_API_KEY) !== undefined;
|
|
104
|
+
case "ai-gateway":
|
|
105
|
+
case "ai-gateway-logs":
|
|
106
|
+
return present(env.AI_GATEWAY_AUTH_TOKEN) !== undefined;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
/** The route's public base URL, or `null` while it is not configured. */
|
|
110
|
+
baseUrl(route) {
|
|
111
|
+
const resolved = target(env, route);
|
|
112
|
+
return resolved === undefined
|
|
113
|
+
? null
|
|
114
|
+
: resolved.base.href.replace(/\/+$/u, "");
|
|
115
|
+
},
|
|
116
|
+
/**
|
|
117
|
+
* Send a credential-free request to a route. Refuses (`denied`) any
|
|
118
|
+
* destination outside the route and (`unavailable`) a route with no
|
|
119
|
+
* credential. Redirects are never followed, so a redirect cannot carry
|
|
120
|
+
* the key to another origin (workerd has no `error` mode; `manual` hands
|
|
121
|
+
* the 3xx back instead).
|
|
122
|
+
*/
|
|
123
|
+
async fetch(route, input, init) {
|
|
124
|
+
const resolved = target(env, route);
|
|
125
|
+
if (resolved === undefined)
|
|
126
|
+
throw new VeilError("unavailable");
|
|
127
|
+
let request;
|
|
128
|
+
try {
|
|
129
|
+
request = new Request(input, init);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Malformed, or a URL carrying userinfo: nothing Veil will send a key to.
|
|
133
|
+
throw new VeilError("denied");
|
|
134
|
+
}
|
|
135
|
+
if (!within(new URL(request.url), resolved.base))
|
|
136
|
+
throw new VeilError("denied");
|
|
137
|
+
const headers = new Headers(request.headers);
|
|
138
|
+
authorize(route, headers, resolved.credential);
|
|
139
|
+
try {
|
|
140
|
+
return await transport(new Request(request, { headers, redirect: "manual" }));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
// A cancelled call keeps its own reason; anything else is sanitized so
|
|
144
|
+
// a transport message can never carry request bytes back to the caller.
|
|
145
|
+
if (request.signal.aborted)
|
|
146
|
+
throw error;
|
|
147
|
+
throw new VeilError("unavailable");
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** A `fetch` bound to one route, for consumers that take a plain fetch. */
|
|
153
|
+
export function providerFetch(egress, route) {
|
|
154
|
+
return (input, init) => egress.fetch(route, input, init);
|
|
155
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Host-owned policy, never populated from a Connector or OpenAPI document. */
|
|
2
|
+
export type PublicEgressPolicy = Readonly<{
|
|
3
|
+
extraPorts?: readonly number[];
|
|
4
|
+
deniedHostSuffixes?: readonly string[];
|
|
5
|
+
deniedCidrs?: readonly string[];
|
|
6
|
+
}>;
|
|
7
|
+
/** Validate host-owned entries using the maintained IP parser and WHATWG URL grammar. */
|
|
8
|
+
export declare function validatePublicEgressPolicyEntries(configuration: PublicEgressPolicy): boolean;
|
|
9
|
+
/** The maintained IP parser owns CIDR matching, including mapped IPv4. */
|
|
10
|
+
export declare function createPublicEgressPolicy(configuration?: PublicEgressPolicy): {
|
|
11
|
+
destination(url: URL): void;
|
|
12
|
+
addresses(values: readonly string[]): void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import ipaddr from "ipaddr.js";
|
|
2
|
+
import { VeilError } from "./index.js";
|
|
3
|
+
import { canonicalAddress } from "./model-key-network.js";
|
|
4
|
+
/** Validate host-owned entries using the maintained IP parser and WHATWG URL grammar. */
|
|
5
|
+
export function validatePublicEgressPolicyEntries(configuration) {
|
|
6
|
+
const cidrs = configuration.deniedCidrs ?? [];
|
|
7
|
+
const suffixes = configuration.deniedHostSuffixes ?? [];
|
|
8
|
+
if (new Set(cidrs).size !== cidrs.length ||
|
|
9
|
+
new Set(suffixes).size !== suffixes.length)
|
|
10
|
+
return false;
|
|
11
|
+
for (const cidr of cidrs) {
|
|
12
|
+
if (!ipaddr.isValidCIDR(cidr))
|
|
13
|
+
return false;
|
|
14
|
+
const [address, prefix] = ipaddr.parseCIDR(cidr);
|
|
15
|
+
if (address instanceof ipaddr.IPv4) {
|
|
16
|
+
if (prefix < 8 ||
|
|
17
|
+
cidr !==
|
|
18
|
+
`${ipaddr.IPv4.networkAddressFromCIDR(cidr).toString()}/${String(prefix)}`)
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
if (prefix < 16 ||
|
|
23
|
+
address.zoneId !== undefined ||
|
|
24
|
+
[
|
|
25
|
+
"::ffff:0:0/96",
|
|
26
|
+
"::ffff:0:0:0/96",
|
|
27
|
+
"64:ff9b::/96",
|
|
28
|
+
"64:ff9b:1::/48",
|
|
29
|
+
"2002::/16",
|
|
30
|
+
"2001::/32",
|
|
31
|
+
].some((range) => {
|
|
32
|
+
const [embedded, embeddedPrefix] = ipaddr.IPv6.parseCIDR(range);
|
|
33
|
+
return address.match(embedded, Math.min(prefix, embeddedPrefix));
|
|
34
|
+
}) ||
|
|
35
|
+
cidr !==
|
|
36
|
+
`${ipaddr.IPv6.networkAddressFromCIDR(cidr).toRFC5952String()}/${String(prefix)}`)
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const suffix of suffixes) {
|
|
41
|
+
const input = `https://${suffix}`;
|
|
42
|
+
if (!URL.canParse(input))
|
|
43
|
+
return false;
|
|
44
|
+
const url = new URL(input);
|
|
45
|
+
const labels = suffix.split(".");
|
|
46
|
+
if (url.hostname !== suffix ||
|
|
47
|
+
url.username ||
|
|
48
|
+
url.password ||
|
|
49
|
+
url.port ||
|
|
50
|
+
url.pathname !== "/" ||
|
|
51
|
+
url.search ||
|
|
52
|
+
url.hash ||
|
|
53
|
+
ipaddr.isValid(suffix) ||
|
|
54
|
+
labels.length < 2 ||
|
|
55
|
+
suffix.length > 253 ||
|
|
56
|
+
!labels.every((label) => label.length <= 63 &&
|
|
57
|
+
/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label)))
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
/** The maintained IP parser owns CIDR matching, including mapped IPv4. */
|
|
63
|
+
export function createPublicEgressPolicy(configuration = {}) {
|
|
64
|
+
if (!validatePublicEgressPolicyEntries(configuration))
|
|
65
|
+
throw new VeilError("invalid_input");
|
|
66
|
+
const ports = new Set([443, ...(configuration.extraPorts ?? [])]);
|
|
67
|
+
if ([...ports].some((port) => !Number.isInteger(port) || port < 1 || port > 65535))
|
|
68
|
+
throw new VeilError("invalid_input");
|
|
69
|
+
try {
|
|
70
|
+
const suffixes = configuration.deniedHostSuffixes ?? [];
|
|
71
|
+
const ranges = (configuration.deniedCidrs ?? []).map((cidr) => ipaddr.parseCIDR(cidr));
|
|
72
|
+
function address(value) {
|
|
73
|
+
const canonical = canonicalAddress(value);
|
|
74
|
+
if (canonical !== null &&
|
|
75
|
+
ipaddr.subnetMatch(ipaddr.process(canonical), { denied: ranges }, "allowed") === "denied")
|
|
76
|
+
throw new VeilError("denied");
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
destination(url) {
|
|
80
|
+
const port = url.port === ""
|
|
81
|
+
? url.protocol === "https:"
|
|
82
|
+
? 443
|
|
83
|
+
: 80
|
|
84
|
+
: Number(url.port);
|
|
85
|
+
if (!ports.has(port))
|
|
86
|
+
throw new VeilError("denied");
|
|
87
|
+
const hostname = url.hostname.toLowerCase().replace(/\.$/u, "");
|
|
88
|
+
if (suffixes.some((suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`)))
|
|
89
|
+
throw new VeilError("denied");
|
|
90
|
+
address(hostname);
|
|
91
|
+
},
|
|
92
|
+
addresses(values) {
|
|
93
|
+
for (const value of values)
|
|
94
|
+
address(value);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new VeilError("invalid_input");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export { validatePublicEgressPolicyEntries } from "./public-egress-policy.js";
|
|
2
|
+
import { type PublicEgressPolicy } from "./public-egress-policy.js";
|
|
3
|
+
export { createPublicEgressPolicy, type PublicEgressPolicy, } from "./public-egress-policy.js";
|
|
4
|
+
import { VeilError } from "./index.js";
|
|
5
|
+
import { type HostResolver } from "./model-key-network.js";
|
|
6
|
+
/**
|
|
7
|
+
* Veil's public-web door: an anonymous GET to a public URL, with no
|
|
8
|
+
* credential, from the one sanctioned egress. The name is resolved once,
|
|
9
|
+
* every answer must be public unicast (IPv4 and IPv6; loopback, private,
|
|
10
|
+
* link-local, metadata and address-embedding ranges are refused), and the
|
|
11
|
+
* connection is made to exactly one of those checked addresses. Host and TLS
|
|
12
|
+
* server name stay the URL's own, so certificates verify against the name the
|
|
13
|
+
* caller asked for while a second lookup can never move the connection (DNS
|
|
14
|
+
* rebinding). Each call is one hop: it never follows a redirect, so the
|
|
15
|
+
* caller's next hop resolves and is checked again.
|
|
16
|
+
*/
|
|
17
|
+
/** A connected socket, shaped like the Workers `cloudflare:sockets` Socket. */
|
|
18
|
+
export type PublicSocket = Readonly<{
|
|
19
|
+
readable: ReadableStream<Uint8Array>;
|
|
20
|
+
writable: WritableStream<Uint8Array>;
|
|
21
|
+
opened: Promise<unknown>;
|
|
22
|
+
startTls: (options: Readonly<{
|
|
23
|
+
expectedServerHostname: string;
|
|
24
|
+
}>) => PublicSocket;
|
|
25
|
+
close: () => Promise<void>;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* The host's socket primitive. Veil calls it only with an address it checked,
|
|
29
|
+
* never with a name, so no resolver sits between the check and the connection.
|
|
30
|
+
*/
|
|
31
|
+
export type PublicConnect = (address: Readonly<{
|
|
32
|
+
hostname: string;
|
|
33
|
+
port: number;
|
|
34
|
+
}>, options: Readonly<{
|
|
35
|
+
secureTransport: "off" | "starttls";
|
|
36
|
+
allowHalfOpen: false;
|
|
37
|
+
}>) => PublicSocket;
|
|
38
|
+
/** One anonymous GET. Only these two request headers can ever be sent. */
|
|
39
|
+
export type PublicGetRequest = Readonly<{
|
|
40
|
+
url: URL;
|
|
41
|
+
accept: string;
|
|
42
|
+
userAgent: string;
|
|
43
|
+
signal: AbortSignal;
|
|
44
|
+
}>;
|
|
45
|
+
export type PublicGet = (request: PublicGetRequest) => Promise<Response>;
|
|
46
|
+
export type PublicFetchDoor = Readonly<{
|
|
47
|
+
get: PublicGet;
|
|
48
|
+
}>;
|
|
49
|
+
/** Where a refusal came from; the message never carries remote bytes. */
|
|
50
|
+
export type PublicFetchStage = "address" | "dns" | "connect" | "response";
|
|
51
|
+
export declare class PublicFetchError extends VeilError {
|
|
52
|
+
readonly stage: PublicFetchStage;
|
|
53
|
+
constructor(code: VeilError["code"], stage: PublicFetchStage);
|
|
54
|
+
}
|
|
55
|
+
/** Private HTTP adapter shared by anonymous GET and the OpenAPI policy door. */
|
|
56
|
+
export type PublicHttpRequest = Readonly<{
|
|
57
|
+
url: URL;
|
|
58
|
+
method: string;
|
|
59
|
+
headers: Headers;
|
|
60
|
+
body: Uint8Array;
|
|
61
|
+
signal: AbortSignal;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* The public-fetch door. `resolve` is the only name lookup and runs once per
|
|
65
|
+
* call; `connect` is handed only the addresses that lookup returned and
|
|
66
|
+
* Veil checked.
|
|
67
|
+
*/
|
|
68
|
+
export type PublicHttpDoor = Readonly<{
|
|
69
|
+
send: (request: PublicHttpRequest) => Promise<Response>;
|
|
70
|
+
}>;
|
|
71
|
+
export declare function createPublicHttp(options: Readonly<{
|
|
72
|
+
connect: PublicConnect;
|
|
73
|
+
resolve: HostResolver;
|
|
74
|
+
networkPolicy?: PublicEgressPolicy;
|
|
75
|
+
}>): PublicHttpDoor;
|
|
76
|
+
/** Anonymous GET keeps its existing two-header authority. */
|
|
77
|
+
export declare function createPublicFetch(options: Readonly<{
|
|
78
|
+
connect: PublicConnect;
|
|
79
|
+
resolve: HostResolver;
|
|
80
|
+
networkPolicy?: PublicEgressPolicy;
|
|
81
|
+
}>): PublicFetchDoor;
|
|
82
|
+
/**
|
|
83
|
+
* The production door: the host's socket primitive plus its `fetch`, used only
|
|
84
|
+
* for the DNS-over-HTTPS lookup of the one resolution each call makes.
|
|
85
|
+
*/
|
|
86
|
+
export declare function configuredPublicFetch(options: Readonly<{
|
|
87
|
+
connect: PublicConnect;
|
|
88
|
+
fetch: typeof fetch;
|
|
89
|
+
networkPolicy?: PublicEgressPolicy;
|
|
90
|
+
}>): PublicFetchDoor;
|