@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -2
  3. package/dist/best-effort-cleanup.d.ts +2 -0
  4. package/dist/best-effort-cleanup.js +5 -0
  5. package/dist/event-verifiers.d.ts +80 -0
  6. package/dist/event-verifiers.js +168 -0
  7. package/dist/gatekeeper.d.ts +202 -0
  8. package/dist/gatekeeper.js +231 -0
  9. package/dist/index.d.ts +254 -0
  10. package/dist/index.js +267 -0
  11. package/dist/model-credentials.d.ts +226 -0
  12. package/dist/model-credentials.js +133 -0
  13. package/dist/model-key-grants.d.ts +56 -0
  14. package/dist/model-key-grants.js +107 -0
  15. package/dist/model-key-network.d.ts +33 -0
  16. package/dist/model-key-network.js +202 -0
  17. package/dist/model-keys.d.ts +42 -0
  18. package/dist/model-keys.js +115 -0
  19. package/dist/nango-custody-http.d.ts +16 -0
  20. package/dist/nango-custody-http.js +93 -0
  21. package/dist/nango-git.d.ts +19 -0
  22. package/dist/nango-git.js +125 -0
  23. package/dist/nango-model-keys.d.ts +13 -0
  24. package/dist/nango-model-keys.js +48 -0
  25. package/dist/nango-world-seat.d.ts +14 -0
  26. package/dist/nango-world-seat.js +47 -0
  27. package/dist/nango.d.ts +16 -0
  28. package/dist/nango.js +300 -0
  29. package/dist/openapi-contract.d.ts +64 -0
  30. package/dist/openapi-contract.js +11 -0
  31. package/dist/openapi-effect.d.ts +9 -0
  32. package/dist/openapi-effect.js +284 -0
  33. package/dist/openapi-http.d.ts +2 -0
  34. package/dist/openapi-http.js +37 -0
  35. package/dist/openapi-secrets.d.ts +6 -0
  36. package/dist/openapi-secrets.js +80 -0
  37. package/dist/openapi.d.ts +5 -0
  38. package/dist/openapi.js +24 -0
  39. package/dist/owned-infrastructure.d.ts +13 -0
  40. package/dist/owned-infrastructure.js +17 -0
  41. package/dist/provider-egress.d.ts +51 -0
  42. package/dist/provider-egress.js +155 -0
  43. package/dist/public-egress-policy.d.ts +13 -0
  44. package/dist/public-egress-policy.js +101 -0
  45. package/dist/public-fetch.d.ts +90 -0
  46. package/dist/public-fetch.js +347 -0
  47. package/dist/webhook-trigger-secrets.d.ts +23 -0
  48. package/dist/webhook-trigger-secrets.js +53 -0
  49. package/dist/world-seat.d.ts +40 -0
  50. package/dist/world-seat.js +82 -0
  51. 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;