@query-farm/vgi 0.40.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/attach-ticket.d.ts +179 -0
- package/dist/catalog/attach-option.d.ts +30 -0
- package/dist/client-entry.js +1 -1
- package/dist/client-entry.js.map +3 -3
- package/dist/crypto.d.ts +5 -1
- package/dist/http/fetch.d.ts +6 -0
- package/dist/index.core.d.ts +2 -1
- package/dist/index.js +1969 -1446
- package/dist/index.js.map +17 -15
- package/dist/protocol/dispatch.d.ts +13 -0
- package/dist/protocol/handlers/unimplemented.d.ts +17 -0
- package/dist/rpc-server.d.ts +14 -0
- package/dist/serve-entry.js +560 -10
- package/dist/serve-entry.js.map +12 -9
- package/dist/worker-cf-entry.js +606 -62
- package/dist/worker-cf-entry.js.map +13 -11
- package/dist/worker.d.ts +1 -0
- package/package.json +3 -3
- package/src/attach-ticket.ts +611 -0
- package/src/catalog/attach-option.ts +75 -0
- package/src/catalog/composite.ts +5 -1
- package/src/crypto.ts +13 -1
- package/src/http/fetch.ts +23 -1
- package/src/index.core.ts +27 -1
- package/src/protocol/dispatch.ts +16 -0
- package/src/protocol/handlers/catalog/admin.ts +21 -1
- package/src/protocol/handlers/unimplemented.ts +74 -0
- package/src/rpc-server.ts +50 -5
- package/src/serve-entry.ts +4 -0
- package/src/worker.ts +13 -0
package/dist/worker-cf-entry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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({
|
|
@@ -8896,10 +9429,42 @@ function registerCatalogMethods(protocol, catalog, _catalogName, signingKey) {
|
|
|
8896
9429
|
registerCatalogIndexMethods(protocol, getCatalog, signingKey);
|
|
8897
9430
|
}
|
|
8898
9431
|
|
|
9432
|
+
// src/protocol/handlers/unimplemented.ts
|
|
9433
|
+
import { MethodNotImplementedError } from "@query-farm/vgi-rpc";
|
|
9434
|
+
var UNIMPLEMENTED_METHODS = [
|
|
9435
|
+
{ name: "aggregate_streaming_open", params: REQUEST_PARAMS_SCHEMA, result: RESULT_BINARY_SCHEMA },
|
|
9436
|
+
{ name: "aggregate_streaming_chunk", params: REQUEST_PARAMS_SCHEMA, result: RESULT_BINARY_SCHEMA },
|
|
9437
|
+
{ name: "aggregate_streaming_close", params: REQUEST_PARAMS_SCHEMA, result: RESULT_BINARY_SCHEMA },
|
|
9438
|
+
{ name: "aggregate_window_init", params: REQUEST_PARAMS_SCHEMA, result: RESULT_BINARY_SCHEMA },
|
|
9439
|
+
{ name: "aggregate_window", params: REQUEST_PARAMS_SCHEMA, result: RESULT_BINARY_SCHEMA },
|
|
9440
|
+
{ name: "aggregate_window_batch", params: REQUEST_PARAMS_SCHEMA, result: RESULT_BINARY_SCHEMA },
|
|
9441
|
+
{ name: "aggregate_window_destructor", params: REQUEST_PARAMS_SCHEMA, result: RESULT_BINARY_SCHEMA },
|
|
9442
|
+
{ name: "catalog_index_create", params: CatalogIndexCreateParamsSchema, result: emptyResultSchema },
|
|
9443
|
+
{ name: "catalog_index_drop", params: CatalogIndexDropParamsSchema, result: emptyResultSchema },
|
|
9444
|
+
{ name: "catalog_table_column_comment_set", params: CatalogTableColumnCommentSetParamsSchema, result: emptyResultSchema },
|
|
9445
|
+
{ name: "catalog_table_insert_function_get", params: CatalogTableInsertFunctionGetParamsSchema, result: RESULT_BINARY_SCHEMA },
|
|
9446
|
+
{ name: "catalog_table_update_function_get", params: CatalogTableUpdateFunctionGetParamsSchema, result: RESULT_BINARY_SCHEMA },
|
|
9447
|
+
{ name: "catalog_table_delete_function_get", params: CatalogTableDeleteFunctionGetParamsSchema, result: RESULT_BINARY_SCHEMA }
|
|
9448
|
+
];
|
|
9449
|
+
function unimplementedMessage(method) {
|
|
9450
|
+
return `${method} is not implemented by this worker`;
|
|
9451
|
+
}
|
|
9452
|
+
function registerUnimplementedMethods(protocol) {
|
|
9453
|
+
for (const m of UNIMPLEMENTED_METHODS) {
|
|
9454
|
+
protocol.unary(m.name, {
|
|
9455
|
+
params: m.params,
|
|
9456
|
+
result: m.result,
|
|
9457
|
+
handler: async () => {
|
|
9458
|
+
throw new MethodNotImplementedError(unimplementedMessage(m.name));
|
|
9459
|
+
}
|
|
9460
|
+
});
|
|
9461
|
+
}
|
|
9462
|
+
}
|
|
9463
|
+
|
|
8899
9464
|
// src/protocol/dispatch.ts
|
|
8900
9465
|
var VGI_PROTOCOL_NAME = "vgi.v2";
|
|
8901
9466
|
function buildVgiProtocol(config) {
|
|
8902
|
-
const protocol = new
|
|
9467
|
+
const protocol = new Protocol2(VGI_PROTOCOL_NAME, { protocolVersion: "2.1.0" });
|
|
8903
9468
|
registerFunctionMethods(protocol, {
|
|
8904
9469
|
registry: config.registry,
|
|
8905
9470
|
signingKey: config.signingKey,
|
|
@@ -8908,6 +9473,7 @@ function buildVgiProtocol(config) {
|
|
|
8908
9473
|
registerAggregateMethods(protocol, config.registry);
|
|
8909
9474
|
registerTableBufferingMethods(protocol, config.registry, config.signingKey);
|
|
8910
9475
|
registerCatalogMethods(protocol, config.catalogInterface, config.catalogName, config.signingKey);
|
|
9476
|
+
registerUnimplementedMethods(protocol);
|
|
8911
9477
|
return protocol;
|
|
8912
9478
|
}
|
|
8913
9479
|
|
|
@@ -9029,7 +9595,15 @@ function buildRpcServer(primary, hosting, options) {
|
|
|
9029
9595
|
throw new Error(`Unknown transport ${JSON.stringify(options.transport)}; expected one of ${[...TRANSPORTS].join(", ")}.`);
|
|
9030
9596
|
}
|
|
9031
9597
|
const protocols = validatedHostedProtocols(hosting, primary);
|
|
9032
|
-
|
|
9598
|
+
let identity;
|
|
9599
|
+
if (IDENTITY_TRANSPORTS.has(options.transport)) {
|
|
9600
|
+
const env = options.env ?? processEnv();
|
|
9601
|
+
const grantKeys = hosting.grantKeys === undefined ? GrantKeys.fromEnv(env) : hosting.grantKeys;
|
|
9602
|
+
identity = buildIdentity(hosting, grantKeys, options.env);
|
|
9603
|
+
const tickets = buildAttachTickets(hosting, grantKeys, options.attachTickets, env);
|
|
9604
|
+
if (tickets)
|
|
9605
|
+
protocols.push(tickets);
|
|
9606
|
+
}
|
|
9033
9607
|
return new VgiRpcServer(primary, {
|
|
9034
9608
|
serverId: options.serverId,
|
|
9035
9609
|
protocols,
|
|
@@ -9075,9 +9649,8 @@ function validatedHostedProtocols(hosting, primary) {
|
|
|
9075
9649
|
}
|
|
9076
9650
|
return out;
|
|
9077
9651
|
}
|
|
9078
|
-
function buildIdentity(hosting, env) {
|
|
9652
|
+
function buildIdentity(hosting, grantKeys, env) {
|
|
9079
9653
|
const { resolveToken, mintGrant } = hosting;
|
|
9080
|
-
const grantKeys = hosting.grantKeys === undefined ? GrantKeys.fromEnv(env ?? processEnv()) : hosting.grantKeys;
|
|
9081
9654
|
if (!resolveToken && !mintGrant && !grantKeys)
|
|
9082
9655
|
return;
|
|
9083
9656
|
return new IdentityImpl({
|
|
@@ -9088,6 +9661,17 @@ function buildIdentity(hosting, env) {
|
|
|
9088
9661
|
maxAuthAge: hosting.maxAuthAge
|
|
9089
9662
|
});
|
|
9090
9663
|
}
|
|
9664
|
+
function buildAttachTickets(hosting, grantKeys, config, env) {
|
|
9665
|
+
if (!config || !config.signingKeyConfigured || !config.signingKey)
|
|
9666
|
+
return;
|
|
9667
|
+
if (!grantKeys && !hosting.mintGrant)
|
|
9668
|
+
return;
|
|
9669
|
+
return buildAttachTicketsProtocol({
|
|
9670
|
+
signingKey: config.signingKey,
|
|
9671
|
+
catalogInterface: config.catalogInterface,
|
|
9672
|
+
maxTtlSeconds: resolveTicketMaxTtl(grantKeys, env)
|
|
9673
|
+
});
|
|
9674
|
+
}
|
|
9091
9675
|
function resolveIntrospectPrincipals(explicit, env) {
|
|
9092
9676
|
let principals;
|
|
9093
9677
|
if (explicit !== undefined) {
|
|
@@ -9146,7 +9730,17 @@ function createVgiFetch(opts) {
|
|
|
9146
9730
|
signingKey: opts.signingKey,
|
|
9147
9731
|
...opts.protocol
|
|
9148
9732
|
});
|
|
9149
|
-
const server = buildRpcServer(protocol, opts, {
|
|
9733
|
+
const server = buildRpcServer(protocol, opts, {
|
|
9734
|
+
transport: "http",
|
|
9735
|
+
serverId,
|
|
9736
|
+
attachTickets: {
|
|
9737
|
+
signingKey: opts.signingKey,
|
|
9738
|
+
signingKeyConfigured: opts.signingKeyConfigured ?? true,
|
|
9739
|
+
catalogInterface: opts.protocol.catalogInterface
|
|
9740
|
+
}
|
|
9741
|
+
});
|
|
9742
|
+
const reservedCheck = checkReservedAttachOptions(opts.protocol.catalogInterface);
|
|
9743
|
+
reservedCheck.catch((e) => console.error(`VGI worker refused to serve: ${e.message}`));
|
|
9150
9744
|
const handler = createHttpHandler(server, {
|
|
9151
9745
|
prefix,
|
|
9152
9746
|
serverId,
|
|
@@ -9164,6 +9758,7 @@ function createVgiFetch(opts) {
|
|
|
9164
9758
|
enableLandingPage: false
|
|
9165
9759
|
});
|
|
9166
9760
|
return async (req) => {
|
|
9761
|
+
await reservedCheck;
|
|
9167
9762
|
const storage = await ensureRequestAuthStorage();
|
|
9168
9763
|
if (!storage)
|
|
9169
9764
|
return handler(req);
|
|
@@ -10316,57 +10911,6 @@ function resolveMetadata(func) {
|
|
|
10316
10911
|
};
|
|
10317
10912
|
}
|
|
10318
10913
|
|
|
10319
|
-
// src/catalog/attach-option.ts
|
|
10320
|
-
init_arrow();
|
|
10321
|
-
var SPEC_SCHEMA = schema2([
|
|
10322
|
-
field2("name", utf82(), false),
|
|
10323
|
-
field2("description", utf82(), false),
|
|
10324
|
-
field2("type", binary2(), false),
|
|
10325
|
-
field2("default_value", binary2(), true),
|
|
10326
|
-
field2("required", bool2(), true),
|
|
10327
|
-
field2("secret", bool2(), true)
|
|
10328
|
-
]);
|
|
10329
|
-
function serializeAttachOptionSpec(spec) {
|
|
10330
|
-
if (spec.required && spec.default !== undefined && spec.default !== null) {
|
|
10331
|
-
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.");
|
|
10332
|
-
}
|
|
10333
|
-
const typeSchema = schema2([field2("value", spec.type, true)]);
|
|
10334
|
-
const typeBytes = serializeSchema(typeSchema);
|
|
10335
|
-
let defaultBytes = null;
|
|
10336
|
-
if (spec.default !== undefined && spec.default !== null) {
|
|
10337
|
-
const defaultBatch = batchFromColumns({ value: [spec.default] }, typeSchema);
|
|
10338
|
-
defaultBytes = serializeBatch(defaultBatch);
|
|
10339
|
-
}
|
|
10340
|
-
const batch = batchFromColumns({
|
|
10341
|
-
name: [spec.name],
|
|
10342
|
-
description: [spec.description],
|
|
10343
|
-
type: [typeBytes],
|
|
10344
|
-
default_value: [defaultBytes],
|
|
10345
|
-
required: [spec.required ?? false],
|
|
10346
|
-
secret: [spec.secret ?? false]
|
|
10347
|
-
}, SPEC_SCHEMA);
|
|
10348
|
-
return serializeBatch(batch);
|
|
10349
|
-
}
|
|
10350
|
-
function serializeAttachOptionSpecs(specs) {
|
|
10351
|
-
return Array.from(specs, serializeAttachOptionSpec);
|
|
10352
|
-
}
|
|
10353
|
-
class MissingAttachOptionsError extends Error {
|
|
10354
|
-
missing;
|
|
10355
|
-
constructor(catalogName, missing) {
|
|
10356
|
-
const joined = missing.map((name) => `'${name}'`).join(", ");
|
|
10357
|
-
super(`Catalog '${catalogName}' cannot be attached without the required ` + `option${missing.length > 1 ? "s" : ""} ${joined}.`);
|
|
10358
|
-
this.name = "MissingAttachOptionsError";
|
|
10359
|
-
this.missing = missing;
|
|
10360
|
-
}
|
|
10361
|
-
}
|
|
10362
|
-
function validateRequiredAttachOptions(catalogName, specs, options) {
|
|
10363
|
-
const supplied = new Set(Object.keys(options).map((key) => key.toLowerCase()));
|
|
10364
|
-
const missing = Array.from(specs).filter((spec) => spec.required && !supplied.has(spec.name.toLowerCase())).map((spec) => spec.name);
|
|
10365
|
-
if (missing.length > 0) {
|
|
10366
|
-
throw new MissingAttachOptionsError(catalogName, missing);
|
|
10367
|
-
}
|
|
10368
|
-
}
|
|
10369
|
-
|
|
10370
10914
|
// src/catalog/read-only.ts
|
|
10371
10915
|
var READ_ONLY_ITEM_ATTACH_ID = new TextEncoder().encode("readonly-catalog-");
|
|
10372
10916
|
|
|
@@ -11115,7 +11659,7 @@ class CompositeCatalogInterface extends CatalogInterface {
|
|
|
11115
11659
|
const idx = attachOpaqueData[ROUTE_BYTE];
|
|
11116
11660
|
const route = this._backends[idx];
|
|
11117
11661
|
if (!route) {
|
|
11118
|
-
throw new
|
|
11662
|
+
throw new OpaqueDataRejectedError("attach_opaque_data");
|
|
11119
11663
|
}
|
|
11120
11664
|
return route;
|
|
11121
11665
|
}
|
|
@@ -11701,4 +12245,4 @@ export {
|
|
|
11701
12245
|
utf82 as utf8
|
|
11702
12246
|
};
|
|
11703
12247
|
|
|
11704
|
-
//# debugId=
|
|
12248
|
+
//# debugId=CA8AE11CB8003E9464756E2164756E21
|