@query-farm/vgi-rpc 0.6.4 → 0.7.1
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 +55 -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/auth.d.ts +5 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/client/capabilities.d.ts +25 -0
- package/dist/client/capabilities.d.ts.map +1 -0
- package/dist/client/connect.d.ts +10 -0
- package/dist/client/connect.d.ts.map +1 -1
- package/dist/client/introspect.d.ts +21 -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/oauth.d.ts +9 -0
- package/dist/client/oauth.d.ts.map +1 -1
- package/dist/client/pipe.d.ts +24 -0
- package/dist/client/pipe.d.ts.map +1 -1
- package/dist/client/stream.d.ts +19 -2
- package/dist/client/stream.d.ts.map +1 -1
- package/dist/client/types.d.ts +23 -0
- package/dist/client/types.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 +30 -2
- 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 +64 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/external.d.ts +27 -5
- package/dist/external.d.ts.map +1 -1
- package/dist/http/auth.d.ts +13 -0
- package/dist/http/auth.d.ts.map +1 -1
- package/dist/http/bearer.d.ts.map +1 -1
- package/dist/http/common.d.ts +43 -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/jwt.d.ts +1 -0
- package/dist/http/jwt.d.ts.map +1 -1
- package/dist/http/mtls.d.ts +9 -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 +43 -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 -3511
- package/dist/index.js.map +20 -38
- 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 +55 -0
- package/dist/launcher/serve-unix.d.ts.map +1 -0
- package/dist/launcher/state.d.ts +71 -0
- package/dist/launcher/state.d.ts.map +1 -0
- package/dist/otel.d.ts.map +1 -1
- package/dist/protocol.d.ts +19 -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 +270 -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 +35 -21
- package/src/access-log.ts +200 -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/auth.ts +5 -0
- package/src/client/capabilities.ts +84 -0
- package/src/client/connect.ts +113 -26
- package/src/client/introspect.ts +74 -38
- package/src/client/ipc.ts +37 -27
- package/src/client/oauth.ts +9 -0
- package/src/client/pipe.ts +36 -9
- package/src/client/stream.ts +43 -20
- package/src/client/types.ts +23 -0
- package/src/client/uploadUrl.ts +169 -0
- package/src/constants.ts +34 -2
- 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 +87 -0
- package/src/external.ts +49 -30
- package/src/http/auth.ts +13 -0
- package/src/http/bearer.ts +2 -5
- package/src/http/common.ts +91 -23
- package/src/http/dispatch.ts +373 -46
- package/src/http/handler.ts +790 -68
- package/src/http/index.ts +1 -0
- package/src/http/jwt.ts +1 -0
- package/src/http/mtls.ts +25 -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 +170 -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 +386 -0
- package/src/launcher/state.ts +257 -0
- package/src/otel.ts +39 -33
- package/src/protocol.ts +30 -3
- package/src/schema.ts +107 -56
- package/src/server.ts +196 -20
- package/src/types.ts +376 -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/errors.ts
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
/** Error thrown when the server encounters an RPC protocol error. */
|
|
5
5
|
export class RpcError extends Error {
|
|
6
6
|
constructor(
|
|
7
|
+
/** Remote error class name (e.g. `"ValueError"`). */
|
|
7
8
|
public readonly errorType: string,
|
|
9
|
+
/** Human-readable message from the remote error. */
|
|
8
10
|
public readonly errorMessage: string,
|
|
11
|
+
/** Remote stack-trace text, or an empty string when unavailable. */
|
|
9
12
|
public readonly remoteTraceback: string,
|
|
10
13
|
) {
|
|
11
14
|
super(`${errorType}: ${errorMessage}`);
|
|
@@ -20,3 +23,87 @@ export class VersionError extends Error {
|
|
|
20
23
|
this.name = "VersionError";
|
|
21
24
|
}
|
|
22
25
|
}
|
|
26
|
+
|
|
27
|
+
/** `vgi_rpc.error_kind` batch-metadata value for {@link MethodNotImplementedError}.
|
|
28
|
+
* Mirrors Python's `vgi_rpc.metadata.ERROR_KIND_*` constants. */
|
|
29
|
+
export const ERROR_KIND_METHOD_NOT_IMPLEMENTED = "method_not_implemented";
|
|
30
|
+
/** `vgi_rpc.error_kind` batch-metadata value for {@link SessionLostError}. */
|
|
31
|
+
export const ERROR_KIND_SESSION_LOST = "session_lost";
|
|
32
|
+
/** `vgi_rpc.error_kind` batch-metadata value for {@link ServerDrainingError}. */
|
|
33
|
+
export const ERROR_KIND_SERVER_DRAINING = "server_draining";
|
|
34
|
+
export const ERROR_KIND_PROTOCOL_VERSION_MISMATCH = "protocol_version_mismatch";
|
|
35
|
+
|
|
36
|
+
/** Raised when the client's declared `vgi_rpc.protocol_version` is
|
|
37
|
+
* incompatible with the server's. Subclass of `VersionError` so existing
|
|
38
|
+
* catch sites continue to write a typed error stream and keep serving.
|
|
39
|
+
* Carries a directional message that tells the reader which side to
|
|
40
|
+
* upgrade. Mirrors Python's `vgi_rpc.rpc.ProtocolVersionError`. */
|
|
41
|
+
export class ProtocolVersionError extends VersionError {
|
|
42
|
+
static readonly errorKind = ERROR_KIND_PROTOCOL_VERSION_MISMATCH;
|
|
43
|
+
readonly errorKind = ERROR_KIND_PROTOCOL_VERSION_MISMATCH;
|
|
44
|
+
constructor(message: string) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = "ProtocolVersionError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
51
|
+
|
|
52
|
+
/** Parse a canonical semver string into `[major, minor, patch]`. Throws on
|
|
53
|
+
* any input that isn't `MAJOR.MINOR.PATCH` with non-negative integers and
|
|
54
|
+
* no leading zeros (except literal `0`). No prereleases, no build metadata.
|
|
55
|
+
* Mirrors Python's `vgi_rpc.metadata.parse_version`. */
|
|
56
|
+
export function parseProtocolVersion(value: string): [number, number, number] {
|
|
57
|
+
const m = SEMVER_REGEX.exec(value);
|
|
58
|
+
if (!m) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Invalid protocol version '${value}': expected canonical semver ` +
|
|
61
|
+
"MAJOR.MINOR.PATCH with non-negative integers and no leading zeros " +
|
|
62
|
+
"(no prereleases or build metadata).",
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Raised when a client invokes a method the server does not implement.
|
|
69
|
+
*
|
|
70
|
+
* Mirrors Python's `vgi_rpc.rpc.MethodNotImplementedError`. The static
|
|
71
|
+
* `errorKind` is hoisted onto the error batch metadata as
|
|
72
|
+
* `vgi_rpc.error_kind` so clients can branch on the typed marker without
|
|
73
|
+
* string-matching the message.
|
|
74
|
+
*/
|
|
75
|
+
export class MethodNotImplementedError extends Error {
|
|
76
|
+
/** Typed `vgi_rpc.error_kind` marker for this error class. */
|
|
77
|
+
static readonly errorKind = ERROR_KIND_METHOD_NOT_IMPLEMENTED;
|
|
78
|
+
/** Typed `vgi_rpc.error_kind` marker hoisted onto the error batch metadata. */
|
|
79
|
+
readonly errorKind = ERROR_KIND_METHOD_NOT_IMPLEMENTED;
|
|
80
|
+
constructor(message: string) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.name = "MethodNotImplementedError";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Raised when a sticky session token is malformed, expired, evicted, or
|
|
87
|
+
* bound to a different worker / principal. HTTP-only. */
|
|
88
|
+
export class SessionLostError extends Error {
|
|
89
|
+
/** Typed `vgi_rpc.error_kind` marker for this error class. */
|
|
90
|
+
static readonly errorKind = ERROR_KIND_SESSION_LOST;
|
|
91
|
+
/** Typed `vgi_rpc.error_kind` marker hoisted onto the error batch metadata. */
|
|
92
|
+
readonly errorKind = ERROR_KIND_SESSION_LOST;
|
|
93
|
+
constructor(message: string) {
|
|
94
|
+
super(message);
|
|
95
|
+
this.name = "SessionLostError";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Raised when `ctx.openSession` is called while the server is draining. */
|
|
100
|
+
export class ServerDrainingError extends Error {
|
|
101
|
+
/** Typed `vgi_rpc.error_kind` marker for this error class. */
|
|
102
|
+
static readonly errorKind = ERROR_KIND_SERVER_DRAINING;
|
|
103
|
+
/** Typed `vgi_rpc.error_kind` marker hoisted onto the error batch metadata. */
|
|
104
|
+
readonly errorKind = ERROR_KIND_SERVER_DRAINING;
|
|
105
|
+
constructor(message: string) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.name = "ServerDrainingError";
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/external.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* download URL and SHA-256 checksum in metadata.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { deserializeBatch, serializeBatch, type VgiBatch, type VgiSchema } from "./arrow/index.js";
|
|
14
14
|
import { LOCATION_KEY, LOCATION_SHA256_KEY, LOG_LEVEL_KEY } from "./constants.js";
|
|
15
15
|
import { zstdCompress, zstdDecompress } from "./util/zstd.js";
|
|
16
16
|
import { buildEmptyBatch } from "./wire/response.js";
|
|
@@ -25,6 +25,28 @@ export interface ExternalStorage {
|
|
|
25
25
|
upload(data: Uint8Array, contentEncoding: string): Promise<string>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** A pre-signed PUT/GET URL pair for client-side data upload. */
|
|
29
|
+
export interface UploadUrl {
|
|
30
|
+
/** Pre-signed PUT URL the client uploads to. */
|
|
31
|
+
uploadUrl: string;
|
|
32
|
+
/** Pre-signed GET URL the server fetches from. */
|
|
33
|
+
downloadUrl: string;
|
|
34
|
+
/** Expiration time (UTC) for the URL pair. */
|
|
35
|
+
expiresAt: Date;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generates pre-signed upload URL pairs for client-vended externalization.
|
|
40
|
+
*
|
|
41
|
+
* Implementations must be safe to call from multiple concurrent requests.
|
|
42
|
+
* Object lifecycle is the operator's responsibility — uploaded objects are
|
|
43
|
+
* not automatically deleted by vgi-rpc.
|
|
44
|
+
*/
|
|
45
|
+
export interface UploadUrlProvider {
|
|
46
|
+
/** Allocate one upload/download URL pair. */
|
|
47
|
+
generateUploadUrl(): Promise<UploadUrl> | UploadUrl;
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
/** Configuration for external storage of large batches. */
|
|
29
51
|
export interface ExternalLocationConfig {
|
|
30
52
|
/** Storage backend for uploading. */
|
|
@@ -32,7 +54,12 @@ export interface ExternalLocationConfig {
|
|
|
32
54
|
/** Minimum batch byte size to trigger externalization. Default: 1MB. */
|
|
33
55
|
externalizeThresholdBytes?: number;
|
|
34
56
|
/** Optional zstd compression for uploaded data. */
|
|
35
|
-
compression?: {
|
|
57
|
+
compression?: {
|
|
58
|
+
/** Compression algorithm; only `"zstd"` is currently supported. */
|
|
59
|
+
algorithm: "zstd";
|
|
60
|
+
/** zstd compression level. Default: 3. */
|
|
61
|
+
level?: number;
|
|
62
|
+
};
|
|
36
63
|
/** URL validator called before fetching. Throw to reject. Default: HTTPS-only. */
|
|
37
64
|
urlValidator?: ((url: string) => void) | null;
|
|
38
65
|
}
|
|
@@ -70,7 +97,7 @@ async function sha256Hex(data: Uint8Array): Promise<string> {
|
|
|
70
97
|
// ---------------------------------------------------------------------------
|
|
71
98
|
|
|
72
99
|
/** Returns true if the batch is a zero-row pointer to external data. */
|
|
73
|
-
export function isExternalLocationBatch(batch:
|
|
100
|
+
export function isExternalLocationBatch(batch: VgiBatch): boolean {
|
|
74
101
|
if (batch.numRows !== 0) return false;
|
|
75
102
|
const meta = batch.metadata;
|
|
76
103
|
if (!meta) return false;
|
|
@@ -82,7 +109,7 @@ export function isExternalLocationBatch(batch: RecordBatch): boolean {
|
|
|
82
109
|
// ---------------------------------------------------------------------------
|
|
83
110
|
|
|
84
111
|
/** Create a zero-row pointer batch with location URL and optional SHA-256. */
|
|
85
|
-
export function makeExternalLocationBatch(schema:
|
|
112
|
+
export function makeExternalLocationBatch(schema: VgiSchema, url: string, sha256?: string): VgiBatch {
|
|
86
113
|
const metadata = new Map<string, string>();
|
|
87
114
|
metadata.set(LOCATION_KEY, url);
|
|
88
115
|
if (sha256) {
|
|
@@ -95,22 +122,13 @@ export function makeExternalLocationBatch(schema: Schema, url: string, sha256?:
|
|
|
95
122
|
// IPC serialization helpers
|
|
96
123
|
// ---------------------------------------------------------------------------
|
|
97
124
|
|
|
98
|
-
function serializeBatchToIpc(batch:
|
|
99
|
-
|
|
100
|
-
writer.reset(undefined, batch.schema);
|
|
101
|
-
writer.write(batch);
|
|
102
|
-
writer.close();
|
|
103
|
-
return writer.toUint8Array(true);
|
|
125
|
+
function serializeBatchToIpc(batch: VgiBatch): Uint8Array {
|
|
126
|
+
return serializeBatch(batch);
|
|
104
127
|
}
|
|
105
128
|
|
|
106
|
-
function batchByteSize(batch:
|
|
107
|
-
// Arrow TS data.byteLength doesn't reflect actual data size.
|
|
129
|
+
function batchByteSize(batch: VgiBatch): number {
|
|
108
130
|
// Estimate from IPC serialization size for threshold check.
|
|
109
|
-
|
|
110
|
-
writer.reset(undefined, batch.schema);
|
|
111
|
-
writer.write(batch);
|
|
112
|
-
writer.close();
|
|
113
|
-
return writer.toUint8Array(true).byteLength;
|
|
131
|
+
return serializeBatch(batch).byteLength;
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
// ---------------------------------------------------------------------------
|
|
@@ -122,9 +140,9 @@ function batchByteSize(batch: RecordBatch): number {
|
|
|
122
140
|
* Returns the original batch unchanged if below threshold or no config.
|
|
123
141
|
*/
|
|
124
142
|
export async function maybeExternalizeBatch(
|
|
125
|
-
batch:
|
|
143
|
+
batch: VgiBatch,
|
|
126
144
|
config?: ExternalLocationConfig | null,
|
|
127
|
-
): Promise<
|
|
145
|
+
): Promise<VgiBatch> {
|
|
128
146
|
if (!config?.storage) return batch;
|
|
129
147
|
if (batch.numRows === 0) return batch;
|
|
130
148
|
|
|
@@ -140,7 +158,7 @@ export async function maybeExternalizeBatch(
|
|
|
140
158
|
// Optionally compress
|
|
141
159
|
let contentEncoding = "";
|
|
142
160
|
if (config.compression?.algorithm === "zstd") {
|
|
143
|
-
ipcData = zstdCompress(ipcData, config.compression.level ?? 3) as Uint8Array;
|
|
161
|
+
ipcData = (await zstdCompress(ipcData, config.compression.level ?? 3)) as Uint8Array;
|
|
144
162
|
contentEncoding = "zstd";
|
|
145
163
|
}
|
|
146
164
|
|
|
@@ -160,9 +178,9 @@ export async function maybeExternalizeBatch(
|
|
|
160
178
|
* Returns the original batch unchanged if not a pointer or no config.
|
|
161
179
|
*/
|
|
162
180
|
export async function resolveExternalLocation(
|
|
163
|
-
batch:
|
|
181
|
+
batch: VgiBatch,
|
|
164
182
|
config?: ExternalLocationConfig | null,
|
|
165
|
-
): Promise<
|
|
183
|
+
): Promise<VgiBatch> {
|
|
166
184
|
if (!config) return batch;
|
|
167
185
|
if (!isExternalLocationBatch(batch)) return batch;
|
|
168
186
|
|
|
@@ -182,10 +200,14 @@ export async function resolveExternalLocation(
|
|
|
182
200
|
}
|
|
183
201
|
let data = new Uint8Array(await response.arrayBuffer());
|
|
184
202
|
|
|
185
|
-
// Decompress if needed
|
|
203
|
+
// Decompress if needed. Cap the decompressed size at 16x the
|
|
204
|
+
// compressed body — generous for typical Arrow IPC zstd ratios but
|
|
205
|
+
// tight enough that a tiny response cannot inflate to multi-GB.
|
|
206
|
+
// Mirrors Python's external_fetch.fetch_url.
|
|
186
207
|
const contentEncoding = response.headers.get("Content-Encoding");
|
|
187
208
|
if (contentEncoding === "zstd") {
|
|
188
|
-
|
|
209
|
+
const cap = data.byteLength * 16;
|
|
210
|
+
data = new Uint8Array(await zstdDecompress(data, cap));
|
|
189
211
|
}
|
|
190
212
|
|
|
191
213
|
// Verify SHA-256 if present
|
|
@@ -198,12 +220,9 @@ export async function resolveExternalLocation(
|
|
|
198
220
|
}
|
|
199
221
|
|
|
200
222
|
// Parse IPC stream
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const resolved = reader.next();
|
|
204
|
-
if (!resolved || resolved.done || !resolved.value) {
|
|
223
|
+
const resolved = deserializeBatch(data);
|
|
224
|
+
if (resolved.numRows === 0 && resolved.schema.fields.length === 0) {
|
|
205
225
|
throw new Error(`No data batch found in external IPC stream from ${url}`);
|
|
206
226
|
}
|
|
207
|
-
|
|
208
|
-
return resolved.value;
|
|
227
|
+
return resolved;
|
|
209
228
|
}
|
package/src/http/auth.ts
CHANGED
|
@@ -8,14 +8,27 @@ export type AuthenticateFn = (request: Request) => AuthContext | Promise<AuthCon
|
|
|
8
8
|
|
|
9
9
|
/** RFC 9728 OAuth Protected Resource Metadata. */
|
|
10
10
|
export interface OAuthResourceMetadata {
|
|
11
|
+
/** The protected resource's canonical URL. Doubles as the base for the
|
|
12
|
+
* `/_oauth/callback` redirect URI. */
|
|
11
13
|
resource: string;
|
|
14
|
+
/** Authorization-server issuer URLs. The PKCE flow uses
|
|
15
|
+
* `authorizationServers[0]` for OIDC discovery. */
|
|
12
16
|
authorizationServers: string[];
|
|
17
|
+
/** Scopes the resource advertises. When non-empty these become the PKCE
|
|
18
|
+
* authorization request's space-joined `scope`, taking precedence over
|
|
19
|
+
* {@link HttpHandlerOptions.oauthPkceScope}. */
|
|
13
20
|
scopesSupported?: string[];
|
|
21
|
+
/** Advertised bearer methods (e.g. `["header"]`). */
|
|
14
22
|
bearerMethodsSupported?: string[];
|
|
23
|
+
/** JWS algorithms the resource accepts. */
|
|
15
24
|
resourceSigningAlgValuesSupported?: string[];
|
|
25
|
+
/** Human-readable resource name. */
|
|
16
26
|
resourceName?: string;
|
|
27
|
+
/** Documentation URL for the resource. */
|
|
17
28
|
resourceDocumentation?: string;
|
|
29
|
+
/** Policy URL for the resource. */
|
|
18
30
|
resourcePolicyUri?: string;
|
|
31
|
+
/** Terms-of-service URL for the resource. */
|
|
19
32
|
resourceTosUri?: string;
|
|
20
33
|
/** OAuth client_id that clients should use with the authorization server. */
|
|
21
34
|
clientId?: string;
|
package/src/http/bearer.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import { timingSafeEqual } from "node:crypto";
|
|
5
4
|
import type { AuthContext } from "../auth.js";
|
|
5
|
+
import { constantTimeEqual } from "../util/web-crypto.js";
|
|
6
6
|
import type { AuthenticateFn } from "./auth.js";
|
|
7
7
|
|
|
8
8
|
/** Receives the raw bearer token string, returns an AuthContext on success. Must throw on failure. */
|
|
@@ -30,10 +30,7 @@ export function bearerAuthenticate(options: { validate: BearerValidateFn }): Aut
|
|
|
30
30
|
/** Constant-time string comparison to prevent timing attacks on token lookup. */
|
|
31
31
|
function safeEqual(a: string, b: string): boolean {
|
|
32
32
|
const enc = new TextEncoder();
|
|
33
|
-
|
|
34
|
-
const bufB = enc.encode(b);
|
|
35
|
-
if (bufA.byteLength !== bufB.byteLength) return false;
|
|
36
|
-
return timingSafeEqual(bufA, bufB);
|
|
33
|
+
return constantTimeEqual(enc.encode(a), enc.encode(b));
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
/**
|
package/src/http/common.ts
CHANGED
|
@@ -1,11 +1,67 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
conformBatchToSchema,
|
|
6
|
+
deserializeBatch,
|
|
7
|
+
serializeBatches,
|
|
8
|
+
type VgiBatch,
|
|
9
|
+
type VgiSchema,
|
|
10
|
+
} from "../arrow/index.js";
|
|
11
|
+
import { RPC_ERROR_HEADER } from "../constants.js";
|
|
12
|
+
import type { CookieSpec } from "../types.js";
|
|
6
13
|
|
|
14
|
+
/** MIME type for Arrow IPC stream request and response bodies. */
|
|
7
15
|
export const ARROW_CONTENT_TYPE = "application/vnd.apache.arrow.stream";
|
|
8
16
|
|
|
17
|
+
// Sticky session header conventions (HTTP-only). Mirrors Python's
|
|
18
|
+
// `vgi_rpc.http._common`. Headers — not cookies — so multiple concurrent
|
|
19
|
+
// sessions to one host from a single client multiplex correctly.
|
|
20
|
+
export const SESSION_HEADER = "VGI-Session";
|
|
21
|
+
export const SESSION_ACCEPT_HEADER = "VGI-Session-Accept";
|
|
22
|
+
export const SESSION_CLOSE_HEADER = "VGI-Session-Close";
|
|
23
|
+
export const STICKY_ENABLED_HEADER = "VGI-Sticky-Enabled";
|
|
24
|
+
export const STICKY_DEFAULT_TTL_HEADER = "VGI-Sticky-Default-TTL";
|
|
25
|
+
export const STICKY_ECHO_HEADERS_HEADER = "VGI-Sticky-Echo-Headers";
|
|
26
|
+
|
|
27
|
+
/** Prefix the server uses to tell the client "echo this header on subsequent
|
|
28
|
+
* requests in this session". Clients capture and replay
|
|
29
|
+
* `VGI-Echo-<name>: <value>` as plain `<name>: <value>` for the session
|
|
30
|
+
* lifetime — used for client-driven routing (e.g. `fly-force-instance-id`). */
|
|
31
|
+
export const ECHO_HEADER_PREFIX = "VGI-Echo-";
|
|
32
|
+
|
|
33
|
+
/** Framework-managed sticky session teardown endpoint path component.
|
|
34
|
+
* `DELETE {prefix}/__session__` idempotently closes the session referenced
|
|
35
|
+
* by the request's `VGI-Session` header. */
|
|
36
|
+
export const SESSION_ENDPOINT = "__session__";
|
|
37
|
+
|
|
38
|
+
/** Serialize a CookieSpec into a Set-Cookie header value. */
|
|
39
|
+
export function formatSetCookieHeader(c: CookieSpec): string {
|
|
40
|
+
const parts: string[] = [];
|
|
41
|
+
if (c.delete) {
|
|
42
|
+
parts.push(`${c.name}=`);
|
|
43
|
+
parts.push("Max-Age=0");
|
|
44
|
+
} else {
|
|
45
|
+
parts.push(`${c.name}=${c.value}`);
|
|
46
|
+
if (c.maxAge !== undefined) parts.push(`Max-Age=${c.maxAge}`);
|
|
47
|
+
if (c.expires) parts.push(`Expires=${c.expires.toUTCString()}`);
|
|
48
|
+
}
|
|
49
|
+
if (c.path) parts.push(`Path=${c.path}`);
|
|
50
|
+
if (c.domain) parts.push(`Domain=${c.domain}`);
|
|
51
|
+
if (c.secure) parts.push("Secure");
|
|
52
|
+
if (c.httpOnly) parts.push("HttpOnly");
|
|
53
|
+
if (c.sameSite) parts.push(`SameSite=${c.sameSite}`);
|
|
54
|
+
if (c.partitioned) parts.push("Partitioned");
|
|
55
|
+
return parts.join("; ");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Append Set-Cookie headers for each queued CookieSpec onto an existing Headers object. */
|
|
59
|
+
export function appendCookieHeaders(headers: Headers, cookies: readonly CookieSpec[]): void {
|
|
60
|
+
for (const c of cookies) {
|
|
61
|
+
headers.append("Set-Cookie", formatSetCookieHeader(c));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
9
65
|
export class HttpRpcError extends Error {
|
|
10
66
|
constructor(
|
|
11
67
|
message: string,
|
|
@@ -16,35 +72,47 @@ export class HttpRpcError extends Error {
|
|
|
16
72
|
}
|
|
17
73
|
}
|
|
18
74
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Serialize a schema + batches into a complete IPC stream as Uint8Array.
|
|
77
|
+
*
|
|
78
|
+
* A single IPC stream is `[schema_msg, batch_msg, batch_msg, ..., EOS]`.
|
|
79
|
+
* Each backend implements `serializeBatches` to write that atomically —
|
|
80
|
+
* arrow-js via `RecordBatchStreamWriter`, flechette via `tablesToIPC`
|
|
81
|
+
* (added in our flechette fork). Naive concatenation of per-batch streams
|
|
82
|
+
* produces multiple EOS markers and breaks readers.
|
|
83
|
+
*/
|
|
84
|
+
export function serializeIpcStream(schema: VgiSchema, batches: VgiBatch[]): Uint8Array {
|
|
85
|
+
const conformed = batches.map((b) => conformBatchToSchema(b, schema));
|
|
86
|
+
return serializeBatches(schema, conformed);
|
|
28
87
|
}
|
|
29
88
|
|
|
30
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* Create a Response with Arrow IPC content type.
|
|
91
|
+
*
|
|
92
|
+
* Server errors (status 500) are translated to HTTP 200 with an
|
|
93
|
+
* ``X-VGI-RPC-Error: true`` header so that clients which discard
|
|
94
|
+
* response bodies on 5xx still receive the Arrow IPC error metadata.
|
|
95
|
+
* Client errors (400, 401, 404, 415) are passed through unchanged.
|
|
96
|
+
*/
|
|
31
97
|
export function arrowResponse(body: Uint8Array, status = 200, extraHeaders?: Headers): Response {
|
|
32
98
|
const headers = extraHeaders ?? new Headers();
|
|
33
99
|
headers.set("Content-Type", ARROW_CONTENT_TYPE);
|
|
100
|
+
if (status === 500) {
|
|
101
|
+
headers.set(RPC_ERROR_HEADER, "true");
|
|
102
|
+
return new Response(body as unknown as BodyInit, { status: 200, headers });
|
|
103
|
+
}
|
|
34
104
|
return new Response(body as unknown as BodyInit, { status, headers });
|
|
35
105
|
}
|
|
36
106
|
|
|
37
|
-
/** Read schema + first batch from an IPC stream body. */
|
|
38
|
-
export async function readRequestFromBody(body: Uint8Array): Promise<{ schema:
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
107
|
+
/** Read schema + first batch from an IPC stream body via the facade. */
|
|
108
|
+
export async function readRequestFromBody(body: Uint8Array): Promise<{ schema: VgiSchema; batch: VgiBatch }> {
|
|
109
|
+
const batch = deserializeBatch(body);
|
|
110
|
+
// Reject only truly empty bodies. A zero-field, zero-row batch with batch
|
|
111
|
+
// metadata is a legal exchange/cancel/continuation signal — the state
|
|
112
|
+
// token rides on `batch.metadata` and downstream code (cancel detection,
|
|
113
|
+
// schema conformance gating) is built to handle it.
|
|
114
|
+
if (batch.schema.fields.length === 0 && batch.numRows === 0 && (batch.metadata?.size ?? 0) === 0) {
|
|
43
115
|
throw new HttpRpcError("Empty IPC stream: no schema", 400);
|
|
44
116
|
}
|
|
45
|
-
|
|
46
|
-
if (batches.length === 0) {
|
|
47
|
-
throw new HttpRpcError("IPC stream contains no batches", 400);
|
|
48
|
-
}
|
|
49
|
-
return { schema, batch: batches[0] };
|
|
117
|
+
return { schema: batch.schema, batch };
|
|
50
118
|
}
|