@query-farm/vgi 0.39.0 → 0.41.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.
@@ -4676,7 +4676,7 @@ var arrowStateSerializer = {
4676
4676
  };
4677
4677
 
4678
4678
  // src/protocol/dispatch.ts
4679
- import { Protocol } from "@query-farm/vgi-rpc";
4679
+ import { Protocol as Protocol2 } from "@query-farm/vgi-rpc";
4680
4680
 
4681
4681
  // src/protocol/handlers/function.ts
4682
4682
  init_arrow();
@@ -6495,6 +6495,8 @@ var ATTACH_AAD_PREFIX = _UTF8.encode("vgi.attach_opaque_data.v1\x00");
6495
6495
  var TRANSACTION_AAD_PREFIX = _UTF8.encode("vgi.transaction_opaque_data.v1\x00");
6496
6496
 
6497
6497
  class OpaqueDataRejectedError extends Error {
6498
+ errorCode = "INVALID_ARGUMENT";
6499
+ errorKind = "opaque_data_not_recognized";
6498
6500
  constructor(field = "opaque data") {
6499
6501
  super(`${field} not recognized`);
6500
6502
  this.name = "OpaqueDataRejectedError";
@@ -6530,8 +6532,11 @@ function attachAad(auth) {
6530
6532
  function transactionAad(auth, attachEnvelope) {
6531
6533
  return concat(TRANSACTION_AAD_PREFIX, identityTail(auth), Uint8Array.of(0), attachEnvelope);
6532
6534
  }
6533
- async function sealBytes(payload, key, aad, version) {
6534
- const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LEN));
6535
+ async function sealBytes(payload, key, aad, version, fixedNonce) {
6536
+ if (fixedNonce !== undefined && fixedNonce.length !== NONCE_LEN) {
6537
+ throw new Error(`nonce must be ${NONCE_LEN} bytes`);
6538
+ }
6539
+ const nonce = fixedNonce ?? crypto.getRandomValues(new Uint8Array(NONCE_LEN));
6535
6540
  const ciphertext = xchacha20poly1305(await normalizeKey(key), nonce, aad).encrypt(payload);
6536
6541
  const out = new Uint8Array(1 + NONCE_LEN + ciphertext.length);
6537
6542
  out[0] = version & 255;
@@ -8419,6 +8424,526 @@ class CatalogInterface {
8419
8424
  transactionRollback(attachOpaqueData, transactionOpaqueData) {}
8420
8425
  }
8421
8426
 
8427
+ // src/attach-ticket.ts
8428
+ init_arrow();
8429
+ import {
8430
+ Protocol,
8431
+ badRequest,
8432
+ errorInfo,
8433
+ preconditionFailure
8434
+ } from "@query-farm/vgi-rpc";
8435
+
8436
+ // src/catalog/attach-option.ts
8437
+ init_arrow();
8438
+ var RESERVED_ATTACH_OPTION = "vgi_attach_ticket";
8439
+ function assertNoReservedAttachOption(specs) {
8440
+ for (const spec of specs) {
8441
+ if (spec.name.toLowerCase() === RESERVED_ATTACH_OPTION) {
8442
+ throw new Error(`Attach option '${spec.name}' uses the reserved name '${RESERVED_ATTACH_OPTION}': the framework ` + "reads it as an attach ticket before any catalog code runs. Rename the option.");
8443
+ }
8444
+ }
8445
+ }
8446
+ var SPEC_SCHEMA = schema2([
8447
+ field2("name", utf82(), false),
8448
+ field2("description", utf82(), false),
8449
+ field2("type", binary2(), false),
8450
+ field2("default_value", binary2(), true),
8451
+ field2("required", bool2(), true),
8452
+ field2("secret", bool2(), true)
8453
+ ]);
8454
+ function serializeAttachOptionSpec(spec) {
8455
+ assertNoReservedAttachOption([spec]);
8456
+ if (spec.required && spec.default !== undefined && spec.default !== null) {
8457
+ throw new Error(`Attach option '${spec.name}' is required but also declares a default ` + `(${JSON.stringify(spec.default)}); an option with a default is always ` + "satisfiable without the caller. Drop one.");
8458
+ }
8459
+ const typeSchema = schema2([field2("value", spec.type, true)]);
8460
+ const typeBytes = serializeSchema(typeSchema);
8461
+ let defaultBytes = null;
8462
+ if (spec.default !== undefined && spec.default !== null) {
8463
+ const defaultBatch = batchFromColumns({ value: [spec.default] }, typeSchema);
8464
+ defaultBytes = serializeBatch(defaultBatch);
8465
+ }
8466
+ const batch = batchFromColumns({
8467
+ name: [spec.name],
8468
+ description: [spec.description],
8469
+ type: [typeBytes],
8470
+ default_value: [defaultBytes],
8471
+ required: [spec.required ?? false],
8472
+ secret: [spec.secret ?? false]
8473
+ }, SPEC_SCHEMA);
8474
+ return serializeBatch(batch);
8475
+ }
8476
+ function serializeAttachOptionSpecs(specs) {
8477
+ return Array.from(specs, serializeAttachOptionSpec);
8478
+ }
8479
+ function deserializeAttachOptionSpec(bytes) {
8480
+ const row = [...iterRows(deserializeBatch(bytes))][0];
8481
+ if (row === undefined) {
8482
+ throw new Error("Cannot deserialize AttachOptionSpec from an empty batch");
8483
+ }
8484
+ const typeSchema = deserializeSchema(toUint8Array(row.type));
8485
+ const type = typeSchema.fields[0].type;
8486
+ let value;
8487
+ const defaultBytes = row.default_value;
8488
+ if (defaultBytes != null) {
8489
+ const asBytes = toUint8Array(defaultBytes);
8490
+ if (asBytes.length > 0) {
8491
+ const defaultRow = [...iterRows(deserializeBatch(asBytes))][0];
8492
+ value = defaultRow?.value;
8493
+ }
8494
+ }
8495
+ return {
8496
+ name: String(row.name),
8497
+ description: String(row.description ?? ""),
8498
+ type,
8499
+ ...value === undefined || value === null ? {} : { default: value },
8500
+ required: row.required === true,
8501
+ secret: row.secret === true
8502
+ };
8503
+ }
8504
+ function deserializeAttachOptionSpecs(specs) {
8505
+ return Array.from(specs, deserializeAttachOptionSpec);
8506
+ }
8507
+
8508
+ class MissingAttachOptionsError extends Error {
8509
+ missing;
8510
+ constructor(catalogName, missing) {
8511
+ const joined = missing.map((name) => `'${name}'`).join(", ");
8512
+ super(`Catalog '${catalogName}' cannot be attached without the required ` + `option${missing.length > 1 ? "s" : ""} ${joined}.`);
8513
+ this.name = "MissingAttachOptionsError";
8514
+ this.missing = missing;
8515
+ }
8516
+ }
8517
+ function validateRequiredAttachOptions(catalogName, specs, options) {
8518
+ specs = Array.from(specs);
8519
+ assertNoReservedAttachOption(specs);
8520
+ const supplied = new Set(Object.keys(options).map((key) => key.toLowerCase()));
8521
+ const missing = Array.from(specs).filter((spec) => spec.required && !supplied.has(spec.name.toLowerCase())).map((spec) => spec.name);
8522
+ if (missing.length > 0) {
8523
+ throw new MissingAttachOptionsError(catalogName, missing);
8524
+ }
8525
+ }
8526
+
8527
+ class ReservedAttachOptionError extends Error {
8528
+ constructor(message) {
8529
+ super(message);
8530
+ this.name = "ReservedAttachOptionError";
8531
+ }
8532
+ }
8533
+ async function checkReservedAttachOptions(catalog) {
8534
+ if (!catalog?.catalogsInfo)
8535
+ return;
8536
+ let infos;
8537
+ try {
8538
+ infos = await catalog.catalogsInfo();
8539
+ } catch (e) {
8540
+ if (e instanceof Error && e.message.includes(`reserved name '${RESERVED_ATTACH_OPTION}'`)) {
8541
+ throw new ReservedAttachOptionError(e.message);
8542
+ }
8543
+ return;
8544
+ }
8545
+ for (const info of infos ?? []) {
8546
+ for (const raw of info.attach_option_specs ?? []) {
8547
+ let name;
8548
+ try {
8549
+ name = deserializeAttachOptionSpec(toUint8Array(raw)).name;
8550
+ } catch {
8551
+ continue;
8552
+ }
8553
+ try {
8554
+ assertNoReservedAttachOption([{ name }]);
8555
+ } catch (e) {
8556
+ throw new ReservedAttachOptionError(e.message);
8557
+ }
8558
+ }
8559
+ }
8560
+ }
8561
+
8562
+ // src/attach-ticket.ts
8563
+ var ATTACH_TICKET_PREFIX = "vgia1.";
8564
+ var ATTACH_TICKET_OPTION = RESERVED_ATTACH_OPTION;
8565
+ var ATTACH_TICKETS_PROTOCOL_NAME = "vgi.attach_tickets.v1";
8566
+ var ATTACH_TICKETS_PROTOCOL_VERSION = "1.0.0";
8567
+ var TICKET_ENVELOPE_VERSION = 1;
8568
+ var MAX_OPTIONS_BYTES = 16 * 1024;
8569
+ var MAX_TICKET_CHARS = 32 * 1024;
8570
+ var CLOCK_SKEW_SECONDS = 60;
8571
+ var UTF82 = new TextEncoder;
8572
+ var STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
8573
+ var TICKET_AAD_DOMAIN = UTF82.encode("vgi.attach_ticket.v1\x00");
8574
+ var TICKET_ID = /^[0-9a-f]{32}$/;
8575
+ var B64URL = /^[A-Za-z0-9_-]+$/;
8576
+ var MAX_TEXT = 65535;
8577
+
8578
+ class AttachTicketInvalidError extends Error {
8579
+ static errorKind = "attach_ticket_invalid";
8580
+ errorKind = "attach_ticket_invalid";
8581
+ errorCode = "INVALID_ARGUMENT";
8582
+ errorDetails;
8583
+ constructor(detail = "attach ticket not accepted") {
8584
+ super(detail);
8585
+ this.name = "AttachTicketInvalidError";
8586
+ this.errorDetails = [badRequest([{ field: ATTACH_TICKET_OPTION, description: detail }])];
8587
+ }
8588
+ }
8589
+
8590
+ class AttachTicketExpiredError extends Error {
8591
+ static errorKind = "attach_ticket_expired";
8592
+ errorKind = "attach_ticket_expired";
8593
+ errorCode = "FAILED_PRECONDITION";
8594
+ errorDetails;
8595
+ constructor(detail = "attach ticket has expired") {
8596
+ super(detail);
8597
+ this.name = "AttachTicketExpiredError";
8598
+ this.errorDetails = [
8599
+ preconditionFailure([{ type: "ATTACH_TICKET", subject: ATTACH_TICKET_OPTION, description: detail }])
8600
+ ];
8601
+ }
8602
+ }
8603
+
8604
+ class AttachTicketRequestError extends Error {
8605
+ errorKind = "invalid_request";
8606
+ errorCode = "INVALID_ARGUMENT";
8607
+ errorDetails;
8608
+ constructor(message, violations) {
8609
+ super(message);
8610
+ this.name = "AttachTicketRequestError";
8611
+ this.errorDetails = [
8612
+ badRequest(violations.map(([f, description]) => ({ field: f, description })))
8613
+ ];
8614
+ }
8615
+ }
8616
+
8617
+ class SealAttachDeniedError extends Error {
8618
+ errorKind = "action_denied";
8619
+ errorCode = "PERMISSION_DENIED";
8620
+ errorDetails = [errorInfo({ action: "seal_attach" })];
8621
+ constructor(message) {
8622
+ super(message);
8623
+ this.name = "SealAttachDeniedError";
8624
+ }
8625
+ }
8626
+ function attachTicketAad(principal) {
8627
+ const p = UTF82.encode(principal);
8628
+ const out = new Uint8Array(TICKET_AAD_DOMAIN.length + p.length);
8629
+ out.set(TICKET_AAD_DOMAIN, 0);
8630
+ out.set(p, TICKET_AAD_DOMAIN.length);
8631
+ return out;
8632
+ }
8633
+ function packText(value, name) {
8634
+ const raw = UTF82.encode(value);
8635
+ if (raw.length > MAX_TEXT)
8636
+ throw new Error(`${name} is longer than 65535 bytes`);
8637
+ const out = new Uint8Array(2 + raw.length);
8638
+ new DataView(out.buffer).setUint16(0, raw.length, true);
8639
+ out.set(raw, 2);
8640
+ return out;
8641
+ }
8642
+ function encodePayload(c) {
8643
+ if (c.optionsIpc.length > MAX_OPTIONS_BYTES) {
8644
+ throw new Error(`options are ${c.optionsIpc.length} bytes; a ticket carries at most ${MAX_OPTIONS_BYTES}`);
8645
+ }
8646
+ const head = new Uint8Array(16);
8647
+ const hv = new DataView(head.buffer);
8648
+ hv.setBigInt64(0, BigInt(c.issuedAt), true);
8649
+ hv.setBigInt64(8, BigInt(c.expiresAt), true);
8650
+ const optLen = new Uint8Array(4);
8651
+ new DataView(optLen.buffer).setUint32(0, c.optionsIpc.length, true);
8652
+ const parts = [
8653
+ head,
8654
+ packText(c.ticketId, "ticket_id"),
8655
+ packText(c.catalogName, "catalog_name"),
8656
+ packText(c.dataVersionSpec, "data_version_spec"),
8657
+ packText(c.implementationVersion, "implementation_version"),
8658
+ optLen,
8659
+ c.optionsIpc
8660
+ ];
8661
+ let total = 0;
8662
+ for (const p of parts)
8663
+ total += p.length;
8664
+ const out = new Uint8Array(total);
8665
+ let off = 0;
8666
+ for (const p of parts) {
8667
+ out.set(p, off);
8668
+ off += p.length;
8669
+ }
8670
+ return out;
8671
+ }
8672
+ function decodePayload(payload) {
8673
+ const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
8674
+ let pos = 0;
8675
+ const need = (n) => {
8676
+ if (pos + n > payload.length)
8677
+ throw new AttachTicketInvalidError("attach ticket payload is truncated");
8678
+ };
8679
+ const text = () => {
8680
+ need(2);
8681
+ const len = view.getUint16(pos, true);
8682
+ pos += 2;
8683
+ need(len);
8684
+ const raw = payload.subarray(pos, pos + len);
8685
+ pos += len;
8686
+ try {
8687
+ return STRICT_UTF8.decode(raw);
8688
+ } catch {
8689
+ throw new AttachTicketInvalidError("attach ticket payload is not UTF-8");
8690
+ }
8691
+ };
8692
+ need(16);
8693
+ const issuedAt = Number(view.getBigInt64(0, true));
8694
+ const expiresAt = Number(view.getBigInt64(8, true));
8695
+ pos = 16;
8696
+ const ticketId = text();
8697
+ const catalogName = text();
8698
+ const dataVersionSpec = text();
8699
+ const implementationVersion = text();
8700
+ need(4);
8701
+ const optionsLen = view.getUint32(pos, true);
8702
+ pos += 4;
8703
+ if (optionsLen > MAX_OPTIONS_BYTES)
8704
+ throw new AttachTicketInvalidError("attach ticket options exceed 16 KiB");
8705
+ need(optionsLen);
8706
+ const optionsIpc = payload.slice(pos, pos + optionsLen);
8707
+ pos += optionsLen;
8708
+ if (pos !== payload.length)
8709
+ throw new AttachTicketInvalidError("attach ticket payload has trailing bytes");
8710
+ if (!TICKET_ID.test(ticketId))
8711
+ throw new AttachTicketInvalidError("attach ticket id is not 32 lowercase hex");
8712
+ if (!catalogName)
8713
+ throw new AttachTicketInvalidError("attach ticket names no catalog");
8714
+ if (expiresAt !== 0 && expiresAt <= issuedAt)
8715
+ throw new AttachTicketInvalidError("attach ticket lifetime is empty");
8716
+ return { issuedAt, expiresAt, ticketId, catalogName, dataVersionSpec, implementationVersion, optionsIpc };
8717
+ }
8718
+ function b64url(data) {
8719
+ let bin = "";
8720
+ for (let i = 0;i < data.length; i++)
8721
+ bin += String.fromCharCode(data[i]);
8722
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
8723
+ }
8724
+ function b64urlStrict(text) {
8725
+ if (!B64URL.test(text) || text.length % 4 === 1) {
8726
+ throw new AttachTicketInvalidError("attach ticket is not unpadded base64url");
8727
+ }
8728
+ const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - text.length % 4) % 4);
8729
+ const bin = atob(padded);
8730
+ const raw = new Uint8Array(bin.length);
8731
+ for (let i = 0;i < bin.length; i++)
8732
+ raw[i] = bin.charCodeAt(i);
8733
+ if (b64url(raw) !== text)
8734
+ throw new AttachTicketInvalidError("attach ticket is not canonical base64url");
8735
+ return raw;
8736
+ }
8737
+ function randomHex(bytes) {
8738
+ const b = crypto.getRandomValues(new Uint8Array(bytes));
8739
+ let s = "";
8740
+ for (const x of b)
8741
+ s += x.toString(16).padStart(2, "0");
8742
+ return s;
8743
+ }
8744
+ async function mintAttachTicket(signingKey, opts) {
8745
+ if (!opts.principal)
8746
+ throw new Error("a ticket needs a principal");
8747
+ if (!opts.catalogName)
8748
+ throw new Error("a ticket needs a catalog name");
8749
+ const claims = {
8750
+ issuedAt: Math.trunc(opts.issuedAt),
8751
+ expiresAt: Math.trunc(opts.expiresAt),
8752
+ ticketId: opts.ticketId ?? randomHex(16),
8753
+ catalogName: opts.catalogName,
8754
+ dataVersionSpec: opts.dataVersionSpec ?? "",
8755
+ implementationVersion: opts.implementationVersion ?? "",
8756
+ optionsIpc: opts.optionsIpc
8757
+ };
8758
+ if (!TICKET_ID.test(claims.ticketId))
8759
+ throw new Error("ticket_id must be 32 lowercase hex");
8760
+ if (claims.expiresAt !== 0 && claims.expiresAt <= claims.issuedAt) {
8761
+ throw new Error("expires_at must be 0 or after issued_at");
8762
+ }
8763
+ const envelope = await sealBytes(encodePayload(claims), signingKey, attachTicketAad(opts.principal), TICKET_ENVELOPE_VERSION, opts.nonce);
8764
+ const token = ATTACH_TICKET_PREFIX + b64url(envelope);
8765
+ if (token.length > MAX_TICKET_CHARS) {
8766
+ throw new Error(`the ticket would be ${token.length} characters; at most ${MAX_TICKET_CHARS} are accepted`);
8767
+ }
8768
+ return { token, claims };
8769
+ }
8770
+ async function openAttachTicket(signingKey, token, principal, now) {
8771
+ if (!token.startsWith(ATTACH_TICKET_PREFIX))
8772
+ throw new AttachTicketInvalidError("not an attach ticket");
8773
+ if (token.length > MAX_TICKET_CHARS)
8774
+ throw new AttachTicketInvalidError("attach ticket is too long");
8775
+ const envelope = b64urlStrict(token.slice(ATTACH_TICKET_PREFIX.length));
8776
+ if (!principal)
8777
+ throw new AttachTicketInvalidError("an anonymous caller cannot redeem an attach ticket");
8778
+ let payload;
8779
+ try {
8780
+ payload = await openBytes(envelope, signingKey, attachTicketAad(principal), TICKET_ENVELOPE_VERSION);
8781
+ } catch (e) {
8782
+ if (e instanceof OpaqueDataRejectedError) {
8783
+ throw new AttachTicketInvalidError("attach ticket failed verification");
8784
+ }
8785
+ throw e;
8786
+ }
8787
+ const claims = decodePayload(payload);
8788
+ const current = now ?? Date.now() / 1000;
8789
+ if (claims.issuedAt > current + CLOCK_SKEW_SECONDS)
8790
+ throw new AttachTicketExpiredError("attach ticket is not yet valid");
8791
+ if (claims.expiresAt !== 0 && current >= claims.expiresAt + CLOCK_SKEW_SECONDS) {
8792
+ throw new AttachTicketExpiredError("attach ticket has expired");
8793
+ }
8794
+ return claims;
8795
+ }
8796
+ function callerPrincipal(auth) {
8797
+ if (!auth || !auth.authenticated)
8798
+ return null;
8799
+ return auth.principal || null;
8800
+ }
8801
+ async function redeemAttachTicket(options, signingKey, auth, now) {
8802
+ const keys = Object.keys(options);
8803
+ const ticketKeys = keys.filter((k) => k.toLowerCase() === ATTACH_TICKET_OPTION);
8804
+ if (ticketKeys.length === 0)
8805
+ return null;
8806
+ const others = keys.filter((k) => k !== ticketKeys[0]);
8807
+ if (others.length > 0) {
8808
+ throw new AttachTicketRequestError(`${ATTACH_TICKET_OPTION} must be the only attach option`, others.map((k) => [`options.${k}`, `not allowed alongside ${ATTACH_TICKET_OPTION}`]));
8809
+ }
8810
+ const token = options[ticketKeys[0]];
8811
+ if (typeof token !== "string")
8812
+ throw new AttachTicketInvalidError(`${ATTACH_TICKET_OPTION} must be a string`);
8813
+ if (!signingKey)
8814
+ throw new AttachTicketInvalidError("this worker does not redeem attach tickets");
8815
+ const claims = await openAttachTicket(signingKey, token, callerPrincipal(auth), now);
8816
+ let restored = {};
8817
+ if (claims.optionsIpc.length > 0) {
8818
+ try {
8819
+ restored = decodeOptionsBatch(claims.optionsIpc);
8820
+ } catch {
8821
+ throw new AttachTicketInvalidError("attach ticket options are not an Arrow IPC record");
8822
+ }
8823
+ }
8824
+ return {
8825
+ name: claims.catalogName,
8826
+ optionsIpc: claims.optionsIpc.length > 0 ? claims.optionsIpc : null,
8827
+ options: restored,
8828
+ dataVersionSpec: claims.dataVersionSpec || null,
8829
+ implementationVersion: claims.implementationVersion || null
8830
+ };
8831
+ }
8832
+ var SEAL_ATTACH_REQUEST_SCHEMA = schema2([
8833
+ field2("catalog_name", utf82(), false),
8834
+ field2("options", binary2(), true),
8835
+ field2("data_version_spec", utf82(), false),
8836
+ field2("implementation_version", utf82(), false),
8837
+ field2("ttl_seconds", int642(), false)
8838
+ ]);
8839
+ var ATTACH_TICKET_SCHEMA = schema2([
8840
+ field2("ticket", utf82(), false),
8841
+ field2("expires_at", float642(), false)
8842
+ ]);
8843
+ function resolveTicketMaxTtl(grantKeys, env) {
8844
+ if (grantKeys)
8845
+ return Math.trunc(grantKeys.maxTtlSeconds);
8846
+ const raw = (env?.VGI_RPC_GRANT_MAX_TTL_SECONDS ?? "").trim();
8847
+ if (!raw)
8848
+ return null;
8849
+ const value = /^\d+$/.test(raw) ? Number(raw) : 0;
8850
+ if (!(value > 0))
8851
+ throw new Error(`VGI_RPC_GRANT_MAX_TTL_SECONDS=${JSON.stringify(raw)} must be a positive integer`);
8852
+ return value;
8853
+ }
8854
+ async function declaredSpecs(catalog, catalogName) {
8855
+ if (!catalog)
8856
+ return null;
8857
+ const infos = catalog.catalogsInfo ? await catalog.catalogsInfo() : catalog.catalogs().map((name) => ({ name, attach_option_specs: [] }));
8858
+ for (const info of infos) {
8859
+ if (info.name === catalogName) {
8860
+ return deserializeAttachOptionSpecs((info.attach_option_specs ?? []).map((b) => toUint8Array(b)));
8861
+ }
8862
+ }
8863
+ return null;
8864
+ }
8865
+ async function sealAttach2(config, request, auth) {
8866
+ const principal = callerPrincipal(auth);
8867
+ if (principal === null)
8868
+ throw new SealAttachDeniedError("an anonymous caller cannot seal an attach ticket");
8869
+ const violations = [];
8870
+ const ttl = Number(request.ttl_seconds ?? 0);
8871
+ if (!Number.isFinite(ttl) || ttl < 0)
8872
+ violations.push(["ttl_seconds", "must be 0 (as long as allowed) or positive"]);
8873
+ const catalogName = String(request.catalog_name ?? "");
8874
+ let optionsIpc = new Uint8Array(0);
8875
+ let options = {};
8876
+ if (request.options != null) {
8877
+ const raw = toUint8Array(request.options);
8878
+ if (raw.length > 0) {
8879
+ const batch = deserializeBatch(raw);
8880
+ if (batch.numRows > 1) {
8881
+ violations.push(["options", "must be a one-row record"]);
8882
+ } else if (batch.numRows === 1) {
8883
+ options = decodeOptionsBatch(raw);
8884
+ optionsIpc = raw;
8885
+ }
8886
+ }
8887
+ }
8888
+ const specs = await declaredSpecs(config.catalogInterface, catalogName);
8889
+ if (specs === null) {
8890
+ violations.push(["catalog_name", `no catalog named ${JSON.stringify(catalogName)}`]);
8891
+ } else {
8892
+ const declared = new Set(specs.map((s) => s.name.toLowerCase()));
8893
+ for (const name of Object.keys(options)) {
8894
+ if (name.toLowerCase() === ATTACH_TICKET_OPTION) {
8895
+ violations.push([`options.${name}`, "a ticket cannot seal another ticket"]);
8896
+ } else if (!declared.has(name.toLowerCase())) {
8897
+ violations.push([`options.${name}`, "not an attach option this catalog declares"]);
8898
+ }
8899
+ }
8900
+ const supplied = new Set(Object.keys(options).map((n) => n.toLowerCase()));
8901
+ for (const spec of specs) {
8902
+ if (spec.required && !supplied.has(spec.name.toLowerCase()))
8903
+ violations.push([`options.${spec.name}`, "required"]);
8904
+ }
8905
+ }
8906
+ if (optionsIpc.length > MAX_OPTIONS_BYTES) {
8907
+ violations.push(["options", `${optionsIpc.length} bytes; a ticket carries at most ${MAX_OPTIONS_BYTES}`]);
8908
+ }
8909
+ if (violations.length > 0)
8910
+ throw new AttachTicketRequestError("seal_attach request is invalid", violations);
8911
+ const issuedAt = Math.trunc(config.now ? config.now() : Date.now() / 1000);
8912
+ const ceiling = config.maxTtlSeconds;
8913
+ const lifetime = ttl === 0 ? ceiling : ceiling === null ? Math.trunc(ttl) : Math.min(Math.trunc(ttl), ceiling);
8914
+ const expiresAt = lifetime === null ? 0 : issuedAt + lifetime;
8915
+ let token;
8916
+ try {
8917
+ ({ token } = await mintAttachTicket(config.signingKey, {
8918
+ principal,
8919
+ catalogName,
8920
+ optionsIpc,
8921
+ dataVersionSpec: String(request.data_version_spec ?? ""),
8922
+ implementationVersion: String(request.implementation_version ?? ""),
8923
+ issuedAt,
8924
+ expiresAt
8925
+ }));
8926
+ } catch (e) {
8927
+ throw new AttachTicketRequestError("seal_attach request is invalid", [["request", e.message]]);
8928
+ }
8929
+ return { ticket: token, expires_at: expiresAt === 0 ? Number.POSITIVE_INFINITY : expiresAt };
8930
+ }
8931
+ function buildAttachTicketsProtocol(config) {
8932
+ const p = new Protocol(ATTACH_TICKETS_PROTOCOL_NAME, { protocolVersion: ATTACH_TICKETS_PROTOCOL_VERSION });
8933
+ p.unary("seal_attach", {
8934
+ params: REQUEST_PARAMS_SCHEMA,
8935
+ result: RESULT_BINARY_SCHEMA,
8936
+ doc: "Seal the caller's attach of request.catalog_name into a ticket.",
8937
+ handler: async (params, ctx) => {
8938
+ const request = unwrapRequest(params.request);
8939
+ const sealed = await sealAttach2(config, request, ctx?.auth);
8940
+ const batch = batchFromColumns({ ticket: [sealed.ticket], expires_at: [sealed.expires_at] }, ATTACH_TICKET_SCHEMA);
8941
+ return { result: serializeBatch(batch) };
8942
+ }
8943
+ });
8944
+ return p;
8945
+ }
8946
+
8422
8947
  // src/protocol/handlers/catalog/admin.ts
8423
8948
  function registerCatalogAdminMethods(protocol, getCatalog, signingKey) {
8424
8949
  catalogUnary(protocol, signingKey, "catalog_catalogs", {
@@ -8443,7 +8968,15 @@ function registerCatalogAdminMethods(protocol, getCatalog, signingKey) {
8443
8968
  handler: async (params, ctx) => {
8444
8969
  const innerParams = unwrapRequest(params.request);
8445
8970
  const cat = getCatalog();
8446
- const optionsDict = decodeOptionsBatch(innerParams.options);
8971
+ let optionsDict = decodeOptionsBatch(innerParams.options);
8972
+ const restored = await redeemAttachTicket(optionsDict, signingKey, ctx?.auth ?? currentRequestAuth());
8973
+ if (restored !== null) {
8974
+ innerParams.name = restored.name;
8975
+ innerParams.options = restored.optionsIpc;
8976
+ innerParams.data_version_spec = restored.dataVersionSpec;
8977
+ innerParams.implementation_version = restored.implementationVersion;
8978
+ optionsDict = restored.options;
8979
+ }
8447
8980
  const result = await cat.attach(innerParams.name, optionsDict, innerParams.data_version_spec ?? null, innerParams.implementation_version ?? null);
8448
8981
  const sealedAttach = await sealAttach(toUint8Array(result.attach_opaque_data), ctx?.auth, signingKey);
8449
8982
  return wrapResult(buildCatalogAttachResult({
@@ -8899,7 +9432,7 @@ function registerCatalogMethods(protocol, catalog, _catalogName, signingKey) {
8899
9432
  // src/protocol/dispatch.ts
8900
9433
  var VGI_PROTOCOL_NAME = "vgi.v2";
8901
9434
  function buildVgiProtocol(config) {
8902
- const protocol = new Protocol(VGI_PROTOCOL_NAME, { protocolVersion: "2.1.0" });
9435
+ const protocol = new Protocol2(VGI_PROTOCOL_NAME, { protocolVersion: "2.1.0" });
8903
9436
  registerFunctionMethods(protocol, {
8904
9437
  registry: config.registry,
8905
9438
  signingKey: config.signingKey,
@@ -9016,6 +9549,7 @@ function createLandingRoutes(info) {
9016
9549
 
9017
9550
  // src/rpc-server.ts
9018
9551
  import {
9552
+ GrantKeys,
9019
9553
  IdentityImpl,
9020
9554
  VgiRpcServer
9021
9555
  } from "@query-farm/vgi-rpc";
@@ -9028,11 +9562,20 @@ function buildRpcServer(primary, hosting, options) {
9028
9562
  throw new Error(`Unknown transport ${JSON.stringify(options.transport)}; expected one of ${[...TRANSPORTS].join(", ")}.`);
9029
9563
  }
9030
9564
  const protocols = validatedHostedProtocols(hosting, primary);
9031
- const identity = IDENTITY_TRANSPORTS.has(options.transport) ? buildIdentity(hosting, options.env) : undefined;
9565
+ let identity;
9566
+ if (IDENTITY_TRANSPORTS.has(options.transport)) {
9567
+ const env = options.env ?? processEnv();
9568
+ const grantKeys = hosting.grantKeys === undefined ? GrantKeys.fromEnv(env) : hosting.grantKeys;
9569
+ identity = buildIdentity(hosting, grantKeys, options.env);
9570
+ const tickets = buildAttachTickets(hosting, grantKeys, options.attachTickets, env);
9571
+ if (tickets)
9572
+ protocols.push(tickets);
9573
+ }
9032
9574
  return new VgiRpcServer(primary, {
9033
9575
  serverId: options.serverId,
9034
9576
  protocols,
9035
- identity
9577
+ identity,
9578
+ grantKeys: null
9036
9579
  });
9037
9580
  }
9038
9581
  function validatedHostedProtocols(hosting, primary) {
@@ -9073,17 +9616,29 @@ function validatedHostedProtocols(hosting, primary) {
9073
9616
  }
9074
9617
  return out;
9075
9618
  }
9076
- function buildIdentity(hosting, env) {
9619
+ function buildIdentity(hosting, grantKeys, env) {
9077
9620
  const { resolveToken, mintGrant } = hosting;
9078
- if (!resolveToken && !mintGrant)
9621
+ if (!resolveToken && !mintGrant && !grantKeys)
9079
9622
  return;
9080
9623
  return new IdentityImpl({
9081
9624
  resolveToken,
9082
9625
  mintGrant,
9626
+ grantKeys: grantKeys ?? undefined,
9083
9627
  introspectPrincipals: resolveToken ? resolveIntrospectPrincipals(hosting.introspectPrincipals, env) : undefined,
9084
9628
  maxAuthAge: hosting.maxAuthAge
9085
9629
  });
9086
9630
  }
9631
+ function buildAttachTickets(hosting, grantKeys, config, env) {
9632
+ if (!config || !config.signingKeyConfigured || !config.signingKey)
9633
+ return;
9634
+ if (!grantKeys && !hosting.mintGrant)
9635
+ return;
9636
+ return buildAttachTicketsProtocol({
9637
+ signingKey: config.signingKey,
9638
+ catalogInterface: config.catalogInterface,
9639
+ maxTtlSeconds: resolveTicketMaxTtl(grantKeys, env)
9640
+ });
9641
+ }
9087
9642
  function resolveIntrospectPrincipals(explicit, env) {
9088
9643
  let principals;
9089
9644
  if (explicit !== undefined) {
@@ -9100,6 +9655,9 @@ function resolveIntrospectPrincipals(explicit, env) {
9100
9655
  }
9101
9656
  return principals;
9102
9657
  }
9658
+ function processEnv() {
9659
+ return globalThis.process?.env;
9660
+ }
9103
9661
 
9104
9662
  // src/http/fetch.ts
9105
9663
  var requestAuthStorage;
@@ -9139,7 +9697,17 @@ function createVgiFetch(opts) {
9139
9697
  signingKey: opts.signingKey,
9140
9698
  ...opts.protocol
9141
9699
  });
9142
- const server = buildRpcServer(protocol, opts, { transport: "http", serverId });
9700
+ const server = buildRpcServer(protocol, opts, {
9701
+ transport: "http",
9702
+ serverId,
9703
+ attachTickets: {
9704
+ signingKey: opts.signingKey,
9705
+ signingKeyConfigured: opts.signingKeyConfigured ?? true,
9706
+ catalogInterface: opts.protocol.catalogInterface
9707
+ }
9708
+ });
9709
+ const reservedCheck = checkReservedAttachOptions(opts.protocol.catalogInterface);
9710
+ reservedCheck.catch((e) => console.error(`VGI worker refused to serve: ${e.message}`));
9143
9711
  const handler = createHttpHandler(server, {
9144
9712
  prefix,
9145
9713
  serverId,
@@ -9157,6 +9725,7 @@ function createVgiFetch(opts) {
9157
9725
  enableLandingPage: false
9158
9726
  });
9159
9727
  return async (req) => {
9728
+ await reservedCheck;
9160
9729
  const storage = await ensureRequestAuthStorage();
9161
9730
  if (!storage)
9162
9731
  return handler(req);
@@ -9167,6 +9736,7 @@ function createVgiFetch(opts) {
9167
9736
  // src/worker-cf-entry.ts
9168
9737
  import {
9169
9738
  AuthUnavailableError as AuthUnavailableError2,
9739
+ GrantKeys as GrantKeys3,
9170
9740
  GrantRefusedError as GrantRefusedError2,
9171
9741
  IdentityUnavailableError as IdentityUnavailableError2,
9172
9742
  StatusError as StatusError2
@@ -10308,57 +10878,6 @@ function resolveMetadata(func) {
10308
10878
  };
10309
10879
  }
10310
10880
 
10311
- // src/catalog/attach-option.ts
10312
- init_arrow();
10313
- var SPEC_SCHEMA = schema2([
10314
- field2("name", utf82(), false),
10315
- field2("description", utf82(), false),
10316
- field2("type", binary2(), false),
10317
- field2("default_value", binary2(), true),
10318
- field2("required", bool2(), true),
10319
- field2("secret", bool2(), true)
10320
- ]);
10321
- function serializeAttachOptionSpec(spec) {
10322
- if (spec.required && spec.default !== undefined && spec.default !== null) {
10323
- throw new Error(`Attach option '${spec.name}' is required but also declares a default ` + `(${JSON.stringify(spec.default)}); an option with a default is always ` + "satisfiable without the caller. Drop one.");
10324
- }
10325
- const typeSchema = schema2([field2("value", spec.type, true)]);
10326
- const typeBytes = serializeSchema(typeSchema);
10327
- let defaultBytes = null;
10328
- if (spec.default !== undefined && spec.default !== null) {
10329
- const defaultBatch = batchFromColumns({ value: [spec.default] }, typeSchema);
10330
- defaultBytes = serializeBatch(defaultBatch);
10331
- }
10332
- const batch = batchFromColumns({
10333
- name: [spec.name],
10334
- description: [spec.description],
10335
- type: [typeBytes],
10336
- default_value: [defaultBytes],
10337
- required: [spec.required ?? false],
10338
- secret: [spec.secret ?? false]
10339
- }, SPEC_SCHEMA);
10340
- return serializeBatch(batch);
10341
- }
10342
- function serializeAttachOptionSpecs(specs) {
10343
- return Array.from(specs, serializeAttachOptionSpec);
10344
- }
10345
- class MissingAttachOptionsError extends Error {
10346
- missing;
10347
- constructor(catalogName, missing) {
10348
- const joined = missing.map((name) => `'${name}'`).join(", ");
10349
- super(`Catalog '${catalogName}' cannot be attached without the required ` + `option${missing.length > 1 ? "s" : ""} ${joined}.`);
10350
- this.name = "MissingAttachOptionsError";
10351
- this.missing = missing;
10352
- }
10353
- }
10354
- function validateRequiredAttachOptions(catalogName, specs, options) {
10355
- const supplied = new Set(Object.keys(options).map((key) => key.toLowerCase()));
10356
- const missing = Array.from(specs).filter((spec) => spec.required && !supplied.has(spec.name.toLowerCase())).map((spec) => spec.name);
10357
- if (missing.length > 0) {
10358
- throw new MissingAttachOptionsError(catalogName, missing);
10359
- }
10360
- }
10361
-
10362
10881
  // src/catalog/read-only.ts
10363
10882
  var READ_ONLY_ITEM_ATTACH_ID = new TextEncoder().encode("readonly-catalog-");
10364
10883
 
@@ -11107,7 +11626,7 @@ class CompositeCatalogInterface extends CatalogInterface {
11107
11626
  const idx = attachOpaqueData[ROUTE_BYTE];
11108
11627
  const route = this._backends[idx];
11109
11628
  if (!route) {
11110
- throw new Error(`CompositeCatalog: no backend at route-byte index ${idx} (have ${this._backends.length})`);
11629
+ throw new OpaqueDataRejectedError("attach_opaque_data");
11111
11630
  }
11112
11631
  return route;
11113
11632
  }
@@ -11566,6 +12085,7 @@ import { str, bytes, int as int3, int32 as int323, float, float32 as float323, b
11566
12085
  import { subprocessConnect, httpConnect, tcpConnect } from "@query-farm/vgi-rpc";
11567
12086
  import {
11568
12087
  AuthUnavailableError,
12088
+ GrantKeys as GrantKeys2,
11569
12089
  GrantRefusedError,
11570
12090
  IdentityUnavailableError,
11571
12091
  StatusError
@@ -11585,6 +12105,7 @@ export {
11585
12105
  EXCHANGE_STATE_SCHEMA,
11586
12106
  FunctionRegistry,
11587
12107
  FunctionType,
12108
+ GrantKeys3 as GrantKeys,
11588
12109
  GrantRefusedError2 as GrantRefusedError,
11589
12110
  IdentityUnavailableError2 as IdentityUnavailableError,
11590
12111
  IntervalUnit2 as IntervalUnit,
@@ -11691,4 +12212,4 @@ export {
11691
12212
  utf82 as utf8
11692
12213
  };
11693
12214
 
11694
- //# debugId=41BE8762862D859564756E2164756E21
12215
+ //# debugId=8A1BBED7A349251A64756E2164756E21