@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.
Files changed (177) hide show
  1. package/dist/access-log.d.ts +55 -0
  2. package/dist/access-log.d.ts.map +1 -0
  3. package/dist/arrow/impl-arrowjs/index.d.ts +96 -0
  4. package/dist/arrow/impl-arrowjs/index.d.ts.map +1 -0
  5. package/dist/arrow/impl-flechette/index.d.ts +102 -0
  6. package/dist/arrow/impl-flechette/index.d.ts.map +1 -0
  7. package/dist/arrow/impl-flechette/message-meta.d.ts +11 -0
  8. package/dist/arrow/impl-flechette/message-meta.d.ts.map +1 -0
  9. package/dist/arrow/index.d.ts +4 -0
  10. package/dist/arrow/index.d.ts.map +1 -0
  11. package/dist/arrow/predicates.d.ts +44 -0
  12. package/dist/arrow/predicates.d.ts.map +1 -0
  13. package/dist/arrow/types.d.ts +62 -0
  14. package/dist/arrow/types.d.ts.map +1 -0
  15. package/dist/auth.d.ts +5 -0
  16. package/dist/auth.d.ts.map +1 -1
  17. package/dist/client/capabilities.d.ts +25 -0
  18. package/dist/client/capabilities.d.ts.map +1 -0
  19. package/dist/client/connect.d.ts +10 -0
  20. package/dist/client/connect.d.ts.map +1 -1
  21. package/dist/client/introspect.d.ts +21 -0
  22. package/dist/client/introspect.d.ts.map +1 -1
  23. package/dist/client/ipc.d.ts +8 -2
  24. package/dist/client/ipc.d.ts.map +1 -1
  25. package/dist/client/oauth.d.ts +9 -0
  26. package/dist/client/oauth.d.ts.map +1 -1
  27. package/dist/client/pipe.d.ts +24 -0
  28. package/dist/client/pipe.d.ts.map +1 -1
  29. package/dist/client/stream.d.ts +19 -2
  30. package/dist/client/stream.d.ts.map +1 -1
  31. package/dist/client/types.d.ts +23 -0
  32. package/dist/client/types.d.ts.map +1 -1
  33. package/dist/client/uploadUrl.d.ts +25 -0
  34. package/dist/client/uploadUrl.d.ts.map +1 -0
  35. package/dist/constants.d.ts +30 -2
  36. package/dist/constants.d.ts.map +1 -1
  37. package/dist/crypto.d.ts +22 -0
  38. package/dist/crypto.d.ts.map +1 -0
  39. package/dist/dispatch/describe.d.ts +10 -6
  40. package/dist/dispatch/describe.d.ts.map +1 -1
  41. package/dist/dispatch/stream.d.ts +2 -2
  42. package/dist/dispatch/stream.d.ts.map +1 -1
  43. package/dist/dispatch/unary.d.ts +2 -2
  44. package/dist/dispatch/unary.d.ts.map +1 -1
  45. package/dist/errors.d.ts +64 -1
  46. package/dist/errors.d.ts.map +1 -1
  47. package/dist/external.d.ts +27 -5
  48. package/dist/external.d.ts.map +1 -1
  49. package/dist/http/auth.d.ts +13 -0
  50. package/dist/http/auth.d.ts.map +1 -1
  51. package/dist/http/bearer.d.ts.map +1 -1
  52. package/dist/http/common.d.ts +43 -7
  53. package/dist/http/common.d.ts.map +1 -1
  54. package/dist/http/dispatch.d.ts +20 -2
  55. package/dist/http/dispatch.d.ts.map +1 -1
  56. package/dist/http/handler.d.ts.map +1 -1
  57. package/dist/http/index.d.ts +1 -0
  58. package/dist/http/index.d.ts.map +1 -1
  59. package/dist/http/jwt.d.ts +1 -0
  60. package/dist/http/jwt.d.ts.map +1 -1
  61. package/dist/http/mtls.d.ts +9 -1
  62. package/dist/http/mtls.d.ts.map +1 -1
  63. package/dist/http/oauth-pkce.d.ts +141 -0
  64. package/dist/http/oauth-pkce.d.ts.map +1 -0
  65. package/dist/http/pages.d.ts +3 -0
  66. package/dist/http/pages.d.ts.map +1 -1
  67. package/dist/http/sticky.d.ts +124 -0
  68. package/dist/http/sticky.d.ts.map +1 -0
  69. package/dist/http/token.d.ts +43 -12
  70. package/dist/http/token.d.ts.map +1 -1
  71. package/dist/http/types.d.ts +68 -5
  72. package/dist/http/types.d.ts.map +1 -1
  73. package/dist/index.d.ts +6 -4
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +1275 -3511
  76. package/dist/index.js.map +20 -38
  77. package/dist/launcher/hash.d.ts +22 -0
  78. package/dist/launcher/hash.d.ts.map +1 -0
  79. package/dist/launcher/index.d.ts +23 -0
  80. package/dist/launcher/index.d.ts.map +1 -0
  81. package/dist/launcher/launch.d.ts +27 -0
  82. package/dist/launcher/launch.d.ts.map +1 -0
  83. package/dist/launcher/lock.d.ts +19 -0
  84. package/dist/launcher/lock.d.ts.map +1 -0
  85. package/dist/launcher/serve-unix.d.ts +55 -0
  86. package/dist/launcher/serve-unix.d.ts.map +1 -0
  87. package/dist/launcher/state.d.ts +71 -0
  88. package/dist/launcher/state.d.ts.map +1 -0
  89. package/dist/otel.d.ts.map +1 -1
  90. package/dist/protocol.d.ts +19 -2
  91. package/dist/protocol.d.ts.map +1 -1
  92. package/dist/schema.d.ts +45 -18
  93. package/dist/schema.d.ts.map +1 -1
  94. package/dist/server.d.ts +23 -2
  95. package/dist/server.d.ts.map +1 -1
  96. package/dist/types.d.ts +270 -12
  97. package/dist/types.d.ts.map +1 -1
  98. package/dist/util/gzip.d.ts +10 -0
  99. package/dist/util/gzip.d.ts.map +1 -0
  100. package/dist/util/schema.d.ts +3 -15
  101. package/dist/util/schema.d.ts.map +1 -1
  102. package/dist/util/web-crypto.d.ts +22 -0
  103. package/dist/util/web-crypto.d.ts.map +1 -0
  104. package/dist/util/zstd.d.ts +26 -3
  105. package/dist/util/zstd.d.ts.map +1 -1
  106. package/dist/wire/opaque.d.ts +11 -0
  107. package/dist/wire/opaque.d.ts.map +1 -0
  108. package/dist/wire/reader.d.ts +5 -5
  109. package/dist/wire/reader.d.ts.map +1 -1
  110. package/dist/wire/request.d.ts +11 -3
  111. package/dist/wire/request.d.ts.map +1 -1
  112. package/dist/wire/response.d.ts +6 -6
  113. package/dist/wire/response.d.ts.map +1 -1
  114. package/dist/wire/writer.d.ts +49 -39
  115. package/dist/wire/writer.d.ts.map +1 -1
  116. package/package.json +35 -21
  117. package/src/access-log.ts +200 -0
  118. package/src/arrow/impl-arrowjs/index.ts +433 -0
  119. package/src/arrow/impl-flechette/index.ts +414 -0
  120. package/src/arrow/impl-flechette/message-meta.ts +174 -0
  121. package/src/arrow/index.ts +89 -0
  122. package/src/arrow/predicates.ts +56 -0
  123. package/src/arrow/types.ts +73 -0
  124. package/src/auth.ts +5 -0
  125. package/src/client/capabilities.ts +84 -0
  126. package/src/client/connect.ts +113 -26
  127. package/src/client/introspect.ts +74 -38
  128. package/src/client/ipc.ts +37 -27
  129. package/src/client/oauth.ts +9 -0
  130. package/src/client/pipe.ts +36 -9
  131. package/src/client/stream.ts +43 -20
  132. package/src/client/types.ts +23 -0
  133. package/src/client/uploadUrl.ts +169 -0
  134. package/src/constants.ts +34 -2
  135. package/src/crypto.ts +95 -0
  136. package/src/dispatch/describe.ts +146 -107
  137. package/src/dispatch/stream.ts +53 -24
  138. package/src/dispatch/unary.ts +5 -4
  139. package/src/errors.ts +87 -0
  140. package/src/external.ts +49 -30
  141. package/src/http/auth.ts +13 -0
  142. package/src/http/bearer.ts +2 -5
  143. package/src/http/common.ts +91 -23
  144. package/src/http/dispatch.ts +373 -46
  145. package/src/http/handler.ts +790 -68
  146. package/src/http/index.ts +1 -0
  147. package/src/http/jwt.ts +1 -0
  148. package/src/http/mtls.ts +25 -3
  149. package/src/http/oauth-pkce.ts +1035 -0
  150. package/src/http/pages.ts +30 -15
  151. package/src/http/sticky.ts +429 -0
  152. package/src/http/token.ts +170 -75
  153. package/src/http/types.ts +69 -5
  154. package/src/index.ts +40 -1
  155. package/src/launcher/hash.ts +104 -0
  156. package/src/launcher/index.ts +35 -0
  157. package/src/launcher/launch.ts +284 -0
  158. package/src/launcher/lock.ts +171 -0
  159. package/src/launcher/serve-unix.ts +386 -0
  160. package/src/launcher/state.ts +257 -0
  161. package/src/otel.ts +39 -33
  162. package/src/protocol.ts +30 -3
  163. package/src/schema.ts +107 -56
  164. package/src/server.ts +196 -20
  165. package/src/types.ts +376 -18
  166. package/src/util/gzip.ts +63 -0
  167. package/src/util/schema.ts +4 -22
  168. package/src/util/web-crypto.ts +98 -0
  169. package/src/util/zstd.ts +133 -14
  170. package/src/wire/opaque.ts +37 -0
  171. package/src/wire/reader.ts +5 -4
  172. package/src/wire/request.ts +67 -8
  173. package/src/wire/response.ts +51 -85
  174. package/src/wire/writer.ts +165 -69
  175. package/dist/util/conform.d.ts +0 -18
  176. package/dist/util/conform.d.ts.map +0 -1
  177. 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 { type RecordBatch, RecordBatchReader, RecordBatchStreamWriter, type Schema } from "@query-farm/apache-arrow";
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?: { algorithm: "zstd"; level?: number };
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: RecordBatch): boolean {
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: Schema, url: string, sha256?: string): RecordBatch {
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: RecordBatch): Uint8Array {
99
- const writer = new RecordBatchStreamWriter();
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: RecordBatch): number {
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
- const writer = new RecordBatchStreamWriter();
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: RecordBatch,
143
+ batch: VgiBatch,
126
144
  config?: ExternalLocationConfig | null,
127
- ): Promise<RecordBatch> {
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: RecordBatch,
181
+ batch: VgiBatch,
164
182
  config?: ExternalLocationConfig | null,
165
- ): Promise<RecordBatch> {
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
- data = new Uint8Array(zstdDecompress(data));
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 reader = await RecordBatchReader.from(data);
202
- await reader.open();
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;
@@ -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
- const bufA = enc.encode(a);
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
  /**
@@ -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 { type RecordBatch, RecordBatchReader, RecordBatchStreamWriter, type Schema } from "@query-farm/apache-arrow";
5
- import { conformBatchToSchema } from "../util/conform.js";
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
- /** Serialize a schema + batches into a complete IPC stream as Uint8Array. */
20
- export function serializeIpcStream(schema: Schema, batches: RecordBatch[]): Uint8Array {
21
- const writer = new RecordBatchStreamWriter();
22
- writer.reset(undefined, schema);
23
- for (const batch of batches) {
24
- writer.write(conformBatchToSchema(batch, schema));
25
- }
26
- writer.close();
27
- return writer.toUint8Array(true);
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
- /** Create a Response with Arrow IPC content type. Casts Uint8Array for TS lib compat. */
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: Schema; batch: RecordBatch }> {
39
- const reader = await RecordBatchReader.from(body);
40
- await reader.open();
41
- const schema = reader.schema;
42
- if (!schema) {
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
- const batches = reader.readAll();
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
  }