veryfront 0.1.228 → 0.1.230
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/esm/deno.js +1 -1
- package/esm/src/agent/hosted-child-lifecycle.d.ts +41 -0
- package/esm/src/agent/hosted-child-lifecycle.d.ts.map +1 -0
- package/esm/src/agent/hosted-child-lifecycle.js +47 -0
- package/esm/src/agent/hosted-lifecycle.d.ts +41 -0
- package/esm/src/agent/hosted-lifecycle.d.ts.map +1 -0
- package/esm/src/agent/hosted-lifecycle.js +77 -0
- package/esm/src/agent/index.d.ts +2 -0
- package/esm/src/agent/index.d.ts.map +1 -1
- package/esm/src/agent/index.js +2 -0
- package/esm/src/agent/runtime/sse-utils.d.ts.map +1 -1
- package/esm/src/agent/runtime/sse-utils.js +9 -1
- package/esm/src/channels/control-plane.d.ts +21 -0
- package/esm/src/channels/control-plane.d.ts.map +1 -1
- package/esm/src/channels/control-plane.js +48 -0
- package/esm/src/channels/invoke.d.ts +1 -1
- package/esm/src/channels/invoke.d.ts.map +1 -1
- package/esm/src/channels/invoke.js +1 -1
- package/esm/src/server/handlers/preview/hmr.handler.d.ts.map +1 -1
- package/esm/src/server/handlers/preview/hmr.handler.js +21 -9
- package/esm/src/server/runtime-handler/adapter-factory.d.ts +5 -2
- package/esm/src/server/runtime-handler/adapter-factory.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/adapter-factory.js +18 -1
- package/esm/src/server/runtime-handler/index.d.ts.map +1 -1
- package/esm/src/server/runtime-handler/index.js +5 -2
- package/esm/src/server/services/rsc/orchestrators/page-handler.d.ts.map +1 -1
- package/esm/src/server/services/rsc/orchestrators/page-handler.js +22 -1
- package/esm/src/server/utils/proxy-trust.d.ts +33 -0
- package/esm/src/server/utils/proxy-trust.d.ts.map +1 -0
- package/esm/src/server/utils/proxy-trust.js +41 -0
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +1 -1
- package/src/deno.js +1 -1
- package/src/src/agent/hosted-child-lifecycle.ts +121 -0
- package/src/src/agent/hosted-lifecycle.ts +159 -0
- package/src/src/agent/index.ts +15 -0
- package/src/src/agent/runtime/sse-utils.ts +9 -1
- package/src/src/channels/control-plane.ts +52 -0
- package/src/src/channels/invoke.ts +1 -1
- package/src/src/server/handlers/preview/hmr.handler.ts +32 -26
- package/src/src/server/runtime-handler/adapter-factory.ts +23 -3
- package/src/src/server/runtime-handler/index.ts +5 -2
- package/src/src/server/services/rsc/orchestrators/page-handler.ts +23 -1
- package/src/src/server/utils/proxy-trust.ts +56 -0
- package/src/src/utils/version-constant.ts +1 -1
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { buildNonceAttribute } from "../../../../html/html-escape.js";
|
|
2
|
+
/**
|
|
3
|
+
* Serialize a value as a JSON string literal that is safe to embed inside an
|
|
4
|
+
* inline HTML <script>. JSON already escapes quotes, backslashes, and control
|
|
5
|
+
* characters; we additionally escape:
|
|
6
|
+
* - `<` / `>` so `</script>`, `<!--`, and `<script` cannot appear literally,
|
|
7
|
+
* - `&` as defense-in-depth against reparsing contexts (e.g. HTML entity
|
|
8
|
+
* re-decoding in some legacy paths),
|
|
9
|
+
* - U+2028 / U+2029 which are valid JSON but terminate JS string literals
|
|
10
|
+
* in older browsers.
|
|
11
|
+
*
|
|
12
|
+
* See VULN-INJ-1 in the security audit.
|
|
13
|
+
*/
|
|
14
|
+
function jsonForScript(value) {
|
|
15
|
+
return JSON.stringify(value)
|
|
16
|
+
.replace(/</g, "\\u003c")
|
|
17
|
+
.replace(/>/g, "\\u003e")
|
|
18
|
+
.replace(/&/g, "\\u0026")
|
|
19
|
+
.replace(/\u2028/g, "\\u2028")
|
|
20
|
+
.replace(/\u2029/g, "\\u2029");
|
|
21
|
+
}
|
|
2
22
|
export class PageHandler {
|
|
3
23
|
handle(pathname, searchParams, nonce) {
|
|
4
24
|
const html = this.buildHtml(pathname, searchParams, nonce);
|
|
@@ -10,6 +30,7 @@ export class PageHandler {
|
|
|
10
30
|
const queryString = searchParams.toString();
|
|
11
31
|
const renderUrl = `/_veryfront/rsc/render${pathname}${queryString ? `?${queryString}` : ""}`;
|
|
12
32
|
const nonceAttr = buildNonceAttribute(nonce);
|
|
33
|
+
const renderUrlJs = jsonForScript(renderUrl);
|
|
13
34
|
return `<!DOCTYPE html>
|
|
14
35
|
<html lang="en">
|
|
15
36
|
<head>
|
|
@@ -57,7 +78,7 @@ export class PageHandler {
|
|
|
57
78
|
}
|
|
58
79
|
|
|
59
80
|
(async () => {
|
|
60
|
-
const renderUrl =
|
|
81
|
+
const renderUrl = ${renderUrlJs};
|
|
61
82
|
const payload =
|
|
62
83
|
(await fetchPayload(renderUrl)) ??
|
|
63
84
|
(await fetchPayload('/_veryfront/rsc/payload')) ??
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy trust boundary.
|
|
3
|
+
*
|
|
4
|
+
* Forwarded headers such as `x-forwarded-host` and `x-project-path` must only be
|
|
5
|
+
* honoured when the request is known to come from a trusted upstream proxy.
|
|
6
|
+
* Any other treatment lets an attacker reaching the runtime directly spoof the
|
|
7
|
+
* origin host or point project discovery at arbitrary filesystem paths.
|
|
8
|
+
*
|
|
9
|
+
* A request is considered proxy-trusted when either:
|
|
10
|
+
* 1. The operator has opted in via `VERYFRONT_TRUST_FORWARDED_HEADERS=1`
|
|
11
|
+
* (strict "1" match — "true", "yes", whitespace-padded values do NOT count
|
|
12
|
+
* so misconfiguration fails closed); or
|
|
13
|
+
* 2. The request carries a valid `x-veryfront-dispatch-jws` header that
|
|
14
|
+
* cryptographically verifies against the configured control-plane public
|
|
15
|
+
* key and whose `iat`/`exp` claims are within the allowed freshness
|
|
16
|
+
* window. Presence alone is NOT trusted because the proxy does not strip
|
|
17
|
+
* this header from untrusted inbound requests (it has to pass through to
|
|
18
|
+
* the channel-invoke / channel-assistants handlers unchanged), so a
|
|
19
|
+
* direct-access attacker could otherwise set any value and promote
|
|
20
|
+
* forwarded-header spoofing.
|
|
21
|
+
*
|
|
22
|
+
* @module server/utils/proxy-trust
|
|
23
|
+
*/
|
|
24
|
+
export interface ProxyTrustOptions {
|
|
25
|
+
/**
|
|
26
|
+
* PEM-encoded Ed25519 public key used to verify `x-veryfront-dispatch-jws`.
|
|
27
|
+
* When absent, the dispatch-JWS trust signal is disabled (fails closed) and
|
|
28
|
+
* only the operator opt-in env var can unlock proxy trust.
|
|
29
|
+
*/
|
|
30
|
+
publicKeyPem?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function isProxyTrusted(req: Request, options?: ProxyTrustOptions): Promise<boolean>;
|
|
33
|
+
//# sourceMappingURL=proxy-trust.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-trust.d.ts","sourceRoot":"","sources":["../../../../src/src/server/utils/proxy-trust.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAQH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,cAAc,CAClC,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,OAAO,CAAC,CAalB"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy trust boundary.
|
|
3
|
+
*
|
|
4
|
+
* Forwarded headers such as `x-forwarded-host` and `x-project-path` must only be
|
|
5
|
+
* honoured when the request is known to come from a trusted upstream proxy.
|
|
6
|
+
* Any other treatment lets an attacker reaching the runtime directly spoof the
|
|
7
|
+
* origin host or point project discovery at arbitrary filesystem paths.
|
|
8
|
+
*
|
|
9
|
+
* A request is considered proxy-trusted when either:
|
|
10
|
+
* 1. The operator has opted in via `VERYFRONT_TRUST_FORWARDED_HEADERS=1`
|
|
11
|
+
* (strict "1" match — "true", "yes", whitespace-padded values do NOT count
|
|
12
|
+
* so misconfiguration fails closed); or
|
|
13
|
+
* 2. The request carries a valid `x-veryfront-dispatch-jws` header that
|
|
14
|
+
* cryptographically verifies against the configured control-plane public
|
|
15
|
+
* key and whose `iat`/`exp` claims are within the allowed freshness
|
|
16
|
+
* window. Presence alone is NOT trusted because the proxy does not strip
|
|
17
|
+
* this header from untrusted inbound requests (it has to pass through to
|
|
18
|
+
* the channel-invoke / channel-assistants handlers unchanged), so a
|
|
19
|
+
* direct-access attacker could otherwise set any value and promote
|
|
20
|
+
* forwarded-header spoofing.
|
|
21
|
+
*
|
|
22
|
+
* @module server/utils/proxy-trust
|
|
23
|
+
*/
|
|
24
|
+
import { verifyDispatchJwsSignature } from "../../channels/control-plane.js";
|
|
25
|
+
import { getHostEnv } from "../../platform/compat/process.js";
|
|
26
|
+
const DISPATCH_JWS_HEADER = "x-veryfront-dispatch-jws";
|
|
27
|
+
const MAX_DISPATCH_SIGNATURE_AGE_SECONDS = 60;
|
|
28
|
+
export async function isProxyTrusted(req, options = {}) {
|
|
29
|
+
if (getHostEnv("VERYFRONT_TRUST_FORWARDED_HEADERS") === "1")
|
|
30
|
+
return true;
|
|
31
|
+
const jws = req.headers.get(DISPATCH_JWS_HEADER);
|
|
32
|
+
if (!jws)
|
|
33
|
+
return false;
|
|
34
|
+
const { publicKeyPem } = options;
|
|
35
|
+
if (!publicKeyPem)
|
|
36
|
+
return false;
|
|
37
|
+
return verifyDispatchJwsSignature(jws, {
|
|
38
|
+
publicKeyPem,
|
|
39
|
+
maxAgeSeconds: MAX_DISPATCH_SIGNATURE_AGE_SECONDS,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.230";
|
|
2
2
|
//# sourceMappingURL=version-constant.d.ts.map
|
package/package.json
CHANGED
package/src/deno.js
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export interface HostedChildLifecycleTerminalState {
|
|
2
|
+
status: "completed" | "failed" | "cancelled";
|
|
3
|
+
usage?: {
|
|
4
|
+
inputTokens?: number;
|
|
5
|
+
outputTokens?: number;
|
|
6
|
+
totalTokens?: number;
|
|
7
|
+
};
|
|
8
|
+
terminalErrorCode?: string | null;
|
|
9
|
+
terminalErrorMessage?: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HostedChildLifecycleCompletedState
|
|
13
|
+
extends Omit<HostedChildLifecycleTerminalState, "status"> {
|
|
14
|
+
status: "completed";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HostedChildLifecycleAdapter {
|
|
18
|
+
pending?: () => Promise<void> | void;
|
|
19
|
+
running?: () => Promise<void> | void;
|
|
20
|
+
completed?: (
|
|
21
|
+
terminalState: HostedChildLifecycleTerminalState,
|
|
22
|
+
) => Promise<void> | void;
|
|
23
|
+
failed?: (
|
|
24
|
+
terminalState: HostedChildLifecycleTerminalState,
|
|
25
|
+
) => Promise<void> | void;
|
|
26
|
+
cancelled?: (
|
|
27
|
+
terminalState: HostedChildLifecycleTerminalState,
|
|
28
|
+
) => Promise<void> | void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface HostedChildLifecycleErrorState
|
|
32
|
+
extends Omit<HostedChildLifecycleTerminalState, "status"> {
|
|
33
|
+
status: "failed" | "cancelled";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HostedChildLifecycleRunnerOptions<TResult> {
|
|
37
|
+
adapter: HostedChildLifecycleAdapter;
|
|
38
|
+
execute: () => Promise<TResult> | TResult;
|
|
39
|
+
resolveCompletedState?: (
|
|
40
|
+
result: TResult,
|
|
41
|
+
) =>
|
|
42
|
+
| Promise<HostedChildLifecycleCompletedState>
|
|
43
|
+
| HostedChildLifecycleCompletedState;
|
|
44
|
+
resolveErrorState: (
|
|
45
|
+
error: unknown,
|
|
46
|
+
) =>
|
|
47
|
+
| Promise<HostedChildLifecycleErrorState>
|
|
48
|
+
| HostedChildLifecycleErrorState;
|
|
49
|
+
onLifecycleError?: (error: unknown) => Promise<void> | void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type HostedChildLifecycleRunResult<TResult> =
|
|
53
|
+
| {
|
|
54
|
+
status: "completed";
|
|
55
|
+
result: TResult;
|
|
56
|
+
terminalState: HostedChildLifecycleTerminalState;
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
status: "failed" | "cancelled";
|
|
60
|
+
error: unknown;
|
|
61
|
+
terminalState: HostedChildLifecycleTerminalState;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
async function dispatchTerminalState(
|
|
65
|
+
adapter: HostedChildLifecycleAdapter,
|
|
66
|
+
terminalState: HostedChildLifecycleTerminalState,
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
if (terminalState.status === "cancelled") {
|
|
69
|
+
await adapter.cancelled?.(terminalState);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (terminalState.status === "failed") {
|
|
74
|
+
await adapter.failed?.(terminalState);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await adapter.completed?.(terminalState);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function runHostedChildLifecycle<TResult>(
|
|
82
|
+
options: HostedChildLifecycleRunnerOptions<TResult>,
|
|
83
|
+
): Promise<HostedChildLifecycleRunResult<TResult>> {
|
|
84
|
+
await options.adapter.pending?.();
|
|
85
|
+
await options.adapter.running?.();
|
|
86
|
+
|
|
87
|
+
let result: TResult;
|
|
88
|
+
try {
|
|
89
|
+
result = await options.execute();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const terminalState = await options.resolveErrorState(error);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await dispatchTerminalState(options.adapter, terminalState);
|
|
95
|
+
} catch (lifecycleError) {
|
|
96
|
+
if (options.onLifecycleError) {
|
|
97
|
+
await options.onLifecycleError(lifecycleError);
|
|
98
|
+
} else {
|
|
99
|
+
throw lifecycleError;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
status: terminalState.status,
|
|
105
|
+
error,
|
|
106
|
+
terminalState,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const terminalState = options.resolveCompletedState
|
|
111
|
+
? await options.resolveCompletedState(result)
|
|
112
|
+
: { status: "completed" as const };
|
|
113
|
+
|
|
114
|
+
await dispatchTerminalState(options.adapter, terminalState);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
status: "completed",
|
|
118
|
+
result,
|
|
119
|
+
terminalState,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export interface HostedLifecycleTerminalState {
|
|
2
|
+
status: "completed" | "failed" | "cancelled";
|
|
3
|
+
metadata?: {
|
|
4
|
+
modelId?: string;
|
|
5
|
+
usage?: {
|
|
6
|
+
inputTokens?: number;
|
|
7
|
+
outputTokens?: number;
|
|
8
|
+
cachedInputTokens?: number;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
terminalErrorCode?: string | null;
|
|
12
|
+
terminalErrorMessage?: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface HostedLifecycleExecution<TChunk> {
|
|
16
|
+
stream: AsyncIterable<TChunk>;
|
|
17
|
+
waitForFinish: () => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HostedLifecycleAdapter<TRun, TChunk> {
|
|
21
|
+
startRun: (input: { abortSignal: AbortSignal }) => Promise<TRun> | TRun;
|
|
22
|
+
appendEvents?: (run: TRun, chunk: TChunk) => Promise<void> | void;
|
|
23
|
+
persistTranscriptChunk?: (run: TRun, chunk: TChunk) => Promise<void> | void;
|
|
24
|
+
persistTranscriptTerminalState?: (
|
|
25
|
+
run: TRun,
|
|
26
|
+
terminalState: HostedLifecycleTerminalState,
|
|
27
|
+
) => Promise<void> | void;
|
|
28
|
+
onTerminalState?: (
|
|
29
|
+
run: TRun,
|
|
30
|
+
terminalState: HostedLifecycleTerminalState,
|
|
31
|
+
) => Promise<void> | void;
|
|
32
|
+
finalizeRun?: (run: TRun, terminalState: HostedLifecycleTerminalState) => Promise<void> | void;
|
|
33
|
+
cancelRun?: (run: TRun, terminalState: HostedLifecycleTerminalState) => Promise<void> | void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HostedLifecycleRunnerOptions<TRun, TChunk> {
|
|
37
|
+
abortSignal: AbortSignal;
|
|
38
|
+
execution: HostedLifecycleExecution<TChunk>;
|
|
39
|
+
adapter: HostedLifecycleAdapter<TRun, TChunk>;
|
|
40
|
+
resolveTerminalState: () => Promise<HostedLifecycleTerminalState> | HostedLifecycleTerminalState;
|
|
41
|
+
resolveErrorTerminalState?: (
|
|
42
|
+
error: unknown,
|
|
43
|
+
) => Promise<HostedLifecycleTerminalState> | HostedLifecycleTerminalState;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface HostedLifecycleRunResult<TRun> {
|
|
47
|
+
run: TRun;
|
|
48
|
+
terminalState: HostedLifecycleTerminalState;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getTerminalErrorMessage(error: unknown): string {
|
|
52
|
+
return error instanceof Error ? error.message : String(error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function defaultErrorTerminalState(
|
|
56
|
+
abortSignal: AbortSignal,
|
|
57
|
+
error: unknown,
|
|
58
|
+
): HostedLifecycleTerminalState {
|
|
59
|
+
if (abortSignal.aborted) {
|
|
60
|
+
return {
|
|
61
|
+
status: "cancelled",
|
|
62
|
+
terminalErrorCode: "ABORTED",
|
|
63
|
+
terminalErrorMessage: getTerminalErrorMessage(error),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
status: "failed",
|
|
69
|
+
terminalErrorCode: "STREAM_ERROR",
|
|
70
|
+
terminalErrorMessage: getTerminalErrorMessage(error),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function captureHookError(
|
|
75
|
+
callback: (() => Promise<void> | void) | undefined,
|
|
76
|
+
): Promise<unknown | null> {
|
|
77
|
+
if (!callback) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await callback();
|
|
83
|
+
return null;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function runTerminalHooks<TRun, TChunk>(input: {
|
|
90
|
+
run: TRun;
|
|
91
|
+
terminalState: HostedLifecycleTerminalState;
|
|
92
|
+
adapter: HostedLifecycleAdapter<TRun, TChunk>;
|
|
93
|
+
}): Promise<void> {
|
|
94
|
+
let firstHookError: unknown | null = null;
|
|
95
|
+
|
|
96
|
+
const persistError = await captureHookError(() =>
|
|
97
|
+
input.adapter.persistTranscriptTerminalState?.(input.run, input.terminalState)
|
|
98
|
+
);
|
|
99
|
+
if (persistError) {
|
|
100
|
+
firstHookError = persistError;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const terminalObserverError = await captureHookError(() =>
|
|
104
|
+
input.adapter.onTerminalState?.(input.run, input.terminalState)
|
|
105
|
+
);
|
|
106
|
+
if (!firstHookError && terminalObserverError) {
|
|
107
|
+
firstHookError = terminalObserverError;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const terminalControlError = await captureHookError(() =>
|
|
111
|
+
input.terminalState.status === "cancelled"
|
|
112
|
+
? input.adapter.cancelRun?.(input.run, input.terminalState)
|
|
113
|
+
: input.adapter.finalizeRun?.(input.run, input.terminalState)
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (firstHookError) {
|
|
117
|
+
throw firstHookError;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (terminalControlError) {
|
|
121
|
+
throw terminalControlError;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function runHostedLifecycle<TRun, TChunk>(
|
|
126
|
+
options: HostedLifecycleRunnerOptions<TRun, TChunk>,
|
|
127
|
+
): Promise<HostedLifecycleRunResult<TRun>> {
|
|
128
|
+
const run = await options.adapter.startRun({ abortSignal: options.abortSignal });
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
for await (const chunk of options.execution.stream) {
|
|
132
|
+
await options.adapter.appendEvents?.(run, chunk);
|
|
133
|
+
await options.adapter.persistTranscriptChunk?.(run, chunk);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await options.execution.waitForFinish();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const terminalState = options.resolveErrorTerminalState
|
|
139
|
+
? await options.resolveErrorTerminalState(error)
|
|
140
|
+
: defaultErrorTerminalState(options.abortSignal, error);
|
|
141
|
+
|
|
142
|
+
await runTerminalHooks({
|
|
143
|
+
run,
|
|
144
|
+
terminalState,
|
|
145
|
+
adapter: options.adapter,
|
|
146
|
+
}).catch(() => undefined);
|
|
147
|
+
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const terminalState = await options.resolveTerminalState();
|
|
152
|
+
await runTerminalHooks({
|
|
153
|
+
run,
|
|
154
|
+
terminalState,
|
|
155
|
+
adapter: options.adapter,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return { run, terminalState };
|
|
159
|
+
}
|
package/src/src/agent/index.ts
CHANGED
|
@@ -180,6 +180,21 @@ export {
|
|
|
180
180
|
createAgUiBrowserResponseStream,
|
|
181
181
|
type CreateAgUiBrowserResponseStreamInput,
|
|
182
182
|
} from "./ag-ui-browser-response-stream.js";
|
|
183
|
+
export {
|
|
184
|
+
type HostedChildLifecycleAdapter,
|
|
185
|
+
type HostedChildLifecycleRunnerOptions,
|
|
186
|
+
type HostedChildLifecycleRunResult,
|
|
187
|
+
type HostedChildLifecycleTerminalState,
|
|
188
|
+
runHostedChildLifecycle,
|
|
189
|
+
} from "./hosted-child-lifecycle.js";
|
|
190
|
+
export {
|
|
191
|
+
type HostedLifecycleAdapter,
|
|
192
|
+
type HostedLifecycleExecution,
|
|
193
|
+
type HostedLifecycleRunnerOptions,
|
|
194
|
+
type HostedLifecycleRunResult,
|
|
195
|
+
type HostedLifecycleTerminalState,
|
|
196
|
+
runHostedLifecycle,
|
|
197
|
+
} from "./hosted-lifecycle.js";
|
|
183
198
|
export {
|
|
184
199
|
mergeToolCallInput,
|
|
185
200
|
mergeToolInputDelta,
|
|
@@ -20,7 +20,15 @@ export function sendSSE(
|
|
|
20
20
|
encoder: TextEncoder,
|
|
21
21
|
event: Record<string, unknown>,
|
|
22
22
|
): void {
|
|
23
|
-
|
|
23
|
+
try {
|
|
24
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (isClosedStreamControllerError(error)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export function closeSSEStream(controller: ReadableStreamDefaultController): void {
|
|
@@ -294,6 +294,58 @@ export async function listRuntimeAgents(
|
|
|
294
294
|
return RuntimeAgentListResponseSchema.parse({ agents });
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Verify the Ed25519 signature of a dispatch JWS and the recency of its
|
|
299
|
+
* timestamps, without binding to a particular request body or audience.
|
|
300
|
+
*
|
|
301
|
+
* This is intentionally weaker than {@link verifyDispatchJws}: it answers
|
|
302
|
+
* "was this JWS minted by a holder of the control-plane private key and is it
|
|
303
|
+
* still fresh?" and is used as a trust signal in code paths (proxy-trust,
|
|
304
|
+
* adapter selection) that don't yet have access to the authoritative request
|
|
305
|
+
* body or project audience. Callers that consume request payloads MUST still
|
|
306
|
+
* call {@link verifyDispatchJws} / {@link verifyControlPlaneJws} to bind the
|
|
307
|
+
* signature to the body and project.
|
|
308
|
+
*
|
|
309
|
+
* Returns true iff the signature verifies and `iat`/`exp` are within the
|
|
310
|
+
* allowed skew and max-age window. All other failures (including parsing
|
|
311
|
+
* errors) resolve to false so callers can treat the signal as present-but-not-
|
|
312
|
+
* proven without raising.
|
|
313
|
+
*/
|
|
314
|
+
export async function verifyDispatchJwsSignature(
|
|
315
|
+
jws: string,
|
|
316
|
+
options: {
|
|
317
|
+
publicKeyPem: string;
|
|
318
|
+
maxAgeSeconds: number;
|
|
319
|
+
},
|
|
320
|
+
): Promise<boolean> {
|
|
321
|
+
try {
|
|
322
|
+
const parts = jws.split(".");
|
|
323
|
+
if (parts.length !== 3) return false;
|
|
324
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
325
|
+
if (!encodedHeader || !encodedPayload || !encodedSignature) return false;
|
|
326
|
+
|
|
327
|
+
compactJwsHeaderSchema.parse(parseCompactJwsPart(encodedHeader));
|
|
328
|
+
const claims = dispatchClaimsSchema.parse(parseCompactJwsPart(encodedPayload));
|
|
329
|
+
|
|
330
|
+
const signingInput = new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`);
|
|
331
|
+
const signature = base64urlDecodeToBytes(encodedSignature);
|
|
332
|
+
const publicKey = await importEd25519PublicKey(options.publicKeyPem);
|
|
333
|
+
const verified = await dntShim.crypto.subtle.verify("Ed25519", publicKey, signature, signingInput);
|
|
334
|
+
if (!verified) return false;
|
|
335
|
+
|
|
336
|
+
if (claims.iss !== "veryfront-api") return false;
|
|
337
|
+
|
|
338
|
+
const now = Math.floor(Date.now() / 1000);
|
|
339
|
+
if (claims.exp <= now) return false;
|
|
340
|
+
if (claims.iat > now + SIGNATURE_SKEW_SECONDS) return false;
|
|
341
|
+
if (now - claims.iat > options.maxAgeSeconds) return false;
|
|
342
|
+
|
|
343
|
+
return true;
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
297
349
|
export async function verifyDispatchJws(
|
|
298
350
|
jws: string,
|
|
299
351
|
body: string,
|
|
@@ -153,7 +153,7 @@ export async function listChannelAssistants(
|
|
|
153
153
|
|
|
154
154
|
return ChannelAssistantsResponseSchema.parse({ assistants });
|
|
155
155
|
}
|
|
156
|
-
export { verifyDispatchJws } from "./control-plane.js";
|
|
156
|
+
export { verifyDispatchJws, verifyDispatchJwsSignature } from "./control-plane.js";
|
|
157
157
|
|
|
158
158
|
function normalizeConversationPart(
|
|
159
159
|
part: z.infer<typeof rawHistoryPartSchema>,
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
import { getPingIntervalMs, startPingInterval, stopPingInterval } from "./hmr-ping-keepalive.js";
|
|
30
30
|
import { broadcastUpdate, getMetrics } from "./hmr-message-router.js";
|
|
31
31
|
import { getEffectiveRequestHost } from "../../utils/request-host.js";
|
|
32
|
+
import { isProxyTrusted } from "../../utils/proxy-trust.js";
|
|
33
|
+
import { getHostEnv } from "../../../platform/compat/process.js";
|
|
32
34
|
|
|
33
35
|
const logger = serverLogger.component("hmr-handler");
|
|
34
36
|
|
|
@@ -89,14 +91,24 @@ export class HMRHandler extends BaseHandler {
|
|
|
89
91
|
});
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
handle(req: Request, ctx: HandlerContext): Promise<HandlerResult> {
|
|
93
|
-
if (!this.shouldHandle(req, ctx)) return
|
|
94
|
+
async handle(req: Request, ctx: HandlerContext): Promise<HandlerResult> {
|
|
95
|
+
if (!this.shouldHandle(req, ctx)) return this.continue();
|
|
94
96
|
|
|
95
97
|
const url = new URL(req.url);
|
|
96
98
|
const queryEnv = url.searchParams.get("x-environment");
|
|
97
99
|
const isPreviewMode = ctx.requestContext?.mode === "preview" || queryEnv === "preview";
|
|
98
100
|
const isLocal = !!ctx.isLocalProject;
|
|
99
|
-
|
|
101
|
+
// SECURITY: x-forwarded-host is client-controlled unless we trust the upstream proxy.
|
|
102
|
+
// Honouring it unconditionally lets any remote client present `x-forwarded-host: localhost`
|
|
103
|
+
// and unlock the localhost short-circuit that opens HMR (VULN-SRV-4). Only consult
|
|
104
|
+
// forwarded headers when the request is proxy-trusted; otherwise use Host / url.host.
|
|
105
|
+
// Proxy trust requires a verifiable dispatch JWS (or operator opt-in) — mere header
|
|
106
|
+
// presence is not enough, since `x-veryfront-dispatch-jws` is not stripped on ingress.
|
|
107
|
+
const publicKeyPem = ctx.adapter?.env?.get("CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY") ??
|
|
108
|
+
getHostEnv("CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY");
|
|
109
|
+
const host = (await isProxyTrusted(req, { publicKeyPem }))
|
|
110
|
+
? getEffectiveRequestHost(req, url)
|
|
111
|
+
: (req.headers.get("host") ?? url.host);
|
|
100
112
|
const isLocalhost = isLocalDevHost(host);
|
|
101
113
|
|
|
102
114
|
if (!isPreviewMode && !isLocal && !isLocalhost) {
|
|
@@ -109,7 +121,7 @@ export class HMRHandler extends BaseHandler {
|
|
|
109
121
|
isLocal,
|
|
110
122
|
isLocalhost,
|
|
111
123
|
});
|
|
112
|
-
return
|
|
124
|
+
return this.continue();
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
HMRHandler.initialize();
|
|
@@ -127,29 +139,25 @@ export class HMRHandler extends BaseHandler {
|
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
{ headers: { "content-type": "application/json" } },
|
|
144
|
-
),
|
|
142
|
+
return this.respond(
|
|
143
|
+
new Response(
|
|
144
|
+
JSON.stringify({
|
|
145
|
+
status: "ok",
|
|
146
|
+
clients: getClientCount(),
|
|
147
|
+
clientDetails: getClientDetails(),
|
|
148
|
+
metrics: {
|
|
149
|
+
...getMetrics(),
|
|
150
|
+
reloadNotifierMetrics: ReloadNotifier.getMetrics(),
|
|
151
|
+
},
|
|
152
|
+
message: "HMR WebSocket endpoint - connect via WebSocket",
|
|
153
|
+
}),
|
|
154
|
+
{ headers: { "content-type": "application/json" } },
|
|
145
155
|
),
|
|
146
156
|
);
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
if (!ctx.adapter?.server) {
|
|
150
|
-
return
|
|
151
|
-
this.respond(new Response("WebSocket not supported", { status: 501 })),
|
|
152
|
-
);
|
|
160
|
+
return this.respond(new Response("WebSocket not supported", { status: 501 }));
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
try {
|
|
@@ -234,12 +242,10 @@ export class HMRHandler extends BaseHandler {
|
|
|
234
242
|
totalClients: getClientCount(),
|
|
235
243
|
});
|
|
236
244
|
|
|
237
|
-
return
|
|
245
|
+
return this.respond(response);
|
|
238
246
|
} catch (error) {
|
|
239
247
|
logger.error("WebSocket upgrade failed", { error });
|
|
240
|
-
return
|
|
241
|
-
this.respond(new Response("WebSocket upgrade failed", { status: 500 })),
|
|
242
|
-
);
|
|
248
|
+
return this.respond(new Response("WebSocket upgrade failed", { status: 500 }));
|
|
243
249
|
}
|
|
244
250
|
}
|
|
245
251
|
|