@query-farm/vgi-rpc 0.6.3 → 0.7.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/dist/access-log.d.ts +50 -0
- package/dist/access-log.d.ts.map +1 -0
- package/dist/arrow/impl-arrowjs/index.d.ts +96 -0
- package/dist/arrow/impl-arrowjs/index.d.ts.map +1 -0
- package/dist/arrow/impl-flechette/index.d.ts +102 -0
- package/dist/arrow/impl-flechette/index.d.ts.map +1 -0
- package/dist/arrow/impl-flechette/message-meta.d.ts +11 -0
- package/dist/arrow/impl-flechette/message-meta.d.ts.map +1 -0
- package/dist/arrow/index.d.ts +4 -0
- package/dist/arrow/index.d.ts.map +1 -0
- package/dist/arrow/predicates.d.ts +44 -0
- package/dist/arrow/predicates.d.ts.map +1 -0
- package/dist/arrow/types.d.ts +62 -0
- package/dist/arrow/types.d.ts.map +1 -0
- package/dist/client/capabilities.d.ts +25 -0
- package/dist/client/capabilities.d.ts.map +1 -0
- package/dist/client/connect.d.ts.map +1 -1
- package/dist/client/introspect.d.ts +7 -0
- package/dist/client/introspect.d.ts.map +1 -1
- package/dist/client/ipc.d.ts +8 -2
- package/dist/client/ipc.d.ts.map +1 -1
- package/dist/client/pipe.d.ts.map +1 -1
- package/dist/client/stream.d.ts +11 -2
- package/dist/client/stream.d.ts.map +1 -1
- package/dist/client/uploadUrl.d.ts +25 -0
- package/dist/client/uploadUrl.d.ts.map +1 -0
- package/dist/constants.d.ts +15 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/crypto.d.ts +22 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/dispatch/describe.d.ts +10 -6
- package/dist/dispatch/describe.d.ts.map +1 -1
- package/dist/dispatch/stream.d.ts +2 -2
- package/dist/dispatch/stream.d.ts.map +1 -1
- package/dist/dispatch/unary.d.ts +2 -2
- package/dist/dispatch/unary.d.ts.map +1 -1
- package/dist/errors.d.ts +46 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/external.d.ts +25 -5
- package/dist/external.d.ts.map +1 -1
- package/dist/http/bearer.d.ts.map +1 -1
- package/dist/http/common.d.ts +42 -7
- package/dist/http/common.d.ts.map +1 -1
- package/dist/http/dispatch.d.ts +20 -2
- package/dist/http/dispatch.d.ts.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/mtls.d.ts +2 -1
- package/dist/http/mtls.d.ts.map +1 -1
- package/dist/http/oauth-pkce.d.ts +141 -0
- package/dist/http/oauth-pkce.d.ts.map +1 -0
- package/dist/http/pages.d.ts +3 -0
- package/dist/http/pages.d.ts.map +1 -1
- package/dist/http/sticky.d.ts +124 -0
- package/dist/http/sticky.d.ts.map +1 -0
- package/dist/http/token.d.ts +38 -12
- package/dist/http/token.d.ts.map +1 -1
- package/dist/http/types.d.ts +68 -5
- package/dist/http/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1275 -3507
- package/dist/index.js.map +19 -37
- package/dist/launcher/hash.d.ts +22 -0
- package/dist/launcher/hash.d.ts.map +1 -0
- package/dist/launcher/index.d.ts +23 -0
- package/dist/launcher/index.d.ts.map +1 -0
- package/dist/launcher/launch.d.ts +27 -0
- package/dist/launcher/launch.d.ts.map +1 -0
- package/dist/launcher/lock.d.ts +19 -0
- package/dist/launcher/lock.d.ts.map +1 -0
- package/dist/launcher/serve-unix.d.ts +54 -0
- package/dist/launcher/serve-unix.d.ts.map +1 -0
- package/dist/launcher/state.d.ts +59 -0
- package/dist/launcher/state.d.ts.map +1 -0
- package/dist/otel.d.ts.map +1 -1
- package/dist/protocol.d.ts +16 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/schema.d.ts +45 -18
- package/dist/schema.d.ts.map +1 -1
- package/dist/server.d.ts +23 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/types.d.ts +216 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/util/gzip.d.ts +10 -0
- package/dist/util/gzip.d.ts.map +1 -0
- package/dist/util/schema.d.ts +3 -15
- package/dist/util/schema.d.ts.map +1 -1
- package/dist/util/web-crypto.d.ts +22 -0
- package/dist/util/web-crypto.d.ts.map +1 -0
- package/dist/util/zstd.d.ts +26 -3
- package/dist/util/zstd.d.ts.map +1 -1
- package/dist/wire/opaque.d.ts +11 -0
- package/dist/wire/opaque.d.ts.map +1 -0
- package/dist/wire/reader.d.ts +5 -5
- package/dist/wire/reader.d.ts.map +1 -1
- package/dist/wire/request.d.ts +11 -3
- package/dist/wire/request.d.ts.map +1 -1
- package/dist/wire/response.d.ts +6 -6
- package/dist/wire/response.d.ts.map +1 -1
- package/dist/wire/writer.d.ts +49 -39
- package/dist/wire/writer.d.ts.map +1 -1
- package/package.json +24 -10
- package/src/access-log.ts +195 -0
- package/src/arrow/impl-arrowjs/index.ts +433 -0
- package/src/arrow/impl-flechette/index.ts +414 -0
- package/src/arrow/impl-flechette/message-meta.ts +174 -0
- package/src/arrow/index.ts +89 -0
- package/src/arrow/predicates.ts +56 -0
- package/src/arrow/types.ts +73 -0
- package/src/client/capabilities.ts +84 -0
- package/src/client/connect.ts +103 -26
- package/src/client/introspect.ts +60 -38
- package/src/client/ipc.ts +37 -27
- package/src/client/pipe.ts +12 -9
- package/src/client/stream.ts +34 -19
- package/src/client/uploadUrl.ts +169 -0
- package/src/constants.ts +18 -1
- package/src/crypto.ts +95 -0
- package/src/dispatch/describe.ts +146 -107
- package/src/dispatch/stream.ts +53 -24
- package/src/dispatch/unary.ts +5 -4
- package/src/errors.ts +76 -0
- package/src/external.ts +43 -29
- package/src/http/bearer.ts +2 -5
- package/src/http/common.ts +90 -23
- package/src/http/dispatch.ts +373 -46
- package/src/http/handler.ts +794 -68
- package/src/http/index.ts +1 -0
- package/src/http/mtls.ts +18 -3
- package/src/http/oauth-pkce.ts +1035 -0
- package/src/http/pages.ts +30 -15
- package/src/http/sticky.ts +429 -0
- package/src/http/token.ts +165 -75
- package/src/http/types.ts +69 -5
- package/src/index.ts +40 -1
- package/src/launcher/hash.ts +104 -0
- package/src/launcher/index.ts +35 -0
- package/src/launcher/launch.ts +284 -0
- package/src/launcher/lock.ts +171 -0
- package/src/launcher/serve-unix.ts +385 -0
- package/src/launcher/state.ts +245 -0
- package/src/otel.ts +39 -33
- package/src/protocol.ts +27 -3
- package/src/schema.ts +107 -56
- package/src/server.ts +196 -20
- package/src/types.ts +322 -18
- package/src/util/gzip.ts +63 -0
- package/src/util/schema.ts +4 -22
- package/src/util/web-crypto.ts +98 -0
- package/src/util/zstd.ts +133 -14
- package/src/wire/opaque.ts +37 -0
- package/src/wire/reader.ts +5 -4
- package/src/wire/request.ts +67 -8
- package/src/wire/response.ts +51 -85
- package/src/wire/writer.ts +165 -69
- package/dist/util/conform.d.ts +0 -18
- package/dist/util/conform.d.ts.map +0 -1
- package/src/util/conform.ts +0 -94
package/src/http/token.ts
CHANGED
|
@@ -1,68 +1,142 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { openBytes, SealError, sealBytes } from "../crypto.js";
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const _UTF8 = new TextEncoder();
|
|
7
|
+
|
|
8
|
+
const TOKEN_VERSION = 4;
|
|
9
|
+
|
|
10
|
+
const AAD_PREFIX = _UTF8.encode("vgi_rpc.state.v4\0");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build the AEAD associated data that binds a state token to its issuing
|
|
14
|
+
* principal. Anonymous and authenticated tokens produce distinct AAD
|
|
15
|
+
* strings, so an anonymous token cannot be opened by a named identity
|
|
16
|
+
* (and vice versa).
|
|
17
|
+
*/
|
|
18
|
+
export function computeAad(principal: string | null | undefined): Uint8Array {
|
|
19
|
+
if (!principal) {
|
|
20
|
+
const tail = _UTF8.encode("\0anonymous");
|
|
21
|
+
return concatBytes(AAD_PREFIX, tail);
|
|
22
|
+
}
|
|
23
|
+
const pBytes = _UTF8.encode(principal);
|
|
24
|
+
const tail = new Uint8Array(1 + pBytes.length);
|
|
25
|
+
tail[0] = 0x01;
|
|
26
|
+
tail.set(pBytes, 1);
|
|
27
|
+
return concatBytes(AAD_PREFIX, tail);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Base64 helpers — `btoa`/`atob` exist on Node 16+, Bun, and workerd; we work
|
|
31
|
+
// in chunks to stay below the per-call argument limit (Latin-1 only, so we
|
|
32
|
+
// move byte-by-byte through `String.fromCharCode`).
|
|
33
|
+
|
|
34
|
+
export function bytesToBase64(bytes: Uint8Array): string {
|
|
35
|
+
let s = "";
|
|
36
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
37
|
+
s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
38
|
+
}
|
|
39
|
+
return btoa(s);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function base64ToBytes(b64: string): Uint8Array {
|
|
43
|
+
const bin = atob(b64);
|
|
44
|
+
const out = new Uint8Array(bin.length);
|
|
45
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Little-endian writers/readers — Buffer was the previous abstraction; we now
|
|
50
|
+
// touch DataView for portability across Node, Bun, and workerd.
|
|
51
|
+
|
|
52
|
+
function writeU32LE(view: DataView, offset: number, value: number): void {
|
|
53
|
+
view.setUint32(offset, value, /* littleEndian */ true);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeU64LE(view: DataView, offset: number, value: bigint): void {
|
|
57
|
+
view.setBigUint64(offset, value, /* littleEndian */ true);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readU32LE(view: DataView, offset: number): number {
|
|
61
|
+
return view.getUint32(offset, /* littleEndian */ true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readU64LE(view: DataView, offset: number): bigint {
|
|
65
|
+
return view.getBigUint64(offset, /* littleEndian */ true);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function concatBytes(...parts: Uint8Array[]): Uint8Array {
|
|
69
|
+
let total = 0;
|
|
70
|
+
for (const p of parts) total += p.length;
|
|
71
|
+
const out = new Uint8Array(total);
|
|
72
|
+
let offset = 0;
|
|
73
|
+
for (const p of parts) {
|
|
74
|
+
out.set(p, offset);
|
|
75
|
+
offset += p.length;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
10
79
|
|
|
11
80
|
/**
|
|
12
|
-
*
|
|
81
|
+
* Seal a state token with XChaCha20-Poly1305 AEAD (v4 wire format).
|
|
82
|
+
*
|
|
83
|
+
* Layout (base64-encoded):
|
|
84
|
+
*
|
|
85
|
+
* ```
|
|
86
|
+
* [1B version=4]
|
|
87
|
+
* [24B XChaCha20-Poly1305 nonce (random)]
|
|
88
|
+
* [.. ciphertext + 16B Poly1305 tag]
|
|
89
|
+
* plaintext:
|
|
90
|
+
* [8B created_at uint64 LE]
|
|
91
|
+
* [4B state_len uint32 LE] [state_len bytes]
|
|
92
|
+
* [4B schema_len uint32 LE] [schema_len bytes]
|
|
93
|
+
* [4B input_schema_len LE] [input_schema_len bytes]
|
|
94
|
+
* ```
|
|
13
95
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* [32B HMAC-SHA256(signing_key, all above bytes)]
|
|
96
|
+
* `created_at` lives inside the ciphertext so TTL enforcement runs after
|
|
97
|
+
* authenticity. The version byte is informational (a self-describing
|
|
98
|
+
* format marker); a tampered version byte still fails decryption because
|
|
99
|
+
* we use the matching algorithm for that version. `principal` is bound
|
|
100
|
+
* via AEAD associated data so a token minted for one identity fails
|
|
101
|
+
* decryption when presented by another.
|
|
21
102
|
*/
|
|
22
103
|
export function packStateToken(
|
|
23
104
|
stateBytes: Uint8Array,
|
|
24
105
|
schemaBytes: Uint8Array,
|
|
25
106
|
inputSchemaBytes: Uint8Array,
|
|
26
|
-
|
|
107
|
+
tokenKey: Uint8Array,
|
|
108
|
+
principal: string | null | undefined,
|
|
27
109
|
createdAt?: number,
|
|
28
110
|
): string {
|
|
111
|
+
if (tokenKey.length !== 32) {
|
|
112
|
+
throw new Error("XChaCha20-Poly1305 token key must be 32 bytes");
|
|
113
|
+
}
|
|
29
114
|
const now = createdAt ?? Math.floor(Date.now() / 1000);
|
|
30
115
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
116
|
+
const plaintextLen = 8 + 4 + stateBytes.length + 4 + schemaBytes.length + 4 + inputSchemaBytes.length;
|
|
117
|
+
const plaintext = new Uint8Array(plaintextLen);
|
|
118
|
+
const view = new DataView(plaintext.buffer);
|
|
33
119
|
let offset = 0;
|
|
34
120
|
|
|
35
|
-
|
|
36
|
-
buf.writeUInt8(TOKEN_VERSION, offset);
|
|
37
|
-
offset += 1;
|
|
38
|
-
|
|
39
|
-
// created_at as uint64 LE
|
|
40
|
-
buf.writeBigUInt64LE(BigInt(now), offset);
|
|
121
|
+
writeU64LE(view, offset, BigInt(now));
|
|
41
122
|
offset += 8;
|
|
42
123
|
|
|
43
|
-
|
|
44
|
-
buf.writeUInt32LE(stateBytes.length, offset);
|
|
124
|
+
writeU32LE(view, offset, stateBytes.length);
|
|
45
125
|
offset += 4;
|
|
46
|
-
|
|
126
|
+
plaintext.set(stateBytes, offset);
|
|
47
127
|
offset += stateBytes.length;
|
|
48
128
|
|
|
49
|
-
|
|
50
|
-
buf.writeUInt32LE(schemaBytes.length, offset);
|
|
129
|
+
writeU32LE(view, offset, schemaBytes.length);
|
|
51
130
|
offset += 4;
|
|
52
|
-
|
|
131
|
+
plaintext.set(schemaBytes, offset);
|
|
53
132
|
offset += schemaBytes.length;
|
|
54
133
|
|
|
55
|
-
|
|
56
|
-
buf.writeUInt32LE(inputSchemaBytes.length, offset);
|
|
134
|
+
writeU32LE(view, offset, inputSchemaBytes.length);
|
|
57
135
|
offset += 4;
|
|
58
|
-
|
|
59
|
-
offset += inputSchemaBytes.length;
|
|
136
|
+
plaintext.set(inputSchemaBytes, offset);
|
|
60
137
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const token = Buffer.concat([buf, mac]);
|
|
64
|
-
|
|
65
|
-
return token.toString("base64");
|
|
138
|
+
const wire = sealBytes(plaintext, tokenKey, { aad: computeAad(principal), version: TOKEN_VERSION });
|
|
139
|
+
return bytesToBase64(wire);
|
|
66
140
|
}
|
|
67
141
|
|
|
68
142
|
export interface UnpackedToken {
|
|
@@ -73,40 +147,59 @@ export interface UnpackedToken {
|
|
|
73
147
|
}
|
|
74
148
|
|
|
75
149
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
150
|
+
* Open and verify a state token. Decryption (which checks the Poly1305
|
|
151
|
+
* tag) authenticates the payload; any tampering, wrong key, or AAD
|
|
152
|
+
* mismatch (e.g. cross-principal replay) surfaces as a uniform
|
|
153
|
+
* "signature verification failed" error so callers cannot distinguish
|
|
154
|
+
* failure modes via timing or message content.
|
|
155
|
+
*
|
|
156
|
+
* Throws on tampered, expired, malformed, or unknown-version tokens.
|
|
78
157
|
*/
|
|
79
|
-
export function unpackStateToken(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
158
|
+
export function unpackStateToken(
|
|
159
|
+
tokenBase64: string,
|
|
160
|
+
tokenKey: Uint8Array,
|
|
161
|
+
tokenTtl: number,
|
|
162
|
+
principal: string | null | undefined,
|
|
163
|
+
): UnpackedToken {
|
|
164
|
+
let raw: Uint8Array;
|
|
165
|
+
try {
|
|
166
|
+
raw = base64ToBytes(tokenBase64);
|
|
167
|
+
} catch {
|
|
168
|
+
throw new Error("Malformed state token");
|
|
84
169
|
}
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
170
|
+
// Pre-check the envelope version separately so callers can distinguish
|
|
171
|
+
// "wrong format" from "tampered". Mirrors the pre-refactor error shape.
|
|
172
|
+
if (raw.length >= 1 && raw[0] !== TOKEN_VERSION) {
|
|
173
|
+
throw new Error(`Unsupported state token version: ${raw[0]}`);
|
|
174
|
+
}
|
|
175
|
+
let plaintext: Uint8Array;
|
|
176
|
+
try {
|
|
177
|
+
plaintext = openBytes(raw, tokenKey, { aad: computeAad(principal), version: TOKEN_VERSION });
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (err instanceof SealError) {
|
|
180
|
+
throw new Error("State token signature verification failed");
|
|
181
|
+
}
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
if (plaintext.length < 8) {
|
|
185
|
+
throw new Error("State token truncated");
|
|
94
186
|
}
|
|
95
187
|
|
|
188
|
+
// Copy each bytes section into a freshly-allocated Uint8Array with
|
|
189
|
+
// byteOffset=0. arrow-js's schema deserializer wraps the result as Int32Array
|
|
190
|
+
// and throws 'RangeError: Byte offset is not aligned' if the slice happens
|
|
191
|
+
// to start at a non-4-aligned offset. Copying normalizes the alignment.
|
|
192
|
+
const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
|
|
96
193
|
let offset = 0;
|
|
194
|
+
const copyAligned = (start: number, len: number) => {
|
|
195
|
+
const out = new Uint8Array(len);
|
|
196
|
+
out.set(plaintext.subarray(start, start + len));
|
|
197
|
+
return out;
|
|
198
|
+
};
|
|
97
199
|
|
|
98
|
-
|
|
99
|
-
const version = payload.readUInt8(offset);
|
|
100
|
-
offset += 1;
|
|
101
|
-
if (version !== TOKEN_VERSION) {
|
|
102
|
-
throw new Error(`Unsupported state token version: ${version}`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// created_at
|
|
106
|
-
const createdAt = Number(payload.readBigUInt64LE(offset));
|
|
200
|
+
const createdAt = Number(readU64LE(view, offset));
|
|
107
201
|
offset += 8;
|
|
108
202
|
|
|
109
|
-
// TTL check
|
|
110
203
|
if (tokenTtl > 0) {
|
|
111
204
|
const now = Math.floor(Date.now() / 1000);
|
|
112
205
|
if (now - createdAt > tokenTtl) {
|
|
@@ -114,31 +207,28 @@ export function unpackStateToken(tokenBase64: string, signingKey: Uint8Array, to
|
|
|
114
207
|
}
|
|
115
208
|
}
|
|
116
209
|
|
|
117
|
-
|
|
118
|
-
const stateLen = payload.readUInt32LE(offset);
|
|
210
|
+
const stateLen = readU32LE(view, offset);
|
|
119
211
|
offset += 4;
|
|
120
|
-
if (offset + stateLen >
|
|
212
|
+
if (offset + stateLen > plaintext.length) {
|
|
121
213
|
throw new Error("State token truncated (state)");
|
|
122
214
|
}
|
|
123
|
-
const stateBytes =
|
|
215
|
+
const stateBytes = copyAligned(offset, stateLen);
|
|
124
216
|
offset += stateLen;
|
|
125
217
|
|
|
126
|
-
|
|
127
|
-
const schemaLen = payload.readUInt32LE(offset);
|
|
218
|
+
const schemaLen = readU32LE(view, offset);
|
|
128
219
|
offset += 4;
|
|
129
|
-
if (offset + schemaLen >
|
|
220
|
+
if (offset + schemaLen > plaintext.length) {
|
|
130
221
|
throw new Error("State token truncated (schema)");
|
|
131
222
|
}
|
|
132
|
-
const schemaBytes =
|
|
223
|
+
const schemaBytes = copyAligned(offset, schemaLen);
|
|
133
224
|
offset += schemaLen;
|
|
134
225
|
|
|
135
|
-
|
|
136
|
-
const inputSchemaLen = payload.readUInt32LE(offset);
|
|
226
|
+
const inputSchemaLen = readU32LE(view, offset);
|
|
137
227
|
offset += 4;
|
|
138
|
-
if (offset + inputSchemaLen >
|
|
228
|
+
if (offset + inputSchemaLen > plaintext.length) {
|
|
139
229
|
throw new Error("State token truncated (input schema)");
|
|
140
230
|
}
|
|
141
|
-
const inputSchemaBytes =
|
|
231
|
+
const inputSchemaBytes = copyAligned(offset, inputSchemaLen);
|
|
142
232
|
|
|
143
233
|
return { stateBytes, schemaBytes, inputSchemaBytes, createdAt };
|
|
144
234
|
}
|
package/src/http/types.ts
CHANGED
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import type { ExternalLocationConfig } from "../external.js";
|
|
5
|
-
import type { DispatchHook } from "../types.js";
|
|
4
|
+
import type { ExternalLocationConfig, UploadUrlProvider } from "../external.js";
|
|
5
|
+
import type { DispatchHook, ServeStartHook } from "../types.js";
|
|
6
6
|
import type { AuthenticateFn, OAuthResourceMetadata } from "./auth.js";
|
|
7
7
|
|
|
8
8
|
/** Configuration options for createHttpHandler(). */
|
|
9
9
|
export interface HttpHandlerOptions {
|
|
10
10
|
/** URL path prefix for all endpoints. Default: "" (root). */
|
|
11
11
|
prefix?: string;
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/** XChaCha20-Poly1305 master key (32 bytes) used to seal stream state
|
|
13
|
+
* tokens. A random 32-byte key is generated if omitted (tokens won't
|
|
14
|
+
* survive a restart or load-balance across workers). */
|
|
15
|
+
tokenKey?: Uint8Array;
|
|
14
16
|
/** State token time-to-live in seconds. Default: 3600 (1 hour). 0 disables TTL checks. */
|
|
15
17
|
tokenTtl?: number;
|
|
16
18
|
/** CORS allowed origins. If set, CORS headers are added to all responses. */
|
|
17
19
|
corsOrigins?: string;
|
|
20
|
+
/** Access-Control-Max-Age value in seconds for preflight OPTIONS responses. Default: 7200 (2 hours). null omits the header. */
|
|
21
|
+
corsMaxAge?: number | null;
|
|
18
22
|
/** Maximum request body size in bytes. Advertised via VGI-Max-Request-Bytes header. */
|
|
19
23
|
maxRequestBytes?: number;
|
|
20
|
-
/**
|
|
24
|
+
/** Cap on the post-decompression size of a `Content-Encoding: zstd`
|
|
25
|
+
* request body, in bytes. Defends against zstd decompression bombs:
|
|
26
|
+
* a tiny compressed frame can declare a huge decompressed size and
|
|
27
|
+
* blow up the server before {@link maxRequestBytes} ever sees the
|
|
28
|
+
* payload. When omitted, defaults to `maxRequestBytes * 16` if that
|
|
29
|
+
* is set, otherwise unbounded. */
|
|
30
|
+
maxDecompressedRequestBytes?: number;
|
|
31
|
+
/** Maximum bytes before a producer stream emits a continuation token.
|
|
32
|
+
*
|
|
33
|
+
* @deprecated Use {@link maxResponseBytes} instead. The cap now governs all
|
|
34
|
+
* HTTP method responses (unary, exchange, producer), not just producer streams.
|
|
35
|
+
*/
|
|
21
36
|
maxStreamResponseBytes?: number;
|
|
37
|
+
/** HTTP body cap. Hard for unary and stream-exchange (overshoot surfaces
|
|
38
|
+
* as 200 + X-VGI-RPC-Error EXCEPTION batch). Soft for producer streams
|
|
39
|
+
* (overshoot mints a continuation token). Externalised payloads do not
|
|
40
|
+
* count toward this — they leave only tiny pointer batches on the wire.
|
|
41
|
+
* Advertised via VGI-Max-Response-Bytes. Undefined = unbounded. */
|
|
42
|
+
maxResponseBytes?: number;
|
|
43
|
+
/** Cap on bytes uploaded to external storage during one HTTP response.
|
|
44
|
+
* Always hard — externalised uploads have no escape valve. Advertised via
|
|
45
|
+
* VGI-Max-Externalized-Response-Bytes. Undefined = unbounded. */
|
|
46
|
+
maxExternalizedResponseBytes?: number;
|
|
22
47
|
/** Server ID included in response metadata. Random if omitted. */
|
|
23
48
|
serverId?: string;
|
|
24
49
|
/** Custom state serializer for stream state objects. Default: JSON with BigInt support. */
|
|
@@ -32,18 +57,57 @@ export interface HttpHandlerOptions {
|
|
|
32
57
|
oauthResourceMetadata?: OAuthResourceMetadata;
|
|
33
58
|
/** Optional dispatch hook for observability (tracing, metrics). */
|
|
34
59
|
dispatchHook?: DispatchHook;
|
|
60
|
+
/** Optional lifecycle hook fired once on the first dispatched request.
|
|
61
|
+
* Mirrors Python's on_serve_start; lazy-firing keeps it fork-safe for
|
|
62
|
+
* pre-fork servers. */
|
|
63
|
+
onServeStart?: ServeStartHook;
|
|
35
64
|
/** Enable HTML landing page at GET {prefix}/. Default: true. */
|
|
36
65
|
enableLandingPage?: boolean;
|
|
37
66
|
/** Enable HTML describe/API reference page at GET {prefix}/describe. Default: true. */
|
|
38
67
|
enableDescribePage?: boolean;
|
|
39
68
|
/** Enable HTML 404 page for unmatched GET routes. Default: true. */
|
|
40
69
|
enableNotFoundPage?: boolean;
|
|
70
|
+
/** Enable JSON health endpoint at GET {prefix}/health. Default: true. */
|
|
71
|
+
enableHealthEndpoint?: boolean;
|
|
41
72
|
/** Protocol name shown in HTML pages. Defaults to the Protocol's name. */
|
|
42
73
|
protocolName?: string;
|
|
74
|
+
/** Operator-supplied protocol-contract version label, surfaced on every
|
|
75
|
+
* access-log record so dashboards and alerts can key off contract
|
|
76
|
+
* changes. Mirrors the Python `RpcServer(..., protocol_version=...)`
|
|
77
|
+
* argument. */
|
|
78
|
+
protocolVersion?: string;
|
|
43
79
|
/** URL to service's source repository, shown in landing/describe pages. */
|
|
44
80
|
repositoryUrl?: string;
|
|
45
81
|
/** External storage config for externalizing large response batches. */
|
|
46
82
|
externalLocation?: ExternalLocationConfig;
|
|
83
|
+
/** Provider for vending pre-signed upload URLs to clients via {prefix}/__upload_url__/init. */
|
|
84
|
+
uploadUrlProvider?: UploadUrlProvider;
|
|
85
|
+
/** Optional advertised maximum upload size, surfaced via VGI-Max-Upload-Bytes. */
|
|
86
|
+
maxUploadBytes?: number;
|
|
87
|
+
/** OAuth scope for PKCE authorization requests. Default: "openid email". */
|
|
88
|
+
oauthPkceScope?: string;
|
|
89
|
+
/** Allowed return-to origins for external frontend redirects. Default: Set(["https://cupola.query-farm.services"]). */
|
|
90
|
+
allowedReturnOrigins?: ReadonlySet<string>;
|
|
91
|
+
|
|
92
|
+
/** Enable opt-in sticky sessions on this HTTP handler. When enabled the
|
|
93
|
+
* server advertises `VGI-Sticky-Enabled: true` (capability discovery),
|
|
94
|
+
* honours `VGI-Session` / `VGI-Session-Accept` headers, and exposes a
|
|
95
|
+
* `DELETE {prefix}/__session__` teardown endpoint. Default: false. */
|
|
96
|
+
enableSticky?: boolean;
|
|
97
|
+
/** Default session TTL in seconds when `ctx.openSession` is called without
|
|
98
|
+
* an explicit `ttl` override. Default: 300. */
|
|
99
|
+
stickyDefaultTtl?: number;
|
|
100
|
+
/** Headers the server emits as `VGI-Echo-<name>: <value>` on the
|
|
101
|
+
* session-opening response. A conformant client captures them and replays
|
|
102
|
+
* them on every subsequent request in the session — used for
|
|
103
|
+
* client-driven routing (e.g. `fly-force-instance-id` on Fly.io). */
|
|
104
|
+
stickyEchoHeaders?: Record<string, string>;
|
|
105
|
+
/** Internal — invoked once at handler creation with a {@link DrainHandle}
|
|
106
|
+
* when sticky is enabled. Conformance fixtures use this to wire up the
|
|
107
|
+
* test-only `/__test_drain__` admin endpoint without the library
|
|
108
|
+
* exposing the registry directly. Production code should hold the
|
|
109
|
+
* handle returned by a future `createHttpHandlerWithDrainHandle` helper. */
|
|
110
|
+
_onStickyHandle?: (handle: import("./sticky.js").DrainHandle) => void;
|
|
47
111
|
}
|
|
48
112
|
|
|
49
113
|
/** Serializer for stream state objects stored in state tokens. */
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
+
export { AccessLogHook, type AccessLogSink, FdSink } from "./access-log.js";
|
|
4
5
|
export { AuthContext } from "./auth.js";
|
|
5
6
|
export * from "./client/index.js";
|
|
6
7
|
export {
|
|
7
8
|
DESCRIBE_METHOD_NAME,
|
|
8
9
|
DESCRIBE_VERSION,
|
|
9
10
|
DESCRIBE_VERSION_KEY,
|
|
11
|
+
ERROR_KIND_KEY,
|
|
10
12
|
LOG_EXTRA_KEY,
|
|
11
13
|
LOG_LEVEL_KEY,
|
|
12
14
|
LOG_MESSAGE_KEY,
|
|
@@ -14,11 +16,21 @@ export {
|
|
|
14
16
|
REQUEST_ID_KEY,
|
|
15
17
|
REQUEST_VERSION,
|
|
16
18
|
REQUEST_VERSION_KEY,
|
|
19
|
+
RPC_ERROR_HEADER,
|
|
17
20
|
RPC_METHOD_KEY,
|
|
18
21
|
SERVER_ID_KEY,
|
|
19
22
|
STATE_KEY,
|
|
20
23
|
} from "./constants.js";
|
|
21
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
ERROR_KIND_METHOD_NOT_IMPLEMENTED,
|
|
26
|
+
ERROR_KIND_SERVER_DRAINING,
|
|
27
|
+
ERROR_KIND_SESSION_LOST,
|
|
28
|
+
MethodNotImplementedError,
|
|
29
|
+
RpcError,
|
|
30
|
+
ServerDrainingError,
|
|
31
|
+
SessionLostError,
|
|
32
|
+
VersionError,
|
|
33
|
+
} from "./errors.js";
|
|
22
34
|
export {
|
|
23
35
|
type ExternalLocationConfig,
|
|
24
36
|
type ExternalStorage,
|
|
@@ -54,6 +66,25 @@ export {
|
|
|
54
66
|
type XfccElement,
|
|
55
67
|
type XfccValidateFn,
|
|
56
68
|
} from "./http/index.js";
|
|
69
|
+
export {
|
|
70
|
+
acquireLock,
|
|
71
|
+
computeHash as launcherComputeHash,
|
|
72
|
+
defaultStateDir,
|
|
73
|
+
type FileLockHandle,
|
|
74
|
+
type GcResult,
|
|
75
|
+
gcStateDir,
|
|
76
|
+
type LaunchConfig,
|
|
77
|
+
launch,
|
|
78
|
+
probeSocket,
|
|
79
|
+
type ServeUnixHandle,
|
|
80
|
+
type ServeUnixOptions,
|
|
81
|
+
type SocketPaths,
|
|
82
|
+
type StatusRow,
|
|
83
|
+
serveUnix,
|
|
84
|
+
socketPaths,
|
|
85
|
+
statusRows,
|
|
86
|
+
tryAcquireLock,
|
|
87
|
+
} from "./launcher/index.js";
|
|
57
88
|
export { Protocol } from "./protocol.js";
|
|
58
89
|
export {
|
|
59
90
|
bool,
|
|
@@ -62,10 +93,16 @@ export {
|
|
|
62
93
|
float32,
|
|
63
94
|
inferParamTypes,
|
|
64
95
|
int,
|
|
96
|
+
int8,
|
|
97
|
+
int16,
|
|
65
98
|
int32,
|
|
66
99
|
type SchemaLike,
|
|
67
100
|
str,
|
|
68
101
|
toSchema,
|
|
102
|
+
uint8,
|
|
103
|
+
uint16,
|
|
104
|
+
uint32,
|
|
105
|
+
uint64,
|
|
69
106
|
} from "./schema.js";
|
|
70
107
|
export { VgiRpcServer } from "./server.js";
|
|
71
108
|
export {
|
|
@@ -83,5 +120,7 @@ export {
|
|
|
83
120
|
OutputCollector,
|
|
84
121
|
type ProducerFn,
|
|
85
122
|
type ProducerInit,
|
|
123
|
+
type ServeStartHook,
|
|
124
|
+
TransportKind,
|
|
86
125
|
type UnaryHandler,
|
|
87
126
|
} from "./types.js";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hash a worker tuple (argv + cwd + filtered env) into a deterministic
|
|
6
|
+
* 16-hex-character identifier.
|
|
7
|
+
*
|
|
8
|
+
* Cross-language contract — must match Python's `vgi_rpc.launcher.compute_hash`
|
|
9
|
+
* byte-for-byte so the same worker tuple resolves to the same socket path
|
|
10
|
+
* regardless of which language's launcher discovered it first. The
|
|
11
|
+
* canonical form is:
|
|
12
|
+
*
|
|
13
|
+
* ```python
|
|
14
|
+
* canonical = {
|
|
15
|
+
* "cmd": list(worker_argv),
|
|
16
|
+
* "cwd": cwd if cwd is not None else os.getcwd(),
|
|
17
|
+
* "env": {k: v for k, v in sorted(os.environ.items()) if k.startswith("VGI_RPC_")},
|
|
18
|
+
* }
|
|
19
|
+
* payload = json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
20
|
+
* sha256(payload).hexdigest()[:16]
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* `scripts/regenerate_launcher_parity_vectors.py` in vgi-rpc-python emits a
|
|
24
|
+
* golden vector table; the parity test in `test/launcher.hash.test.ts`
|
|
25
|
+
* asserts byte equality against it.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const HASH_LEN = 16;
|
|
29
|
+
|
|
30
|
+
/** Recursively stringify with sorted object keys and `,`/`:` separators —
|
|
31
|
+
* the JS equivalent of `json.dumps(..., sort_keys=True, separators=(",",":"))`.
|
|
32
|
+
* We can't reuse `JSON.stringify` because the V8 implementation preserves
|
|
33
|
+
* insertion order rather than sorting. */
|
|
34
|
+
function canonicalJson(value: unknown): string {
|
|
35
|
+
if (value === null) return "null";
|
|
36
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
37
|
+
if (typeof value === "number") {
|
|
38
|
+
// Python json emits integers without a trailing `.0` and floats with the
|
|
39
|
+
// shortest round-trippable form. JS `JSON.stringify` matches this for
|
|
40
|
+
// both integer-valued and finite floats; `Infinity`/`NaN` would diverge
|
|
41
|
+
// (Python raises) but they shouldn't occur in launcher payloads.
|
|
42
|
+
return JSON.stringify(value);
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === "object") {
|
|
49
|
+
const keys = Object.keys(value as Record<string, unknown>).sort();
|
|
50
|
+
const parts = keys.map((k) => `${JSON.stringify(k)}:${canonicalJson((value as Record<string, unknown>)[k])}`);
|
|
51
|
+
return `{${parts.join(",")}}`;
|
|
52
|
+
}
|
|
53
|
+
throw new TypeError(`canonicalJson: unsupported type ${typeof value}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function sha256Hex(data: Uint8Array): Promise<string> {
|
|
57
|
+
// Web Crypto digest accepts BufferSource; copy into a fresh ArrayBuffer
|
|
58
|
+
// to dodge SharedArrayBuffer constraints in some runtimes.
|
|
59
|
+
const buf = new ArrayBuffer(data.byteLength);
|
|
60
|
+
new Uint8Array(buf).set(data);
|
|
61
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
62
|
+
return Array.from(new Uint8Array(digest))
|
|
63
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
64
|
+
.join("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compute the 16-hex-char tuple hash for a worker.
|
|
69
|
+
*
|
|
70
|
+
* @param workerArgv The worker command and its arguments.
|
|
71
|
+
* @param cwd Working directory; defaults to `process.cwd()`.
|
|
72
|
+
* @param env Process environment; defaults to `process.env`. Only
|
|
73
|
+
* keys starting with `VGI_RPC_` participate in the hash —
|
|
74
|
+
* workers that differ only in unrelated env (PATH,
|
|
75
|
+
* HOME, …) intentionally share a worker.
|
|
76
|
+
*/
|
|
77
|
+
export async function computeHash(
|
|
78
|
+
workerArgv: readonly string[],
|
|
79
|
+
cwd?: string,
|
|
80
|
+
env?: Record<string, string | undefined>,
|
|
81
|
+
): Promise<string> {
|
|
82
|
+
const cwdValue = cwd !== undefined ? cwd : process.cwd();
|
|
83
|
+
const sourceEnv = env ?? (process.env as Record<string, string | undefined>);
|
|
84
|
+
|
|
85
|
+
const filteredEnv: Record<string, string> = {};
|
|
86
|
+
for (const key of Object.keys(sourceEnv)) {
|
|
87
|
+
if (key.startsWith("VGI_RPC_")) {
|
|
88
|
+
const v = sourceEnv[key];
|
|
89
|
+
if (v !== undefined) filteredEnv[key] = v;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const canonical = {
|
|
94
|
+
cmd: [...workerArgv],
|
|
95
|
+
cwd: cwdValue,
|
|
96
|
+
env: filteredEnv,
|
|
97
|
+
};
|
|
98
|
+
const payload = new TextEncoder().encode(canonicalJson(canonical));
|
|
99
|
+
const hex = await sha256Hex(payload);
|
|
100
|
+
return hex.slice(0, HASH_LEN);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Exposed for parity-test fixtures that need to inspect the canonical form. */
|
|
104
|
+
export const _internal = { canonicalJson };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AF_UNIX worker launcher — TypeScript port of `vgi_rpc.launcher`.
|
|
6
|
+
*
|
|
7
|
+
* Two halves:
|
|
8
|
+
*
|
|
9
|
+
* - **Coordination** ({@link launch}, {@link computeHash}, {@link gcStateDir},
|
|
10
|
+
* {@link statusRows}): spawn-or-reuse a long-running worker process for a
|
|
11
|
+
* given command tuple, returning the AF_UNIX socket path the caller
|
|
12
|
+
* should connect to. Hash and on-disk layout match the Python
|
|
13
|
+
* implementation byte-for-byte so workers in any language under the
|
|
14
|
+
* same tuple resolve to the same socket.
|
|
15
|
+
*
|
|
16
|
+
* - **Worker runner** ({@link serveUnix}): bind a Unix socket and serve a
|
|
17
|
+
* {@link Protocol} via per-connection IPC streams. Implements the
|
|
18
|
+
* `--unix PATH` / `--idle-timeout SEC` / `UNIX:<path>` contract so
|
|
19
|
+
* launchers (Python or TS) can spawn TS workers transparently.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export { computeHash } from "./hash.js";
|
|
23
|
+
export { type LaunchConfig, launch } from "./launch.js";
|
|
24
|
+
export { acquireLock, type FileLockHandle, tryAcquireLock } from "./lock.js";
|
|
25
|
+
export { type ServeUnixHandle, type ServeUnixOptions, serveUnix } from "./serve-unix.js";
|
|
26
|
+
export {
|
|
27
|
+
defaultStateDir,
|
|
28
|
+
type GcResult,
|
|
29
|
+
gcStateDir,
|
|
30
|
+
probeSocket,
|
|
31
|
+
type SocketPaths,
|
|
32
|
+
type StatusRow,
|
|
33
|
+
socketPaths,
|
|
34
|
+
statusRows,
|
|
35
|
+
} from "./state.js";
|