@jeffusion/bungee-core 5.18.0 → 5.19.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/gateway/contracts.d.ts +29 -2
- package/dist/gateway/dispatch-plugin.d.ts +7 -0
- package/dist/gateway/dispatch-targets.d.ts +4 -0
- package/dist/gateway/plugin.js +2 -0
- package/dist/gateway/routing-plugin.d.ts +3 -1
- package/dist/hooks/plugin-hooks.d.ts +3 -1
- package/dist/main.js +146 -34
- package/dist/plugin-extensions.d.ts +2 -0
- package/dist/plugins/ai-transformer/index.js.map +1 -1
- package/dist/plugins/anthropic-request-sanitizer/index.js.map +1 -1
- package/dist/plugins/anthropic-tool-name-transformer/index.js.map +1 -1
- package/dist/plugins/chatgpt-oauth/control.js.map +1 -1
- package/dist/plugins/chatgpt-oauth/index.js.map +1 -1
- package/dist/plugins/codex-router/index.js +179 -2
- package/dist/plugins/codex-router/index.js.map +6 -5
- package/dist/plugins/deepseek-reasoning-fix/index.js.map +1 -1
- package/dist/plugins/key-access/control.js.map +2 -2
- package/dist/plugins/key-access/index.js +14 -10
- package/dist/plugins/key-access/index.js.map +4 -4
- package/dist/plugins/key-access/ingress.js +14 -10
- package/dist/plugins/key-access/ingress.js.map +4 -4
- package/dist/plugins/key-rate-limit/index.js.map +1 -1
- package/dist/plugins/local-accounts/control.js.map +1 -1
- package/dist/plugins/local-accounts/index.js.map +1 -1
- package/dist/plugins/model-mapping/index.js.map +1 -1
- package/dist/plugins/models-dev/index.js.map +1 -1
- package/dist/plugins/openai-messages-to-chat/index.js.map +1 -1
- package/dist/plugins/signature-repair/index.js.map +1 -1
- package/dist/plugins/token-budget/control.js.map +1 -1
- package/dist/plugins/token-budget/index.js.map +1 -1
- package/dist/plugins/token-metering/index.js.map +1 -1
- package/dist/plugins/token-stats/control.js.map +1 -1
- package/dist/plugins/token-stats/index.js.map +1 -1
- package/dist/scoped-plugin-registry.d.ts +1 -0
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Typed extension stages shared by built-in and project plugins. */
|
|
2
|
-
import type { AppConfig, RouteConfig, CorsConfig } from '@jeffusion/bungee-types';
|
|
2
|
+
import type { AppConfig, RouteConfig, CorsConfig, LLMProtocol } from '@jeffusion/bungee-types';
|
|
3
3
|
import type { ExpressionContext } from '../expression-engine';
|
|
4
4
|
import type { EffectiveRouteConfig, RuntimeUpstream, RequestSnapshot } from '../worker/types';
|
|
5
5
|
import type { HandleRequestRuntimeContext } from './request-plugin';
|
|
@@ -24,6 +24,33 @@ export type GatewayBodyRuleArguments = [body: Record<string, any>, rules: Modifi
|
|
|
24
24
|
export type GatewayHeaderRuleArguments = [headers: Headers, rules: ModificationRules['headers'], context: ExpressionContext];
|
|
25
25
|
export type GatewayCorsArguments = [response: Response, cors: CorsConfig | undefined, request: Request];
|
|
26
26
|
export type GatewayQueryRuleArguments = [params: URLSearchParams, rules: ModificationRules['query'], context: ExpressionContext, requestLog: any];
|
|
27
|
+
export interface GatewayDispatchTarget {
|
|
28
|
+
readonly type: 'route' | 'service';
|
|
29
|
+
readonly id: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DispatchRequestInput {
|
|
32
|
+
readonly context: import('../hooks').MutableRequestContext;
|
|
33
|
+
readonly targets: readonly (GatewayDispatchTarget & {
|
|
34
|
+
readonly protocol?: LLMProtocol;
|
|
35
|
+
})[];
|
|
36
|
+
readonly principal?: import('../plugin-extensions').DataPrincipal;
|
|
37
|
+
readonly signal: AbortSignal;
|
|
38
|
+
readonly servingRevision?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface DispatchRequestDecision {
|
|
41
|
+
readonly target: GatewayDispatchTarget;
|
|
42
|
+
}
|
|
43
|
+
export interface GatewayDispatchInput extends Omit<DispatchRequestInput, 'targets'> {
|
|
44
|
+
readonly config: AppConfig;
|
|
45
|
+
readonly entry: RouteConfig;
|
|
46
|
+
}
|
|
47
|
+
export interface GatewayDispatchDecision {
|
|
48
|
+
readonly route: RouteConfig;
|
|
49
|
+
readonly effective: EffectiveRouteConfig;
|
|
50
|
+
readonly context: import('../hooks').MutableRequestContext;
|
|
51
|
+
readonly entryRouteId?: string;
|
|
52
|
+
readonly target?: GatewayDispatchTarget;
|
|
53
|
+
}
|
|
27
54
|
export interface GatewayRouteInput {
|
|
28
55
|
readonly request: Request;
|
|
29
56
|
readonly config: AppConfig;
|
|
@@ -107,4 +134,4 @@ export interface GatewayLogResult {
|
|
|
107
134
|
readonly logger?: RequestLogger;
|
|
108
135
|
}
|
|
109
136
|
/** Provider stages are fixed, required, and registered exactly once at startup. */
|
|
110
|
-
export declare const GATEWAY_PROVIDER_STAGES: readonly ["onGatewayWebSocket", "onGatewayBody", "onGatewayRequest", "onGatewayRoute", "onGatewayAdmission", "onGatewayAdmissionSession", "onGatewayAdmissionPrepare", "onGatewaySelect", "onGatewayFailover", "onGatewayRetry", "onGatewayForward", "onGatewayHeaderRules", "onGatewayCors", "onGatewayBodyRules", "onGatewayQueryRules", "onGatewayResponseRules", "onGatewayLog"];
|
|
137
|
+
export declare const GATEWAY_PROVIDER_STAGES: readonly ["onGatewayWebSocket", "onGatewayBody", "onGatewayRequest", "onGatewayRoute", "onGatewayDispatch", "onGatewayAdmission", "onGatewayAdmissionSession", "onGatewayAdmissionPrepare", "onGatewaySelect", "onGatewayFailover", "onGatewayRetry", "onGatewayForward", "onGatewayHeaderRules", "onGatewayCors", "onGatewayBodyRules", "onGatewayQueryRules", "onGatewayResponseRules", "onGatewayLog"];
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PluginConfigField } from '../plugin.types';
|
|
2
|
+
import type { GatewayDispatchTarget } from './contracts';
|
|
3
|
+
/** Extract references only from the trusted schema, never from arbitrary request JSON. */
|
|
4
|
+
export declare function declaredDispatchTargets(options: Record<string, unknown>, fields: readonly PluginConfigField[]): GatewayDispatchTarget[];
|
package/dist/gateway/plugin.js
CHANGED
|
@@ -325,6 +325,8 @@ function createPluginHooks() {
|
|
|
325
325
|
onGatewayBody: new SyncBailHook("onGatewayBody"),
|
|
326
326
|
onGatewayRequest: new AsyncSeriesBailHook("onGatewayRequest"),
|
|
327
327
|
onGatewayRoute: new AsyncSeriesBailHook("onGatewayRoute"),
|
|
328
|
+
onGatewayDispatch: new AsyncSeriesBailHook("onGatewayDispatch"),
|
|
329
|
+
onDispatchRequest: new AsyncSeriesBailHook("onDispatchRequest"),
|
|
328
330
|
onGatewayAdmission: new AsyncSeriesBailHook("onGatewayAdmission"),
|
|
329
331
|
onGatewayAdmissionSession: new AsyncSeriesBailHook("onGatewayAdmissionSession"),
|
|
330
332
|
onGatewayAdmissionPrepare: new AsyncSeriesBailHook("onGatewayAdmissionPrepare"),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { CorsConfig } from '@jeffusion/bungee-types';
|
|
1
|
+
import type { AppConfig, RouteConfig, CorsConfig } from '@jeffusion/bungee-types';
|
|
2
|
+
import type { EffectiveRouteConfig } from '../worker/types';
|
|
2
3
|
import type { Plugin, PluginHooks } from '@jeffusion/bungee-core/plugin';
|
|
4
|
+
export declare function resolveEffectiveRoute(config: AppConfig, route: RouteConfig): EffectiveRouteConfig;
|
|
3
5
|
export declare function applyCorsHeaders(response: Response, cors: CorsConfig | undefined, request: Request): Response;
|
|
4
6
|
export declare class RoutingPlugin implements Plugin {
|
|
5
7
|
bodyRequirements(): {
|
|
@@ -11,7 +11,7 @@ import type { PluginServices } from '../plugin-services';
|
|
|
11
11
|
import type { PluginStorage, SSEEnvelope } from '../plugin.types';
|
|
12
12
|
import type { InterceptResult, PluginPhase } from '@jeffusion/bungee-types';
|
|
13
13
|
import type { RawResponseResult } from '../plugin-control/contracts';
|
|
14
|
-
import type { GatewayBodyArguments, GatewayRequestArguments, GatewayForwardArguments, GatewayResponseRuleArguments, GatewayBodyRuleArguments, GatewayQueryRuleArguments, GatewayHeaderRuleArguments, GatewayCorsArguments, GatewayRouteInput, GatewayRouteDecision, GatewayAdmissionInput, GatewayAdmissionDecision, GatewayAdmissionSessionInput, GatewayAdmissionPrepareInput, GatewaySelectInput, GatewaySelectDecision, GatewayFailoverInput, GatewayRetryInput, GatewayLogInput, GatewayLogResult } from '../gateway/contracts';
|
|
14
|
+
import type { GatewayBodyArguments, GatewayRequestArguments, GatewayForwardArguments, GatewayResponseRuleArguments, GatewayBodyRuleArguments, GatewayQueryRuleArguments, GatewayHeaderRuleArguments, GatewayCorsArguments, GatewayRouteInput, GatewayRouteDecision, GatewayDispatchInput, GatewayDispatchDecision, DispatchRequestInput, DispatchRequestDecision, GatewayAdmissionInput, GatewayAdmissionDecision, GatewayAdmissionSessionInput, GatewayAdmissionPrepareInput, GatewaySelectInput, GatewaySelectDecision, GatewayFailoverInput, GatewayRetryInput, GatewayLogInput, GatewayLogResult } from '../gateway/contracts';
|
|
15
15
|
import type { WorkerRequestAdmission, PreparedAdmissionAttempt } from '../data-admission/worker';
|
|
16
16
|
import type { FailoverCoordinator } from '../worker/upstream/failover-coordinator';
|
|
17
17
|
import type { ProxyRequestResult } from '../gateway/forward-plugin';
|
|
@@ -233,6 +233,8 @@ export interface PluginHooks {
|
|
|
233
233
|
onGatewayCors: AsyncSeriesBailHook<GatewayCorsArguments, Response>;
|
|
234
234
|
onGatewayRequest: AsyncSeriesBailHook<GatewayRequestArguments, Response>;
|
|
235
235
|
onGatewayRoute: AsyncSeriesBailHook<[GatewayRouteInput], GatewayRouteDecision>;
|
|
236
|
+
onGatewayDispatch: AsyncSeriesBailHook<[GatewayDispatchInput], GatewayDispatchDecision>;
|
|
237
|
+
onDispatchRequest: AsyncSeriesBailHook<[DispatchRequestInput], DispatchRequestDecision>;
|
|
236
238
|
onGatewayAdmission: AsyncSeriesBailHook<[GatewayAdmissionInput], GatewayAdmissionDecision>;
|
|
237
239
|
onGatewayAdmissionSession: AsyncSeriesBailHook<[GatewayAdmissionSessionInput], WorkerRequestAdmission>;
|
|
238
240
|
onGatewayAdmissionPrepare: AsyncSeriesBailHook<[GatewayAdmissionPrepareInput], PreparedAdmissionAttempt[]>;
|
package/dist/main.js
CHANGED
|
@@ -455,14 +455,17 @@ var require_main = __commonJS(function(exports, module) {
|
|
|
455
455
|
function validateGatewayTargets(config, schemas, context) {
|
|
456
456
|
if (!schemas)
|
|
457
457
|
return;
|
|
458
|
-
const routes = new
|
|
459
|
-
const services = new
|
|
458
|
+
const routes = new Map(config.routes.map((route) => [route.id, route]));
|
|
459
|
+
const services = new Map(config.services.map((service) => [service.id, service]));
|
|
460
460
|
const edges = new Map;
|
|
461
461
|
const visit = (value, field, owner, path) => {
|
|
462
462
|
if (field.type === "gateway_target" && value && typeof value === "object") {
|
|
463
463
|
const target = value;
|
|
464
|
-
|
|
464
|
+
const entity = (target.type === "route" ? routes : services).get(target.id);
|
|
465
|
+
if (!entity)
|
|
465
466
|
context.add("invalid_value", path, "Dispatch target does not exist");
|
|
467
|
+
else if (!entity.llm_protocol)
|
|
468
|
+
context.add("invalid_value", path, "Dispatch target requires an explicit receiving protocol");
|
|
466
469
|
const refs = edges.get(owner) ?? new Set;
|
|
467
470
|
refs.add(`${target.type}:${target.id}`);
|
|
468
471
|
edges.set(owner, refs);
|
|
@@ -30765,7 +30768,7 @@ var package_default;
|
|
|
30765
30768
|
var init_package = __esm(() => {
|
|
30766
30769
|
package_default = {
|
|
30767
30770
|
name: "@jeffusion/bungee-core",
|
|
30768
|
-
version: "5.
|
|
30771
|
+
version: "5.19.0",
|
|
30769
30772
|
description: "Bungee reverse proxy core engine",
|
|
30770
30773
|
main: "./dist/main.js",
|
|
30771
30774
|
types: "./dist/main.d.ts",
|
|
@@ -202919,6 +202922,8 @@ function createPluginHooks() {
|
|
|
202919
202922
|
onGatewayBody: new SyncBailHook("onGatewayBody"),
|
|
202920
202923
|
onGatewayRequest: new AsyncSeriesBailHook("onGatewayRequest"),
|
|
202921
202924
|
onGatewayRoute: new AsyncSeriesBailHook("onGatewayRoute"),
|
|
202925
|
+
onGatewayDispatch: new AsyncSeriesBailHook("onGatewayDispatch"),
|
|
202926
|
+
onDispatchRequest: new AsyncSeriesBailHook("onDispatchRequest"),
|
|
202922
202927
|
onGatewayAdmission: new AsyncSeriesBailHook("onGatewayAdmission"),
|
|
202923
202928
|
onGatewayAdmissionSession: new AsyncSeriesBailHook("onGatewayAdmissionSession"),
|
|
202924
202929
|
onGatewayAdmissionPrepare: new AsyncSeriesBailHook("onGatewayAdmissionPrepare"),
|
|
@@ -202990,6 +202995,23 @@ var init_hooks = __esm(() => {
|
|
|
202990
202995
|
init_plugin_hooks();
|
|
202991
202996
|
});
|
|
202992
202997
|
|
|
202998
|
+
// src/gateway/dispatch-targets.ts
|
|
202999
|
+
function declaredDispatchTargets(options, fields) {
|
|
203000
|
+
const result = [];
|
|
203001
|
+
const walk = (value, field) => {
|
|
203002
|
+
if (field.type === "gateway_target" && value && typeof value === "object") {
|
|
203003
|
+
const target = value;
|
|
203004
|
+
if (["route", "service"].includes(target.type) && typeof target.id === "string")
|
|
203005
|
+
result.push({ type: target.type, id: target.id });
|
|
203006
|
+
} else if (field.type === "array" && field.items && Array.isArray(value))
|
|
203007
|
+
value.forEach((item) => walk(item, field.items));
|
|
203008
|
+
else if (field.type === "object" && field.properties && value && typeof value === "object")
|
|
203009
|
+
field.properties.forEach((property) => walk(value[property.name], property));
|
|
203010
|
+
};
|
|
203011
|
+
fields.forEach((field) => walk(options[field.name], field));
|
|
203012
|
+
return result;
|
|
203013
|
+
}
|
|
203014
|
+
|
|
202993
203015
|
// src/plugin-services/rpc-runtime.ts
|
|
202994
203016
|
import { isProxy as isProxy4 } from "util/types";
|
|
202995
203017
|
function positiveLimit2(value, fallback, label) {
|
|
@@ -213763,6 +213785,9 @@ ${errorMessages}`);
|
|
|
213763
213785
|
getGlobalAdmissionHandlers() {
|
|
213764
213786
|
return this.globalInstances.map((instance) => instance.handler);
|
|
213765
213787
|
}
|
|
213788
|
+
getDeclaredDispatchTargets(routeId) {
|
|
213789
|
+
return (this.routeInstances.get(routeId) ?? []).flatMap((instance) => declaredDispatchTargets(instance.config.options ?? {}, this.pluginClasses.get(instance.handler.pluginName)?.configSchema ?? []));
|
|
213790
|
+
}
|
|
213766
213791
|
async initializeFromConfig(config, dependencies) {
|
|
213767
213792
|
this.dependencies = dependencies;
|
|
213768
213793
|
if (dependencies) {
|
|
@@ -213835,8 +213860,9 @@ ${errorMessages}`);
|
|
|
213835
213860
|
logger2.error({ error: result.error, pluginConfig, routeId }, "Failed to create route plugin instance after retries");
|
|
213836
213861
|
}
|
|
213837
213862
|
}
|
|
213838
|
-
|
|
213839
|
-
|
|
213863
|
+
const dispatchServices = this.getDeclaredDispatchTargets(routeId).filter((target) => target.type === "service").map((target) => config.services?.find((service) => service.id === target.id)).filter((service) => !!service);
|
|
213864
|
+
const relatedServices = [...new Map([...config.services?.filter((service) => service.name === route.service) ?? [], ...dispatchServices].map((service) => [service.name, service])).values()];
|
|
213865
|
+
for (const service of relatedServices) {
|
|
213840
213866
|
if (service?.plugins?.length) {
|
|
213841
213867
|
const serviceName = service.name;
|
|
213842
213868
|
for (const pluginConfig of orderBindings(service.plugins)) {
|
|
@@ -213853,7 +213879,15 @@ ${errorMessages}`);
|
|
|
213853
213879
|
}
|
|
213854
213880
|
}
|
|
213855
213881
|
}
|
|
213856
|
-
const endpoints = resolveEffectiveRouteEndpoints(route, config.services);
|
|
213882
|
+
const endpoints = [...resolveEffectiveRouteEndpoints(route, config.services)];
|
|
213883
|
+
const endpointIds = new Set(endpoints.flatMap((endpoint) => endpoint.id ? [endpoint.id] : []));
|
|
213884
|
+
for (const endpoint of dispatchServices.flatMap((service) => service.endpoints)) {
|
|
213885
|
+
if (endpoint.id && endpointIds.has(endpoint.id))
|
|
213886
|
+
continue;
|
|
213887
|
+
if (endpoint.id)
|
|
213888
|
+
endpointIds.add(endpoint.id);
|
|
213889
|
+
endpoints.push(endpoint);
|
|
213890
|
+
}
|
|
213857
213891
|
for (const [upstreamIndex, upstream] of endpoints.entries()) {
|
|
213858
213892
|
const upstreamId = upstream.id || String(upstreamIndex);
|
|
213859
213893
|
for (const pluginConfig of orderBindings(upstream.plugins || [])) {
|
|
@@ -214333,6 +214367,7 @@ var init_contracts = __esm(() => {
|
|
|
214333
214367
|
"onGatewayBody",
|
|
214334
214368
|
"onGatewayRequest",
|
|
214335
214369
|
"onGatewayRoute",
|
|
214370
|
+
"onGatewayDispatch",
|
|
214336
214371
|
"onGatewayAdmission",
|
|
214337
214372
|
"onGatewayAdmissionSession",
|
|
214338
214373
|
"onGatewayAdmissionPrepare",
|
|
@@ -214609,7 +214644,7 @@ class DataAdmissionHost {
|
|
|
214609
214644
|
if (this.options.authorizeWorker(worker) !== "active")
|
|
214610
214645
|
throw new DataAdmissionError(503, "admission_worker_not_active");
|
|
214611
214646
|
for (const requirement of this.routeRequirements)
|
|
214612
|
-
if (requirement.routeIds.includes(target.routeId) && !this.plugins.some((runtime) => runtime.publication.name === requirement.plugin && runtime.plugin.resolveIdentity))
|
|
214647
|
+
if ((requirement.routeIds.includes(target.routeId) || !!target.entryRouteId && requirement.routeIds.includes(target.entryRouteId)) && !this.plugins.some((runtime) => runtime.publication.name === requirement.plugin && runtime.plugin.resolveIdentity))
|
|
214613
214648
|
throw new DataAdmissionError(503, "route_protection_unavailable");
|
|
214614
214649
|
let principal = ANONYMOUS_PRINCIPAL;
|
|
214615
214650
|
for (const runtime of this.plugins)
|
|
@@ -214620,7 +214655,7 @@ class DataAdmissionHost {
|
|
|
214620
214655
|
throw normalizeAdmissionError(error) ?? new DataAdmissionError(503, "route_protection_unavailable");
|
|
214621
214656
|
}
|
|
214622
214657
|
}
|
|
214623
|
-
if (principal.domain === "anonymous" && this.routeRequirements.some((requirement) => requirement.routeIds.includes(target.routeId)))
|
|
214658
|
+
if (principal.domain === "anonymous" && this.routeRequirements.some((requirement) => requirement.routeIds.includes(target.routeId) || !!target.entryRouteId && requirement.routeIds.includes(target.entryRouteId)))
|
|
214624
214659
|
throw new DataAdmissionError(503, "route_protection_unavailable");
|
|
214625
214660
|
const requirements = {};
|
|
214626
214661
|
for (const runtime of this.plugins) {
|
|
@@ -214651,7 +214686,7 @@ class DataAdmissionHost {
|
|
|
214651
214686
|
if (this.options.authorizeWorker(worker) !== "active")
|
|
214652
214687
|
throw new DataAdmissionError(503, "admission_worker_not_active");
|
|
214653
214688
|
for (const requirement of this.routeRequirements) {
|
|
214654
|
-
if (requirement.routeIds.includes(target.routeId) && !this.plugins.some((p) => p.publication.name === requirement.plugin && p.plugin.resolveIdentity)) {
|
|
214689
|
+
if ((requirement.routeIds.includes(target.routeId) || !!target.entryRouteId && requirement.routeIds.includes(target.entryRouteId)) && !this.plugins.some((p) => p.publication.name === requirement.plugin && p.plugin.resolveIdentity)) {
|
|
214655
214690
|
throw new DataAdmissionError(503, "route_protection_unavailable");
|
|
214656
214691
|
}
|
|
214657
214692
|
}
|
|
@@ -214667,10 +214702,10 @@ class DataAdmissionHost {
|
|
|
214667
214702
|
throw new DataAdmissionError(503, "route_protection_unavailable");
|
|
214668
214703
|
}
|
|
214669
214704
|
}
|
|
214670
|
-
if (principal.domain === "anonymous" && this.routeRequirements.some((r) => r.routeIds.includes(target.routeId)))
|
|
214705
|
+
if (principal.domain === "anonymous" && this.routeRequirements.some((r) => r.routeIds.includes(target.routeId) || !!target.entryRouteId && r.routeIds.includes(target.entryRouteId)))
|
|
214671
214706
|
throw new DataAdmissionError(503, "route_protection_unavailable");
|
|
214672
214707
|
const ticket = this.previews.get(target.requestId);
|
|
214673
|
-
if (!preview && expectedVersion !== undefined && (!ticket || ticket.version !== this.version || ticket.expires <= this.now() || ticket.worker !== workerKey(worker) || ticket.target.attemptId !== target.attemptId || ticket.target.url !== target.url || ticket.target.model !== target.model || ticket.target.routeId !== target.routeId || JSON.stringify(ticket.target.principal) !== JSON.stringify(principal) || ticket.target.serviceId !== target.serviceId || ticket.target.upstreamId !== target.upstreamId)) {
|
|
214708
|
+
if (!preview && expectedVersion !== undefined && (!ticket || ticket.version !== this.version || ticket.expires <= this.now() || ticket.worker !== workerKey(worker) || ticket.target.attemptId !== target.attemptId || ticket.target.url !== target.url || ticket.target.model !== target.model || ticket.target.routeId !== target.routeId || ticket.target.entryRouteId !== target.entryRouteId || JSON.stringify(ticket.target.principal) !== JSON.stringify(principal) || ticket.target.serviceId !== target.serviceId || ticket.target.upstreamId !== target.upstreamId)) {
|
|
214674
214709
|
throw new DataAdmissionError(409, "admission_preview_changed");
|
|
214675
214710
|
}
|
|
214676
214711
|
const nowTarget = Object.freeze({ ...target, principal, now: !preview && ticket ? ticket.target.now : this.now() });
|
|
@@ -214716,7 +214751,7 @@ class DataAdmissionHost {
|
|
|
214716
214751
|
plan.runtime.state = plan.state;
|
|
214717
214752
|
}
|
|
214718
214753
|
this.previews.delete(target.requestId);
|
|
214719
|
-
this.grants.set(target.requestId, { routeId: target.routeId, sourcePrincipal: structuredClone(target.principal), worker: workerKey(worker), grant, plugins: this.plugins.map((runtime) => ({ publication: runtime.publication, plugin: runtime.plugin, state: null })), expires: null });
|
|
214754
|
+
this.grants.set(target.requestId, { entryRouteId: target.entryRouteId, routeId: target.routeId, sourcePrincipal: structuredClone(target.principal), worker: workerKey(worker), grant, plugins: this.plugins.map((runtime) => ({ publication: runtime.publication, plugin: runtime.plugin, state: null })), expires: null });
|
|
214720
214755
|
return grant;
|
|
214721
214756
|
}
|
|
214722
214757
|
beforeAttempt(target, worker) {
|
|
@@ -214739,7 +214774,7 @@ class DataAdmissionHost {
|
|
|
214739
214774
|
stored.expires = this.now() + 15000;
|
|
214740
214775
|
}
|
|
214741
214776
|
assertGrant(stored, target, worker) {
|
|
214742
|
-
if (stored.worker !== workerKey(worker) || stored.routeId !== target.routeId || JSON.stringify(stored.sourcePrincipal) !== JSON.stringify(target.principal) || !["active", "retired"].includes(this.options.authorizeWorker(worker)))
|
|
214777
|
+
if (stored.worker !== workerKey(worker) || stored.routeId !== target.routeId || stored.entryRouteId !== target.entryRouteId || JSON.stringify(stored.sourcePrincipal) !== JSON.stringify(target.principal) || !["active", "retired"].includes(this.options.authorizeWorker(worker)))
|
|
214743
214778
|
throw new DataAdmissionError(403, "admission_identity_mismatch");
|
|
214744
214779
|
}
|
|
214745
214780
|
}
|
|
@@ -221778,7 +221813,7 @@ async function executeHttpRequestInternal(req, config, runtimeContextOrSelector,
|
|
|
221778
221813
|
=== Incoming Request ===`);
|
|
221779
221814
|
const routeMatchStart = performance.now();
|
|
221780
221815
|
const routeDecision = requireGatewayResult(await gatewayHooks().onGatewayRoute.promise({ request: req, config }), "onGatewayRoute");
|
|
221781
|
-
|
|
221816
|
+
let route = routeDecision.route;
|
|
221782
221817
|
if (!route) {
|
|
221783
221818
|
logger2.error({ request: requestLog }, `No route found for path: ${url.pathname}`);
|
|
221784
221819
|
success = false;
|
|
@@ -221793,39 +221828,62 @@ async function executeHttpRequestInternal(req, config, runtimeContextOrSelector,
|
|
|
221793
221828
|
originalHeaders[key] = value;
|
|
221794
221829
|
});
|
|
221795
221830
|
reqLogger.setOriginalRequestHeaders(redactRequestHeaders(originalHeaders));
|
|
221796
|
-
routeId = route.path;
|
|
221797
|
-
routeServiceName = route.service;
|
|
221798
|
-
const currentRouteId = route.path;
|
|
221799
|
-
const effectiveRoute = requireGatewayResult(routeDecision.effective, "onGatewayRoute");
|
|
221800
|
-
const endpoints = effectiveRoute.endpoints;
|
|
221801
|
-
const runtimeStateKey = route.service ?? route.path;
|
|
221802
221831
|
if (hasWorkerAdmissionSession() && !trustedIdentity) {
|
|
221803
221832
|
success = false;
|
|
221804
221833
|
responseStatus = 401;
|
|
221805
221834
|
return finalizeLocalResponse(Response.json({ error: "unauthorized" }, { status: 401, headers: { "www-authenticate": "Bearer" } }));
|
|
221806
221835
|
}
|
|
221836
|
+
if (routeDecision.response) {
|
|
221837
|
+
responseStatus = routeDecision.response.status;
|
|
221838
|
+
success = responseStatus < 400;
|
|
221839
|
+
return routeDecision.responseKind === "rule" ? finalizeRootStreamingResponse(routeDecision.response) : finalizeLocalResponse(routeDecision.response);
|
|
221840
|
+
}
|
|
221841
|
+
const snapshotStart = performance.now();
|
|
221842
|
+
const processingDeadline = AbortSignal.timeout(route.timeouts?.request_ms ?? 30000);
|
|
221843
|
+
const processingSignal = AbortSignal.any([req.signal, processingDeadline]);
|
|
221844
|
+
const requestSnapshot = await createRequestSnapshot(req, config.body_parser_limit, processingSignal, originalBody, { requestId, attemptId: requestId, direction: "request", stage: "original-request", version: 0, contentType: req.headers.get("content-type") ?? "", contentEncoding: req.headers.get("content-encoding") ?? "" });
|
|
221845
|
+
requestBodySource = requestSnapshot.bodySource;
|
|
221846
|
+
const entryRoute = route;
|
|
221847
|
+
const dispatchContext = createPhaseContext(requestSnapshot, requestId, entryRoute.path, entryRoute.service);
|
|
221848
|
+
const dispatchHooks = requestRegistry?.getRoutePrecompiledHooks?.(entryRoute.path);
|
|
221849
|
+
if (dispatchHooks?.hooks.onDispatchRequest.hasCallbacks()) {
|
|
221850
|
+
await readSnapshotJson(requestSnapshot, "internal-dispatch", true);
|
|
221851
|
+
dispatchContext.body = structuredClone(requestSnapshot.body);
|
|
221852
|
+
}
|
|
221853
|
+
const dispatch = requireGatewayResult(await gatewayHooks().onGatewayDispatch.promise({
|
|
221854
|
+
config,
|
|
221855
|
+
entry: entryRoute,
|
|
221856
|
+
context: dispatchContext,
|
|
221857
|
+
signal: processingSignal,
|
|
221858
|
+
principal: trustedIdentity?.principal,
|
|
221859
|
+
servingRevision: runtimeContext?.servingRevision
|
|
221860
|
+
}), "onGatewayDispatch");
|
|
221861
|
+
route = dispatch.route;
|
|
221862
|
+
if (dispatch.target) {
|
|
221863
|
+
requestSnapshot.body = dispatch.context.body;
|
|
221864
|
+
reqLogger.addStep("internal_dispatch", { entry: entryRoute.id ?? entryRoute.path, target: dispatch.target, model: dispatch.context.body?.model });
|
|
221865
|
+
}
|
|
221866
|
+
routeId = route.path;
|
|
221867
|
+
routeServiceName = route.service;
|
|
221868
|
+
const currentRouteId = route.path;
|
|
221869
|
+
const effectiveRoute = dispatch.effective;
|
|
221870
|
+
const endpoints = effectiveRoute.endpoints;
|
|
221871
|
+
const runtimeStateKey = route.service ?? route.path;
|
|
221807
221872
|
if (trustedIdentity) {
|
|
221808
221873
|
for (const handler of admissionHandlers)
|
|
221809
221874
|
retainOwner(handler.pluginName);
|
|
221810
221875
|
dataAdmission = requireGatewayResult(await gatewayHooks().onGatewayAdmissionSession.promise({ handlers: admissionHandlers, identity: {
|
|
221811
221876
|
requestId,
|
|
221812
221877
|
principal: trustedIdentity.principal,
|
|
221878
|
+
...dispatch.entryRouteId ? { entryRouteId: dispatch.entryRouteId } : {},
|
|
221813
221879
|
routeId: route.id ?? currentRouteId,
|
|
221814
221880
|
serviceId: route.service_id ?? config.services?.find((service) => service.name === route.service)?.id ?? null
|
|
221815
221881
|
}, invoke: (plugin, method, payload, target) => requestRegistry.invokeAdmissionRpc(plugin, method, payload, target, ownerLeases.get(`${plugin}\x00global`)) }), "onGatewayAdmissionSession");
|
|
221816
221882
|
}
|
|
221817
|
-
if (routeDecision.response) {
|
|
221818
|
-
responseStatus = routeDecision.response.status;
|
|
221819
|
-
success = responseStatus < 400;
|
|
221820
|
-
return routeDecision.responseKind === "rule" ? finalizeRootStreamingResponse(routeDecision.response) : finalizeLocalResponse(routeDecision.response);
|
|
221821
|
-
}
|
|
221822
|
-
const snapshotStart = performance.now();
|
|
221823
|
-
const processingDeadline = AbortSignal.timeout(effectiveRoute.timeouts?.request_ms ?? 30000);
|
|
221824
|
-
const processingSignal = AbortSignal.any([req.signal, processingDeadline]);
|
|
221825
|
-
const requestSnapshot = await createRequestSnapshot(req, config.body_parser_limit, processingSignal, originalBody, { requestId, attemptId: requestId, direction: "request", stage: "original-request", version: 0, contentType: req.headers.get("content-type") ?? "", contentEncoding: req.headers.get("content-encoding") ?? "" });
|
|
221826
|
-
requestBodySource = requestSnapshot.bodySource;
|
|
221827
221883
|
const demandHooks = requestRegistry?.getPrecompiledHooks(currentRouteId, undefined, routeServiceName);
|
|
221828
221884
|
const demandContext = createPhaseContext(requestSnapshot, requestId, currentRouteId, routeServiceName);
|
|
221885
|
+
if (dispatch.target)
|
|
221886
|
+
demandContext.url = new URL(dispatch.context.url);
|
|
221829
221887
|
applyRoutePathRewriteToContext(demandContext, route, requestLog);
|
|
221830
221888
|
const routeDemand = collectPluginBodyRequirements([demandHooks?.routePhase, demandHooks?.servicePhase], {
|
|
221831
221889
|
requestId,
|
|
@@ -221844,7 +221902,7 @@ async function executeHttpRequestInternal(req, config, runtimeContextOrSelector,
|
|
|
221844
221902
|
const routeNeedsBody = analyzeExpressionDependencies(routeRequestDemand, "request").requestBody || analyzeExpressionDependencies(route.response, "response").requestBody;
|
|
221845
221903
|
if (routeDemand.replay || route.retry?.enabled || effectiveRoute.failover?.enabled)
|
|
221846
221904
|
await requestBodySource?.buffer("configured-replay");
|
|
221847
|
-
if (selectionNeedsBody || routeNeedsBody || routeDemand.request !== "none")
|
|
221905
|
+
if (!requestSnapshot.is_json_body && (selectionNeedsBody || routeNeedsBody || routeDemand.request !== "none"))
|
|
221848
221906
|
await readSnapshotJson(requestSnapshot, selectionNeedsBody ? "selection-expression" : routeDemand.request !== "none" ? "plugin-body-demand" : "route-expression", routeDemand.request === "json-write" && !["GET", "HEAD"].includes(req.method));
|
|
221849
221907
|
reqLogger.addStep("request_body_plan", { mode: requestBodySource?.mode, reasons: requestBodySource?.reasons ?? [], replay: requestBodySource?.replayable ?? false, source: "wire" });
|
|
221850
221908
|
reqLogger.addStepWithDuration("request_snapshot_created", performance.now() - snapshotStart, {
|
|
@@ -221863,6 +221921,11 @@ async function executeHttpRequestInternal(req, config, runtimeContextOrSelector,
|
|
|
221863
221921
|
method: req.method,
|
|
221864
221922
|
env: process.env
|
|
221865
221923
|
};
|
|
221924
|
+
if (dispatch.target && entryRoute.id !== route.id) {
|
|
221925
|
+
const entryAdmission = requireGatewayResult(await gatewayHooks().onGatewayAdmission.promise({ route: entryRoute, trustedPeer: getTrustedWorkerPeer(req), context: expressionContext, servingRevision: runtimeContext?.servingRevision, signal: processingSignal }), "onGatewayAdmission");
|
|
221926
|
+
if (!entryAdmission.allowed)
|
|
221927
|
+
throw new DataAdmissionError(entryAdmission.retryAfterMs === undefined ? 503 : 429, "entry_route_rate_limited");
|
|
221928
|
+
}
|
|
221866
221929
|
const rateLimit = requireGatewayResult(await gatewayHooks().onGatewayAdmission.promise({
|
|
221867
221930
|
route,
|
|
221868
221931
|
trustedPeer: getTrustedWorkerPeer(req),
|
|
@@ -221891,7 +221954,9 @@ async function executeHttpRequestInternal(req, config, runtimeContextOrSelector,
|
|
|
221891
221954
|
const scopedRegistry = requestRegistry;
|
|
221892
221955
|
const requestPhaseHooks = scopedRegistry?.getPrecompiledHooks(currentRouteId, undefined, routeServiceName) ?? null;
|
|
221893
221956
|
const phaseContext = createPhaseContext(requestSnapshot, requestId, currentRouteId, routeServiceName);
|
|
221894
|
-
phaseContext.bodyWrite = routeDemand.request === "json-write";
|
|
221957
|
+
phaseContext.bodyWrite = !!dispatch.target || routeDemand.request === "json-write";
|
|
221958
|
+
if (dispatch.target)
|
|
221959
|
+
phaseContext.url = new URL(dispatch.context.url);
|
|
221895
221960
|
phaseContext.bodyRequirements = routeDemand;
|
|
221896
221961
|
applyRoutePathRewriteToContext(phaseContext, route, requestLog);
|
|
221897
221962
|
const routePhaseResponse = await executePreFailoverPhase(requestPhaseHooks?.routePhase, phaseContext, {
|
|
@@ -222942,6 +223007,49 @@ var init_routing_plugin = __esm(() => {
|
|
|
222942
223007
|
init_endpoint_resolver();
|
|
222943
223008
|
});
|
|
222944
223009
|
|
|
223010
|
+
// src/gateway/dispatch-plugin.ts
|
|
223011
|
+
class DispatchPlugin {
|
|
223012
|
+
bodyRequirements() {
|
|
223013
|
+
return { request: "none" };
|
|
223014
|
+
}
|
|
223015
|
+
register(hooks) {
|
|
223016
|
+
hooks.onGatewayDispatch.tapPromise("builtin.dispatch", async ({ config, entry, context, signal, principal, servingRevision }) => {
|
|
223017
|
+
const registry = getScopedPluginRegistry();
|
|
223018
|
+
const references = registry?.getDeclaredDispatchTargets?.(entry.path) ?? [];
|
|
223019
|
+
const targets = references.map((target) => {
|
|
223020
|
+
const entity = (target.type === "route" ? config.routes : config.services ?? []).find((entity) => entity.id === target.id);
|
|
223021
|
+
return { ...target, protocol: entity?.llm_protocol };
|
|
223022
|
+
});
|
|
223023
|
+
const dispatch = await registry?.getRoutePrecompiledHooks?.(entry.path)?.hooks.onDispatchRequest.promise({ context, signal, principal, servingRevision, targets });
|
|
223024
|
+
if (!dispatch)
|
|
223025
|
+
return { route: entry, effective: resolveEffectiveRoute(config, entry), context };
|
|
223026
|
+
if (!targets.some((target) => target.type === dispatch.target.type && target.id === dispatch.target.id))
|
|
223027
|
+
throw new DataAdmissionError(403, "dispatch_target_not_declared");
|
|
223028
|
+
signal.throwIfAborted();
|
|
223029
|
+
let route;
|
|
223030
|
+
if (dispatch.target.type === "route") {
|
|
223031
|
+
const target = config.routes.find((route) => route.id === dispatch.target.id);
|
|
223032
|
+
if (!target || target.id === entry.id || target.direct_response?.enabled || target.redirect?.enabled || target.response_rules?.some((rule) => rule.enabled))
|
|
223033
|
+
throw new DataAdmissionError(422, "dispatch_target_unavailable");
|
|
223034
|
+
route = target;
|
|
223035
|
+
const suffix = context.url.pathname.startsWith(entry.path) ? context.url.pathname.slice(entry.path.length) : "";
|
|
223036
|
+
context.url.pathname = target.path.replace(/\/$/, "") + suffix;
|
|
223037
|
+
} else {
|
|
223038
|
+
const service = config.services?.find((service) => service.id === dispatch.target.id);
|
|
223039
|
+
if (!service)
|
|
223040
|
+
throw new DataAdmissionError(422, "dispatch_target_unavailable");
|
|
223041
|
+
route = { ...entry, service: service.name, service_id: service.id, endpoints: undefined };
|
|
223042
|
+
}
|
|
223043
|
+
return { route, effective: resolveEffectiveRoute(config, route), context, entryRouteId: entry.id ?? entry.path, target: dispatch.target };
|
|
223044
|
+
});
|
|
223045
|
+
}
|
|
223046
|
+
}
|
|
223047
|
+
var init_dispatch_plugin = __esm(() => {
|
|
223048
|
+
init_errors();
|
|
223049
|
+
init_scoped_plugin_registry();
|
|
223050
|
+
init_routing_plugin();
|
|
223051
|
+
});
|
|
223052
|
+
|
|
222945
223053
|
// src/rate-limit/json.ts
|
|
222946
223054
|
class RateLimitJsonScanner {
|
|
222947
223055
|
content;
|
|
@@ -224575,7 +224683,9 @@ function exact3(value, keys) {
|
|
|
224575
224683
|
}
|
|
224576
224684
|
function parseAdmissionTarget(value) {
|
|
224577
224685
|
const target = object(value);
|
|
224578
|
-
exact3(target, ["requestId", "attemptId", "principal", "routeId", "serviceId", "upstreamId", "url", "model", "now"]);
|
|
224686
|
+
exact3(target, ["requestId", "attemptId", "principal", "routeId", "serviceId", "upstreamId", "url", "model", "now", ...Object.hasOwn(target, "entryRouteId") ? ["entryRouteId"] : []]);
|
|
224687
|
+
if (Object.hasOwn(target, "entryRouteId") && (typeof target.entryRouteId !== "string" || !target.entryRouteId || target.entryRouteId.length > 256))
|
|
224688
|
+
throw new Error("invalid RPC entry route");
|
|
224579
224689
|
for (const name of ["requestId", "attemptId", "routeId", "upstreamId", "url"])
|
|
224580
224690
|
if (typeof target[name] !== "string" || !target[name].length || target[name].length > (name === "url" ? 65536 : 256))
|
|
224581
224691
|
throw new Error("invalid RPC target");
|
|
@@ -230573,6 +230683,7 @@ function gatewayBuiltins() {
|
|
|
230573
230683
|
new BodyServicePlugin,
|
|
230574
230684
|
new HttpRequestPlugin,
|
|
230575
230685
|
new RoutingPlugin,
|
|
230686
|
+
new DispatchPlugin,
|
|
230576
230687
|
new AdmissionPlugin,
|
|
230577
230688
|
new SelectionPlugin,
|
|
230578
230689
|
new RetryPlugin,
|
|
@@ -230633,6 +230744,7 @@ var init_runtime = __esm(() => {
|
|
|
230633
230744
|
init_contracts();
|
|
230634
230745
|
init_request_plugin();
|
|
230635
230746
|
init_forward_plugin();
|
|
230747
|
+
init_dispatch_plugin();
|
|
230636
230748
|
init_routing_plugin();
|
|
230637
230749
|
init_admission_plugin();
|
|
230638
230750
|
init_selection_plugin();
|
|
@@ -31,6 +31,8 @@ export interface ManagementProvider {
|
|
|
31
31
|
sessionCookie?(request: Request): string | undefined;
|
|
32
32
|
}
|
|
33
33
|
export interface AdmissionTarget {
|
|
34
|
+
/** Host-pinned entry scope for one internal handoff. */
|
|
35
|
+
readonly entryRouteId?: string;
|
|
34
36
|
readonly requestId: string;
|
|
35
37
|
readonly attemptId: string;
|
|
36
38
|
readonly principal: DataPrincipal;
|