@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/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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"}
|
package/dist/util/conform.d.ts
CHANGED
|
@@ -8,9 +8,11 @@ import { RecordBatch, type Schema } from "@query-farm/apache-arrow";
|
|
|
8
8
|
* match the writer's schema. Cloning each child Data with the schema's field
|
|
9
9
|
* type fixes the type metadata while preserving the underlying buffers.
|
|
10
10
|
*
|
|
11
|
-
* This is also used to cast compatible input types (e.g.,
|
|
12
|
-
*
|
|
13
|
-
* declared input schema.
|
|
11
|
+
* This is also used to cast compatible input types (e.g., int32→float64,
|
|
12
|
+
* float32→float64) when the input batch schema doesn't exactly match the
|
|
13
|
+
* method's declared input schema. When the underlying buffer layout differs
|
|
14
|
+
* (e.g., 4-byte int32 vs 8-byte float64), we read the values and build a
|
|
15
|
+
* new vector with the target type.
|
|
14
16
|
*/
|
|
15
17
|
export declare function conformBatchToSchema(batch: RecordBatch, schema: Schema): RecordBatch;
|
|
16
18
|
//# sourceMappingURL=conform.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conform.d.ts","sourceRoot":"","sources":["../../src/util/conform.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"conform.d.ts","sourceRoot":"","sources":["../../src/util/conform.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,WAAW,EACX,KAAK,MAAM,EAIZ,MAAM,0BAA0B,CAAC;AAkBlC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAiDpF"}
|
|
@@ -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,14 +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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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}`);
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
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/common.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
// © Copyright 2025-2026, Query.Farm LLC - https://query.farm
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
type RecordBatch,
|
|
6
|
-
RecordBatchReader,
|
|
7
|
-
RecordBatchStreamWriter,
|
|
8
|
-
type Schema,
|
|
9
|
-
} from "@query-farm/apache-arrow";
|
|
4
|
+
import { type RecordBatch, RecordBatchReader, RecordBatchStreamWriter, type Schema } from "@query-farm/apache-arrow";
|
|
10
5
|
import { conformBatchToSchema } from "../util/conform.js";
|
|
11
6
|
|
|
12
7
|
export const ARROW_CONTENT_TYPE = "application/vnd.apache.arrow.stream";
|
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";
|