@query-farm/vgi-rpc 0.4.0 → 0.6.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 (80) hide show
  1. package/README.md +47 -0
  2. package/dist/client/connect.d.ts.map +1 -1
  3. package/dist/client/index.d.ts +1 -1
  4. package/dist/client/index.d.ts.map +1 -1
  5. package/dist/client/oauth.d.ts +36 -0
  6. package/dist/client/oauth.d.ts.map +1 -1
  7. package/dist/client/pipe.d.ts +3 -0
  8. package/dist/client/pipe.d.ts.map +1 -1
  9. package/dist/client/stream.d.ts +3 -0
  10. package/dist/client/stream.d.ts.map +1 -1
  11. package/dist/client/types.d.ts +4 -0
  12. package/dist/client/types.d.ts.map +1 -1
  13. package/dist/constants.d.ts +3 -1
  14. package/dist/constants.d.ts.map +1 -1
  15. package/dist/dispatch/describe.d.ts.map +1 -1
  16. package/dist/dispatch/stream.d.ts +2 -1
  17. package/dist/dispatch/stream.d.ts.map +1 -1
  18. package/dist/dispatch/unary.d.ts +2 -1
  19. package/dist/dispatch/unary.d.ts.map +1 -1
  20. package/dist/external.d.ts +45 -0
  21. package/dist/external.d.ts.map +1 -0
  22. package/dist/gcs.d.ts +38 -0
  23. package/dist/gcs.d.ts.map +1 -0
  24. package/dist/http/auth.d.ts +13 -2
  25. package/dist/http/auth.d.ts.map +1 -1
  26. package/dist/http/bearer.d.ts +34 -0
  27. package/dist/http/bearer.d.ts.map +1 -0
  28. package/dist/http/dispatch.d.ts +2 -0
  29. package/dist/http/dispatch.d.ts.map +1 -1
  30. package/dist/http/handler.d.ts.map +1 -1
  31. package/dist/http/index.d.ts +4 -0
  32. package/dist/http/index.d.ts.map +1 -1
  33. package/dist/http/jwt.d.ts +2 -2
  34. package/dist/http/jwt.d.ts.map +1 -1
  35. package/dist/http/mtls.d.ts +78 -0
  36. package/dist/http/mtls.d.ts.map +1 -0
  37. package/dist/http/pages.d.ts +9 -0
  38. package/dist/http/pages.d.ts.map +1 -0
  39. package/dist/http/types.d.ts +17 -1
  40. package/dist/http/types.d.ts.map +1 -1
  41. package/dist/index.d.ts +3 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +1119 -230
  44. package/dist/index.js.map +24 -20
  45. package/dist/otel.d.ts +47 -0
  46. package/dist/otel.d.ts.map +1 -0
  47. package/dist/s3.d.ts +43 -0
  48. package/dist/s3.d.ts.map +1 -0
  49. package/dist/server.d.ts +6 -0
  50. package/dist/server.d.ts.map +1 -1
  51. package/dist/types.d.ts +30 -0
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +44 -1
  54. package/src/client/connect.ts +13 -5
  55. package/src/client/index.ts +10 -1
  56. package/src/client/introspect.ts +1 -1
  57. package/src/client/oauth.ts +94 -1
  58. package/src/client/pipe.ts +19 -4
  59. package/src/client/stream.ts +20 -7
  60. package/src/client/types.ts +4 -0
  61. package/src/constants.ts +4 -1
  62. package/src/dispatch/describe.ts +20 -0
  63. package/src/dispatch/stream.ts +7 -1
  64. package/src/dispatch/unary.ts +6 -1
  65. package/src/external.ts +209 -0
  66. package/src/gcs.ts +86 -0
  67. package/src/http/auth.ts +67 -4
  68. package/src/http/bearer.ts +107 -0
  69. package/src/http/dispatch.ts +26 -6
  70. package/src/http/handler.ts +81 -4
  71. package/src/http/index.ts +10 -0
  72. package/src/http/jwt.ts +17 -3
  73. package/src/http/mtls.ts +298 -0
  74. package/src/http/pages.ts +298 -0
  75. package/src/http/types.ts +17 -1
  76. package/src/index.ts +25 -0
  77. package/src/otel.ts +161 -0
  78. package/src/s3.ts +94 -0
  79. package/src/server.ts +42 -8
  80. package/src/types.ts +34 -0
package/dist/index.js CHANGED
@@ -103,21 +103,247 @@ var SERVER_ID_KEY = "vgi_rpc.server_id";
103
103
  var REQUEST_ID_KEY = "vgi_rpc.request_id";
104
104
  var PROTOCOL_NAME_KEY = "vgi_rpc.protocol_name";
105
105
  var DESCRIBE_VERSION_KEY = "vgi_rpc.describe_version";
106
- var DESCRIBE_VERSION = "2";
106
+ var DESCRIBE_VERSION = "3";
107
107
  var DESCRIBE_METHOD_NAME = "__describe__";
108
108
  var STATE_KEY = "vgi_rpc.stream_state#b64";
109
+ var LOCATION_KEY = "vgi_rpc.location";
110
+ var LOCATION_SHA256_KEY = "vgi_rpc.location.sha256";
109
111
 
110
- // src/http/common.ts
112
+ // src/external.ts
111
113
  import { RecordBatchReader, RecordBatchStreamWriter } from "@query-farm/apache-arrow";
114
+ init_zstd();
112
115
 
113
- // src/util/conform.ts
116
+ // src/wire/response.ts
114
117
  import {
118
+ Data,
119
+ DataType,
115
120
  makeData,
116
121
  RecordBatch,
117
122
  Struct,
118
- Type,
119
123
  vectorFromArray
120
124
  } from "@query-farm/apache-arrow";
125
+ function coerceInt64(schema, values) {
126
+ const result = { ...values };
127
+ for (const field of schema.fields) {
128
+ const val = result[field.name];
129
+ if (val === undefined)
130
+ continue;
131
+ if (!DataType.isInt(field.type) || field.type.bitWidth !== 64)
132
+ continue;
133
+ if (Array.isArray(val)) {
134
+ result[field.name] = val.map((v) => typeof v === "number" ? BigInt(v) : v);
135
+ } else if (typeof val === "number") {
136
+ result[field.name] = BigInt(val);
137
+ }
138
+ }
139
+ return result;
140
+ }
141
+ function buildResultBatch(schema, values, serverId, requestId) {
142
+ const metadata = new Map;
143
+ metadata.set(SERVER_ID_KEY, serverId);
144
+ if (requestId !== null) {
145
+ metadata.set(REQUEST_ID_KEY, requestId);
146
+ }
147
+ if (schema.fields.length === 0) {
148
+ return buildEmptyBatch(schema, metadata);
149
+ }
150
+ for (const field of schema.fields) {
151
+ if (values[field.name] === undefined && !field.nullable) {
152
+ const got = Object.keys(values);
153
+ throw new TypeError(`Handler result missing required field '${field.name}'. Got keys: [${got.join(", ")}]`);
154
+ }
155
+ }
156
+ const coerced = coerceInt64(schema, values);
157
+ const children = schema.fields.map((f) => {
158
+ const val = coerced[f.name];
159
+ if (val instanceof Data) {
160
+ return val;
161
+ }
162
+ const arr = vectorFromArray([val], f.type);
163
+ return arr.data[0];
164
+ });
165
+ const structType = new Struct(schema.fields);
166
+ const data = makeData({
167
+ type: structType,
168
+ length: 1,
169
+ children,
170
+ nullCount: 0
171
+ });
172
+ return new RecordBatch(schema, data, metadata);
173
+ }
174
+ function buildErrorBatch(schema, error, serverId, requestId) {
175
+ const metadata = new Map;
176
+ metadata.set(LOG_LEVEL_KEY, "EXCEPTION");
177
+ metadata.set(LOG_MESSAGE_KEY, `${error.constructor.name}: ${error.message}`);
178
+ const extra = {
179
+ exception_type: error.constructor.name,
180
+ exception_message: error.message,
181
+ traceback: error.stack ?? ""
182
+ };
183
+ metadata.set(LOG_EXTRA_KEY, JSON.stringify(extra));
184
+ metadata.set(SERVER_ID_KEY, serverId);
185
+ if (requestId !== null) {
186
+ metadata.set(REQUEST_ID_KEY, requestId);
187
+ }
188
+ return buildEmptyBatch(schema, metadata);
189
+ }
190
+ function buildLogBatch(schema, level, message, extra, serverId, requestId) {
191
+ const metadata = new Map;
192
+ metadata.set(LOG_LEVEL_KEY, level);
193
+ metadata.set(LOG_MESSAGE_KEY, message);
194
+ if (extra) {
195
+ metadata.set(LOG_EXTRA_KEY, JSON.stringify(extra));
196
+ }
197
+ if (serverId != null) {
198
+ metadata.set(SERVER_ID_KEY, serverId);
199
+ }
200
+ if (requestId != null) {
201
+ metadata.set(REQUEST_ID_KEY, requestId);
202
+ }
203
+ return buildEmptyBatch(schema, metadata);
204
+ }
205
+ function makeEmptyData(type) {
206
+ if (DataType.isStruct(type)) {
207
+ const children = type.children.map((f) => makeEmptyData(f.type));
208
+ return makeData({ type, length: 0, children, nullCount: 0 });
209
+ }
210
+ if (DataType.isList(type)) {
211
+ const childData = makeEmptyData(type.children[0].type);
212
+ return makeData({ type, length: 0, children: [childData], nullCount: 0, valueOffsets: new Int32Array([0]) });
213
+ }
214
+ if (DataType.isFixedSizeList(type)) {
215
+ const childData = makeEmptyData(type.children[0].type);
216
+ return makeData({ type, length: 0, child: childData, nullCount: 0 });
217
+ }
218
+ if (DataType.isMap(type)) {
219
+ const entryType = type.children[0]?.type;
220
+ const entryData = entryType ? makeEmptyData(entryType) : makeData({ type: new Struct([]), length: 0, children: [], nullCount: 0 });
221
+ return makeData({ type, length: 0, children: [entryData], nullCount: 0, valueOffsets: new Int32Array([0]) });
222
+ }
223
+ return makeData({ type, length: 0, nullCount: 0 });
224
+ }
225
+ function buildEmptyBatch(schema, metadata) {
226
+ const children = schema.fields.map((f) => makeEmptyData(f.type));
227
+ const structType = new Struct(schema.fields);
228
+ const data = makeData({
229
+ type: structType,
230
+ length: 0,
231
+ children,
232
+ nullCount: 0
233
+ });
234
+ return new RecordBatch(schema, data, metadata);
235
+ }
236
+
237
+ // src/external.ts
238
+ var DEFAULT_THRESHOLD = 1048576;
239
+ function httpsOnlyValidator(url) {
240
+ const parsed = new URL(url);
241
+ if (parsed.protocol !== "https:") {
242
+ throw new Error(`External location URL must use HTTPS, got "${parsed.protocol}"`);
243
+ }
244
+ }
245
+ async function sha256Hex(data) {
246
+ const buf = new ArrayBuffer(data.byteLength);
247
+ new Uint8Array(buf).set(data);
248
+ const hash = await crypto.subtle.digest("SHA-256", buf);
249
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
250
+ }
251
+ function isExternalLocationBatch(batch) {
252
+ if (batch.numRows !== 0)
253
+ return false;
254
+ const meta = batch.metadata;
255
+ if (!meta)
256
+ return false;
257
+ return meta.has(LOCATION_KEY) && !meta.has(LOG_LEVEL_KEY);
258
+ }
259
+ function makeExternalLocationBatch(schema, url, sha256) {
260
+ const metadata = new Map;
261
+ metadata.set(LOCATION_KEY, url);
262
+ if (sha256) {
263
+ metadata.set(LOCATION_SHA256_KEY, sha256);
264
+ }
265
+ return buildEmptyBatch(schema, metadata);
266
+ }
267
+ function serializeBatchToIpc(batch) {
268
+ const writer = new RecordBatchStreamWriter;
269
+ writer.reset(undefined, batch.schema);
270
+ writer.write(batch);
271
+ writer.close();
272
+ return writer.toUint8Array(true);
273
+ }
274
+ function batchByteSize(batch) {
275
+ const writer = new RecordBatchStreamWriter;
276
+ writer.reset(undefined, batch.schema);
277
+ writer.write(batch);
278
+ writer.close();
279
+ return writer.toUint8Array(true).byteLength;
280
+ }
281
+ async function maybeExternalizeBatch(batch, config) {
282
+ if (!config?.storage)
283
+ return batch;
284
+ if (batch.numRows === 0)
285
+ return batch;
286
+ const threshold = config.externalizeThresholdBytes ?? DEFAULT_THRESHOLD;
287
+ if (batchByteSize(batch) < threshold)
288
+ return batch;
289
+ let ipcData = serializeBatchToIpc(batch);
290
+ const checksum = await sha256Hex(ipcData);
291
+ let contentEncoding = "";
292
+ if (config.compression?.algorithm === "zstd") {
293
+ ipcData = zstdCompress(ipcData, config.compression.level ?? 3);
294
+ contentEncoding = "zstd";
295
+ }
296
+ const url = await config.storage.upload(ipcData, contentEncoding);
297
+ return makeExternalLocationBatch(batch.schema, url, checksum);
298
+ }
299
+ async function resolveExternalLocation(batch, config) {
300
+ if (!config)
301
+ return batch;
302
+ if (!isExternalLocationBatch(batch))
303
+ return batch;
304
+ const url = batch.metadata?.get(LOCATION_KEY);
305
+ if (!url)
306
+ return batch;
307
+ const validator = config.urlValidator === null ? undefined : config.urlValidator ?? httpsOnlyValidator;
308
+ if (validator) {
309
+ validator(url);
310
+ }
311
+ const response = await fetch(url);
312
+ if (!response.ok) {
313
+ throw new Error(`External location fetch failed: ${response.status} ${response.statusText} [url: ${url}]`);
314
+ }
315
+ let data = new Uint8Array(await response.arrayBuffer());
316
+ const contentEncoding = response.headers.get("Content-Encoding");
317
+ if (contentEncoding === "zstd") {
318
+ data = new Uint8Array(zstdDecompress(data));
319
+ }
320
+ const expectedSha256 = batch.metadata?.get(LOCATION_SHA256_KEY);
321
+ if (expectedSha256) {
322
+ const actualSha256 = await sha256Hex(data);
323
+ if (actualSha256 !== expectedSha256) {
324
+ throw new Error(`SHA-256 checksum mismatch for ${url}: expected ${expectedSha256}, got ${actualSha256}`);
325
+ }
326
+ }
327
+ const reader = await RecordBatchReader.from(data);
328
+ await reader.open();
329
+ const resolved = reader.next();
330
+ if (!resolved || resolved.done || !resolved.value) {
331
+ throw new Error(`No data batch found in external IPC stream from ${url}`);
332
+ }
333
+ return resolved.value;
334
+ }
335
+
336
+ // src/http/common.ts
337
+ import { RecordBatchReader as RecordBatchReader2, RecordBatchStreamWriter as RecordBatchStreamWriter2 } from "@query-farm/apache-arrow";
338
+
339
+ // src/util/conform.ts
340
+ import {
341
+ makeData as makeData2,
342
+ RecordBatch as RecordBatch2,
343
+ Struct as Struct2,
344
+ Type,
345
+ vectorFromArray as vectorFromArray2
346
+ } from "@query-farm/apache-arrow";
121
347
  function needsValueCast(src, dst) {
122
348
  if (src.typeId === dst.typeId)
123
349
  return false;
@@ -153,19 +379,19 @@ function conformBatchToSchema(batch, schema) {
153
379
  const v = col.get(r);
154
380
  values.push(typeof v === "bigint" ? Number(v) : v);
155
381
  }
156
- return vectorFromArray(values, dstType).data[0];
382
+ return vectorFromArray2(values, dstType).data[0];
157
383
  }
