@query-farm/vgi-rpc 0.3.4 → 0.6.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/README.md +47 -0
- 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 +62 -0
- package/dist/client/oauth.d.ts.map +1 -0
- package/dist/client/pipe.d.ts +3 -0
- package/dist/client/pipe.d.ts.map +1 -1
- package/dist/client/stream.d.ts +5 -0
- package/dist/client/stream.d.ts.map +1 -1
- package/dist/client/types.d.ts +6 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/constants.d.ts +3 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/dispatch/describe.d.ts.map +1 -1
- package/dist/dispatch/stream.d.ts +2 -1
- package/dist/dispatch/stream.d.ts.map +1 -1
- package/dist/dispatch/unary.d.ts +2 -1
- package/dist/dispatch/unary.d.ts.map +1 -1
- package/dist/external.d.ts +45 -0
- package/dist/external.d.ts.map +1 -0
- package/dist/gcs.d.ts +38 -0
- package/dist/gcs.d.ts.map +1 -0
- package/dist/http/auth.d.ts +32 -0
- package/dist/http/auth.d.ts.map +1 -0
- package/dist/http/bearer.d.ts +34 -0
- package/dist/http/bearer.d.ts.map +1 -0
- package/dist/http/dispatch.d.ts +4 -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 +8 -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/mtls.d.ts +78 -0
- package/dist/http/mtls.d.ts.map +1 -0
- package/dist/http/pages.d.ts +9 -0
- package/dist/http/pages.d.ts.map +1 -0
- package/dist/http/types.d.ts +22 -1
- package/dist/http/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2576 -317
- package/dist/index.js.map +27 -18
- package/dist/otel.d.ts +47 -0
- package/dist/otel.d.ts.map +1 -0
- package/dist/s3.d.ts +43 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/types.d.ts +38 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/wire/response.d.ts.map +1 -1
- package/package.json +46 -2
- package/src/auth.ts +31 -0
- package/src/client/connect.ts +28 -6
- package/src/client/index.ts +11 -0
- package/src/client/introspect.ts +15 -3
- package/src/client/oauth.ts +167 -0
- package/src/client/pipe.ts +19 -4
- package/src/client/stream.ts +32 -7
- package/src/client/types.ts +6 -0
- package/src/constants.ts +4 -1
- package/src/dispatch/describe.ts +20 -0
- package/src/dispatch/stream.ts +18 -4
- package/src/dispatch/unary.ts +6 -1
- package/src/external.ts +209 -0
- package/src/gcs.ts +86 -0
- package/src/http/auth.ts +110 -0
- package/src/http/bearer.ts +107 -0
- package/src/http/dispatch.ts +32 -10
- package/src/http/handler.ts +120 -3
- package/src/http/index.ts +14 -0
- package/src/http/jwt.ts +80 -0
- package/src/http/mtls.ts +298 -0
- package/src/http/pages.ts +298 -0
- package/src/http/types.ts +23 -1
- package/src/index.ts +32 -0
- package/src/otel.ts +161 -0
- package/src/s3.ts +94 -0
- package/src/server.ts +42 -8
- package/src/types.ts +51 -3
- package/src/wire/response.ts +28 -14
package/dist/index.js
CHANGED
|
@@ -50,6 +50,48 @@ var init_zstd = __esm(() => {
|
|
|
50
50
|
isBun = typeof globalThis.Bun !== "undefined";
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
// src/errors.ts
|
|
54
|
+
class RpcError extends Error {
|
|
55
|
+
errorType;
|
|
56
|
+
errorMessage;
|
|
57
|
+
remoteTraceback;
|
|
58
|
+
constructor(errorType, errorMessage, remoteTraceback) {
|
|
59
|
+
super(`${errorType}: ${errorMessage}`);
|
|
60
|
+
this.errorType = errorType;
|
|
61
|
+
this.errorMessage = errorMessage;
|
|
62
|
+
this.remoteTraceback = remoteTraceback;
|
|
63
|
+
this.name = "RpcError";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class VersionError extends Error {
|
|
68
|
+
constructor(message) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = "VersionError";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/auth.ts
|
|
75
|
+
class AuthContext {
|
|
76
|
+
domain;
|
|
77
|
+
authenticated;
|
|
78
|
+
principal;
|
|
79
|
+
claims;
|
|
80
|
+
constructor(domain, authenticated, principal, claims = {}) {
|
|
81
|
+
this.domain = domain;
|
|
82
|
+
this.authenticated = authenticated;
|
|
83
|
+
this.principal = principal;
|
|
84
|
+
this.claims = claims;
|
|
85
|
+
}
|
|
86
|
+
static anonymous() {
|
|
87
|
+
return new AuthContext("", false, null);
|
|
88
|
+
}
|
|
89
|
+
requireAuthenticated() {
|
|
90
|
+
if (!this.authenticated) {
|
|
91
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
53
95
|
// src/constants.ts
|
|
54
96
|
var RPC_METHOD_KEY = "vgi_rpc.method";
|
|
55
97
|
var LOG_LEVEL_KEY = "vgi_rpc.log_level";
|
|
@@ -61,21 +103,247 @@ var SERVER_ID_KEY = "vgi_rpc.server_id";
|
|
|
61
103
|
var REQUEST_ID_KEY = "vgi_rpc.request_id";
|
|
62
104
|
var PROTOCOL_NAME_KEY = "vgi_rpc.protocol_name";
|
|
63
105
|
var DESCRIBE_VERSION_KEY = "vgi_rpc.describe_version";
|
|
64
|
-
var DESCRIBE_VERSION = "
|
|
106
|
+
var DESCRIBE_VERSION = "3";
|
|
65
107
|
var DESCRIBE_METHOD_NAME = "__describe__";
|
|
66
108
|
var STATE_KEY = "vgi_rpc.stream_state#b64";
|
|
109
|
+
var LOCATION_KEY = "vgi_rpc.location";
|
|
110
|
+
var LOCATION_SHA256_KEY = "vgi_rpc.location.sha256";
|
|
67
111
|
|
|
68
|
-
// src/
|
|
112
|
+
// src/external.ts
|
|
69
113
|
import { RecordBatchReader, RecordBatchStreamWriter } from "@query-farm/apache-arrow";
|
|
114
|
+
init_zstd();
|
|
70
115
|
|
|
71
|
-
// src/
|
|
116
|
+
// src/wire/response.ts
|
|
72
117
|
import {
|
|
118
|
+
Data,
|
|
119
|
+
DataType,
|
|
73
120
|
makeData,
|
|
74
121
|
RecordBatch,
|
|
75
122
|
Struct,
|
|
76
|
-
Type,
|
|
77
123
|
vectorFromArray
|
|
78
124
|
} from "@query-farm/apache-arrow";
|
|
125
|
+
function coerceInt64(schema, values) {
|
|
126
|
+
const result = { ...values };
|
|
127
|
+
for (const field of schema.fields) {
|
|
128
|
+
const val = result[field.name];
|
|
129
|
+
if (val === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
if (!DataType.isInt(field.type) || field.type.bitWidth !== 64)
|
|
132
|
+
continue;
|
|
133
|
+
if (Array.isArray(val)) {
|
|
134
|
+
result[field.name] = val.map((v) => typeof v === "number" ? BigInt(v) : v);
|
|
135
|
+
} else if (typeof val === "number") {
|
|
136
|
+
result[field.name] = BigInt(val);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
function buildResultBatch(schema, values, serverId, requestId) {
|
|
142
|
+
const metadata = new Map;
|
|
143
|
+
metadata.set(SERVER_ID_KEY, serverId);
|
|
144
|
+
if (requestId !== null) {
|
|
145
|
+
metadata.set(REQUEST_ID_KEY, requestId);
|
|
146
|
+
}
|
|
147
|
+
if (schema.fields.length === 0) {
|
|
148
|
+
return buildEmptyBatch(schema, metadata);
|
|
149
|
+
}
|
|
150
|
+
for (const field of schema.fields) {
|
|
151
|
+
if (values[field.name] === undefined && !field.nullable) {
|
|
152
|
+
const got = Object.keys(values);
|
|
153
|
+
throw new TypeError(`Handler result missing required field '${field.name}'. Got keys: [${got.join(", ")}]`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const coerced = coerceInt64(schema, values);
|
|
157
|
+
const children = schema.fields.map((f) => {
|
|
158
|
+
const val = coerced[f.name];
|
|
159
|
+
if (val instanceof Data) {
|
|
160
|
+
return val;
|
|
161
|
+
}
|
|
162
|
+
const arr = vectorFromArray([val], f.type);
|
|
163
|
+
return arr.data[0];
|
|
164
|
+
});
|
|
165
|
+
const structType = new Struct(schema.fields);
|
|
166
|
+
const data = makeData({
|
|
167
|
+
type: structType,
|
|
168
|
+
length: 1,
|
|
169
|
+
children,
|
|
170
|
+
nullCount: 0
|
|
171
|
+
});
|
|
172
|
+
return new RecordBatch(schema, data, metadata);
|
|
173
|
+
}
|
|
174
|
+
function buildErrorBatch(schema, error, serverId, requestId) {
|
|
175
|
+
const metadata = new Map;
|
|
176
|
+
metadata.set(LOG_LEVEL_KEY, "EXCEPTION");
|
|
177
|
+
metadata.set(LOG_MESSAGE_KEY, `${error.constructor.name}: ${error.message}`);
|
|
178
|
+
const extra = {
|
|
179
|
+
exception_type: error.constructor.name,
|
|
180
|
+
exception_message: error.message,
|
|
181
|
+
traceback: error.stack ?? ""
|
|
182
|
+
};
|
|
183
|
+
metadata.set(LOG_EXTRA_KEY, JSON.stringify(extra));
|
|
184
|
+
metadata.set(SERVER_ID_KEY, serverId);
|
|
185
|
+
if (requestId !== null) {
|
|
186
|
+
metadata.set(REQUEST_ID_KEY, requestId);
|
|
187
|
+
}
|
|
188
|
+
return buildEmptyBatch(schema, metadata);
|
|
189
|
+
}
|
|
190
|
+
function buildLogBatch(schema, level, message, extra, serverId, requestId) {
|
|
191
|
+
const metadata = new Map;
|
|
192
|
+
metadata.set(LOG_LEVEL_KEY, level);
|
|
193
|
+
metadata.set(LOG_MESSAGE_KEY, message);
|
|
194
|
+
if (extra) {
|
|
195
|
+
metadata.set(LOG_EXTRA_KEY, JSON.stringify(extra));
|
|
196
|
+
}
|
|
197
|
+
if (serverId != null) {
|
|
198
|
+
metadata.set(SERVER_ID_KEY, serverId);
|
|
199
|
+
}
|
|
200
|
+
if (requestId != null) {
|
|
201
|
+
metadata.set(REQUEST_ID_KEY, requestId);
|
|
202
|
+
}
|
|
203
|
+
return buildEmptyBatch(schema, metadata);
|
|
204
|
+
}
|
|
205
|
+
function makeEmptyData(type) {
|
|
206
|
+
if (DataType.isStruct(type)) {
|
|
207
|
+
const children = type.children.map((f) => makeEmptyData(f.type));
|
|
208
|
+
return makeData({ type, length: 0, children, nullCount: 0 });
|
|
209
|
+
}
|
|
210
|
+
if (DataType.isList(type)) {
|
|
211
|
+
const childData = makeEmptyData(type.children[0].type);
|
|
212
|
+
return makeData({ type, length: 0, children: [childData], nullCount: 0, valueOffsets: new Int32Array([0]) });
|
|
213
|
+
}
|
|
214
|
+
if (DataType.isFixedSizeList(type)) {
|
|
215
|
+
const childData = makeEmptyData(type.children[0].type);
|
|
216
|
+
return makeData({ type, length: 0, child: childData, nullCount: 0 });
|
|
217
|
+
}
|
|
218
|
+
if (DataType.isMap(type)) {
|
|
219
|
+
const entryType = type.children[0]?.type;
|
|
220
|
+
const entryData = entryType ? makeEmptyData(entryType) : makeData({ type: new Struct([]), length: 0, children: [], nullCount: 0 });
|
|
221
|
+
return makeData({ type, length: 0, children: [entryData], nullCount: 0, valueOffsets: new Int32Array([0]) });
|
|
222
|
+
}
|
|
223
|
+
return makeData({ type, length: 0, nullCount: 0 });
|
|
224
|
+
}
|
|
225
|
+
function buildEmptyBatch(schema, metadata) {
|
|
226
|
+
const children = schema.fields.map((f) => makeEmptyData(f.type));
|
|
227
|
+
const structType = new Struct(schema.fields);
|
|
228
|
+
const data = makeData({
|
|
229
|
+
type: structType,
|
|
230
|
+
length: 0,
|
|
231
|
+
children,
|
|
232
|
+
nullCount: 0
|
|
233
|
+
});
|
|
234
|
+
return new RecordBatch(schema, data, metadata);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/external.ts
|
|
238
|
+
var DEFAULT_THRESHOLD = 1048576;
|
|
239
|
+
function httpsOnlyValidator(url) {
|
|
240
|
+
const parsed = new URL(url);
|
|
241
|
+
if (parsed.protocol !== "https:") {
|
|
242
|
+
throw new Error(`External location URL must use HTTPS, got "${parsed.protocol}"`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function sha256Hex(data) {
|
|
246
|
+
const buf = new ArrayBuffer(data.byteLength);
|
|
247
|
+
new Uint8Array(buf).set(data);
|
|
248
|
+
const hash = await crypto.subtle.digest("SHA-256", buf);
|
|
249
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
250
|
+
}
|
|
251
|
+
function isExternalLocationBatch(batch) {
|
|
252
|
+
if (batch.numRows !== 0)
|
|
253
|
+
return false;
|
|
254
|
+
const meta = batch.metadata;
|
|
255
|
+
if (!meta)
|
|
256
|
+
return false;
|
|
257
|
+
return meta.has(LOCATION_KEY) && !meta.has(LOG_LEVEL_KEY);
|
|
258
|
+
}
|
|
259
|
+
function makeExternalLocationBatch(schema, url, sha256) {
|
|
260
|
+
const metadata = new Map;
|
|
261
|
+
metadata.set(LOCATION_KEY, url);
|
|
262
|
+
if (sha256) {
|
|
263
|
+
metadata.set(LOCATION_SHA256_KEY, sha256);
|
|
264
|
+
}
|
|
265
|
+
return buildEmptyBatch(schema, metadata);
|
|
266
|
+
}
|
|
267
|
+
function serializeBatchToIpc(batch) {
|
|
268
|
+
const writer = new RecordBatchStreamWriter;
|
|
269
|
+
writer.reset(undefined, batch.schema);
|
|
270
|
+
writer.write(batch);
|
|
271
|
+
writer.close();
|
|
272
|
+
return writer.toUint8Array(true);
|
|
273
|
+
}
|
|
274
|
+
function batchByteSize(batch) {
|
|
275
|
+
const writer = new RecordBatchStreamWriter;
|
|
276
|
+
writer.reset(undefined, batch.schema);
|
|
277
|
+
writer.write(batch);
|
|
278
|
+
writer.close();
|
|
279
|
+
return writer.toUint8Array(true).byteLength;
|
|
280
|
+
}
|
|
281
|
+
async function maybeExternalizeBatch(batch, config) {
|
|
282
|
+
if (!config?.storage)
|
|
283
|
+
return batch;
|
|
284
|
+
if (batch.numRows === 0)
|
|
285
|
+
return batch;
|
|
286
|
+
const threshold = config.externalizeThresholdBytes ?? DEFAULT_THRESHOLD;
|
|
287
|
+
if (batchByteSize(batch) < threshold)
|
|
288
|
+
return batch;
|
|
289
|
+
let ipcData = serializeBatchToIpc(batch);
|
|
290
|
+
const checksum = await sha256Hex(ipcData);
|
|
291
|
+
let contentEncoding = "";
|
|
292
|
+
if (config.compression?.algorithm === "zstd") {
|
|
293
|
+
ipcData = zstdCompress(ipcData, config.compression.level ?? 3);
|
|
294
|
+
contentEncoding = "zstd";
|
|
295
|
+
}
|
|
296
|
+
const url = await config.storage.upload(ipcData, contentEncoding);
|
|
297
|
+
return makeExternalLocationBatch(batch.schema, url, checksum);
|
|
298
|
+
}
|
|
299
|
+
async function resolveExternalLocation(batch, config) {
|
|
300
|
+
if (!config)
|
|
301
|
+
return batch;
|
|
302
|
+
if (!isExternalLocationBatch(batch))
|
|
303
|
+
return batch;
|
|
304
|
+
const url = batch.metadata?.get(LOCATION_KEY);
|
|
305
|
+
if (!url)
|
|
306
|
+
return batch;
|
|
307
|
+
const validator = config.urlValidator === null ? undefined : config.urlValidator ?? httpsOnlyValidator;
|
|
308
|
+
if (validator) {
|
|
309
|
+
validator(url);
|
|
310
|
+
}
|
|
311
|
+
const response = await fetch(url);
|
|
312
|
+
if (!response.ok) {
|
|
313
|
+
throw new Error(`External location fetch failed: ${response.status} ${response.statusText} [url: ${url}]`);
|
|
314
|
+
}
|
|
315
|
+
let data = new Uint8Array(await response.arrayBuffer());
|
|
316
|
+
const contentEncoding = response.headers.get("Content-Encoding");
|
|
317
|
+
if (contentEncoding === "zstd") {
|
|
318
|
+
data = new Uint8Array(zstdDecompress(data));
|
|
319
|
+
}
|
|
320
|
+
const expectedSha256 = batch.metadata?.get(LOCATION_SHA256_KEY);
|
|
321
|
+
if (expectedSha256) {
|
|
322
|
+
const actualSha256 = await sha256Hex(data);
|
|
323
|
+
if (actualSha256 !== expectedSha256) {
|
|
324
|
+
throw new Error(`SHA-256 checksum mismatch for ${url}: expected ${expectedSha256}, got ${actualSha256}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const reader = await RecordBatchReader.from(data);
|
|
328
|
+
await reader.open();
|
|
329
|
+
const resolved = reader.next();
|
|
330
|
+
if (!resolved || resolved.done || !resolved.value) {
|
|
331
|
+
throw new Error(`No data batch found in external IPC stream from ${url}`);
|
|
332
|
+
}
|
|
333
|
+
return resolved.value;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/http/common.ts
|
|
337
|
+
import { RecordBatchReader as RecordBatchReader2, RecordBatchStreamWriter as RecordBatchStreamWriter2 } from "@query-farm/apache-arrow";
|
|
338
|
+
|
|
339
|
+
// src/util/conform.ts
|
|
340
|
+
import {
|
|
341
|
+
makeData as makeData2,
|
|
342
|
+
RecordBatch as RecordBatch2,
|
|
343
|
+
Struct as Struct2,
|
|
344
|
+
Type,
|
|
345
|
+
vectorFromArray as vectorFromArray2
|
|
346
|
+
} from "@query-farm/apache-arrow";
|
|
79
347
|
function needsValueCast(src, dst) {
|
|
80
348
|
if (src.typeId === dst.typeId)
|
|
81
349
|
return false;
|
|
@@ -111,19 +379,19 @@ function conformBatchToSchema(batch, schema) {
|
|
|
111
379
|
const v = col.get(r);
|
|
112
380
|
values.push(typeof v === "bigint" ? Number(v) : v);
|
|
113
381
|
}
|
|
114
|
-
return
|
|
382
|
+
return vectorFromArray2(values, dstType).data[0];
|
|
115
383
|
}
|
|
116
384
|
return srcChild.clone(dstType);
|
|
117
385
|
});
|
|
118
|
-
const structType = new
|
|
119
|
-
const data =
|
|
386
|
+
const structType = new Struct2(schema.fields);
|
|
387
|
+
const data = makeData2({
|
|
120
388
|
type: structType,
|
|
121
389
|
length: batch.numRows,
|
|
122
390
|
children,
|
|
123
391
|
nullCount: batch.data.nullCount,
|
|
124
392
|
nullBitmap: batch.data.nullBitmap
|
|
125
393
|
});
|
|
126
|
-
return new
|
|
394
|
+
return new RecordBatch2(schema, data, batch.metadata);
|
|
127
395
|
}
|
|
128
396
|
|
|
129
397
|
// src/http/common.ts
|
|
@@ -138,7 +406,7 @@ class HttpRpcError extends Error {
|
|
|
138
406
|
}
|
|
139
407
|
}
|
|
140
408
|
function serializeIpcStream(schema, batches) {
|
|
141
|
-
const writer = new
|
|
409
|
+
const writer = new RecordBatchStreamWriter2;
|
|
142
410
|
writer.reset(undefined, schema);
|
|
143
411
|
for (const batch of batches) {
|
|
144
412
|
writer.write(conformBatchToSchema(batch, schema));
|
|
@@ -152,7 +420,7 @@ function arrowResponse(body, status = 200, extraHeaders) {
|
|
|
152
420
|
return new Response(body, { status, headers });
|
|
153
421
|
}
|
|
154
422
|
async function readRequestFromBody(body) {
|
|
155
|
-
const reader = await
|
|
423
|
+
const reader = await RecordBatchReader2.from(body);
|
|
156
424
|
await reader.open();
|
|
157
425
|
const schema = reader.schema;
|
|
158
426
|
if (!schema) {
|
|
@@ -166,46 +434,25 @@ async function readRequestFromBody(body) {
|
|
|
166
434
|
}
|
|
167
435
|
|
|
168
436
|
// src/client/introspect.ts
|
|
169
|
-
import { Schema as ArrowSchema, RecordBatchReader as
|
|
437
|
+
import { Schema as ArrowSchema, RecordBatchReader as RecordBatchReader5 } from "@query-farm/apache-arrow";
|
|
170
438
|
|
|
171
439
|
// src/client/ipc.ts
|
|
172
440
|
import {
|
|
173
441
|
Binary,
|
|
174
442
|
Bool,
|
|
175
|
-
DataType,
|
|
443
|
+
DataType as DataType2,
|
|
176
444
|
Float64,
|
|
177
445
|
Int64,
|
|
178
|
-
makeData as
|
|
179
|
-
RecordBatch as
|
|
180
|
-
RecordBatchReader as
|
|
181
|
-
Struct as
|
|
446
|
+
makeData as makeData3,
|
|
447
|
+
RecordBatch as RecordBatch3,
|
|
448
|
+
RecordBatchReader as RecordBatchReader4,
|
|
449
|
+
Struct as Struct3,
|
|
182
450
|
Utf8,
|
|
183
|
-
vectorFromArray as
|
|
451
|
+
vectorFromArray as vectorFromArray3
|
|
184
452
|
} from "@query-farm/apache-arrow";
|
|
185
453
|
|
|
186
|
-
// src/errors.ts
|
|
187
|
-
class RpcError extends Error {
|
|
188
|
-
errorType;
|
|
189
|
-
errorMessage;
|
|
190
|
-
remoteTraceback;
|
|
191
|
-
constructor(errorType, errorMessage, remoteTraceback) {
|
|
192
|
-
super(`${errorType}: ${errorMessage}`);
|
|
193
|
-
this.errorType = errorType;
|
|
194
|
-
this.errorMessage = errorMessage;
|
|
195
|
-
this.remoteTraceback = remoteTraceback;
|
|
196
|
-
this.name = "RpcError";
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
class VersionError extends Error {
|
|
201
|
-
constructor(message) {
|
|
202
|
-
super(message);
|
|
203
|
-
this.name = "VersionError";
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
454
|
// src/wire/reader.ts
|
|
208
|
-
import { RecordBatchReader as
|
|
455
|
+
import { RecordBatchReader as RecordBatchReader3 } from "@query-farm/apache-arrow";
|
|
209
456
|
|
|
210
457
|
class IpcStreamReader {
|
|
211
458
|
reader;
|
|
@@ -215,7 +462,7 @@ class IpcStreamReader {
|
|
|
215
462
|
this.reader = reader;
|
|
216
463
|
}
|
|
217
464
|
static async create(input) {
|
|
218
|
-
const reader = await
|
|
465
|
+
const reader = await RecordBatchReader3.from(input);
|
|
219
466
|
await reader.open({ autoDestroy: false });
|
|
220
467
|
if (reader.closed) {
|
|
221
468
|
throw new Error("Input stream closed before first IPC message");
|
|
@@ -292,12 +539,12 @@ function inferArrowType(value) {
|
|
|
292
539
|
function coerceForArrow(type, value) {
|
|
293
540
|
if (value == null)
|
|
294
541
|
return value;
|
|
295
|
-
if (
|
|
542
|
+
if (DataType2.isInt(type) && type.bitWidth === 64) {
|
|
296
543
|
if (typeof value === "number")
|
|
297
544
|
return BigInt(value);
|
|
298
545
|
return value;
|
|
299
546
|
}
|
|
300
|
-
if (
|
|
547
|
+
if (DataType2.isMap(type)) {
|
|
301
548
|
if (value instanceof Map) {
|
|
302
549
|
const entriesField = type.children[0];
|
|
303
550
|
const valueType = entriesField.type.children[1].type;
|
|
@@ -309,7 +556,7 @@ function coerceForArrow(type, value) {
|
|
|
309
556
|
}
|
|
310
557
|
return value;
|
|
311
558
|
}
|
|
312
|
-
if (
|
|
559
|
+
if (DataType2.isList(type)) {
|
|
313
560
|
if (Array.isArray(value)) {
|
|
314
561
|
const elemType = type.children[0].type;
|
|
315
562
|
return value.map((v) => coerceForArrow(elemType, v));
|
|
@@ -323,32 +570,32 @@ function buildRequestIpc(schema, params, method) {
|
|
|
323
570
|
metadata.set(RPC_METHOD_KEY, method);
|
|
324
571
|
metadata.set(REQUEST_VERSION_KEY, REQUEST_VERSION);
|
|
325
572
|
if (schema.fields.length === 0) {
|
|
326
|
-
const structType2 = new
|
|
327
|
-
const data2 =
|
|
573
|
+
const structType2 = new Struct3(schema.fields);
|
|
574
|
+
const data2 = makeData3({
|
|
328
575
|
type: structType2,
|
|
329
576
|
length: 1,
|
|
330
577
|
children: [],
|
|
331
578
|
nullCount: 0
|
|
332
579
|
});
|
|
333
|
-
const batch2 = new
|
|
580
|
+
const batch2 = new RecordBatch3(schema, data2, metadata);
|
|
334
581
|
return serializeIpcStream(schema, [batch2]);
|
|
335
582
|
}
|
|
336
583
|
const children = schema.fields.map((f) => {
|
|
337
584
|
const val = coerceForArrow(f.type, params[f.name]);
|
|
338
|
-
return
|
|
585
|
+
return vectorFromArray3([val], f.type).data[0];
|
|
339
586
|
});
|
|
340
|
-
const structType = new
|
|
341
|
-
const data =
|
|
587
|
+
const structType = new Struct3(schema.fields);
|
|
588
|
+
const data = makeData3({
|
|
342
589
|
type: structType,
|
|
343
590
|
length: 1,
|
|
344
591
|
children,
|
|
345
592
|
nullCount: 0
|
|
346
593
|
});
|
|
347
|
-
const batch = new
|
|
594
|
+
const batch = new RecordBatch3(schema, data, metadata);
|
|
348
595
|
return serializeIpcStream(schema, [batch]);
|
|
349
596
|
}
|
|
350
597
|
async function readResponseBatches(body) {
|
|
351
|
-
const reader = await
|
|
598
|
+
const reader = await RecordBatchReader4.from(body);
|
|
352
599
|
await reader.open();
|
|
353
600
|
const schema = reader.schema;
|
|
354
601
|
if (!schema) {
|
|
@@ -422,7 +669,7 @@ async function readSequentialStreams(body) {
|
|
|
422
669
|
|
|
423
670
|
// src/client/introspect.ts
|
|
424
671
|
async function deserializeSchema(bytes) {
|
|
425
|
-
const reader = await
|
|
672
|
+
const reader = await RecordBatchReader5.from(bytes);
|
|
426
673
|
await reader.open();
|
|
427
674
|
return reader.schema;
|
|
428
675
|
}
|
|
@@ -486,21 +733,28 @@ async function parseDescribeResponse(batches, onLog) {
|
|
|
486
733
|
return { protocolName, methods };
|
|
487
734
|
}
|
|
488
735
|
async function httpIntrospect(baseUrl, options) {
|
|
489
|
-
const prefix = options?.prefix ?? "
|
|
736
|
+
const prefix = options?.prefix ?? "";
|
|
490
737
|
const emptySchema = new ArrowSchema([]);
|
|
491
738
|
const body = buildRequestIpc(emptySchema, {}, DESCRIBE_METHOD_NAME);
|
|
739
|
+
const headers = { "Content-Type": ARROW_CONTENT_TYPE };
|
|
740
|
+
if (options?.authorization) {
|
|
741
|
+
headers.Authorization = options.authorization;
|
|
742
|
+
}
|
|
492
743
|
const response = await fetch(`${baseUrl}${prefix}/${DESCRIBE_METHOD_NAME}`, {
|
|
493
744
|
method: "POST",
|
|
494
|
-
headers
|
|
745
|
+
headers,
|
|
495
746
|
body
|
|
496
747
|
});
|
|
748
|
+
if (response.status === 401) {
|
|
749
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
750
|
+
}
|
|
497
751
|
const responseBody = new Uint8Array(await response.arrayBuffer());
|
|
498
752
|
const { batches } = await readResponseBatches(responseBody);
|
|
499
753
|
return parseDescribeResponse(batches);
|
|
500
754
|
}
|
|
501
755
|
|
|
502
756
|
// src/client/stream.ts
|
|
503
|
-
import { Field, makeData as
|
|
757
|
+
import { Field, makeData as makeData4, RecordBatch as RecordBatch4, Schema, Struct as Struct4, vectorFromArray as vectorFromArray4 } from "@query-farm/apache-arrow";
|
|
504
758
|
class HttpStreamSession {
|
|
505
759
|
_baseUrl;
|
|
506
760
|
_prefix;
|
|
@@ -515,6 +769,8 @@ class HttpStreamSession {
|
|
|
515
769
|
_compressionLevel;
|
|
516
770
|
_compressFn;
|
|
517
771
|
_decompressFn;
|
|
772
|
+
_authorization;
|
|
773
|
+
_externalConfig;
|
|
518
774
|
constructor(opts) {
|
|
519
775
|
this._baseUrl = opts.baseUrl;
|
|
520
776
|
this._prefix = opts.prefix;
|
|
@@ -529,6 +785,8 @@ class HttpStreamSession {
|
|
|
529
785
|
this._compressionLevel = opts.compressionLevel;
|
|
530
786
|
this._compressFn = opts.compressFn;
|
|
531
787
|
this._decompressFn = opts.decompressFn;
|
|
788
|
+
this._authorization = opts.authorization;
|
|
789
|
+
this._externalConfig = opts.externalConfig;
|
|
532
790
|
}
|
|
533
791
|
get header() {
|
|
534
792
|
return this._header;
|
|
@@ -541,6 +799,9 @@ class HttpStreamSession {
|
|
|
541
799
|
headers["Content-Encoding"] = "zstd";
|
|
542
800
|
headers["Accept-Encoding"] = "zstd";
|
|
543
801
|
}
|
|
802
|
+
if (this._authorization) {
|
|
803
|
+
headers.Authorization = this._authorization;
|
|
804
|
+
}
|
|
544
805
|
return headers;
|
|
545
806
|
}
|
|
546
807
|
_prepareBody(content) {
|
|
@@ -565,7 +826,7 @@ class HttpStreamSession {
|
|
|
565
826
|
const emptyBatch = this._buildEmptyBatch(zeroSchema);
|
|
566
827
|
const metadata2 = new Map;
|
|
567
828
|
metadata2.set(STATE_KEY, this._stateToken);
|
|
568
|
-
const batchWithMeta = new
|
|
829
|
+
const batchWithMeta = new RecordBatch4(zeroSchema, emptyBatch.data, metadata2);
|
|
569
830
|
return this._doExchange(zeroSchema, [batchWithMeta]);
|
|
570
831
|
}
|
|
571
832
|
const keys = Object.keys(input[0]);
|
|
@@ -584,10 +845,10 @@ class HttpStreamSession {
|
|
|
584
845
|
const inputSchema = new Schema(fields);
|
|
585
846
|
const children = inputSchema.fields.map((f) => {
|
|
586
847
|
const values = input.map((row) => row[f.name]);
|
|
587
|
-
return
|
|
848
|
+
return vectorFromArray4(values, f.type).data[0];
|
|
588
849
|
});
|
|
589
|
-
const structType = new
|
|
590
|
-
const data =
|
|
850
|
+
const structType = new Struct4(inputSchema.fields);
|
|
851
|
+
const data = makeData4({
|
|
591
852
|
type: structType,
|
|
592
853
|
length: input.length,
|
|
593
854
|
children,
|
|
@@ -595,7 +856,7 @@ class HttpStreamSession {
|
|
|
595
856
|
});
|
|
596
857
|
const metadata = new Map;
|
|
597
858
|
metadata.set(STATE_KEY, this._stateToken);
|
|
598
|
-
const batch = new
|
|
859
|
+
const batch = new RecordBatch4(inputSchema, data, metadata);
|
|
599
860
|
return this._doExchange(inputSchema, [batch]);
|
|
600
861
|
}
|
|
601
862
|
async _doExchange(schema, batches) {
|
|
@@ -605,6 +866,9 @@ class HttpStreamSession {
|
|
|
605
866
|
headers: this._buildHeaders(),
|
|
606
867
|
body: this._prepareBody(body)
|
|
607
868
|
});
|
|
869
|
+
if (resp.status === 401) {
|
|
870
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
871
|
+
}
|
|
608
872
|
const responseBody = await this._readResponse(resp);
|
|
609
873
|
const { batches: responseBatches } = await readResponseBatches(responseBody);
|
|
610
874
|
let resultRows = [];
|
|
@@ -627,22 +891,26 @@ class HttpStreamSession {
|
|
|
627
891
|
}
|
|
628
892
|
_buildEmptyBatch(schema) {
|
|
629
893
|
const children = schema.fields.map((f) => {
|
|
630
|
-
return
|
|
894
|
+
return makeData4({ type: f.type, length: 0, nullCount: 0 });
|
|
631
895
|
});
|
|
632
|
-
const structType = new
|
|
633
|
-
const data =
|
|
896
|
+
const structType = new Struct4(schema.fields);
|
|
897
|
+
const data = makeData4({
|
|
634
898
|
type: structType,
|
|
635
899
|
length: 0,
|
|
636
900
|
children,
|
|
637
901
|
nullCount: 0
|
|
638
902
|
});
|
|
639
|
-
return new
|
|
903
|
+
return new RecordBatch4(schema, data);
|
|
640
904
|
}
|
|
641
905
|
async* [Symbol.asyncIterator]() {
|
|
642
|
-
for (
|
|
906
|
+
for (let batch of this._pendingBatches) {
|
|
643
907
|
if (batch.numRows === 0) {
|
|
644
|
-
|
|
645
|
-
|
|
908
|
+
if (isExternalLocationBatch(batch)) {
|
|
909
|
+
batch = await resolveExternalLocation(batch, this._externalConfig);
|
|
910
|
+
} else {
|
|
911
|
+
dispatchLogOrError(batch, this._onLog);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
646
914
|
}
|
|
647
915
|
yield extractBatchRows(batch);
|
|
648
916
|
}
|
|
@@ -655,7 +923,7 @@ class HttpStreamSession {
|
|
|
655
923
|
const responseBody = await this._sendContinuation(this._stateToken);
|
|
656
924
|
const { batches } = await readResponseBatches(responseBody);
|
|
657
925
|
let gotContinuation = false;
|
|
658
|
-
for (
|
|
926
|
+
for (let batch of batches) {
|
|
659
927
|
if (batch.numRows === 0) {
|
|
660
928
|
const token = batch.metadata?.get(STATE_KEY);
|
|
661
929
|
if (token) {
|
|
@@ -663,8 +931,12 @@ class HttpStreamSession {
|
|
|
663
931
|
gotContinuation = true;
|
|
664
932
|
continue;
|
|
665
933
|
}
|
|
666
|
-
|
|
667
|
-
|
|
934
|
+
if (isExternalLocationBatch(batch)) {
|
|
935
|
+
batch = await resolveExternalLocation(batch, this._externalConfig);
|
|
936
|
+
} else {
|
|
937
|
+
dispatchLogOrError(batch, this._onLog);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
668
940
|
}
|
|
669
941
|
yield extractBatchRows(batch);
|
|
670
942
|
}
|
|
@@ -676,20 +948,23 @@ class HttpStreamSession {
|
|
|
676
948
|
const emptySchema = new Schema([]);
|
|
677
949
|
const metadata = new Map;
|
|
678
950
|
metadata.set(STATE_KEY, token);
|
|
679
|
-
const structType = new
|
|
680
|
-
const data =
|
|
951
|
+
const structType = new Struct4(emptySchema.fields);
|
|
952
|
+
const data = makeData4({
|
|
681
953
|
type: structType,
|
|
682
954
|
length: 1,
|
|
683
955
|
children: [],
|
|
684
956
|
nullCount: 0
|
|
685
957
|
});
|
|
686
|
-
const batch = new
|
|
958
|
+
const batch = new RecordBatch4(emptySchema, data, metadata);
|
|
687
959
|
const body = serializeIpcStream(emptySchema, [batch]);
|
|
688
960
|
const resp = await fetch(`${this._baseUrl}${this._prefix}/${this._method}/exchange`, {
|
|
689
961
|
method: "POST",
|
|
690
962
|
headers: this._buildHeaders(),
|
|
691
963
|
body: this._prepareBody(body)
|
|
692
964
|
});
|
|
965
|
+
if (resp.status === 401) {
|
|
966
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
967
|
+
}
|
|
693
968
|
return this._readResponse(resp);
|
|
694
969
|
}
|
|
695
970
|
close() {}
|
|
@@ -697,9 +972,11 @@ class HttpStreamSession {
|
|
|
697
972
|
|
|
698
973
|
// src/client/connect.ts
|
|
699
974
|
function httpConnect(baseUrl, options) {
|
|
700
|
-
const prefix = (options?.prefix ?? "
|
|
975
|
+
const prefix = (options?.prefix ?? "").replace(/\/+$/, "");
|
|
701
976
|
const onLog = options?.onLog;
|
|
702
977
|
const compressionLevel = options?.compressionLevel;
|
|
978
|
+
const authorization = options?.authorization;
|
|
979
|
+
const externalConfig = options?.externalLocation;
|
|
703
980
|
let methodCache = null;
|
|
704
981
|
let compressFn;
|
|
705
982
|
let decompressFn;
|
|
@@ -722,6 +999,9 @@ function httpConnect(baseUrl, options) {
|
|
|
722
999
|
headers["Content-Encoding"] = "zstd";
|
|
723
1000
|
headers["Accept-Encoding"] = "zstd";
|
|
724
1001
|
}
|
|
1002
|
+
if (authorization) {
|
|
1003
|
+
headers.Authorization = authorization;
|
|
1004
|
+
}
|
|
725
1005
|
return headers;
|
|
726
1006
|
}
|
|
727
1007
|
function prepareBody(content) {
|
|
@@ -730,6 +1010,11 @@ function httpConnect(baseUrl, options) {
|
|
|
730
1010
|
}
|
|
731
1011
|
return content;
|
|
732
1012
|
}
|
|
1013
|
+
function checkAuth(resp) {
|
|
1014
|
+
if (resp.status === 401) {
|
|
1015
|
+
throw new RpcError("AuthenticationError", "Authentication required", "");
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
733
1018
|
async function readResponse(resp) {
|
|
734
1019
|
let body = new Uint8Array(await resp.arrayBuffer());
|
|
735
1020
|
if (resp.headers.get("Content-Encoding") === "zstd" && decompressFn) {
|
|
@@ -740,7 +1025,7 @@ function httpConnect(baseUrl, options) {
|
|
|
740
1025
|
async function ensureMethodCache() {
|
|
741
1026
|
if (methodCache)
|
|
742
1027
|
return methodCache;
|
|
743
|
-
const desc = await httpIntrospect(baseUrl, { prefix });
|
|
1028
|
+
const desc = await httpIntrospect(baseUrl, { prefix, authorization });
|
|
744
1029
|
methodCache = new Map(desc.methods.map((m) => [m.name, m]));
|
|
745
1030
|
return methodCache;
|
|
746
1031
|
}
|
|
@@ -759,13 +1044,18 @@ function httpConnect(baseUrl, options) {
|
|
|
759
1044
|
headers: buildHeaders(),
|
|
760
1045
|
body: prepareBody(body)
|
|
761
1046
|
});
|
|
1047
|
+
checkAuth(resp);
|
|
762
1048
|
const responseBody = await readResponse(resp);
|
|
763
1049
|
const { batches } = await readResponseBatches(responseBody);
|
|
764
1050
|
let resultBatch = null;
|
|
765
|
-
for (
|
|
1051
|
+
for (let batch of batches) {
|
|
766
1052
|
if (batch.numRows === 0) {
|
|
767
|
-
|
|
768
|
-
|
|
1053
|
+
if (isExternalLocationBatch(batch)) {
|
|
1054
|
+
batch = await resolveExternalLocation(batch, externalConfig);
|
|
1055
|
+
} else {
|
|
1056
|
+
dispatchLogOrError(batch, onLog);
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
769
1059
|
}
|
|
770
1060
|
resultBatch = batch;
|
|
771
1061
|
}
|
|
@@ -794,6 +1084,7 @@ function httpConnect(baseUrl, options) {
|
|
|
794
1084
|
headers: buildHeaders(),
|
|
795
1085
|
body: prepareBody(body)
|
|
796
1086
|
});
|
|
1087
|
+
checkAuth(resp);
|
|
797
1088
|
const responseBody = await readResponse(resp);
|
|
798
1089
|
let header = null;
|
|
799
1090
|
let stateToken = null;
|
|
@@ -899,7 +1190,9 @@ function httpConnect(baseUrl, options) {
|
|
|
899
1190
|
header,
|
|
900
1191
|
compressionLevel,
|
|
901
1192
|
compressFn,
|
|
902
|
-
decompressFn
|
|
1193
|
+
decompressFn,
|
|
1194
|
+
authorization,
|
|
1195
|
+
externalConfig
|
|
903
1196
|
});
|
|
904
1197
|
},
|
|
905
1198
|
async describe() {
|
|
@@ -908,15 +1201,124 @@ function httpConnect(baseUrl, options) {
|
|
|
908
1201
|
close() {}
|
|
909
1202
|
};
|
|
910
1203
|
}
|
|
1204
|
+
// src/client/oauth.ts
|
|
1205
|
+
function parseMetadataJson(json) {
|
|
1206
|
+
const result = {
|
|
1207
|
+
resource: json.resource,
|
|
1208
|
+
authorizationServers: json.authorization_servers
|
|
1209
|
+
};
|
|
1210
|
+
if (json.scopes_supported)
|
|
1211
|
+
result.scopesSupported = json.scopes_supported;
|
|
1212
|
+
if (json.bearer_methods_supported)
|
|
1213
|
+
result.bearerMethodsSupported = json.bearer_methods_supported;
|
|
1214
|
+
if (json.resource_signing_alg_values_supported)
|
|
1215
|
+
result.resourceSigningAlgValuesSupported = json.resource_signing_alg_values_supported;
|
|
1216
|
+
if (json.resource_name)
|
|
1217
|
+
result.resourceName = json.resource_name;
|
|
1218
|
+
if (json.resource_documentation)
|
|
1219
|
+
result.resourceDocumentation = json.resource_documentation;
|
|
1220
|
+
if (json.resource_policy_uri)
|
|
1221
|
+
result.resourcePolicyUri = json.resource_policy_uri;
|
|
1222
|
+
if (json.resource_tos_uri)
|
|
1223
|
+
result.resourceTosUri = json.resource_tos_uri;
|
|
1224
|
+
if (json.client_id)
|
|
1225
|
+
result.clientId = json.client_id;
|
|
1226
|
+
if (json.client_secret)
|
|
1227
|
+
result.clientSecret = json.client_secret;
|
|
1228
|
+
if (json.use_id_token_as_bearer)
|
|
1229
|
+
result.useIdTokenAsBearer = json.use_id_token_as_bearer;
|
|
1230
|
+
if (json.device_code_client_id)
|
|
1231
|
+
result.deviceCodeClientId = json.device_code_client_id;
|
|
1232
|
+
if (json.device_code_client_secret)
|
|
1233
|
+
result.deviceCodeClientSecret = json.device_code_client_secret;
|
|
1234
|
+
return result;
|
|
1235
|
+
}
|
|
1236
|
+
async function httpOAuthMetadata(baseUrl, prefix) {
|
|
1237
|
+
const effectivePrefix = (prefix ?? "").replace(/\/+$/, "");
|
|
1238
|
+
const metadataUrl = `${baseUrl.replace(/\/+$/, "")}/.well-known/oauth-protected-resource${effectivePrefix}`;
|
|
1239
|
+
try {
|
|
1240
|
+
return await fetchOAuthMetadata(metadataUrl);
|
|
1241
|
+
} catch {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
async function fetchOAuthMetadata(metadataUrl) {
|
|
1246
|
+
const response = await fetch(metadataUrl);
|
|
1247
|
+
if (!response.ok) {
|
|
1248
|
+
throw new Error(`Failed to fetch OAuth metadata from ${metadataUrl}: ${response.status}`);
|
|
1249
|
+
}
|
|
1250
|
+
const json = await response.json();
|
|
1251
|
+
return parseMetadataJson(json);
|
|
1252
|
+
}
|
|
1253
|
+
function parseResourceMetadataUrl(wwwAuthenticate) {
|
|
1254
|
+
const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
|
|
1255
|
+
if (!bearerMatch)
|
|
1256
|
+
return null;
|
|
1257
|
+
const params = bearerMatch[1];
|
|
1258
|
+
const metadataMatch = params.match(/resource_metadata="([^"]+)"/);
|
|
1259
|
+
if (!metadataMatch)
|
|
1260
|
+
return null;
|
|
1261
|
+
return metadataMatch[1];
|
|
1262
|
+
}
|
|
1263
|
+
function parseClientId(wwwAuthenticate) {
|
|
1264
|
+
const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
|
|
1265
|
+
if (!bearerMatch)
|
|
1266
|
+
return null;
|
|
1267
|
+
const params = bearerMatch[1];
|
|
1268
|
+
const clientIdMatch = params.match(/client_id="([^"]+)"/);
|
|
1269
|
+
if (!clientIdMatch)
|
|
1270
|
+
return null;
|
|
1271
|
+
return clientIdMatch[1];
|
|
1272
|
+
}
|
|
1273
|
+
function parseClientSecret(wwwAuthenticate) {
|
|
1274
|
+
const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
|
|
1275
|
+
if (!bearerMatch)
|
|
1276
|
+
return null;
|
|
1277
|
+
const params = bearerMatch[1];
|
|
1278
|
+
const match = params.match(/client_secret="([^"]+)"/);
|
|
1279
|
+
if (!match)
|
|
1280
|
+
return null;
|
|
1281
|
+
return match[1];
|
|
1282
|
+
}
|
|
1283
|
+
function parseUseIdTokenAsBearer(wwwAuthenticate) {
|
|
1284
|
+
const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
|
|
1285
|
+
if (!bearerMatch)
|
|
1286
|
+
return false;
|
|
1287
|
+
const params = bearerMatch[1];
|
|
1288
|
+
const match = params.match(/use_id_token_as_bearer="([^"]+)"/);
|
|
1289
|
+
if (!match)
|
|
1290
|
+
return false;
|
|
1291
|
+
return match[1] === "true";
|
|
1292
|
+
}
|
|
1293
|
+
function parseDeviceCodeClientId(wwwAuthenticate) {
|
|
1294
|
+
const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
|
|
1295
|
+
if (!bearerMatch)
|
|
1296
|
+
return null;
|
|
1297
|
+
const params = bearerMatch[1];
|
|
1298
|
+
const match = params.match(/device_code_client_id="([^"]+)"/);
|
|
1299
|
+
if (!match)
|
|
1300
|
+
return null;
|
|
1301
|
+
return match[1];
|
|
1302
|
+
}
|
|
1303
|
+
function parseDeviceCodeClientSecret(wwwAuthenticate) {
|
|
1304
|
+
const bearerMatch = wwwAuthenticate.match(/^Bearer\s+(.*)/i);
|
|
1305
|
+
if (!bearerMatch)
|
|
1306
|
+
return null;
|
|
1307
|
+
const params = bearerMatch[1];
|
|
1308
|
+
const match = params.match(/device_code_client_secret="([^"]+)"/);
|
|
1309
|
+
if (!match)
|
|
1310
|
+
return null;
|
|
1311
|
+
return match[1];
|
|
1312
|
+
}
|
|
911
1313
|
// src/client/pipe.ts
|
|
912
1314
|
import {
|
|
913
1315
|
Field as Field2,
|
|
914
|
-
makeData as
|
|
915
|
-
RecordBatch as
|
|
916
|
-
RecordBatchStreamWriter as
|
|
1316
|
+
makeData as makeData5,
|
|
1317
|
+
RecordBatch as RecordBatch5,
|
|
1318
|
+
RecordBatchStreamWriter as RecordBatchStreamWriter3,
|
|
917
1319
|
Schema as Schema2,
|
|
918
|
-
Struct as
|
|
919
|
-
vectorFromArray as
|
|
1320
|
+
Struct as Struct5,
|
|
1321
|
+
vectorFromArray as vectorFromArray5
|
|
920
1322
|
} from "@query-farm/apache-arrow";
|
|
921
1323
|
class PipeIncrementalWriter {
|
|
922
1324
|
writer;
|
|
@@ -924,7 +1326,7 @@ class PipeIncrementalWriter {
|
|
|
924
1326
|
closed = false;
|
|
925
1327
|
constructor(writeFn, schema) {
|
|
926
1328
|
this.writeFn = writeFn;
|
|
927
|
-
this.writer = new
|
|
1329
|
+
this.writer = new RecordBatchStreamWriter3;
|
|
928
1330
|
this.writer.reset(undefined, schema);
|
|
929
1331
|
this.drain();
|
|
930
1332
|
}
|
|
@@ -962,6 +1364,7 @@ class PipeStreamSession {
|
|
|
962
1364
|
_outputSchema;
|
|
963
1365
|
_releaseBusy;
|
|
964
1366
|
_setDrainPromise;
|
|
1367
|
+
_externalConfig;
|
|
965
1368
|
constructor(opts) {
|
|
966
1369
|
this._reader = opts.reader;
|
|
967
1370
|
this._writeFn = opts.writeFn;
|
|
@@ -970,6 +1373,7 @@ class PipeStreamSession {
|
|
|
970
1373
|
this._outputSchema = opts.outputSchema;
|
|
971
1374
|
this._releaseBusy = opts.releaseBusy;
|
|
972
1375
|
this._setDrainPromise = opts.setDrainPromise;
|
|
1376
|
+
this._externalConfig = opts.externalConfig;
|
|
973
1377
|
}
|
|
974
1378
|
get header() {
|
|
975
1379
|
return this._header;
|
|
@@ -980,6 +1384,9 @@ class PipeStreamSession {
|
|
|
980
1384
|
if (batch === null)
|
|
981
1385
|
return null;
|
|
982
1386
|
if (batch.numRows === 0) {
|
|
1387
|
+
if (isExternalLocationBatch(batch)) {
|
|
1388
|
+
return await resolveExternalLocation(batch, this._externalConfig);
|
|
1389
|
+
}
|
|
983
1390
|
if (dispatchLogOrError(batch, this._onLog)) {
|
|
984
1391
|
continue;
|
|
985
1392
|
}
|
|
@@ -1005,16 +1412,16 @@ class PipeStreamSession {
|
|
|
1005
1412
|
if (input.length === 0) {
|
|
1006
1413
|
inputSchema = this._inputSchema ?? this._outputSchema;
|
|
1007
1414
|
const children = inputSchema.fields.map((f) => {
|
|
1008
|
-
return
|
|
1415
|
+
return makeData5({ type: f.type, length: 0, nullCount: 0 });
|
|
1009
1416
|
});
|
|
1010
|
-
const structType = new
|
|
1011
|
-
const data =
|
|
1417
|
+
const structType = new Struct5(inputSchema.fields);
|
|
1418
|
+
const data = makeData5({
|
|
1012
1419
|
type: structType,
|
|
1013
1420
|
length: 0,
|
|
1014
1421
|
children,
|
|
1015
1422
|
nullCount: 0
|
|
1016
1423
|
});
|
|
1017
|
-
batch = new
|
|
1424
|
+
batch = new RecordBatch5(inputSchema, data);
|
|
1018
1425
|
} else {
|
|
1019
1426
|
const keys = Object.keys(input[0]);
|
|
1020
1427
|
const fields = keys.map((key) => {
|
|
@@ -1039,16 +1446,16 @@ class PipeStreamSession {
|
|
|
1039
1446
|
}
|
|
1040
1447
|
const children = inputSchema.fields.map((f) => {
|
|
1041
1448
|
const values = input.map((row) => row[f.name]);
|
|
1042
|
-
return
|
|
1449
|
+
return vectorFromArray5(values, f.type).data[0];
|
|
1043
1450
|
});
|
|
1044
|
-
const structType = new
|
|
1045
|
-
const data =
|
|
1451
|
+
const structType = new Struct5(inputSchema.fields);
|
|
1452
|
+
const data = makeData5({
|
|
1046
1453
|
type: structType,
|
|
1047
1454
|
length: input.length,
|
|
1048
1455
|
children,
|
|
1049
1456
|
nullCount: 0
|
|
1050
1457
|
});
|
|
1051
|
-
batch = new
|
|
1458
|
+
batch = new RecordBatch5(inputSchema, data);
|
|
1052
1459
|
}
|
|
1053
1460
|
if (!this._inputWriter) {
|
|
1054
1461
|
this._inputWriter = new PipeIncrementalWriter(this._writeFn, inputSchema);
|
|
@@ -1087,14 +1494,14 @@ class PipeStreamSession {
|
|
|
1087
1494
|
try {
|
|
1088
1495
|
const tickSchema = new Schema2([]);
|
|
1089
1496
|
this._inputWriter = new PipeIncrementalWriter(this._writeFn, tickSchema);
|
|
1090
|
-
const structType = new
|
|
1091
|
-
const tickData =
|
|
1497
|
+
const structType = new Struct5(tickSchema.fields);
|
|
1498
|
+
const tickData = makeData5({
|
|
1092
1499
|
type: structType,
|
|
1093
1500
|
length: 0,
|
|
1094
1501
|
children: [],
|
|
1095
1502
|
nullCount: 0
|
|
1096
1503
|
});
|
|
1097
|
-
const tickBatch = new
|
|
1504
|
+
const tickBatch = new RecordBatch5(tickSchema, tickData);
|
|
1098
1505
|
while (true) {
|
|
1099
1506
|
this._inputWriter.write(tickBatch);
|
|
1100
1507
|
await this._ensureOutputStream();
|
|
@@ -1149,6 +1556,7 @@ class PipeStreamSession {
|
|
|
1149
1556
|
}
|
|
1150
1557
|
function pipeConnect(readable, writable, options) {
|
|
1151
1558
|
const onLog = options?.onLog;
|
|
1559
|
+
const externalConfig = options?.externalLocation;
|
|
1152
1560
|
let reader = null;
|
|
1153
1561
|
let readerPromise = null;
|
|
1154
1562
|
let methodCache = null;
|
|
@@ -1224,10 +1632,14 @@ function pipeConnect(readable, writable, options) {
|
|
|
1224
1632
|
throw new Error("EOF reading response");
|
|
1225
1633
|
}
|
|
1226
1634
|
let resultBatch = null;
|
|
1227
|
-
for (
|
|
1635
|
+
for (let batch of response.batches) {
|
|
1228
1636
|
if (batch.numRows === 0) {
|
|
1229
|
-
|
|
1230
|
-
|
|
1637
|
+
if (isExternalLocationBatch(batch)) {
|
|
1638
|
+
batch = await resolveExternalLocation(batch, externalConfig);
|
|
1639
|
+
} else {
|
|
1640
|
+
dispatchLogOrError(batch, onLog);
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1231
1643
|
}
|
|
1232
1644
|
resultBatch = batch;
|
|
1233
1645
|
}
|
|
@@ -1280,7 +1692,8 @@ function pipeConnect(readable, writable, options) {
|
|
|
1280
1692
|
header,
|
|
1281
1693
|
outputSchema,
|
|
1282
1694
|
releaseBusy,
|
|
1283
|
-
setDrainPromise
|
|
1695
|
+
setDrainPromise,
|
|
1696
|
+
externalConfig
|
|
1284
1697
|
});
|
|
1285
1698
|
} catch (e) {
|
|
1286
1699
|
try {
|
|
@@ -1330,7 +1743,8 @@ function subprocessConnect(cmd, options) {
|
|
|
1330
1743
|
}
|
|
1331
1744
|
};
|
|
1332
1745
|
const client = pipeConnect(stdout, writable, {
|
|
1333
|
-
onLog: options?.onLog
|
|
1746
|
+
onLog: options?.onLog,
|
|
1747
|
+
externalLocation: options?.externalLocation
|
|
1334
1748
|
});
|
|
1335
1749
|
const originalClose = client.close;
|
|
1336
1750
|
client.close = () => {
|
|
@@ -1341,127 +1755,144 @@ function subprocessConnect(cmd, options) {
|
|
|
1341
1755
|
};
|
|
1342
1756
|
return client;
|
|
1343
1757
|
}
|
|
1344
|
-
// src/http/
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
continue;
|
|
1368
|
-
if (Array.isArray(val)) {
|
|
1369
|
-
result[field.name] = val.map((v) => typeof v === "number" ? BigInt(v) : v);
|
|
1370
|
-
} else if (typeof val === "number") {
|
|
1371
|
-
result[field.name] = BigInt(val);
|
|
1758
|
+
// src/http/auth.ts
|
|
1759
|
+
function oauthResourceMetadataToJson(metadata) {
|
|
1760
|
+
const json = {
|
|
1761
|
+
resource: metadata.resource,
|
|
1762
|
+
authorization_servers: metadata.authorizationServers
|
|
1763
|
+
};
|
|
1764
|
+
if (metadata.scopesSupported)
|
|
1765
|
+
json.scopes_supported = metadata.scopesSupported;
|
|
1766
|
+
if (metadata.bearerMethodsSupported)
|
|
1767
|
+
json.bearer_methods_supported = metadata.bearerMethodsSupported;
|
|
1768
|
+
if (metadata.resourceSigningAlgValuesSupported)
|
|
1769
|
+
json.resource_signing_alg_values_supported = metadata.resourceSigningAlgValuesSupported;
|
|
1770
|
+
if (metadata.resourceName)
|
|
1771
|
+
json.resource_name = metadata.resourceName;
|
|
1772
|
+
if (metadata.resourceDocumentation)
|
|
1773
|
+
json.resource_documentation = metadata.resourceDocumentation;
|
|
1774
|
+
if (metadata.resourcePolicyUri)
|
|
1775
|
+
json.resource_policy_uri = metadata.resourcePolicyUri;
|
|
1776
|
+
if (metadata.resourceTosUri)
|
|
1777
|
+
json.resource_tos_uri = metadata.resourceTosUri;
|
|
1778
|
+
if (metadata.clientId) {
|
|
1779
|
+
if (!/^[A-Za-z0-9\-._~]+$/.test(metadata.clientId)) {
|
|
1780
|
+
throw new Error(`Invalid client_id: must contain only URL-safe characters [A-Za-z0-9\\-._~]`);
|
|
1372
1781
|
}
|
|
1782
|
+
json.client_id = metadata.clientId;
|
|
1373
1783
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
if (requestId !== null) {
|
|
1380
|
-
metadata.set(REQUEST_ID_KEY, requestId);
|
|
1381
|
-
}
|
|
1382
|
-
if (schema.fields.length === 0) {
|
|
1383
|
-
return buildEmptyBatch(schema, metadata);
|
|
1784
|
+
if (metadata.clientSecret) {
|
|
1785
|
+
if (!/^[A-Za-z0-9\-._~]+$/.test(metadata.clientSecret)) {
|
|
1786
|
+
throw new Error(`Invalid client_secret: must contain only URL-safe characters [A-Za-z0-9\\-._~]`);
|
|
1787
|
+
}
|
|
1788
|
+
json.client_secret = metadata.clientSecret;
|
|
1384
1789
|
}
|
|
1385
|
-
|
|
1386
|
-
if (
|
|
1387
|
-
|
|
1388
|
-
throw new TypeError(`Handler result missing required field '${field.name}'. Got keys: [${got.join(", ")}]`);
|
|
1790
|
+
if (metadata.deviceCodeClientId) {
|
|
1791
|
+
if (!/^[A-Za-z0-9\-._~]+$/.test(metadata.deviceCodeClientId)) {
|
|
1792
|
+
throw new Error(`Invalid device_code_client_id: must contain only URL-safe characters [A-Za-z0-9\\-._~]`);
|
|
1389
1793
|
}
|
|
1794
|
+
json.device_code_client_id = metadata.deviceCodeClientId;
|
|
1390
1795
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
if (val instanceof Data) {
|
|
1395
|
-
return val;
|
|
1796
|
+
if (metadata.deviceCodeClientSecret) {
|
|
1797
|
+
if (!/^[A-Za-z0-9\-._~]+$/.test(metadata.deviceCodeClientSecret)) {
|
|
1798
|
+
throw new Error(`Invalid device_code_client_secret: must contain only URL-safe characters [A-Za-z0-9\\-._~]`);
|
|
1396
1799
|
}
|
|
1397
|
-
|
|
1398
|
-
return arr.data[0];
|
|
1399
|
-
});
|
|
1400
|
-
const structType = new Struct5(schema.fields);
|
|
1401
|
-
const data = makeData5({
|
|
1402
|
-
type: structType,
|
|
1403
|
-
length: 1,
|
|
1404
|
-
children,
|
|
1405
|
-
nullCount: 0
|
|
1406
|
-
});
|
|
1407
|
-
return new RecordBatch5(schema, data, metadata);
|
|
1408
|
-
}
|
|
1409
|
-
function buildErrorBatch(schema, error, serverId, requestId) {
|
|
1410
|
-
const metadata = new Map;
|
|
1411
|
-
metadata.set(LOG_LEVEL_KEY, "EXCEPTION");
|
|
1412
|
-
metadata.set(LOG_MESSAGE_KEY, `${error.constructor.name}: ${error.message}`);
|
|
1413
|
-
const extra = {
|
|
1414
|
-
exception_type: error.constructor.name,
|
|
1415
|
-
exception_message: error.message,
|
|
1416
|
-
traceback: error.stack ?? ""
|
|
1417
|
-
};
|
|
1418
|
-
metadata.set(LOG_EXTRA_KEY, JSON.stringify(extra));
|
|
1419
|
-
metadata.set(SERVER_ID_KEY, serverId);
|
|
1420
|
-
if (requestId !== null) {
|
|
1421
|
-
metadata.set(REQUEST_ID_KEY, requestId);
|
|
1800
|
+
json.device_code_client_secret = metadata.deviceCodeClientSecret;
|
|
1422
1801
|
}
|
|
1423
|
-
|
|
1802
|
+
if (metadata.useIdTokenAsBearer) {
|
|
1803
|
+
json.use_id_token_as_bearer = true;
|
|
1804
|
+
}
|
|
1805
|
+
return json;
|
|
1424
1806
|
}
|
|
1425
|
-
function
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1807
|
+
function wellKnownPath(prefix) {
|
|
1808
|
+
return `/.well-known/oauth-protected-resource${prefix}`;
|
|
1809
|
+
}
|
|
1810
|
+
function buildWwwAuthenticateHeader(metadataUrl, clientId, clientSecret, useIdTokenAsBearer, deviceCodeClientId, deviceCodeClientSecret) {
|
|
1811
|
+
let header = "Bearer";
|
|
1812
|
+
if (metadataUrl) {
|
|
1813
|
+
header += ` resource_metadata="${metadataUrl}"`;
|
|
1431
1814
|
}
|
|
1432
|
-
if (
|
|
1433
|
-
|
|
1815
|
+
if (clientId) {
|
|
1816
|
+
header += `, client_id="${clientId}"`;
|
|
1434
1817
|
}
|
|
1435
|
-
if (
|
|
1436
|
-
|
|
1818
|
+
if (clientSecret) {
|
|
1819
|
+
header += `, client_secret="${clientSecret}"`;
|
|
1437
1820
|
}
|
|
1438
|
-
|
|
1821
|
+
if (deviceCodeClientId) {
|
|
1822
|
+
header += `, device_code_client_id="${deviceCodeClientId}"`;
|
|
1823
|
+
}
|
|
1824
|
+
if (deviceCodeClientSecret) {
|
|
1825
|
+
header += `, device_code_client_secret="${deviceCodeClientSecret}"`;
|
|
1826
|
+
}
|
|
1827
|
+
if (useIdTokenAsBearer) {
|
|
1828
|
+
header += `, use_id_token_as_bearer="true"`;
|
|
1829
|
+
}
|
|
1830
|
+
return header;
|
|
1439
1831
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1832
|
+
// src/http/bearer.ts
|
|
1833
|
+
import { timingSafeEqual } from "node:crypto";
|
|
1834
|
+
function bearerAuthenticate(options) {
|
|
1835
|
+
const { validate } = options;
|
|
1836
|
+
return async function authenticate(request) {
|
|
1837
|
+
const authHeader = request.headers.get("Authorization") ?? "";
|
|
1838
|
+
if (!authHeader.startsWith("Bearer ")) {
|
|
1839
|
+
throw new Error("Missing or invalid Authorization header");
|
|
1840
|
+
}
|
|
1841
|
+
const token = authHeader.slice(7);
|
|
1842
|
+
return validate(token);
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
function safeEqual(a, b) {
|
|
1846
|
+
const enc = new TextEncoder;
|
|
1847
|
+
const bufA = enc.encode(a);
|
|
1848
|
+
const bufB = enc.encode(b);
|
|
1849
|
+
if (bufA.byteLength !== bufB.byteLength)
|
|
1850
|
+
return false;
|
|
1851
|
+
return timingSafeEqual(bufA, bufB);
|
|
1852
|
+
}
|
|
1853
|
+
function bearerAuthenticateStatic(options) {
|
|
1854
|
+
const entries = options.tokens instanceof Map ? [...options.tokens.entries()] : Object.entries(options.tokens);
|
|
1855
|
+
function validate(token) {
|
|
1856
|
+
for (const [key, ctx] of entries) {
|
|
1857
|
+
if (safeEqual(token, key))
|
|
1858
|
+
return ctx;
|
|
1859
|
+
}
|
|
1860
|
+
throw new Error("Unknown bearer token");
|
|
1453
1861
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1862
|
+
return bearerAuthenticate({ validate });
|
|
1863
|
+
}
|
|
1864
|
+
function isCredentialError(err) {
|
|
1865
|
+
return err instanceof Error && err.constructor === Error && err.name !== "PermissionError";
|
|
1866
|
+
}
|
|
1867
|
+
function chainAuthenticate(...authenticators) {
|
|
1868
|
+
if (authenticators.length === 0) {
|
|
1869
|
+
throw new Error("chainAuthenticate requires at least one authenticator");
|
|
1870
|
+
}
|
|
1871
|
+
return async function authenticate(request) {
|
|
1872
|
+
let lastError = null;
|
|
1873
|
+
for (const authFn of authenticators) {
|
|
1874
|
+
try {
|
|
1875
|
+
return await authFn(request);
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
if (isCredentialError(err)) {
|
|
1878
|
+
lastError = err;
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
throw err;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
const error = new Error("No authenticator accepted the request");
|
|
1885
|
+
if (lastError)
|
|
1886
|
+
error.cause = lastError;
|
|
1887
|
+
throw error;
|
|
1888
|
+
};
|
|
1462
1889
|
}
|
|
1890
|
+
// src/http/handler.ts
|
|
1891
|
+
import { randomBytes } from "node:crypto";
|
|
1892
|
+
import { Schema as Schema5 } from "@query-farm/apache-arrow";
|
|
1463
1893
|
|
|
1464
1894
|
// src/types.ts
|
|
1895
|
+
import { RecordBatch as RecordBatch6, recordBatchFromArrays } from "@query-farm/apache-arrow";
|
|
1465
1896
|
var MethodType;
|
|
1466
1897
|
((MethodType2) => {
|
|
1467
1898
|
MethodType2["UNARY"] = "unary";
|
|
@@ -1476,11 +1907,13 @@ class OutputCollector {
|
|
|
1476
1907
|
_outputSchema;
|
|
1477
1908
|
_serverId;
|
|
1478
1909
|
_requestId;
|
|
1479
|
-
|
|
1910
|
+
auth;
|
|
1911
|
+
constructor(outputSchema, producerMode = true, serverId = "", requestId = null, authContext) {
|
|
1480
1912
|
this._outputSchema = outputSchema;
|
|
1481
1913
|
this._producerMode = producerMode;
|
|
1482
1914
|
this._serverId = serverId;
|
|
1483
1915
|
this._requestId = requestId;
|
|
1916
|
+
this.auth = authContext ?? AuthContext.anonymous();
|
|
1484
1917
|
}
|
|
1485
1918
|
get outputSchema() {
|
|
1486
1919
|
return this._outputSchema;
|
|
@@ -1528,7 +1961,7 @@ class OutputCollector {
|
|
|
1528
1961
|
init_zstd();
|
|
1529
1962
|
|
|
1530
1963
|
// src/http/dispatch.ts
|
|
1531
|
-
import { RecordBatch as RecordBatch8, RecordBatchReader as
|
|
1964
|
+
import { RecordBatch as RecordBatch8, RecordBatchReader as RecordBatchReader6, Schema as Schema4 } from "@query-farm/apache-arrow";
|
|
1532
1965
|
|
|
1533
1966
|
// src/dispatch/describe.ts
|
|
1534
1967
|
import {
|
|
@@ -1544,9 +1977,9 @@ import {
|
|
|
1544
1977
|
} from "@query-farm/apache-arrow";
|
|
1545
1978
|
|
|
1546
1979
|
// src/util/schema.ts
|
|
1547
|
-
import { RecordBatchStreamWriter as
|
|
1980
|
+
import { RecordBatchStreamWriter as RecordBatchStreamWriter4 } from "@query-farm/apache-arrow";
|
|
1548
1981
|
function serializeSchema(schema) {
|
|
1549
|
-
const writer = new
|
|
1982
|
+
const writer = new RecordBatchStreamWriter4;
|
|
1550
1983
|
writer.reset(undefined, schema);
|
|
1551
1984
|
writer.close();
|
|
1552
1985
|
return writer.toUint8Array(true);
|
|
@@ -1563,7 +1996,9 @@ var DESCRIBE_SCHEMA = new Schema3([
|
|
|
1563
1996
|
new Field3("param_types_json", new Utf82, true),
|
|
1564
1997
|
new Field3("param_defaults_json", new Utf82, true),
|
|
1565
1998
|
new Field3("has_header", new Bool2, false),
|
|
1566
|
-
new Field3("header_schema_ipc", new Binary2, true)
|
|
1999
|
+
new Field3("header_schema_ipc", new Binary2, true),
|
|
2000
|
+
new Field3("is_exchange", new Bool2, true),
|
|
2001
|
+
new Field3("param_docs_json", new Utf82, true)
|
|
1567
2002
|
]);
|
|
1568
2003
|
function buildDescribeBatch(protocolName, methods, serverId) {
|
|
1569
2004
|
const sortedEntries = [...methods.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
@@ -1577,6 +2012,8 @@ function buildDescribeBatch(protocolName, methods, serverId) {
|
|
|
1577
2012
|
const paramDefaultsJsons = [];
|
|
1578
2013
|
const hasHeaders = [];
|
|
1579
2014
|
const headerSchemas = [];
|
|
2015
|
+
const isExchanges = [];
|
|
2016
|
+
const paramDocsJsons = [];
|
|
1580
2017
|
for (const [name, method] of sortedEntries) {
|
|
1581
2018
|
names.push(name);
|
|
1582
2019
|
methodTypes.push(method.type);
|
|
@@ -1603,6 +2040,14 @@ function buildDescribeBatch(protocolName, methods, serverId) {
|
|
|
1603
2040
|
}
|
|
1604
2041
|
hasHeaders.push(!!method.headerSchema);
|
|
1605
2042
|
headerSchemas.push(method.headerSchema ? serializeSchema(method.headerSchema) : null);
|
|
2043
|
+
if (method.exchangeFn) {
|
|
2044
|
+
isExchanges.push(true);
|
|
2045
|
+
} else if (method.producerFn) {
|
|
2046
|
+
isExchanges.push(false);
|
|
2047
|
+
} else {
|
|
2048
|
+
isExchanges.push(null);
|
|
2049
|
+
}
|
|
2050
|
+
paramDocsJsons.push(null);
|
|
1606
2051
|
}
|
|
1607
2052
|
const nameArr = vectorFromArray6(names, new Utf82);
|
|
1608
2053
|
const methodTypeArr = vectorFromArray6(methodTypes, new Utf82);
|
|
@@ -1614,6 +2059,8 @@ function buildDescribeBatch(protocolName, methods, serverId) {
|
|
|
1614
2059
|
const paramDefaultsArr = vectorFromArray6(paramDefaultsJsons, new Utf82);
|
|
1615
2060
|
const hasHeaderArr = vectorFromArray6(hasHeaders, new Bool2);
|
|
1616
2061
|
const headerSchemaArr = vectorFromArray6(headerSchemas, new Binary2);
|
|
2062
|
+
const isExchangeArr = vectorFromArray6(isExchanges, new Bool2);
|
|
2063
|
+
const paramDocsArr = vectorFromArray6(paramDocsJsons, new Utf82);
|
|
1617
2064
|
const children = [
|
|
1618
2065
|
nameArr.data[0],
|
|
1619
2066
|
methodTypeArr.data[0],
|
|
@@ -1624,7 +2071,9 @@ function buildDescribeBatch(protocolName, methods, serverId) {
|
|
|
1624
2071
|
paramTypesArr.data[0],
|
|
1625
2072
|
paramDefaultsArr.data[0],
|
|
1626
2073
|
hasHeaderArr.data[0],
|
|
1627
|
-
headerSchemaArr.data[0]
|
|
2074
|
+
headerSchemaArr.data[0],
|
|
2075
|
+
isExchangeArr.data[0],
|
|
2076
|
+
paramDocsArr.data[0]
|
|
1628
2077
|
];
|
|
1629
2078
|
const structType = new Struct6(DESCRIBE_SCHEMA.fields);
|
|
1630
2079
|
const data = makeData6({
|
|
@@ -1687,7 +2136,7 @@ function parseRequest(schema, batch) {
|
|
|
1687
2136
|
}
|
|
1688
2137
|
|
|
1689
2138
|
// src/http/token.ts
|
|
1690
|
-
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2139
|
+
import { createHmac, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
1691
2140
|
var TOKEN_VERSION = 2;
|
|
1692
2141
|
var HMAC_LEN = 32;
|
|
1693
2142
|
var MIN_TOKEN_LEN = 1 + 8 + 12 + HMAC_LEN;
|
|
@@ -1724,7 +2173,7 @@ function unpackStateToken(tokenBase64, signingKey, tokenTtl) {
|
|
|
1724
2173
|
const payload = token.subarray(0, token.length - HMAC_LEN);
|
|
1725
2174
|
const receivedMac = token.subarray(token.length - HMAC_LEN);
|
|
1726
2175
|
const expectedMac = createHmac("sha256", signingKey).update(payload).digest();
|
|
1727
|
-
if (!
|
|
2176
|
+
if (!timingSafeEqual2(receivedMac, expectedMac)) {
|
|
1728
2177
|
throw new Error("State token HMAC verification failed");
|
|
1729
2178
|
}
|
|
1730
2179
|
let offset = 0;
|
|
@@ -1766,7 +2215,7 @@ function unpackStateToken(tokenBase64, signingKey, tokenTtl) {
|
|
|
1766
2215
|
|
|
1767
2216
|
// src/http/dispatch.ts
|
|
1768
2217
|
async function deserializeSchema2(bytes) {
|
|
1769
|
-
const reader = await
|
|
2218
|
+
const reader = await RecordBatchReader6.from(bytes);
|
|
1770
2219
|
await reader.open();
|
|
1771
2220
|
return reader.schema;
|
|
1772
2221
|
}
|
|
@@ -1783,15 +2232,20 @@ async function httpDispatchUnary(method, body, ctx) {
|
|
|
1783
2232
|
if (parsed.methodName !== method.name) {
|
|
1784
2233
|
throw new HttpRpcError(`Method name in request '${parsed.methodName}' does not match URL '${method.name}'`, 400);
|
|
1785
2234
|
}
|
|
1786
|
-
const out = new OutputCollector(schema, true, ctx.serverId, parsed.requestId);
|
|
2235
|
+
const out = new OutputCollector(schema, true, ctx.serverId, parsed.requestId, ctx.authContext);
|
|
1787
2236
|
try {
|
|
1788
2237
|
const result = await method.handler(parsed.params, out);
|
|
1789
|
-
|
|
2238
|
+
let resultBatch = buildResultBatch(schema, result, ctx.serverId, parsed.requestId);
|
|
2239
|
+
if (ctx.externalLocation) {
|
|
2240
|
+
resultBatch = await maybeExternalizeBatch(resultBatch, ctx.externalLocation);
|
|
2241
|
+
}
|
|
1790
2242
|
const batches = [...out.batches.map((b) => b.batch), resultBatch];
|
|
1791
2243
|
return arrowResponse(serializeIpcStream(schema, batches));
|
|
1792
2244
|
} catch (error) {
|
|
1793
2245
|
const errBatch = buildErrorBatch(schema, error, ctx.serverId, parsed.requestId);
|
|
1794
|
-
|
|
2246
|
+
const response = arrowResponse(serializeIpcStream(schema, [errBatch]), 500);
|
|
2247
|
+
response.__dispatchError = error;
|
|
2248
|
+
return response;
|
|
1795
2249
|
}
|
|
1796
2250
|
}
|
|
1797
2251
|
async function httpDispatchStreamInit(method, body, ctx) {
|
|
@@ -1813,21 +2267,25 @@ async function httpDispatchStreamInit(method, body, ctx) {
|
|
|
1813
2267
|
} catch (error) {
|
|
1814
2268
|
const errSchema = method.headerSchema ?? EMPTY_SCHEMA;
|
|
1815
2269
|
const errBatch = buildErrorBatch(errSchema, error, ctx.serverId, parsed.requestId);
|
|
1816
|
-
|
|
2270
|
+
const response = arrowResponse(serializeIpcStream(errSchema, [errBatch]), 500);
|
|
2271
|
+
response.__dispatchError = error;
|
|
2272
|
+
return response;
|
|
1817
2273
|
}
|
|
1818
2274
|
const resolvedOutputSchema = state?.__outputSchema ?? outputSchema;
|
|
1819
2275
|
const effectiveProducer = state?.__isProducer ?? isProducer;
|
|
1820
2276
|
let headerBytes = null;
|
|
1821
2277
|
if (method.headerSchema && method.headerInit) {
|
|
1822
2278
|
try {
|
|
1823
|
-
const headerOut = new OutputCollector(method.headerSchema, true, ctx.serverId, parsed.requestId);
|
|
2279
|
+
const headerOut = new OutputCollector(method.headerSchema, true, ctx.serverId, parsed.requestId, ctx.authContext);
|
|
1824
2280
|
const headerValues = method.headerInit(parsed.params, state, headerOut);
|
|
1825
2281
|
const headerBatch = buildResultBatch(method.headerSchema, headerValues, ctx.serverId, parsed.requestId);
|
|
1826
2282
|
const headerBatches = [...headerOut.batches.map((b) => b.batch), headerBatch];
|
|
1827
2283
|
headerBytes = serializeIpcStream(method.headerSchema, headerBatches);
|
|
1828
2284
|
} catch (error) {
|
|
1829
2285
|
const errBatch = buildErrorBatch(method.headerSchema, error, ctx.serverId, parsed.requestId);
|
|
1830
|
-
|
|
2286
|
+
const response = arrowResponse(serializeIpcStream(method.headerSchema, [errBatch]), 500);
|
|
2287
|
+
response.__dispatchError = error;
|
|
2288
|
+
return response;
|
|
1831
2289
|
}
|
|
1832
2290
|
}
|
|
1833
2291
|
if (effectiveProducer) {
|
|
@@ -1888,7 +2346,7 @@ async function httpDispatchStreamExchange(method, body, ctx) {
|
|
|
1888
2346
|
if (effectiveProducer) {
|
|
1889
2347
|
return produceStreamResponse(method, state, outputSchema, inputSchema, ctx, null, null);
|
|
1890
2348
|
} else {
|
|
1891
|
-
const out = new OutputCollector(outputSchema, effectiveProducer, ctx.serverId, null);
|
|
2349
|
+
const out = new OutputCollector(outputSchema, effectiveProducer, ctx.serverId, null, ctx.authContext);
|
|
1892
2350
|
const conformedBatch = conformBatchToSchema(reqBatch, inputSchema);
|
|
1893
2351
|
try {
|
|
1894
2352
|
if (method.exchangeFn) {
|
|
@@ -1902,7 +2360,9 @@ async function httpDispatchStreamExchange(method, body, ctx) {
|
|
|
1902
2360
|
`).slice(0, 5).join(`
|
|
1903
2361
|
`));
|
|
1904
2362
|
const errBatch = buildErrorBatch(outputSchema, error, ctx.serverId, null);
|
|
1905
|
-
|
|
2363
|
+
const response = arrowResponse(serializeIpcStream(outputSchema, [errBatch]), 500);
|
|
2364
|
+
response.__dispatchError = error;
|
|
2365
|
+
return response;
|
|
1906
2366
|
}
|
|
1907
2367
|
const batches = [];
|
|
1908
2368
|
if (out.finished) {
|
|
@@ -1937,8 +2397,9 @@ async function produceStreamResponse(method, state, outputSchema, inputSchema, c
|
|
|
1937
2397
|
const allBatches = [];
|
|
1938
2398
|
const maxBytes = ctx.maxStreamResponseBytes;
|
|
1939
2399
|
let estimatedBytes = 0;
|
|
2400
|
+
let producerError;
|
|
1940
2401
|
while (true) {
|
|
1941
|
-
const out = new OutputCollector(outputSchema, true, ctx.serverId, requestId);
|
|
2402
|
+
const out = new OutputCollector(outputSchema, true, ctx.serverId, requestId, ctx.authContext);
|
|
1942
2403
|
try {
|
|
1943
2404
|
if (method.producerFn) {
|
|
1944
2405
|
await method.producerFn(state, out);
|
|
@@ -1952,6 +2413,7 @@ async function produceStreamResponse(method, state, outputSchema, inputSchema, c
|
|
|
1952
2413
|
`).slice(0, 3).join(`
|
|
1953
2414
|
`));
|
|
1954
2415
|
allBatches.push(buildErrorBatch(outputSchema, error, ctx.serverId, requestId));
|
|
2416
|
+
producerError = error instanceof Error ? error : new Error(String(error));
|
|
1955
2417
|
break;
|
|
1956
2418
|
}
|
|
1957
2419
|
for (const emitted of out.batches) {
|
|
@@ -1981,7 +2443,11 @@ async function produceStreamResponse(method, state, outputSchema, inputSchema, c
|
|
|
1981
2443
|
} else {
|
|
1982
2444
|
responseBody = dataBytes;
|
|
1983
2445
|
}
|
|
1984
|
-
|
|
2446
|
+
const response = arrowResponse(responseBody);
|
|
2447
|
+
if (producerError) {
|
|
2448
|
+
response.__dispatchError = producerError;
|
|
2449
|
+
}
|
|
2450
|
+
return response;
|
|
1985
2451
|
}
|
|
1986
2452
|
function concatBytes(...arrays) {
|
|
1987
2453
|
const totalLen = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
@@ -1994,6 +2460,261 @@ function concatBytes(...arrays) {
|
|
|
1994
2460
|
return result;
|
|
1995
2461
|
}
|
|
1996
2462
|
|
|
2463
|
+
// src/http/pages.ts
|
|
2464
|
+
var LOGO_URL = "https://vgi-rpc-python.query.farm/assets/logo-hero.png";
|
|
2465
|
+
var FONTS = `<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
2466
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
2467
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">`;
|
|
2468
|
+
function escapeHtml(s) {
|
|
2469
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2470
|
+
}
|
|
2471
|
+
function arrowTypeToString(type) {
|
|
2472
|
+
const id = type.typeId;
|
|
2473
|
+
if (id === 5)
|
|
2474
|
+
return "str";
|
|
2475
|
+
if (id === 4)
|
|
2476
|
+
return "bytes";
|
|
2477
|
+
if (id === 2)
|
|
2478
|
+
return "int";
|
|
2479
|
+
if (id === 3)
|
|
2480
|
+
return "float";
|
|
2481
|
+
if (id === 6)
|
|
2482
|
+
return "bool";
|
|
2483
|
+
if (id === 12)
|
|
2484
|
+
return "list";
|
|
2485
|
+
if (id === 17)
|
|
2486
|
+
return "map";
|
|
2487
|
+
if (id === 24)
|
|
2488
|
+
return "enum";
|
|
2489
|
+
return type.toString();
|
|
2490
|
+
}
|
|
2491
|
+
function buildLandingPage(protocolName, serverId, describePath, repoUrl) {
|
|
2492
|
+
const links = [];
|
|
2493
|
+
if (describePath) {
|
|
2494
|
+
links.push(`<a class="primary" href="${escapeHtml(describePath)}">View service API</a>`);
|
|
2495
|
+
}
|
|
2496
|
+
if (repoUrl) {
|
|
2497
|
+
links.push(`<a href="${escapeHtml(repoUrl)}">Source repository</a>`);
|
|
2498
|
+
}
|
|
2499
|
+
links.push(`<a href="https://vgi-rpc.query.farm">Learn more about <code>vgi-rpc</code></a>`);
|
|
2500
|
+
return `<!DOCTYPE html>
|
|
2501
|
+
<html lang="en">
|
|
2502
|
+
<head>
|
|
2503
|
+
<meta charset="utf-8">
|
|
2504
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2505
|
+
<title>${escapeHtml(protocolName)} — vgi-rpc</title>
|
|
2506
|
+
${FONTS}
|
|
2507
|
+
<style>
|
|
2508
|
+
body { font-family: 'Inter', system-ui, -apple-system, sans-serif; max-width: 600px;
|
|
2509
|
+
margin: 0 auto; padding: 60px 20px 0; color: #2c2c1e; text-align: center;
|
|
2510
|
+
background: #faf8f0; }
|
|
2511
|
+
.logo { margin-bottom: 24px; }
|
|
2512
|
+
.logo img { width: 140px; height: 140px; border-radius: 50%;
|
|
2513
|
+
box-shadow: 0 4px 24px rgba(0,0,0,0.12); }
|
|
2514
|
+
h1 { color: #2d5016; margin-bottom: 8px; font-weight: 700; }
|
|
2515
|
+
code { font-family: 'JetBrains Mono', monospace; background: #f0ece0;
|
|
2516
|
+
padding: 2px 6px; border-radius: 3px; font-size: 0.9em; color: #2c2c1e; }
|
|
2517
|
+
a { color: #2d5016; text-decoration: none; }
|
|
2518
|
+
a:hover { color: #4a7c23; }
|
|
2519
|
+
p { line-height: 1.7; color: #6b6b5a; }
|
|
2520
|
+
.meta { font-size: 0.9em; color: #6b6b5a; }
|
|
2521
|
+
.links { margin-top: 28px; display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; }
|
|
2522
|
+
.links a { display: inline-block; padding: 8px 18px; border-radius: 6px;
|
|
2523
|
+
border: 1px solid #4a7c23; color: #2d5016; font-weight: 600;
|
|
2524
|
+
font-size: 0.9em; transition: all 0.2s ease; }
|
|
2525
|
+
.links a:hover { background: #4a7c23; color: #fff; }
|
|
2526
|
+
.links a.primary { background: #2d5016; color: #fff; border-color: #2d5016; }
|
|
2527
|
+
.links a.primary:hover { background: #4a7c23; border-color: #4a7c23; }
|
|
2528
|
+
footer { margin-top: 48px; padding: 20px 0; border-top: 1px solid #f0ece0;
|
|
2529
|
+
color: #6b6b5a; font-size: 0.85em; }
|
|
2530
|
+
footer a { color: #2d5016; font-weight: 600; }
|
|
2531
|
+
footer a:hover { color: #4a7c23; }
|
|
2532
|
+
</style>
|
|
2533
|
+
</head>
|
|
2534
|
+
<body>
|
|
2535
|
+
<div class="logo">
|
|
2536
|
+
<img src="${LOGO_URL}" alt="vgi-rpc logo">
|
|
2537
|
+
</div>
|
|
2538
|
+
<h1>${escapeHtml(protocolName)}</h1>
|
|
2539
|
+
<p class="meta">Powered by <code>vgi-rpc</code> (TypeScript) · server <code>${escapeHtml(serverId)}</code></p>
|
|
2540
|
+
<p>This is a <code>vgi-rpc</code> service endpoint.</p>
|
|
2541
|
+
<div class="links">
|
|
2542
|
+
${links.join(`
|
|
2543
|
+
`)}
|
|
2544
|
+
</div>
|
|
2545
|
+
<footer>
|
|
2546
|
+
© 2026 🚜 <a href="https://query.farm">Query.Farm LLC</a>
|
|
2547
|
+
</footer>
|
|
2548
|
+
</body>
|
|
2549
|
+
</html>`;
|
|
2550
|
+
}
|
|
2551
|
+
function buildNotFoundPage(prefix, protocolName) {
|
|
2552
|
+
const nameFragment = protocolName ? ` (<strong>${escapeHtml(protocolName)}</strong>)` : "";
|
|
2553
|
+
const prefixDisplay = prefix || "/";
|
|
2554
|
+
return `<!DOCTYPE html>
|
|
2555
|
+
<html lang="en">
|
|
2556
|
+
<head>
|
|
2557
|
+
<meta charset="utf-8">
|
|
2558
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2559
|
+
<title>404 — vgi-rpc endpoint</title>
|
|
2560
|
+
<style>
|
|
2561
|
+
body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px;
|
|
2562
|
+
margin: 60px auto; padding: 0 20px; color: #333; text-align: center; }
|
|
2563
|
+
.logo { margin-bottom: 24px; }
|
|
2564
|
+
.logo img { width: 120px; height: 120px; }
|
|
2565
|
+
h1 { color: #555; }
|
|
2566
|
+
code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.95em; }
|
|
2567
|
+
a { color: #0066cc; }
|
|
2568
|
+
p { line-height: 1.6; }
|
|
2569
|
+
</style>
|
|
2570
|
+
</head>
|
|
2571
|
+
<body>
|
|
2572
|
+
<div class="logo">
|
|
2573
|
+
<img src="${LOGO_URL}" alt="vgi-rpc logo">
|
|
2574
|
+
</div>
|
|
2575
|
+
<h1>404 — Not Found</h1>
|
|
2576
|
+
<p>This is a <code>vgi-rpc</code> service endpoint${nameFragment}.</p>
|
|
2577
|
+
<p>RPC methods are available under <code>${escapeHtml(prefixDisplay)}/<method></code>.</p>
|
|
2578
|
+
<p>Learn more at <a href="https://vgi-rpc.query.farm">vgi-rpc.query.farm</a>.</p>
|
|
2579
|
+
</body>
|
|
2580
|
+
</html>`;
|
|
2581
|
+
}
|
|
2582
|
+
function buildMethodCard(method) {
|
|
2583
|
+
const name = escapeHtml(method.name);
|
|
2584
|
+
const isUnary = method.type === "unary";
|
|
2585
|
+
const hasHeader = !!method.headerSchema;
|
|
2586
|
+
const badges = [];
|
|
2587
|
+
badges.push(isUnary ? `<span class="badge badge-unary">unary</span>` : `<span class="badge badge-stream">stream</span>`);
|
|
2588
|
+
if (hasHeader)
|
|
2589
|
+
badges.push(`<span class="badge badge-header">header</span>`);
|
|
2590
|
+
let paramsHtml = "";
|
|
2591
|
+
const paramsSchema = method.paramsSchema;
|
|
2592
|
+
if (paramsSchema.fields.length > 0) {
|
|
2593
|
+
const rows = paramsSchema.fields.map((f) => {
|
|
2594
|
+
const paramName = escapeHtml(f.name);
|
|
2595
|
+
const paramType = escapeHtml(arrowTypeToString(f.type));
|
|
2596
|
+
const defaultVal = method.defaults && f.name in method.defaults ? escapeHtml(JSON.stringify(method.defaults[f.name])) : "—";
|
|
2597
|
+
return `<tr><td><code>${paramName}</code></td><td><code>${paramType}</code></td><td>${defaultVal}</td><td>—</td></tr>`;
|
|
2598
|
+
});
|
|
2599
|
+
paramsHtml = `<div class="section-label">Parameters</div>
|
|
2600
|
+
<table><tr><th>Name</th><th>Type</th><th>Default</th><th>Description</th></tr>
|
|
2601
|
+
${rows.join(`
|
|
2602
|
+
`)}
|
|
2603
|
+
</table>`;
|
|
2604
|
+
} else {
|
|
2605
|
+
paramsHtml = `<p class="no-params">No parameters</p>`;
|
|
2606
|
+
}
|
|
2607
|
+
let returnsHtml = "";
|
|
2608
|
+
if (isUnary && method.resultSchema.fields.length > 0) {
|
|
2609
|
+
const rows = method.resultSchema.fields.map((f) => {
|
|
2610
|
+
return `<tr><td><code>${escapeHtml(f.name)}</code></td><td><code>${escapeHtml(arrowTypeToString(f.type))}</code></td></tr>`;
|
|
2611
|
+
});
|
|
2612
|
+
returnsHtml = `<div class="section-label">Returns</div>
|
|
2613
|
+
<table><tr><th>Name</th><th>Type</th></tr>
|
|
2614
|
+
${rows.join(`
|
|
2615
|
+
`)}
|
|
2616
|
+
</table>`;
|
|
2617
|
+
}
|
|
2618
|
+
let headerHtml = "";
|
|
2619
|
+
if (hasHeader && method.headerSchema && method.headerSchema.fields.length > 0) {
|
|
2620
|
+
const rows = method.headerSchema.fields.map((f) => {
|
|
2621
|
+
return `<tr><td><code>${escapeHtml(f.name)}</code></td><td><code>${escapeHtml(arrowTypeToString(f.type))}</code></td></tr>`;
|
|
2622
|
+
});
|
|
2623
|
+
headerHtml = `<div class="section-label">Stream Header</div>
|
|
2624
|
+
<table><tr><th>Name</th><th>Type</th></tr>
|
|
2625
|
+
${rows.join(`
|
|
2626
|
+
`)}
|
|
2627
|
+
</table>`;
|
|
2628
|
+
}
|
|
2629
|
+
const docHtml = method.doc ? `<p class="docstring">${escapeHtml(method.doc)}</p>` : "";
|
|
2630
|
+
return `<div class="card">
|
|
2631
|
+
<div class="card-header">
|
|
2632
|
+
<span class="method-name">${name}</span>
|
|
2633
|
+
${badges.join(`
|
|
2634
|
+
`)}
|
|
2635
|
+
</div>
|
|
2636
|
+
${docHtml}
|
|
2637
|
+
${paramsHtml}
|
|
2638
|
+
${returnsHtml}
|
|
2639
|
+
${headerHtml}
|
|
2640
|
+
</div>`;
|
|
2641
|
+
}
|
|
2642
|
+
function buildDescribePage(protocolName, serverId, methods, repoUrl) {
|
|
2643
|
+
const sortedMethods = [...methods.entries()].filter(([name]) => name !== "__describe__").sort(([a], [b]) => a.localeCompare(b));
|
|
2644
|
+
const cards = sortedMethods.map(([, method]) => buildMethodCard(method)).join(`
|
|
2645
|
+
`);
|
|
2646
|
+
const repoLink = repoUrl ? ` · <a href="${escapeHtml(repoUrl)}">Source</a>` : "";
|
|
2647
|
+
return `<!DOCTYPE html>
|
|
2648
|
+
<html lang="en">
|
|
2649
|
+
<head>
|
|
2650
|
+
<meta charset="utf-8">
|
|
2651
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2652
|
+
<title>${escapeHtml(protocolName)} API Reference — vgi-rpc</title>
|
|
2653
|
+
${FONTS}
|
|
2654
|
+
<style>
|
|
2655
|
+
body { font-family: 'Inter', system-ui, -apple-system, sans-serif; max-width: 900px;
|
|
2656
|
+
margin: 0 auto; padding: 40px 20px 0; color: #2c2c1e; background: #faf8f0; }
|
|
2657
|
+
.header { text-align: center; margin-bottom: 40px; }
|
|
2658
|
+
.header .logo img { width: 80px; height: 80px; border-radius: 50%;
|
|
2659
|
+
box-shadow: 0 3px 16px rgba(0,0,0,0.10); }
|
|
2660
|
+
.header h1 { margin-bottom: 4px; color: #2d5016; font-weight: 700; }
|
|
2661
|
+
.header .subtitle { color: #6b6b5a; font-size: 1.1em; margin-top: 0; }
|
|
2662
|
+
.header .meta { color: #6b6b5a; font-size: 0.9em; }
|
|
2663
|
+
.header .meta a { color: #2d5016; font-weight: 600; }
|
|
2664
|
+
.header .meta a:hover { color: #4a7c23; }
|
|
2665
|
+
code { font-family: 'JetBrains Mono', monospace; background: #f0ece0;
|
|
2666
|
+
padding: 2px 6px; border-radius: 3px; font-size: 0.85em; color: #2c2c1e; }
|
|
2667
|
+
a { color: #2d5016; text-decoration: none; }
|
|
2668
|
+
a:hover { color: #4a7c23; }
|
|
2669
|
+
.card { border: 1px solid #f0ece0; border-radius: 8px; padding: 20px;
|
|
2670
|
+
margin-bottom: 16px; background: #fff; }
|
|
2671
|
+
.card:hover { border-color: #c8a43a; }
|
|
2672
|
+
.card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
|
2673
|
+
.method-name { font-family: 'JetBrains Mono', monospace; font-size: 1.1em; font-weight: 600;
|
|
2674
|
+
color: #2d5016; }
|
|
2675
|
+
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px;
|
|
2676
|
+
font-size: 0.75em; font-weight: 600; text-transform: uppercase;
|
|
2677
|
+
letter-spacing: 0.03em; }
|
|
2678
|
+
.badge-unary { background: #e8f5e0; color: #2d5016; }
|
|
2679
|
+
.badge-stream { background: #e0ecf5; color: #1a4a6b; }
|
|
2680
|
+
.badge-exchange { background: #f5e6f0; color: #6b234a; }
|
|
2681
|
+
.badge-producer { background: #e0f0f5; color: #1a5a6b; }
|
|
2682
|
+
.badge-header { background: #f5eee0; color: #6b4423; }
|
|
2683
|
+
.docstring { color: #6b6b5a; font-size: 0.9em; margin-top: 0; }
|
|
2684
|
+
table { width: 100%; border-collapse: collapse; font-size: 0.9em; }
|
|
2685
|
+
th { text-align: left; padding: 8px 10px; background: #f0ece0; color: #2c2c1e;
|
|
2686
|
+
font-weight: 600; border-bottom: 2px solid #e0dcd0; }
|
|
2687
|
+
td { padding: 8px 10px; border-bottom: 1px solid #f0ece0; }
|
|
2688
|
+
td code { font-size: 0.85em; }
|
|
2689
|
+
.no-params { color: #6b6b5a; font-style: italic; font-size: 0.9em; }
|
|
2690
|
+
.section-label { font-size: 0.8em; font-weight: 600; text-transform: uppercase;
|
|
2691
|
+
letter-spacing: 0.05em; color: #6b6b5a; margin-top: 14px;
|
|
2692
|
+
margin-bottom: 6px; }
|
|
2693
|
+
footer { text-align: center; margin-top: 48px; padding: 20px 0;
|
|
2694
|
+
border-top: 1px solid #f0ece0; color: #6b6b5a; font-size: 0.85em; }
|
|
2695
|
+
footer a { color: #2d5016; font-weight: 600; }
|
|
2696
|
+
footer a:hover { color: #4a7c23; }
|
|
2697
|
+
</style>
|
|
2698
|
+
</head>
|
|
2699
|
+
<body>
|
|
2700
|
+
<div class="header">
|
|
2701
|
+
<div class="logo">
|
|
2702
|
+
<img src="${LOGO_URL}" alt="vgi-rpc logo">
|
|
2703
|
+
</div>
|
|
2704
|
+
<h1>${escapeHtml(protocolName)}</h1>
|
|
2705
|
+
<p class="subtitle">API Reference</p>
|
|
2706
|
+
<p class="meta">Powered by <code>vgi-rpc</code> (TypeScript) · server <code>${escapeHtml(serverId)}</code>${repoLink}</p>
|
|
2707
|
+
</div>
|
|
2708
|
+
${cards}
|
|
2709
|
+
<footer>
|
|
2710
|
+
<a href="https://vgi-rpc.query.farm">Learn more about <code>vgi-rpc</code></a>
|
|
2711
|
+
·
|
|
2712
|
+
© 2026 🚜 <a href="https://query.farm">Query.Farm LLC</a>
|
|
2713
|
+
</footer>
|
|
2714
|
+
</body>
|
|
2715
|
+
</html>`;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
1997
2718
|
// src/http/types.ts
|
|
1998
2719
|
var jsonStateSerializer = {
|
|
1999
2720
|
serialize(state) {
|
|
@@ -2007,22 +2728,35 @@ var jsonStateSerializer = {
|
|
|
2007
2728
|
// src/http/handler.ts
|
|
2008
2729
|
var EMPTY_SCHEMA2 = new Schema5([]);
|
|
2009
2730
|
function createHttpHandler(protocol, options) {
|
|
2010
|
-
const prefix = (options?.prefix ?? "
|
|
2731
|
+
const prefix = (options?.prefix ?? "").replace(/\/+$/, "");
|
|
2011
2732
|
const signingKey = options?.signingKey ?? randomBytes(32);
|
|
2012
2733
|
const tokenTtl = options?.tokenTtl ?? 3600;
|
|
2013
2734
|
const corsOrigins = options?.corsOrigins;
|
|
2014
2735
|
const maxRequestBytes = options?.maxRequestBytes;
|
|
2015
2736
|
const maxStreamResponseBytes = options?.maxStreamResponseBytes;
|
|
2016
2737
|
const serverId = options?.serverId ?? crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
2738
|
+
const authenticate = options?.authenticate;
|
|
2739
|
+
const oauthMetadata = options?.oauthResourceMetadata;
|
|
2017
2740
|
const methods = protocol.getMethods();
|
|
2018
2741
|
const compressionLevel = options?.compressionLevel;
|
|
2019
2742
|
const stateSerializer = options?.stateSerializer ?? jsonStateSerializer;
|
|
2020
|
-
const
|
|
2743
|
+
const dispatchHook = options?.dispatchHook;
|
|
2744
|
+
const enableLandingPage = options?.enableLandingPage ?? true;
|
|
2745
|
+
const enableDescribePage = options?.enableDescribePage ?? true;
|
|
2746
|
+
const enableNotFoundPage = options?.enableNotFoundPage ?? true;
|
|
2747
|
+
const displayName = options?.protocolName ?? protocol.name;
|
|
2748
|
+
const repoUrl = options?.repositoryUrl ?? null;
|
|
2749
|
+
const landingHtml = enableLandingPage ? buildLandingPage(displayName, serverId, enableDescribePage ? `${prefix}/describe` : null, repoUrl) : null;
|
|
2750
|
+
const describeHtml = enableDescribePage ? buildDescribePage(displayName, serverId, methods, repoUrl) : null;
|
|
2751
|
+
const notFoundHtml = enableNotFoundPage ? buildNotFoundPage(prefix, displayName) : null;
|
|
2752
|
+
const externalLocation = options?.externalLocation;
|
|
2753
|
+
const baseCtx = {
|
|
2021
2754
|
signingKey,
|
|
2022
2755
|
tokenTtl,
|
|
2023
2756
|
serverId,
|
|
2024
2757
|
maxStreamResponseBytes,
|
|
2025
|
-
stateSerializer
|
|
2758
|
+
stateSerializer,
|
|
2759
|
+
externalLocation
|
|
2026
2760
|
};
|
|
2027
2761
|
function addCorsHeaders(headers) {
|
|
2028
2762
|
if (corsOrigins) {
|
|
@@ -2053,6 +2787,18 @@ function createHttpHandler(protocol, options) {
|
|
|
2053
2787
|
return async function handler(request) {
|
|
2054
2788
|
const url = new URL(request.url);
|
|
2055
2789
|
const path = url.pathname;
|
|
2790
|
+
if (oauthMetadata && path === wellKnownPath(prefix)) {
|
|
2791
|
+
if (request.method !== "GET") {
|
|
2792
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
2793
|
+
}
|
|
2794
|
+
const body2 = JSON.stringify(oauthResourceMetadataToJson(oauthMetadata));
|
|
2795
|
+
const headers = new Headers({
|
|
2796
|
+
"Content-Type": "application/json",
|
|
2797
|
+
"Cache-Control": "public, max-age=60"
|
|
2798
|
+
});
|
|
2799
|
+
addCorsHeaders(headers);
|
|
2800
|
+
return new Response(body2, { status: 200, headers });
|
|
2801
|
+
}
|
|
2056
2802
|
if (request.method === "OPTIONS") {
|
|
2057
2803
|
if (path === `${prefix}/__capabilities__`) {
|
|
2058
2804
|
const headers = new Headers;
|
|
@@ -2069,6 +2815,24 @@ function createHttpHandler(protocol, options) {
|
|
|
2069
2815
|
}
|
|
2070
2816
|
return new Response(null, { status: 405 });
|
|
2071
2817
|
}
|
|
2818
|
+
if (request.method === "GET") {
|
|
2819
|
+
if (landingHtml && (path === prefix || path === `${prefix}/`)) {
|
|
2820
|
+
const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
|
|
2821
|
+
addCorsHeaders(headers);
|
|
2822
|
+
return new Response(landingHtml, { status: 200, headers });
|
|
2823
|
+
}
|
|
2824
|
+
if (describeHtml && path === `${prefix}/describe`) {
|
|
2825
|
+
const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
|
|
2826
|
+
addCorsHeaders(headers);
|
|
2827
|
+
return new Response(describeHtml, { status: 200, headers });
|
|
2828
|
+
}
|
|
2829
|
+
if (notFoundHtml) {
|
|
2830
|
+
const headers = new Headers({ "Content-Type": "text/html; charset=utf-8" });
|
|
2831
|
+
addCorsHeaders(headers);
|
|
2832
|
+
return new Response(notFoundHtml, { status: 404, headers });
|
|
2833
|
+
}
|
|
2834
|
+
return new Response("Not Found", { status: 404 });
|
|
2835
|
+
}
|
|
2072
2836
|
if (request.method !== "POST") {
|
|
2073
2837
|
return new Response("Method Not Allowed", { status: 405 });
|
|
2074
2838
|
}
|
|
@@ -2088,6 +2852,22 @@ function createHttpHandler(protocol, options) {
|
|
|
2088
2852
|
if (contentEncoding === "zstd") {
|
|
2089
2853
|
body = zstdDecompress(body);
|
|
2090
2854
|
}
|
|
2855
|
+
const ctx = { ...baseCtx };
|
|
2856
|
+
if (authenticate) {
|
|
2857
|
+
try {
|
|
2858
|
+
ctx.authContext = await authenticate(request);
|
|
2859
|
+
} catch (error) {
|
|
2860
|
+
const headers = new Headers({ "Content-Type": "text/plain" });
|
|
2861
|
+
addCorsHeaders(headers);
|
|
2862
|
+
if (oauthMetadata) {
|
|
2863
|
+
const metadataUrl = new URL(request.url);
|
|
2864
|
+
metadataUrl.pathname = wellKnownPath(prefix);
|
|
2865
|
+
metadataUrl.search = "";
|
|
2866
|
+
headers.set("WWW-Authenticate", buildWwwAuthenticateHeader(metadataUrl.toString(), oauthMetadata.clientId, oauthMetadata.clientSecret, oauthMetadata.useIdTokenAsBearer, oauthMetadata.deviceCodeClientId, oauthMetadata.deviceCodeClientSecret));
|
|
2867
|
+
}
|
|
2868
|
+
return new Response(error.message || "Unauthorized", { status: 401, headers });
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2091
2871
|
if (path === `${prefix}/${DESCRIBE_METHOD_NAME}`) {
|
|
2092
2872
|
try {
|
|
2093
2873
|
const response = httpDispatchDescribe(protocol.name, methods, serverId);
|
|
@@ -2119,6 +2899,18 @@ function createHttpHandler(protocol, options) {
|
|
|
2119
2899
|
const err = new Error(`Unknown method: '${methodName}'. Available methods: [${available.join(", ")}]`);
|
|
2120
2900
|
return compressIfAccepted(makeErrorResponse(err, 404), clientAcceptsZstd);
|
|
2121
2901
|
}
|
|
2902
|
+
const methodType = method.type === "unary" /* UNARY */ ? "unary" : "stream";
|
|
2903
|
+
const info = { method: methodName, methodType, serverId, requestId: null };
|
|
2904
|
+
const stats = {
|
|
2905
|
+
inputBatches: 0,
|
|
2906
|
+
outputBatches: 0,
|
|
2907
|
+
inputRows: 0,
|
|
2908
|
+
outputRows: 0,
|
|
2909
|
+
inputBytes: 0,
|
|
2910
|
+
outputBytes: 0
|
|
2911
|
+
};
|
|
2912
|
+
const hookToken = dispatchHook?.onDispatchStart(info);
|
|
2913
|
+
let dispatchError;
|
|
2122
2914
|
try {
|
|
2123
2915
|
let response;
|
|
2124
2916
|
if (action === "call") {
|
|
@@ -2137,100 +2929,1510 @@ function createHttpHandler(protocol, options) {
|
|
|
2137
2929
|
}
|
|
2138
2930
|
response = await httpDispatchStreamExchange(method, body, ctx);
|
|
2139
2931
|
}
|
|
2932
|
+
const internalError = response.__dispatchError;
|
|
2933
|
+
if (internalError) {
|
|
2934
|
+
dispatchError = internalError instanceof Error ? internalError : new Error(String(internalError));
|
|
2935
|
+
}
|
|
2140
2936
|
addCorsHeaders(response.headers);
|
|
2141
2937
|
return compressIfAccepted(response, clientAcceptsZstd);
|
|
2142
2938
|
} catch (error) {
|
|
2939
|
+
dispatchError = error instanceof Error ? error : new Error(String(error));
|
|
2143
2940
|
if (error instanceof HttpRpcError) {
|
|
2144
2941
|
return compressIfAccepted(makeErrorResponse(error, error.statusCode), clientAcceptsZstd);
|
|
2145
2942
|
}
|
|
2146
2943
|
return compressIfAccepted(makeErrorResponse(error, 500), clientAcceptsZstd);
|
|
2944
|
+
} finally {
|
|
2945
|
+
dispatchHook?.onDispatchEnd(hookToken, info, stats, dispatchError);
|
|
2147
2946
|
}
|
|
2148
2947
|
};
|
|
2149
2948
|
}
|
|
2150
|
-
//
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
Utf8 as Utf83
|
|
2166
|
-
} from "@query-farm/apache-arrow";
|
|
2167
|
-
var str = new Utf83;
|
|
2168
|
-
var bytes = new Binary3;
|
|
2169
|
-
var int = new Int642;
|
|
2170
|
-
var int32 = new Int32;
|
|
2171
|
-
var float = new Float642;
|
|
2172
|
-
var float32 = new Float32;
|
|
2173
|
-
var bool = new Bool3;
|
|
2174
|
-
function toSchema(spec) {
|
|
2175
|
-
if (spec instanceof Schema6)
|
|
2176
|
-
return spec;
|
|
2177
|
-
const fields = [];
|
|
2178
|
-
for (const [name, value] of Object.entries(spec)) {
|
|
2179
|
-
if (value instanceof Field4) {
|
|
2180
|
-
fields.push(value);
|
|
2181
|
-
} else if (value instanceof DataType4) {
|
|
2182
|
-
fields.push(new Field4(name, value, false));
|
|
2183
|
-
} else {
|
|
2184
|
-
throw new TypeError(`Invalid schema value for "${name}": expected DataType or Field, got ${typeof value}`);
|
|
2185
|
-
}
|
|
2949
|
+
// node_modules/oauth4webapi/build/index.js
|
|
2950
|
+
var USER_AGENT;
|
|
2951
|
+
if (typeof navigator === "undefined" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) {
|
|
2952
|
+
const NAME = "oauth4webapi";
|
|
2953
|
+
const VERSION = "v3.8.5";
|
|
2954
|
+
USER_AGENT = `${NAME}/${VERSION}`;
|
|
2955
|
+
}
|
|
2956
|
+
function looseInstanceOf(input, expected) {
|
|
2957
|
+
if (input == null) {
|
|
2958
|
+
return false;
|
|
2959
|
+
}
|
|
2960
|
+
try {
|
|
2961
|
+
return input instanceof expected || Object.getPrototypeOf(input)[Symbol.toStringTag] === expected.prototype[Symbol.toStringTag];
|
|
2962
|
+
} catch {
|
|
2963
|
+
return false;
|
|
2186
2964
|
}
|
|
2187
|
-
return new Schema6(fields);
|
|
2188
2965
|
}
|
|
2189
|
-
var
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2966
|
+
var ERR_INVALID_ARG_VALUE = "ERR_INVALID_ARG_VALUE";
|
|
2967
|
+
var ERR_INVALID_ARG_TYPE = "ERR_INVALID_ARG_TYPE";
|
|
2968
|
+
function CodedTypeError(message, code, cause) {
|
|
2969
|
+
const err = new TypeError(message, { cause });
|
|
2970
|
+
Object.assign(err, { code });
|
|
2971
|
+
return err;
|
|
2972
|
+
}
|
|
2973
|
+
var allowInsecureRequests = Symbol();
|
|
2974
|
+
var clockSkew = Symbol();
|
|
2975
|
+
var clockTolerance = Symbol();
|
|
2976
|
+
var customFetch = Symbol();
|
|
2977
|
+
var modifyAssertion = Symbol();
|
|
2978
|
+
var jweDecrypt = Symbol();
|
|
2979
|
+
var jwksCache = Symbol();
|
|
2980
|
+
var encoder = new TextEncoder;
|
|
2981
|
+
var decoder = new TextDecoder;
|
|
2982
|
+
function buf(input) {
|
|
2983
|
+
if (typeof input === "string") {
|
|
2984
|
+
return encoder.encode(input);
|
|
2985
|
+
}
|
|
2986
|
+
return decoder.decode(input);
|
|
2987
|
+
}
|
|
2988
|
+
var encodeBase64Url;
|
|
2989
|
+
if (Uint8Array.prototype.toBase64) {
|
|
2990
|
+
encodeBase64Url = (input) => {
|
|
2991
|
+
if (input instanceof ArrayBuffer) {
|
|
2992
|
+
input = new Uint8Array(input);
|
|
2993
|
+
}
|
|
2994
|
+
return input.toBase64({ alphabet: "base64url", omitPadding: true });
|
|
2995
|
+
};
|
|
2996
|
+
} else {
|
|
2997
|
+
const CHUNK_SIZE = 32768;
|
|
2998
|
+
encodeBase64Url = (input) => {
|
|
2999
|
+
if (input instanceof ArrayBuffer) {
|
|
3000
|
+
input = new Uint8Array(input);
|
|
3001
|
+
}
|
|
3002
|
+
const arr = [];
|
|
3003
|
+
for (let i = 0;i < input.byteLength; i += CHUNK_SIZE) {
|
|
3004
|
+
arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
|
|
3005
|
+
}
|
|
3006
|
+
return btoa(arr.join("")).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
var decodeBase64Url;
|
|
3010
|
+
if (Uint8Array.fromBase64) {
|
|
3011
|
+
decodeBase64Url = (input) => {
|
|
3012
|
+
try {
|
|
3013
|
+
return Uint8Array.fromBase64(input, { alphabet: "base64url" });
|
|
3014
|
+
} catch (cause) {
|
|
3015
|
+
throw CodedTypeError("The input to be decoded is not correctly encoded.", ERR_INVALID_ARG_VALUE, cause);
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
} else {
|
|
3019
|
+
decodeBase64Url = (input) => {
|
|
3020
|
+
try {
|
|
3021
|
+
const binary = atob(input.replace(/-/g, "+").replace(/_/g, "/").replace(/\s/g, ""));
|
|
3022
|
+
const bytes = new Uint8Array(binary.length);
|
|
3023
|
+
for (let i = 0;i < binary.length; i++) {
|
|
3024
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2210
3025
|
}
|
|
3026
|
+
return bytes;
|
|
3027
|
+
} catch (cause) {
|
|
3028
|
+
throw CodedTypeError("The input to be decoded is not correctly encoded.", ERR_INVALID_ARG_VALUE, cause);
|
|
2211
3029
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
function b64u(input) {
|
|
3033
|
+
if (typeof input === "string") {
|
|
3034
|
+
return decodeBase64Url(input);
|
|
2215
3035
|
}
|
|
2216
|
-
return
|
|
3036
|
+
return encodeBase64Url(input);
|
|
2217
3037
|
}
|
|
2218
3038
|
|
|
2219
|
-
|
|
2220
|
-
|
|
3039
|
+
class UnsupportedOperationError extends Error {
|
|
3040
|
+
code;
|
|
3041
|
+
constructor(message, options) {
|
|
3042
|
+
super(message, options);
|
|
3043
|
+
this.name = this.constructor.name;
|
|
3044
|
+
this.code = UNSUPPORTED_OPERATION;
|
|
3045
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
2221
3048
|
|
|
2222
|
-
class
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
this.name = name;
|
|
3049
|
+
class OperationProcessingError extends Error {
|
|
3050
|
+
code;
|
|
3051
|
+
constructor(message, options) {
|
|
3052
|
+
super(message, options);
|
|
3053
|
+
this.name = this.constructor.name;
|
|
3054
|
+
if (options?.code) {
|
|
3055
|
+
this.code = options?.code;
|
|
3056
|
+
}
|
|
3057
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
2227
3058
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
3059
|
+
}
|
|
3060
|
+
function OPE(message, code, cause) {
|
|
3061
|
+
return new OperationProcessingError(message, { code, cause });
|
|
3062
|
+
}
|
|
3063
|
+
async function calculateJwkThumbprint(jwk) {
|
|
3064
|
+
let components;
|
|
3065
|
+
switch (jwk.kty) {
|
|
3066
|
+
case "EC":
|
|
3067
|
+
components = {
|
|
3068
|
+
crv: jwk.crv,
|
|
3069
|
+
kty: jwk.kty,
|
|
3070
|
+
x: jwk.x,
|
|
3071
|
+
y: jwk.y
|
|
3072
|
+
};
|
|
3073
|
+
break;
|
|
3074
|
+
case "OKP":
|
|
3075
|
+
components = {
|
|
3076
|
+
crv: jwk.crv,
|
|
3077
|
+
kty: jwk.kty,
|
|
3078
|
+
x: jwk.x
|
|
3079
|
+
};
|
|
3080
|
+
break;
|
|
3081
|
+
case "AKP":
|
|
3082
|
+
components = {
|
|
3083
|
+
alg: jwk.alg,
|
|
3084
|
+
kty: jwk.kty,
|
|
3085
|
+
pub: jwk.pub
|
|
3086
|
+
};
|
|
3087
|
+
break;
|
|
3088
|
+
case "RSA":
|
|
3089
|
+
components = {
|
|
3090
|
+
e: jwk.e,
|
|
3091
|
+
kty: jwk.kty,
|
|
3092
|
+
n: jwk.n
|
|
3093
|
+
};
|
|
3094
|
+
break;
|
|
3095
|
+
default:
|
|
3096
|
+
throw new UnsupportedOperationError("unsupported JWK key type", { cause: jwk });
|
|
3097
|
+
}
|
|
3098
|
+
return b64u(await crypto.subtle.digest("SHA-256", buf(JSON.stringify(components))));
|
|
3099
|
+
}
|
|
3100
|
+
function assertCryptoKey(key, it) {
|
|
3101
|
+
if (!(key instanceof CryptoKey)) {
|
|
3102
|
+
throw CodedTypeError(`${it} must be a CryptoKey`, ERR_INVALID_ARG_TYPE);
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
function assertPrivateKey(key, it) {
|
|
3106
|
+
assertCryptoKey(key, it);
|
|
3107
|
+
if (key.type !== "private") {
|
|
3108
|
+
throw CodedTypeError(`${it} must be a private CryptoKey`, ERR_INVALID_ARG_VALUE);
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
function assertPublicKey(key, it) {
|
|
3112
|
+
assertCryptoKey(key, it);
|
|
3113
|
+
if (key.type !== "public") {
|
|
3114
|
+
throw CodedTypeError(`${it} must be a public CryptoKey`, ERR_INVALID_ARG_VALUE);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
function normalizeTyp(value) {
|
|
3118
|
+
return value.toLowerCase().replace(/^application\//, "");
|
|
3119
|
+
}
|
|
3120
|
+
function isJsonObject(input) {
|
|
3121
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
3122
|
+
return false;
|
|
3123
|
+
}
|
|
3124
|
+
return true;
|
|
3125
|
+
}
|
|
3126
|
+
function prepareHeaders(input) {
|
|
3127
|
+
if (looseInstanceOf(input, Headers)) {
|
|
3128
|
+
input = Object.fromEntries(input.entries());
|
|
3129
|
+
}
|
|
3130
|
+
const headers = new Headers(input ?? {});
|
|
3131
|
+
if (USER_AGENT && !headers.has("user-agent")) {
|
|
3132
|
+
headers.set("user-agent", USER_AGENT);
|
|
3133
|
+
}
|
|
3134
|
+
if (headers.has("authorization")) {
|
|
3135
|
+
throw CodedTypeError('"options.headers" must not include the "authorization" header name', ERR_INVALID_ARG_VALUE);
|
|
3136
|
+
}
|
|
3137
|
+
return headers;
|
|
3138
|
+
}
|
|
3139
|
+
function signal(url, value) {
|
|
3140
|
+
if (value !== undefined) {
|
|
3141
|
+
if (typeof value === "function") {
|
|
3142
|
+
value = value(url.href);
|
|
3143
|
+
}
|
|
3144
|
+
if (!(value instanceof AbortSignal)) {
|
|
3145
|
+
throw CodedTypeError('"options.signal" must return or be an instance of AbortSignal', ERR_INVALID_ARG_TYPE);
|
|
3146
|
+
}
|
|
3147
|
+
return value;
|
|
3148
|
+
}
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
function replaceDoubleSlash(pathname) {
|
|
3152
|
+
if (pathname.includes("//")) {
|
|
3153
|
+
return pathname.replace("//", "/");
|
|
3154
|
+
}
|
|
3155
|
+
return pathname;
|
|
3156
|
+
}
|
|
3157
|
+
function prependWellKnown(url, wellKnown, allowTerminatingSlash = false) {
|
|
3158
|
+
if (url.pathname === "/") {
|
|
3159
|
+
url.pathname = wellKnown;
|
|
3160
|
+
} else {
|
|
3161
|
+
url.pathname = replaceDoubleSlash(`${wellKnown}/${allowTerminatingSlash ? url.pathname : url.pathname.replace(/(\/)$/, "")}`);
|
|
3162
|
+
}
|
|
3163
|
+
return url;
|
|
3164
|
+
}
|
|
3165
|
+
function appendWellKnown(url, wellKnown) {
|
|
3166
|
+
url.pathname = replaceDoubleSlash(`${url.pathname}/${wellKnown}`);
|
|
3167
|
+
return url;
|
|
3168
|
+
}
|
|
3169
|
+
async function performDiscovery(input, urlName, transform, options) {
|
|
3170
|
+
if (!(input instanceof URL)) {
|
|
3171
|
+
throw CodedTypeError(`"${urlName}" must be an instance of URL`, ERR_INVALID_ARG_TYPE);
|
|
3172
|
+
}
|
|
3173
|
+
checkProtocol(input, options?.[allowInsecureRequests] !== true);
|
|
3174
|
+
const url = transform(new URL(input.href));
|
|
3175
|
+
const headers = prepareHeaders(options?.headers);
|
|
3176
|
+
headers.set("accept", "application/json");
|
|
3177
|
+
return (options?.[customFetch] || fetch)(url.href, {
|
|
3178
|
+
body: undefined,
|
|
3179
|
+
headers: Object.fromEntries(headers.entries()),
|
|
3180
|
+
method: "GET",
|
|
3181
|
+
redirect: "manual",
|
|
3182
|
+
signal: signal(url, options?.signal)
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
async function discoveryRequest(issuerIdentifier, options) {
|
|
3186
|
+
return performDiscovery(issuerIdentifier, "issuerIdentifier", (url) => {
|
|
3187
|
+
switch (options?.algorithm) {
|
|
3188
|
+
case undefined:
|
|
3189
|
+
case "oidc":
|
|
3190
|
+
appendWellKnown(url, ".well-known/openid-configuration");
|
|
3191
|
+
break;
|
|
3192
|
+
case "oauth2":
|
|
3193
|
+
prependWellKnown(url, ".well-known/oauth-authorization-server");
|
|
3194
|
+
break;
|
|
3195
|
+
default:
|
|
3196
|
+
throw CodedTypeError('"options.algorithm" must be "oidc" (default), or "oauth2"', ERR_INVALID_ARG_VALUE);
|
|
3197
|
+
}
|
|
3198
|
+
return url;
|
|
3199
|
+
}, options);
|
|
3200
|
+
}
|
|
3201
|
+
function assertString(input, it, code, cause) {
|
|
3202
|
+
try {
|
|
3203
|
+
if (typeof input !== "string") {
|
|
3204
|
+
throw CodedTypeError(`${it} must be a string`, ERR_INVALID_ARG_TYPE, cause);
|
|
3205
|
+
}
|
|
3206
|
+
if (input.length === 0) {
|
|
3207
|
+
throw CodedTypeError(`${it} must not be empty`, ERR_INVALID_ARG_VALUE, cause);
|
|
3208
|
+
}
|
|
3209
|
+
} catch (err) {
|
|
3210
|
+
if (code) {
|
|
3211
|
+
throw OPE(err.message, code, cause);
|
|
3212
|
+
}
|
|
3213
|
+
throw err;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
async function processDiscoveryResponse(expectedIssuerIdentifier, response) {
|
|
3217
|
+
const expected = expectedIssuerIdentifier;
|
|
3218
|
+
if (!(expected instanceof URL) && expected !== _nodiscoverycheck) {
|
|
3219
|
+
throw CodedTypeError('"expectedIssuerIdentifier" must be an instance of URL', ERR_INVALID_ARG_TYPE);
|
|
3220
|
+
}
|
|
3221
|
+
if (!looseInstanceOf(response, Response)) {
|
|
3222
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
3223
|
+
}
|
|
3224
|
+
if (response.status !== 200) {
|
|
3225
|
+
throw OPE('"response" is not a conform Authorization Server Metadata response (unexpected HTTP status code)', RESPONSE_IS_NOT_CONFORM, response);
|
|
3226
|
+
}
|
|
3227
|
+
assertReadableResponse(response);
|
|
3228
|
+
const json = await getResponseJsonBody(response);
|
|
3229
|
+
assertString(json.issuer, '"response" body "issuer" property', INVALID_RESPONSE, { body: json });
|
|
3230
|
+
if (expected !== _nodiscoverycheck && new URL(json.issuer).href !== expected.href) {
|
|
3231
|
+
throw OPE('"response" body "issuer" property does not match the expected value', JSON_ATTRIBUTE_COMPARISON, { expected: expected.href, body: json, attribute: "issuer" });
|
|
3232
|
+
}
|
|
3233
|
+
return json;
|
|
3234
|
+
}
|
|
3235
|
+
function assertApplicationJson(response) {
|
|
3236
|
+
assertContentType(response, "application/json");
|
|
3237
|
+
}
|
|
3238
|
+
function notJson(response, ...types) {
|
|
3239
|
+
let msg = '"response" content-type must be ';
|
|
3240
|
+
if (types.length > 2) {
|
|
3241
|
+
const last = types.pop();
|
|
3242
|
+
msg += `${types.join(", ")}, or ${last}`;
|
|
3243
|
+
} else if (types.length === 2) {
|
|
3244
|
+
msg += `${types[0]} or ${types[1]}`;
|
|
3245
|
+
} else {
|
|
3246
|
+
msg += types[0];
|
|
3247
|
+
}
|
|
3248
|
+
return OPE(msg, RESPONSE_IS_NOT_JSON, response);
|
|
3249
|
+
}
|
|
3250
|
+
function assertContentTypes(response, ...types) {
|
|
3251
|
+
if (!types.includes(getContentType(response))) {
|
|
3252
|
+
throw notJson(response, ...types);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
function assertContentType(response, contentType) {
|
|
3256
|
+
if (getContentType(response) !== contentType) {
|
|
3257
|
+
throw notJson(response, contentType);
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
function randomBytes2() {
|
|
3261
|
+
return b64u(crypto.getRandomValues(new Uint8Array(32)));
|
|
3262
|
+
}
|
|
3263
|
+
function psAlg(key) {
|
|
3264
|
+
switch (key.algorithm.hash.name) {
|
|
3265
|
+
case "SHA-256":
|
|
3266
|
+
return "PS256";
|
|
3267
|
+
case "SHA-384":
|
|
3268
|
+
return "PS384";
|
|
3269
|
+
case "SHA-512":
|
|
3270
|
+
return "PS512";
|
|
3271
|
+
default:
|
|
3272
|
+
throw new UnsupportedOperationError("unsupported RsaHashedKeyAlgorithm hash name", {
|
|
3273
|
+
cause: key
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
function rsAlg(key) {
|
|
3278
|
+
switch (key.algorithm.hash.name) {
|
|
3279
|
+
case "SHA-256":
|
|
3280
|
+
return "RS256";
|
|
3281
|
+
case "SHA-384":
|
|
3282
|
+
return "RS384";
|
|
3283
|
+
case "SHA-512":
|
|
3284
|
+
return "RS512";
|
|
3285
|
+
default:
|
|
3286
|
+
throw new UnsupportedOperationError("unsupported RsaHashedKeyAlgorithm hash name", {
|
|
3287
|
+
cause: key
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
function esAlg(key) {
|
|
3292
|
+
switch (key.algorithm.namedCurve) {
|
|
3293
|
+
case "P-256":
|
|
3294
|
+
return "ES256";
|
|
3295
|
+
case "P-384":
|
|
3296
|
+
return "ES384";
|
|
3297
|
+
case "P-521":
|
|
3298
|
+
return "ES512";
|
|
3299
|
+
default:
|
|
3300
|
+
throw new UnsupportedOperationError("unsupported EcKeyAlgorithm namedCurve", { cause: key });
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
function keyToJws(key) {
|
|
3304
|
+
switch (key.algorithm.name) {
|
|
3305
|
+
case "RSA-PSS":
|
|
3306
|
+
return psAlg(key);
|
|
3307
|
+
case "RSASSA-PKCS1-v1_5":
|
|
3308
|
+
return rsAlg(key);
|
|
3309
|
+
case "ECDSA":
|
|
3310
|
+
return esAlg(key);
|
|
3311
|
+
case "Ed25519":
|
|
3312
|
+
case "ML-DSA-44":
|
|
3313
|
+
case "ML-DSA-65":
|
|
3314
|
+
case "ML-DSA-87":
|
|
3315
|
+
return key.algorithm.name;
|
|
3316
|
+
case "EdDSA":
|
|
3317
|
+
return "Ed25519";
|
|
3318
|
+
default:
|
|
3319
|
+
throw new UnsupportedOperationError("unsupported CryptoKey algorithm name", { cause: key });
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
function getClockSkew(client) {
|
|
3323
|
+
const skew = client?.[clockSkew];
|
|
3324
|
+
return typeof skew === "number" && Number.isFinite(skew) ? skew : 0;
|
|
3325
|
+
}
|
|
3326
|
+
function getClockTolerance(client) {
|
|
3327
|
+
const tolerance = client?.[clockTolerance];
|
|
3328
|
+
return typeof tolerance === "number" && Number.isFinite(tolerance) && Math.sign(tolerance) !== -1 ? tolerance : 30;
|
|
3329
|
+
}
|
|
3330
|
+
function epochTime() {
|
|
3331
|
+
return Math.floor(Date.now() / 1000);
|
|
3332
|
+
}
|
|
3333
|
+
function assertAs(as) {
|
|
3334
|
+
if (typeof as !== "object" || as === null) {
|
|
3335
|
+
throw CodedTypeError('"as" must be an object', ERR_INVALID_ARG_TYPE);
|
|
3336
|
+
}
|
|
3337
|
+
assertString(as.issuer, '"as.issuer"');
|
|
3338
|
+
}
|
|
3339
|
+
async function signJwt(header, payload, key) {
|
|
3340
|
+
if (!key.usages.includes("sign")) {
|
|
3341
|
+
throw CodedTypeError('CryptoKey instances used for signing assertions must include "sign" in their "usages"', ERR_INVALID_ARG_VALUE);
|
|
3342
|
+
}
|
|
3343
|
+
const input = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(payload)))}`;
|
|
3344
|
+
const signature = b64u(await crypto.subtle.sign(keyToSubtle(key), key, buf(input)));
|
|
3345
|
+
return `${input}.${signature}`;
|
|
3346
|
+
}
|
|
3347
|
+
var jwkCache;
|
|
3348
|
+
async function getSetPublicJwkCache(key, alg) {
|
|
3349
|
+
const { kty, e, n, x, y, crv, pub } = await crypto.subtle.exportKey("jwk", key);
|
|
3350
|
+
const jwk = { kty, e, n, x, y, crv, pub };
|
|
3351
|
+
if (kty === "AKP")
|
|
3352
|
+
jwk.alg = alg;
|
|
3353
|
+
jwkCache.set(key, jwk);
|
|
3354
|
+
return jwk;
|
|
3355
|
+
}
|
|
3356
|
+
async function publicJwk(key, alg) {
|
|
3357
|
+
jwkCache ||= new WeakMap;
|
|
3358
|
+
return jwkCache.get(key) || getSetPublicJwkCache(key, alg);
|
|
3359
|
+
}
|
|
3360
|
+
var URLParse = URL.parse ? (url, base) => URL.parse(url, base) : (url, base) => {
|
|
3361
|
+
try {
|
|
3362
|
+
return new URL(url, base);
|
|
3363
|
+
} catch {
|
|
3364
|
+
return null;
|
|
3365
|
+
}
|
|
3366
|
+
};
|
|
3367
|
+
function checkProtocol(url, enforceHttps) {
|
|
3368
|
+
if (enforceHttps && url.protocol !== "https:") {
|
|
3369
|
+
throw OPE("only requests to HTTPS are allowed", HTTP_REQUEST_FORBIDDEN, url);
|
|
3370
|
+
}
|
|
3371
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
3372
|
+
throw OPE("only HTTP and HTTPS requests are allowed", REQUEST_PROTOCOL_FORBIDDEN, url);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
function validateEndpoint(value, endpoint, useMtlsAlias, enforceHttps) {
|
|
3376
|
+
let url;
|
|
3377
|
+
if (typeof value !== "string" || !(url = URLParse(value))) {
|
|
3378
|
+
throw OPE(`authorization server metadata does not contain a valid ${useMtlsAlias ? `"as.mtls_endpoint_aliases.${endpoint}"` : `"as.${endpoint}"`}`, value === undefined ? MISSING_SERVER_METADATA : INVALID_SERVER_METADATA, { attribute: useMtlsAlias ? `mtls_endpoint_aliases.${endpoint}` : endpoint });
|
|
3379
|
+
}
|
|
3380
|
+
checkProtocol(url, enforceHttps);
|
|
3381
|
+
return url;
|
|
3382
|
+
}
|
|
3383
|
+
function resolveEndpoint(as, endpoint, useMtlsAlias, enforceHttps) {
|
|
3384
|
+
if (useMtlsAlias && as.mtls_endpoint_aliases && endpoint in as.mtls_endpoint_aliases) {
|
|
3385
|
+
return validateEndpoint(as.mtls_endpoint_aliases[endpoint], endpoint, useMtlsAlias, enforceHttps);
|
|
3386
|
+
}
|
|
3387
|
+
return validateEndpoint(as[endpoint], endpoint, useMtlsAlias, enforceHttps);
|
|
3388
|
+
}
|
|
3389
|
+
class DPoPHandler {
|
|
3390
|
+
#header;
|
|
3391
|
+
#privateKey;
|
|
3392
|
+
#publicKey;
|
|
3393
|
+
#clockSkew;
|
|
3394
|
+
#modifyAssertion;
|
|
3395
|
+
#map;
|
|
3396
|
+
#jkt;
|
|
3397
|
+
constructor(client, keyPair, options) {
|
|
3398
|
+
assertPrivateKey(keyPair?.privateKey, '"DPoP.privateKey"');
|
|
3399
|
+
assertPublicKey(keyPair?.publicKey, '"DPoP.publicKey"');
|
|
3400
|
+
if (!keyPair.publicKey.extractable) {
|
|
3401
|
+
throw CodedTypeError('"DPoP.publicKey.extractable" must be true', ERR_INVALID_ARG_VALUE);
|
|
3402
|
+
}
|
|
3403
|
+
this.#modifyAssertion = options?.[modifyAssertion];
|
|
3404
|
+
this.#clockSkew = getClockSkew(client);
|
|
3405
|
+
this.#privateKey = keyPair.privateKey;
|
|
3406
|
+
this.#publicKey = keyPair.publicKey;
|
|
3407
|
+
branded.add(this);
|
|
3408
|
+
}
|
|
3409
|
+
#get(key) {
|
|
3410
|
+
this.#map ||= new Map;
|
|
3411
|
+
let item = this.#map.get(key);
|
|
3412
|
+
if (item) {
|
|
3413
|
+
this.#map.delete(key);
|
|
3414
|
+
this.#map.set(key, item);
|
|
3415
|
+
}
|
|
3416
|
+
return item;
|
|
3417
|
+
}
|
|
3418
|
+
#set(key, val) {
|
|
3419
|
+
this.#map ||= new Map;
|
|
3420
|
+
this.#map.delete(key);
|
|
3421
|
+
if (this.#map.size === 100) {
|
|
3422
|
+
this.#map.delete(this.#map.keys().next().value);
|
|
3423
|
+
}
|
|
3424
|
+
this.#map.set(key, val);
|
|
3425
|
+
}
|
|
3426
|
+
async calculateThumbprint() {
|
|
3427
|
+
if (!this.#jkt) {
|
|
3428
|
+
const jwk = await crypto.subtle.exportKey("jwk", this.#publicKey);
|
|
3429
|
+
this.#jkt ||= await calculateJwkThumbprint(jwk);
|
|
3430
|
+
}
|
|
3431
|
+
return this.#jkt;
|
|
3432
|
+
}
|
|
3433
|
+
async addProof(url, headers, htm, accessToken) {
|
|
3434
|
+
const alg = keyToJws(this.#privateKey);
|
|
3435
|
+
this.#header ||= {
|
|
3436
|
+
alg,
|
|
3437
|
+
typ: "dpop+jwt",
|
|
3438
|
+
jwk: await publicJwk(this.#publicKey, alg)
|
|
3439
|
+
};
|
|
3440
|
+
const nonce = this.#get(url.origin);
|
|
3441
|
+
const now = epochTime() + this.#clockSkew;
|
|
3442
|
+
const payload = {
|
|
3443
|
+
iat: now,
|
|
3444
|
+
jti: randomBytes2(),
|
|
3445
|
+
htm,
|
|
3446
|
+
nonce,
|
|
3447
|
+
htu: `${url.origin}${url.pathname}`,
|
|
3448
|
+
ath: accessToken ? b64u(await crypto.subtle.digest("SHA-256", buf(accessToken))) : undefined
|
|
3449
|
+
};
|
|
3450
|
+
this.#modifyAssertion?.(this.#header, payload);
|
|
3451
|
+
headers.set("dpop", await signJwt(this.#header, payload, this.#privateKey));
|
|
3452
|
+
}
|
|
3453
|
+
cacheNonce(response, url) {
|
|
3454
|
+
try {
|
|
3455
|
+
const nonce = response.headers.get("dpop-nonce");
|
|
3456
|
+
if (nonce) {
|
|
3457
|
+
this.#set(url.origin, nonce);
|
|
3458
|
+
}
|
|
3459
|
+
} catch {}
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
var tokenMatch = "[a-zA-Z0-9!#$%&\\'\\*\\+\\-\\.\\^_`\\|~]+";
|
|
3463
|
+
var token68Match = "[a-zA-Z0-9\\-\\._\\~\\+\\/]+={0,2}";
|
|
3464
|
+
var quotedMatch = '"((?:[^"\\\\]|\\\\[\\s\\S])*)"';
|
|
3465
|
+
var quotedParamMatcher = "(" + tokenMatch + ")\\s*=\\s*" + quotedMatch;
|
|
3466
|
+
var paramMatcher = "(" + tokenMatch + ")\\s*=\\s*(" + tokenMatch + ")";
|
|
3467
|
+
var schemeRE = new RegExp("^[,\\s]*(" + tokenMatch + ")");
|
|
3468
|
+
var quotedParamRE = new RegExp("^[,\\s]*" + quotedParamMatcher + "[,\\s]*(.*)");
|
|
3469
|
+
var unquotedParamRE = new RegExp("^[,\\s]*" + paramMatcher + "[,\\s]*(.*)");
|
|
3470
|
+
var token68ParamRE = new RegExp("^(" + token68Match + ")(?:$|[,\\s])(.*)");
|
|
3471
|
+
var jwksMap;
|
|
3472
|
+
function setJwksCache(as, jwks, uat, cache) {
|
|
3473
|
+
jwksMap ||= new WeakMap;
|
|
3474
|
+
jwksMap.set(as, {
|
|
3475
|
+
jwks,
|
|
3476
|
+
uat,
|
|
3477
|
+
get age() {
|
|
3478
|
+
return epochTime() - this.uat;
|
|
3479
|
+
}
|
|
3480
|
+
});
|
|
3481
|
+
if (cache) {
|
|
3482
|
+
Object.assign(cache, { jwks: structuredClone(jwks), uat });
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
function isFreshJwksCache(input) {
|
|
3486
|
+
if (typeof input !== "object" || input === null) {
|
|
3487
|
+
return false;
|
|
3488
|
+
}
|
|
3489
|
+
if (!("uat" in input) || typeof input.uat !== "number" || epochTime() - input.uat >= 300) {
|
|
3490
|
+
return false;
|
|
3491
|
+
}
|
|
3492
|
+
if (!("jwks" in input) || !isJsonObject(input.jwks) || !Array.isArray(input.jwks.keys) || !Array.prototype.every.call(input.jwks.keys, isJsonObject)) {
|
|
3493
|
+
return false;
|
|
3494
|
+
}
|
|
3495
|
+
return true;
|
|
3496
|
+
}
|
|
3497
|
+
function clearJwksCache(as, cache) {
|
|
3498
|
+
jwksMap?.delete(as);
|
|
3499
|
+
delete cache?.jwks;
|
|
3500
|
+
delete cache?.uat;
|
|
3501
|
+
}
|
|
3502
|
+
async function getPublicSigKeyFromIssuerJwksUri(as, options, header) {
|
|
3503
|
+
const { alg, kid } = header;
|
|
3504
|
+
checkSupportedJwsAlg(header);
|
|
3505
|
+
if (!jwksMap?.has(as) && isFreshJwksCache(options?.[jwksCache])) {
|
|
3506
|
+
setJwksCache(as, options?.[jwksCache].jwks, options?.[jwksCache].uat);
|
|
3507
|
+
}
|
|
3508
|
+
let jwks;
|
|
3509
|
+
let age;
|
|
3510
|
+
if (jwksMap?.has(as)) {
|
|
3511
|
+
({ jwks, age } = jwksMap.get(as));
|
|
3512
|
+
if (age >= 300) {
|
|
3513
|
+
clearJwksCache(as, options?.[jwksCache]);
|
|
3514
|
+
return getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
3515
|
+
}
|
|
3516
|
+
} else {
|
|
3517
|
+
jwks = await jwksRequest(as, options).then(processJwksResponse);
|
|
3518
|
+
age = 0;
|
|
3519
|
+
setJwksCache(as, jwks, epochTime(), options?.[jwksCache]);
|
|
3520
|
+
}
|
|
3521
|
+
let kty;
|
|
3522
|
+
switch (alg.slice(0, 2)) {
|
|
3523
|
+
case "RS":
|
|
3524
|
+
case "PS":
|
|
3525
|
+
kty = "RSA";
|
|
3526
|
+
break;
|
|
3527
|
+
case "ES":
|
|
3528
|
+
kty = "EC";
|
|
3529
|
+
break;
|
|
3530
|
+
case "Ed":
|
|
3531
|
+
kty = "OKP";
|
|
3532
|
+
break;
|
|
3533
|
+
case "ML":
|
|
3534
|
+
kty = "AKP";
|
|
3535
|
+
break;
|
|
3536
|
+
default:
|
|
3537
|
+
throw new UnsupportedOperationError("unsupported JWS algorithm", { cause: { alg } });
|
|
3538
|
+
}
|
|
3539
|
+
const candidates = jwks.keys.filter((jwk2) => {
|
|
3540
|
+
if (jwk2.kty !== kty) {
|
|
3541
|
+
return false;
|
|
3542
|
+
}
|
|
3543
|
+
if (kid !== undefined && kid !== jwk2.kid) {
|
|
3544
|
+
return false;
|
|
3545
|
+
}
|
|
3546
|
+
if (jwk2.alg !== undefined && alg !== jwk2.alg) {
|
|
3547
|
+
return false;
|
|
3548
|
+
}
|
|
3549
|
+
if (jwk2.use !== undefined && jwk2.use !== "sig") {
|
|
3550
|
+
return false;
|
|
3551
|
+
}
|
|
3552
|
+
if (jwk2.key_ops?.includes("verify") === false) {
|
|
3553
|
+
return false;
|
|
3554
|
+
}
|
|
3555
|
+
switch (true) {
|
|
3556
|
+
case (alg === "ES256" && jwk2.crv !== "P-256"):
|
|
3557
|
+
case (alg === "ES384" && jwk2.crv !== "P-384"):
|
|
3558
|
+
case (alg === "ES512" && jwk2.crv !== "P-521"):
|
|
3559
|
+
case (alg === "Ed25519" && jwk2.crv !== "Ed25519"):
|
|
3560
|
+
case (alg === "EdDSA" && jwk2.crv !== "Ed25519"):
|
|
3561
|
+
return false;
|
|
3562
|
+
}
|
|
3563
|
+
return true;
|
|
3564
|
+
});
|
|
3565
|
+
const { 0: jwk, length } = candidates;
|
|
3566
|
+
if (!length) {
|
|
3567
|
+
if (age >= 60) {
|
|
3568
|
+
clearJwksCache(as, options?.[jwksCache]);
|
|
3569
|
+
return getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
3570
|
+
}
|
|
3571
|
+
throw OPE("error when selecting a JWT verification key, no applicable keys found", KEY_SELECTION, { header, candidates, jwks_uri: new URL(as.jwks_uri) });
|
|
3572
|
+
}
|
|
3573
|
+
if (length !== 1) {
|
|
3574
|
+
throw OPE('error when selecting a JWT verification key, multiple applicable keys found, a "kid" JWT Header Parameter is required', KEY_SELECTION, { header, candidates, jwks_uri: new URL(as.jwks_uri) });
|
|
3575
|
+
}
|
|
3576
|
+
return importJwk(alg, jwk);
|
|
3577
|
+
}
|
|
3578
|
+
var skipSubjectCheck = Symbol();
|
|
3579
|
+
function getContentType(input) {
|
|
3580
|
+
return input.headers.get("content-type")?.split(";")[0];
|
|
3581
|
+
}
|
|
3582
|
+
var idTokenClaims = new WeakMap;
|
|
3583
|
+
var jwtRefs = new WeakMap;
|
|
3584
|
+
function validateAudience(expected, result) {
|
|
3585
|
+
if (Array.isArray(result.claims.aud)) {
|
|
3586
|
+
if (!result.claims.aud.includes(expected)) {
|
|
3587
|
+
throw OPE('unexpected JWT "aud" (audience) claim value', JWT_CLAIM_COMPARISON, {
|
|
3588
|
+
expected,
|
|
3589
|
+
claims: result.claims,
|
|
3590
|
+
claim: "aud"
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
} else if (result.claims.aud !== expected) {
|
|
3594
|
+
throw OPE('unexpected JWT "aud" (audience) claim value', JWT_CLAIM_COMPARISON, {
|
|
3595
|
+
expected,
|
|
3596
|
+
claims: result.claims,
|
|
3597
|
+
claim: "aud"
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
return result;
|
|
3601
|
+
}
|
|
3602
|
+
function validateIssuer(as, result) {
|
|
3603
|
+
const expected = as[_expectedIssuer]?.(result) ?? as.issuer;
|
|
3604
|
+
if (result.claims.iss !== expected) {
|
|
3605
|
+
throw OPE('unexpected JWT "iss" (issuer) claim value', JWT_CLAIM_COMPARISON, {
|
|
3606
|
+
expected,
|
|
3607
|
+
claims: result.claims,
|
|
3608
|
+
claim: "iss"
|
|
3609
|
+
});
|
|
3610
|
+
}
|
|
3611
|
+
return result;
|
|
3612
|
+
}
|
|
3613
|
+
var branded = new WeakSet;
|
|
3614
|
+
var nopkce = Symbol();
|
|
3615
|
+
var jwtClaimNames = {
|
|
3616
|
+
aud: "audience",
|
|
3617
|
+
c_hash: "code hash",
|
|
3618
|
+
client_id: "client id",
|
|
3619
|
+
exp: "expiration time",
|
|
3620
|
+
iat: "issued at",
|
|
3621
|
+
iss: "issuer",
|
|
3622
|
+
jti: "jwt id",
|
|
3623
|
+
nonce: "nonce",
|
|
3624
|
+
s_hash: "state hash",
|
|
3625
|
+
sub: "subject",
|
|
3626
|
+
ath: "access token hash",
|
|
3627
|
+
htm: "http method",
|
|
3628
|
+
htu: "http uri",
|
|
3629
|
+
cnf: "confirmation",
|
|
3630
|
+
auth_time: "authentication time"
|
|
3631
|
+
};
|
|
3632
|
+
function validatePresence(required, result) {
|
|
3633
|
+
for (const claim of required) {
|
|
3634
|
+
if (result.claims[claim] === undefined) {
|
|
3635
|
+
throw OPE(`JWT "${claim}" (${jwtClaimNames[claim]}) claim missing`, INVALID_RESPONSE, {
|
|
3636
|
+
claims: result.claims
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
return result;
|
|
3641
|
+
}
|
|
3642
|
+
var expectNoNonce = Symbol();
|
|
3643
|
+
var skipAuthTimeCheck = Symbol();
|
|
3644
|
+
var UNSUPPORTED_OPERATION = "OAUTH_UNSUPPORTED_OPERATION";
|
|
3645
|
+
var PARSE_ERROR = "OAUTH_PARSE_ERROR";
|
|
3646
|
+
var INVALID_RESPONSE = "OAUTH_INVALID_RESPONSE";
|
|
3647
|
+
var INVALID_REQUEST = "OAUTH_INVALID_REQUEST";
|
|
3648
|
+
var RESPONSE_IS_NOT_JSON = "OAUTH_RESPONSE_IS_NOT_JSON";
|
|
3649
|
+
var RESPONSE_IS_NOT_CONFORM = "OAUTH_RESPONSE_IS_NOT_CONFORM";
|
|
3650
|
+
var HTTP_REQUEST_FORBIDDEN = "OAUTH_HTTP_REQUEST_FORBIDDEN";
|
|
3651
|
+
var REQUEST_PROTOCOL_FORBIDDEN = "OAUTH_REQUEST_PROTOCOL_FORBIDDEN";
|
|
3652
|
+
var JWT_TIMESTAMP_CHECK = "OAUTH_JWT_TIMESTAMP_CHECK_FAILED";
|
|
3653
|
+
var JWT_CLAIM_COMPARISON = "OAUTH_JWT_CLAIM_COMPARISON_FAILED";
|
|
3654
|
+
var JSON_ATTRIBUTE_COMPARISON = "OAUTH_JSON_ATTRIBUTE_COMPARISON_FAILED";
|
|
3655
|
+
var KEY_SELECTION = "OAUTH_KEY_SELECTION_FAILED";
|
|
3656
|
+
var MISSING_SERVER_METADATA = "OAUTH_MISSING_SERVER_METADATA";
|
|
3657
|
+
var INVALID_SERVER_METADATA = "OAUTH_INVALID_SERVER_METADATA";
|
|
3658
|
+
function checkJwtType(expected, result) {
|
|
3659
|
+
if (typeof result.header.typ !== "string" || normalizeTyp(result.header.typ) !== expected) {
|
|
3660
|
+
throw OPE('unexpected JWT "typ" header parameter value', INVALID_RESPONSE, {
|
|
3661
|
+
header: result.header
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
return result;
|
|
3665
|
+
}
|
|
3666
|
+
function assertReadableResponse(response) {
|
|
3667
|
+
if (response.bodyUsed) {
|
|
3668
|
+
throw CodedTypeError('"response" body has been used already', ERR_INVALID_ARG_VALUE);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
async function jwksRequest(as, options) {
|
|
3672
|
+
assertAs(as);
|
|
3673
|
+
const url = resolveEndpoint(as, "jwks_uri", false, options?.[allowInsecureRequests] !== true);
|
|
3674
|
+
const headers = prepareHeaders(options?.headers);
|
|
3675
|
+
headers.set("accept", "application/json");
|
|
3676
|
+
headers.append("accept", "application/jwk-set+json");
|
|
3677
|
+
return (options?.[customFetch] || fetch)(url.href, {
|
|
3678
|
+
body: undefined,
|
|
3679
|
+
headers: Object.fromEntries(headers.entries()),
|
|
3680
|
+
method: "GET",
|
|
3681
|
+
redirect: "manual",
|
|
3682
|
+
signal: signal(url, options?.signal)
|
|
3683
|
+
});
|
|
3684
|
+
}
|
|
3685
|
+
async function processJwksResponse(response) {
|
|
3686
|
+
if (!looseInstanceOf(response, Response)) {
|
|
3687
|
+
throw CodedTypeError('"response" must be an instance of Response', ERR_INVALID_ARG_TYPE);
|
|
3688
|
+
}
|
|
3689
|
+
if (response.status !== 200) {
|
|
3690
|
+
throw OPE('"response" is not a conform JSON Web Key Set response (unexpected HTTP status code)', RESPONSE_IS_NOT_CONFORM, response);
|
|
3691
|
+
}
|
|
3692
|
+
assertReadableResponse(response);
|
|
3693
|
+
const json = await getResponseJsonBody(response, (response2) => assertContentTypes(response2, "application/json", "application/jwk-set+json"));
|
|
3694
|
+
if (!Array.isArray(json.keys)) {
|
|
3695
|
+
throw OPE('"response" body "keys" property must be an array', INVALID_RESPONSE, { body: json });
|
|
3696
|
+
}
|
|
3697
|
+
if (!Array.prototype.every.call(json.keys, isJsonObject)) {
|
|
3698
|
+
throw OPE('"response" body "keys" property members must be JWK formatted objects', INVALID_RESPONSE, { body: json });
|
|
3699
|
+
}
|
|
3700
|
+
return json;
|
|
3701
|
+
}
|
|
3702
|
+
function supported(alg) {
|
|
3703
|
+
switch (alg) {
|
|
3704
|
+
case "PS256":
|
|
3705
|
+
case "ES256":
|
|
3706
|
+
case "RS256":
|
|
3707
|
+
case "PS384":
|
|
3708
|
+
case "ES384":
|
|
3709
|
+
case "RS384":
|
|
3710
|
+
case "PS512":
|
|
3711
|
+
case "ES512":
|
|
3712
|
+
case "RS512":
|
|
3713
|
+
case "Ed25519":
|
|
3714
|
+
case "EdDSA":
|
|
3715
|
+
case "ML-DSA-44":
|
|
3716
|
+
case "ML-DSA-65":
|
|
3717
|
+
case "ML-DSA-87":
|
|
3718
|
+
return true;
|
|
3719
|
+
default:
|
|
3720
|
+
return false;
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
function checkSupportedJwsAlg(header) {
|
|
3724
|
+
if (!supported(header.alg)) {
|
|
3725
|
+
throw new UnsupportedOperationError('unsupported JWS "alg" identifier', {
|
|
3726
|
+
cause: { alg: header.alg }
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
function checkRsaKeyAlgorithm(key) {
|
|
3731
|
+
const { algorithm } = key;
|
|
3732
|
+
if (typeof algorithm.modulusLength !== "number" || algorithm.modulusLength < 2048) {
|
|
3733
|
+
throw new UnsupportedOperationError(`unsupported ${algorithm.name} modulusLength`, {
|
|
3734
|
+
cause: key
|
|
3735
|
+
});
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
function ecdsaHashName(key) {
|
|
3739
|
+
const { algorithm } = key;
|
|
3740
|
+
switch (algorithm.namedCurve) {
|
|
3741
|
+
case "P-256":
|
|
3742
|
+
return "SHA-256";
|
|
3743
|
+
case "P-384":
|
|
3744
|
+
return "SHA-384";
|
|
3745
|
+
case "P-521":
|
|
3746
|
+
return "SHA-512";
|
|
3747
|
+
default:
|
|
3748
|
+
throw new UnsupportedOperationError("unsupported ECDSA namedCurve", { cause: key });
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
function keyToSubtle(key) {
|
|
3752
|
+
switch (key.algorithm.name) {
|
|
3753
|
+
case "ECDSA":
|
|
3754
|
+
return {
|
|
3755
|
+
name: key.algorithm.name,
|
|
3756
|
+
hash: ecdsaHashName(key)
|
|
3757
|
+
};
|
|
3758
|
+
case "RSA-PSS": {
|
|
3759
|
+
checkRsaKeyAlgorithm(key);
|
|
3760
|
+
switch (key.algorithm.hash.name) {
|
|
3761
|
+
case "SHA-256":
|
|
3762
|
+
case "SHA-384":
|
|
3763
|
+
case "SHA-512":
|
|
3764
|
+
return {
|
|
3765
|
+
name: key.algorithm.name,
|
|
3766
|
+
saltLength: parseInt(key.algorithm.hash.name.slice(-3), 10) >> 3
|
|
3767
|
+
};
|
|
3768
|
+
default:
|
|
3769
|
+
throw new UnsupportedOperationError("unsupported RSA-PSS hash name", { cause: key });
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
case "RSASSA-PKCS1-v1_5":
|
|
3773
|
+
checkRsaKeyAlgorithm(key);
|
|
3774
|
+
return key.algorithm.name;
|
|
3775
|
+
case "ML-DSA-44":
|
|
3776
|
+
case "ML-DSA-65":
|
|
3777
|
+
case "ML-DSA-87":
|
|
3778
|
+
case "Ed25519":
|
|
3779
|
+
return key.algorithm.name;
|
|
3780
|
+
}
|
|
3781
|
+
throw new UnsupportedOperationError("unsupported CryptoKey algorithm name", { cause: key });
|
|
3782
|
+
}
|
|
3783
|
+
async function validateJwsSignature(protectedHeader, payload, key, signature) {
|
|
3784
|
+
const data = buf(`${protectedHeader}.${payload}`);
|
|
3785
|
+
const algorithm = keyToSubtle(key);
|
|
3786
|
+
const verified = await crypto.subtle.verify(algorithm, key, signature, data);
|
|
3787
|
+
if (!verified) {
|
|
3788
|
+
throw OPE("JWT signature verification failed", INVALID_RESPONSE, {
|
|
3789
|
+
key,
|
|
3790
|
+
data,
|
|
3791
|
+
signature,
|
|
3792
|
+
algorithm
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
async function validateJwt(jws, checkAlg, clockSkew2, clockTolerance2, decryptJwt) {
|
|
3797
|
+
let { 0: protectedHeader, 1: payload, length } = jws.split(".");
|
|
3798
|
+
if (length === 5) {
|
|
3799
|
+
if (decryptJwt !== undefined) {
|
|
3800
|
+
jws = await decryptJwt(jws);
|
|
3801
|
+
({ 0: protectedHeader, 1: payload, length } = jws.split("."));
|
|
3802
|
+
} else {
|
|
3803
|
+
throw new UnsupportedOperationError("JWE decryption is not configured", { cause: jws });
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
if (length !== 3) {
|
|
3807
|
+
throw OPE("Invalid JWT", INVALID_RESPONSE, jws);
|
|
3808
|
+
}
|
|
3809
|
+
let header;
|
|
3810
|
+
try {
|
|
3811
|
+
header = JSON.parse(buf(b64u(protectedHeader)));
|
|
3812
|
+
} catch (cause) {
|
|
3813
|
+
throw OPE("failed to parse JWT Header body as base64url encoded JSON", PARSE_ERROR, cause);
|
|
3814
|
+
}
|
|
3815
|
+
if (!isJsonObject(header)) {
|
|
3816
|
+
throw OPE("JWT Header must be a top level object", INVALID_RESPONSE, jws);
|
|
3817
|
+
}
|
|
3818
|
+
checkAlg(header);
|
|
3819
|
+
if (header.crit !== undefined) {
|
|
3820
|
+
throw new UnsupportedOperationError('no JWT "crit" header parameter extensions are supported', {
|
|
3821
|
+
cause: { header }
|
|
3822
|
+
});
|
|
3823
|
+
}
|
|
3824
|
+
let claims;
|
|
3825
|
+
try {
|
|
3826
|
+
claims = JSON.parse(buf(b64u(payload)));
|
|
3827
|
+
} catch (cause) {
|
|
3828
|
+
throw OPE("failed to parse JWT Payload body as base64url encoded JSON", PARSE_ERROR, cause);
|
|
3829
|
+
}
|
|
3830
|
+
if (!isJsonObject(claims)) {
|
|
3831
|
+
throw OPE("JWT Payload must be a top level object", INVALID_RESPONSE, jws);
|
|
3832
|
+
}
|
|
3833
|
+
const now = epochTime() + clockSkew2;
|
|
3834
|
+
if (claims.exp !== undefined) {
|
|
3835
|
+
if (typeof claims.exp !== "number") {
|
|
3836
|
+
throw OPE('unexpected JWT "exp" (expiration time) claim type', INVALID_RESPONSE, { claims });
|
|
3837
|
+
}
|
|
3838
|
+
if (claims.exp <= now - clockTolerance2) {
|
|
3839
|
+
throw OPE('unexpected JWT "exp" (expiration time) claim value, expiration is past current timestamp', JWT_TIMESTAMP_CHECK, { claims, now, tolerance: clockTolerance2, claim: "exp" });
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
if (claims.iat !== undefined) {
|
|
3843
|
+
if (typeof claims.iat !== "number") {
|
|
3844
|
+
throw OPE('unexpected JWT "iat" (issued at) claim type', INVALID_RESPONSE, { claims });
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
if (claims.iss !== undefined) {
|
|
3848
|
+
if (typeof claims.iss !== "string") {
|
|
3849
|
+
throw OPE('unexpected JWT "iss" (issuer) claim type', INVALID_RESPONSE, { claims });
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
if (claims.nbf !== undefined) {
|
|
3853
|
+
if (typeof claims.nbf !== "number") {
|
|
3854
|
+
throw OPE('unexpected JWT "nbf" (not before) claim type', INVALID_RESPONSE, { claims });
|
|
3855
|
+
}
|
|
3856
|
+
if (claims.nbf > now + clockTolerance2) {
|
|
3857
|
+
throw OPE('unexpected JWT "nbf" (not before) claim value', JWT_TIMESTAMP_CHECK, {
|
|
3858
|
+
claims,
|
|
3859
|
+
now,
|
|
3860
|
+
tolerance: clockTolerance2,
|
|
3861
|
+
claim: "nbf"
|
|
3862
|
+
});
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
if (claims.aud !== undefined) {
|
|
3866
|
+
if (typeof claims.aud !== "string" && !Array.isArray(claims.aud)) {
|
|
3867
|
+
throw OPE('unexpected JWT "aud" (audience) claim type', INVALID_RESPONSE, { claims });
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
return { header, claims, jwt: jws };
|
|
3871
|
+
}
|
|
3872
|
+
function checkSigningAlgorithm(client, issuer, fallback, header) {
|
|
3873
|
+
if (client !== undefined) {
|
|
3874
|
+
if (typeof client === "string" ? header.alg !== client : !client.includes(header.alg)) {
|
|
3875
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
3876
|
+
header,
|
|
3877
|
+
expected: client,
|
|
3878
|
+
reason: "client configuration"
|
|
3879
|
+
});
|
|
3880
|
+
}
|
|
3881
|
+
return;
|
|
3882
|
+
}
|
|
3883
|
+
if (Array.isArray(issuer)) {
|
|
3884
|
+
if (!issuer.includes(header.alg)) {
|
|
3885
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
3886
|
+
header,
|
|
3887
|
+
expected: issuer,
|
|
3888
|
+
reason: "authorization server metadata"
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
return;
|
|
3892
|
+
}
|
|
3893
|
+
if (fallback !== undefined) {
|
|
3894
|
+
if (typeof fallback === "string" ? header.alg !== fallback : typeof fallback === "function" ? !fallback(header.alg) : !fallback.includes(header.alg)) {
|
|
3895
|
+
throw OPE('unexpected JWT "alg" header parameter', INVALID_RESPONSE, {
|
|
3896
|
+
header,
|
|
3897
|
+
expected: fallback,
|
|
3898
|
+
reason: "default value"
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
throw OPE('missing client or server configuration to verify used JWT "alg" header parameter', undefined, { client, issuer, fallback });
|
|
3904
|
+
}
|
|
3905
|
+
var skipStateCheck = Symbol();
|
|
3906
|
+
var expectNoState = Symbol();
|
|
3907
|
+
function algToSubtle(alg) {
|
|
3908
|
+
switch (alg) {
|
|
3909
|
+
case "PS256":
|
|
3910
|
+
case "PS384":
|
|
3911
|
+
case "PS512":
|
|
3912
|
+
return { name: "RSA-PSS", hash: `SHA-${alg.slice(-3)}` };
|
|
3913
|
+
case "RS256":
|
|
3914
|
+
case "RS384":
|
|
3915
|
+
case "RS512":
|
|
3916
|
+
return { name: "RSASSA-PKCS1-v1_5", hash: `SHA-${alg.slice(-3)}` };
|
|
3917
|
+
case "ES256":
|
|
3918
|
+
case "ES384":
|
|
3919
|
+
return { name: "ECDSA", namedCurve: `P-${alg.slice(-3)}` };
|
|
3920
|
+
case "ES512":
|
|
3921
|
+
return { name: "ECDSA", namedCurve: "P-521" };
|
|
3922
|
+
case "EdDSA":
|
|
3923
|
+
return "Ed25519";
|
|
3924
|
+
case "Ed25519":
|
|
3925
|
+
case "ML-DSA-44":
|
|
3926
|
+
case "ML-DSA-65":
|
|
3927
|
+
case "ML-DSA-87":
|
|
3928
|
+
return alg;
|
|
3929
|
+
default:
|
|
3930
|
+
throw new UnsupportedOperationError("unsupported JWS algorithm", { cause: { alg } });
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
async function importJwk(alg, jwk) {
|
|
3934
|
+
const { ext, key_ops, use, ...key } = jwk;
|
|
3935
|
+
return crypto.subtle.importKey("jwk", key, algToSubtle(alg), true, ["verify"]);
|
|
3936
|
+
}
|
|
3937
|
+
function normalizeHtu(htu) {
|
|
3938
|
+
const url = new URL(htu);
|
|
3939
|
+
url.search = "";
|
|
3940
|
+
url.hash = "";
|
|
3941
|
+
return url.href;
|
|
3942
|
+
}
|
|
3943
|
+
async function validateDPoP(request, accessToken, accessTokenClaims, options) {
|
|
3944
|
+
const headerValue = request.headers.get("dpop");
|
|
3945
|
+
if (headerValue === null) {
|
|
3946
|
+
throw OPE("operation indicated DPoP use but the request has no DPoP HTTP Header", INVALID_REQUEST, { headers: request.headers });
|
|
3947
|
+
}
|
|
3948
|
+
if (request.headers.get("authorization")?.toLowerCase().startsWith("dpop ") === false) {
|
|
3949
|
+
throw OPE(`operation indicated DPoP use but the request's Authorization HTTP Header scheme is not DPoP`, INVALID_REQUEST, { headers: request.headers });
|
|
3950
|
+
}
|
|
3951
|
+
if (typeof accessTokenClaims.cnf?.jkt !== "string") {
|
|
3952
|
+
throw OPE("operation indicated DPoP use but the JWT Access Token has no jkt confirmation claim", INVALID_REQUEST, { claims: accessTokenClaims });
|
|
3953
|
+
}
|
|
3954
|
+
const clockSkew2 = getClockSkew(options);
|
|
3955
|
+
const proof = await validateJwt(headerValue, checkSigningAlgorithm.bind(undefined, options?.signingAlgorithms, undefined, supported), clockSkew2, getClockTolerance(options), undefined).then(checkJwtType.bind(undefined, "dpop+jwt")).then(validatePresence.bind(undefined, ["iat", "jti", "ath", "htm", "htu"]));
|
|
3956
|
+
const now = epochTime() + clockSkew2;
|
|
3957
|
+
const diff = Math.abs(now - proof.claims.iat);
|
|
3958
|
+
if (diff > 300) {
|
|
3959
|
+
throw OPE("DPoP Proof iat is not recent enough", JWT_TIMESTAMP_CHECK, {
|
|
3960
|
+
now,
|
|
3961
|
+
claims: proof.claims,
|
|
3962
|
+
claim: "iat"
|
|
3963
|
+
});
|
|
3964
|
+
}
|
|
3965
|
+
if (proof.claims.htm !== request.method) {
|
|
3966
|
+
throw OPE("DPoP Proof htm mismatch", JWT_CLAIM_COMPARISON, {
|
|
3967
|
+
expected: request.method,
|
|
3968
|
+
claims: proof.claims,
|
|
3969
|
+
claim: "htm"
|
|
3970
|
+
});
|
|
3971
|
+
}
|
|
3972
|
+
if (typeof proof.claims.htu !== "string" || normalizeHtu(proof.claims.htu) !== normalizeHtu(request.url)) {
|
|
3973
|
+
throw OPE("DPoP Proof htu mismatch", JWT_CLAIM_COMPARISON, {
|
|
3974
|
+
expected: normalizeHtu(request.url),
|
|
3975
|
+
claims: proof.claims,
|
|
3976
|
+
claim: "htu"
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
{
|
|
3980
|
+
const expected = b64u(await crypto.subtle.digest("SHA-256", buf(accessToken)));
|
|
3981
|
+
if (proof.claims.ath !== expected) {
|
|
3982
|
+
throw OPE("DPoP Proof ath mismatch", JWT_CLAIM_COMPARISON, {
|
|
3983
|
+
expected,
|
|
3984
|
+
claims: proof.claims,
|
|
3985
|
+
claim: "ath"
|
|
3986
|
+
});
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
{
|
|
3990
|
+
const expected = await calculateJwkThumbprint(proof.header.jwk);
|
|
3991
|
+
if (accessTokenClaims.cnf.jkt !== expected) {
|
|
3992
|
+
throw OPE("JWT Access Token confirmation mismatch", JWT_CLAIM_COMPARISON, {
|
|
3993
|
+
expected,
|
|
3994
|
+
claims: accessTokenClaims,
|
|
3995
|
+
claim: "cnf.jkt"
|
|
3996
|
+
});
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = headerValue.split(".");
|
|
4000
|
+
const signature = b64u(encodedSignature);
|
|
4001
|
+
const { jwk, alg } = proof.header;
|
|
4002
|
+
if (!jwk) {
|
|
4003
|
+
throw OPE("DPoP Proof is missing the jwk header parameter", INVALID_REQUEST, {
|
|
4004
|
+
header: proof.header
|
|
4005
|
+
});
|
|
4006
|
+
}
|
|
4007
|
+
const key = await importJwk(alg, jwk);
|
|
4008
|
+
if (key.type !== "public") {
|
|
4009
|
+
throw OPE("DPoP Proof jwk header parameter must contain a public key", INVALID_REQUEST, {
|
|
4010
|
+
header: proof.header
|
|
4011
|
+
});
|
|
4012
|
+
}
|
|
4013
|
+
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
4014
|
+
}
|
|
4015
|
+
async function validateJwtAccessToken(as, request, expectedAudience, options) {
|
|
4016
|
+
assertAs(as);
|
|
4017
|
+
if (!looseInstanceOf(request, Request)) {
|
|
4018
|
+
throw CodedTypeError('"request" must be an instance of Request', ERR_INVALID_ARG_TYPE);
|
|
4019
|
+
}
|
|
4020
|
+
assertString(expectedAudience, '"expectedAudience"');
|
|
4021
|
+
const authorization = request.headers.get("authorization");
|
|
4022
|
+
if (authorization === null) {
|
|
4023
|
+
throw OPE('"request" is missing an Authorization HTTP Header', INVALID_REQUEST, {
|
|
4024
|
+
headers: request.headers
|
|
4025
|
+
});
|
|
4026
|
+
}
|
|
4027
|
+
let { 0: scheme, 1: accessToken, length } = authorization.split(" ");
|
|
4028
|
+
scheme = scheme.toLowerCase();
|
|
4029
|
+
switch (scheme) {
|
|
4030
|
+
case "dpop":
|
|
4031
|
+
case "bearer":
|
|
4032
|
+
break;
|
|
4033
|
+
default:
|
|
4034
|
+
throw new UnsupportedOperationError("unsupported Authorization HTTP Header scheme", {
|
|
4035
|
+
cause: { headers: request.headers }
|
|
4036
|
+
});
|
|
4037
|
+
}
|
|
4038
|
+
if (length !== 2) {
|
|
4039
|
+
throw OPE("invalid Authorization HTTP Header format", INVALID_REQUEST, {
|
|
4040
|
+
headers: request.headers
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
const requiredClaims = [
|
|
4044
|
+
"iss",
|
|
4045
|
+
"exp",
|
|
4046
|
+
"aud",
|
|
4047
|
+
"sub",
|
|
4048
|
+
"iat",
|
|
4049
|
+
"jti",
|
|
4050
|
+
"client_id"
|
|
4051
|
+
];
|
|
4052
|
+
if (options?.requireDPoP || scheme === "dpop" || request.headers.has("dpop")) {
|
|
4053
|
+
requiredClaims.push("cnf");
|
|
4054
|
+
}
|
|
4055
|
+
const { claims, header } = await validateJwt(accessToken, checkSigningAlgorithm.bind(undefined, options?.signingAlgorithms, undefined, supported), getClockSkew(options), getClockTolerance(options), undefined).then(checkJwtType.bind(undefined, "at+jwt")).then(validatePresence.bind(undefined, requiredClaims)).then(validateIssuer.bind(undefined, as)).then(validateAudience.bind(undefined, expectedAudience)).catch(reassignRSCode);
|
|
4056
|
+
for (const claim of ["client_id", "jti", "sub"]) {
|
|
4057
|
+
if (typeof claims[claim] !== "string") {
|
|
4058
|
+
throw OPE(`unexpected JWT "${claim}" claim type`, INVALID_REQUEST, { claims });
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
if ("cnf" in claims) {
|
|
4062
|
+
if (!isJsonObject(claims.cnf)) {
|
|
4063
|
+
throw OPE('unexpected JWT "cnf" (confirmation) claim value', INVALID_REQUEST, { claims });
|
|
4064
|
+
}
|
|
4065
|
+
const { 0: cnf, length: length2 } = Object.keys(claims.cnf);
|
|
4066
|
+
if (length2) {
|
|
4067
|
+
if (length2 !== 1) {
|
|
4068
|
+
throw new UnsupportedOperationError("multiple confirmation claims are not supported", {
|
|
4069
|
+
cause: { claims }
|
|
4070
|
+
});
|
|
4071
|
+
}
|
|
4072
|
+
if (cnf !== "jkt") {
|
|
4073
|
+
throw new UnsupportedOperationError("unsupported JWT Confirmation method", {
|
|
4074
|
+
cause: { claims }
|
|
4075
|
+
});
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
const { 0: protectedHeader, 1: payload, 2: encodedSignature } = accessToken.split(".");
|
|
4080
|
+
const signature = b64u(encodedSignature);
|
|
4081
|
+
const key = await getPublicSigKeyFromIssuerJwksUri(as, options, header);
|
|
4082
|
+
await validateJwsSignature(protectedHeader, payload, key, signature);
|
|
4083
|
+
if (options?.requireDPoP || scheme === "dpop" || claims.cnf?.jkt !== undefined || request.headers.has("dpop")) {
|
|
4084
|
+
await validateDPoP(request, accessToken, claims, options).catch(reassignRSCode);
|
|
4085
|
+
}
|
|
4086
|
+
return claims;
|
|
4087
|
+
}
|
|
4088
|
+
function reassignRSCode(err) {
|
|
4089
|
+
if (err instanceof OperationProcessingError && err?.code === INVALID_REQUEST) {
|
|
4090
|
+
err.code = INVALID_RESPONSE;
|
|
4091
|
+
}
|
|
4092
|
+
throw err;
|
|
4093
|
+
}
|
|
4094
|
+
async function getResponseJsonBody(response, check = assertApplicationJson) {
|
|
4095
|
+
let json;
|
|
4096
|
+
try {
|
|
4097
|
+
json = await response.json();
|
|
4098
|
+
} catch (cause) {
|
|
4099
|
+
check(response);
|
|
4100
|
+
throw OPE('failed to parse "response" body as JSON', PARSE_ERROR, cause);
|
|
4101
|
+
}
|
|
4102
|
+
if (!isJsonObject(json)) {
|
|
4103
|
+
throw OPE('"response" body must be a top level object', INVALID_RESPONSE, { body: json });
|
|
4104
|
+
}
|
|
4105
|
+
return json;
|
|
4106
|
+
}
|
|
4107
|
+
var _nodiscoverycheck = Symbol();
|
|
4108
|
+
var _expectedIssuer = Symbol();
|
|
4109
|
+
|
|
4110
|
+
// src/http/jwt.ts
|
|
4111
|
+
function jwtAuthenticate(options) {
|
|
4112
|
+
const principalClaim = options.principalClaim ?? "sub";
|
|
4113
|
+
const domain = options.domain ?? "jwt";
|
|
4114
|
+
const audience = options.audience;
|
|
4115
|
+
let asPromise = null;
|
|
4116
|
+
async function getAuthorizationServer() {
|
|
4117
|
+
if (options.jwksUri) {
|
|
4118
|
+
return {
|
|
4119
|
+
issuer: options.issuer,
|
|
4120
|
+
jwks_uri: options.jwksUri
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
const issuerUrl = new URL(options.issuer);
|
|
4124
|
+
const response = await discoveryRequest(issuerUrl);
|
|
4125
|
+
return processDiscoveryResponse(issuerUrl, response);
|
|
4126
|
+
}
|
|
4127
|
+
return async function authenticate(request) {
|
|
4128
|
+
if (!asPromise) {
|
|
4129
|
+
asPromise = getAuthorizationServer();
|
|
4130
|
+
}
|
|
4131
|
+
let as;
|
|
4132
|
+
try {
|
|
4133
|
+
as = await asPromise;
|
|
4134
|
+
} catch (error) {
|
|
4135
|
+
asPromise = null;
|
|
4136
|
+
throw error;
|
|
4137
|
+
}
|
|
4138
|
+
const audiences = Array.isArray(audience) ? audience : [audience];
|
|
4139
|
+
let claims;
|
|
4140
|
+
let lastError;
|
|
4141
|
+
for (const aud of audiences) {
|
|
4142
|
+
try {
|
|
4143
|
+
claims = await validateJwtAccessToken(as, request, aud);
|
|
4144
|
+
break;
|
|
4145
|
+
} catch (error) {
|
|
4146
|
+
lastError = error;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
if (!claims) {
|
|
4150
|
+
throw lastError;
|
|
4151
|
+
}
|
|
4152
|
+
const principal = claims[principalClaim] ?? null;
|
|
4153
|
+
return new AuthContext(domain, true, principal, claims);
|
|
4154
|
+
};
|
|
4155
|
+
}
|
|
4156
|
+
// src/http/mtls.ts
|
|
4157
|
+
import { createHash, X509Certificate } from "node:crypto";
|
|
4158
|
+
function splitRespectingQuotes(text, delimiter) {
|
|
4159
|
+
const parts = [];
|
|
4160
|
+
let current = [];
|
|
4161
|
+
let inQuotes = false;
|
|
4162
|
+
let i = 0;
|
|
4163
|
+
while (i < text.length) {
|
|
4164
|
+
const ch = text[i];
|
|
4165
|
+
if (ch === '"') {
|
|
4166
|
+
inQuotes = !inQuotes;
|
|
4167
|
+
current.push(ch);
|
|
4168
|
+
} else if (ch === "\\" && inQuotes && i + 1 < text.length) {
|
|
4169
|
+
current.push(ch);
|
|
4170
|
+
current.push(text[i + 1]);
|
|
4171
|
+
i++;
|
|
4172
|
+
} else if (ch === delimiter && !inQuotes) {
|
|
4173
|
+
parts.push(current.join(""));
|
|
4174
|
+
current = [];
|
|
4175
|
+
} else {
|
|
4176
|
+
current.push(ch);
|
|
4177
|
+
}
|
|
4178
|
+
i++;
|
|
4179
|
+
}
|
|
4180
|
+
parts.push(current.join(""));
|
|
4181
|
+
return parts;
|
|
4182
|
+
}
|
|
4183
|
+
function unescapeQuoted(text) {
|
|
4184
|
+
return text.replace(/\\(.)/g, "$1");
|
|
4185
|
+
}
|
|
4186
|
+
function extractCn(subject) {
|
|
4187
|
+
for (const part of subject.split(/(?<!\\),/)) {
|
|
4188
|
+
const trimmed = part.trim();
|
|
4189
|
+
if (trimmed.toUpperCase().startsWith("CN=")) {
|
|
4190
|
+
return trimmed.slice(3);
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
return "";
|
|
4194
|
+
}
|
|
4195
|
+
function parseXfcc(headerValue) {
|
|
4196
|
+
const elements = [];
|
|
4197
|
+
for (const rawElement of splitRespectingQuotes(headerValue, ",")) {
|
|
4198
|
+
const trimmed = rawElement.trim();
|
|
4199
|
+
if (!trimmed)
|
|
4200
|
+
continue;
|
|
4201
|
+
const pairs = splitRespectingQuotes(trimmed, ";");
|
|
4202
|
+
const fields = {};
|
|
4203
|
+
for (const pair of pairs) {
|
|
4204
|
+
const p = pair.trim();
|
|
4205
|
+
if (!p)
|
|
4206
|
+
continue;
|
|
4207
|
+
const eqIdx = p.indexOf("=");
|
|
4208
|
+
if (eqIdx < 0)
|
|
4209
|
+
continue;
|
|
4210
|
+
const key = p.slice(0, eqIdx).trim().toLowerCase();
|
|
4211
|
+
let value = p.slice(eqIdx + 1).trim();
|
|
4212
|
+
if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') {
|
|
4213
|
+
value = unescapeQuoted(value.slice(1, -1));
|
|
4214
|
+
}
|
|
4215
|
+
if (key === "cert" || key === "uri" || key === "by") {
|
|
4216
|
+
value = decodeURIComponent(value);
|
|
4217
|
+
}
|
|
4218
|
+
if (key === "dns") {
|
|
4219
|
+
const existing = fields.dns;
|
|
4220
|
+
if (Array.isArray(existing)) {
|
|
4221
|
+
existing.push(value);
|
|
4222
|
+
} else {
|
|
4223
|
+
fields.dns = [value];
|
|
4224
|
+
}
|
|
4225
|
+
} else {
|
|
4226
|
+
fields[key] = value;
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
const dns = Array.isArray(fields.dns) ? fields.dns : [];
|
|
4230
|
+
elements.push({
|
|
4231
|
+
hash: typeof fields.hash === "string" ? fields.hash : null,
|
|
4232
|
+
cert: typeof fields.cert === "string" ? fields.cert : null,
|
|
4233
|
+
subject: typeof fields.subject === "string" ? fields.subject : null,
|
|
4234
|
+
uri: typeof fields.uri === "string" ? fields.uri : null,
|
|
4235
|
+
dns,
|
|
4236
|
+
by: typeof fields.by === "string" ? fields.by : null
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
return elements;
|
|
4240
|
+
}
|
|
4241
|
+
function mtlsAuthenticateXfcc(options) {
|
|
4242
|
+
const validate = options?.validate;
|
|
4243
|
+
const domain = options?.domain ?? "mtls";
|
|
4244
|
+
const selectElement = options?.selectElement ?? "first";
|
|
4245
|
+
return async function authenticate(request) {
|
|
4246
|
+
const headerValue = request.headers.get("x-forwarded-client-cert");
|
|
4247
|
+
if (!headerValue) {
|
|
4248
|
+
throw new Error("Missing x-forwarded-client-cert header");
|
|
4249
|
+
}
|
|
4250
|
+
const elements = parseXfcc(headerValue);
|
|
4251
|
+
if (elements.length === 0) {
|
|
4252
|
+
throw new Error("Empty x-forwarded-client-cert header");
|
|
4253
|
+
}
|
|
4254
|
+
const element = selectElement === "first" ? elements[0] : elements[elements.length - 1];
|
|
4255
|
+
if (validate) {
|
|
4256
|
+
return validate(element);
|
|
4257
|
+
}
|
|
4258
|
+
const principal = element.subject ? extractCn(element.subject) : "";
|
|
4259
|
+
const claims = {};
|
|
4260
|
+
if (element.hash)
|
|
4261
|
+
claims.hash = element.hash;
|
|
4262
|
+
if (element.subject)
|
|
4263
|
+
claims.subject = element.subject;
|
|
4264
|
+
if (element.uri)
|
|
4265
|
+
claims.uri = element.uri;
|
|
4266
|
+
if (element.dns.length > 0)
|
|
4267
|
+
claims.dns = [...element.dns];
|
|
4268
|
+
if (element.by)
|
|
4269
|
+
claims.by = element.by;
|
|
4270
|
+
return new AuthContext(domain, true, principal, claims);
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
function parseCertFromHeader(request, header) {
|
|
4274
|
+
const raw = request.headers.get(header);
|
|
4275
|
+
if (!raw) {
|
|
4276
|
+
throw new Error(`Missing ${header} header`);
|
|
4277
|
+
}
|
|
4278
|
+
const pemStr = decodeURIComponent(raw);
|
|
4279
|
+
if (!pemStr.startsWith("-----BEGIN CERTIFICATE-----")) {
|
|
4280
|
+
throw new Error("Header value is not a PEM certificate");
|
|
4281
|
+
}
|
|
4282
|
+
try {
|
|
4283
|
+
return new X509Certificate(pemStr);
|
|
4284
|
+
} catch (exc) {
|
|
4285
|
+
throw new Error(`Failed to parse PEM certificate: ${exc}`);
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
function checkCertExpiry(cert) {
|
|
4289
|
+
const now = new Date;
|
|
4290
|
+
const notBefore = new Date(cert.validFrom);
|
|
4291
|
+
const notAfter = new Date(cert.validTo);
|
|
4292
|
+
if (now < notBefore) {
|
|
4293
|
+
throw new Error("Certificate is not yet valid");
|
|
4294
|
+
}
|
|
4295
|
+
if (now > notAfter) {
|
|
4296
|
+
throw new Error("Certificate has expired");
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
function mtlsAuthenticate(options) {
|
|
4300
|
+
const { validate, header = "X-SSL-Client-Cert", checkExpiry = false } = options;
|
|
4301
|
+
return async function authenticate(request) {
|
|
4302
|
+
const cert = parseCertFromHeader(request, header);
|
|
4303
|
+
if (checkExpiry) {
|
|
4304
|
+
checkCertExpiry(cert);
|
|
4305
|
+
}
|
|
4306
|
+
return validate(cert);
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
var SUPPORTED_ALGORITHMS = new Set(["sha256", "sha1", "sha384", "sha512"]);
|
|
4310
|
+
function mtlsAuthenticateFingerprint(options) {
|
|
4311
|
+
const { fingerprints, header, algorithm = "sha256", checkExpiry } = options;
|
|
4312
|
+
if (!SUPPORTED_ALGORITHMS.has(algorithm)) {
|
|
4313
|
+
throw new Error(`Unsupported hash algorithm: ${algorithm}`);
|
|
4314
|
+
}
|
|
4315
|
+
const entries = fingerprints instanceof Map ? fingerprints : new Map(Object.entries(fingerprints));
|
|
4316
|
+
function validate(cert) {
|
|
4317
|
+
const fp = createHash(algorithm).update(cert.raw).digest("hex");
|
|
4318
|
+
const ctx = entries.get(fp);
|
|
4319
|
+
if (!ctx) {
|
|
4320
|
+
throw new Error(`Unknown certificate fingerprint: ${fp}`);
|
|
4321
|
+
}
|
|
4322
|
+
return ctx;
|
|
4323
|
+
}
|
|
4324
|
+
return mtlsAuthenticate({ validate, header, checkExpiry });
|
|
4325
|
+
}
|
|
4326
|
+
function mtlsAuthenticateSubject(options) {
|
|
4327
|
+
const { header, domain = "mtls", allowedSubjects = null, checkExpiry } = options ?? {};
|
|
4328
|
+
function validate(cert) {
|
|
4329
|
+
const subjectParts = cert.subject.split(`
|
|
4330
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
4331
|
+
const subjectDn = subjectParts.join(", ");
|
|
4332
|
+
let cn = "";
|
|
4333
|
+
for (const part of subjectParts) {
|
|
4334
|
+
if (part.toUpperCase().startsWith("CN=")) {
|
|
4335
|
+
cn = part.slice(3);
|
|
4336
|
+
break;
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
if (allowedSubjects !== null && !allowedSubjects.has(cn)) {
|
|
4340
|
+
throw new Error(`Subject CN '${cn}' not in allowed subjects`);
|
|
4341
|
+
}
|
|
4342
|
+
const serialHex = BigInt(`0x${cert.serialNumber}`).toString(16);
|
|
4343
|
+
const notValidAfter = new Date(cert.validTo).toISOString();
|
|
4344
|
+
return new AuthContext(domain, true, cn, {
|
|
4345
|
+
subject_dn: subjectDn,
|
|
4346
|
+
serial: serialHex,
|
|
4347
|
+
not_valid_after: notValidAfter
|
|
4348
|
+
});
|
|
4349
|
+
}
|
|
4350
|
+
return mtlsAuthenticate({ validate, header, checkExpiry });
|
|
4351
|
+
}
|
|
4352
|
+
// src/protocol.ts
|
|
4353
|
+
import { Schema as Schema7 } from "@query-farm/apache-arrow";
|
|
4354
|
+
|
|
4355
|
+
// src/schema.ts
|
|
4356
|
+
import {
|
|
4357
|
+
Binary as Binary3,
|
|
4358
|
+
Bool as Bool3,
|
|
4359
|
+
DataType as DataType4,
|
|
4360
|
+
Field as Field4,
|
|
4361
|
+
Float32,
|
|
4362
|
+
Float64 as Float642,
|
|
4363
|
+
Int16,
|
|
4364
|
+
Int32,
|
|
4365
|
+
Int64 as Int642,
|
|
4366
|
+
Schema as Schema6,
|
|
4367
|
+
Utf8 as Utf83
|
|
4368
|
+
} from "@query-farm/apache-arrow";
|
|
4369
|
+
var str = new Utf83;
|
|
4370
|
+
var bytes = new Binary3;
|
|
4371
|
+
var int = new Int642;
|
|
4372
|
+
var int32 = new Int32;
|
|
4373
|
+
var float = new Float642;
|
|
4374
|
+
var float32 = new Float32;
|
|
4375
|
+
var bool = new Bool3;
|
|
4376
|
+
function toSchema(spec) {
|
|
4377
|
+
if (spec instanceof Schema6)
|
|
4378
|
+
return spec;
|
|
4379
|
+
const fields = [];
|
|
4380
|
+
for (const [name, value] of Object.entries(spec)) {
|
|
4381
|
+
if (value instanceof Field4) {
|
|
4382
|
+
fields.push(value);
|
|
4383
|
+
} else if (value instanceof DataType4) {
|
|
4384
|
+
fields.push(new Field4(name, value, false));
|
|
4385
|
+
} else {
|
|
4386
|
+
throw new TypeError(`Invalid schema value for "${name}": expected DataType or Field, got ${typeof value}`);
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
return new Schema6(fields);
|
|
4390
|
+
}
|
|
4391
|
+
var TYPE_MAP = [
|
|
4392
|
+
[Utf83, "str"],
|
|
4393
|
+
[Binary3, "bytes"],
|
|
4394
|
+
[Bool3, "bool"],
|
|
4395
|
+
[Float642, "float"],
|
|
4396
|
+
[Float32, "float"],
|
|
4397
|
+
[Int642, "int"],
|
|
4398
|
+
[Int32, "int"],
|
|
4399
|
+
[Int16, "int"]
|
|
4400
|
+
];
|
|
4401
|
+
function inferParamTypes(spec) {
|
|
4402
|
+
const schema = toSchema(spec);
|
|
4403
|
+
if (schema.fields.length === 0)
|
|
4404
|
+
return;
|
|
4405
|
+
const result = {};
|
|
4406
|
+
for (const field of schema.fields) {
|
|
4407
|
+
let mapped;
|
|
4408
|
+
for (const [ctor, name] of TYPE_MAP) {
|
|
4409
|
+
if (field.type instanceof ctor) {
|
|
4410
|
+
mapped = name;
|
|
4411
|
+
break;
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
if (!mapped)
|
|
4415
|
+
return;
|
|
4416
|
+
result[field.name] = mapped;
|
|
4417
|
+
}
|
|
4418
|
+
return result;
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
// src/protocol.ts
|
|
4422
|
+
var EMPTY_SCHEMA3 = new Schema7([]);
|
|
4423
|
+
|
|
4424
|
+
class Protocol {
|
|
4425
|
+
name;
|
|
4426
|
+
_methods = new Map;
|
|
4427
|
+
constructor(name) {
|
|
4428
|
+
this.name = name;
|
|
4429
|
+
}
|
|
4430
|
+
unary(name, config) {
|
|
4431
|
+
const params = toSchema(config.params);
|
|
4432
|
+
this._methods.set(name, {
|
|
4433
|
+
name,
|
|
4434
|
+
type: "unary" /* UNARY */,
|
|
4435
|
+
paramsSchema: params,
|
|
2234
4436
|
resultSchema: toSchema(config.result),
|
|
2235
4437
|
handler: config.handler,
|
|
2236
4438
|
doc: config.doc,
|
|
@@ -2287,7 +4489,7 @@ import { Schema as Schema9 } from "@query-farm/apache-arrow";
|
|
|
2287
4489
|
// src/dispatch/stream.ts
|
|
2288
4490
|
import { Schema as Schema8 } from "@query-farm/apache-arrow";
|
|
2289
4491
|
var EMPTY_SCHEMA4 = new Schema8([]);
|
|
2290
|
-
async function dispatchStream(method, params, writer, reader, serverId, requestId) {
|
|
4492
|
+
async function dispatchStream(method, params, writer, reader, serverId, requestId, externalConfig) {
|
|
2291
4493
|
const isProducer = !!method.producerFn;
|
|
2292
4494
|
let state;
|
|
2293
4495
|
try {
|
|
@@ -2341,8 +4543,11 @@ async function dispatchStream(method, params, writer, reader, serverId, requestI
|
|
|
2341
4543
|
if (expectedInputSchema && !isProducer && inputBatch.schema !== expectedInputSchema) {
|
|
2342
4544
|
try {
|
|
2343
4545
|
inputBatch = conformBatchToSchema(inputBatch, expectedInputSchema);
|
|
2344
|
-
} catch {
|
|
2345
|
-
|
|
4546
|
+
} catch (e) {
|
|
4547
|
+
if (e instanceof TypeError) {
|
|
4548
|
+
throw e;
|
|
4549
|
+
}
|
|
4550
|
+
console.debug?.(`Schema conformance skipped: ${e instanceof Error ? e.message : e}`);
|
|
2346
4551
|
}
|
|
2347
4552
|
}
|
|
2348
4553
|
const out = new OutputCollector(outputSchema, effectiveProducer, serverId, requestId);
|
|
@@ -2352,7 +4557,11 @@ async function dispatchStream(method, params, writer, reader, serverId, requestI
|
|
|
2352
4557
|
await method.exchangeFn(state, inputBatch, out);
|
|
2353
4558
|
}
|
|
2354
4559
|
for (const emitted of out.batches) {
|
|
2355
|
-
|
|
4560
|
+
let batch = emitted.batch;
|
|
4561
|
+
if (externalConfig) {
|
|
4562
|
+
batch = await maybeExternalizeBatch(batch, externalConfig);
|
|
4563
|
+
}
|
|
4564
|
+
stream.write(batch);
|
|
2356
4565
|
}
|
|
2357
4566
|
if (out.finished) {
|
|
2358
4567
|
break;
|
|
@@ -2368,12 +4577,15 @@ async function dispatchStream(method, params, writer, reader, serverId, requestI
|
|
|
2368
4577
|
}
|
|
2369
4578
|
|
|
2370
4579
|
// src/dispatch/unary.ts
|
|
2371
|
-
async function dispatchUnary(method, params, writer, serverId, requestId) {
|
|
4580
|
+
async function dispatchUnary(method, params, writer, serverId, requestId, externalConfig) {
|
|
2372
4581
|
const schema = method.resultSchema;
|
|
2373
4582
|
const out = new OutputCollector(schema, true, serverId, requestId);
|
|
2374
4583
|
try {
|
|
2375
4584
|
const result = await method.handler(params, out);
|
|
2376
|
-
|
|
4585
|
+
let resultBatch = buildResultBatch(schema, result, serverId, requestId);
|
|
4586
|
+
if (externalConfig) {
|
|
4587
|
+
resultBatch = await maybeExternalizeBatch(resultBatch, externalConfig);
|
|
4588
|
+
}
|
|
2377
4589
|
const batches = [...out.batches.map((b) => b.batch), resultBatch];
|
|
2378
4590
|
writer.writeStream(schema, batches);
|
|
2379
4591
|
} catch (error) {
|
|
@@ -2384,7 +4596,7 @@ async function dispatchUnary(method, params, writer, serverId, requestId) {
|
|
|
2384
4596
|
|
|
2385
4597
|
// src/wire/writer.ts
|
|
2386
4598
|
import { writeSync } from "node:fs";
|
|
2387
|
-
import { RecordBatchStreamWriter as
|
|
4599
|
+
import { RecordBatchStreamWriter as RecordBatchStreamWriter5 } from "@query-farm/apache-arrow";
|
|
2388
4600
|
var STDOUT_FD = 1;
|
|
2389
4601
|
function writeAll(fd, data) {
|
|
2390
4602
|
let offset = 0;
|
|
@@ -2410,7 +4622,7 @@ class IpcStreamWriter {
|
|
|
2410
4622
|
this.fd = fd;
|
|
2411
4623
|
}
|
|
2412
4624
|
writeStream(schema, batches) {
|
|
2413
|
-
const writer = new
|
|
4625
|
+
const writer = new RecordBatchStreamWriter5;
|
|
2414
4626
|
writer.reset(undefined, schema);
|
|
2415
4627
|
for (const batch of batches) {
|
|
2416
4628
|
writer._writeRecordBatch(batch);
|
|
@@ -2430,7 +4642,7 @@ class IncrementalStream {
|
|
|
2430
4642
|
closed = false;
|
|
2431
4643
|
constructor(fd, schema) {
|
|
2432
4644
|
this.fd = fd;
|
|
2433
|
-
this.writer = new
|
|
4645
|
+
this.writer = new RecordBatchStreamWriter5;
|
|
2434
4646
|
this.writer.reset(undefined, schema);
|
|
2435
4647
|
this.drain();
|
|
2436
4648
|
}
|
|
@@ -2464,10 +4676,14 @@ class VgiRpcServer {
|
|
|
2464
4676
|
enableDescribe;
|
|
2465
4677
|
serverId;
|
|
2466
4678
|
describeBatch = null;
|
|
4679
|
+
dispatchHook = null;
|
|
4680
|
+
externalConfig;
|
|
2467
4681
|
constructor(protocol, options) {
|
|
2468
4682
|
this.protocol = protocol;
|
|
2469
4683
|
this.enableDescribe = options?.enableDescribe ?? true;
|
|
2470
4684
|
this.serverId = options?.serverId ?? crypto.randomUUID().replace(/-/g, "").slice(0, 12);
|
|
4685
|
+
this.dispatchHook = options?.dispatchHook ?? null;
|
|
4686
|
+
this.externalConfig = options?.externalLocation;
|
|
2471
4687
|
if (this.enableDescribe) {
|
|
2472
4688
|
const { batch } = buildDescribeBatch(protocol.name, protocol.getMethods(), this.serverId);
|
|
2473
4689
|
this.describeBatch = batch;
|
|
@@ -2537,10 +4753,29 @@ class VgiRpcServer {
|
|
|
2537
4753
|
writer.writeStream(EMPTY_SCHEMA5, [errBatch]);
|
|
2538
4754
|
return;
|
|
2539
4755
|
}
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
4756
|
+
const methodType = method.type === "unary" /* UNARY */ ? "unary" : "stream";
|
|
4757
|
+
const info = { method: methodName, methodType, serverId: this.serverId, requestId };
|
|
4758
|
+
const stats = {
|
|
4759
|
+
inputBatches: 0,
|
|
4760
|
+
outputBatches: 0,
|
|
4761
|
+
inputRows: 0,
|
|
4762
|
+
outputRows: 0,
|
|
4763
|
+
inputBytes: 0,
|
|
4764
|
+
outputBytes: 0
|
|
4765
|
+
};
|
|
4766
|
+
const token = this.dispatchHook?.onDispatchStart(info);
|
|
4767
|
+
let dispatchError;
|
|
4768
|
+
try {
|
|
4769
|
+
if (method.type === "unary" /* UNARY */) {
|
|
4770
|
+
await dispatchUnary(method, params, writer, this.serverId, requestId, this.externalConfig);
|
|
4771
|
+
} else {
|
|
4772
|
+
await dispatchStream(method, params, writer, reader, this.serverId, requestId, this.externalConfig);
|
|
4773
|
+
}
|
|
4774
|
+
} catch (e) {
|
|
4775
|
+
dispatchError = e instanceof Error ? e : new Error(String(e));
|
|
4776
|
+
throw e;
|
|
4777
|
+
} finally {
|
|
4778
|
+
this.dispatchHook?.onDispatchEnd(token, info, stats, dispatchError);
|
|
2544
4779
|
}
|
|
2545
4780
|
}
|
|
2546
4781
|
}
|
|
@@ -2549,19 +4784,42 @@ export {
|
|
|
2549
4784
|
toSchema,
|
|
2550
4785
|
subprocessConnect,
|
|
2551
4786
|
str,
|
|
4787
|
+
resolveExternalLocation,
|
|
2552
4788
|
pipeConnect,
|
|
4789
|
+
parseXfcc,
|
|
4790
|
+
parseUseIdTokenAsBearer,
|
|
4791
|
+
parseResourceMetadataUrl,
|
|
4792
|
+
parseDeviceCodeClientSecret,
|
|
4793
|
+
parseDeviceCodeClientId,
|
|
2553
4794
|
parseDescribeResponse,
|
|
4795
|
+
parseClientSecret,
|
|
4796
|
+
parseClientId,
|
|
4797
|
+
oauthResourceMetadataToJson,
|
|
4798
|
+
mtlsAuthenticateXfcc,
|
|
4799
|
+
mtlsAuthenticateSubject,
|
|
4800
|
+
mtlsAuthenticateFingerprint,
|
|
4801
|
+
mtlsAuthenticate,
|
|
4802
|
+
maybeExternalizeBatch,
|
|
4803
|
+
makeExternalLocationBatch,
|
|
4804
|
+
jwtAuthenticate,
|
|
2554
4805
|
jsonStateSerializer,
|
|
4806
|
+
isExternalLocationBatch,
|
|
2555
4807
|
int32,
|
|
2556
4808
|
int,
|
|
2557
4809
|
inferParamTypes,
|
|
4810
|
+
httpsOnlyValidator,
|
|
4811
|
+
httpOAuthMetadata,
|
|
2558
4812
|
httpIntrospect,
|
|
2559
4813
|
httpConnect,
|
|
2560
4814
|
float32,
|
|
2561
4815
|
float,
|
|
4816
|
+
fetchOAuthMetadata,
|
|
2562
4817
|
createHttpHandler,
|
|
4818
|
+
chainAuthenticate,
|
|
2563
4819
|
bytes,
|
|
2564
4820
|
bool,
|
|
4821
|
+
bearerAuthenticateStatic,
|
|
4822
|
+
bearerAuthenticate,
|
|
2565
4823
|
VgiRpcServer,
|
|
2566
4824
|
VersionError,
|
|
2567
4825
|
STATE_KEY,
|
|
@@ -2583,7 +4841,8 @@ export {
|
|
|
2583
4841
|
DESCRIBE_VERSION_KEY,
|
|
2584
4842
|
DESCRIBE_VERSION,
|
|
2585
4843
|
DESCRIBE_METHOD_NAME,
|
|
4844
|
+
AuthContext,
|
|
2586
4845
|
ARROW_CONTENT_TYPE
|
|
2587
4846
|
};
|
|
2588
4847
|
|
|
2589
|
-
//# debugId=
|
|
4848
|
+
//# debugId=37CD76108224132A64756E2164756E21
|