@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.
Files changed (90) hide show
  1. package/README.md +26 -1
  2. package/dist/arguments/argument-spec.d.ts +4 -2
  3. package/dist/catalog/composite.d.ts +13 -13
  4. package/dist/catalog/descriptors.d.ts +7 -3
  5. package/dist/catalog/interface.d.ts +67 -40
  6. package/dist/catalog/read-only.d.ts +32 -15
  7. package/dist/client/client.d.ts +36 -29
  8. package/dist/client/types.d.ts +2 -0
  9. package/dist/client-entry.d.ts +1 -1
  10. package/dist/client-entry.js +441 -279
  11. package/dist/client-entry.js.map +12 -11
  12. package/dist/filter-pushdown/capabilities.d.ts +3 -0
  13. package/dist/filter-pushdown/deserialize.d.ts +14 -15
  14. package/dist/filter-pushdown/evaluate.d.ts +14 -44
  15. package/dist/filter-pushdown/index.d.ts +2 -2
  16. package/dist/filter-pushdown/types.d.ts +120 -68
  17. package/dist/functions/aggregate.d.ts +4 -0
  18. package/dist/functions/registry.d.ts +5 -5
  19. package/dist/functions/scalar.d.ts +6 -1
  20. package/dist/functions/table-buffering.d.ts +9 -0
  21. package/dist/functions/table-in-out.d.ts +13 -0
  22. package/dist/functions/table.d.ts +9 -2
  23. package/dist/functions/types.d.ts +22 -2
  24. package/dist/generated/vgi-client.d.ts +56 -27
  25. package/dist/generated/vgi-protocol-schemas.d.ts +15 -0
  26. package/dist/http/fetch.d.ts +7 -1
  27. package/dist/index.core.d.ts +2 -1
  28. package/dist/index.js +2317 -1354
  29. package/dist/index.js.map +43 -40
  30. package/dist/metadata/types.d.ts +15 -2
  31. package/dist/protocol/handlers/shared.d.ts +3 -1
  32. package/dist/protocol/serializers/splits.d.ts +1 -1
  33. package/dist/protocol/types.d.ts +9 -3
  34. package/dist/schema-path.d.ts +7 -0
  35. package/dist/serve-entry.d.ts +22 -1
  36. package/dist/serve-entry.js +412 -238
  37. package/dist/serve-entry.js.map +27 -26
  38. package/dist/split-token.d.ts +1 -1
  39. package/dist/types.d.ts +8 -0
  40. package/dist/worker-cf-entry.js +2030 -1201
  41. package/dist/worker-cf-entry.js.map +40 -36
  42. package/dist/worker.d.ts +1 -1
  43. package/package.json +3 -3
  44. package/src/arguments/argument-spec.ts +12 -4
  45. package/src/catalog/composite.ts +26 -26
  46. package/src/catalog/descriptors.ts +12 -3
  47. package/src/catalog/interface.ts +98 -48
  48. package/src/catalog/read-only.ts +124 -67
  49. package/src/client/client.ts +92 -72
  50. package/src/client/types.ts +2 -0
  51. package/src/client-entry.ts +2 -1
  52. package/src/filter-pushdown/capabilities.ts +13 -0
  53. package/src/filter-pushdown/collector.ts +22 -35
  54. package/src/filter-pushdown/deserialize.ts +657 -221
  55. package/src/filter-pushdown/evaluate.ts +410 -499
  56. package/src/filter-pushdown/index.ts +16 -2
  57. package/src/filter-pushdown/types.ts +88 -71
  58. package/src/functions/aggregate.ts +9 -1
  59. package/src/functions/registry.ts +24 -22
  60. package/src/functions/scalar.ts +26 -0
  61. package/src/functions/table-buffering.ts +46 -7
  62. package/src/functions/table-in-out.ts +187 -30
  63. package/src/functions/table.ts +58 -20
  64. package/src/functions/types.ts +24 -1
  65. package/src/generated/vgi-client.ts +61 -29
  66. package/src/generated/vgi-protocol-schemas.ts +225 -54
  67. package/src/http/fetch.ts +12 -0
  68. package/src/index.core.ts +2 -0
  69. package/src/metadata/resolve.ts +43 -1
  70. package/src/metadata/serialize.ts +43 -5
  71. package/src/metadata/types.ts +17 -1
  72. package/src/protocol/dispatch.ts +1 -1
  73. package/src/protocol/handlers/aggregate.ts +13 -7
  74. package/src/protocol/handlers/catalog/admin.ts +6 -6
  75. package/src/protocol/handlers/catalog/index_methods.ts +2 -2
  76. package/src/protocol/handlers/catalog/macro.ts +4 -4
  77. package/src/protocol/handlers/catalog/shared.ts +2 -0
  78. package/src/protocol/handlers/catalog/table.ts +16 -16
  79. package/src/protocol/handlers/catalog/view.ts +5 -5
  80. package/src/protocol/handlers/function.ts +2 -2
  81. package/src/protocol/handlers/shared.ts +12 -3
  82. package/src/protocol/handlers/table-buffering.ts +4 -4
  83. package/src/protocol/serializers/bind.ts +15 -6
  84. package/src/protocol/serializers/splits.ts +4 -2
  85. package/src/protocol/types.ts +9 -3
  86. package/src/schema-path.ts +25 -0
  87. package/src/serve-entry.ts +65 -3
  88. package/src/split-token.ts +2 -2
  89. package/src/types.ts +9 -0
  90. 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.schemaName,
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.schemaName,
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
- return batchToScalarDict(innerBatch);
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
- schema_name?: string | null;
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
- schemaName: req.schema_name ?? null,
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
- // `schemaName` scopes resolution to the schema the caller named. The
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
- schemaName?: string | null,
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
- schemaName: schemaName ?? null,
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.schema_name ?? null);
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
- // `schema_name` is LAST (not before them), and `function_type` is a dictionary
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
- // (schema_name, function_name). Null for callers with no catalog context.
66
- field("schema_name", utf8(), true),
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
- schema_name: req.schema_name ?? null,
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
- schema_name: params.schema_name ? String(params.schema_name) : null,
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
- schemaName: string;
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
- schemaName: dict.schema_name == null ? "" : String(dict.schema_name),
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),
@@ -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 `(schema_name,
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.schema_name`.
96
+ * binds). Mirrors vgi-python's `BindRequest.schema_path`.
97
97
  */
98
- schema_name?: string | null;
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
+ }
@@ -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 type { AuthenticateFn } from "@query-farm/vgi-rpc";
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 fetch = createVgiWorkerFetch(opts);
224
- const server = runtime.serve({ port, fetch });
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}`;
@@ -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
- schemaName: string,
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("schema_name", enc.encode(schemaName)),
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 { VgiRpcServer, serveUnix, serveTcp, serveStream as serveStreamRpc } from "@query-farm/vgi-rpc";
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 { CatalogDescriptor } from "./catalog/descriptors.js";
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.name, config.catalog.name);
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) => {