@query-farm/vgi 0.29.0 → 0.29.1
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 +26 -1
- package/dist/arguments/argument-spec.d.ts +4 -2
- package/dist/catalog/composite.d.ts +13 -13
- package/dist/catalog/descriptors.d.ts +7 -3
- package/dist/catalog/interface.d.ts +67 -40
- package/dist/catalog/read-only.d.ts +32 -15
- package/dist/client/client.d.ts +36 -29
- package/dist/client/types.d.ts +2 -0
- package/dist/client-entry.d.ts +1 -1
- package/dist/client-entry.js +441 -279
- package/dist/client-entry.js.map +12 -11
- package/dist/filter-pushdown/capabilities.d.ts +3 -0
- package/dist/filter-pushdown/deserialize.d.ts +14 -15
- package/dist/filter-pushdown/evaluate.d.ts +14 -44
- package/dist/filter-pushdown/index.d.ts +2 -2
- package/dist/filter-pushdown/types.d.ts +120 -68
- package/dist/functions/aggregate.d.ts +4 -0
- package/dist/functions/registry.d.ts +5 -5
- package/dist/functions/scalar.d.ts +6 -1
- package/dist/functions/table-buffering.d.ts +9 -0
- package/dist/functions/table-in-out.d.ts +13 -0
- package/dist/functions/table.d.ts +9 -2
- package/dist/functions/types.d.ts +22 -2
- package/dist/generated/vgi-client.d.ts +56 -27
- package/dist/generated/vgi-protocol-schemas.d.ts +15 -0
- package/dist/http/fetch.d.ts +7 -1
- package/dist/index.core.d.ts +2 -1
- package/dist/index.js +2317 -1354
- package/dist/index.js.map +43 -40
- package/dist/metadata/types.d.ts +15 -2
- package/dist/protocol/handlers/shared.d.ts +3 -1
- package/dist/protocol/serializers/splits.d.ts +1 -1
- package/dist/protocol/types.d.ts +9 -3
- package/dist/schema-path.d.ts +7 -0
- package/dist/serve-entry.d.ts +22 -1
- package/dist/serve-entry.js +412 -238
- package/dist/serve-entry.js.map +27 -26
- package/dist/split-token.d.ts +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/worker-cf-entry.js +2030 -1201
- package/dist/worker-cf-entry.js.map +40 -36
- package/dist/worker.d.ts +1 -1
- package/package.json +3 -3
- package/src/arguments/argument-spec.ts +12 -4
- package/src/catalog/composite.ts +26 -26
- package/src/catalog/descriptors.ts +12 -3
- package/src/catalog/interface.ts +98 -48
- package/src/catalog/read-only.ts +124 -67
- package/src/client/client.ts +92 -72
- package/src/client/types.ts +2 -0
- package/src/client-entry.ts +2 -1
- package/src/filter-pushdown/capabilities.ts +13 -0
- package/src/filter-pushdown/collector.ts +22 -35
- package/src/filter-pushdown/deserialize.ts +657 -221
- package/src/filter-pushdown/evaluate.ts +410 -499
- package/src/filter-pushdown/index.ts +16 -2
- package/src/filter-pushdown/types.ts +88 -71
- package/src/functions/aggregate.ts +9 -1
- package/src/functions/registry.ts +24 -22
- package/src/functions/scalar.ts +26 -0
- package/src/functions/table-buffering.ts +46 -7
- package/src/functions/table-in-out.ts +187 -30
- package/src/functions/table.ts +58 -20
- package/src/functions/types.ts +24 -1
- package/src/generated/vgi-client.ts +61 -29
- package/src/generated/vgi-protocol-schemas.ts +225 -54
- package/src/http/fetch.ts +12 -0
- package/src/index.core.ts +2 -0
- package/src/metadata/resolve.ts +43 -1
- package/src/metadata/serialize.ts +43 -5
- package/src/metadata/types.ts +17 -1
- package/src/protocol/dispatch.ts +1 -1
- package/src/protocol/handlers/aggregate.ts +13 -7
- package/src/protocol/handlers/catalog/admin.ts +6 -6
- package/src/protocol/handlers/catalog/index_methods.ts +2 -2
- package/src/protocol/handlers/catalog/macro.ts +4 -4
- package/src/protocol/handlers/catalog/shared.ts +2 -0
- package/src/protocol/handlers/catalog/table.ts +16 -16
- package/src/protocol/handlers/catalog/view.ts +5 -5
- package/src/protocol/handlers/function.ts +2 -2
- package/src/protocol/handlers/shared.ts +12 -3
- package/src/protocol/handlers/table-buffering.ts +4 -4
- package/src/protocol/serializers/bind.ts +15 -6
- package/src/protocol/serializers/splits.ts +4 -2
- package/src/protocol/types.ts +9 -3
- package/src/schema-path.ts +25 -0
- package/src/serve-entry.ts +65 -3
- package/src/split-token.ts +2 -2
- package/src/types.ts +9 -0
- package/src/worker.ts +49 -4
|
@@ -173,7 +173,7 @@ async function resolveSplitPayloads(
|
|
|
173
173
|
if (!request.split_tokens) return;
|
|
174
174
|
const fp = fingerprintInputs(toUint8Array(innerParams.bind_call));
|
|
175
175
|
const expected = await bindFingerprint(
|
|
176
|
-
fp.
|
|
176
|
+
fp.schemaPath,
|
|
177
177
|
fp.functionName,
|
|
178
178
|
fp.args,
|
|
179
179
|
fp.settings,
|
|
@@ -517,7 +517,7 @@ export function registerFunctionMethods(protocol: Protocol, config: FunctionHand
|
|
|
517
517
|
// layout can change without touching worker code in five languages.
|
|
518
518
|
const fp = fingerprintInputs(toUint8Array(innerParams.bind_call));
|
|
519
519
|
const fingerprint = await bindFingerprint(
|
|
520
|
-
fp.
|
|
520
|
+
fp.schemaPath,
|
|
521
521
|
fp.functionName,
|
|
522
522
|
fp.args,
|
|
523
523
|
fp.settings,
|
|
@@ -7,6 +7,7 @@ import { batchToScalarDict, deserializeBatch, serializeBatch, batchFromColumns }
|
|
|
7
7
|
import { toUint8Array } from "../../util/bytes.js";
|
|
8
8
|
import { TableInOutPhase } from "../../types.js";
|
|
9
9
|
import type { InitRequest } from "../types.js";
|
|
10
|
+
import { normalizeSchemaPath } from "../../schema-path.js";
|
|
10
11
|
|
|
11
12
|
// The Python vgi-rpc framework wraps ALL non-void unary results in a single
|
|
12
13
|
// "result" column. For ArrowSerializableDataclass types, the result is serialized
|
|
@@ -31,7 +32,15 @@ export const RESULT_BINARY_NULLABLE_SCHEMA = schema([
|
|
|
31
32
|
export function unwrapRequest(requestBytes: any): Record<string, any> {
|
|
32
33
|
const bytes = toUint8Array(requestBytes);
|
|
33
34
|
const innerBatch = deserializeBatch(bytes);
|
|
34
|
-
|
|
35
|
+
const params = batchToScalarDict(innerBatch);
|
|
36
|
+
normalizeRequestSchemaPaths(params);
|
|
37
|
+
return params;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Convert Arrow list scalars at RPC boundaries into ordinary string arrays. */
|
|
41
|
+
export function normalizeRequestSchemaPaths(params: Record<string, any>): void {
|
|
42
|
+
if (params.path != null) params.path = normalizeSchemaPath(params.path);
|
|
43
|
+
if (params.schema_path != null) params.schema_path = normalizeSchemaPath(params.schema_path);
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
/**
|
|
@@ -57,7 +66,7 @@ export function overloadContext(
|
|
|
57
66
|
arguments: any;
|
|
58
67
|
input_schema: any;
|
|
59
68
|
function_type: any;
|
|
60
|
-
|
|
69
|
+
schema_path?: string[] | null;
|
|
61
70
|
attach_opaque_data?: Uint8Array | null;
|
|
62
71
|
},
|
|
63
72
|
catalogInterface?: { catalogNameForAttach(a: Uint8Array): string | null },
|
|
@@ -78,7 +87,7 @@ export function overloadContext(
|
|
|
78
87
|
isScalar: String(req.function_type).toLowerCase() === "scalar",
|
|
79
88
|
// Scopes resolution when the caller named a schema; a name registered in
|
|
80
89
|
// several schemas is otherwise ambiguous.
|
|
81
|
-
|
|
90
|
+
schemaPath: req.schema_path ?? null,
|
|
82
91
|
catalogName,
|
|
83
92
|
};
|
|
84
93
|
}
|
|
@@ -46,19 +46,19 @@ export function registerTableBufferingMethods(
|
|
|
46
46
|
registry: FunctionRegistry,
|
|
47
47
|
signingKey?: Uint8Array,
|
|
48
48
|
): void {
|
|
49
|
-
// `
|
|
49
|
+
// `schemaPath` scopes resolution to the schema the caller named. The
|
|
50
50
|
// buffering process/combine RPCs carry it (protocol 1.2.0), so a name
|
|
51
51
|
// declared in two schemas resolves to the one the request names rather than
|
|
52
52
|
// colliding on the flat by-name index. `null` keeps the cross-schema lookup.
|
|
53
53
|
function resolveBuffering(
|
|
54
54
|
name: string,
|
|
55
|
-
|
|
55
|
+
schemaPath?: string[] | null,
|
|
56
56
|
): TableBufferingVgiFunction {
|
|
57
57
|
const func = registry.get(name, {
|
|
58
58
|
arguments: new Arguments(),
|
|
59
59
|
inputSchema: null,
|
|
60
60
|
isScalar: false,
|
|
61
|
-
|
|
61
|
+
schemaPath: schemaPath ?? null,
|
|
62
62
|
}) as any;
|
|
63
63
|
if (!func || func.kind !== "table_buffering") {
|
|
64
64
|
throw new Error(`Function '${name}' is not a table_buffering function`);
|
|
@@ -86,7 +86,7 @@ export function registerTableBufferingMethods(
|
|
|
86
86
|
: rawAttach;
|
|
87
87
|
const transactionId = inner.transaction_id ? toUint8Array(inner.transaction_id) : null;
|
|
88
88
|
|
|
89
|
-
const func = resolveBuffering(functionName, inner.
|
|
89
|
+
const func = resolveBuffering(functionName, inner.schema_path ?? null);
|
|
90
90
|
const bound = new BoundStorage(defaultStorage, executionId);
|
|
91
91
|
const payload = await bound.stateGet(
|
|
92
92
|
FrameworkNS.BUFFERING_INIT,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// ArrowSerializableDataclass field declaration order so the format is
|
|
4
4
|
// positional-compatible with the Python reader.
|
|
5
5
|
|
|
6
|
-
import { type VgiSchema, schema, field, type VgiBatch, type VgiDataType, utf8, binary, bool, struct, dictionary, int16 } from "../../arrow/index.js";
|
|
6
|
+
import { type VgiSchema, schema, field, type VgiBatch, type VgiDataType, utf8, binary, bool, struct, dictionary, int16, list } from "../../arrow/index.js";
|
|
7
7
|
import { Arguments } from "../../arguments/arguments.js";
|
|
8
8
|
import { FunctionType } from "../../types.js";
|
|
9
9
|
import type { BindRequest, BindResponse, CopyFromContext, CopyToContext } from "../types.js";
|
|
@@ -31,7 +31,7 @@ import { serializeArguments, deserializeArguments } from "./arguments.js";
|
|
|
31
31
|
// two schemas in full and leaves the reader to diff them by eye.
|
|
32
32
|
//
|
|
33
33
|
// So `copy_from` and `copy_to` are always present and null on a non-COPY bind,
|
|
34
|
-
// `
|
|
34
|
+
// `schema_path` and `argument_names` are last, and `function_type` is a dictionary
|
|
35
35
|
// rather than plain utf8.
|
|
36
36
|
const COPY_FROM_STRUCT_TYPE = struct([
|
|
37
37
|
field("format", utf8(), false),
|
|
@@ -62,8 +62,10 @@ const BIND_REQUEST_SCHEMA = schema([
|
|
|
62
62
|
field("copy_to", COPY_TO_STRUCT_TYPE, true),
|
|
63
63
|
// Catalog schema owning the function. A worker may register one name in
|
|
64
64
|
// several schemas, so the bare name is not a unique key — resolution is by
|
|
65
|
-
// (
|
|
66
|
-
field("
|
|
65
|
+
// (schema_path, function_name). Null for callers with no catalog context.
|
|
66
|
+
field("schema_path", list(field("item", utf8(), true)), true),
|
|
67
|
+
// Complete resolved logical signature, including const args and varargs.
|
|
68
|
+
field("argument_names", list(field("item", utf8(), true)), true),
|
|
67
69
|
]);
|
|
68
70
|
|
|
69
71
|
export function serializeBindRequest(req: BindRequest): VgiBatch {
|
|
@@ -89,7 +91,8 @@ export function serializeBindRequest(req: BindRequest): VgiBatch {
|
|
|
89
91
|
copy_to: req.copy_to
|
|
90
92
|
? { format: req.copy_to.format, file_path: req.copy_to.file_path }
|
|
91
93
|
: null,
|
|
92
|
-
|
|
94
|
+
schema_path: req.schema_path ?? null,
|
|
95
|
+
argument_names: req.argument_names ?? null,
|
|
93
96
|
});
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -164,7 +167,13 @@ export function deserializeBindRequest(
|
|
|
164
167
|
at_unit: params.at_unit ? String(params.at_unit) : null,
|
|
165
168
|
at_value: params.at_value ? String(params.at_value) : null,
|
|
166
169
|
// Empty string -> null, same convention as at_unit/at_value.
|
|
167
|
-
|
|
170
|
+
schema_path: params.schema_path
|
|
171
|
+
? (Array.isArray(params.schema_path) ? params.schema_path : [...params.schema_path]).map(String)
|
|
172
|
+
: null,
|
|
173
|
+
argument_names: params.argument_names
|
|
174
|
+
? (Array.isArray(params.argument_names) ? params.argument_names : [...params.argument_names])
|
|
175
|
+
.map((value: unknown) => value == null ? null : String(value))
|
|
176
|
+
: null,
|
|
168
177
|
// COPY ... FROM context — absent for ordinary scans (params.copy_from
|
|
169
178
|
// undefined -> null). The struct column decodes to a plain object.
|
|
170
179
|
copy_from: parseCopyFromContext(params.copy_from),
|
|
@@ -158,14 +158,16 @@ export function deserializePlanRequest(params: Record<string, any>): TableFuncti
|
|
|
158
158
|
* bytes are.
|
|
159
159
|
*/
|
|
160
160
|
export function fingerprintInputs(bindCallBytes: Uint8Array): {
|
|
161
|
-
|
|
161
|
+
schemaPath: string[];
|
|
162
162
|
functionName: string;
|
|
163
163
|
args: Uint8Array;
|
|
164
164
|
settings: Uint8Array;
|
|
165
165
|
} {
|
|
166
166
|
const dict = batchToScalarDict(deserializeBatch(bindCallBytes));
|
|
167
167
|
return {
|
|
168
|
-
|
|
168
|
+
schemaPath: dict.schema_path == null
|
|
169
|
+
? []
|
|
170
|
+
: (Array.isArray(dict.schema_path) ? dict.schema_path : [...dict.schema_path]).map(String),
|
|
169
171
|
functionName: String(dict.function_name ?? ""),
|
|
170
172
|
args: dict.arguments ? toUint8Array(dict.arguments) : new Uint8Array(0),
|
|
171
173
|
settings: dict.settings ? toUint8Array(dict.settings) : new Uint8Array(0),
|
package/src/protocol/types.ts
CHANGED
|
@@ -91,11 +91,17 @@ export interface BindRequest {
|
|
|
91
91
|
/**
|
|
92
92
|
* Catalog schema that declares the function being bound. A worker may
|
|
93
93
|
* register the same name in more than one schema, so the bare name does not
|
|
94
|
-
* identify an implementation — resolution is by `(
|
|
94
|
+
* identify an implementation — resolution is by `(schema_path,
|
|
95
95
|
* function_name)`. Null for callers with no catalog context (COPY handler
|
|
96
|
-
* binds). Mirrors vgi-python's `BindRequest.
|
|
96
|
+
* binds). Mirrors vgi-python's `BindRequest.schema_path`.
|
|
97
97
|
*/
|
|
98
|
-
|
|
98
|
+
schema_path?: string[] | null;
|
|
99
|
+
/**
|
|
100
|
+
* Resolved name for every logical argument, in complete call order. Fixed
|
|
101
|
+
* parameters carry their declared names, unnamed varargs carry `null`, and
|
|
102
|
+
* named varargs retain the caller-provided name. `null` means unavailable.
|
|
103
|
+
*/
|
|
104
|
+
argument_names?: (string | null)[] | null;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
export interface BindResponse {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
|
+
|
|
3
|
+
/** Raw identifier components of a catalog schema, from root to leaf. */
|
|
4
|
+
export type SchemaPath = string[];
|
|
5
|
+
|
|
6
|
+
/** Case-insensitive key used for catalog lookup and function dispatch. */
|
|
7
|
+
export function normalizeSchemaPath(path: Iterable<unknown> | string): string[] {
|
|
8
|
+
return typeof path === "string" ? [path] : Array.from(path, String);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function schemaPathKey(path: Iterable<unknown> | string): string {
|
|
12
|
+
const normalized = normalizeSchemaPath(path);
|
|
13
|
+
if (normalized.length === 0 || normalized.some((part) => part.length === 0)) {
|
|
14
|
+
throw new Error("schema paths must contain at least one non-empty component");
|
|
15
|
+
}
|
|
16
|
+
return normalized.map((part) => part.toLowerCase()).join("\u0000");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function schemaPathsEqual(left: Iterable<unknown> | string, right: Iterable<unknown> | string): boolean {
|
|
20
|
+
return schemaPathKey(left) === schemaPathKey(right);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function schemaPathDisplay(path: Iterable<unknown> | string): string {
|
|
24
|
+
return normalizeSchemaPath(path).map((part) => JSON.stringify(part)).join(".");
|
|
25
|
+
}
|
package/src/serve-entry.ts
CHANGED
|
@@ -23,7 +23,14 @@
|
|
|
23
23
|
|
|
24
24
|
import type { CatalogInterface } from "./catalog/interface.js";
|
|
25
25
|
import type { FunctionRegistry } from "./functions/registry.js";
|
|
26
|
-
import
|
|
26
|
+
import {
|
|
27
|
+
irohForwardedHeaderIdentityProvider,
|
|
28
|
+
IROH_FORWARDED_ENDPOINT_HEADER,
|
|
29
|
+
observePeerIdentity,
|
|
30
|
+
peerIdentityPrimary,
|
|
31
|
+
type AuthenticateFn,
|
|
32
|
+
type PeerResolutionOptions,
|
|
33
|
+
} from "@query-farm/vgi-rpc";
|
|
27
34
|
import { createVgiFetch } from "./http/fetch.js";
|
|
28
35
|
|
|
29
36
|
/** Environment variables `serveVgiWorker` reads. Injectable for testing. */
|
|
@@ -39,6 +46,21 @@ export const DEFAULT_TOKEN_TTL = 3600;
|
|
|
39
46
|
/** Bytes of HMAC key `createHttpHandler` expects; 64 hex characters. */
|
|
40
47
|
export const SIGNING_KEY_BYTES = 32;
|
|
41
48
|
|
|
49
|
+
/** HTTP trust boundary for a colocated `vgi-iroh-bridge`. */
|
|
50
|
+
export interface IrohHttpBridgeOptions {
|
|
51
|
+
/** Worker-local namespace for EndpointId principals. */
|
|
52
|
+
issuer: string;
|
|
53
|
+
/** Exact bridge IP addresses. No CIDRs or hostnames. */
|
|
54
|
+
trustedProxyAddresses?: readonly string[];
|
|
55
|
+
/** Authenticate the EndpointId (default) or expose evidence only. */
|
|
56
|
+
authenticate?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Host-specific physical peer and raw-header adapter. Required because a
|
|
59
|
+
* Fetch Request alone does not preserve duplicate header fields.
|
|
60
|
+
*/
|
|
61
|
+
peerResolutionContext?: (request: Request) => PeerResolutionOptions | Promise<PeerResolutionOptions>;
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
export interface ServeVgiWorkerOptions {
|
|
43
65
|
/** Short worker name shown on the landing page, e.g. "ishares". */
|
|
44
66
|
name: string;
|
|
@@ -55,6 +77,8 @@ export interface ServeVgiWorkerOptions {
|
|
|
55
77
|
repositoryUrl?: string;
|
|
56
78
|
/** Overrides `$PORT` (default 8787). Pass 0 to bind an ephemeral port. */
|
|
57
79
|
port?: number;
|
|
80
|
+
/** Bind address. Iroh bridge mode defaults to loopback; otherwise Bun's default. */
|
|
81
|
+
hostname?: string;
|
|
58
82
|
/** Overrides `$VGI_SIGNING_KEY`. 32 bytes. */
|
|
59
83
|
signingKey?: Uint8Array;
|
|
60
84
|
/** Overrides `$VGI_TOKEN_TTL` (default 3600 seconds). */
|
|
@@ -72,6 +96,8 @@ export interface ServeVgiWorkerOptions {
|
|
|
72
96
|
/** Authenticates each request, returning the caller's `AuthContext`.
|
|
73
97
|
* Omit for an anonymous worker (the default). */
|
|
74
98
|
authenticate?: AuthenticateFn;
|
|
99
|
+
/** Preserve authenticated Iroh identity forwarded by an adjacent HTTP bridge. */
|
|
100
|
+
irohBridge?: IrohHttpBridgeOptions;
|
|
75
101
|
}
|
|
76
102
|
|
|
77
103
|
/** The slice of `Bun.serve`'s return value this module uses. */
|
|
@@ -79,11 +105,13 @@ export interface VgiHttpServer {
|
|
|
79
105
|
readonly port: number;
|
|
80
106
|
readonly url: URL;
|
|
81
107
|
stop(closeActiveConnections?: boolean): void;
|
|
108
|
+
requestIP?(request: Request): { address: string } | null;
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
interface BunLike {
|
|
85
112
|
serve(options: {
|
|
86
113
|
port: number;
|
|
114
|
+
hostname?: string;
|
|
87
115
|
fetch: (req: Request) => Promise<Response>;
|
|
88
116
|
}): VgiHttpServer;
|
|
89
117
|
}
|
|
@@ -188,6 +216,13 @@ export function createVgiWorkerFetch(
|
|
|
188
216
|
// mean "off" would silently re-enable it. `undefined` falls through to the env
|
|
189
217
|
// var and then to that same default.
|
|
190
218
|
const corsOrigins = opts.corsOrigins === null ? null : (opts.corsOrigins ?? env.CORS_ORIGINS);
|
|
219
|
+
const bridge = opts.irohBridge;
|
|
220
|
+
if (bridge && !bridge.peerResolutionContext) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
"irohBridge requires a peerResolutionContext when createVgiWorkerFetch is used without serveVgiWorker",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const trustedProxyAddresses = bridge?.trustedProxyAddresses ?? ["127.0.0.1"];
|
|
191
226
|
|
|
192
227
|
return createVgiFetch({
|
|
193
228
|
protocol: { registry: opts.registry, catalogInterface: opts.catalogInterface },
|
|
@@ -199,6 +234,13 @@ export function createVgiWorkerFetch(
|
|
|
199
234
|
repositoryUrl: opts.repositoryUrl,
|
|
200
235
|
landingInfo: { name: opts.name, doc: opts.doc, version: opts.version },
|
|
201
236
|
authenticate: opts.authenticate,
|
|
237
|
+
peerIdentityProviders: bridge
|
|
238
|
+
? [irohForwardedHeaderIdentityProvider({ issuer: bridge.issuer, trustedProxyAddresses })]
|
|
239
|
+
: undefined,
|
|
240
|
+
peerAuthenticationPolicy: bridge
|
|
241
|
+
? (bridge.authenticate === false ? observePeerIdentity : peerIdentityPrimary("iroh"))
|
|
242
|
+
: undefined,
|
|
243
|
+
peerResolutionContext: bridge?.peerResolutionContext,
|
|
202
244
|
});
|
|
203
245
|
}
|
|
204
246
|
|
|
@@ -220,8 +262,28 @@ export function serveVgiWorker(opts: ServeVgiWorkerOptions): VgiHttpServer {
|
|
|
220
262
|
|
|
221
263
|
const env = opts.env ?? (process.env as ServeEnv);
|
|
222
264
|
const port = resolvePort(opts.port, env);
|
|
223
|
-
const
|
|
224
|
-
|
|
265
|
+
const hostname = opts.hostname ?? (opts.irohBridge ? "127.0.0.1" : undefined);
|
|
266
|
+
if (opts.irohBridge && !["127.0.0.1", "::1", "localhost"].includes(hostname ?? "")) {
|
|
267
|
+
throw new Error("an Iroh HTTP bridge upstream must bind loopback");
|
|
268
|
+
}
|
|
269
|
+
let server: VgiHttpServer;
|
|
270
|
+
const bridge = opts.irohBridge && !opts.irohBridge.peerResolutionContext
|
|
271
|
+
? {
|
|
272
|
+
...opts.irohBridge,
|
|
273
|
+
peerResolutionContext(request: Request): PeerResolutionOptions {
|
|
274
|
+
const immediatePeer = server.requestIP?.(request)?.address;
|
|
275
|
+
const forwarded = request.headers.get(IROH_FORWARDED_ENDPOINT_HEADER);
|
|
276
|
+
return {
|
|
277
|
+
immediatePeer,
|
|
278
|
+
headers: forwarded === null
|
|
279
|
+
? {}
|
|
280
|
+
: { [IROH_FORWARDED_ENDPOINT_HEADER]: [forwarded] },
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
: opts.irohBridge;
|
|
285
|
+
const fetch = createVgiWorkerFetch({ ...opts, irohBridge: bridge });
|
|
286
|
+
server = runtime.serve({ port, hostname, fetch });
|
|
225
287
|
|
|
226
288
|
if (!opts.quiet) {
|
|
227
289
|
const base = `http://localhost:${server.port}`;
|
package/src/split-token.ts
CHANGED
|
@@ -112,7 +112,7 @@ function equalBytes(a: Uint8Array, b: Uint8Array): boolean {
|
|
|
112
112
|
* browser and Cloudflare Workers builds.
|
|
113
113
|
*/
|
|
114
114
|
export async function bindFingerprint(
|
|
115
|
-
|
|
115
|
+
schemaPath: string[],
|
|
116
116
|
functionName: string,
|
|
117
117
|
args: Uint8Array,
|
|
118
118
|
settings: Uint8Array,
|
|
@@ -123,7 +123,7 @@ export async function bindFingerprint(
|
|
|
123
123
|
const field = (label: string, value: Uint8Array) => concat(enc.encode(label), zero, value, zero);
|
|
124
124
|
const message = concat(
|
|
125
125
|
AAD_PREFIX,
|
|
126
|
-
field("
|
|
126
|
+
field("schema_path", enc.encode(JSON.stringify(schemaPath))),
|
|
127
127
|
field("function_name", enc.encode(functionName)),
|
|
128
128
|
field("arguments", args),
|
|
129
129
|
field("settings", settings),
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,15 @@ export enum NullHandling {
|
|
|
22
22
|
SPECIAL = "SPECIAL",
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export enum ArgumentMonotonicity {
|
|
26
|
+
UNKNOWN = "UNKNOWN",
|
|
27
|
+
CONSTANT = "CONSTANT",
|
|
28
|
+
NON_DECREASING = "NON_DECREASING",
|
|
29
|
+
STRICTLY_INCREASING = "STRICTLY_INCREASING",
|
|
30
|
+
NON_INCREASING = "NON_INCREASING",
|
|
31
|
+
STRICTLY_DECREASING = "STRICTLY_DECREASING",
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
export enum OrderPreservation {
|
|
26
35
|
/** Output rows are in same order as input rows (DuckDB INSERTION_ORDER). */
|
|
27
36
|
PRESERVES_ORDER = "PRESERVES_ORDER",
|
package/src/worker.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
// Copyright 2025, 2026 Query Farm LLC - https://query.farm
|
|
2
2
|
// VGI Worker: main entry point for running a VGI function server.
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
VgiRpcServer,
|
|
6
|
+
observePeerIdentity,
|
|
7
|
+
peerIdentityPrimary,
|
|
8
|
+
serveUnix,
|
|
9
|
+
serveTcp,
|
|
10
|
+
serveStream as serveStreamRpc,
|
|
11
|
+
} from "@query-farm/vgi-rpc";
|
|
5
12
|
import { FunctionRegistry } from "./functions/registry.js";
|
|
6
13
|
import type { VgiFunction } from "./functions/types.js";
|
|
7
14
|
import { buildVgiProtocol } from "./protocol/dispatch.js";
|
|
8
15
|
import type { Protocol } from "@query-farm/vgi-rpc";
|
|
9
|
-
import type
|
|
16
|
+
import { schemaDescriptorPath, type CatalogDescriptor } from "./catalog/descriptors.js";
|
|
10
17
|
import type { CatalogInterface } from "./catalog/interface.js";
|
|
11
18
|
import { ReadOnlyCatalogInterface } from "./catalog/read-only.js";
|
|
12
19
|
|
|
@@ -41,6 +48,10 @@ interface LauncherArgs {
|
|
|
41
48
|
/** TCP bind port; present iff `--tcp` was passed. */
|
|
42
49
|
tcpPort?: number;
|
|
43
50
|
idleTimeout?: number;
|
|
51
|
+
irohRawUpstream?: boolean;
|
|
52
|
+
irohIssuer?: string;
|
|
53
|
+
irohTrustedProxyAddresses: string[];
|
|
54
|
+
irohAuthenticate: boolean;
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
// Parse a `[HOST:]PORT` --tcp bind spec into a LauncherArgs. A bare PORT (no
|
|
@@ -58,7 +69,7 @@ function parseTcpSpec(spec: string, out: LauncherArgs): void {
|
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
function parseLauncherArgs(argv: readonly string[]): LauncherArgs {
|
|
61
|
-
const out: LauncherArgs = {};
|
|
72
|
+
const out: LauncherArgs = { irohTrustedProxyAddresses: [], irohAuthenticate: true };
|
|
62
73
|
for (let i = 0; i < argv.length; i++) {
|
|
63
74
|
const a = argv[i];
|
|
64
75
|
if (a === "--unix" && i + 1 < argv.length) {
|
|
@@ -73,6 +84,22 @@ function parseLauncherArgs(argv: readonly string[]): LauncherArgs {
|
|
|
73
84
|
out.idleTimeout = Number(argv[++i]);
|
|
74
85
|
} else if (a.startsWith("--idle-timeout=")) {
|
|
75
86
|
out.idleTimeout = Number(a.slice("--idle-timeout=".length));
|
|
87
|
+
} else if (a === "--iroh-raw-upstream" && i + 1 < argv.length) {
|
|
88
|
+
parseTcpSpec(argv[++i], out);
|
|
89
|
+
out.irohRawUpstream = true;
|
|
90
|
+
} else if (a.startsWith("--iroh-raw-upstream=")) {
|
|
91
|
+
parseTcpSpec(a.slice("--iroh-raw-upstream=".length), out);
|
|
92
|
+
out.irohRawUpstream = true;
|
|
93
|
+
} else if (a === "--iroh-issuer" && i + 1 < argv.length) {
|
|
94
|
+
out.irohIssuer = argv[++i];
|
|
95
|
+
} else if (a.startsWith("--iroh-issuer=")) {
|
|
96
|
+
out.irohIssuer = a.slice("--iroh-issuer=".length);
|
|
97
|
+
} else if (a === "--iroh-trusted-proxy" && i + 1 < argv.length) {
|
|
98
|
+
out.irohTrustedProxyAddresses.push(...argv[++i].split(",").filter(Boolean));
|
|
99
|
+
} else if (a.startsWith("--iroh-trusted-proxy=")) {
|
|
100
|
+
out.irohTrustedProxyAddresses.push(...a.slice("--iroh-trusted-proxy=".length).split(",").filter(Boolean));
|
|
101
|
+
} else if (a === "--iroh-observe") {
|
|
102
|
+
out.irohAuthenticate = false;
|
|
76
103
|
}
|
|
77
104
|
}
|
|
78
105
|
return out;
|
|
@@ -122,7 +149,7 @@ export class Worker {
|
|
|
122
149
|
this._registry.register(func);
|
|
123
150
|
// Also index by owning schema + catalog so a schema-qualified bind
|
|
124
151
|
// resolves when one name is declared in more than one schema.
|
|
125
|
-
this._registry.registerInSchema(func, schema
|
|
152
|
+
this._registry.registerInSchema(func, schemaDescriptorPath(schema), config.catalog.name);
|
|
126
153
|
}
|
|
127
154
|
}
|
|
128
155
|
}
|
|
@@ -149,6 +176,12 @@ export class Worker {
|
|
|
149
176
|
debug(() => `[worker] protocol built`);
|
|
150
177
|
|
|
151
178
|
const launcher = parseLauncherArgs(argv);
|
|
179
|
+
if (launcher.irohRawUpstream) {
|
|
180
|
+
if (!launcher.irohIssuer) throw new TypeError("--iroh-raw-upstream requires --iroh-issuer");
|
|
181
|
+
if (!["127.0.0.1", "::1", "localhost"].includes(launcher.tcpHost ?? "")) {
|
|
182
|
+
throw new TypeError("--iroh-raw-upstream must bind loopback");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
152
185
|
if (launcher.unixPath !== undefined) {
|
|
153
186
|
// VGI_WORKER_IDLE_TIMEOUT overrides whatever the C++ launcher
|
|
154
187
|
// appended via --idle-timeout. Useful in test setups where the
|
|
@@ -191,6 +224,18 @@ export class Worker {
|
|
|
191
224
|
host: launcher.tcpHost,
|
|
192
225
|
port: launcher.tcpPort,
|
|
193
226
|
idleTimeout,
|
|
227
|
+
...(launcher.irohRawUpstream
|
|
228
|
+
? {
|
|
229
|
+
proxyProtocolV2Required: true,
|
|
230
|
+
trustedProxyAddresses: launcher.irohTrustedProxyAddresses.length > 0
|
|
231
|
+
? launcher.irohTrustedProxyAddresses
|
|
232
|
+
: ["127.0.0.1"],
|
|
233
|
+
irohProxyIssuer: launcher.irohIssuer,
|
|
234
|
+
peerAuthenticationPolicy: launcher.irohAuthenticate
|
|
235
|
+
? peerIdentityPrimary("iroh")
|
|
236
|
+
: observePeerIdentity,
|
|
237
|
+
}
|
|
238
|
+
: {}),
|
|
194
239
|
}).then((handle) => handle.done).then(() => {
|
|
195
240
|
debug(() => `[worker] serveTcp done (idle shutdown)`);
|
|
196
241
|
}).catch((err: any) => {
|