@query-farm/vgi-rpc 0.3.3 → 0.4.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/auth.d.ts +13 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/client/connect.d.ts.map +1 -1
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/introspect.d.ts +1 -0
- package/dist/client/introspect.d.ts.map +1 -1
- package/dist/client/oauth.d.ts +26 -0
- package/dist/client/oauth.d.ts.map +1 -0
- package/dist/client/stream.d.ts +2 -0
- package/dist/client/stream.d.ts.map +1 -1
- package/dist/client/types.d.ts +2 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/dispatch/stream.d.ts.map +1 -1
- package/dist/http/auth.d.ts +21 -0
- package/dist/http/auth.d.ts.map +1 -0
- package/dist/http/common.d.ts.map +1 -1
- package/dist/http/dispatch.d.ts +2 -0
- package/dist/http/dispatch.d.ts.map +1 -1
- package/dist/http/handler.d.ts.map +1 -1
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/jwt.d.ts +21 -0
- package/dist/http/jwt.d.ts.map +1 -0
- package/dist/http/types.d.ts +5 -0
- package/dist/http/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1479 -71
- package/dist/index.js.map +20 -15
- package/dist/types.d.ts +8 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/util/conform.d.ts +5 -3
- package/dist/util/conform.d.ts.map +1 -1
- package/dist/wire/response.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/auth.ts +31 -0
- package/src/client/connect.ts +15 -1
- package/src/client/index.ts +2 -0
- package/src/client/introspect.ts +14 -2
- package/src/client/oauth.ts +74 -0
- package/src/client/stream.ts +12 -0
- package/src/client/types.ts +2 -0
- package/src/dispatch/stream.ts +11 -5
- package/src/http/auth.ts +47 -0
- package/src/http/common.ts +1 -6
- package/src/http/dispatch.ts +6 -4
- package/src/http/handler.ts +41 -1
- package/src/http/index.ts +4 -0
- package/src/http/jwt.ts +66 -0
- package/src/http/types.ts +6 -0
- package/src/index.ts +7 -0
- package/src/types.ts +17 -3
- package/src/util/conform.ts +68 -5
- package/src/wire/response.ts +28 -14
package/src/http/jwt.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import * as oauth from "oauth4webapi";
|
|
5
|
+
import { AuthContext } from "../auth.js";
|
|
6
|
+
import type { AuthenticateFn } from "./auth.js";
|
|
7
|
+
|
|
8
|
+
export interface JwtAuthenticateOptions {
|
|
9
|
+
/** The expected `iss` claim (also used to discover AS metadata). */
|
|
10
|
+
issuer: string;
|
|
11
|
+
/** The expected `aud` claim. */
|
|
12
|
+
audience: string;
|
|
13
|
+
/** Explicit JWKS URI. If omitted, discovered from issuer metadata. */
|
|
14
|
+
jwksUri?: string;
|
|
15
|
+
/** JWT claim to use as the principal. Default: "sub". */
|
|
16
|
+
principalClaim?: string;
|
|
17
|
+
/** AuthContext domain. Default: "jwt". */
|
|
18
|
+
domain?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create an AuthenticateFn that validates JWT Bearer tokens using oauth4webapi.
|
|
23
|
+
*
|
|
24
|
+
* On first call, discovers the Authorization Server metadata from the issuer
|
|
25
|
+
* to obtain the JWKS URI (unless `jwksUri` is provided directly).
|
|
26
|
+
*/
|
|
27
|
+
export function jwtAuthenticate(options: JwtAuthenticateOptions): AuthenticateFn {
|
|
28
|
+
const principalClaim = options.principalClaim ?? "sub";
|
|
29
|
+
const domain = options.domain ?? "jwt";
|
|
30
|
+
const audience = options.audience;
|
|
31
|
+
|
|
32
|
+
let asPromise: Promise<oauth.AuthorizationServer> | null = null;
|
|
33
|
+
|
|
34
|
+
async function getAuthorizationServer(): Promise<oauth.AuthorizationServer> {
|
|
35
|
+
if (options.jwksUri) {
|
|
36
|
+
return {
|
|
37
|
+
issuer: options.issuer as `https://${string}`,
|
|
38
|
+
jwks_uri: options.jwksUri,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const issuerUrl = new URL(options.issuer);
|
|
42
|
+
const response = await oauth.discoveryRequest(issuerUrl);
|
|
43
|
+
return oauth.processDiscoveryResponse(issuerUrl, response);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return async function authenticate(request: Request): Promise<AuthContext> {
|
|
47
|
+
if (!asPromise) {
|
|
48
|
+
asPromise = getAuthorizationServer();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let as: oauth.AuthorizationServer;
|
|
52
|
+
try {
|
|
53
|
+
as = await asPromise;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Reset so next request retries discovery
|
|
56
|
+
asPromise = null;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// validateJwtAccessToken throws on failure, returns claims on success
|
|
61
|
+
const claims = await oauth.validateJwtAccessToken(as, request, audience);
|
|
62
|
+
const principal = (claims[principalClaim] as string | undefined) ?? null;
|
|
63
|
+
|
|
64
|
+
return new AuthContext(domain, true, principal, claims as unknown as Record<string, any>);
|
|
65
|
+
};
|
|
66
|
+
}
|
package/src/http/types.ts
CHANGED
|
@@ -1,6 +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 type { AuthenticateFn, OAuthResourceMetadata } from "./auth.js";
|
|
5
|
+
|
|
4
6
|
/** Configuration options for createHttpHandler(). */
|
|
5
7
|
export interface HttpHandlerOptions {
|
|
6
8
|
/** URL path prefix for all endpoints. Default: "/vgi" */
|
|
@@ -22,6 +24,10 @@ export interface HttpHandlerOptions {
|
|
|
22
24
|
/** zstd compression level for responses (1-22). If set, responses are
|
|
23
25
|
* compressed when the client sends Accept-Encoding: zstd. */
|
|
24
26
|
compressionLevel?: number;
|
|
27
|
+
/** Optional authentication callback. Called for each request before dispatch. */
|
|
28
|
+
authenticate?: AuthenticateFn;
|
|
29
|
+
/** Optional RFC 9728 OAuth Protected Resource Metadata. Served at well-known endpoint. */
|
|
30
|
+
oauthResourceMetadata?: OAuthResourceMetadata;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/** Serializer for stream state objects stored in state tokens. */
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
+
export { AuthContext } from "./auth.js";
|
|
4
5
|
export * from "./client/index.js";
|
|
5
6
|
export {
|
|
6
7
|
DESCRIBE_METHOD_NAME,
|
|
@@ -20,9 +21,14 @@ export {
|
|
|
20
21
|
export { RpcError, VersionError } from "./errors.js";
|
|
21
22
|
export {
|
|
22
23
|
ARROW_CONTENT_TYPE,
|
|
24
|
+
type AuthenticateFn,
|
|
23
25
|
createHttpHandler,
|
|
24
26
|
type HttpHandlerOptions,
|
|
27
|
+
type JwtAuthenticateOptions,
|
|
25
28
|
jsonStateSerializer,
|
|
29
|
+
jwtAuthenticate,
|
|
30
|
+
type OAuthResourceMetadata,
|
|
31
|
+
oauthResourceMetadataToJson,
|
|
26
32
|
type StateSerializer,
|
|
27
33
|
type UnpackedToken,
|
|
28
34
|
unpackStateToken,
|
|
@@ -42,6 +48,7 @@ export {
|
|
|
42
48
|
} from "./schema.js";
|
|
43
49
|
export { VgiRpcServer } from "./server.js";
|
|
44
50
|
export {
|
|
51
|
+
type CallContext,
|
|
45
52
|
type ExchangeFn,
|
|
46
53
|
type ExchangeInit,
|
|
47
54
|
type HeaderInit,
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import { RecordBatch, recordBatchFromArrays, type Schema } from "@query-farm/apache-arrow";
|
|
5
|
+
import { AuthContext } from "./auth.js";
|
|
5
6
|
import { buildLogBatch, coerceInt64 } from "./wire/response.js";
|
|
6
7
|
|
|
7
8
|
export enum MethodType {
|
|
@@ -14,6 +15,11 @@ export interface LogContext {
|
|
|
14
15
|
clientLog(level: string, message: string, extra?: Record<string, string>): void;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/** Extended context with authentication info, available to handlers. */
|
|
19
|
+
export interface CallContext extends LogContext {
|
|
20
|
+
readonly auth: AuthContext;
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
/** Handler for unary (request-response) RPC methods. */
|
|
18
24
|
export type UnaryHandler = (
|
|
19
25
|
params: Record<string, any>,
|
|
@@ -61,7 +67,7 @@ export interface EmittedBatch {
|
|
|
61
67
|
* Accumulates output batches during a produce/exchange call.
|
|
62
68
|
* Enforces that exactly one data batch is emitted per call (plus any number of log batches).
|
|
63
69
|
*/
|
|
64
|
-
export class OutputCollector implements
|
|
70
|
+
export class OutputCollector implements CallContext {
|
|
65
71
|
private _batches: EmittedBatch[] = [];
|
|
66
72
|
private _dataBatchIdx: number | null = null;
|
|
67
73
|
private _finished = false;
|
|
@@ -69,12 +75,20 @@ export class OutputCollector implements LogContext {
|
|
|
69
75
|
private _outputSchema: Schema;
|
|
70
76
|
private _serverId: string;
|
|
71
77
|
private _requestId: string | null;
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
readonly auth: AuthContext;
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
outputSchema: Schema,
|
|
82
|
+
producerMode = true,
|
|
83
|
+
serverId = "",
|
|
84
|
+
requestId: string | null = null,
|
|
85
|
+
authContext?: AuthContext,
|
|
86
|
+
) {
|
|
74
87
|
this._outputSchema = outputSchema;
|
|
75
88
|
this._producerMode = producerMode;
|
|
76
89
|
this._serverId = serverId;
|
|
77
90
|
this._requestId = requestId;
|
|
91
|
+
this.auth = authContext ?? AuthContext.anonymous();
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
get outputSchema(): Schema {
|
package/src/util/conform.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
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 {
|
|
5
|
+
type DataType,
|
|
6
|
+
makeData,
|
|
7
|
+
RecordBatch,
|
|
8
|
+
type Schema,
|
|
9
|
+
Struct,
|
|
10
|
+
Type,
|
|
11
|
+
vectorFromArray,
|
|
12
|
+
} from "@query-farm/apache-arrow";
|
|
13
|
+
|
|
14
|
+
/** Return true when the source type's values can be losslessly read and
|
|
15
|
+
* re-encoded into the target type (e.g., int32 → float64). */
|
|
16
|
+
function needsValueCast(src: DataType, dst: DataType): boolean {
|
|
17
|
+
if (src.typeId === dst.typeId) return false;
|
|
18
|
+
// Same broad family (e.g. Float → Float64) — clone is sufficient.
|
|
19
|
+
if (src.constructor === dst.constructor) return false;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Check if a type is a numeric type we can cast between.
|
|
24
|
+
* Uses typeId instead of instanceof because IPC-deserialized types
|
|
25
|
+
* may be generic (e.g., Int_ instead of Int64). */
|
|
26
|
+
function isNumeric(t: DataType): boolean {
|
|
27
|
+
return t.typeId === Type.Int || t.typeId === Type.Float;
|
|
28
|
+
}
|
|
5
29
|
|
|
6
30
|
/**
|
|
7
31
|
* Rebuild a batch's data to match the given schema's field types.
|
|
@@ -12,13 +36,52 @@ import { RecordBatch, Struct, makeData, type Schema } from "@query-farm/apache-a
|
|
|
12
36
|
* match the writer's schema. Cloning each child Data with the schema's field
|
|
13
37
|
* type fixes the type metadata while preserving the underlying buffers.
|
|
14
38
|
*
|
|
15
|
-
* This is also used to cast compatible input types (e.g.,
|
|
16
|
-
*
|
|
17
|
-
* declared input schema.
|
|
39
|
+
* This is also used to cast compatible input types (e.g., int32→float64,
|
|
40
|
+
* float32→float64) when the input batch schema doesn't exactly match the
|
|
41
|
+
* method's declared input schema. When the underlying buffer layout differs
|
|
42
|
+
* (e.g., 4-byte int32 vs 8-byte float64), we read the values and build a
|
|
43
|
+
* new vector with the target type.
|
|
18
44
|
*/
|
|
19
45
|
export function conformBatchToSchema(batch: RecordBatch, schema: Schema): RecordBatch {
|
|
20
46
|
if (batch.numRows === 0) return batch;
|
|
21
|
-
|
|
47
|
+
|
|
48
|
+
// Validate field count and names match before attempting any cast.
|
|
49
|
+
if (batch.schema.fields.length !== schema.fields.length) {
|
|
50
|
+
throw new TypeError(`Field count mismatch: expected ${schema.fields.length}, got ${batch.schema.fields.length}`);
|
|
51
|
+
}
|
|
52
|
+
for (let i = 0; i < schema.fields.length; i++) {
|
|
53
|
+
if (batch.schema.fields[i].name !== schema.fields[i].name) {
|
|
54
|
+
throw new TypeError(
|
|
55
|
+
`Field name mismatch at index ${i}: expected '${schema.fields[i].name}', got '${batch.schema.fields[i].name}'`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const children = schema.fields.map((f, i) => {
|
|
61
|
+
const srcChild = batch.data.children[i];
|
|
62
|
+
const srcType = srcChild.type;
|
|
63
|
+
const dstType = f.type;
|
|
64
|
+
|
|
65
|
+
if (!needsValueCast(srcType, dstType)) {
|
|
66
|
+
return srcChild.clone(dstType);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Numeric → numeric: read values and rebuild with target type.
|
|
70
|
+
if (isNumeric(srcType) && isNumeric(dstType)) {
|
|
71
|
+
// Read source values via the batch's column vector.
|
|
72
|
+
const col = batch.getChildAt(i)!;
|
|
73
|
+
const values: number[] = [];
|
|
74
|
+
for (let r = 0; r < batch.numRows; r++) {
|
|
75
|
+
const v = col.get(r);
|
|
76
|
+
values.push(typeof v === "bigint" ? Number(v) : (v as number));
|
|
77
|
+
}
|
|
78
|
+
return vectorFromArray(values, dstType).data[0];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fallback: clone type metadata (works for same-layout types).
|
|
82
|
+
return srcChild.clone(dstType);
|
|
83
|
+
});
|
|
84
|
+
|
|
22
85
|
const structType = new Struct(schema.fields);
|
|
23
86
|
const data = makeData({
|
|
24
87
|
type: structType,
|
package/src/wire/response.ts
CHANGED
|
@@ -133,25 +133,39 @@ export function buildLogBatch(
|
|
|
133
133
|
return buildEmptyBatch(schema, metadata);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Recursively create empty (0-row) Data for any Arrow type,
|
|
138
|
+
* including complex types (Struct, List, FixedSizeList, Map).
|
|
139
|
+
*/
|
|
140
|
+
function makeEmptyData(type: DataType): Data {
|
|
141
|
+
if (DataType.isStruct(type)) {
|
|
142
|
+
const children = type.children.map((f: Field) => makeEmptyData(f.type));
|
|
143
|
+
return makeData({ type, length: 0, children, nullCount: 0 });
|
|
144
|
+
}
|
|
145
|
+
if (DataType.isList(type)) {
|
|
146
|
+
const childData = makeEmptyData(type.children[0].type);
|
|
147
|
+
return makeData({ type, length: 0, children: [childData], nullCount: 0, valueOffsets: new Int32Array([0]) } as any);
|
|
148
|
+
}
|
|
149
|
+
if (DataType.isFixedSizeList(type)) {
|
|
150
|
+
const childData = makeEmptyData(type.children[0].type);
|
|
151
|
+
return makeData({ type, length: 0, child: childData, nullCount: 0 } as any);
|
|
152
|
+
}
|
|
153
|
+
if (DataType.isMap(type)) {
|
|
154
|
+
const entryType = type.children[0]?.type;
|
|
155
|
+
const entryData = entryType
|
|
156
|
+
? makeEmptyData(entryType)
|
|
157
|
+
: makeData({ type: new Struct([]), length: 0, children: [], nullCount: 0 });
|
|
158
|
+
return makeData({ type, length: 0, children: [entryData], nullCount: 0, valueOffsets: new Int32Array([0]) } as any);
|
|
159
|
+
}
|
|
160
|
+
return makeData({ type, length: 0, nullCount: 0 });
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
/**
|
|
137
164
|
* Build a 0-row batch from a schema with metadata.
|
|
138
165
|
* Used for error/log batches.
|
|
139
166
|
*/
|
|
140
167
|
export function buildEmptyBatch(schema: Schema, metadata?: Map<string, string>): RecordBatch {
|
|
141
|
-
const children = schema.fields.map((f: Field) =>
|
|
142
|
-
return makeData({ type: f.type, length: 0, nullCount: 0 });
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
if (schema.fields.length === 0) {
|
|
146
|
-
const structType = new Struct(schema.fields);
|
|
147
|
-
const data = makeData({
|
|
148
|
-
type: structType,
|
|
149
|
-
length: 0,
|
|
150
|
-
children: [],
|
|
151
|
-
nullCount: 0,
|
|
152
|
-
});
|
|
153
|
-
return new RecordBatch(schema, data, metadata);
|
|
154
|
-
}
|
|
168
|
+
const children = schema.fields.map((f: Field) => makeEmptyData(f.type));
|
|
155
169
|
|
|
156
170
|
const structType = new Struct(schema.fields);
|
|
157
171
|
const data = makeData({
|