@velum-labs/routekit-gateway 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +22 -0
- package/dist/auth.js +52 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/provenance.js +10 -0
- package/dist/server.js +26 -12
- package/dist/switching-proxy.d.ts +3 -0
- package/dist/switching-proxy.js +36 -8
- package/dist/test/auth.test.js +21 -1
- package/package.json +5 -5
package/dist/auth.d.ts
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import type { IncomingMessage } from "node:http";
|
|
2
|
+
/** Trusted principal header injected by the switching proxy after auth. */
|
|
3
|
+
export declare const ROUTEKIT_PRINCIPAL_HEADER = "x-routekit-principal";
|
|
4
|
+
export type GatewayPrincipal = {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
role: "owner" | "admin";
|
|
8
|
+
};
|
|
2
9
|
/** Constant-time string equality (length-independent; no timing leaks). */
|
|
3
10
|
export declare function timingSafeStringEqual(a: string, b: string): boolean;
|
|
4
11
|
/** Verify an `Authorization: Bearer <token>` header value. */
|
|
5
12
|
export declare function verifyBearerToken(header: string | undefined, expected: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Extract the presented credential from either `Authorization: Bearer` or
|
|
15
|
+
* `x-api-key`. Returns undefined when neither is present.
|
|
16
|
+
*/
|
|
17
|
+
export declare function presentedCredential(req: IncomingMessage): string | undefined;
|
|
6
18
|
/**
|
|
7
19
|
* The gateway's request-authorization rule: a matching bearer token or a
|
|
8
20
|
* matching `x-api-key` header.
|
|
9
21
|
*/
|
|
10
22
|
export declare function authorizedRequest(req: IncomingMessage, token: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a principal from the request using a token registry. Falls back to
|
|
25
|
+
* a single shared token (legacy) when `legacyToken` matches.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolvePrincipal(req: IncomingMessage, input: {
|
|
28
|
+
resolve?: (presented: string) => GatewayPrincipal | undefined;
|
|
29
|
+
legacyToken?: string;
|
|
30
|
+
}): GatewayPrincipal | undefined;
|
|
31
|
+
/** Parse a trusted principal header value injected by the switching proxy. */
|
|
32
|
+
export declare function parsePrincipalHeader(value: string | undefined): GatewayPrincipal | undefined;
|
package/dist/auth.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* neither content nor length differences are observable through timing.
|
|
5
5
|
*/
|
|
6
6
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
7
|
+
/** Trusted principal header injected by the switching proxy after auth. */
|
|
8
|
+
export const ROUTEKIT_PRINCIPAL_HEADER = "x-routekit-principal";
|
|
7
9
|
/** Constant-time string equality (length-independent; no timing leaks). */
|
|
8
10
|
export function timingSafeStringEqual(a, b) {
|
|
9
11
|
const aDigest = createHash("sha256").update(a, "utf8").digest();
|
|
@@ -16,6 +18,21 @@ export function timingSafeStringEqual(a, b) {
|
|
|
16
18
|
export function verifyBearerToken(header, expected) {
|
|
17
19
|
return typeof header === "string" && timingSafeStringEqual(header, `Bearer ${expected}`);
|
|
18
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Extract the presented credential from either `Authorization: Bearer` or
|
|
23
|
+
* `x-api-key`. Returns undefined when neither is present.
|
|
24
|
+
*/
|
|
25
|
+
export function presentedCredential(req) {
|
|
26
|
+
const authorization = req.headers.authorization;
|
|
27
|
+
if (typeof authorization === "string" && authorization.startsWith("Bearer ")) {
|
|
28
|
+
const token = authorization.slice("Bearer ".length);
|
|
29
|
+
return token.length > 0 ? token : undefined;
|
|
30
|
+
}
|
|
31
|
+
const apiKey = req.headers["x-api-key"];
|
|
32
|
+
if (typeof apiKey === "string" && apiKey.length > 0)
|
|
33
|
+
return apiKey;
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
19
36
|
/**
|
|
20
37
|
* The gateway's request-authorization rule: a matching bearer token or a
|
|
21
38
|
* matching `x-api-key` header.
|
|
@@ -26,3 +43,38 @@ export function authorizedRequest(req, token) {
|
|
|
26
43
|
const apiKey = req.headers["x-api-key"];
|
|
27
44
|
return typeof apiKey === "string" && timingSafeStringEqual(apiKey, token);
|
|
28
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a principal from the request using a token registry. Falls back to
|
|
48
|
+
* a single shared token (legacy) when `legacyToken` matches.
|
|
49
|
+
*/
|
|
50
|
+
export function resolvePrincipal(req, input) {
|
|
51
|
+
const presented = presentedCredential(req);
|
|
52
|
+
if (presented === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
if (input.resolve !== undefined) {
|
|
55
|
+
const principal = input.resolve(presented);
|
|
56
|
+
if (principal !== undefined)
|
|
57
|
+
return principal;
|
|
58
|
+
}
|
|
59
|
+
if (input.legacyToken !== undefined && timingSafeStringEqual(presented, input.legacyToken)) {
|
|
60
|
+
return { id: "default", label: "default", role: "owner" };
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
/** Parse a trusted principal header value injected by the switching proxy. */
|
|
65
|
+
export function parsePrincipalHeader(value) {
|
|
66
|
+
if (value === undefined || value.length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(value);
|
|
70
|
+
if (typeof parsed.id !== "string" ||
|
|
71
|
+
typeof parsed.label !== "string" ||
|
|
72
|
+
(parsed.role !== "owner" && parsed.role !== "admin")) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return { id: parsed.id, label: parsed.label, role: parsed.role };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -35,7 +35,8 @@ export { DEFAULT_MODEL_PRICING, estimateCost, formatUsd, lookupPricing, meterCal
|
|
|
35
35
|
export type { CallCostRecord, ModelPricing, ProviderCostMetadata, TokenUsage } from "./cost.js";
|
|
36
36
|
export { buildModelCallRecord, MODEL_CALL_ID_HEADER, modelCallId, readProducerVersion, resolveProducerGitSha, responseBodyHash, UNKNOWN_GIT_SHA } from "./provenance.js";
|
|
37
37
|
export type { GatewayDialect, ModelCallRecord, ModelGatewayCallContext, ModelGatewayCallResult, ProvenanceSink } from "./provenance.js";
|
|
38
|
-
export { authorizedRequest } from "./auth.js";
|
|
38
|
+
export { authorizedRequest, parsePrincipalHeader, presentedCredential, resolvePrincipal, ROUTEKIT_PRINCIPAL_HEADER } from "./auth.js";
|
|
39
|
+
export type { GatewayPrincipal } from "./auth.js";
|
|
39
40
|
export { errorEvent, finishChunk, noticeChunk, reasoningChunk, sseResponse } from "./sse-wire.js";
|
|
40
41
|
export { ChatStreamAssembler } from "./sse/chat-assembler.js";
|
|
41
42
|
export type { AssembledToolCall } from "./sse/chat-assembler.js";
|
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ export { ACP_PROTOCOL_VERSION, runAcpAgent } from "./acp-agent.js";
|
|
|
18
18
|
export { ACP_REGISTRY_URL, fetchAcpRegistry, installAcpAdapters } from "./acp-registry.js";
|
|
19
19
|
export { DEFAULT_MODEL_PRICING, estimateCost, formatUsd, lookupPricing, meterCall, parseUsage, parseUsageFromSse } from "./cost.js";
|
|
20
20
|
export { buildModelCallRecord, MODEL_CALL_ID_HEADER, modelCallId, readProducerVersion, resolveProducerGitSha, responseBodyHash, UNKNOWN_GIT_SHA } from "./provenance.js";
|
|
21
|
-
export { authorizedRequest } from "./auth.js";
|
|
21
|
+
export { authorizedRequest, parsePrincipalHeader, presentedCredential, resolvePrincipal, ROUTEKIT_PRINCIPAL_HEADER } from "./auth.js";
|
|
22
22
|
export { errorEvent, finishChunk, noticeChunk, reasoningChunk, sseResponse } from "./sse-wire.js";
|
|
23
23
|
export { ChatStreamAssembler } from "./sse/chat-assembler.js";
|
|
24
24
|
export { decodeBufferedSse, SseDecoder, SseParseError } from "./sse/parse.js";
|
package/dist/provenance.js
CHANGED
|
@@ -153,6 +153,16 @@ export function buildModelCallRecord(context, result) {
|
|
|
153
153
|
...(context.attribution.account !== undefined
|
|
154
154
|
? { account: { seat: context.attribution.account.seat } }
|
|
155
155
|
: {}),
|
|
156
|
+
...(context.attribution.principal !== undefined
|
|
157
|
+
? {
|
|
158
|
+
principal: {
|
|
159
|
+
token_id: context.attribution.principal.token_id,
|
|
160
|
+
...(context.attribution.principal.label !== undefined
|
|
161
|
+
? { label: context.attribution.principal.label }
|
|
162
|
+
: {})
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
: {}),
|
|
156
166
|
attempts: context.attribution.attempts,
|
|
157
167
|
retries: context.attribution.retries,
|
|
158
168
|
account_failovers: context.attribution.account_failovers
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createServer } from "node:http";
|
|
|
3
3
|
import { ProviderFailureError } from "@velum-labs/routekit-contracts";
|
|
4
4
|
import { anthropicModelsResponse, handleAnthropicMessages, handleCountTokens, resolveClaudeModelAlias } from "./adapters/anthropic.js";
|
|
5
5
|
import { effectiveModel, isStream, withDefaultModel } from "./adapters/chat.js";
|
|
6
|
-
import { authorizedRequest } from "./auth.js";
|
|
6
|
+
import { authorizedRequest, parsePrincipalHeader, ROUTEKIT_PRINCIPAL_HEADER } from "./auth.js";
|
|
7
7
|
import { cursorModelVariants, isCursorChatBody, resolveCursorModelSelection, translateCursorRequest } from "./adapters/cursor.js";
|
|
8
8
|
import { withReasoningSelection } from "./adapters/openai-chat-wire.js";
|
|
9
9
|
import { handleResponses } from "./adapters/responses.js";
|
|
@@ -331,6 +331,16 @@ export async function startGateway(options) {
|
|
|
331
331
|
return;
|
|
332
332
|
}
|
|
333
333
|
const requestContext = { headers: req.headers };
|
|
334
|
+
const headerPrincipal = parsePrincipalHeader(typeof req.headers[ROUTEKIT_PRINCIPAL_HEADER] === "string"
|
|
335
|
+
? req.headers[ROUTEKIT_PRINCIPAL_HEADER]
|
|
336
|
+
: undefined);
|
|
337
|
+
const requestPrincipal = headerPrincipal === undefined
|
|
338
|
+
? undefined
|
|
339
|
+
: { token_id: headerPrincipal.id, label: headerPrincipal.label };
|
|
340
|
+
const dispatchModelCall = (route) => handleModelCall(res, provenance, {
|
|
341
|
+
...route,
|
|
342
|
+
...(requestPrincipal !== undefined ? { principal: requestPrincipal } : {})
|
|
343
|
+
});
|
|
334
344
|
if (method === "POST" && (path === "/v1/chat/completions" || path === "/chat/completions")) {
|
|
335
345
|
const raw = await readJson(req, res);
|
|
336
346
|
if (raw === NO_BODY)
|
|
@@ -338,7 +348,7 @@ export async function startGateway(options) {
|
|
|
338
348
|
if (rejectInvalid(res, validateChatRequest(raw)))
|
|
339
349
|
return;
|
|
340
350
|
const body = withDefaultModel(raw, backend.defaultModel);
|
|
341
|
-
await
|
|
351
|
+
await dispatchModelCall({
|
|
342
352
|
dialect: "openai-chat",
|
|
343
353
|
body,
|
|
344
354
|
defaultModel: backend.defaultModel,
|
|
@@ -394,7 +404,7 @@ export async function startGateway(options) {
|
|
|
394
404
|
if (rejectInvalid(res, validateChatRequest(translated)))
|
|
395
405
|
return;
|
|
396
406
|
const body = withDefaultModel(translated, backend.defaultModel);
|
|
397
|
-
await
|
|
407
|
+
await dispatchModelCall({
|
|
398
408
|
dialect: "openai-chat",
|
|
399
409
|
body,
|
|
400
410
|
defaultModel: backend.defaultModel,
|
|
@@ -412,7 +422,7 @@ export async function startGateway(options) {
|
|
|
412
422
|
if (raw === NO_BODY)
|
|
413
423
|
return;
|
|
414
424
|
const body = withDefaultModel(raw, backend.defaultModel);
|
|
415
|
-
await
|
|
425
|
+
await dispatchModelCall({
|
|
416
426
|
dialect: "openai-embeddings",
|
|
417
427
|
body,
|
|
418
428
|
defaultModel: backend.defaultModel,
|
|
@@ -478,7 +488,7 @@ export async function startGateway(options) {
|
|
|
478
488
|
if (anthropicRelay !== undefined &&
|
|
479
489
|
route?.provider === "claude-code") {
|
|
480
490
|
const relayBody = withModel(rawBody, route.nativeId);
|
|
481
|
-
await
|
|
491
|
+
await dispatchModelCall({
|
|
482
492
|
dialect: "anthropic-messages",
|
|
483
493
|
body,
|
|
484
494
|
defaultModel: backend.defaultModel,
|
|
@@ -497,7 +507,7 @@ export async function startGateway(options) {
|
|
|
497
507
|
if (anthropicRelay !== undefined &&
|
|
498
508
|
backend.resolveModelRoute === undefined &&
|
|
499
509
|
anthropicRelay.shouldRelay(req.headers, requestedModel, (model) => backend.servesModel?.(model) ?? false)) {
|
|
500
|
-
await
|
|
510
|
+
await dispatchModelCall({
|
|
501
511
|
dialect: "anthropic-messages",
|
|
502
512
|
body,
|
|
503
513
|
defaultModel: backend.defaultModel,
|
|
@@ -515,7 +525,7 @@ export async function startGateway(options) {
|
|
|
515
525
|
});
|
|
516
526
|
return;
|
|
517
527
|
}
|
|
518
|
-
await
|
|
528
|
+
await dispatchModelCall({
|
|
519
529
|
dialect: "anthropic-messages",
|
|
520
530
|
body,
|
|
521
531
|
defaultModel: backend.defaultModel,
|
|
@@ -548,7 +558,7 @@ export async function startGateway(options) {
|
|
|
548
558
|
: resolveNativeModelRoute(backend, "codex", requestedModel);
|
|
549
559
|
}
|
|
550
560
|
catch (error) {
|
|
551
|
-
await
|
|
561
|
+
await dispatchModelCall({
|
|
552
562
|
dialect: "openai-responses",
|
|
553
563
|
body,
|
|
554
564
|
defaultModel: backend.defaultModel,
|
|
@@ -566,7 +576,7 @@ export async function startGateway(options) {
|
|
|
566
576
|
if (codexProviderRelay !== undefined &&
|
|
567
577
|
route?.provider === "codex") {
|
|
568
578
|
const relayBody = withModel(body, route.nativeId);
|
|
569
|
-
await
|
|
579
|
+
await dispatchModelCall({
|
|
570
580
|
dialect: "openai-responses",
|
|
571
581
|
body: canonicalBody,
|
|
572
582
|
defaultModel: backend.defaultModel,
|
|
@@ -587,7 +597,7 @@ export async function startGateway(options) {
|
|
|
587
597
|
(codexProviderRelay === undefined ||
|
|
588
598
|
backend.resolveModelRoute === undefined) &&
|
|
589
599
|
codexRequestRelay.shouldRelay(req.headers, requestedModel, (model) => backend.servesModel?.(model) ?? false)) {
|
|
590
|
-
await
|
|
600
|
+
await dispatchModelCall({
|
|
591
601
|
dialect: "openai-responses",
|
|
592
602
|
body,
|
|
593
603
|
defaultModel: backend.defaultModel,
|
|
@@ -605,7 +615,7 @@ export async function startGateway(options) {
|
|
|
605
615
|
});
|
|
606
616
|
return;
|
|
607
617
|
}
|
|
608
|
-
await
|
|
618
|
+
await dispatchModelCall({
|
|
609
619
|
dialect: "openai-responses",
|
|
610
620
|
body: canonicalBody,
|
|
611
621
|
defaultModel: backend.defaultModel,
|
|
@@ -836,6 +846,7 @@ export function collectAttribution(seed) {
|
|
|
836
846
|
provider: current.provider,
|
|
837
847
|
billing_mode: current.billing_mode,
|
|
838
848
|
...(current.account !== undefined ? { account: current.account } : {}),
|
|
849
|
+
...(current.principal !== undefined ? { principal: current.principal } : {}),
|
|
839
850
|
attempts: Math.max(1, attempts),
|
|
840
851
|
retries,
|
|
841
852
|
account_failovers: accountFailovers
|
|
@@ -845,7 +856,10 @@ export function collectAttribution(seed) {
|
|
|
845
856
|
}
|
|
846
857
|
async function handleModelCall(res, sink, route) {
|
|
847
858
|
const callId = modelCallId();
|
|
848
|
-
const attribution = collectAttribution(
|
|
859
|
+
const attribution = collectAttribution({
|
|
860
|
+
...route.attribution,
|
|
861
|
+
...(route.principal !== undefined ? { principal: route.principal } : {})
|
|
862
|
+
});
|
|
849
863
|
const started = Date.now();
|
|
850
864
|
const startedAt = new Date(started).toISOString();
|
|
851
865
|
const context = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type GatewayPrincipal } from "./auth.js";
|
|
1
2
|
export type SwitchingGatewayProxy = {
|
|
2
3
|
url(): string;
|
|
3
4
|
port(): number;
|
|
@@ -12,4 +13,6 @@ export declare function startSwitchingGatewayProxy(input: {
|
|
|
12
13
|
host?: string;
|
|
13
14
|
port?: number;
|
|
14
15
|
authToken?: string;
|
|
16
|
+
/** Resolve a named data-plane principal; preferred over `authToken` alone. */
|
|
17
|
+
resolveDataPrincipal?: (presented: string) => GatewayPrincipal | undefined;
|
|
15
18
|
}): Promise<SwitchingGatewayProxy>;
|
package/dist/switching-proxy.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createServer } from "node:http";
|
|
11
11
|
import { assertAuthenticatedBind, trimTrailingSlashes } from "@velum-labs/routekit-runtime";
|
|
12
|
-
import { authorizedRequest } from "./auth.js";
|
|
12
|
+
import { ROUTEKIT_PRINCIPAL_HEADER, authorizedRequest, resolvePrincipal } from "./auth.js";
|
|
13
13
|
const MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
14
14
|
const HOP_BY_HOP = new Set([
|
|
15
15
|
"connection",
|
|
@@ -21,11 +21,18 @@ const HOP_BY_HOP = new Set([
|
|
|
21
21
|
"transfer-encoding",
|
|
22
22
|
"upgrade"
|
|
23
23
|
]);
|
|
24
|
-
function requestHeaders(headers) {
|
|
24
|
+
function requestHeaders(headers, principal) {
|
|
25
25
|
const result = new Headers();
|
|
26
26
|
for (const [name, value] of Object.entries(headers)) {
|
|
27
|
-
|
|
27
|
+
const lower = name.toLowerCase();
|
|
28
|
+
if (value === undefined ||
|
|
29
|
+
HOP_BY_HOP.has(lower) ||
|
|
30
|
+
lower === "host" ||
|
|
31
|
+
// Never forward a client-supplied principal; the proxy is the only
|
|
32
|
+
// trusted source of identity for the inner gateway.
|
|
33
|
+
lower === ROUTEKIT_PRINCIPAL_HEADER) {
|
|
28
34
|
continue;
|
|
35
|
+
}
|
|
29
36
|
if (Array.isArray(value)) {
|
|
30
37
|
for (const entry of value)
|
|
31
38
|
result.append(name, entry);
|
|
@@ -34,6 +41,9 @@ function requestHeaders(headers) {
|
|
|
34
41
|
result.set(name, value);
|
|
35
42
|
}
|
|
36
43
|
}
|
|
44
|
+
if (principal !== undefined) {
|
|
45
|
+
result.set(ROUTEKIT_PRINCIPAL_HEADER, JSON.stringify(principal));
|
|
46
|
+
}
|
|
37
47
|
return result;
|
|
38
48
|
}
|
|
39
49
|
async function requestBody(req) {
|
|
@@ -92,6 +102,7 @@ async function pipe(res, upstream) {
|
|
|
92
102
|
export async function startSwitchingGatewayProxy(input) {
|
|
93
103
|
const host = input.host ?? "127.0.0.1";
|
|
94
104
|
assertAuthenticatedBind(host, input.authToken);
|
|
105
|
+
const authEnabled = input.resolveDataPrincipal !== undefined || input.authToken !== undefined;
|
|
95
106
|
let active = {
|
|
96
107
|
url: trimTrailingSlashes(input.target),
|
|
97
108
|
leases: 0,
|
|
@@ -119,11 +130,28 @@ export async function startSwitchingGatewayProxy(input) {
|
|
|
119
130
|
});
|
|
120
131
|
return;
|
|
121
132
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
let principal;
|
|
134
|
+
if (authEnabled) {
|
|
135
|
+
principal = resolvePrincipal(req, {
|
|
136
|
+
...(input.resolveDataPrincipal !== undefined
|
|
137
|
+
? { resolve: input.resolveDataPrincipal }
|
|
138
|
+
: {}),
|
|
139
|
+
...(input.authToken !== undefined ? { legacyToken: input.authToken } : {})
|
|
125
140
|
});
|
|
126
|
-
|
|
141
|
+
if (principal === undefined) {
|
|
142
|
+
// Preserve the single-token path for callers that only pass authToken.
|
|
143
|
+
if (input.resolveDataPrincipal === undefined &&
|
|
144
|
+
input.authToken !== undefined &&
|
|
145
|
+
authorizedRequest(req, input.authToken)) {
|
|
146
|
+
principal = { id: "default", label: "default", role: "owner" };
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
writeJson(res, 401, {
|
|
150
|
+
error: { message: "unauthorized", type: "auth_error" }
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
127
155
|
}
|
|
128
156
|
const selected = active;
|
|
129
157
|
selected.leases += 1;
|
|
@@ -137,7 +165,7 @@ export async function startSwitchingGatewayProxy(input) {
|
|
|
137
165
|
const body = await requestBody(req);
|
|
138
166
|
const upstream = await fetch(`${selected.url}${path}`, {
|
|
139
167
|
method: req.method ?? "GET",
|
|
140
|
-
headers: requestHeaders(req.headers),
|
|
168
|
+
headers: requestHeaders(req.headers, principal),
|
|
141
169
|
...(body !== undefined ? { body } : {}),
|
|
142
170
|
signal: AbortSignal.any([
|
|
143
171
|
aborter.signal,
|
package/dist/test/auth.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { authorizedRequest, timingSafeStringEqual, verifyBearerToken } from "../auth.js";
|
|
3
|
+
import { authorizedRequest, parsePrincipalHeader, resolvePrincipal, timingSafeStringEqual, verifyBearerToken } from "../auth.js";
|
|
4
4
|
function requestWithHeaders(headers) {
|
|
5
5
|
return { headers };
|
|
6
6
|
}
|
|
@@ -23,3 +23,23 @@ test("authorizedRequest accepts bearer header or x-api-key, rejects otherwise",
|
|
|
23
23
|
assert.equal(authorizedRequest(requestWithHeaders({ "x-api-key": "nope" }), "tok"), false);
|
|
24
24
|
assert.equal(authorizedRequest(requestWithHeaders({}), "tok"), false);
|
|
25
25
|
});
|
|
26
|
+
test("resolvePrincipal uses the registry and falls back to a legacy token", () => {
|
|
27
|
+
const principal = resolvePrincipal(requestWithHeaders({ authorization: "Bearer named" }), {
|
|
28
|
+
resolve: (presented) => presented === "named" ? { id: "abc", label: "bob", role: "admin" } : undefined
|
|
29
|
+
});
|
|
30
|
+
assert.deepEqual(principal, { id: "abc", label: "bob", role: "admin" });
|
|
31
|
+
assert.deepEqual(resolvePrincipal(requestWithHeaders({ "x-api-key": "legacy" }), { legacyToken: "legacy" }), { id: "default", label: "default", role: "owner" });
|
|
32
|
+
assert.equal(resolvePrincipal(requestWithHeaders({ authorization: "Bearer nope" }), {
|
|
33
|
+
resolve: () => undefined,
|
|
34
|
+
legacyToken: "legacy"
|
|
35
|
+
}), undefined);
|
|
36
|
+
});
|
|
37
|
+
test("parsePrincipalHeader accepts only well-formed JSON principals", () => {
|
|
38
|
+
assert.deepEqual(parsePrincipalHeader('{"id":"a","label":"bob","role":"admin"}'), {
|
|
39
|
+
id: "a",
|
|
40
|
+
label: "bob",
|
|
41
|
+
role: "admin"
|
|
42
|
+
});
|
|
43
|
+
assert.equal(parsePrincipalHeader('{"id":"a","label":"bob","role":"root"}'), undefined);
|
|
44
|
+
assert.equal(parsePrincipalHeader("not-json"), undefined);
|
|
45
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velum-labs/routekit-gateway",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.13.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/velum-labs/routekit.git",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"zod": "4.4.3",
|
|
30
|
-
"@velum-labs/routekit-contracts": "0.
|
|
31
|
-
"@velum-labs/routekit-registry": "0.
|
|
32
|
-
"@velum-labs/routekit-
|
|
33
|
-
"@velum-labs/routekit-
|
|
30
|
+
"@velum-labs/routekit-contracts": "0.13.0",
|
|
31
|
+
"@velum-labs/routekit-registry": "0.13.0",
|
|
32
|
+
"@velum-labs/routekit-runtime": "0.13.0",
|
|
33
|
+
"@velum-labs/routekit-tracing": "0.13.0"
|
|
34
34
|
},
|
|
35
35
|
"keywords": [
|
|
36
36
|
"routekit",
|