@query-farm/vgi-rpc 0.3.4 → 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/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 +1416 -46
- package/dist/index.js.map +18 -13
- package/dist/types.d.ts +8 -2
- package/dist/types.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 -3
- package/src/http/auth.ts +47 -0
- 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/wire/response.ts +28 -14
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RecordBatch, type Schema } from "@query-farm/apache-arrow";
|
|
2
|
+
import { AuthContext } from "./auth.js";
|
|
2
3
|
export declare enum MethodType {
|
|
3
4
|
UNARY = "unary",
|
|
4
5
|
STREAM = "stream"
|
|
@@ -7,6 +8,10 @@ export declare enum MethodType {
|
|
|
7
8
|
export interface LogContext {
|
|
8
9
|
clientLog(level: string, message: string, extra?: Record<string, string>): void;
|
|
9
10
|
}
|
|
11
|
+
/** Extended context with authentication info, available to handlers. */
|
|
12
|
+
export interface CallContext extends LogContext {
|
|
13
|
+
readonly auth: AuthContext;
|
|
14
|
+
}
|
|
10
15
|
/** Handler for unary (request-response) RPC methods. */
|
|
11
16
|
export type UnaryHandler = (params: Record<string, any>, ctx: LogContext) => Promise<Record<string, any>> | Record<string, any>;
|
|
12
17
|
/** Initialization function for producer streams. Returns the initial state object. */
|
|
@@ -45,7 +50,7 @@ export interface EmittedBatch {
|
|
|
45
50
|
* Accumulates output batches during a produce/exchange call.
|
|
46
51
|
* Enforces that exactly one data batch is emitted per call (plus any number of log batches).
|
|
47
52
|
*/
|
|
48
|
-
export declare class OutputCollector implements
|
|
53
|
+
export declare class OutputCollector implements CallContext {
|
|
49
54
|
private _batches;
|
|
50
55
|
private _dataBatchIdx;
|
|
51
56
|
private _finished;
|
|
@@ -53,7 +58,8 @@ export declare class OutputCollector implements LogContext {
|
|
|
53
58
|
private _outputSchema;
|
|
54
59
|
private _serverId;
|
|
55
60
|
private _requestId;
|
|
56
|
-
|
|
61
|
+
readonly auth: AuthContext;
|
|
62
|
+
constructor(outputSchema: Schema, producerMode?: boolean, serverId?: string, requestId?: string | null, authContext?: AuthContext);
|
|
57
63
|
get outputSchema(): Schema;
|
|
58
64
|
get finished(): boolean;
|
|
59
65
|
get batches(): EmittedBatch[];
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAyB,KAAK,MAAM,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/wire/response.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,WAAW,EACX,KAAK,MAAM,EAGZ,MAAM,0BAA0B,CAAC;AAGlC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAc5F;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,WAAW,CAwCb;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAiBrH;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,QAAQ,CAAC,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,WAAW,CAeb;
|
|
1
|
+
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/wire/response.ts"],"names":[],"mappings":"AAGA,OAAO,EAKL,WAAW,EACX,KAAK,MAAM,EAGZ,MAAM,0BAA0B,CAAC;AAGlC;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAc5F;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,WAAW,CAwCb;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,WAAW,CAiBrH;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,QAAQ,CAAC,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,WAAW,CAeb;AA6BD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,WAAW,CAY3F"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@query-farm/vgi-rpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://vgi-rpc-typescript.query.farm",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"src"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@query-farm/apache-arrow": "*"
|
|
25
|
+
"@query-farm/apache-arrow": "*",
|
|
26
|
+
"oauth4webapi": "^3.8.5"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|
|
28
29
|
"@biomejs/biome": "^2.4.5",
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { RpcError } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
/** Authentication context available to RPC handlers. */
|
|
7
|
+
export class AuthContext {
|
|
8
|
+
readonly domain: string;
|
|
9
|
+
readonly authenticated: boolean;
|
|
10
|
+
readonly principal: string | null;
|
|
11
|
+
readonly claims: Record<string, any>;
|
|
12
|
+
|
|
13
|
+
constructor(domain: string, authenticated: boolean, principal: string | null, claims: Record<string, any> = {}) {
|
|
14
|
+
this.domain = domain;
|
|
15
|
+
this.authenticated = authenticated;
|
|
16
|
+
this.principal = principal;
|
|
17
|
+
this.claims = claims;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Create an unauthenticated (anonymous) context. */
|
|
21
|
+
static anonymous(): AuthContext {
|
|
22
|
+
return new AuthContext("", false, null);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Throw an RpcError if this context is not authenticated. */
|
|
26
|
+
requireAuthenticated(): void {
|
|
27
|
+
if (!this.authenticated) {
|
|
28
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/client/connect.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { RecordBatch, Schema } from "@query-farm/apache-arrow";
|
|
5
5
|
import { LOG_LEVEL_KEY, STATE_KEY } from "../constants.js";
|
|
6
|
+
import { RpcError } from "../errors.js";
|
|
6
7
|
import { ARROW_CONTENT_TYPE } from "../http/common.js";
|
|
7
8
|
import { httpIntrospect, type MethodInfo, type ServiceDescription } from "./introspect.js";
|
|
8
9
|
import {
|
|
@@ -29,6 +30,7 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
29
30
|
const prefix = (options?.prefix ?? "/vgi").replace(/\/+$/, "");
|
|
30
31
|
const onLog = options?.onLog;
|
|
31
32
|
const compressionLevel = options?.compressionLevel;
|
|
33
|
+
const authorization = options?.authorization;
|
|
32
34
|
|
|
33
35
|
let methodCache: Map<string, MethodInfo> | null = null;
|
|
34
36
|
let compressFn: CompressFn | undefined;
|
|
@@ -55,6 +57,9 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
55
57
|
headers["Content-Encoding"] = "zstd";
|
|
56
58
|
headers["Accept-Encoding"] = "zstd";
|
|
57
59
|
}
|
|
60
|
+
if (authorization) {
|
|
61
|
+
headers.Authorization = authorization;
|
|
62
|
+
}
|
|
58
63
|
return headers;
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -65,6 +70,12 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
65
70
|
return content;
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
function checkAuth(resp: Response): void {
|
|
74
|
+
if (resp.status === 401) {
|
|
75
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
68
79
|
async function readResponse(resp: Response): Promise<Uint8Array<ArrayBuffer>> {
|
|
69
80
|
let body = new Uint8Array(await resp.arrayBuffer());
|
|
70
81
|
if (resp.headers.get("Content-Encoding") === "zstd" && decompressFn) {
|
|
@@ -75,7 +86,7 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
75
86
|
|
|
76
87
|
async function ensureMethodCache(): Promise<Map<string, MethodInfo>> {
|
|
77
88
|
if (methodCache) return methodCache;
|
|
78
|
-
const desc = await httpIntrospect(baseUrl, { prefix });
|
|
89
|
+
const desc = await httpIntrospect(baseUrl, { prefix, authorization });
|
|
79
90
|
methodCache = new Map(desc.methods.map((m) => [m.name, m]));
|
|
80
91
|
return methodCache;
|
|
81
92
|
}
|
|
@@ -98,6 +109,7 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
98
109
|
headers: buildHeaders(),
|
|
99
110
|
body: prepareBody(body) as unknown as BodyInit,
|
|
100
111
|
});
|
|
112
|
+
checkAuth(resp);
|
|
101
113
|
|
|
102
114
|
const responseBody = await readResponse(resp);
|
|
103
115
|
const { batches } = await readResponseBatches(responseBody);
|
|
@@ -146,6 +158,7 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
146
158
|
headers: buildHeaders(),
|
|
147
159
|
body: prepareBody(body) as unknown as BodyInit,
|
|
148
160
|
});
|
|
161
|
+
checkAuth(resp);
|
|
149
162
|
|
|
150
163
|
const responseBody = await readResponse(resp);
|
|
151
164
|
|
|
@@ -288,6 +301,7 @@ export function httpConnect(baseUrl: string, options?: HttpConnectOptions): RpcC
|
|
|
288
301
|
compressionLevel,
|
|
289
302
|
compressFn,
|
|
290
303
|
decompressFn,
|
|
304
|
+
authorization,
|
|
291
305
|
});
|
|
292
306
|
},
|
|
293
307
|
|
package/src/client/index.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
export { httpConnect, type RpcClient } from "./connect.js";
|
|
5
5
|
export { httpIntrospect, type MethodInfo, parseDescribeResponse, type ServiceDescription } from "./introspect.js";
|
|
6
|
+
export type { OAuthResourceMetadataResponse } from "./oauth.js";
|
|
7
|
+
export { fetchOAuthMetadata, httpOAuthMetadata, parseResourceMetadataUrl } from "./oauth.js";
|
|
6
8
|
export { PipeStreamSession, pipeConnect, subprocessConnect } from "./pipe.js";
|
|
7
9
|
export { HttpStreamSession } from "./stream.js";
|
|
8
10
|
export type {
|
package/src/client/introspect.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { Schema as ArrowSchema, type RecordBatch, RecordBatchReader, type Schema } from "@query-farm/apache-arrow";
|
|
5
5
|
import { DESCRIBE_METHOD_NAME, PROTOCOL_NAME_KEY } from "../constants.js";
|
|
6
|
+
import { RpcError } from "../errors.js";
|
|
6
7
|
import { ARROW_CONTENT_TYPE } from "../http/common.js";
|
|
7
8
|
import { buildRequestIpc, dispatchLogOrError, readResponseBatches } from "./ipc.js";
|
|
8
9
|
import type { LogMessage } from "./types.js";
|
|
@@ -116,16 +117,27 @@ export async function parseDescribeResponse(
|
|
|
116
117
|
/**
|
|
117
118
|
* Send a __describe__ request and return a ServiceDescription.
|
|
118
119
|
*/
|
|
119
|
-
export async function httpIntrospect(
|
|
120
|
+
export async function httpIntrospect(
|
|
121
|
+
baseUrl: string,
|
|
122
|
+
options?: { prefix?: string; authorization?: string },
|
|
123
|
+
): Promise<ServiceDescription> {
|
|
120
124
|
const prefix = options?.prefix ?? "/vgi";
|
|
121
125
|
const emptySchema = new ArrowSchema([]);
|
|
122
126
|
const body = buildRequestIpc(emptySchema, {}, DESCRIBE_METHOD_NAME);
|
|
123
127
|
|
|
128
|
+
const headers: Record<string, string> = { "Content-Type": ARROW_CONTENT_TYPE };
|
|
129
|
+
if (options?.authorization) {
|
|
130
|
+
headers.Authorization = options.authorization;
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
const response = await fetch(`${baseUrl}${prefix}/${DESCRIBE_METHOD_NAME}`, {
|
|
125
134
|
method: "POST",
|
|
126
|
-
headers
|
|
135
|
+
headers,
|
|
127
136
|
body: body as unknown as BodyInit,
|
|
128
137
|
});
|
|
138
|
+
if (response.status === 401) {
|
|
139
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
140
|
+
}
|
|
129
141
|
|
|
130
142
|
const responseBody = new Uint8Array(await response.arrayBuffer());
|
|
131
143
|
const { batches } = await readResponseBatches(responseBody);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/** RFC 9728 OAuth Protected Resource Metadata (client-side response). */
|
|
5
|
+
export interface OAuthResourceMetadataResponse {
|
|
6
|
+
resource: string;
|
|
7
|
+
authorizationServers: string[];
|
|
8
|
+
scopesSupported?: string[];
|
|
9
|
+
bearerMethodsSupported?: string[];
|
|
10
|
+
resourceName?: string;
|
|
11
|
+
resourceDocumentation?: string;
|
|
12
|
+
resourcePolicyUri?: string;
|
|
13
|
+
resourceTosUri?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseMetadataJson(json: Record<string, any>): OAuthResourceMetadataResponse {
|
|
17
|
+
const result: OAuthResourceMetadataResponse = {
|
|
18
|
+
resource: json.resource,
|
|
19
|
+
authorizationServers: json.authorization_servers,
|
|
20
|
+
};
|
|
21
|
+
if (json.scopes_supported) result.scopesSupported = json.scopes_supported;
|
|
22
|
+
if (json.bearer_methods_supported) result.bearerMethodsSupported = json.bearer_methods_supported;
|
|
23
|
+
if (json.resource_name) result.resourceName = json.resource_name;
|
|
24
|
+
if (json.resource_documentation) result.resourceDocumentation = json.resource_documentation;
|
|
25
|
+
if (json.resource_policy_uri) result.resourcePolicyUri = json.resource_policy_uri;
|
|
26
|
+
if (json.resource_tos_uri) result.resourceTosUri = json.resource_tos_uri;
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Discover OAuth Protected Resource Metadata (RFC 9728) from a vgi-rpc server.
|
|
32
|
+
* Returns `null` if the server does not serve the well-known endpoint.
|
|
33
|
+
*/
|
|
34
|
+
export async function httpOAuthMetadata(
|
|
35
|
+
baseUrl: string,
|
|
36
|
+
prefix?: string,
|
|
37
|
+
): Promise<OAuthResourceMetadataResponse | null> {
|
|
38
|
+
const effectivePrefix = (prefix ?? "/vgi").replace(/\/+$/, "");
|
|
39
|
+
const metadataUrl = `${baseUrl.replace(/\/+$/, "")}/.well-known/oauth-protected-resource${effectivePrefix}`;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return await fetchOAuthMetadata(metadataUrl);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fetch OAuth Protected Resource Metadata from an explicit metadata URL.
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchOAuthMetadata(metadataUrl: string): Promise<OAuthResourceMetadataResponse> {
|
|
52
|
+
const response = await fetch(metadataUrl);
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(`Failed to fetch OAuth metadata from ${metadataUrl}: ${response.status}`);
|
|
55
|
+
}
|
|
56
|
+
const json = await response.json();
|
|
57
|
+
return parseMetadataJson(json);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract the `resource_metadata` URL from a WWW-Authenticate Bearer challenge.
|
|
62
|
+
* Returns `null` if no resource_metadata parameter is found.
|
|
63
|
+
*/
|
|
64
|
+
export function parseResourceMetadataUrl(wwwAuthenticate: string): string | null {
|
|
65
|
+
// Parse Bearer challenge parameters per RFC 6750
|
|
66
|
+
const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
|
|
67
|
+
if (!bearerMatch) return null;
|
|
68
|
+
|
|
69
|
+
const params = bearerMatch[1];
|
|
70
|
+
const metadataMatch = params.match(/resource_metadata="([^"]+)"/);
|
|
71
|
+
if (!metadataMatch) return null;
|
|
72
|
+
|
|
73
|
+
return metadataMatch[1];
|
|
74
|
+
}
|
package/src/client/stream.ts
CHANGED
|
@@ -25,6 +25,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
25
25
|
private _compressionLevel?: number;
|
|
26
26
|
private _compressFn?: CompressFn;
|
|
27
27
|
private _decompressFn?: DecompressFn;
|
|
28
|
+
private _authorization?: string;
|
|
28
29
|
|
|
29
30
|
constructor(opts: {
|
|
30
31
|
baseUrl: string;
|
|
@@ -40,6 +41,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
40
41
|
compressionLevel?: number;
|
|
41
42
|
compressFn?: CompressFn;
|
|
42
43
|
decompressFn?: DecompressFn;
|
|
44
|
+
authorization?: string;
|
|
43
45
|
}) {
|
|
44
46
|
this._baseUrl = opts.baseUrl;
|
|
45
47
|
this._prefix = opts.prefix;
|
|
@@ -54,6 +56,7 @@ export class HttpStreamSession implements StreamSession {
|
|
|
54
56
|
this._compressionLevel = opts.compressionLevel;
|
|
55
57
|
this._compressFn = opts.compressFn;
|
|
56
58
|
this._decompressFn = opts.decompressFn;
|
|
59
|
+
this._authorization = opts.authorization;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
get header(): Record<string, any> | null {
|
|
@@ -68,6 +71,9 @@ export class HttpStreamSession implements StreamSession {
|
|
|
68
71
|
headers["Content-Encoding"] = "zstd";
|
|
69
72
|
headers["Accept-Encoding"] = "zstd";
|
|
70
73
|
}
|
|
74
|
+
if (this._authorization) {
|
|
75
|
+
headers.Authorization = this._authorization;
|
|
76
|
+
}
|
|
71
77
|
return headers;
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -154,6 +160,9 @@ export class HttpStreamSession implements StreamSession {
|
|
|
154
160
|
headers: this._buildHeaders(),
|
|
155
161
|
body: this._prepareBody(body) as unknown as BodyInit,
|
|
156
162
|
});
|
|
163
|
+
if (resp.status === 401) {
|
|
164
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
165
|
+
}
|
|
157
166
|
|
|
158
167
|
const responseBody = await this._readResponse(resp);
|
|
159
168
|
const { batches: responseBatches } = await readResponseBatches(responseBody);
|
|
@@ -261,6 +270,9 @@ export class HttpStreamSession implements StreamSession {
|
|
|
261
270
|
headers: this._buildHeaders(),
|
|
262
271
|
body: this._prepareBody(body) as unknown as BodyInit,
|
|
263
272
|
});
|
|
273
|
+
if (resp.status === 401) {
|
|
274
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
275
|
+
}
|
|
264
276
|
|
|
265
277
|
return this._readResponse(resp);
|
|
266
278
|
}
|
package/src/client/types.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface HttpConnectOptions {
|
|
|
5
5
|
prefix?: string;
|
|
6
6
|
onLog?: (msg: LogMessage) => void;
|
|
7
7
|
compressionLevel?: number;
|
|
8
|
+
/** Authorization header value (e.g. "Bearer <token>"). Sent with every request. */
|
|
9
|
+
authorization?: string;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export interface LogMessage {
|
package/src/dispatch/stream.ts
CHANGED
|
@@ -107,12 +107,20 @@ export async function dispatchStream(
|
|
|
107
107
|
let inputBatch = await reader.readNextBatch();
|
|
108
108
|
if (!inputBatch) break;
|
|
109
109
|
|
|
110
|
-
// Cast compatible input types when schema doesn't match exactly
|
|
110
|
+
// Cast compatible input types when schema doesn't match exactly.
|
|
111
|
+
// If conformance fails (e.g., completely different schemas like a dummy
|
|
112
|
+
// registration schema vs actual data), pass the original batch through —
|
|
113
|
+
// the exchange handler may handle dynamic schemas internally.
|
|
111
114
|
if (expectedInputSchema && !isProducer && inputBatch.schema !== expectedInputSchema) {
|
|
112
115
|
try {
|
|
113
116
|
inputBatch = conformBatchToSchema(inputBatch, expectedInputSchema);
|
|
114
|
-
} catch {
|
|
115
|
-
|
|
117
|
+
} catch (e) {
|
|
118
|
+
if (e instanceof TypeError) {
|
|
119
|
+
// Field name/count mismatch — propagate as error (matches Python behavior).
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
// Other conformance failures: pass through for dynamic schema handlers.
|
|
123
|
+
console.debug?.(`Schema conformance skipped: ${e instanceof Error ? e.message : e}`);
|
|
116
124
|
}
|
|
117
125
|
}
|
|
118
126
|
|
package/src/http/auth.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { AuthContext } from "../auth.js";
|
|
5
|
+
|
|
6
|
+
/** Async function that authenticates an incoming HTTP request. */
|
|
7
|
+
export type AuthenticateFn = (request: Request) => AuthContext | Promise<AuthContext>;
|
|
8
|
+
|
|
9
|
+
/** RFC 9728 OAuth Protected Resource Metadata. */
|
|
10
|
+
export interface OAuthResourceMetadata {
|
|
11
|
+
resource: string;
|
|
12
|
+
authorizationServers: string[];
|
|
13
|
+
scopesSupported?: string[];
|
|
14
|
+
bearerMethodsSupported?: string[];
|
|
15
|
+
resourceName?: string;
|
|
16
|
+
resourceDocumentation?: string;
|
|
17
|
+
resourcePolicyUri?: string;
|
|
18
|
+
resourceTosUri?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Convert OAuthResourceMetadata to RFC 9728 snake_case JSON object. */
|
|
22
|
+
export function oauthResourceMetadataToJson(metadata: OAuthResourceMetadata): Record<string, any> {
|
|
23
|
+
const json: Record<string, any> = {
|
|
24
|
+
resource: metadata.resource,
|
|
25
|
+
authorization_servers: metadata.authorizationServers,
|
|
26
|
+
};
|
|
27
|
+
if (metadata.scopesSupported) json.scopes_supported = metadata.scopesSupported;
|
|
28
|
+
if (metadata.bearerMethodsSupported) json.bearer_methods_supported = metadata.bearerMethodsSupported;
|
|
29
|
+
if (metadata.resourceName) json.resource_name = metadata.resourceName;
|
|
30
|
+
if (metadata.resourceDocumentation) json.resource_documentation = metadata.resourceDocumentation;
|
|
31
|
+
if (metadata.resourcePolicyUri) json.resource_policy_uri = metadata.resourcePolicyUri;
|
|
32
|
+
if (metadata.resourceTosUri) json.resource_tos_uri = metadata.resourceTosUri;
|
|
33
|
+
return json;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Compute the well-known path for OAuth Protected Resource Metadata. */
|
|
37
|
+
export function wellKnownPath(prefix: string): string {
|
|
38
|
+
return `/.well-known/oauth-protected-resource${prefix}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Build a WWW-Authenticate header value with optional resource_metadata URL. */
|
|
42
|
+
export function buildWwwAuthenticateHeader(metadataUrl?: string): string {
|
|
43
|
+
if (metadataUrl) {
|
|
44
|
+
return `Bearer resource_metadata="${metadataUrl}"`;
|
|
45
|
+
}
|
|
46
|
+
return "Bearer";
|
|
47
|
+
}
|
package/src/http/dispatch.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
4
|
import { RecordBatch, RecordBatchReader, Schema } from "@query-farm/apache-arrow";
|
|
5
|
+
import type { AuthContext } from "../auth.js";
|
|
5
6
|
import { STATE_KEY } from "../constants.js";
|
|
6
7
|
import { buildDescribeBatch, DESCRIBE_SCHEMA } from "../dispatch/describe.js";
|
|
7
8
|
import type { MethodDefinition } from "../types.js";
|
|
@@ -28,6 +29,7 @@ export interface DispatchContext {
|
|
|
28
29
|
serverId: string;
|
|
29
30
|
maxStreamResponseBytes?: number;
|
|
30
31
|
stateSerializer: StateSerializer;
|
|
32
|
+
authContext?: AuthContext;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/** Dispatch a __describe__ request. */
|
|
@@ -55,7 +57,7 @@ export async function httpDispatchUnary(
|
|
|
55
57
|
throw new HttpRpcError(`Method name in request '${parsed.methodName}' does not match URL '${method.name}'`, 400);
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
const out = new OutputCollector(schema, true, ctx.serverId, parsed.requestId);
|
|
60
|
+
const out = new OutputCollector(schema, true, ctx.serverId, parsed.requestId, ctx.authContext);
|
|
59
61
|
|
|
60
62
|
try {
|
|
61
63
|
const result = await method.handler!(parsed.params, out);
|
|
@@ -107,7 +109,7 @@ export async function httpDispatchStreamInit(
|
|
|
107
109
|
let headerBytes: Uint8Array | null = null;
|
|
108
110
|
if (method.headerSchema && method.headerInit) {
|
|
109
111
|
try {
|
|
110
|
-
const headerOut = new OutputCollector(method.headerSchema, true, ctx.serverId, parsed.requestId);
|
|
112
|
+
const headerOut = new OutputCollector(method.headerSchema, true, ctx.serverId, parsed.requestId, ctx.authContext);
|
|
111
113
|
const headerValues = method.headerInit(parsed.params, state, headerOut);
|
|
112
114
|
const headerBatch = buildResultBatch(method.headerSchema, headerValues, ctx.serverId, parsed.requestId);
|
|
113
115
|
const headerBatches = [...headerOut.batches.map((b) => b.batch), headerBatch];
|
|
@@ -205,7 +207,7 @@ export async function httpDispatchStreamExchange(
|
|
|
205
207
|
// Exchange path — also handles exchange-registered methods acting as
|
|
206
208
|
// producers (__isProducer=true). Use producer mode on the OutputCollector
|
|
207
209
|
// when effectiveProducer so finish() is allowed.
|
|
208
|
-
const out = new OutputCollector(outputSchema, effectiveProducer, ctx.serverId, null);
|
|
210
|
+
const out = new OutputCollector(outputSchema, effectiveProducer, ctx.serverId, null, ctx.authContext);
|
|
209
211
|
|
|
210
212
|
// Cast compatible input types (e.g., decimal→double, int32→int64)
|
|
211
213
|
const conformedBatch = conformBatchToSchema(reqBatch, inputSchema);
|
|
@@ -283,7 +285,7 @@ async function produceStreamResponse(
|
|
|
283
285
|
let estimatedBytes = 0;
|
|
284
286
|
|
|
285
287
|
while (true) {
|
|
286
|
-
const out = new OutputCollector(outputSchema, true, ctx.serverId, requestId);
|
|
288
|
+
const out = new OutputCollector(outputSchema, true, ctx.serverId, requestId, ctx.authContext);
|
|
287
289
|
|
|
288
290
|
try {
|
|
289
291
|
if (method.producerFn) {
|
package/src/http/handler.ts
CHANGED
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { Schema } from "@query-farm/apache-arrow";
|
|
6
|
+
import type { AuthContext } from "../auth.js";
|
|
6
7
|
import { DESCRIBE_METHOD_NAME } from "../constants.js";
|
|
7
8
|
import type { Protocol } from "../protocol.js";
|
|
8
9
|
import { MethodType } from "../types.js";
|
|
9
10
|
import { zstdCompress, zstdDecompress } from "../util/zstd.js";
|
|
10
11
|
import { buildErrorBatch } from "../wire/response.js";
|
|
12
|
+
import { buildWwwAuthenticateHeader, oauthResourceMetadataToJson, wellKnownPath } from "./auth.js";
|
|
11
13
|
import { ARROW_CONTENT_TYPE, arrowResponse, HttpRpcError, serializeIpcStream } from "./common.js";
|
|
12
14
|
import {
|
|
13
15
|
httpDispatchDescribe,
|
|
@@ -43,12 +45,16 @@ export function createHttpHandler(
|
|
|
43
45
|
const maxStreamResponseBytes = options?.maxStreamResponseBytes;
|
|
44
46
|
const serverId = options?.serverId ?? crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
45
47
|
|
|
48
|
+
const authenticate = options?.authenticate;
|
|
49
|
+
const oauthMetadata = options?.oauthResourceMetadata;
|
|
50
|
+
|
|
46
51
|
const methods = protocol.getMethods();
|
|
47
52
|
|
|
48
53
|
const compressionLevel = options?.compressionLevel;
|
|
49
54
|
const stateSerializer = options?.stateSerializer ?? jsonStateSerializer;
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
// ctx is built per-request to include authContext; base fields set here
|
|
57
|
+
const baseCtx = {
|
|
52
58
|
signingKey,
|
|
53
59
|
tokenTtl,
|
|
54
60
|
serverId,
|
|
@@ -88,6 +94,20 @@ export function createHttpHandler(
|
|
|
88
94
|
const url = new URL(request.url);
|
|
89
95
|
const path = url.pathname;
|
|
90
96
|
|
|
97
|
+
// Well-known endpoint: RFC 9728 OAuth Protected Resource Metadata
|
|
98
|
+
if (oauthMetadata && path === wellKnownPath(prefix)) {
|
|
99
|
+
if (request.method !== "GET") {
|
|
100
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
101
|
+
}
|
|
102
|
+
const body = JSON.stringify(oauthResourceMetadataToJson(oauthMetadata));
|
|
103
|
+
const headers = new Headers({
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
"Cache-Control": "public, max-age=3600",
|
|
106
|
+
});
|
|
107
|
+
addCorsHeaders(headers);
|
|
108
|
+
return new Response(body, { status: 200, headers });
|
|
109
|
+
}
|
|
110
|
+
|
|
91
111
|
// CORS preflight
|
|
92
112
|
if (request.method === "OPTIONS") {
|
|
93
113
|
if (path === `${prefix}/__capabilities__`) {
|
|
@@ -135,6 +155,26 @@ export function createHttpHandler(
|
|
|
135
155
|
body = zstdDecompress(body);
|
|
136
156
|
}
|
|
137
157
|
|
|
158
|
+
// Build per-request dispatch context
|
|
159
|
+
const ctx = { ...baseCtx } as typeof baseCtx & { authContext?: AuthContext };
|
|
160
|
+
|
|
161
|
+
// Authentication
|
|
162
|
+
if (authenticate) {
|
|
163
|
+
try {
|
|
164
|
+
ctx.authContext = await authenticate(request);
|
|
165
|
+
} catch (error: any) {
|
|
166
|
+
const headers = new Headers({ "Content-Type": "text/plain" });
|
|
167
|
+
addCorsHeaders(headers);
|
|
168
|
+
if (oauthMetadata) {
|
|
169
|
+
const metadataUrl = new URL(request.url);
|
|
170
|
+
metadataUrl.pathname = wellKnownPath(prefix);
|
|
171
|
+
metadataUrl.search = "";
|
|
172
|
+
headers.set("WWW-Authenticate", buildWwwAuthenticateHeader(metadataUrl.toString()));
|
|
173
|
+
}
|
|
174
|
+
return new Response(error.message || "Unauthorized", { status: 401, headers });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
138
178
|
// Route: {prefix}/__describe__
|
|
139
179
|
if (path === `${prefix}/${DESCRIBE_METHOD_NAME}`) {
|
|
140
180
|
try {
|
package/src/http/index.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
+
export type { AuthenticateFn, OAuthResourceMetadata } from "./auth.js";
|
|
5
|
+
export { oauthResourceMetadataToJson } from "./auth.js";
|
|
4
6
|
export { ARROW_CONTENT_TYPE } from "./common.js";
|
|
5
7
|
export { createHttpHandler } from "./handler.js";
|
|
8
|
+
export type { JwtAuthenticateOptions } from "./jwt.js";
|
|
9
|
+
export { jwtAuthenticate } from "./jwt.js";
|
|
6
10
|
export { type UnpackedToken, unpackStateToken } from "./token.js";
|
|
7
11
|
export type { HttpHandlerOptions, StateSerializer } from "./types.js";
|
|
8
12
|
export { jsonStateSerializer } from "./types.js";
|
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
|
+
}
|