158
384
  return srcChild.clone(dstType);
159
385
  });
160
- const structType = new Struct(schema.fields);
161
- const data = makeData({
386
+ const structType = new Struct2(schema.fields);
387
+ const data = makeData2({
162
388
  type: structType,
163
389
  length: batch.numRows,
164
390
  children,
165
391
  nullCount: batch.data.nullCount,
166
392
  nullBitmap: batch.data.nullBitmap
167
393
  });
168
- return new RecordBatch(schema, data, batch.metadata);
394
+ return new RecordBatch2(schema, data, batch.metadata);
169
395
  }
170
396
 
171
397
  // src/http/common.ts
@@ -180,7 +406,7 @@ class HttpRpcError extends Error {
180
406
  }
181
407
  }
182
408
  function serializeIpcStream(schema, batches) {
183
- const writer = new RecordBatchStreamWriter;
409
+ const writer = new RecordBatchStreamWriter2;
184
410
  writer.reset(undefined, schema);
185
411
  for (const batch of batches) {
186
412
  writer.write(conformBatchToSchema(batch, schema));
@@ -194,7 +420,7 @@ function arrowResponse(body, status = 200, extraHeaders) {
194
420
  return new Response(body, { status, headers });
195
421
  }
196
422
  async function readRequestFromBody(body) {
197
- const reader = await RecordBatchReader.from(body);
423
+ const reader = await RecordBatchReader2.from(body);
198
424
  await reader.open();
199
425
  const schema = reader.schema;
200
426
  if (!schema) {
@@ -208,25 +434,25 @@ async function readRequestFromBody(body) {
208
434
  }
209
435
 
210
436
  // src/client/introspect.ts
211
- import { Schema as ArrowSchema, RecordBatchReader as RecordBatchReader4 } from "@query-farm/apache-arrow";
437
+ import { Schema as ArrowSchema, RecordBatchReader as RecordBatchReader5 } from "@query-farm/apache-arrow";
212
438
 
213
439
  // src/client/ipc.ts
214
440
  import {
215
441
  Binary,
216
442
  Bool,
217
- DataType,
443
+ DataType as DataType2,
218
444
  Float64,
219
445
  Int64,
220
- makeData as makeData2,
221
- RecordBatch as RecordBatch2,
222
- RecordBatchReader as RecordBatchReader3,
223
- Struct as Struct2,
446
+ makeData as makeData3,
447
+ RecordBatch as RecordBatch3,
448
+ RecordBatchReader as RecordBatchReader4,
449
+ Struct as Struct3,
224
450
  Utf8,
225
- vectorFromArray as vectorFromArray2
451
+ vectorFromArray as vectorFromArray3
226
452
  } from "@query-farm/apache-arrow";
227
453
 
228
454
  // src/wire/reader.ts
229
- import { RecordBatchReader as RecordBatchReader2 } from "@query-farm/apache-arrow";
455
+ import { RecordBatchReader as RecordBatchReader3 } from "@query-farm/apache-arrow";
230
456
 
231
457
  class IpcStreamReader {
232
458
  reader;
@@ -236,7 +462,7 @@ class IpcStreamReader {
236
462
  this.reader = reader;
237
463
  }
238
464
  static async create(input) {
239
- const reader = await RecordBatchReader2.from(input);
465
+ const reader = await RecordBatchReader3.from(input);
240
466
  await reader.open({ autoDestroy: false });
241
467
  if (reader.closed) {
242
468
  throw new Error("Input stream closed before first IPC message");
@@ -313,12 +539,12 @@ function inferArrowType(value) {
313
539
  function coerceForArrow(type, value) {
314
540
  if (value == null)
315
541
  return value;
316
- if (DataType.isInt(type) && type.bitWidth === 64) {
542
+ if (DataType2.isInt(type) && type.bitWidth === 64) {
317
543
  if (typeof value === "number")
318
544
  return BigInt(value);
319
545
  return value;
320
546
  }
321
- if (DataType.isMap(type)) {
547
+ if (DataType2.isMap(type)) {
322
548
  if (value instanceof Map) {
323
549
  const entriesField = type.children[0];
324
550
  const valueType = entriesField.type.children[1].type;
@@ -330,7 +556,7 @@ function coerceForArrow(type, value) {
330
556
  }
331
557
  return value;
332
558
  }
333
- if (DataType.isList(type)) {
559
+ if (DataType2.isList(type)) {
334
560
  if (Array.isArray(value)) {
335
561
  const elemType = type.children[0].type;
336
562
  return value.map((v) => coerceForArrow(elemType, v));
@@ -344,32 +570,32 @@ function buildRequestIpc(schema, params, method) {
344
570
  metadata.set(RPC_METHOD_KEY, method);
345
571
  metadata.set(REQUEST_VERSION_KEY, REQUEST_VERSION);
346
572
  if (schema.fields.length === 0) {
347
- const structType2 = new Struct2(schema.fields);
348
- const data2 = makeData2({
573
+ const structType2 = new Struct3(schema.fields);
574
+ const data2 = makeData3({
349
575
  type: structType2,
350
576
  length: 1,
351
577
  children: [],
352
578
  nullCount: 0
353
579
  });
354
- const batch2 = new RecordBatch2(schema, data2, metadata);
580
+ const batch2 = new RecordBatch3(schema, data2, metadata);
355
581
  return serializeIpcStream(schema, [batch2]);
356
582
  }
357
583
  const children = schema.fields.map((f) => {
358
584
  const val = coerceForArrow(f.type, params[f.name]);
359
- return vectorFromArray2([val], f.type).data[0];
585
+ return vectorFromArray3([val], f.type).data[0];
360
586
  });
361
- const structType = new Struct2(schema.fields);
362
- const data = makeData2({
587
+ const structType = new Struct3(schema.fields);
588
+ const data = makeData3({
363
589
  type: structType,
364
590
  length: 1,
365
591
  children,
366
592
  nullCount: 0
367
593
  });
368
- const batch = new RecordBatch2(schema, data, metadata);
594
+ const batch = new RecordBatch3(schema, data, metadata);
369
595
  return serializeIpcStream(schema, [batch]);
370
596
  }
371
597
  async function readResponseBatches(body) {
372
- const reader = await RecordBatchReader3.from(body);
598
+ const reader = await RecordBatchReader4.from(body);
373
599
  await reader.open();
374
600
  const schema = reader.schema;
375
601
  if (!schema) {
@@ -443,7 +669,7 @@ async function readSequentialStreams(body) {
443
669
 
444
670
  // src/client/introspect.ts
445
671
  async function deserializeSchema(bytes) {
446
- const reader = await RecordBatchReader4.from(bytes);
672
+ const reader = await RecordBatchReader5.from(bytes);
447
673
  await reader.open();
448
674
  return reader.schema;
449
675
  }
@@ -507,7 +733,7 @@ async function parseDescribeResponse(batches, onLog) {
507
733
  return { protocolName, methods };
508
734
  }
509
735
  async function httpIntrospect(baseUrl, options) {
510
- const prefix = options?.prefix ?? "/vgi";
736
+ const prefix = options?.prefix ?? "";
511
737
  const emptySchema = new ArrowSchema([]);
512
738
  const body = buildRequestIpc(emptySchema, {}, DESCRIBE_METHOD_NAME);
513
739
  const headers = { "Content-Type": ARROW_CONTENT_TYPE };
@@ -528,7 +754,7 @@ async function httpIntrospect(baseUrl, options) {
528
754
  }
529
755
 
530
756
  // src/client/stream.ts
531
- import { Field, makeData as makeData3, RecordBatch as RecordBatch3, Schema, Struct as Struct3, vectorFromArray as vectorFromArray3 } from "@query-farm/apache-arrow";
757
+ import { Field, makeData as makeData4, RecordBatch as RecordBatch4, Schema, Struct as Struct4, vectorFromArray as vectorFromArray4 } from "@query-farm/apache-arrow";
532
758
  class HttpStreamSession {
533
759
  _baseUrl;
534
760
  _prefix;
@@ -544,6 +770,7 @@ class HttpStreamSession {
544
770
  _compressFn;
545
771
  _decompressFn;
546
772
  _authorization;
773
+ _externalConfig;
547
774
  constructor(opts) {
548
775
  this._baseUrl = opts.baseUrl;
549
776
  this._prefix = opts.prefix;
@@ -559,6 +786,7 @@ class HttpStreamSession {
559
786
  this._compressFn = opts.compressFn;
560
787
  this._decompressFn = opts.decompressFn;
561
788
  this._authorization = opts.authorization;
789
+ this._externalConfig = opts.externalConfig;
562
790
  }
563
791
  get header() {
564
792
  return this._header;
@@ -598,7 +826,7 @@ class HttpStreamSession {
598
826
  const emptyBatch = this._buildEmptyBatch(zeroSchema);
599
827
  const metadata2 = new Map;
600
828
  metadata2.set(STATE_KEY, this._stateToken);
601
- const batchWithMeta = new RecordBatch3(zeroSchema, emptyBatch.data, metadata2);
829
+ const batchWithMeta = new RecordBatch4(zeroSchema, emptyBatch.data, metadata2);
602
830
  return this._doExchange(zeroSchema, [batchWithMeta]);
603
831
  }
604
832
  const keys = Object.keys(input[0]);
@@ -617,10 +845,10 @@ class HttpStreamSession {
617
845
  const inputSchema = new Schema(fields);
618
846
  const children = inputSchema.fields.map((f) => {
619
847
  const values = input.map((row) => row[f.name]);
620
- return vectorFromArray3(values, f.type).data[0];
848
+ return vectorFromArray4(values, f.type).data[0];
621
849
  });
622
- const structType = new Struct3(inputSchema.fields);
623
- const data = makeData3({
850
+ const structType = new Struct4(inputSchema.fields);
851
+ const data = makeData4({
624
852
  type: structType,
625
853
  length: input.length,
626
854
  children,
@@ -628,7 +856,7 @@ class HttpStreamSession {
628
856
  });
629
857
  const metadata = new Map;
630
858
  metadata.set(STATE_KEY, this._stateToken);
631
- const batch = new RecordBatch3(inputSchema, data, metadata);
859
+ const batch = new RecordBatch4(inputSchema, data, metadata);
632
860
  return this._doExchange(inputSchema, [batch]);
633
861
  }
634
862
  async _doExchange(schema, batches) {
@@ -663,22 +891,26 @@ class HttpStreamSession {
663
891
  }
664
892
  _buildEmptyBatch(schema) {
665
893
  const children = schema.fields.map((f) => {
666
- return makeData3({ type: f.type, length: 0, nullCount: 0 });
894
+ return makeData4({ type: f.type, length: 0, nullCount: 0 });
667
895
  });
668
- const structType = new Struct3(schema.fields);
669
- const data = makeData3({
896
+ const structType = new Struct4(schema.fields);
897
+ const data = makeData4({
670
898
  type: structType,
671
899
  length: 0,
672
900
  children,
673
901
  nullCount: 0
674
902
  });
675
- return new RecordBatch3(schema, data);
903
+ return new RecordBatch4(schema, data);
676
904
  }
677
905
  async* [Symbol.asyncIterator]() {
678
- for (const batch of this._pendingBatches) {
906
+ for (let batch of this._pendingBatches) {
679
907
  if (batch.numRows === 0) {
680
- dispatchLogOrError(batch, this._onLog);
681
- continue;
908
+ if (isExternalLocationBatch(batch)) {
909
+ batch = await resolveExternalLocation(batch, this._externalConfig);
910
+ } else {
911
+ dispatchLogOrError(batch, this._onLog);
912
+ continue;
913
+ }
682
914
  }
683
915
  yield extractBatchRows(batch);
684
916
  }
@@ -691,7 +923,7 @@ class HttpStreamSession {
691
923
  const responseBody = await this._sendContinuation(this._stateToken);
692
924
  const { batches } = await readResponseBatches(responseBody);
693
925
  let gotContinuation = false;
694
- for (const batch of batches) {
926
+ for (let batch of batches) {
695
927
  if (batch.numRows === 0) {
696
928
  const token = batch.metadata?.get(STATE_KEY);
697
929
  if (token) {
@@ -699,8 +931,12 @@ class HttpStreamSession {
699
931
  gotContinuation = true;
700
932
  continue;
701
933
  }
702
- dispatchLogOrError(batch, this._onLog);
703
- continue;
934
+ if (isExternalLocationBatch(batch)) {
935
+ batch = await resolveExternalLocation(batch, this._externalConfig);
936
+ } else {
937
+ dispatchLogOrError(batch, this._onLog);
938
+ continue;
939
+ }
704
940
  }
705
941
  yield extractBatchRows(batch);
706
942
  }
@@ -712,14 +948,14 @@ class HttpStreamSession {
712
948
  const emptySchema = new Schema([]);
713
949
  const metadata = new Map;
714
950
  metadata.set(STATE_KEY, token);
715
- const structType = new Struct3(emptySchema.fields);
716
- const data = makeData3({
951
+ const structType = new Struct4(emptySchema.fields);
952
+ const data = makeData4({
717
953
  type: structType,
718
954
  length: 1,
719
955
  children: [],
720
956
  nullCount: 0
721
957
  });
722
- const batch = new RecordBatch3(emptySchema, data, metadata);
958
+ const batch = new RecordBatch4(emptySchema, data, metadata);
723
959
  const body = serializeIpcStream(emptySchema, [batch]);
724
960
  const resp = await fetch(`${this._baseUrl}${this._prefix}/${this._method}/exchange`, {
725
961
  method: "POST",
@@ -736,10 +972,11 @@ class HttpStreamSession {
736
972
 
737
973
  // src/client/connect.ts
738
974
  function httpConnect(baseUrl, options) {
739
- const prefix = (options?.prefix ?? "/vgi").replace(/\/+$/, "");
975
+ const prefix = (options?.prefix ?? "").replace(/\/+$/, "");
740
976
  const onLog = options?.onLog;
741
977
  const compressionLevel = options?.compressionLevel;
742
978
  const authorization = options?.authorization;
979
+ const externalConfig = options?.externalLocation;
743
980
  let methodCache = null;
744
981
  let compressFn;
745
982
  let decompressFn;
@@ -811,10 +1048,14 @@ function httpConnect(baseUrl, options) {
811
1048
  const responseBody = await readResponse(resp);
812
1049
  const { batches } = await readResponseBatches(responseBody);
813
1050
  let resultBatch = null;
814
- for (const batch of batches) {
1051
+ for (let batch of batches) {
815
1052
  if (batch.numRows === 0) {
816
- dispatchLogOrError(batch, onLog);
817
- continue;
1053
+ if (isExternalLocationBatch(batch)) {
1054
+ batch = await resolveExternalLocation(batch, externalConfig);
1055
+ } else {
1056
+ dispatchLogOrError(batch, onLog);
1057
+ continue;
1058
+ }
818
1059
  }
819
1060
  resultBatch = batch;
820
1061
  }
@@ -950,7 +1191,8 @@ function httpConnect(baseUrl, options) {
950
1191
  compressionLevel,
951
1192
  compressFn,
952
1193
  decompressFn,
953
- authorization
1194
+ authorization,
1195
+ externalConfig
954
1196
  });
955
1197
  },
956
1198
  async describe() {
@@ -969,6 +1211,8 @@ function parseMetadataJson(json) {
969
1211
  result.scopesSupported = json.scopes_supported;
970
1212
  if (json.bearer_methods_supported)
971
1213
  result.bearerMethodsSupported = json.bearer_methods_supported;
1214
+ if (json.resource_signing_alg_values_supported)
1215
+ result.resourceSigningAlgValuesSupported = json.resource_signing_alg_values_supported;
972
1216
  if (json.resource_name)
973
1217
  result.resourceName = json.resource_name;
974
1218
  if (json.resource_documentation)
@@ -977,10 +1221,20 @@ function parseMetadataJson(json) {
977
1221
  result.resourcePolicyUri = json.resource_policy_uri;
978
1222
  if (json.resource_tos_uri)
979
1223
  result.resourceTosUri = json.resource_tos_uri;
1224
+ if (json.client_id)
1225
+ result.clientId = json.client_id;
1226
+ if (json.client_secret)
1227
+ result.clientSecret = json.client_secret;
1228
+ if (json.use_id_token_as_bearer)
1229
+ result.useIdTokenAsBearer = json.use_id_token_as_bearer;
1230
+ if (json.device_code_client_id)
1231
+ result.deviceCodeClientId = json.device_code_client_id;
1232
+ if (json.device_code_client_secret)
1233
+ result.deviceCodeClientSecret = json.device_code_client_secret;
980
1234
  return result;
981
1235
  }
982
1236
  async function httpOAuthMetadata(baseUrl, prefix) {
983
- const effectivePrefix = (prefix ?? "/vgi").replace(/\/+$/, "");
1237
+ const effectivePrefix = (prefix ?? "").replace(/\/+$/, "");
984
1238
  const metadataUrl = `${baseUrl.replace(/\/+$/, "")}/.well-known/oauth-protected-resource${effectivePrefix}`;
985
1239
  try {
986
1240
  return await fetchOAuthMetadata(metadataUrl);
@@ -1006,15 +1260,65 @@ function parseResourceMetadataUrl(wwwAuthenticate) {
1006
1260
  return null;
1007
1261
  return metadataMatch[1];
1008
1262
  }
1263
+ function parseClientId(wwwAuthenticate) {
1264
+ const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
1265
+ if (!bearerMatch)
1266
+ return null;
1267
+ const params = bearerMatch[1];
1268
+ const clientIdMatch = params.match(/client_id="([^"]+)"/);
1269
+ if (!clientIdMatch)
1270
+ return null;
1271
+ return clientIdMatch[1];
1272
+ }
1273
+ function parseClientSecret(wwwAuthenticate) {
1274
+ const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
1275
+ if (!bearerMatch)
1276
+ return null;
1277
+ const params = bearerMatch[1];
1278
+ const match = params.match(/client_secret="([^"]+)"/);
1279
+ if (!match)
1280
+ return null;
1281
+ return match[1];
1282
+ }
1283
+ function parseUseIdTokenAsBearer(wwwAuthenticate) {
1284
+ const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
1285
+ if (!bearerMatch)
1286
+ return false;
1287
+ const params = bearerMatch[1];
1288
+ const match = params.match(/use_id_token_as_bearer="([^"]+)"/);
1289
+ if (!match)
1290
+ return false;
1291
+ return match[1] === "true";
1292
+ }
1293
+ function parseDeviceCodeClientId(wwwAuthenticate) {
1294
+ const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
1295
+ if (!bearerMatch)
1296
+ return null;
1297
+ const params = bearerMatch[1];
1298
+ const match = params.match(/device_code_client_id="([^"]+)"/);
1299
+ if (!match)
1300
+ return null;
1301
+ return match[1];
1302
+ }
1303
+ function parseDeviceCodeClientSecret(wwwAuthenticate) {
1304
+ const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
1305
+ if (!bearerMatch)
1306
+ return null;
1307
+ const params = bearerMatch[1];
1308
+ const match = params.match(/device_code_client_secret="([^"]+)"/);
1309
+ if (!match)
1310
+ return null;
1311
+ return match[1];
1312
+ }
1009
1313
  // src/client/pipe.ts
1010
1314
  import {
1011
1315
  Field as Field2,
1012
- makeData as makeData4,
1013
- RecordBatch as RecordBatch4,
1014
- RecordBatchStreamWriter as RecordBatchStreamWriter2,
1316
+ makeData as makeData5,
1317
+ RecordBatch as RecordBatch5,
1318
+ RecordBatchStreamWriter as RecordBatchStreamWriter3,
1015
1319
  Schema as Schema2,
1016
- Struct as Struct4,
1017
- vectorFromArray as vectorFromArray4
1320
+ Struct as Struct5,
1321
+ vectorFromArray as vectorFromArray5
1018
1322
  } from "@query-farm/apache-arrow";
1019
1323
  class PipeIncrementalWriter {
1020
1324
  writer;
@@ -1022,7 +1326,7 @@ class PipeIncrementalWriter {
1022
1326
  closed = false;
1023
1327
  constructor(writeFn, schema) {
1024
1328
  this.writeFn = writeFn;
1025
- this.writer = new RecordBatchStreamWriter2;
1329
+ this.writer = new RecordBatchStreamWriter3;
1026
1330
  this.writer.reset(undefined, schema);
1027
1331
  this.drain();
1028
1332
  }
@@ -1060,6 +1364,7 @@ class PipeStreamSession {
1060
1364
  _outputSchema;
1061
1365
  _releaseBusy;
1062
1366
  _setDrainPromise;
1367
+ _externalConfig;
1063
1368
  constructor(opts) {
1064
1369
  this._reader = opts.reader;
1065
1370
  this._writeFn = opts.writeFn;
@@ -1068,6 +1373,7 @@ class PipeStreamSession {
1068
1373
  this._outputSchema = opts.outputSchema;
1069
1374
  this._releaseBusy = opts.releaseBusy;
1070
1375
  this._setDrainPromise = opts.setDrainPromise;
1376
+ this._externalConfig = opts.externalConfig;
1071
1377
  }
1072
1378
  get header() {
1073
1379
  return this._header;
@@ -1078,6 +1384,9 @@ class PipeStreamSession {
1078
1384
  if (batch === null)
1079
1385
  return null;
1080
1386
  if (batch.numRows === 0) {
1387
+ if (isExternalLocationBatch(batch)) {
1388
+ return await resolveExternalLocation(batch, this._externalConfig);
1389
+ }
1081
1390
  if (dispatchLogOrError(batch, this._onLog)) {
1082
1391
  continue;
1083
1392
  }
@@ -1103,16 +1412,16 @@ class PipeStreamSession {
1103
1412
  if (input.length === 0) {
1104
1413
  inputSchema = this._inputSchema ?? this._outputSchema;
1105
1414
  const children = inputSchema.fields.map((f) => {
1106
- return makeData4({ type: f.type, length: 0, nullCount: 0 });
1415
+ return makeData5({ type: f.type, length: 0, nullCount: 0 });
1107
1416
  });
1108
- const structType = new Struct4(inputSchema.fields);
1109
- const data = makeData4({
1417
+ const structType = new Struct5(inputSchema.fields);
1418
+ const data = makeData5({
1110
1419
  type: structType,
1111
1420
  length: 0,
1112
1421
  children,
1113
1422
  nullCount: 0
1114
1423
  });
1115
- batch = new RecordBatch4(inputSchema, data);
1424
+ batch = new RecordBatch5(inputSchema, data);
1116
1425
  } else {
1117
1426
  const keys = Object.keys(input[0]);
1118
1427
  const fields = keys.map((key) => {
@@ -1137,16 +1446,16 @@ class PipeStreamSession {
1137
1446
  }
1138
1447
  const children = inputSchema.fields.map((f) => {
1139
1448
  const values = input.map((row) => row[f.name]);
1140
- return vectorFromArray4(values, f.type).data[0];
1449
+ return vectorFromArray5(values, f.type).data[0];
1141
1450
  });
1142
- const structType = new Struct4(inputSchema.fields);
1143
- const data = makeData4({
1451
+ const structType = new Struct5(inputSchema.fields);
1452
+ const data = makeData5({
1144
1453
  type: structType,
1145
1454
  length: input.length,
1146
1455
  children,
1147
1456
  nullCount: 0
1148
1457
  });
1149
- batch = new RecordBatch4(inputSchema, data);
1458
+ batch = new RecordBatch5(inputSchema, data);
1150
1459
  }
1151
1460
  if (!this._inputWriter) {
1152
1461
  this._inputWriter = new PipeIncrementalWriter(this._writeFn, inputSchema);
@@ -1185,14 +1494,14 @@ class PipeStreamSession {
1185
1494
  try {
1186
1495
  const tickSchema = new Schema2([]);
1187
1496
  this._inputWriter = new PipeIncrementalWriter(this._writeFn, tickSchema);
1188
- const structType = new Struct4(tickSchema.fields);
1189
- const tickData = makeData4({
1497
+ const structType = new Struct5(tickSchema.fields);
1498
+ const tickData = makeData5({
1190
1499
  type: structType,
1191
1500
  length: 0,
1192
1501
  children: [],
1193
1502
  nullCount: 0
1194
1503
  });
1195
- const tickBatch = new RecordBatch4(tickSchema, tickData);
1504
+ const tickBatch = new RecordBatch5(tickSchema, tickData);
1196
1505
  while (true) {
1197
1506
  this._inputWriter.write(tickBatch);
1198
1507
  await this._ensureOutputStream();
@@ -1247,6 +1556,7 @@ class PipeStreamSession {
1247
1556
  }
1248
1557
  function pipeConnect(readable, writable, options) {
1249
1558
  const onLog = options?.onLog;
1559
+ const externalConfig = options?.externalLocation;
1250
1560
  let reader = null;
1251
1561
  let readerPromise = null;
1252
1562
  let methodCache = null;
@@ -1322,10 +1632,14 @@ function pipeConnect(readable, writable, options) {
1322
1632
  throw new Error("EOF reading response");
1323
1633
  }
1324
1634
  let resultBatch = null;
1325
- for (const batch of response.batches) {
1635
+ for (let batch of response.batches) {
1326
1636
  if (batch.numRows === 0) {
1327
- dispatchLogOrError(batch, onLog);
1328
- continue;
1637
+ if (isExternalLocationBatch(batch)) {
1638
+ batch = await resolveExternalLocation(batch, externalConfig);
1639
+ } else {
1640
+ dispatchLogOrError(batch, onLog);
1641
+ continue;
1642
+ }
1329
1643
  }
1330
1644
  resultBatch = batch;
1331
1645
  }
@@ -1378,7 +1692,8 @@ function pipeConnect(readable, writable, options) {
1378
1692
  header,
1379
1693
  outputSchema,
1380
1694
  releaseBusy,
1381
- setDrainPromise
1695
+ setDrainPromise,
1696
+ externalConfig
1382
1697
  });
1383
1698
  } catch (e) {
1384
1699
  try {
@@ -1428,7 +1743,8 @@ function subprocessConnect(cmd, options) {
1428
1743
  }
1429
1744
  };
1430
1745
  const client = pipeConnect(stdout, writable, {
1431
- onLog: options?.onLog
1746
+ onLog: options?.onLog,
1747
+ externalLocation: options?.externalLocation
1432
1748
  });
1433
1749
  const originalClose = client.close;
1434
1750
  client.close = () => {
@@ -1449,6 +1765,8 @@ function oauthResourceMetadataToJson(metadata) {
1449
1765
  json.scopes_supported = metadata.scopesSupported;
1450
1766
  if (metadata.bearerMethodsSupported)
1451
1767
  json.bearer_methods_supported = metadata.bearerMethodsSupported;
1768
+ if (metadata.resourceSigningAlgValuesSupported)
1769
+ json.resource_signing_alg_values_supported = metadata.resourceSigningAlgValuesSupported;
1452
1770
  if (metadata.resourceName)
1453
1771
  json.resource_name = metadata.resourceName;
1454
1772
  if (metadata.resourceDocumentation)
@@ -1457,146 +1775,124 @@ function oauthResourceMetadataToJson(metadata) {
1457
1775
  json.resource_policy_uri = metadata.resourcePolicyUri;
1458
1776
  if (metadata.resourceTosUri)
1459
1777
  json.resource_tos_uri = metadata.resourceTosUri;
1778
+ if (metadata.clientId) {
1779
+ if (!/^[A-Za-z0-9\-._~]+$/.test(metadata.clientId)) {
1780
+ throw new Error(`Invalid client_id: must contain only URL-safe characters [A-Za-z0-9\\-._~]`);
1781
+ }
1782
+ json.client_id = metadata.clientId;
1783
+ }
1784
+ if (metadata.clientSecret) {
1785
+ if (!/^[A-Za-z0-9\-._~]+$/.test(metadata.clientSecret)) {
1786
+ throw new Error(`Invalid client_secret: must contain only URL-safe characters [A-Za-z0-9\\-._~]`);
1787
+ }
1788
+ json.client_secret = metadata.clientSecret;
1789
+ }
1790
+ if (metadata.deviceCodeClientId) {
1791
+ if (!/^[A-Za-z0-9\-._~]+$/.test(metadata.deviceCodeClientId)) {
1792
+ throw new Error(`Invalid device_code_client_id: must contain only URL-safe characters [A-Za-z0-9\\-._~]`);
1793
+ }
1794
+ json.device_code_client_id = metadata.deviceCodeClientId;
1795
+ }
1796
+ if (metadata.deviceCodeClientSecret) {
1797
+ if (!/^[A-Za-z0-9\-._~]+$/.test(metadata.deviceCodeClientSecret)) {
1798
+ throw new Error(`Invalid device_code_client_secret: must contain only URL-safe characters [A-Za-z0-9\\-._~]`);
1799
+ }
1800
+ json.device_code_client_secret = metadata.deviceCodeClientSecret;
1801
+ }
1802
+ if (metadata.useIdTokenAsBearer) {
1803
+ json.use_id_token_as_bearer = true;
1804
+ }
1460
1805
  return json;
1461
1806
  }
1462
1807
  function wellKnownPath(prefix) {
1463
1808
  return `/.well-known/oauth-protected-resource${prefix}`;
1464
1809
  }
1465
- function buildWwwAuthenticateHeader(metadataUrl) {
1810
+ function buildWwwAuthenticateHeader(metadataUrl, clientId, clientSecret, useIdTokenAsBearer, deviceCodeClientId, deviceCodeClientSecret) {
1811
+ let header = "Bearer";
1466
1812
  if (metadataUrl) {
1467
- return `Bearer resource_metadata="${metadataUrl}"`;
1813
+ header += ` resource_metadata="${metadataUrl}"`;
1468
1814
  }
1469
- return "Bearer";
1470
- }
1471
- // src/http/handler.ts
1472
- import { randomBytes } from "node:crypto";
1473
- import { Schema as Schema5 } from "@query-farm/apache-arrow";
1474
-
1475
- // src/types.ts
1476
- import { RecordBatch as RecordBatch6, recordBatchFromArrays } from "@query-farm/apache-arrow";
1477
-
1478
- // src/wire/response.ts
1479
- import {
1480
- Data,
1481
- DataType as DataType2,
1482
- makeData as makeData5,
1483
- RecordBatch as RecordBatch5,
1484
- Struct as Struct5,
1485
- vectorFromArray as vectorFromArray5
1486
- } from "@query-farm/apache-arrow";
1487
- function coerceInt64(schema, values) {
1488
- const result = { ...values };
1489
- for (const field of schema.fields) {
1490
- const val = result[field.name];
1491
- if (val === undefined)
1492
- continue;
1493
- if (!DataType2.isInt(field.type) || field.type.bitWidth !== 64)
1494
- continue;
1495
- if (Array.isArray(val)) {
1496
- result[field.name] = val.map((v) => typeof v === "number" ? BigInt(v) : v);
1497
- } else if (typeof val === "number") {
1498
- result[field.name] = BigInt(val);
1499
- }
1815
+ if (clientId) {
1816
+ header += `, client_id="${clientId}"`;
1500
1817
  }
1501
- return result;
1502
- }
1503
- function buildResultBatch(schema, values, serverId, requestId) {
1504
- const metadata = new Map;
1505
- metadata.set(SERVER_ID_KEY, serverId);
1506
- if (requestId !== null) {
1507
- metadata.set(REQUEST_ID_KEY, requestId);
1818
+ if (clientSecret) {
1819
+ header += `, client_secret="${clientSecret}"`;
1508
1820
  }
1509
- if (schema.fields.length === 0) {
1510
- return buildEmptyBatch(schema, metadata);
1821
+ if (deviceCodeClientId) {
1822
+ header += `, device_code_client_id="${deviceCodeClientId}"`;
1511
1823
  }
1512
- for (const field of schema.fields) {
1513
- if (values[field.name] === undefined && !field.nullable) {
1514
- const got = Object.keys(values);
1515
- throw new TypeError(`Handler result missing required field '${field.name}'. Got keys: [${got.join(", ")}]`);
1516
- }
1824
+ if (deviceCodeClientSecret) {
1825
+ header += `, device_code_client_secret="${deviceCodeClientSecret}"`;
1517
1826
  }
1518
- const coerced = coerceInt64(schema, values);
1519
- const children = schema.fields.map((f) => {
1520
- const val = coerced[f.name];
1521
- if (val instanceof Data) {
1522
- return val;
1523
- }
1524
- const arr = vectorFromArray5([val], f.type);
1525
- return arr.data[0];
1526
- });
1527
- const structType = new Struct5(schema.fields);
1528
- const data = makeData5({
1529
- type: structType,
1530
- length: 1,
1531
- children,
1532
- nullCount: 0
1533
- });
1534
- return new RecordBatch5(schema, data, metadata);
1827
+ if (useIdTokenAsBearer) {
1828
+ header += `, use_id_token_as_bearer="true"`;
1829
+ }
1830
+ return header;
1535
1831
  }
1536
- function buildErrorBatch(schema, error, serverId, requestId) {
1537
- const metadata = new Map;
1538
- metadata.set(LOG_LEVEL_KEY, "EXCEPTION");
1539
- metadata.set(LOG_MESSAGE_KEY, `${error.constructor.name}: ${error.message}`);
1540
- const extra = {
1541
- exception_type: error.constructor.name,
1542
- exception_message: error.message,
1543
- traceback: error.stack ?? ""
1832
+ // src/http/bearer.ts
1833
+ import { timingSafeEqual } from "node:crypto";
1834
+ function bearerAuthenticate(options) {
1835
+ const { validate } = options;
1836
+ return async function authenticate(request) {
1837
+ const authHeader = request.headers.get("Authorization") ?? "";
1838
+ if (!authHeader.startsWith("Bearer ")) {
1839
+ throw new Error("Missing or invalid Authorization header");
1840
+ }
1841
+ const token = authHeader.slice(7);
1842
+ return validate(token);
1544
1843
  };
1545
- metadata.set(LOG_EXTRA_KEY, JSON.stringify(extra));
1546
- metadata.set(SERVER_ID_KEY, serverId);
1547
- if (requestId !== null) {
1548
- metadata.set(REQUEST_ID_KEY, requestId);
1549
- }
1550
- return buildEmptyBatch(schema, metadata);
1551
1844
  }
1552
- function buildLogBatch(schema, level, message, extra, serverId, requestId) {
1553
- const metadata = new Map;
1554
- metadata.set(LOG_LEVEL_KEY, level);
1555
- metadata.set(LOG_MESSAGE_KEY, message);
1556
- if (extra) {
1557
- metadata.set(LOG_EXTRA_KEY, JSON.stringify(extra));
1558
- }
1559
- if (serverId != null) {
1560
- metadata.set(SERVER_ID_KEY, serverId);
1561
- }
1562
- if (requestId != null) {
1563
- metadata.set(REQUEST_ID_KEY, requestId);
1564
- }
1565
- return buildEmptyBatch(schema, metadata);
1845
+ function safeEqual(a, b) {
1846
+ const enc = new TextEncoder;
1847
+ const bufA = enc.encode(a);
1848
+ const bufB = enc.encode(b);
1849
+ if (bufA.byteLength !== bufB.byteLength)
1850
+ return false;
1851
+ return timingSafeEqual(bufA, bufB);
1566
1852
  }
1567
- function makeEmptyData(type) {
1568
- if (DataType2.isStruct(type)) {
1569
- const children = type.children.map((f) => makeEmptyData(f.type));
1570
- return makeData5({ type, length: 0, children, nullCount: 0 });
1571
- }
1572
- if (DataType2.isList(type)) {
1573
- const childData = makeEmptyData(type.children[0].type);
1574
- return makeData5({ type, length: 0, children: [childData], nullCount: 0, valueOffsets: new Int32Array([0]) });
1575
- }
1576
- if (DataType2.isFixedSizeList(type)) {
1577
- const childData = makeEmptyData(type.children[0].type);
1578
- return makeData5({ type, length: 0, child: childData, nullCount: 0 });
1579
- }
1580
- if (DataType2.isMap(type)) {
1581
- const entryType = type.children[0]?.type;
1582
- const entryData = entryType ? makeEmptyData(entryType) : makeData5({ type: new Struct5([]), length: 0, children: [], nullCount: 0 });
1583
- return makeData5({ type, length: 0, children: [entryData], nullCount: 0, valueOffsets: new Int32Array([0]) });
1853
+ function bearerAuthenticateStatic(options) {
1854
+ const entries = options.tokens instanceof Map ? [...options.tokens.entries()] : Object.entries(options.tokens);
1855
+ function validate(token) {
1856
+ for (const [key, ctx] of entries) {
1857
+ if (safeEqual(token, key))
1858
+ return ctx;
1859
+ }
1860
+ throw new Error("Unknown bearer token");
1584
1861
  }
1585
- return makeData5({ type, length: 0, nullCount: 0 });
1862
+ return bearerAuthenticate({ validate });
1586
1863
  }
1587
- function buildEmptyBatch(schema, metadata) {
1588
- const children = schema.fields.map((f) => makeEmptyData(f.type));
1589
- const structType = new Struct5(schema.fields);
1590
- const data = makeData5({
1591
- type: structType,
1592
- length: 0,
1593
- children,
1594
- nullCount: 0
1595
- });
1596
- return new RecordBatch5(schema, data, metadata);
1864
+ function isCredentialError(err) {
1865
+ return err instanceof Error && err.constructor === Error && err.name !== "PermissionError";
1866
+ }
1867
+ function chainAuthenticate(...authenticators) {
1868
+ if (authenticators.length === 0) {
1869
+ throw new Error("chainAuthenticate requires at least one authenticator");
1870
+ }
1871
+ return async function authenticate(request) {
1872
+ let lastError = null;
1873
+ for (const authFn of authenticators) {
1874
+ try {
1875
+ return await authFn(request);
1876
+ } catch (err) {
1877
+ if (isCredentialError(err)) {
1878
+ lastError = err;
1879
+ continue;
1880
+ }
1881
+ throw err;
1882
+ }
1883
+ }
1884
+ const error = new Error("No authenticator accepted the request");
1885
+ if (lastError)
1886
+ error.cause = lastError;
1887
+ throw error;
1888
+ };
1597
1889
  }
1890
+ // src/http/handler.ts
1891
+ import { randomBytes } from "node:crypto";
1892
+ import { Schema as Schema5 } from "@query-farm/apache-arrow";
1598
1893
 
1599
1894
  // src/types.ts
1895
+ import { RecordBatch as RecordBatch6, recordBatchFromArrays } from "@query-farm/apache-arrow";
1600
1896
  var MethodType;
1601
1897
  ((MethodType2) => {
1602
1898
  MethodType2["UNARY"] = "unary";
@@ -1665,7 +1961,7 @@ class OutputCollector {
1665
1961
  init_zstd();
1666
1962
 
1667
1963
  // src/http/dispatch.ts
1668
- import { RecordBatch as RecordBatch8, RecordBatchReader as RecordBatchReader5, Schema as Schema4 } from "@query-farm/apache-arrow";
1964
+ import { RecordBatch as RecordBatch8, RecordBatchReader as RecordBatchReader6, Schema as Schema4 } from "@query-farm/apache-arrow";
1669
1965
 
1670
1966
  // src/dispatch/describe.ts
1671
1967
  import {
@@ -1681,9 +1977,9 @@ import {
1681
1977
  } from "@query-farm/apache-arrow";
1682
1978
 
1683
1979
  // src/util/schema.ts
1684
- import { RecordBatchStreamWriter as RecordBatchStreamWriter3 } from "@query-farm/apache-arrow";
1980
+ import { RecordBatchStreamWriter as RecordBatchStreamWriter4 } from "@query-farm/apache-arrow";
1685
1981
  function serializeSchema(schema) {
1686
- const writer = new RecordBatchStreamWriter3;
1982
+ const writer = new RecordBatchStreamWriter4;
1687
1983
  writer.reset(undefined, schema);
1688
1984
  writer.close();
1689
1985
  return writer.toUint8Array(true);
@@ -1700,7 +1996,9 @@ var DESCRIBE_SCHEMA = new Schema3([
1700
1996
  new Field3("param_types_json", new Utf82, true),
1701
1997
  new Field3("param_defaults_json", new Utf82, true),
1702
1998
  new Field3("has_header", new Bool2, false),
1703
- new Field3("header_schema_ipc", new Binary2, true)
1999
+ new Field3("header_schema_ipc", new Binary2, true),
2000
+ new Field3("is_exchange", new Bool2, true),
2001
+ new Field3("param_docs_json", new Utf82, true)
1704
2002
  ]);
1705
2003
  function buildDescribeBatch(protocolName, methods, serverId) {
1706
2004
  const sortedEntries = [...methods.entries()].sort(([a], [b]) => a.localeCompare(b));
@@ -1714,6 +2012,8 @@ function buildDescribeBatch(protocolName, methods, serverId) {
1714
2012
  const paramDefaultsJsons = [];
1715
2013
  const hasHeaders = [];
1716
2014
  const headerSchemas = [];
2015
+ const isExchanges = [];
2016
+ const paramDocsJsons = [];
1717
2017
  for (const [name, method] of sortedEntries) {
1718
2018
  names.push(name);
1719
2019
  methodTypes.push(method.type);
@@ -1740,6 +2040,14 @@ function buildDescribeBatch(protocolName, methods, serverId) {
1740
2040
  }
1741
2041
  hasHeaders.push(!!method.headerSchema);
1742
2042
  headerSchemas.push(method.headerSchema ? serializeSchema(method.headerSchema) : null);
2043
+ if (method.exchangeFn) {
2044
+ isExchanges.push(true);
2045
+ } else if (method.producerFn) {
2046
+ isExchanges.push(false);
2047
+ } else {
2048
+ isExchanges.push(null);
2049
+ }
2050
+ paramDocsJsons.push(null);
1743
2051
  }
1744
2052
  const nameArr = vectorFromArray6(names, new Utf82);
1745
2053
  const methodTypeArr = vectorFromArray6(methodTypes, new Utf82);
@@ -1751,6 +2059,8 @@ function buildDescribeBatch(protocolName, methods, serverId) {
1751
2059
  const paramDefaultsArr = vectorFromArray6(paramDefaultsJsons, new Utf82);
1752
2060
  const hasHeaderArr = vectorFromArray6(hasHeaders, new Bool2);
1753
2061
  const headerSchemaArr = vectorFromArray6(headerSchemas, new Binary2);
2062
+ const isExchangeArr = vectorFromArray6(isExchanges, new Bool2);
2063
+ const paramDocsArr = vectorFromArray6(paramDocsJsons, new Utf82);
1754
2064
  const children = [
1755
2065
  nameArr.data[0],
1756
2066
  methodTypeArr.data[0],
@@ -1761,7 +2071,9 @@ function buildDescribeBatch(protocolName, methods, serverId) {
1761
2071
  paramTypesArr.data[0],
1762
2072
  paramDefaultsArr.data[0],
1763
2073
  hasHeaderArr.data[0],
1764
- headerSchemaArr.data[0]
2074
+ headerSchemaArr.data[0],
2075
+ isExchangeArr.data[0],
2076
+ paramDocsArr.data[0]
1765
2077
  ];
1766
2078
  const structType = new Struct6(DESCRIBE_SCHEMA.fields);
1767
2079
  const data = makeData6({
@@ -1824,7 +2136,7 @@ function parseRequest(schema, batch) {
1824
2136
  }
1825
2137
 
1826
2138
  // src/http/token.ts
1827
- import { createHmac, timingSafeEqual } from "node:crypto";
2139
+ import { createHmac, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
1828
2140
  var TOKEN_VERSION = 2;
1829
2141
  var HMAC_LEN = 32;
1830
2142
  var MIN_TOKEN_LEN = 1 + 8 + 12 + HMAC_LEN;
@@ -1861,7 +2173,7 @@ function unpackStateToken(tokenBase64, signingKey, tokenTtl) {
1861
2173
  const payload = token.subarray(0, token.length - HMAC_LEN);
1862
2174
  const receivedMac = token.subarray(token.length - HMAC_LEN);
1863
2175
  const expectedMac = createHmac("sha256", signingKey).update(payload).digest();
1864
- if (!timingSafeEqual(receivedMac, expectedMac)) {
2176
+ if (!timingSafeEqual2(receivedMac, expectedMac)) {
1865
2177
  throw new Error("State token HMAC verification failed");
1866
2178
  }
1867
2179
  let offset = 0;
@@ -1903,7 +2215,7 @@ function unpackStateToken(tokenBase64, signingKey, tokenTtl) {
1903
2215
 
1904
2216
  // src/http/dispatch.ts
1905
2217
  async function deserializeSchema2(bytes) {
1906
- const reader = await RecordBatchReader5.from(bytes);
2218
+ const reader = await RecordBatchReader6.from(bytes);
1907
2219
  await reader.open();
1908
2220
  return reader.schema;
1909
2221
  }
@@ -1923,12 +2235,17 @@ async function httpDispatchUnary(method, body, ctx) {
1923
2235
  const out = new OutputCollector(schema, true, ctx.serverId, parsed.requestId, ctx.authContext);
1924
2236
  try {
1925
2237
  const result = await method.handler(parsed.params, out);
1926
- const resultBatch = buildResultBatch(schema, result, ctx.serverId, parsed.requestId);
2238
+ let resultBatch = buildResultBatch(schema, result, ctx.serverId, parsed.requestId);
2239
+ if (ctx.externalLocation) {
2240
+ resultBatch = await maybeExternalizeBatch(resultBatch, ctx.externalLocation);
2241
+ }
1927
2242
  const batches = [...out.batches.map((b) => b.batch), resultBatch];
1928
2243
  return arrowResponse(serializeIpcStream(schema, batches));
1929
2244
  } catch (error) {
1930
2245
  const errBatch = buildErrorBatch(schema, error, ctx.serverId, parsed.requestId);
1931
- return arrowResponse(serializeIpcStream(schema, [errBatch]), 500);
2246
+ const response = arrowResponse(serializeIpcStream(schema, [errBatch]), 500);
2247
+ response.__dispatchError = error;
2248
+ return response;
1932
2249
  }
1933
2250
  }
1934
2251
  async function httpDispatchStreamInit(method, body, ctx) {
@@ -1950,7 +2267,9 @@ async function httpDispatchStreamInit(method, body, ctx) {
1950
2267
  } catch (error) {
1951
2268
  const errSchema = method.headerSchema ?? EMPTY_SCHEMA;
1952
2269
  const errBatch = buildErrorBatch(errSchema, error, ctx.serverId, parsed.requestId);
1953
- return arrowResponse(serializeIpcStream(errSchema, [errBatch]), 500);
2270
+ const response = arrowResponse(serializeIpcStream(errSchema, [errBatch]), 500);
2271
+ response.__dispatchError = error;
2272
+ return response;
1954
2273
  }
1955
2274
  const resolvedOutputSchema = state?.__outputSchema ?? outputSchema;
1956
2275
  const effectiveProducer = state?.__isProducer ?? isProducer;
@@ -1964,7 +2283,9 @@ async function httpDispatchStreamInit(method, body, ctx) {
1964
2283
  headerBytes = serializeIpcStream(method.headerSchema, headerBatches);
1965
2284
  } catch (error) {
1966
2285
  const errBatch = buildErrorBatch(method.headerSchema, error, ctx.serverId, parsed.requestId);
1967
- return arrowResponse(serializeIpcStream(method.headerSchema, [errBatch]), 500);
2286
+ const response = arrowResponse(serializeIpcStream(method.headerSchema, [errBatch]), 500);
2287
+ response.__dispatchError = error;
2288
+ return response;
1968
2289
  }
1969
2290
  }
1970
2291
  if (effectiveProducer) {
@@ -2039,7 +2360,9 @@ async function httpDispatchStreamExchange(method, body, ctx) {
2039
2360
  `).slice(0, 5).join(`
2040
2361
  `));
2041
2362
  const errBatch = buildErrorBatch(outputSchema, error, ctx.serverId, null);
2042
- return arrowResponse(serializeIpcStream(outputSchema, [errBatch]), 500);
2363
+ const response = arrowResponse(serializeIpcStream(outputSchema, [errBatch]), 500);
2364
+ response.__dispatchError = error;
2365
+ return response;
2043
2366
  }
2044
2367
  const batches = [];
2045
2368
  if (out.finished) {
@@ -2074,6 +2397,7 @@ async function produceStreamResponse(method, state, outputSchema, inputSchema, c
2074
2397
  const allBatches = [];
2075
2398
  const maxBytes = ctx.maxStreamResponseBytes;
2076
2399
  let estimatedBytes = 0;
2400
+ let producerError;
2077
2401
  while (true) {
2078
2402
  const out = new OutputCollector(outputSchema, true, ctx.serverId, requestId, ctx.authContext);
2079
2403
  try {
@@ -2089,6 +2413,7 @@ async function produceStreamResponse(method, state, outputSchema, inputSchema, c
2089
2413
  `).slice(0, 3).join(`
2090
2414
  `));
2091
2415
  allBatches.push(buildErrorBatch(outputSchema, error, ctx.serverId, requestId));
2416
+ producerError = error instanceof Error ? error : new Error(String(error));
2092
2417
  break;
2093
2418
  }
2094
2419
  for (const emitted of out.batches) {
@@ -2118,7 +2443,11 @@ async function produceStreamResponse(method, state, outputSchema, inputSchema, c
2118
2443
  } else {
2119
2444
  responseBody = dataBytes;
2120
2445
  }
2121
- return arrowResponse(responseBody);
2446
+ const response = arrowResponse(responseBody);
2447
+ if (producerError) {
2448
+ response.__dispatchError = producerError;
2449
+ }
2450
+ return response;
2122
2451
  }
2123
2452
  function concatBytes(...arrays) {
2124
2453
  const totalLen = arrays.reduce((sum, a) => sum + a.length, 0);
@@ -2131,6 +2460,261 @@ function concatBytes(...arrays) {
2131
2460
  return result;
2132
2461
  }
2133
2462
 
2463
+ // src/http/pages.ts
2464
+ var LOGO_URL = "https://vgi-rpc-python.query.farm/assets/logo-hero.png";
2465
+ var FONTS = `<link rel="preconnect" href="https://fonts.googleapis.com">
2466
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
2467
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">`;
2468
+ function escapeHtml(s) {
2469
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2470
+ }
2471
+ function arrowTypeToString(type) {
2472
+ const id = type.typeId;
2473
+ if (id === 5)
2474
+ return "str";
2475
+ if (id === 4)
2476
+ return "bytes";
2477
+ if (id === 2)
2478
+ return "int";
2479
+ if (id === 3)
2480
+ return "float";
2481
+ if (id === 6)
2482
+ return "bool";
2483
+ if (id === 12)
2484
+ return "list";
2485
+ if (id === 17)
2486
+ return "map";
2487
+ if (id === 24)
2488
+ return "enum";
2489
+ return type.toString();
2490
+ }
2491
+ function buildLandingPage(protocolName, serverId, describePath, repoUrl) {
2492
+ const links = [];
2493
+ if (describePath) {
2494
+ links.push(`<a class="primary" href="${escapeHtml(describePath)}">View service API</a>`);
2495
+ }
2496
+ if (repoUrl) {
2497
+ links.push(`<a href="${escapeHtml(repoUrl)}">Source repository</a>`);
2498
+ }
2499
+ links.push(`<a href="https://vgi-rpc.query.farm">Learn more about <code>vgi-rpc</code></a>`);
2500
+ return `<!DOCTYPE html>
2501
+ <html lang="en">
2502
+ <head>
2503
+ <meta charset="utf-8">
2504
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2505
+ <title>${escapeHtml(protocolName)} — vgi-rpc</title>
2506
+ ${FONTS}
2507
+ <style>
2508
+ body { font-family: 'Inter', system-ui, -apple-system, sans-serif; max-width: 600px;
2509
+ margin: 0 auto; padding: 60px 20px 0; color: #2c2c1e; text-align: center;
2510
+ background: #faf8f0; }
2511
+ .logo { margin-bottom: 24px; }
2512
+ .logo img { width: 140px; height: 140px; border-radius: 50%;
2513
+ box-shadow: 0 4px 24px rgba(0,0,0,0.12); }
2514
+ h1 { color: #2d5016; margin-bottom: 8px; font-weight: 700; }
2515
+ code { font-family: 'JetBrains Mono', monospace; background: #f0ece0;
2516
+ padding: 2px 6px; border-radius: 3px; font-size: 0.9em; color: #2c2c1e; }
2517
+ a { color: #2d5016; text-decoration: none; }
2518
+ a:hover { color: #4a7c23; }
2519
+ p { line-height: 1.7; color: #6b6b5a; }
2520
+ .meta { font-size: 0.9em; color: #6b6b5a; }
2521
+ .links { margin-top: 28px; display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
2522
+ .links a { display: inline-block; padding: 8px 18px; border-radius: 6px;
2523
+ border: 1px solid #4a7c23; color: #2d5016; font-weight: 600;
2524
+ font-size: 0.9em; transition: all 0.2s ease; }
2525
+ .links a:hover { background: #4a7c23; color: #fff; }
2526
+ .links a.primary { background: #2d5016; color: #fff; border-color: #2d5016; }
2527
+ .links a.primary:hover { background: #4a7c23; border-color: #4a7c23; }
2528
+ footer { margin-top: 48px; padding: 20px 0; border-top: 1px solid #f0ece0;
2529
+ color: #6b6b5a; font-size: 0.85em; }
2530
+ footer a { color: #2d5016; font-weight: 600; }
2531
+ footer a:hover { color: #4a7c23; }
2532
+ </style>
2533
+ </head>
2534
+ <body>
2535
+ <div class="logo">
2536
+ <img src="${LOGO_URL}" alt="vgi-rpc logo">
2537
+ </div>
2538
+ <h1>${escapeHtml(protocolName)}</h1>
2539
+ <p class="meta">Powered by <code>vgi-rpc</code> (TypeScript) &middot; server <code>${escapeHtml(serverId)}</code></p>
2540
+ <p>This is a <code>vgi-rpc</code> service endpoint.</p>
2541
+ <div class="links">
2542
+ ${links.join(`
2543
+ `)}
2544
+ </div>
2545
+ <footer>
2546
+ &copy; 2026 &#x1F69C; <a href="https://query.farm">Query.Farm LLC</a>
2547
+ </footer>
2548
+ </body>
2549
+ </html>`;
2550
+ }
2551
+ function buildNotFoundPage(prefix, protocolName) {
2552
+ const nameFragment = protocolName ? ` (<strong>${escapeHtml(protocolName)}</strong>)` : "";
2553
+ const prefixDisplay = prefix || "/";
2554
+ return `<!DOCTYPE html>
2555
+ <html lang="en">
2556
+ <head>
2557
+ <meta charset="utf-8">
2558
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2559
+ <title>404 — vgi-rpc endpoint</title>
2560
+ <style>
2561
+ body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px;
2562
+ margin: 60px auto; padding: 0 20px; color: #333; text-align: center; }
2563
+ .logo { margin-bottom: 24px; }
2564
+ .logo img { width: 120px; height: 120px; }
2565
+ h1 { color: #555; }
2566
+ code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.95em; }
2567
+ a { color: #0066cc; }
2568
+ p { line-height: 1.6; }
2569
+ </style>
2570
+ </head>
2571
+ <body>
2572
+ <div class="logo">
2573
+ <img src="${LOGO_URL}" alt="vgi-rpc logo">
2574
+ </div>
2575
+ <h1>404 — Not Found</h1>
2576
+ <p>This is a <code>vgi-rpc</code> service endpoint${nameFragment}.</p>
2577
+ <p>RPC methods are available under <code>${escapeHtml(prefixDisplay)}/&lt;method&gt;</code>.</p>
2578
+ <p>Learn more at <a href="https://vgi-rpc.query.farm">vgi-rpc.query.farm</a>.</p>
2579
+ </body>
2580
+ </html>`;
2581
+ }
2582
+ function buildMethodCard(method) {
2583
+ const name = escapeHtml(method.name);
2584
+ const isUnary = method.type === "unary";
2585
+ const hasHeader = !!method.headerSchema;
2586
+ const badges = [];
2587
+ badges.push(isUnary ? `<span class="badge badge-unary">unary</span>` : `<span class="badge badge-stream">stream</span>`);
2588
+ if (hasHeader)
2589
+ badges.push(`<span class="badge badge-header">header</span>`);
2590
+ let paramsHtml = "";
2591
+ const paramsSchema = method.paramsSchema;
2592
+ if (paramsSchema.fields.length > 0) {
2593
+ const rows = paramsSchema.fields.map((f) => {
2594
+ const paramName = escapeHtml(f.name);
2595
+ const paramType = escapeHtml(arrowTypeToString(f.type));
2596
+ const defaultVal = method.defaults && f.name in method.defaults ? escapeHtml(JSON.stringify(method.defaults[f.name])) : "&mdash;";
2597
+ return `<tr><td><code>${paramName}</code></td><td><code>${paramType}</code></td><td>${defaultVal}</td><td>&mdash;</td></tr>`;
2598
+ });
2599
+ paramsHtml = `<div class="section-label">Parameters</div>
2600
+ <table><tr><th>Name</th><th>Type</th><th>Default</th><th>Description</th></tr>
2601
+ ${rows.join(`
2602
+ `)}
2603
+ </table>`;
2604
+ } else {
2605
+ paramsHtml = `<p class="no-params">No parameters</p>`;
2606
+ }
2607
+ let returnsHtml = "";
2608
+ if (isUnary && method.resultSchema.fields.length > 0) {
2609
+ const rows = method.resultSchema.fields.map((f) => {
2610
+ return `<tr><td><code>${escapeHtml(f.name)}</code></td><td><code>${escapeHtml(arrowTypeToString(f.type))}</code></td></tr>`;
2611
+ });
2612
+ returnsHtml = `<div class="section-label">Returns</div>
2613
+ <table><tr><th>Name</th><th>Type</th></tr>
2614
+ ${rows.join(`
2615
+ `)}
2616
+ </table>`;
2617
+ }
2618
+ let headerHtml = "";
2619
+ if (hasHeader && method.headerSchema && method.headerSchema.fields.length > 0) {
2620
+ const rows = method.headerSchema.fields.map((f) => {
2621
+ return `<tr><td><code>${escapeHtml(f.name)}</code></td><td><code>${escapeHtml(arrowTypeToString(f.type))}</code></td></tr>`;
2622
+ });
2623
+ headerHtml = `<div class="section-label">Stream Header</div>
2624
+ <table><tr><th>Name</th><th>Type</th></tr>
2625
+ ${rows.join(`
2626
+ `)}
2627
+ </table>`;
2628
+ }
2629
+ const docHtml = method.doc ? `<p class="docstring">${escapeHtml(method.doc)}</p>` : "";
2630
+ return `<div class="card">
2631
+ <div class="card-header">
2632
+ <span class="method-name">${name}</span>
2633
+ ${badges.join(`
2634
+ `)}
2635
+ </div>
2636
+ ${docHtml}
2637
+ ${paramsHtml}
2638
+ ${returnsHtml}
2639
+ ${headerHtml}
2640
+ </div>`;
2641
+ }
2642
+ function buildDescribePage(protocolName, serverId, methods, repoUrl) {
2643
+ const sortedMethods = [...methods.entries()].filter(([name]) => name !== "__describe__").sort(([a], [b]) => a.localeCompare(b));
2644
+ const cards = sortedMethods.map(([, method]) => buildMethodCard(method)).join(`
2645
+ `);
2646
+ const repoLink = repoUrl ? ` &middot; <a href="${escapeHtml(repoUrl)}">Source</a>` : "";
2647
+ return `<!DOCTYPE html>
2648
+ <html lang="en">
2649
+ <head>
2650
+ <meta charset="utf-8">
2651
+ <meta name="viewport" content="width=device-width, initial-scale=1">
2652
+ <title>${escapeHtml(protocolName)} API Reference — vgi-rpc</title>
2653
+ ${FONTS}
2654
+ <style>
2655
+ body { font-family: 'Inter', system-ui, -apple-system, sans-serif; max-width: 900px;
2656
+ margin: 0 auto; padding: 40px 20px 0; color: #2c2c1e; background: #faf8f0; }
2657
+ .header { text-align: center; margin-bottom: 40px; }
2658
+ .header .logo img { width: 80px; height: 80px; border-radius: 50%;
2659
+ box-shadow: 0 3px 16px rgba(0,0,0,0.10); }
2660
+ .header h1 { margin-bottom: 4px; color: #2d5016; font-weight: 700; }
2661
+ .header .subtitle { color: #6b6b5a; font-size: 1.1em; margin-top: 0; }
2662
+ .header .meta { color: #6b6b5a; font-size: 0.9em; }
2663
+ .header .meta a { color: #2d5016; font-weight: 600; }
2664
+ .header .meta a:hover { color: #4a7c23; }
2665
+ code { font-family: 'JetBrains Mono', monospace; background: #f0ece0;
2666
+ padding: 2px 6px; border-radius: 3px; font-size: 0.85em; color: #2c2c1e; }
2667
+ a { color: #2d5016; text-decoration: none; }
2668
+ a:hover { color: #4a7c23; }
2669
+ .card { border: 1px solid #f0ece0; border-radius: 8px; padding: 20px;
2670
+ margin-bottom: 16px; background: #fff; }
2671
+ .card:hover { border-color: #c8a43a; }
2672
+ .card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
2673
+ .method-name { font-family: 'JetBrains Mono', monospace; font-size: 1.1em; font-weight: 600;
2674
+ color: #2d5016; }
2675
+ .badge { display: inline-block; padding: 2px 8px; border-radius: 4px;
2676
+ font-size: 0.75em; font-weight: 600; text-transform: uppercase;
2677
+ letter-spacing: 0.03em; }
2678
+ .badge-unary { background: #e8f5e0; color: #2d5016; }
2679
+ .badge-stream { background: #e0ecf5; color: #1a4a6b; }
2680
+ .badge-exchange { background: #f5e6f0; color: #6b234a; }
2681
+ .badge-producer { background: #e0f0f5; color: #1a5a6b; }
2682
+ .badge-header { background: #f5eee0; color: #6b4423; }
2683
+ .docstring { color: #6b6b5a; font-size: 0.9em; margin-top: 0; }
2684
+ table { width: 100%; border-collapse: collapse; font-size: 0.9em; }
2685
+ th { text-align: left; padding: 8px 10px; background: #f0ece0; color: #2c2c1e;
2686
+ font-weight: 600; border-bottom: 2px solid #e0dcd0; }
2687
+ td { padding: 8px 10px; border-bottom: 1px solid #f0ece0; }
2688
+ td code { font-size: 0.85em; }
2689
+ .no-params { color: #6b6b5a; font-style: italic; font-size: 0.9em; }
2690
+ .section-label { font-size: 0.8em; font-weight: 600; text-transform: uppercase;
2691
+ letter-spacing: 0.05em; color: #6b6b5a; margin-top: 14px;
2692
+ margin-bottom: 6px; }
2693
+ footer { text-align: center; margin-top: 48px; padding: 20px 0;
2694
+ border-top: 1px solid #f0ece0; color: #6b6b5a; font-size: 0.85em; }
2695
+ footer a { color: #2d5016; font-weight: 600; }
2696
+ footer a:hover { color: #4a7c23; }
2697
+ </style>
2698
+ </head>
2699
+ <body>
2700
+ <div class="header">
2701
+ <div class="logo">
2702
+ <img src="${LOGO_URL}" alt="vgi-rpc logo">
2703
+ </div>
2704
+ <h1>${escapeHtml(protocolName)}</h1>
2705
+ <p class="subtitle">API Reference</p>
2706
+ <p class="meta">Powered by <code>vgi-rpc</code> (TypeScript) &middot; server <code>${escapeHtml(serverId)}</code>${repoLink}</p>
2707
+ </div>
2708
+ ${cards}
2709
+ <footer>
2710
+ <a href="https://vgi-rpc.query.farm">Learn more about <code>vgi-rpc</code></a>
2711
+ &middot;
2712
+ &copy; 2026 &#x1F69C; <a href="https://query.farm">Query.Farm LLC</a>
2713
+ </footer>
2714
+ </body>
2715
+ </html>`;
2716
+ }
2717
+
2134
2718
  // src/http/types.ts
2135
2719
  var jsonStateSerializer = {
2136
2720
  serialize(state) {
@@ -2144,7 +2728,7 @@ var jsonStateSerializer = {
2144
2728
  // src/http/handler.ts
2145
2729
  var EMPTY_SCHEMA2 = new Schema5([]);
2146
2730
  function createHttpHandler(protocol, options) {
2147
- const prefix = (options?.prefix ?? "/vgi").replace(/\/+$/, "");
2731
+ const prefix = (options?.prefix ?? "").replace(/\/+$/, "");
2148
2732
  const signingKey = options?.signingKey ?? randomBytes(32);
2149
2733
  const tokenTtl = options?.tokenTtl ?? 3600;
2150
2734
  const corsOrigins = options?.corsOrigins;
@@ -2156,12 +2740,23 @@ function createHttpHandler(protocol, options) {
2156
2740
  const methods = protocol.getMethods();
2157
2741
  const compressionLevel = options?.compressionLevel;
2158
2742
  const stateSerializer = options?.stateSerializer ?? jsonStateSerializer;
2743
+ const dispatchHook = options?.dispatchHook;
2744
+ const enableLandingPage = options?.enableLandingPage ?? true;
2745
+ const enableDescribePage = options?.enableDescribePage ?? true;
2746
+ const enableNotFoundPage = options?.enableNotFoundPage ?? true;
2747
+ const displayName = options?.protocolName ?? protocol.name;
2748
+ const repoUrl = options?.repositoryUrl ?? null;
2749
+ const landingHtml = enableLandingPage ? buildLandingPage(displayName, serverId, enableDescribePage ? `${prefix}/describe` : null, repoUrl) : null;
2750
+ const describeHtml = enableDescribePage ? buildDescribePage(displayName, serverId, methods, repoUrl) : null;
2751
+ const notFoundHtml = enableNotFoundPage ? buildNotFoundPage(prefix, displayName) : null;
2752
+ const externalLocation = options?.externalLocation;
2159
2753
  const baseCtx = {
2160
2754
  signingKey,
2161
2755
  tokenTtl,
2162
2756
  serverId,
2163
2757
  maxStreamResponseBytes,
2164
- stateSerializer
2758
+ stateSerializer,
2759
+ externalLocation
2165
2760
  };
2166
2761
  function addCorsHeaders(headers) {
2167
2762
  if (corsOrigins) {
@@ -2199,7 +2794,7 @@ function createHttpHandler(protocol, options) {
2199
2794
  const body2 = JSON.stringify(oauthResourceMetadataToJson(oauthMetadata));
2200
2795
  const headers = new Headers({
2201
2796
  "Content-Type": "application/json",
2202
- "Cache-Control": "public, max-age=3600"
2797
+ "Cache-Control": "public, max-age=60"
2203
2798
  });
2204
2799
  addCorsHeaders(headers);
2205
2800
  return new Response(body2, { status: 200, headers });
@@ -2220,6 +2815,24 @@ function createHttpHandler(protocol, options) {
2220
2815
  }
2221
2816
  return new Response(null, { status: 405 });
2222
2817
  }
2818
+ if (request.method === "GET") {
2819
+ if (landingHtml && (path === prefix || path === `${prefix}/`)) {
2820
+ const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
2821
+ addCorsHeaders(headers);
2822
+ return new Response(landingHtml, { status: 200, headers });
2823
+ }
2824
+ if (describeHtml && path === `${prefix}/describe`) {
2825
+ const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
2826
+ addCorsHeaders(headers);
2827
+ return new Response(describeHtml, { status: 200, headers });
2828
+ }
2829
+ if (notFoundHtml) {
2830
+ const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
2831
+ addCorsHeaders(headers);
2832
+ return new Response(notFoundHtml, { status: 404, headers });
2833
+ }
2834
+ return new Response("Not Found", { status: 404 });
2835
+ }
2223
2836
  if (request.method !== "POST") {
2224
2837
  return new Response("Method Not Allowed", { status: 405 });
2225
2838
  }
@@ -2250,7 +2863,7 @@ function createHttpHandler(protocol, options) {
2250
2863
  const metadataUrl = new URL(request.url);
2251
2864
  metadataUrl.pathname = wellKnownPath(prefix);
2252
2865
  metadataUrl.search = "";
2253
- headers.set("WWW-Authenticate", buildWwwAuthenticateHeader(metadataUrl.toString()));
2866
+ headers.set("WWW-Authenticate", buildWwwAuthenticateHeader(metadataUrl.toString(), oauthMetadata.clientId, oauthMetadata.clientSecret, oauthMetadata.useIdTokenAsBearer, oauthMetadata.deviceCodeClientId, oauthMetadata.deviceCodeClientSecret));
2254
2867
  }
2255
2868
  return new Response(error.message || "Unauthorized", { status: 401, headers });
2256
2869
  }
@@ -2286,6 +2899,18 @@ function createHttpHandler(protocol, options) {
2286
2899
  const err = new Error(`Unknown method: '${methodName}'. Available methods: [${available.join(", ")}]`);
2287
2900
  return compressIfAccepted(makeErrorResponse(err, 404), clientAcceptsZstd);
2288
2901
  }
2902
+ const methodType = method.type === "unary" /* UNARY */ ? "unary" : "stream";
2903
+ const info = { method: methodName, methodType, serverId, requestId: null };
2904
+ const stats = {
2905
+ inputBatches: 0,
2906
+ outputBatches: 0,
2907
+ inputRows: 0,
2908
+ outputRows: 0,
2909
+ inputBytes: 0,
2910
+ outputBytes: 0
2911
+ };
2912
+ const hookToken = dispatchHook?.onDispatchStart(info);
2913
+ let dispatchError;
2289
2914
  try {
2290
2915
  let response;
2291
2916
  if (action === "call") {
@@ -2304,13 +2929,20 @@ function createHttpHandler(protocol, options) {
2304
2929
  }
2305
2930
  response = await httpDispatchStreamExchange(method, body, ctx);
2306
2931
  }
2932
+ const internalError = response.__dispatchError;
2933
+ if (internalError) {
2934
+ dispatchError = internalError instanceof Error ? internalError : new Error(String(internalError));
2935
+ }
2307
2936
  addCorsHeaders(response.headers);
2308
2937
  return compressIfAccepted(response, clientAcceptsZstd);
2309
2938
  } catch (error) {
2939
+ dispatchError = error instanceof Error ? error : new Error(String(error));
2310
2940
  if (error instanceof HttpRpcError) {
2311
2941
  return compressIfAccepted(makeErrorResponse(error, error.statusCode), clientAcceptsZstd);
2312
2942
  }
2313
2943
  return compressIfAccepted(makeErrorResponse(error, 500), clientAcceptsZstd);
2944
+ } finally {
2945
+ dispatchHook?.onDispatchEnd(hookToken, info, stats, dispatchError);
2314
2946
  }
2315
2947
  };
2316
2948
  }
@@ -3503,11 +4135,220 @@ function jwtAuthenticate(options) {
3503
4135
  asPromise = null;
3504
4136
  throw error;
3505
4137
  }
3506
- const claims = await validateJwtAccessToken(as, request, audience);
4138
+ const audiences = Array.isArray(audience) ? audience : [audience];
4139
+ let claims;
4140
+ let lastError;
4141
+ for (const aud of audiences) {
4142
+ try {
4143
+ claims = await validateJwtAccessToken(as, request, aud);
4144
+ break;
4145
+ } catch (error) {
4146
+ lastError = error;
4147
+ }
4148
+ }
4149
+ if (!claims) {
4150
+ throw lastError;
4151
+ }
3507
4152
  const principal = claims[principalClaim] ?? null;
3508
4153
  return new AuthContext(domain, true, principal, claims);
3509
4154
  };
3510
4155
  }
4156
+ // src/http/mtls.ts
4157
+ import { createHash, X509Certificate } from "node:crypto";
4158
+ function splitRespectingQuotes(text, delimiter) {
4159
+ const parts = [];
4160
+ let current = [];
4161
+ let inQuotes = false;
4162
+ let i = 0;
4163
+ while (i < text.length) {
4164
+ const ch = text[i];
4165
+ if (ch === '"') {
4166
+ inQuotes = !inQuotes;
4167
+ current.push(ch);
4168
+ } else if (ch === "\\" && inQuotes && i + 1 < text.length) {
4169
+ current.push(ch);
4170
+ current.push(text[i + 1]);
4171
+ i++;
4172
+ } else if (ch === delimiter && !inQuotes) {
4173
+ parts.push(current.join(""));
4174
+ current = [];
4175
+ } else {
4176
+ current.push(ch);
4177
+ }
4178
+ i++;
4179
+ }
4180
+ parts.push(current.join(""));
4181
+ return parts;
4182
+ }
4183
+ function unescapeQuoted(text) {
4184
+ return text.replace(/\\(.)/g, "$1");
4185
+ }
4186
+ function extractCn(subject) {
4187
+ for (const part of subject.split(/(?<!\\),/)) {
4188
+ const trimmed = part.trim();
4189
+ if (trimmed.toUpperCase().startsWith("CN=")) {
4190
+ return trimmed.slice(3);
4191
+ }
4192
+ }
4193
+ return "";
4194
+ }
4195
+ function parseXfcc(headerValue) {
4196
+ const elements = [];
4197
+ for (const rawElement of splitRespectingQuotes(headerValue, ",")) {
4198
+ const trimmed = rawElement.trim();
4199
+ if (!trimmed)
4200
+ continue;
4201
+ const pairs = splitRespectingQuotes(trimmed, ";");
4202
+ const fields = {};
4203
+ for (const pair of pairs) {
4204
+ const p = pair.trim();
4205
+ if (!p)
4206
+ continue;
4207
+ const eqIdx = p.indexOf("=");
4208
+ if (eqIdx < 0)
4209
+ continue;
4210
+ const key = p.slice(0, eqIdx).trim().toLowerCase();
4211
+ let value = p.slice(eqIdx + 1).trim();
4212
+ if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') {
4213
+ value = unescapeQuoted(value.slice(1, -1));
4214
+ }
4215
+ if (key === "cert" || key === "uri" || key === "by") {
4216
+ value = decodeURIComponent(value);
4217
+ }
4218
+ if (key === "dns") {
4219
+ const existing = fields.dns;
4220
+ if (Array.isArray(existing)) {
4221
+ existing.push(value);
4222
+ } else {
4223
+ fields.dns = [value];
4224
+ }
4225
+ } else {
4226
+ fields[key] = value;
4227
+ }
4228
+ }
4229
+ const dns = Array.isArray(fields.dns) ? fields.dns : [];
4230
+ elements.push({
4231
+ hash: typeof fields.hash === "string" ? fields.hash : null,
4232
+ cert: typeof fields.cert === "string" ? fields.cert : null,
4233
+ subject: typeof fields.subject === "string" ? fields.subject : null,
4234
+ uri: typeof fields.uri === "string" ? fields.uri : null,
4235
+ dns,
4236
+ by: typeof fields.by === "string" ? fields.by : null
4237
+ });
4238
+ }
4239
+ return elements;
4240
+ }
4241
+ function mtlsAuthenticateXfcc(options) {
4242
+ const validate = options?.validate;
4243
+ const domain = options?.domain ?? "mtls";
4244
+ const selectElement = options?.selectElement ?? "first";
4245
+ return async function authenticate(request) {
4246
+ const headerValue = request.headers.get("x-forwarded-client-cert");
4247
+ if (!headerValue) {
4248
+ throw new Error("Missing x-forwarded-client-cert header");
4249
+ }
4250
+ const elements = parseXfcc(headerValue);
4251
+ if (elements.length === 0) {
4252
+ throw new Error("Empty x-forwarded-client-cert header");
4253
+ }
4254
+ const element = selectElement === "first" ? elements[0] : elements[elements.length - 1];
4255
+ if (validate) {
4256
+ return validate(element);
4257
+ }
4258
+ const principal = element.subject ? extractCn(element.subject) : "";
4259
+ const claims = {};
4260
+ if (element.hash)
4261
+ claims.hash = element.hash;
4262
+ if (element.subject)
4263
+ claims.subject = element.subject;
4264
+ if (element.uri)
4265
+ claims.uri = element.uri;
4266
+ if (element.dns.length > 0)
4267
+ claims.dns = [...element.dns];
4268
+ if (element.by)
4269
+ claims.by = element.by;
4270
+ return new AuthContext(domain, true, principal, claims);
4271
+ };
4272
+ }
4273
+ function parseCertFromHeader(request, header) {
4274
+ const raw = request.headers.get(header);
4275
+ if (!raw) {
4276
+ throw new Error(`Missing ${header} header`);
4277
+ }
4278
+ const pemStr = decodeURIComponent(raw);
4279
+ if (!pemStr.startsWith("-----BEGIN CERTIFICATE-----")) {
4280
+ throw new Error("Header value is not a PEM certificate");
4281
+ }
4282
+ try {
4283
+ return new X509Certificate(pemStr);
4284
+ } catch (exc) {
4285
+ throw new Error(`Failed to parse PEM certificate: ${exc}`);
4286
+ }
4287
+ }
4288
+ function checkCertExpiry(cert) {
4289
+ const now = new Date;
4290
+ const notBefore = new Date(cert.validFrom);
4291
+ const notAfter = new Date(cert.validTo);
4292
+ if (now < notBefore) {
4293
+ throw new Error("Certificate is not yet valid");
4294
+ }
4295
+ if (now > notAfter) {
4296
+ throw new Error("Certificate has expired");
4297
+ }
4298
+ }
4299
+ function mtlsAuthenticate(options) {
4300
+ const { validate, header = "X-SSL-Client-Cert", checkExpiry = false } = options;
4301
+ return async function authenticate(request) {
4302
+ const cert = parseCertFromHeader(request, header);
4303
+ if (checkExpiry) {
4304
+ checkCertExpiry(cert);
4305
+ }
4306
+ return validate(cert);
4307
+ };
4308
+ }
4309
+ var SUPPORTED_ALGORITHMS = new Set(["sha256", "sha1", "sha384", "sha512"]);
4310
+ function mtlsAuthenticateFingerprint(options) {
4311
+ const { fingerprints, header, algorithm = "sha256", checkExpiry } = options;
4312
+ if (!SUPPORTED_ALGORITHMS.has(algorithm)) {
4313
+ throw new Error(`Unsupported hash algorithm: ${algorithm}`);
4314
+ }
4315
+ const entries = fingerprints instanceof Map ? fingerprints : new Map(Object.entries(fingerprints));
4316
+ function validate(cert) {
4317
+ const fp = createHash(algorithm).update(cert.raw).digest("hex");
4318
+ const ctx = entries.get(fp);
4319
+ if (!ctx) {
4320
+ throw new Error(`Unknown certificate fingerprint: ${fp}`);
4321
+ }
4322
+ return ctx;
4323
+ }
4324
+ return mtlsAuthenticate({ validate, header, checkExpiry });
4325
+ }
4326
+ function mtlsAuthenticateSubject(options) {
4327
+ const { header, domain = "mtls", allowedSubjects = null, checkExpiry } = options ?? {};
4328
+ function validate(cert) {
4329
+ const subjectParts = cert.subject.split(`
4330
+ `).map((s) => s.trim()).filter(Boolean);
4331
+ const subjectDn = subjectParts.join(", ");
4332
+ let cn = "";
4333
+ for (const part of subjectParts) {
4334
+ if (part.toUpperCase().startsWith("CN=")) {
4335
+ cn = part.slice(3);
4336
+ break;
4337
+ }
4338
+ }
4339
+ if (allowedSubjects !== null && !allowedSubjects.has(cn)) {
4340
+ throw new Error(`Subject CN '${cn}' not in allowed subjects`);
4341
+ }
4342
+ const serialHex = BigInt(`0x${cert.serialNumber}`).toString(16);
4343
+ const notValidAfter = new Date(cert.validTo).toISOString();
4344
+ return new AuthContext(domain, true, cn, {
4345
+ subject_dn: subjectDn,
4346
+ serial: serialHex,
4347
+ not_valid_after: notValidAfter
4348
+ });
4349
+ }
4350
+ return mtlsAuthenticate({ validate, header, checkExpiry });
4351
+ }
3511
4352
  // src/protocol.ts
3512
4353
  import { Schema as Schema7 } from "@query-farm/apache-arrow";
3513
4354
 
@@ -3648,7 +4489,7 @@ import { Schema as Schema9 } from "@query-farm/apache-arrow";
3648
4489
  // src/dispatch/stream.ts
3649
4490
  import { Schema as Schema8 } from "@query-farm/apache-arrow";
3650
4491
  var EMPTY_SCHEMA4 = new Schema8([]);
3651
- async function dispatchStream(method, params, writer, reader, serverId, requestId) {
4492
+ async function dispatchStream(method, params, writer, reader, serverId, requestId, externalConfig) {
3652
4493
  const isProducer = !!method.producerFn;
3653
4494
  let state;
3654
4495
  try {
@@ -3716,7 +4557,11 @@ async function dispatchStream(method, params, writer, reader, serverId, requestI
3716
4557
  await method.exchangeFn(state, inputBatch, out);
3717
4558
  }
3718
4559
  for (const emitted of out.batches) {
3719
- stream.write(emitted.batch);
4560
+ let batch = emitted.batch;
4561
+ if (externalConfig) {
4562
+ batch = await maybeExternalizeBatch(batch, externalConfig);
4563
+ }
4564
+ stream.write(batch);
3720
4565
  }
3721
4566
  if (out.finished) {
3722
4567
  break;
@@ -3732,12 +4577,15 @@ async function dispatchStream(method, params, writer, reader, serverId, requestI
3732
4577
  }
3733
4578
 
3734
4579
  // src/dispatch/unary.ts
3735
- async function dispatchUnary(method, params, writer, serverId, requestId) {
4580
+ async function dispatchUnary(method, params, writer, serverId, requestId, externalConfig) {
3736
4581
  const schema = method.resultSchema;
3737
4582
  const out = new OutputCollector(schema, true, serverId, requestId);
3738
4583
  try {
3739
4584
  const result = await method.handler(params, out);
3740
- const resultBatch = buildResultBatch(schema, result, serverId, requestId);
4585
+ let resultBatch = buildResultBatch(schema, result, serverId, requestId);
4586
+ if (externalConfig) {
4587
+ resultBatch = await maybeExternalizeBatch(resultBatch, externalConfig);
4588
+ }
3741
4589
  const batches = [...out.batches.map((b) => b.batch), resultBatch];
3742
4590
  writer.writeStream(schema, batches);
3743
4591
  } catch (error) {
@@ -3748,7 +4596,7 @@ async function dispatchUnary(method, params, writer, serverId, requestId) {
3748
4596
 
3749
4597
  // src/wire/writer.ts
3750
4598
  import { writeSync } from "node:fs";
3751
- import { RecordBatchStreamWriter as RecordBatchStreamWriter4 } from "@query-farm/apache-arrow";
4599
+ import { RecordBatchStreamWriter as RecordBatchStreamWriter5 } from "@query-farm/apache-arrow";
3752
4600
  var STDOUT_FD = 1;
3753
4601
  function writeAll(fd, data) {
3754
4602
  let offset = 0;
@@ -3774,7 +4622,7 @@ class IpcStreamWriter {
3774
4622
  this.fd = fd;
3775
4623
  }
3776
4624
  writeStream(schema, batches) {
3777
- const writer = new RecordBatchStreamWriter4;
4625
+ const writer = new RecordBatchStreamWriter5;
3778
4626
  writer.reset(undefined, schema);
3779
4627
  for (const batch of batches) {
3780
4628
  writer._writeRecordBatch(batch);
@@ -3794,7 +4642,7 @@ class IncrementalStream {
3794
4642
  closed = false;
3795
4643
  constructor(fd, schema) {
3796
4644
  this.fd = fd;
3797
- this.writer = new RecordBatchStreamWriter4;
4645
+ this.writer = new RecordBatchStreamWriter5;
3798
4646
  this.writer.reset(undefined, schema);
3799
4647
  this.drain();
3800
4648
  }
@@ -3828,10 +4676,14 @@ class VgiRpcServer {
3828
4676
  enableDescribe;
3829
4677
  serverId;
3830
4678
  describeBatch = null;
4679
+ dispatchHook = null;
4680
+ externalConfig;
3831
4681
  constructor(protocol, options) {
3832
4682
  this.protocol = protocol;
3833
4683
  this.enableDescribe = options?.enableDescribe ?? true;
3834
4684
  this.serverId = options?.serverId ?? crypto.randomUUID().replace(/-/g, "").slice(0, 12);
4685
+ this.dispatchHook = options?.dispatchHook ?? null;
4686
+ this.externalConfig = options?.externalLocation;
3835
4687
  if (this.enableDescribe) {
3836
4688
  const { batch } = buildDescribeBatch(protocol.name, protocol.getMethods(), this.serverId);
3837
4689
  this.describeBatch = batch;
@@ -3901,10 +4753,29 @@ class VgiRpcServer {
3901
4753
  writer.writeStream(EMPTY_SCHEMA5, [errBatch]);
3902
4754
  return;
3903
4755
  }
3904
- if (method.type === "unary" /* UNARY */) {
3905
- await dispatchUnary(method, params, writer, this.serverId, requestId);
3906
- } else {
3907
- await dispatchStream(method, params, writer, reader, this.serverId, requestId);
4756
+ const methodType = method.type === "unary" /* UNARY */ ? "unary" : "stream";
4757
+ const info = { method: methodName, methodType, serverId: this.serverId, requestId };
4758
+ const stats = {
4759
+ inputBatches: 0,
4760
+ outputBatches: 0,
4761
+ inputRows: 0,
4762
+ outputRows: 0,
4763
+ inputBytes: 0,
4764
+ outputBytes: 0
4765
+ };
4766
+ const token = this.dispatchHook?.onDispatchStart(info);
4767
+ let dispatchError;
4768
+ try {
4769
+ if (method.type === "unary" /* UNARY */) {
4770
+ await dispatchUnary(method, params, writer, this.serverId, requestId, this.externalConfig);
4771
+ } else {
4772
+ await dispatchStream(method, params, writer, reader, this.serverId, requestId, this.externalConfig);
4773
+ }
4774
+ } catch (e) {
4775
+ dispatchError = e instanceof Error ? e : new Error(String(e));
4776
+ throw e;
4777
+ } finally {
4778
+ this.dispatchHook?.onDispatchEnd(token, info, stats, dispatchError);
3908
4779
  }
3909
4780
  }
3910
4781
  }
@@ -3913,15 +4784,30 @@ export {
3913
4784
  toSchema,
3914
4785
  subprocessConnect,
3915
4786
  str,
4787
+ resolveExternalLocation,
3916
4788
  pipeConnect,
4789
+ parseXfcc,
4790
+ parseUseIdTokenAsBearer,
3917
4791
  parseResourceMetadataUrl,
4792
+ parseDeviceCodeClientSecret,
4793
+ parseDeviceCodeClientId,
3918
4794
  parseDescribeResponse,
4795
+ parseClientSecret,
4796
+ parseClientId,
3919
4797
  oauthResourceMetadataToJson,
4798
+ mtlsAuthenticateXfcc,
4799
+ mtlsAuthenticateSubject,
4800
+ mtlsAuthenticateFingerprint,
4801
+ mtlsAuthenticate,
4802
+ maybeExternalizeBatch,
4803
+ makeExternalLocationBatch,
3920
4804
  jwtAuthenticate,
3921
4805
  jsonStateSerializer,
4806
+ isExternalLocationBatch,
3922
4807
  int32,
3923
4808
  int,
3924
4809
  inferParamTypes,
4810
+ httpsOnlyValidator,
3925
4811
  httpOAuthMetadata,
3926
4812
  httpIntrospect,
3927
4813
  httpConnect,
@@ -3929,8 +4815,11 @@ export {
3929
4815
  float,
3930
4816
  fetchOAuthMetadata,
3931
4817
  createHttpHandler,
4818
+ chainAuthenticate,
3932
4819
  bytes,
3933
4820
  bool,
4821
+ bearerAuthenticateStatic,
4822
+ bearerAuthenticate,
3934
4823
  VgiRpcServer,
3935
4824
  VersionError,
3936
4825
  STATE_KEY,
@@ -3956,4 +4845,4 @@ export {
3956
4845
  ARROW_CONTENT_TYPE
3957
4846
  };
3958
4847
 
3959
- //# debugId=FCA96ECA4C5D644864756E2164756E21
4848
+ //# debugId=37CD76108224132A64756E2164756E21