@query-farm/vgi 0.37.0 → 0.38.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 +74 -0
- package/dist/catalog/attach-option.d.ts +11 -8
- package/dist/catalog/composite.d.ts +23 -1
- package/dist/catalog/contents-digest.d.ts +25 -0
- package/dist/catalog/interface.d.ts +107 -2
- package/dist/catalog/read-only.d.ts +45 -1
- package/dist/client/client.d.ts +19 -1
- package/dist/client-entry.js +141 -4
- package/dist/client-entry.js.map +10 -9
- package/dist/codec/asd.d.ts +8 -0
- package/dist/generated/vgi-client.d.ts +19 -0
- package/dist/generated/vgi-protocol-schemas.d.ts +3 -0
- package/dist/generated/vgi-protocol-types.d.ts +26 -0
- package/dist/http/fetch.d.ts +2 -1
- package/dist/index.core.d.ts +5 -2
- package/dist/index.js +574 -36
- package/dist/index.js.map +21 -17
- package/dist/protocol/handlers/catalog/contents.d.ts +27 -0
- package/dist/rpc-server.d.ts +97 -0
- package/dist/serve-entry.d.ts +2 -1
- package/dist/serve-entry.js +405 -25
- package/dist/serve-entry.js.map +18 -14
- package/dist/worker-cf-entry.d.ts +3 -0
- package/dist/worker-cf-entry.js +528 -31
- package/dist/worker-cf-entry.js.map +21 -17
- package/dist/worker.d.ts +9 -1
- package/package.json +3 -3
- package/src/catalog/attach-option.ts +11 -8
- package/src/catalog/composite.ts +81 -0
- package/src/catalog/contents-digest.ts +78 -0
- package/src/catalog/interface.ts +167 -5
- package/src/catalog/read-only.ts +68 -6
- package/src/client/client.ts +53 -0
- package/src/codec/asd.ts +96 -1
- package/src/generated/vgi-client.ts +22 -1
- package/src/generated/vgi-protocol-schemas.ts +30 -1
- package/src/generated/vgi-protocol-types.ts +126 -0
- package/src/http/fetch.ts +10 -2
- package/src/index.core.ts +21 -2
- package/src/protocol/dispatch.ts +1 -1
- package/src/protocol/handlers/catalog/admin.ts +35 -26
- package/src/protocol/handlers/catalog/contents.ts +162 -0
- package/src/protocol/handlers/catalog/macro.ts +2 -1
- package/src/protocol/handlers/catalog/shared.ts +12 -1
- package/src/rpc-server.ts +273 -0
- package/src/serve-entry.ts +7 -1
- package/src/worker-cf-entry.ts +8 -0
- package/src/worker.ts +24 -6
|
@@ -52,7 +52,8 @@ export function registerCatalogMacroMethods(protocol: Protocol, getCatalog: GetC
|
|
|
52
52
|
result: emptyResultSchema,
|
|
53
53
|
handler: async (params) => {
|
|
54
54
|
const cat = getCatalog();
|
|
55
|
-
|
|
55
|
+
// catalogUnary already flattened the wrapped request.
|
|
56
|
+
const innerParams = params;
|
|
56
57
|
await cat.macroCreate(
|
|
57
58
|
toUint8Array(innerParams.attach_opaque_data),
|
|
58
59
|
innerParams.schema_path,
|
|
@@ -7,7 +7,7 @@ import type { CatalogInterface } from "../../../catalog/interface.js";
|
|
|
7
7
|
import { NoCatalogError } from "../../../errors.js";
|
|
8
8
|
import { deserializeBatch } from "../../../util/arrow/index.js";
|
|
9
9
|
import { toUint8Array } from "../../../util/bytes.js";
|
|
10
|
-
import { normalizeRequestSchemaPaths } from "../shared.js";
|
|
10
|
+
import { normalizeRequestSchemaPaths, unwrapRequest } from "../shared.js";
|
|
11
11
|
import {
|
|
12
12
|
sealBytes,
|
|
13
13
|
openBytes,
|
|
@@ -195,6 +195,17 @@ export function catalogUnary(
|
|
|
195
195
|
result: config.result as any,
|
|
196
196
|
doc: config.doc,
|
|
197
197
|
handler: async (params: Record<string, any>, ctx: any) => {
|
|
198
|
+
// catalog_create / catalog_table_create / catalog_macro_create (and
|
|
199
|
+
// catalog_index_create) send one `request: binary` column holding the
|
|
200
|
+
// IPC-serialized request record, so their attach/transaction values ride
|
|
201
|
+
// INSIDE it. Flatten it first, so handlers read the request's fields
|
|
202
|
+
// directly and those values are opened (and the framework UUID stripped)
|
|
203
|
+
// like every other catalog call's. Before this, table_create read every
|
|
204
|
+
// field off the envelope (all undefined) and macro_create handed the
|
|
205
|
+
// catalog an unopened attach value.
|
|
206
|
+
if (params.request != null && params.attach_opaque_data === undefined && params.name === undefined) {
|
|
207
|
+
params = unwrapRequest(params.request);
|
|
208
|
+
}
|
|
198
209
|
normalizeRequestSchemaPaths(params);
|
|
199
210
|
await unwrapParamsOpaque(params, ctx, signingKey);
|
|
200
211
|
return config.handler(params, ctx);
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
// The one place a VGI worker's vgi-rpc server is built.
|
|
3
|
+
//
|
|
4
|
+
// Every transport a worker serves -- stdin/stdout, AF_UNIX (the launcher), raw
|
|
5
|
+
// TCP, the Iroh raw upstream, a caller-provided byte stream (Web Worker), and
|
|
6
|
+
// HTTP (Bun.serve and Cloudflare Workers alike) -- builds its server through
|
|
7
|
+
// `buildRpcServer`. Before this existed each built its own, five times over,
|
|
8
|
+
// which is how "what is hosted on which transport" drifts.
|
|
9
|
+
//
|
|
10
|
+
// What a server hosts, in reflection order:
|
|
11
|
+
//
|
|
12
|
+
// 1. The worker's own protocol, `vgi.v2`.
|
|
13
|
+
// 2. The worker's `hostedProtocols()`, in the order returned -- on EVERY
|
|
14
|
+
// transport.
|
|
15
|
+
// 3. `vgi_rpc.Reflection.v1` -- on every transport.
|
|
16
|
+
// 4. `vgi_rpc.Identity.v1` -- HTTP only, and only when the worker supplies
|
|
17
|
+
// `resolveToken` and/or `mintGrant`.
|
|
18
|
+
//
|
|
19
|
+
// Extra protocols cannot change `vgi.v2` behaviour: vgi-rpc routes every
|
|
20
|
+
// request on its `vgi_rpc.protocol` key with no fallback to the primary, so a
|
|
21
|
+
// client that only ever names `vgi.v2` (the DuckDB extension) dispatches
|
|
22
|
+
// exactly as it would against a single-protocol server.
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
type GrantMinter,
|
|
26
|
+
IdentityImpl,
|
|
27
|
+
type Protocol,
|
|
28
|
+
type TokenResolver,
|
|
29
|
+
VgiRpcServer,
|
|
30
|
+
} from "@query-farm/vgi-rpc";
|
|
31
|
+
|
|
32
|
+
/** The transport a server is being built for. Only `"http"` changes what is
|
|
33
|
+
* hosted (identity); the rest are recorded so the decision lives here rather
|
|
34
|
+
* than at each call site. `"iroh"` is the raw TCP upstream of a
|
|
35
|
+
* `vgi-iroh-bridge`; `"stream"` a caller-provided byte-stream pair. */
|
|
36
|
+
export type ServerTransport = "pipe" | "unix" | "tcp" | "iroh" | "stream" | "http";
|
|
37
|
+
|
|
38
|
+
const TRANSPORTS: ReadonlySet<string> = new Set<ServerTransport>(["pipe", "unix", "tcp", "iroh", "stream", "http"]);
|
|
39
|
+
|
|
40
|
+
/** Transports that authenticate their callers, and so may host
|
|
41
|
+
* `vgi_rpc.Identity.v1`. Identity answers questions about *other* callers'
|
|
42
|
+
* credentials; its allowlist is a list of principals, which a transport with
|
|
43
|
+
* no caller identity cannot check. */
|
|
44
|
+
const IDENTITY_TRANSPORTS: ReadonlySet<string> = new Set<ServerTransport>(["http"]);
|
|
45
|
+
|
|
46
|
+
const RESERVED_PREFIX = "vgi_rpc.";
|
|
47
|
+
|
|
48
|
+
/** Environment variable naming the principals allowed to call `introspect_token`. */
|
|
49
|
+
export const INTROSPECT_PRINCIPALS_ENV = "VGI_INTROSPECT_PRINCIPALS";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What a worker hosts beyond `vgi.v2`, and whether it opts into identity.
|
|
53
|
+
*
|
|
54
|
+
* Accepted by `new Worker({...})`, `serveVgiWorker`, `createVgiWorkerFetch`
|
|
55
|
+
* and `createVgiFetch` (Cloudflare Workers) alike, so one worker definition
|
|
56
|
+
* hosts the same set on every transport.
|
|
57
|
+
*/
|
|
58
|
+
export interface HostingOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Additional vgi-rpc protocols to host beside `vgi.v2`, on the **same**
|
|
61
|
+
* listener, whatever the transport.
|
|
62
|
+
*
|
|
63
|
+
* A vgi-rpc `Protocol` carries its handlers, so each entry is a whole
|
|
64
|
+
* `(protocol, implementation)` pair. Called **once**, when the server is
|
|
65
|
+
* built; it may consult configuration or the environment, but the answer is
|
|
66
|
+
* fixed for the life of the process, so reflection output and protocol
|
|
67
|
+
* hashes stay stable. Hosted after `vgi.v2`, in the order returned.
|
|
68
|
+
*
|
|
69
|
+
* The protocol is the unit of optionality: there is no way to host a subset
|
|
70
|
+
* of a protocol's methods. A capability that is optional is its own
|
|
71
|
+
* protocol, returned here or not.
|
|
72
|
+
*
|
|
73
|
+
* Each needs a distinct wire name, which may not be `vgi.v2` and may not
|
|
74
|
+
* use the reserved `vgi_rpc.` prefix: reflection is hosted automatically and
|
|
75
|
+
* `vgi_rpc.Identity.v1` is enabled through {@link resolveToken} /
|
|
76
|
+
* {@link mintGrant}. Violations are a startup error naming this hook.
|
|
77
|
+
*/
|
|
78
|
+
hostedProtocols?: () => Iterable<Protocol>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve an opaque bearer credential to the identity it authenticates as.
|
|
82
|
+
* Supplying it hosts `vgi_rpc.Identity.v1`'s `introspect_token` -- on HTTP,
|
|
83
|
+
* the transport that authenticates callers. Absent, the method is absent
|
|
84
|
+
* (not hosted-and-refusing), which keeps a dependency upgrade from growing a
|
|
85
|
+
* credential-to-identity oracle on every existing worker.
|
|
86
|
+
*
|
|
87
|
+
* Return `null` for "the store answered and this credential is unknown".
|
|
88
|
+
* For "I could not find out" (a store or sidecar outage, a timeout, a 5xx)
|
|
89
|
+
* throw `AuthUnavailableError` from `@query-farm/vgi-rpc` -- the same error
|
|
90
|
+
* an `authenticate` callback throws for an outage. The framework translates
|
|
91
|
+
* it to `identity_unavailable` with that error's retry hint
|
|
92
|
+
* (`vgi_rpc.RetryInfo`), so callers know to retry rather than cache a
|
|
93
|
+
* refusal. `IdentityUnavailableError` works too. Never throw a plain
|
|
94
|
+
* `Error` for an outage: a caller cannot tell it from a definitive answer.
|
|
95
|
+
*
|
|
96
|
+
* Requires {@link introspectPrincipals} (or `VGI_INTROSPECT_PRINCIPALS`);
|
|
97
|
+
* there is no permissive default, and a worker without one refuses to start.
|
|
98
|
+
*/
|
|
99
|
+
resolveToken?: TokenResolver;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Mint a standing delegation credential for the *calling* user. Supplying
|
|
103
|
+
* it hosts `issue_grant` (HTTP only). Throw `GrantRefusedError` to decline,
|
|
104
|
+
* and `AuthUnavailableError` for a transient failure (translated as for
|
|
105
|
+
* {@link resolveToken}). Needs no allowlist: it is not an oracle about
|
|
106
|
+
* anybody else.
|
|
107
|
+
*/
|
|
108
|
+
mintGrant?: GrantMinter;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Principals permitted to call `introspect_token`. When omitted,
|
|
112
|
+
* `VGI_INTROSPECT_PRINCIPALS` (comma-separated) is read. Only consulted when
|
|
113
|
+
* {@link resolveToken} is supplied.
|
|
114
|
+
*/
|
|
115
|
+
introspectPrincipals?: Iterable<string>;
|
|
116
|
+
|
|
117
|
+
/** Maximum age of the caller's authentication for `issue_grant`, in
|
|
118
|
+
* seconds. vgi-rpc's default when omitted. */
|
|
119
|
+
maxAuthAge?: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Server-level options passed through to `VgiRpcServer`. */
|
|
123
|
+
export interface BuildRpcServerOptions {
|
|
124
|
+
/** Which transport this server will serve. */
|
|
125
|
+
transport: ServerTransport;
|
|
126
|
+
/** Server identifier, surfaced through reflection. */
|
|
127
|
+
serverId?: string;
|
|
128
|
+
/** Environment to read `VGI_INTROSPECT_PRINCIPALS` from. Default
|
|
129
|
+
* `process.env` where it exists (not on Cloudflare Workers). */
|
|
130
|
+
env?: Record<string, string | undefined>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build the vgi-rpc server for a worker's `vgi.v2` protocol on `transport`.
|
|
135
|
+
*
|
|
136
|
+
* Calls `hosting.hostedProtocols()` exactly once and hosts the result beside
|
|
137
|
+
* `primary`; see the module comment for the full hosted set.
|
|
138
|
+
*
|
|
139
|
+
* @throws TypeError `hostedProtocols()` returned something other than an
|
|
140
|
+
* iterable of vgi-rpc `Protocol`s.
|
|
141
|
+
* @throws Error A hosted protocol's name is invalid, reserved, `vgi.v2`'s, or
|
|
142
|
+
* repeated; the transport is unknown; or the worker supplies `resolveToken`
|
|
143
|
+
* on HTTP with no introspector allowlist.
|
|
144
|
+
*/
|
|
145
|
+
export function buildRpcServer(
|
|
146
|
+
primary: Protocol,
|
|
147
|
+
hosting: HostingOptions,
|
|
148
|
+
options: BuildRpcServerOptions,
|
|
149
|
+
): VgiRpcServer {
|
|
150
|
+
if (!TRANSPORTS.has(options.transport)) {
|
|
151
|
+
throw new Error(`Unknown transport ${JSON.stringify(options.transport)}; expected one of ${[...TRANSPORTS].join(", ")}.`);
|
|
152
|
+
}
|
|
153
|
+
const protocols = validatedHostedProtocols(hosting, primary);
|
|
154
|
+
const identity = IDENTITY_TRANSPORTS.has(options.transport) ? buildIdentity(hosting, options.env) : undefined;
|
|
155
|
+
return new VgiRpcServer(primary, {
|
|
156
|
+
serverId: options.serverId,
|
|
157
|
+
protocols,
|
|
158
|
+
identity,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Call `hostedProtocols()` once and check its shape and names.
|
|
163
|
+
*
|
|
164
|
+
* vgi-rpc checks these too, but its messages name a protocol and not the
|
|
165
|
+
* worker hook that supplied it; checking here first lets the error say which
|
|
166
|
+
* option to fix. */
|
|
167
|
+
function validatedHostedProtocols(hosting: HostingOptions, primary: Protocol): Protocol[] {
|
|
168
|
+
if (hosting.hostedProtocols === undefined) return [];
|
|
169
|
+
const owner = "hostedProtocols()";
|
|
170
|
+
if (typeof hosting.hostedProtocols !== "function") {
|
|
171
|
+
throw new TypeError(`${owner} must be a function returning vgi-rpc Protocols, got ${typeof hosting.hostedProtocols}.`);
|
|
172
|
+
}
|
|
173
|
+
const raw = hosting.hostedProtocols();
|
|
174
|
+
if (raw == null || typeof (raw as Iterable<Protocol>)[Symbol.iterator] !== "function" || typeof raw === "string") {
|
|
175
|
+
throw new TypeError(`${owner} must return an iterable of vgi-rpc Protocols, got ${String(raw)}.`);
|
|
176
|
+
}
|
|
177
|
+
const seen = new Set<string>();
|
|
178
|
+
const out: Protocol[] = [];
|
|
179
|
+
let index = 0;
|
|
180
|
+
for (const proto of raw as Iterable<Protocol>) {
|
|
181
|
+
const p = proto as Partial<Protocol> | null;
|
|
182
|
+
if (p == null || typeof p.name !== "string" || typeof p.getMethod !== "function") {
|
|
183
|
+
throw new TypeError(`${owner} entry ${index} must be a vgi-rpc Protocol, got ${String(proto)}.`);
|
|
184
|
+
}
|
|
185
|
+
const name = p.name;
|
|
186
|
+
if (name.startsWith(RESERVED_PREFIX)) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`${owner} entry ${index} is named '${name}', which claims the reserved '${RESERVED_PREFIX}' prefix. ` +
|
|
189
|
+
"Framework protocols are not supplied through this hook: reflection is hosted automatically, and " +
|
|
190
|
+
"vgi_rpc.Identity.v1 is enabled by supplying resolveToken and/or mintGrant.",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.]*$/.test(name)) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`${owner} entry ${index}: protocol name '${name}' is not an identifier, optionally dot-qualified ` +
|
|
196
|
+
"(e.g. 'acme.Reports.v1').",
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (name === primary.name) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`${owner} entry ${index} is named '${name}', the worker's own protocol. Give it a distinct name.`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (seen.has(name)) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`${owner} lists protocol name '${name}' twice. The name is the routing key, so each hosted protocol ` +
|
|
207
|
+
"needs a distinct name.",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
seen.add(name);
|
|
211
|
+
out.push(proto);
|
|
212
|
+
index++;
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Build the `vgi_rpc.Identity.v1` implementation, or `undefined`.
|
|
218
|
+
*
|
|
219
|
+
* `undefined` unless the worker supplies at least one hook -- and that is the
|
|
220
|
+
* point: the server then does not host the protocol at all, rather than
|
|
221
|
+
* hosting it and refusing every call. The two hooks are independent. */
|
|
222
|
+
function buildIdentity(
|
|
223
|
+
hosting: HostingOptions,
|
|
224
|
+
env: Record<string, string | undefined> | undefined,
|
|
225
|
+
): IdentityImpl | undefined {
|
|
226
|
+
const { resolveToken, mintGrant } = hosting;
|
|
227
|
+
if (!resolveToken && !mintGrant) return undefined;
|
|
228
|
+
return new IdentityImpl({
|
|
229
|
+
resolveToken,
|
|
230
|
+
mintGrant,
|
|
231
|
+
// Only meaningful for `introspect_token`; a worker that mints but resolves
|
|
232
|
+
// nothing is not an oracle and needs no allowlist.
|
|
233
|
+
introspectPrincipals: resolveToken ? resolveIntrospectPrincipals(hosting.introspectPrincipals, env) : undefined,
|
|
234
|
+
maxAuthAge: hosting.maxAuthAge,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve the introspector allowlist, or refuse to start.
|
|
240
|
+
*
|
|
241
|
+
* Fail-closed rather than defaulting to "any authenticated caller":
|
|
242
|
+
* authenticating and introspecting are different capabilities, and a
|
|
243
|
+
* permissive default lets any user resolve any other user's credential to its
|
|
244
|
+
* owner. A worker that supplies `resolveToken` and forgets the allowlist must
|
|
245
|
+
* not start.
|
|
246
|
+
*/
|
|
247
|
+
function resolveIntrospectPrincipals(
|
|
248
|
+
explicit: Iterable<string> | undefined,
|
|
249
|
+
env: Record<string, string | undefined> | undefined,
|
|
250
|
+
): string[] {
|
|
251
|
+
let principals: string[];
|
|
252
|
+
if (explicit !== undefined) {
|
|
253
|
+
principals = [...explicit].map((p) => p.trim()).filter((p) => p !== "");
|
|
254
|
+
} else {
|
|
255
|
+
const source = env ?? (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env;
|
|
256
|
+
const raw = source?.[INTROSPECT_PRINCIPALS_ENV] ?? "";
|
|
257
|
+
principals = raw
|
|
258
|
+
.split(",")
|
|
259
|
+
.map((p) => p.trim())
|
|
260
|
+
.filter((p) => p !== "");
|
|
261
|
+
}
|
|
262
|
+
if (principals.length === 0) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
"This worker supplies resolveToken, which hosts the vgi_rpc.Identity.v1 protocol, but no " +
|
|
265
|
+
`introspector allowlist was configured. Set ${INTROSPECT_PRINCIPALS_ENV} (comma-separated) or pass ` +
|
|
266
|
+
"introspectPrincipals.\n\n" +
|
|
267
|
+
"There is no permissive default on purpose: introspection is a separate capability from " +
|
|
268
|
+
"authentication, and allowing every authenticated caller lets any user resolve any other user's " +
|
|
269
|
+
"credential to its owner. Remove resolveToken to leave the protocol unhosted entirely.",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
return principals;
|
|
273
|
+
}
|
package/src/serve-entry.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type PeerResolutionOptions,
|
|
33
33
|
} from "@query-farm/vgi-rpc";
|
|
34
34
|
import { createVgiFetch } from "./http/fetch.js";
|
|
35
|
+
import type { HostingOptions } from "./rpc-server.js";
|
|
35
36
|
|
|
36
37
|
/** Environment variables `serveVgiWorker` reads. Injectable for testing. */
|
|
37
38
|
export interface ServeEnv {
|
|
@@ -61,7 +62,7 @@ export interface IrohHttpBridgeOptions {
|
|
|
61
62
|
peerResolutionContext?: (request: Request) => PeerResolutionOptions | Promise<PeerResolutionOptions>;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
export interface ServeVgiWorkerOptions {
|
|
65
|
+
export interface ServeVgiWorkerOptions extends HostingOptions {
|
|
65
66
|
/** Short worker name shown on the landing page, e.g. "ishares". */
|
|
66
67
|
name: string;
|
|
67
68
|
/** One-line description of what the worker serves. */
|
|
@@ -241,6 +242,11 @@ export function createVgiWorkerFetch(
|
|
|
241
242
|
? (bridge.authenticate === false ? observePeerIdentity : peerIdentityPrimary("iroh"))
|
|
242
243
|
: undefined,
|
|
243
244
|
peerResolutionContext: bridge?.peerResolutionContext,
|
|
245
|
+
hostedProtocols: opts.hostedProtocols,
|
|
246
|
+
resolveToken: opts.resolveToken,
|
|
247
|
+
mintGrant: opts.mintGrant,
|
|
248
|
+
introspectPrincipals: opts.introspectPrincipals,
|
|
249
|
+
maxAuthAge: opts.maxAuthAge,
|
|
244
250
|
});
|
|
245
251
|
}
|
|
246
252
|
|
package/src/worker-cf-entry.ts
CHANGED
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
|
|
16
16
|
export { createVgiFetch } from "./http/fetch.js";
|
|
17
17
|
export type { VgiFetchOptions } from "./http/fetch.js";
|
|
18
|
+
export type { HostingOptions } from "./rpc-server.js";
|
|
19
|
+
export {
|
|
20
|
+
AuthUnavailableError,
|
|
21
|
+
GrantRefusedError,
|
|
22
|
+
IdentityUnavailableError,
|
|
23
|
+
StatusError,
|
|
24
|
+
} from "@query-farm/vgi-rpc";
|
|
25
|
+
export type { GrantMinter, IssuedGrant, TokenIdentity, TokenResolver } from "@query-farm/vgi-rpc";
|
|
18
26
|
|
|
19
27
|
// Re-export the public types CF Worker authors need to wire a registry +
|
|
20
28
|
// catalog. They import from "@query-farm/vgi/worker-cf" rather than from
|
package/src/worker.ts
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
// VGI Worker: main entry point for running a VGI function server.
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
VgiRpcServer,
|
|
6
5
|
observePeerIdentity,
|
|
7
6
|
peerIdentityPrimary,
|
|
8
7
|
serveUnix,
|
|
9
8
|
serveTcp,
|
|
10
9
|
serveStream as serveStreamRpc,
|
|
10
|
+
type VgiRpcServer,
|
|
11
11
|
} from "@query-farm/vgi-rpc";
|
|
12
|
+
import { buildRpcServer, type HostingOptions, type ServerTransport } from "./rpc-server.js";
|
|
12
13
|
import { FunctionRegistry } from "./functions/registry.js";
|
|
13
14
|
import type { VgiFunction } from "./functions/types.js";
|
|
14
15
|
import { buildVgiProtocol } from "./protocol/dispatch.js";
|
|
@@ -105,7 +106,7 @@ function parseLauncherArgs(argv: readonly string[]): LauncherArgs {
|
|
|
105
106
|
return out;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
export interface WorkerConfig {
|
|
109
|
+
export interface WorkerConfig extends HostingOptions {
|
|
109
110
|
functions?: VgiFunction[];
|
|
110
111
|
catalog?: CatalogDescriptor;
|
|
111
112
|
catalogInterface?: CatalogInterface;
|
|
@@ -124,6 +125,7 @@ export interface WorkerConfig {
|
|
|
124
125
|
|
|
125
126
|
export class Worker {
|
|
126
127
|
private _registry: FunctionRegistry;
|
|
128
|
+
private _hosting: HostingOptions;
|
|
127
129
|
private _catalogInterface?: CatalogInterface;
|
|
128
130
|
private _catalogName?: string;
|
|
129
131
|
|
|
@@ -133,6 +135,13 @@ export class Worker {
|
|
|
133
135
|
// index is what makes a schema-qualified bind resolve correctly.
|
|
134
136
|
this._registry = config.registry ?? new FunctionRegistry();
|
|
135
137
|
this._catalogName = config.catalogName;
|
|
138
|
+
this._hosting = {
|
|
139
|
+
hostedProtocols: config.hostedProtocols,
|
|
140
|
+
resolveToken: config.resolveToken,
|
|
141
|
+
mintGrant: config.mintGrant,
|
|
142
|
+
introspectPrincipals: config.introspectPrincipals,
|
|
143
|
+
maxAuthAge: config.maxAuthAge,
|
|
144
|
+
};
|
|
136
145
|
|
|
137
146
|
// Register explicit functions
|
|
138
147
|
if (config.functions) {
|
|
@@ -195,7 +204,7 @@ export class Worker {
|
|
|
195
204
|
debug(() =>
|
|
196
205
|
`[worker] AF_UNIX mode: ${launcher.unixPath} idle=${idleTimeout ?? 300}s${envOverride !== undefined && envOverride !== "" ? " (env override)" : ""}`,
|
|
197
206
|
);
|
|
198
|
-
serveUnix(protocol, {
|
|
207
|
+
serveUnix(this.buildServer(protocol, "unix"), {
|
|
199
208
|
unixPath: launcher.unixPath,
|
|
200
209
|
idleTimeout,
|
|
201
210
|
}).then((handle) => handle.done).then(() => {
|
|
@@ -220,7 +229,7 @@ export class Worker {
|
|
|
220
229
|
debug(() =>
|
|
221
230
|
`[worker] TCP mode: ${launcher.tcpHost}:${launcher.tcpPort} idle=${idleTimeout ?? 300}s${envOverride !== undefined && envOverride !== "" ? " (env override)" : ""}`,
|
|
222
231
|
);
|
|
223
|
-
serveTcp(protocol, {
|
|
232
|
+
serveTcp(this.buildServer(protocol, launcher.irohRawUpstream ? "iroh" : "tcp"), {
|
|
224
233
|
host: launcher.tcpHost,
|
|
225
234
|
port: launcher.tcpPort,
|
|
226
235
|
idleTimeout,
|
|
@@ -245,7 +254,7 @@ export class Worker {
|
|
|
245
254
|
return;
|
|
246
255
|
}
|
|
247
256
|
|
|
248
|
-
const server =
|
|
257
|
+
const server = this.buildServer(protocol, "pipe");
|
|
249
258
|
debug(() => `[worker] server created, calling run()`);
|
|
250
259
|
server.run().then(() => {
|
|
251
260
|
debug(() => `[worker] server.run() resolved cleanly`);
|
|
@@ -259,6 +268,15 @@ export class Worker {
|
|
|
259
268
|
}
|
|
260
269
|
}
|
|
261
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Build the vgi-rpc server for one transport: `vgi.v2`, the worker's
|
|
273
|
+
* `hostedProtocols()` and reflection everywhere; identity on HTTP only. The
|
|
274
|
+
* one construction path every transport uses (see `rpc-server.ts`).
|
|
275
|
+
*/
|
|
276
|
+
private buildServer(protocol: Protocol, transport: ServerTransport): VgiRpcServer {
|
|
277
|
+
return buildRpcServer(protocol, this._hosting, { transport });
|
|
278
|
+
}
|
|
279
|
+
|
|
262
280
|
/** Build the VGI protocol from this worker's registry + catalog interface. */
|
|
263
281
|
private buildProtocol(): Protocol {
|
|
264
282
|
return buildVgiProtocol({
|
|
@@ -286,6 +304,6 @@ export class Worker {
|
|
|
286
304
|
readable: ReadableStream<Uint8Array> | NodeJS.ReadableStream,
|
|
287
305
|
writable?: number | import("node:net").Socket | import("@query-farm/vgi-rpc").ByteSink,
|
|
288
306
|
): Promise<void> {
|
|
289
|
-
await serveStreamRpc(this.buildProtocol(), { readable, writable });
|
|
307
|
+
await serveStreamRpc(this.buildServer(this.buildProtocol(), "stream"), { readable, writable });
|
|
290
308
|
}
|
|
291
309
|
}
|