@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.
- package/README.md +75 -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 +4 -3
- package/dist/index.js +1964 -1447
- package/dist/index.js.map +15 -14
- package/dist/rpc-server.d.ts +37 -1
- package/dist/serve-entry.d.ts +13 -1
- package/dist/serve-entry.js +556 -13
- package/dist/serve-entry.js.map +10 -8
- package/dist/worker-cf-entry.d.ts +1 -1
- package/dist/worker-cf-entry.js +584 -63
- package/dist/worker-cf-entry.js.map +12 -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 +29 -1
- package/src/protocol/handlers/catalog/admin.ts +21 -1
- package/src/rpc-server.ts +102 -3
- package/src/serve-entry.ts +23 -1
- package/src/worker-cf-entry.ts +1 -0
- package/src/worker.ts +13 -0
package/dist/serve-entry.js
CHANGED
|
@@ -517,7 +517,8 @@ import {
|
|
|
517
517
|
irohForwardedHeaderIdentityProvider,
|
|
518
518
|
IROH_FORWARDED_ENDPOINT_HEADER,
|
|
519
519
|
observePeerIdentity,
|
|
520
|
-
peerIdentityPrimary
|
|
520
|
+
peerIdentityPrimary,
|
|
521
|
+
GrantKeys as GrantKeys2
|
|
521
522
|
} from "@query-farm/vgi-rpc";
|
|
522
523
|
|
|
523
524
|
// src/http/fetch.ts
|
|
@@ -3173,7 +3174,7 @@ var arrowStateSerializer = {
|
|
|
3173
3174
|
};
|
|
3174
3175
|
|
|
3175
3176
|
// src/protocol/dispatch.ts
|
|
3176
|
-
import { Protocol } from "@query-farm/vgi-rpc";
|
|
3177
|
+
import { Protocol as Protocol2 } from "@query-farm/vgi-rpc";
|
|
3177
3178
|
|
|
3178
3179
|
// src/util/statistics.ts
|
|
3179
3180
|
function serializeColumnStatistics(stats, _cacheMaxAgeSeconds) {
|
|
@@ -4954,6 +4955,8 @@ var ATTACH_AAD_PREFIX = _UTF8.encode("vgi.attach_opaque_data.v1\x00");
|
|
|
4954
4955
|
var TRANSACTION_AAD_PREFIX = _UTF8.encode("vgi.transaction_opaque_data.v1\x00");
|
|
4955
4956
|
|
|
4956
4957
|
class OpaqueDataRejectedError extends Error {
|
|
4958
|
+
errorCode = "INVALID_ARGUMENT";
|
|
4959
|
+
errorKind = "opaque_data_not_recognized";
|
|
4957
4960
|
constructor(field = "opaque data") {
|
|
4958
4961
|
super(`${field} not recognized`);
|
|
4959
4962
|
this.name = "OpaqueDataRejectedError";
|
|
@@ -4989,8 +4992,11 @@ function attachAad(auth) {
|
|
|
4989
4992
|
function transactionAad(auth, attachEnvelope) {
|
|
4990
4993
|
return concat(TRANSACTION_AAD_PREFIX, identityTail(auth), Uint8Array.of(0), attachEnvelope);
|
|
4991
4994
|
}
|
|
4992
|
-
async function sealBytes(payload, key, aad, version) {
|
|
4993
|
-
|
|
4995
|
+
async function sealBytes(payload, key, aad, version, fixedNonce) {
|
|
4996
|
+
if (fixedNonce !== undefined && fixedNonce.length !== NONCE_LEN) {
|
|
4997
|
+
throw new Error(`nonce must be ${NONCE_LEN} bytes`);
|
|
4998
|
+
}
|
|
4999
|
+
const nonce = fixedNonce ?? crypto.getRandomValues(new Uint8Array(NONCE_LEN));
|
|
4994
5000
|
const ciphertext = xchacha20poly1305(await normalizeKey(key), nonce, aad).encrypt(payload);
|
|
4995
5001
|
const out = new Uint8Array(1 + NONCE_LEN + ciphertext.length);
|
|
4996
5002
|
out[0] = version & 255;
|
|
@@ -6407,6 +6413,479 @@ var encodeCopyFromFormatInfo = (v) => encodeASD(CopyFromFormatInfoSchema, {
|
|
|
6407
6413
|
ordered: v.ordered ?? false
|
|
6408
6414
|
});
|
|
6409
6415
|
|
|
6416
|
+
// src/attach-ticket.ts
|
|
6417
|
+
import {
|
|
6418
|
+
Protocol,
|
|
6419
|
+
badRequest,
|
|
6420
|
+
errorInfo,
|
|
6421
|
+
preconditionFailure
|
|
6422
|
+
} from "@query-farm/vgi-rpc";
|
|
6423
|
+
|
|
6424
|
+
// src/catalog/attach-option.ts
|
|
6425
|
+
var RESERVED_ATTACH_OPTION = "vgi_attach_ticket";
|
|
6426
|
+
function assertNoReservedAttachOption(specs) {
|
|
6427
|
+
for (const spec of specs) {
|
|
6428
|
+
if (spec.name.toLowerCase() === RESERVED_ATTACH_OPTION) {
|
|
6429
|
+
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.");
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6433
|
+
var SPEC_SCHEMA = schema2([
|
|
6434
|
+
field2("name", utf82(), false),
|
|
6435
|
+
field2("description", utf82(), false),
|
|
6436
|
+
field2("type", binary2(), false),
|
|
6437
|
+
field2("default_value", binary2(), true),
|
|
6438
|
+
field2("required", bool2(), true),
|
|
6439
|
+
field2("secret", bool2(), true)
|
|
6440
|
+
]);
|
|
6441
|
+
function deserializeAttachOptionSpec(bytes) {
|
|
6442
|
+
const row = [...iterRows(deserializeBatch(bytes))][0];
|
|
6443
|
+
if (row === undefined) {
|
|
6444
|
+
throw new Error("Cannot deserialize AttachOptionSpec from an empty batch");
|
|
6445
|
+
}
|
|
6446
|
+
const typeSchema = deserializeSchema(toUint8Array(row.type));
|
|
6447
|
+
const type = typeSchema.fields[0].type;
|
|
6448
|
+
let value;
|
|
6449
|
+
const defaultBytes = row.default_value;
|
|
6450
|
+
if (defaultBytes != null) {
|
|
6451
|
+
const asBytes = toUint8Array(defaultBytes);
|
|
6452
|
+
if (asBytes.length > 0) {
|
|
6453
|
+
const defaultRow = [...iterRows(deserializeBatch(asBytes))][0];
|
|
6454
|
+
value = defaultRow?.value;
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
return {
|
|
6458
|
+
name: String(row.name),
|
|
6459
|
+
description: String(row.description ?? ""),
|
|
6460
|
+
type,
|
|
6461
|
+
...value === undefined || value === null ? {} : { default: value },
|
|
6462
|
+
required: row.required === true,
|
|
6463
|
+
secret: row.secret === true
|
|
6464
|
+
};
|
|
6465
|
+
}
|
|
6466
|
+
function deserializeAttachOptionSpecs(specs) {
|
|
6467
|
+
return Array.from(specs, deserializeAttachOptionSpec);
|
|
6468
|
+
}
|
|
6469
|
+
class ReservedAttachOptionError extends Error {
|
|
6470
|
+
constructor(message) {
|
|
6471
|
+
super(message);
|
|
6472
|
+
this.name = "ReservedAttachOptionError";
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
async function checkReservedAttachOptions(catalog) {
|
|
6476
|
+
if (!catalog?.catalogsInfo)
|
|
6477
|
+
return;
|
|
6478
|
+
let infos;
|
|
6479
|
+
try {
|
|
6480
|
+
infos = await catalog.catalogsInfo();
|
|
6481
|
+
} catch (e) {
|
|
6482
|
+
if (e instanceof Error && e.message.includes(`reserved name '${RESERVED_ATTACH_OPTION}'`)) {
|
|
6483
|
+
throw new ReservedAttachOptionError(e.message);
|
|
6484
|
+
}
|
|
6485
|
+
return;
|
|
6486
|
+
}
|
|
6487
|
+
for (const info of infos ?? []) {
|
|
6488
|
+
for (const raw of info.attach_option_specs ?? []) {
|
|
6489
|
+
let name;
|
|
6490
|
+
try {
|
|
6491
|
+
name = deserializeAttachOptionSpec(toUint8Array(raw)).name;
|
|
6492
|
+
} catch {
|
|
6493
|
+
continue;
|
|
6494
|
+
}
|
|
6495
|
+
try {
|
|
6496
|
+
assertNoReservedAttachOption([{ name }]);
|
|
6497
|
+
} catch (e) {
|
|
6498
|
+
throw new ReservedAttachOptionError(e.message);
|
|
6499
|
+
}
|
|
6500
|
+
}
|
|
6501
|
+
}
|
|
6502
|
+
}
|
|
6503
|
+
|
|
6504
|
+
// src/attach-ticket.ts
|
|
6505
|
+
var ATTACH_TICKET_PREFIX = "vgia1.";
|
|
6506
|
+
var ATTACH_TICKET_OPTION = RESERVED_ATTACH_OPTION;
|
|
6507
|
+
var ATTACH_TICKETS_PROTOCOL_NAME = "vgi.attach_tickets.v1";
|
|
6508
|
+
var ATTACH_TICKETS_PROTOCOL_VERSION = "1.0.0";
|
|
6509
|
+
var TICKET_ENVELOPE_VERSION = 1;
|
|
6510
|
+
var MAX_OPTIONS_BYTES = 16 * 1024;
|
|
6511
|
+
var MAX_TICKET_CHARS = 32 * 1024;
|
|
6512
|
+
var CLOCK_SKEW_SECONDS = 60;
|
|
6513
|
+
var UTF82 = new TextEncoder;
|
|
6514
|
+
var STRICT_UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
6515
|
+
var TICKET_AAD_DOMAIN = UTF82.encode("vgi.attach_ticket.v1\x00");
|
|
6516
|
+
var TICKET_ID = /^[0-9a-f]{32}$/;
|
|
6517
|
+
var B64URL = /^[A-Za-z0-9_-]+$/;
|
|
6518
|
+
var MAX_TEXT = 65535;
|
|
6519
|
+
|
|
6520
|
+
class AttachTicketInvalidError extends Error {
|
|
6521
|
+
static errorKind = "attach_ticket_invalid";
|
|
6522
|
+
errorKind = "attach_ticket_invalid";
|
|
6523
|
+
errorCode = "INVALID_ARGUMENT";
|
|
6524
|
+
errorDetails;
|
|
6525
|
+
constructor(detail = "attach ticket not accepted") {
|
|
6526
|
+
super(detail);
|
|
6527
|
+
this.name = "AttachTicketInvalidError";
|
|
6528
|
+
this.errorDetails = [badRequest([{ field: ATTACH_TICKET_OPTION, description: detail }])];
|
|
6529
|
+
}
|
|
6530
|
+
}
|
|
6531
|
+
|
|
6532
|
+
class AttachTicketExpiredError extends Error {
|
|
6533
|
+
static errorKind = "attach_ticket_expired";
|
|
6534
|
+
errorKind = "attach_ticket_expired";
|
|
6535
|
+
errorCode = "FAILED_PRECONDITION";
|
|
6536
|
+
errorDetails;
|
|
6537
|
+
constructor(detail = "attach ticket has expired") {
|
|
6538
|
+
super(detail);
|
|
6539
|
+
this.name = "AttachTicketExpiredError";
|
|
6540
|
+
this.errorDetails = [
|
|
6541
|
+
preconditionFailure([{ type: "ATTACH_TICKET", subject: ATTACH_TICKET_OPTION, description: detail }])
|
|
6542
|
+
];
|
|
6543
|
+
}
|
|
6544
|
+
}
|
|
6545
|
+
|
|
6546
|
+
class AttachTicketRequestError extends Error {
|
|
6547
|
+
errorKind = "invalid_request";
|
|
6548
|
+
errorCode = "INVALID_ARGUMENT";
|
|
6549
|
+
errorDetails;
|
|
6550
|
+
constructor(message, violations) {
|
|
6551
|
+
super(message);
|
|
6552
|
+
this.name = "AttachTicketRequestError";
|
|
6553
|
+
this.errorDetails = [
|
|
6554
|
+
badRequest(violations.map(([f, description]) => ({ field: f, description })))
|
|
6555
|
+
];
|
|
6556
|
+
}
|
|
6557
|
+
}
|
|
6558
|
+
|
|
6559
|
+
class SealAttachDeniedError extends Error {
|
|
6560
|
+
errorKind = "action_denied";
|
|
6561
|
+
errorCode = "PERMISSION_DENIED";
|
|
6562
|
+
errorDetails = [errorInfo({ action: "seal_attach" })];
|
|
6563
|
+
constructor(message) {
|
|
6564
|
+
super(message);
|
|
6565
|
+
this.name = "SealAttachDeniedError";
|
|
6566
|
+
}
|
|
6567
|
+
}
|
|
6568
|
+
function attachTicketAad(principal) {
|
|
6569
|
+
const p = UTF82.encode(principal);
|
|
6570
|
+
const out = new Uint8Array(TICKET_AAD_DOMAIN.length + p.length);
|
|
6571
|
+
out.set(TICKET_AAD_DOMAIN, 0);
|
|
6572
|
+
out.set(p, TICKET_AAD_DOMAIN.length);
|
|
6573
|
+
return out;
|
|
6574
|
+
}
|
|
6575
|
+
function packText(value, name) {
|
|
6576
|
+
const raw = UTF82.encode(value);
|
|
6577
|
+
if (raw.length > MAX_TEXT)
|
|
6578
|
+
throw new Error(`${name} is longer than 65535 bytes`);
|
|
6579
|
+
const out = new Uint8Array(2 + raw.length);
|
|
6580
|
+
new DataView(out.buffer).setUint16(0, raw.length, true);
|
|
6581
|
+
out.set(raw, 2);
|
|
6582
|
+
return out;
|
|
6583
|
+
}
|
|
6584
|
+
function encodePayload(c) {
|
|
6585
|
+
if (c.optionsIpc.length > MAX_OPTIONS_BYTES) {
|
|
6586
|
+
throw new Error(`options are ${c.optionsIpc.length} bytes; a ticket carries at most ${MAX_OPTIONS_BYTES}`);
|
|
6587
|
+
}
|
|
6588
|
+
const head = new Uint8Array(16);
|
|
6589
|
+
const hv = new DataView(head.buffer);
|
|
6590
|
+
hv.setBigInt64(0, BigInt(c.issuedAt), true);
|
|
6591
|
+
hv.setBigInt64(8, BigInt(c.expiresAt), true);
|
|
6592
|
+
const optLen = new Uint8Array(4);
|
|
6593
|
+
new DataView(optLen.buffer).setUint32(0, c.optionsIpc.length, true);
|
|
6594
|
+
const parts = [
|
|
6595
|
+
head,
|
|
6596
|
+
packText(c.ticketId, "ticket_id"),
|
|
6597
|
+
packText(c.catalogName, "catalog_name"),
|
|
6598
|
+
packText(c.dataVersionSpec, "data_version_spec"),
|
|
6599
|
+
packText(c.implementationVersion, "implementation_version"),
|
|
6600
|
+
optLen,
|
|
6601
|
+
c.optionsIpc
|
|
6602
|
+
];
|
|
6603
|
+
let total = 0;
|
|
6604
|
+
for (const p of parts)
|
|
6605
|
+
total += p.length;
|
|
6606
|
+
const out = new Uint8Array(total);
|
|
6607
|
+
let off = 0;
|
|
6608
|
+
for (const p of parts) {
|
|
6609
|
+
out.set(p, off);
|
|
6610
|
+
off += p.length;
|
|
6611
|
+
}
|
|
6612
|
+
return out;
|
|
6613
|
+
}
|
|
6614
|
+
function decodePayload(payload) {
|
|
6615
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
6616
|
+
let pos = 0;
|
|
6617
|
+
const need = (n) => {
|
|
6618
|
+
if (pos + n > payload.length)
|
|
6619
|
+
throw new AttachTicketInvalidError("attach ticket payload is truncated");
|
|
6620
|
+
};
|
|
6621
|
+
const text = () => {
|
|
6622
|
+
need(2);
|
|
6623
|
+
const len = view.getUint16(pos, true);
|
|
6624
|
+
pos += 2;
|
|
6625
|
+
need(len);
|
|
6626
|
+
const raw = payload.subarray(pos, pos + len);
|
|
6627
|
+
pos += len;
|
|
6628
|
+
try {
|
|
6629
|
+
return STRICT_UTF8.decode(raw);
|
|
6630
|
+
} catch {
|
|
6631
|
+
throw new AttachTicketInvalidError("attach ticket payload is not UTF-8");
|
|
6632
|
+
}
|
|
6633
|
+
};
|
|
6634
|
+
need(16);
|
|
6635
|
+
const issuedAt = Number(view.getBigInt64(0, true));
|
|
6636
|
+
const expiresAt = Number(view.getBigInt64(8, true));
|
|
6637
|
+
pos = 16;
|
|
6638
|
+
const ticketId = text();
|
|
6639
|
+
const catalogName = text();
|
|
6640
|
+
const dataVersionSpec = text();
|
|
6641
|
+
const implementationVersion = text();
|
|
6642
|
+
need(4);
|
|
6643
|
+
const optionsLen = view.getUint32(pos, true);
|
|
6644
|
+
pos += 4;
|
|
6645
|
+
if (optionsLen > MAX_OPTIONS_BYTES)
|
|
6646
|
+
throw new AttachTicketInvalidError("attach ticket options exceed 16 KiB");
|
|
6647
|
+
need(optionsLen);
|
|
6648
|
+
const optionsIpc = payload.slice(pos, pos + optionsLen);
|
|
6649
|
+
pos += optionsLen;
|
|
6650
|
+
if (pos !== payload.length)
|
|
6651
|
+
throw new AttachTicketInvalidError("attach ticket payload has trailing bytes");
|
|
6652
|
+
if (!TICKET_ID.test(ticketId))
|
|
6653
|
+
throw new AttachTicketInvalidError("attach ticket id is not 32 lowercase hex");
|
|
6654
|
+
if (!catalogName)
|
|
6655
|
+
throw new AttachTicketInvalidError("attach ticket names no catalog");
|
|
6656
|
+
if (expiresAt !== 0 && expiresAt <= issuedAt)
|
|
6657
|
+
throw new AttachTicketInvalidError("attach ticket lifetime is empty");
|
|
6658
|
+
return { issuedAt, expiresAt, ticketId, catalogName, dataVersionSpec, implementationVersion, optionsIpc };
|
|
6659
|
+
}
|
|
6660
|
+
function b64url(data) {
|
|
6661
|
+
let bin = "";
|
|
6662
|
+
for (let i = 0;i < data.length; i++)
|
|
6663
|
+
bin += String.fromCharCode(data[i]);
|
|
6664
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
6665
|
+
}
|
|
6666
|
+
function b64urlStrict(text) {
|
|
6667
|
+
if (!B64URL.test(text) || text.length % 4 === 1) {
|
|
6668
|
+
throw new AttachTicketInvalidError("attach ticket is not unpadded base64url");
|
|
6669
|
+
}
|
|
6670
|
+
const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - text.length % 4) % 4);
|
|
6671
|
+
const bin = atob(padded);
|
|
6672
|
+
const raw = new Uint8Array(bin.length);
|
|
6673
|
+
for (let i = 0;i < bin.length; i++)
|
|
6674
|
+
raw[i] = bin.charCodeAt(i);
|
|
6675
|
+
if (b64url(raw) !== text)
|
|
6676
|
+
throw new AttachTicketInvalidError("attach ticket is not canonical base64url");
|
|
6677
|
+
return raw;
|
|
6678
|
+
}
|
|
6679
|
+
function randomHex(bytes) {
|
|
6680
|
+
const b = crypto.getRandomValues(new Uint8Array(bytes));
|
|
6681
|
+
let s = "";
|
|
6682
|
+
for (const x of b)
|
|
6683
|
+
s += x.toString(16).padStart(2, "0");
|
|
6684
|
+
return s;
|
|
6685
|
+
}
|
|
6686
|
+
async function mintAttachTicket(signingKey, opts) {
|
|
6687
|
+
if (!opts.principal)
|
|
6688
|
+
throw new Error("a ticket needs a principal");
|
|
6689
|
+
if (!opts.catalogName)
|
|
6690
|
+
throw new Error("a ticket needs a catalog name");
|
|
6691
|
+
const claims = {
|
|
6692
|
+
issuedAt: Math.trunc(opts.issuedAt),
|
|
6693
|
+
expiresAt: Math.trunc(opts.expiresAt),
|
|
6694
|
+
ticketId: opts.ticketId ?? randomHex(16),
|
|
6695
|
+
catalogName: opts.catalogName,
|
|
6696
|
+
dataVersionSpec: opts.dataVersionSpec ?? "",
|
|
6697
|
+
implementationVersion: opts.implementationVersion ?? "",
|
|
6698
|
+
optionsIpc: opts.optionsIpc
|
|
6699
|
+
};
|
|
6700
|
+
if (!TICKET_ID.test(claims.ticketId))
|
|
6701
|
+
throw new Error("ticket_id must be 32 lowercase hex");
|
|
6702
|
+
if (claims.expiresAt !== 0 && claims.expiresAt <= claims.issuedAt) {
|
|
6703
|
+
throw new Error("expires_at must be 0 or after issued_at");
|
|
6704
|
+
}
|
|
6705
|
+
const envelope = await sealBytes(encodePayload(claims), signingKey, attachTicketAad(opts.principal), TICKET_ENVELOPE_VERSION, opts.nonce);
|
|
6706
|
+
const token = ATTACH_TICKET_PREFIX + b64url(envelope);
|
|
6707
|
+
if (token.length > MAX_TICKET_CHARS) {
|
|
6708
|
+
throw new Error(`the ticket would be ${token.length} characters; at most ${MAX_TICKET_CHARS} are accepted`);
|
|
6709
|
+
}
|
|
6710
|
+
return { token, claims };
|
|
6711
|
+
}
|
|
6712
|
+
async function openAttachTicket(signingKey, token, principal, now) {
|
|
6713
|
+
if (!token.startsWith(ATTACH_TICKET_PREFIX))
|
|
6714
|
+
throw new AttachTicketInvalidError("not an attach ticket");
|
|
6715
|
+
if (token.length > MAX_TICKET_CHARS)
|
|
6716
|
+
throw new AttachTicketInvalidError("attach ticket is too long");
|
|
6717
|
+
const envelope = b64urlStrict(token.slice(ATTACH_TICKET_PREFIX.length));
|
|
6718
|
+
if (!principal)
|
|
6719
|
+
throw new AttachTicketInvalidError("an anonymous caller cannot redeem an attach ticket");
|
|
6720
|
+
let payload;
|
|
6721
|
+
try {
|
|
6722
|
+
payload = await openBytes(envelope, signingKey, attachTicketAad(principal), TICKET_ENVELOPE_VERSION);
|
|
6723
|
+
} catch (e) {
|
|
6724
|
+
if (e instanceof OpaqueDataRejectedError) {
|
|
6725
|
+
throw new AttachTicketInvalidError("attach ticket failed verification");
|
|
6726
|
+
}
|
|
6727
|
+
throw e;
|
|
6728
|
+
}
|
|
6729
|
+
const claims = decodePayload(payload);
|
|
6730
|
+
const current = now ?? Date.now() / 1000;
|
|
6731
|
+
if (claims.issuedAt > current + CLOCK_SKEW_SECONDS)
|
|
6732
|
+
throw new AttachTicketExpiredError("attach ticket is not yet valid");
|
|
6733
|
+
if (claims.expiresAt !== 0 && current >= claims.expiresAt + CLOCK_SKEW_SECONDS) {
|
|
6734
|
+
throw new AttachTicketExpiredError("attach ticket has expired");
|
|
6735
|
+
}
|
|
6736
|
+
return claims;
|
|
6737
|
+
}
|
|
6738
|
+
function callerPrincipal(auth) {
|
|
6739
|
+
if (!auth || !auth.authenticated)
|
|
6740
|
+
return null;
|
|
6741
|
+
return auth.principal || null;
|
|
6742
|
+
}
|
|
6743
|
+
async function redeemAttachTicket(options, signingKey, auth, now) {
|
|
6744
|
+
const keys = Object.keys(options);
|
|
6745
|
+
const ticketKeys = keys.filter((k) => k.toLowerCase() === ATTACH_TICKET_OPTION);
|
|
6746
|
+
if (ticketKeys.length === 0)
|
|
6747
|
+
return null;
|
|
6748
|
+
const others = keys.filter((k) => k !== ticketKeys[0]);
|
|
6749
|
+
if (others.length > 0) {
|
|
6750
|
+
throw new AttachTicketRequestError(`${ATTACH_TICKET_OPTION} must be the only attach option`, others.map((k) => [`options.${k}`, `not allowed alongside ${ATTACH_TICKET_OPTION}`]));
|
|
6751
|
+
}
|
|
6752
|
+
const token = options[ticketKeys[0]];
|
|
6753
|
+
if (typeof token !== "string")
|
|
6754
|
+
throw new AttachTicketInvalidError(`${ATTACH_TICKET_OPTION} must be a string`);
|
|
6755
|
+
if (!signingKey)
|
|
6756
|
+
throw new AttachTicketInvalidError("this worker does not redeem attach tickets");
|
|
6757
|
+
const claims = await openAttachTicket(signingKey, token, callerPrincipal(auth), now);
|
|
6758
|
+
let restored = {};
|
|
6759
|
+
if (claims.optionsIpc.length > 0) {
|
|
6760
|
+
try {
|
|
6761
|
+
restored = decodeOptionsBatch(claims.optionsIpc);
|
|
6762
|
+
} catch {
|
|
6763
|
+
throw new AttachTicketInvalidError("attach ticket options are not an Arrow IPC record");
|
|
6764
|
+
}
|
|
6765
|
+
}
|
|
6766
|
+
return {
|
|
6767
|
+
name: claims.catalogName,
|
|
6768
|
+
optionsIpc: claims.optionsIpc.length > 0 ? claims.optionsIpc : null,
|
|
6769
|
+
options: restored,
|
|
6770
|
+
dataVersionSpec: claims.dataVersionSpec || null,
|
|
6771
|
+
implementationVersion: claims.implementationVersion || null
|
|
6772
|
+
};
|
|
6773
|
+
}
|
|
6774
|
+
var SEAL_ATTACH_REQUEST_SCHEMA = schema2([
|
|
6775
|
+
field2("catalog_name", utf82(), false),
|
|
6776
|
+
field2("options", binary2(), true),
|
|
6777
|
+
field2("data_version_spec", utf82(), false),
|
|
6778
|
+
field2("implementation_version", utf82(), false),
|
|
6779
|
+
field2("ttl_seconds", int642(), false)
|
|
6780
|
+
]);
|
|
6781
|
+
var ATTACH_TICKET_SCHEMA = schema2([
|
|
6782
|
+
field2("ticket", utf82(), false),
|
|
6783
|
+
field2("expires_at", float642(), false)
|
|
6784
|
+
]);
|
|
6785
|
+
function resolveTicketMaxTtl(grantKeys, env) {
|
|
6786
|
+
if (grantKeys)
|
|
6787
|
+
return Math.trunc(grantKeys.maxTtlSeconds);
|
|
6788
|
+
const raw = (env?.VGI_RPC_GRANT_MAX_TTL_SECONDS ?? "").trim();
|
|
6789
|
+
if (!raw)
|
|
6790
|
+
return null;
|
|
6791
|
+
const value = /^\d+$/.test(raw) ? Number(raw) : 0;
|
|
6792
|
+
if (!(value > 0))
|
|
6793
|
+
throw new Error(`VGI_RPC_GRANT_MAX_TTL_SECONDS=${JSON.stringify(raw)} must be a positive integer`);
|
|
6794
|
+
return value;
|
|
6795
|
+
}
|
|
6796
|
+
async function declaredSpecs(catalog, catalogName) {
|
|
6797
|
+
if (!catalog)
|
|
6798
|
+
return null;
|
|
6799
|
+
const infos = catalog.catalogsInfo ? await catalog.catalogsInfo() : catalog.catalogs().map((name) => ({ name, attach_option_specs: [] }));
|
|
6800
|
+
for (const info of infos) {
|
|
6801
|
+
if (info.name === catalogName) {
|
|
6802
|
+
return deserializeAttachOptionSpecs((info.attach_option_specs ?? []).map((b) => toUint8Array(b)));
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
return null;
|
|
6806
|
+
}
|
|
6807
|
+
async function sealAttach2(config, request, auth) {
|
|
6808
|
+
const principal = callerPrincipal(auth);
|
|
6809
|
+
if (principal === null)
|
|
6810
|
+
throw new SealAttachDeniedError("an anonymous caller cannot seal an attach ticket");
|
|
6811
|
+
const violations = [];
|
|
6812
|
+
const ttl = Number(request.ttl_seconds ?? 0);
|
|
6813
|
+
if (!Number.isFinite(ttl) || ttl < 0)
|
|
6814
|
+
violations.push(["ttl_seconds", "must be 0 (as long as allowed) or positive"]);
|
|
6815
|
+
const catalogName = String(request.catalog_name ?? "");
|
|
6816
|
+
let optionsIpc = new Uint8Array(0);
|
|
6817
|
+
let options = {};
|
|
6818
|
+
if (request.options != null) {
|
|
6819
|
+
const raw = toUint8Array(request.options);
|
|
6820
|
+
if (raw.length > 0) {
|
|
6821
|
+
const batch = deserializeBatch(raw);
|
|
6822
|
+
if (batch.numRows > 1) {
|
|
6823
|
+
violations.push(["options", "must be a one-row record"]);
|
|
6824
|
+
} else if (batch.numRows === 1) {
|
|
6825
|
+
options = decodeOptionsBatch(raw);
|
|
6826
|
+
optionsIpc = raw;
|
|
6827
|
+
}
|
|
6828
|
+
}
|
|
6829
|
+
}
|
|
6830
|
+
const specs = await declaredSpecs(config.catalogInterface, catalogName);
|
|
6831
|
+
if (specs === null) {
|
|
6832
|
+
violations.push(["catalog_name", `no catalog named ${JSON.stringify(catalogName)}`]);
|
|
6833
|
+
} else {
|
|
6834
|
+
const declared = new Set(specs.map((s) => s.name.toLowerCase()));
|
|
6835
|
+
for (const name of Object.keys(options)) {
|
|
6836
|
+
if (name.toLowerCase() === ATTACH_TICKET_OPTION) {
|
|
6837
|
+
violations.push([`options.${name}`, "a ticket cannot seal another ticket"]);
|
|
6838
|
+
} else if (!declared.has(name.toLowerCase())) {
|
|
6839
|
+
violations.push([`options.${name}`, "not an attach option this catalog declares"]);
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
const supplied = new Set(Object.keys(options).map((n) => n.toLowerCase()));
|
|
6843
|
+
for (const spec of specs) {
|
|
6844
|
+
if (spec.required && !supplied.has(spec.name.toLowerCase()))
|
|
6845
|
+
violations.push([`options.${spec.name}`, "required"]);
|
|
6846
|
+
}
|
|
6847
|
+
}
|
|
6848
|
+
if (optionsIpc.length > MAX_OPTIONS_BYTES) {
|
|
6849
|
+
violations.push(["options", `${optionsIpc.length} bytes; a ticket carries at most ${MAX_OPTIONS_BYTES}`]);
|
|
6850
|
+
}
|
|
6851
|
+
if (violations.length > 0)
|
|
6852
|
+
throw new AttachTicketRequestError("seal_attach request is invalid", violations);
|
|
6853
|
+
const issuedAt = Math.trunc(config.now ? config.now() : Date.now() / 1000);
|
|
6854
|
+
const ceiling = config.maxTtlSeconds;
|
|
6855
|
+
const lifetime = ttl === 0 ? ceiling : ceiling === null ? Math.trunc(ttl) : Math.min(Math.trunc(ttl), ceiling);
|
|
6856
|
+
const expiresAt = lifetime === null ? 0 : issuedAt + lifetime;
|
|
6857
|
+
let token;
|
|
6858
|
+
try {
|
|
6859
|
+
({ token } = await mintAttachTicket(config.signingKey, {
|
|
6860
|
+
principal,
|
|
6861
|
+
catalogName,
|
|
6862
|
+
optionsIpc,
|
|
6863
|
+
dataVersionSpec: String(request.data_version_spec ?? ""),
|
|
6864
|
+
implementationVersion: String(request.implementation_version ?? ""),
|
|
6865
|
+
issuedAt,
|
|
6866
|
+
expiresAt
|
|
6867
|
+
}));
|
|
6868
|
+
} catch (e) {
|
|
6869
|
+
throw new AttachTicketRequestError("seal_attach request is invalid", [["request", e.message]]);
|
|
6870
|
+
}
|
|
6871
|
+
return { ticket: token, expires_at: expiresAt === 0 ? Number.POSITIVE_INFINITY : expiresAt };
|
|
6872
|
+
}
|
|
6873
|
+
function buildAttachTicketsProtocol(config) {
|
|
6874
|
+
const p = new Protocol(ATTACH_TICKETS_PROTOCOL_NAME, { protocolVersion: ATTACH_TICKETS_PROTOCOL_VERSION });
|
|
6875
|
+
p.unary("seal_attach", {
|
|
6876
|
+
params: REQUEST_PARAMS_SCHEMA,
|
|
6877
|
+
result: RESULT_BINARY_SCHEMA,
|
|
6878
|
+
doc: "Seal the caller's attach of request.catalog_name into a ticket.",
|
|
6879
|
+
handler: async (params, ctx) => {
|
|
6880
|
+
const request = unwrapRequest(params.request);
|
|
6881
|
+
const sealed = await sealAttach2(config, request, ctx?.auth);
|
|
6882
|
+
const batch = batchFromColumns({ ticket: [sealed.ticket], expires_at: [sealed.expires_at] }, ATTACH_TICKET_SCHEMA);
|
|
6883
|
+
return { result: serializeBatch(batch) };
|
|
6884
|
+
}
|
|
6885
|
+
});
|
|
6886
|
+
return p;
|
|
6887
|
+
}
|
|
6888
|
+
|
|
6410
6889
|
// src/protocol/handlers/catalog/admin.ts
|
|
6411
6890
|
function registerCatalogAdminMethods(protocol, getCatalog, signingKey) {
|
|
6412
6891
|
catalogUnary(protocol, signingKey, "catalog_catalogs", {
|
|
@@ -6431,7 +6910,15 @@ function registerCatalogAdminMethods(protocol, getCatalog, signingKey) {
|
|
|
6431
6910
|
handler: async (params, ctx) => {
|
|
6432
6911
|
const innerParams = unwrapRequest(params.request);
|
|
6433
6912
|
const cat = getCatalog();
|
|
6434
|
-
|
|
6913
|
+
let optionsDict = decodeOptionsBatch(innerParams.options);
|
|
6914
|
+
const restored = await redeemAttachTicket(optionsDict, signingKey, ctx?.auth ?? currentRequestAuth());
|
|
6915
|
+
if (restored !== null) {
|
|
6916
|
+
innerParams.name = restored.name;
|
|
6917
|
+
innerParams.options = restored.optionsIpc;
|
|
6918
|
+
innerParams.data_version_spec = restored.dataVersionSpec;
|
|
6919
|
+
innerParams.implementation_version = restored.implementationVersion;
|
|
6920
|
+
optionsDict = restored.options;
|
|
6921
|
+
}
|
|
6435
6922
|
const result = await cat.attach(innerParams.name, optionsDict, innerParams.data_version_spec ?? null, innerParams.implementation_version ?? null);
|
|
6436
6923
|
const sealedAttach = await sealAttach(toUint8Array(result.attach_opaque_data), ctx?.auth, signingKey);
|
|
6437
6924
|
return wrapResult(buildCatalogAttachResult({
|
|
@@ -6887,7 +7374,7 @@ function registerCatalogMethods(protocol, catalog, _catalogName, signingKey) {
|
|
|
6887
7374
|
// src/protocol/dispatch.ts
|
|
6888
7375
|
var VGI_PROTOCOL_NAME = "vgi.v2";
|
|
6889
7376
|
function buildVgiProtocol(config) {
|
|
6890
|
-
const protocol = new
|
|
7377
|
+
const protocol = new Protocol2(VGI_PROTOCOL_NAME, { protocolVersion: "2.1.0" });
|
|
6891
7378
|
registerFunctionMethods(protocol, {
|
|
6892
7379
|
registry: config.registry,
|
|
6893
7380
|
signingKey: config.signingKey,
|
|
@@ -7004,6 +7491,7 @@ function createLandingRoutes(info) {
|
|
|
7004
7491
|
|
|
7005
7492
|
// src/rpc-server.ts
|
|
7006
7493
|
import {
|
|
7494
|
+
GrantKeys,
|
|
7007
7495
|
IdentityImpl,
|
|
7008
7496
|
VgiRpcServer
|
|
7009
7497
|
} from "@query-farm/vgi-rpc";
|
|
@@ -7016,11 +7504,20 @@ function buildRpcServer(primary, hosting, options) {
|
|
|
7016
7504
|
throw new Error(`Unknown transport ${JSON.stringify(options.transport)}; expected one of ${[...TRANSPORTS].join(", ")}.`);
|
|
7017
7505
|
}
|
|
7018
7506
|
const protocols = validatedHostedProtocols(hosting, primary);
|
|
7019
|
-
|
|
7507
|
+
let identity;
|
|
7508
|
+
if (IDENTITY_TRANSPORTS.has(options.transport)) {
|
|
7509
|
+
const env = options.env ?? processEnv();
|
|
7510
|
+
const grantKeys = hosting.grantKeys === undefined ? GrantKeys.fromEnv(env) : hosting.grantKeys;
|
|
7511
|
+
identity = buildIdentity(hosting, grantKeys, options.env);
|
|
7512
|
+
const tickets = buildAttachTickets(hosting, grantKeys, options.attachTickets, env);
|
|
7513
|
+
if (tickets)
|
|
7514
|
+
protocols.push(tickets);
|
|
7515
|
+
}
|
|
7020
7516
|
return new VgiRpcServer(primary, {
|
|
7021
7517
|
serverId: options.serverId,
|
|
7022
7518
|
protocols,
|
|
7023
|
-
identity
|
|
7519
|
+
identity,
|
|
7520
|
+
grantKeys: null
|
|
7024
7521
|
});
|
|
7025
7522
|
}
|
|
7026
7523
|
function validatedHostedProtocols(hosting, primary) {
|
|
@@ -7061,17 +7558,29 @@ function validatedHostedProtocols(hosting, primary) {
|
|
|
7061
7558
|
}
|
|
7062
7559
|
return out;
|
|
7063
7560
|
}
|
|
7064
|
-
function buildIdentity(hosting, env) {
|
|
7561
|
+
function buildIdentity(hosting, grantKeys, env) {
|
|
7065
7562
|
const { resolveToken, mintGrant } = hosting;
|
|
7066
|
-
if (!resolveToken && !mintGrant)
|
|
7563
|
+
if (!resolveToken && !mintGrant && !grantKeys)
|
|
7067
7564
|
return;
|
|
7068
7565
|
return new IdentityImpl({
|
|
7069
7566
|
resolveToken,
|
|
7070
7567
|
mintGrant,
|
|
7568
|
+
grantKeys: grantKeys ?? undefined,
|
|
7071
7569
|
introspectPrincipals: resolveToken ? resolveIntrospectPrincipals(hosting.introspectPrincipals, env) : undefined,
|
|
7072
7570
|
maxAuthAge: hosting.maxAuthAge
|
|
7073
7571
|
});
|
|
7074
7572
|
}
|
|
7573
|
+
function buildAttachTickets(hosting, grantKeys, config, env) {
|
|
7574
|
+
if (!config || !config.signingKeyConfigured || !config.signingKey)
|
|
7575
|
+
return;
|
|
7576
|
+
if (!grantKeys && !hosting.mintGrant)
|
|
7577
|
+
return;
|
|
7578
|
+
return buildAttachTicketsProtocol({
|
|
7579
|
+
signingKey: config.signingKey,
|
|
7580
|
+
catalogInterface: config.catalogInterface,
|
|
7581
|
+
maxTtlSeconds: resolveTicketMaxTtl(grantKeys, env)
|
|
7582
|
+
});
|
|
7583
|
+
}
|
|
7075
7584
|
function resolveIntrospectPrincipals(explicit, env) {
|
|
7076
7585
|
let principals;
|
|
7077
7586
|
if (explicit !== undefined) {
|
|
@@ -7088,6 +7597,26 @@ function resolveIntrospectPrincipals(explicit, env) {
|
|
|
7088
7597
|
}
|
|
7089
7598
|
return principals;
|
|
7090
7599
|
}
|
|
7600
|
+
function processEnv() {
|
|
7601
|
+
return globalThis.process?.env;
|
|
7602
|
+
}
|
|
7603
|
+
function grantKeysFromArgv(argv, env = processEnv()) {
|
|
7604
|
+
const keys = [];
|
|
7605
|
+
for (let i = 0;i < argv.length; i++) {
|
|
7606
|
+
const a = argv[i];
|
|
7607
|
+
if (a === "--grant-key") {
|
|
7608
|
+
if (i + 1 >= argv.length)
|
|
7609
|
+
throw new Error("--grant-key requires a value");
|
|
7610
|
+
keys.push(argv[++i]);
|
|
7611
|
+
} else if (a.startsWith("--grant-key=")) {
|
|
7612
|
+
keys.push(a.slice("--grant-key=".length));
|
|
7613
|
+
}
|
|
7614
|
+
}
|
|
7615
|
+
if (keys.length === 0)
|
|
7616
|
+
return;
|
|
7617
|
+
const fromEnv = GrantKeys.fromEnv({ ...env ?? {}, VGI_RPC_GRANT_KEYS: keys.join(",") });
|
|
7618
|
+
return fromEnv ?? undefined;
|
|
7619
|
+
}
|
|
7091
7620
|
|
|
7092
7621
|
// src/http/fetch.ts
|
|
7093
7622
|
var requestAuthStorage;
|
|
@@ -7127,7 +7656,17 @@ function createVgiFetch(opts) {
|
|
|
7127
7656
|
signingKey: opts.signingKey,
|
|
7128
7657
|
...opts.protocol
|
|
7129
7658
|
});
|
|
7130
|
-
const server = buildRpcServer(protocol, opts, {
|
|
7659
|
+
const server = buildRpcServer(protocol, opts, {
|
|
7660
|
+
transport: "http",
|
|
7661
|
+
serverId,
|
|
7662
|
+
attachTickets: {
|
|
7663
|
+
signingKey: opts.signingKey,
|
|
7664
|
+
signingKeyConfigured: opts.signingKeyConfigured ?? true,
|
|
7665
|
+
catalogInterface: opts.protocol.catalogInterface
|
|
7666
|
+
}
|
|
7667
|
+
});
|
|
7668
|
+
const reservedCheck = checkReservedAttachOptions(opts.protocol.catalogInterface);
|
|
7669
|
+
reservedCheck.catch((e) => console.error(`VGI worker refused to serve: ${e.message}`));
|
|
7131
7670
|
const handler = createHttpHandler(server, {
|
|
7132
7671
|
prefix,
|
|
7133
7672
|
serverId,
|
|
@@ -7145,6 +7684,7 @@ function createVgiFetch(opts) {
|
|
|
7145
7684
|
enableLandingPage: false
|
|
7146
7685
|
});
|
|
7147
7686
|
return async (req) => {
|
|
7687
|
+
await reservedCheck;
|
|
7148
7688
|
const storage = await ensureRequestAuthStorage();
|
|
7149
7689
|
if (!storage)
|
|
7150
7690
|
return handler(req);
|
|
@@ -7211,6 +7751,7 @@ function resolveTokenTtl(explicit, env) {
|
|
|
7211
7751
|
function createVgiWorkerFetch(opts) {
|
|
7212
7752
|
const env = opts.env ?? process.env;
|
|
7213
7753
|
const signingKey = resolveSigningKey(opts.signingKey, env);
|
|
7754
|
+
const signingKeyConfigured = opts.signingKey !== undefined || !!env.VGI_SIGNING_KEY?.trim();
|
|
7214
7755
|
const tokenTtl = resolveTokenTtl(opts.tokenTtl, env);
|
|
7215
7756
|
const corsOrigins = opts.corsOrigins === null ? null : opts.corsOrigins ?? env.CORS_ORIGINS;
|
|
7216
7757
|
const bridge = opts.irohBridge;
|
|
@@ -7221,6 +7762,7 @@ function createVgiWorkerFetch(opts) {
|
|
|
7221
7762
|
return createVgiFetch({
|
|
7222
7763
|
protocol: { registry: opts.registry, catalogInterface: opts.catalogInterface },
|
|
7223
7764
|
signingKey,
|
|
7765
|
+
signingKeyConfigured,
|
|
7224
7766
|
tokenTtl,
|
|
7225
7767
|
prefix: opts.prefix ?? "",
|
|
7226
7768
|
serverId: opts.serverId ?? opts.name,
|
|
@@ -7235,7 +7777,8 @@ function createVgiWorkerFetch(opts) {
|
|
|
7235
7777
|
resolveToken: opts.resolveToken,
|
|
7236
7778
|
mintGrant: opts.mintGrant,
|
|
7237
7779
|
introspectPrincipals: opts.introspectPrincipals,
|
|
7238
|
-
maxAuthAge: opts.maxAuthAge
|
|
7780
|
+
maxAuthAge: opts.maxAuthAge,
|
|
7781
|
+
grantKeys: opts.grantKeys !== undefined ? opts.grantKeys : grantKeysFromArgv(opts.argv ?? process.argv.slice(2), env) ?? GrantKeys2.fromEnv(env)
|
|
7239
7782
|
});
|
|
7240
7783
|
}
|
|
7241
7784
|
function serveVgiWorker(opts) {
|
|
@@ -7283,4 +7826,4 @@ export {
|
|
|
7283
7826
|
serveVgiWorker
|
|
7284
7827
|
};
|
|
7285
7828
|
|
|
7286
|
-
//# debugId=
|
|
7829
|
+
//# debugId=9AD0D580690218C964756E2164756E21
|