@agent-native/core 0.84.65 → 0.84.67
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +108 -3
- package/corpus/core/src/server/analytics.ts +1 -34
- package/corpus/core/src/server/ssr-handler.ts +18 -377
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +0 -2
- package/corpus/templates/design/changelog/2026-07-03-design-chat-recovers-stuck-screen-edits-inside-the-worker.md +5 -0
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +0 -1
- package/dist/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +75 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/analytics.d.ts +1 -19
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +1 -31
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +18 -271
- package/dist/server/ssr-handler.js.map +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.84.67
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 171f6e6: Remove app document CSP headers so hosted Google Tag Manager and framework inline bootstrap scripts are not blocked or reported by a shared policy.
|
|
8
|
+
|
|
9
|
+
## 0.84.66
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 70a6085: Keep hosted chat runs inside the active worker when a progress-aware action-preparation checkpoint asks to continue, instead of depending on the browser to start the recovery turn.
|
|
14
|
+
|
|
3
15
|
## 0.84.65
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.84.
|
|
3
|
+
"version": "0.84.67",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -971,6 +971,12 @@ const TOOL_INPUT_ACTIVITY_INTERVAL_MS = 1500;
|
|
|
971
971
|
const ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS = 90_000;
|
|
972
972
|
const ACTION_PREPARATION_ZERO_BYTE_RESTART_LIMIT = 2;
|
|
973
973
|
const MODEL_STREAM_NO_PROGRESS_TIMEOUT_MS = 90_000;
|
|
974
|
+
const MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT = 6;
|
|
975
|
+
const RUN_BUDGET_EXHAUSTED_ERROR_CODE = "run_budget_exhausted";
|
|
976
|
+
const RUN_BUDGET_EXHAUSTED_MESSAGE =
|
|
977
|
+
"I ran out of time before finishing this step. " +
|
|
978
|
+
"I stopped rather than keep retrying silently. " +
|
|
979
|
+
"Check any completed tool cards above before retrying, ideally as one smaller follow-up.";
|
|
974
980
|
const MAX_TEXT_ATTACHMENT_CHARS = 60_000;
|
|
975
981
|
const MAX_SELECTION_CONTEXT_CHARS = 8_000;
|
|
976
982
|
const MAX_RESOURCE_INVENTORY_ITEMS = 40;
|
|
@@ -2978,7 +2984,7 @@ export async function runAgentLoop(opts: {
|
|
|
2978
2984
|
};
|
|
2979
2985
|
}
|
|
2980
2986
|
return (
|
|
2981
|
-
zeroByteToolInputRestart.count
|
|
2987
|
+
zeroByteToolInputRestart.count >=
|
|
2982
2988
|
ACTION_PREPARATION_ZERO_BYTE_RESTART_LIMIT &&
|
|
2983
2989
|
now - zeroByteToolInputRestart.firstStartedAt >=
|
|
2984
2990
|
ACTION_PREPARATION_NO_PROGRESS_TIMEOUT_MS
|
|
@@ -4430,6 +4436,82 @@ export function backgroundContinuationReasonForRun(
|
|
|
4430
4436
|
return "run_timeout";
|
|
4431
4437
|
}
|
|
4432
4438
|
|
|
4439
|
+
export async function runAgentLoopWithMainChatInternalContinuations(
|
|
4440
|
+
opts: Parameters<typeof runAgentLoop>[0],
|
|
4441
|
+
): Promise<Awaited<ReturnType<typeof runAgentLoop>>> {
|
|
4442
|
+
const usage: Awaited<ReturnType<typeof runAgentLoop>> = {
|
|
4443
|
+
inputTokens: 0,
|
|
4444
|
+
outputTokens: 0,
|
|
4445
|
+
cacheReadTokens: 0,
|
|
4446
|
+
cacheWriteTokens: 0,
|
|
4447
|
+
model: opts.model,
|
|
4448
|
+
};
|
|
4449
|
+
const addUsage = (next: Awaited<ReturnType<typeof runAgentLoop>>) => {
|
|
4450
|
+
usage.inputTokens += next.inputTokens;
|
|
4451
|
+
usage.outputTokens += next.outputTokens;
|
|
4452
|
+
usage.cacheReadTokens += next.cacheReadTokens;
|
|
4453
|
+
usage.cacheWriteTokens += next.cacheWriteTokens;
|
|
4454
|
+
usage.model = next.model;
|
|
4455
|
+
};
|
|
4456
|
+
|
|
4457
|
+
const localTurnEvents: AgentChatEvent[] = [];
|
|
4458
|
+
let lastAttemptWasUnfinishedContinuation = false;
|
|
4459
|
+
for (
|
|
4460
|
+
let attempt = 0;
|
|
4461
|
+
!opts.signal.aborted && attempt < MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT;
|
|
4462
|
+
attempt++
|
|
4463
|
+
) {
|
|
4464
|
+
lastAttemptWasUnfinishedContinuation = false;
|
|
4465
|
+
let continuationReason: AgentLoopContinuationReason | undefined;
|
|
4466
|
+
const attemptStartIndex = localTurnEvents.length;
|
|
4467
|
+
const send = (event: AgentChatEvent) => {
|
|
4468
|
+
localTurnEvents.push(event);
|
|
4469
|
+
if (
|
|
4470
|
+
event.type === "auto_continue" &&
|
|
4471
|
+
isAgentLoopContinuationReason(event.reason)
|
|
4472
|
+
) {
|
|
4473
|
+
continuationReason = event.reason;
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
opts.send(event);
|
|
4477
|
+
};
|
|
4478
|
+
|
|
4479
|
+
const nextUsage = await runAgentLoop({ ...opts, send });
|
|
4480
|
+
addUsage(nextUsage);
|
|
4481
|
+
|
|
4482
|
+
if (!continuationReason || opts.signal.aborted) {
|
|
4483
|
+
return usage;
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
lastAttemptWasUnfinishedContinuation = true;
|
|
4487
|
+
const attemptEvents = localTurnEvents.slice(attemptStartIndex);
|
|
4488
|
+
const completedSideEffect = attemptEvents.some(
|
|
4489
|
+
(event) =>
|
|
4490
|
+
event.type === "tool_done" &&
|
|
4491
|
+
event.completedSideEffect === true &&
|
|
4492
|
+
event.isError !== true,
|
|
4493
|
+
);
|
|
4494
|
+
if (!completedSideEffect) {
|
|
4495
|
+
opts.send({ type: "clear" });
|
|
4496
|
+
}
|
|
4497
|
+
const actionPreparationTool =
|
|
4498
|
+
lastUnfinishedPreparingActionToolFromEvents(localTurnEvents);
|
|
4499
|
+
appendAgentLoopContinuation(opts.messages, continuationReason, {
|
|
4500
|
+
...(actionPreparationTool ? { actionPreparationTool } : {}),
|
|
4501
|
+
});
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
if (!opts.signal.aborted && lastAttemptWasUnfinishedContinuation) {
|
|
4505
|
+
opts.send({
|
|
4506
|
+
type: "error",
|
|
4507
|
+
error: RUN_BUDGET_EXHAUSTED_MESSAGE,
|
|
4508
|
+
errorCode: RUN_BUDGET_EXHAUSTED_ERROR_CODE,
|
|
4509
|
+
recoverable: true,
|
|
4510
|
+
});
|
|
4511
|
+
}
|
|
4512
|
+
return usage;
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4433
4515
|
function endsAtContinuationBoundary(run: ActiveRun): boolean {
|
|
4434
4516
|
return (
|
|
4435
4517
|
endsAtInternalContinuationBoundary(run) ||
|
|
@@ -5770,6 +5852,11 @@ export function createProductionAgentHandler(
|
|
|
5770
5852
|
continuationDispatchPath,
|
|
5771
5853
|
);
|
|
5772
5854
|
try {
|
|
5855
|
+
await recordRunDiagnostic(
|
|
5856
|
+
run.runId,
|
|
5857
|
+
RUN_DIAG_STAGE.workerSetupStep,
|
|
5858
|
+
`chain_dispatch_start nextRunId=${nextRunId} reason=${continuationReason} path=${continuationDispatchPath}`,
|
|
5859
|
+
).catch(() => {});
|
|
5773
5860
|
await fireInternalDispatch({
|
|
5774
5861
|
event,
|
|
5775
5862
|
// Continuation chunks use the same path resolution as the
|
|
@@ -5795,11 +5882,28 @@ export function createProductionAgentHandler(
|
|
|
5795
5882
|
continuationExpectsNetlifyBackgroundFunction,
|
|
5796
5883
|
},
|
|
5797
5884
|
},
|
|
5885
|
+
settleMs: continuationExpectsNetlifyBackgroundFunction
|
|
5886
|
+
? BACKGROUND_CLAIM_GRACE_MS
|
|
5887
|
+
: undefined,
|
|
5798
5888
|
});
|
|
5889
|
+
await recordRunDiagnostic(
|
|
5890
|
+
run.runId,
|
|
5891
|
+
RUN_DIAG_STAGE.workerSetupStep,
|
|
5892
|
+
`chain_dispatch_sent nextRunId=${nextRunId} reason=${continuationReason}`,
|
|
5893
|
+
).catch(() => {});
|
|
5799
5894
|
} catch (chainErr) {
|
|
5800
5895
|
// Chain dispatch failed — fail loud so the held row goes
|
|
5801
5896
|
// terminal instead of spinning. The reaper would also catch
|
|
5802
5897
|
// it, but this is immediate and truthful.
|
|
5898
|
+
await recordRunDiagnostic(
|
|
5899
|
+
run.runId,
|
|
5900
|
+
RUN_DIAG_STAGE.workerThrew,
|
|
5901
|
+
`chain_dispatch_failed nextRunId=${nextRunId} ${
|
|
5902
|
+
chainErr instanceof Error
|
|
5903
|
+
? chainErr.message
|
|
5904
|
+
: String(chainErr)
|
|
5905
|
+
}`,
|
|
5906
|
+
).catch(() => {});
|
|
5803
5907
|
console.error(
|
|
5804
5908
|
"[agent-chat] background continuation dispatch failed:",
|
|
5805
5909
|
chainErr instanceof Error ? chainErr.message : chainErr,
|
|
@@ -6242,7 +6346,7 @@ export function createProductionAgentHandler(
|
|
|
6242
6346
|
if (obsConfig.enabled) {
|
|
6243
6347
|
instrumented = true;
|
|
6244
6348
|
loopUsage = await instrumentAgentLoop({
|
|
6245
|
-
runAgentLoop,
|
|
6349
|
+
runAgentLoop: runAgentLoopWithMainChatInternalContinuations,
|
|
6246
6350
|
loopOpts: agentLoopOpts,
|
|
6247
6351
|
runId,
|
|
6248
6352
|
threadId: threadId ?? null,
|
|
@@ -6272,7 +6376,8 @@ export function createProductionAgentHandler(
|
|
|
6272
6376
|
if (instrumented) throw err;
|
|
6273
6377
|
}
|
|
6274
6378
|
if (!instrumented) {
|
|
6275
|
-
loopUsage =
|
|
6379
|
+
loopUsage =
|
|
6380
|
+
await runAgentLoopWithMainChatInternalContinuations(agentLoopOpts);
|
|
6276
6381
|
}
|
|
6277
6382
|
|
|
6278
6383
|
// Record token usage for cost monitoring so the Usage panel in
|
|
@@ -42,42 +42,9 @@ function getGaMeasurementId(): string | null {
|
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
* Script hosts the injected GA loader pulls executable code from. Google Tag
|
|
47
|
-
* Manager serves `gtag/js`, and GA4 can lazy-load additional collectors from
|
|
48
|
-
* `www.google-analytics.com`. These must be listed in the document `script-src`
|
|
49
|
-
* so the CSP reflects the code the framework itself injects (see
|
|
50
|
-
* `applyDocumentCsp` in `ssr-handler.ts`).
|
|
51
|
-
*/
|
|
52
|
-
export const GA_CSP_SCRIPT_HOSTS = [
|
|
53
|
-
"https://www.googletagmanager.com",
|
|
54
|
-
"https://www.google-analytics.com",
|
|
55
|
-
] as const;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Network/image hosts used by the GA4 loader when it sends page-view and event
|
|
59
|
-
* beacons. These are separate from `script-src`: a stricter deployment CSP with
|
|
60
|
-
* `connect-src 'self'` or `img-src 'self'` can load gtag.js but still drop all
|
|
61
|
-
* analytics events unless these hosts are present too.
|
|
62
|
-
*/
|
|
63
|
-
export const GA_CSP_CONNECT_HOSTS = [
|
|
64
|
-
"https://www.google-analytics.com",
|
|
65
|
-
"https://analytics.google.com",
|
|
66
|
-
"https://stats.g.doubleclick.net",
|
|
67
|
-
"https://region1.google-analytics.com",
|
|
68
|
-
] as const;
|
|
69
|
-
|
|
70
|
-
export const GA_CSP_IMG_HOSTS = [
|
|
71
|
-
"https://www.google-analytics.com",
|
|
72
|
-
"https://www.googletagmanager.com",
|
|
73
|
-
"https://stats.g.doubleclick.net",
|
|
74
|
-
] as const;
|
|
75
|
-
|
|
76
45
|
/**
|
|
77
46
|
* The exact JS body (no surrounding `<script>` tags) of the inline gtag config
|
|
78
|
-
* block injected next to the gtag.js loader.
|
|
79
|
-
* hash it for the `script-src` CSP directive — the hash must be computed from
|
|
80
|
-
* the identical string that `getGaScript()` embeds, so both call this helper.
|
|
47
|
+
* block injected next to the gtag.js loader.
|
|
81
48
|
* Returns `null` when GA is not configured.
|
|
82
49
|
*/
|
|
83
50
|
export function getGaInlineConfigScriptBody(): string | null {
|
|
@@ -30,18 +30,11 @@ import {
|
|
|
30
30
|
AGENT_NATIVE_SOCIAL_IMAGE_WIDTH,
|
|
31
31
|
withAgentNativeSocialImageCacheBuster,
|
|
32
32
|
} from "../shared/social-meta.js";
|
|
33
|
-
import {
|
|
34
|
-
GA_CSP_CONNECT_HOSTS,
|
|
35
|
-
GA_CSP_IMG_HOSTS,
|
|
36
|
-
GA_CSP_SCRIPT_HOSTS,
|
|
37
|
-
getGaInlineConfigScriptBody,
|
|
38
|
-
} from "./analytics.js";
|
|
39
33
|
import {
|
|
40
34
|
getAppBasePathFromViteEnv,
|
|
41
35
|
stripAppBasePath as canonicalStripAppBasePath,
|
|
42
36
|
} from "./app-base-path.js";
|
|
43
37
|
import { runWithRequestContext } from "./request-context.js";
|
|
44
|
-
import { computeInlineScriptHash } from "./security-headers.js";
|
|
45
38
|
import { getSentryClientConfigScript } from "./sentry-config.js";
|
|
46
39
|
|
|
47
40
|
export {
|
|
@@ -258,376 +251,17 @@ function applyDefaultSpeculationRulesHeader(
|
|
|
258
251
|
}
|
|
259
252
|
|
|
260
253
|
/**
|
|
261
|
-
*
|
|
262
|
-
* Returns `null` if the input is falsy or has no recognisable `</script>` end.
|
|
263
|
-
* Used to compute the sha256 hash of framework-injected inline scripts so the
|
|
264
|
-
* hash can be listed in app-owned `script-src` CSP directives.
|
|
265
|
-
*/
|
|
266
|
-
function extractScriptBody(scriptTag: string | null): string | null {
|
|
267
|
-
if (!scriptTag) return null;
|
|
268
|
-
const start = scriptTag.indexOf(">") + 1;
|
|
269
|
-
const end = scriptTag.lastIndexOf("</script>");
|
|
270
|
-
if (start <= 0 || end < start) return null;
|
|
271
|
-
return scriptTag.slice(start, end);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
type CspDirective = {
|
|
275
|
-
name: string;
|
|
276
|
-
tokens: string[];
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const CSP_DIRECTIVES_WITH_VALUE_TOKENS = new Set([
|
|
280
|
-
"base-uri",
|
|
281
|
-
"block-all-mixed-content",
|
|
282
|
-
"child-src",
|
|
283
|
-
"connect-src",
|
|
284
|
-
"default-src",
|
|
285
|
-
"fenced-frame-src",
|
|
286
|
-
"font-src",
|
|
287
|
-
"form-action",
|
|
288
|
-
"frame-ancestors",
|
|
289
|
-
"frame-src",
|
|
290
|
-
"img-src",
|
|
291
|
-
"manifest-src",
|
|
292
|
-
"media-src",
|
|
293
|
-
"navigate-to",
|
|
294
|
-
"object-src",
|
|
295
|
-
"plugin-types",
|
|
296
|
-
"prefetch-src",
|
|
297
|
-
"referrer",
|
|
298
|
-
"reflected-xss",
|
|
299
|
-
"require-sri-for",
|
|
300
|
-
"require-trusted-types-for",
|
|
301
|
-
"report-to",
|
|
302
|
-
"report-uri",
|
|
303
|
-
"sandbox",
|
|
304
|
-
"script-src",
|
|
305
|
-
"script-src-attr",
|
|
306
|
-
"script-src-elem",
|
|
307
|
-
"style-src",
|
|
308
|
-
"style-src-attr",
|
|
309
|
-
"style-src-elem",
|
|
310
|
-
"trusted-types",
|
|
311
|
-
"upgrade-insecure-requests",
|
|
312
|
-
"webrtc",
|
|
313
|
-
"worker-src",
|
|
314
|
-
]);
|
|
315
|
-
|
|
316
|
-
function hasCommaJoinedCspPolicies(policy: string): boolean {
|
|
317
|
-
let commaIndex = policy.indexOf(",");
|
|
318
|
-
while (commaIndex !== -1) {
|
|
319
|
-
const afterComma = policy.slice(commaIndex + 1);
|
|
320
|
-
const directive = /^\s+([a-z][a-z0-9-]*)(?=\s|;|$)/i.exec(afterComma)?.[1];
|
|
321
|
-
if (
|
|
322
|
-
directive &&
|
|
323
|
-
CSP_DIRECTIVES_WITH_VALUE_TOKENS.has(directive.toLowerCase())
|
|
324
|
-
) {
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
commaIndex = policy.indexOf(",", commaIndex + 1);
|
|
328
|
-
}
|
|
329
|
-
return false;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function parseCsp(policy: string): CspDirective[] {
|
|
333
|
-
return policy
|
|
334
|
-
.split(";")
|
|
335
|
-
.map((part) => part.trim())
|
|
336
|
-
.filter(Boolean)
|
|
337
|
-
.map((part) => {
|
|
338
|
-
const [name = "", ...tokens] = part.split(/\s+/);
|
|
339
|
-
return { name: name.toLowerCase(), tokens };
|
|
340
|
-
})
|
|
341
|
-
.filter((directive) => directive.name);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function serializeCsp(directives: CspDirective[]): string {
|
|
345
|
-
return directives
|
|
346
|
-
.map((directive) =>
|
|
347
|
-
[directive.name, ...directive.tokens].filter(Boolean).join(" "),
|
|
348
|
-
)
|
|
349
|
-
.join("; ");
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function appendCspTokens(
|
|
353
|
-
tokens: string[],
|
|
354
|
-
additions: readonly string[],
|
|
355
|
-
): string[] {
|
|
356
|
-
if (!additions.length) return tokens;
|
|
357
|
-
const next = tokens.filter((token) => token !== "'none'");
|
|
358
|
-
const seen = new Set(next);
|
|
359
|
-
for (const token of additions) {
|
|
360
|
-
if (!token || seen.has(token)) continue;
|
|
361
|
-
next.push(token);
|
|
362
|
-
seen.add(token);
|
|
363
|
-
}
|
|
364
|
-
return next;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function findCspDirective(
|
|
368
|
-
directives: CspDirective[],
|
|
369
|
-
name: string,
|
|
370
|
-
): CspDirective | undefined {
|
|
371
|
-
return directives.find((directive) => directive.name === name);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function appendToExistingOrDefaultCspDirective(
|
|
375
|
-
directives: CspDirective[],
|
|
376
|
-
name: string,
|
|
377
|
-
additions: readonly string[],
|
|
378
|
-
): void {
|
|
379
|
-
if (!additions.length) return;
|
|
380
|
-
const existing = findCspDirective(directives, name);
|
|
381
|
-
if (existing) {
|
|
382
|
-
existing.tokens = appendCspTokens(existing.tokens, additions);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const defaultSrc = findCspDirective(directives, "default-src");
|
|
387
|
-
if (!defaultSrc) return;
|
|
388
|
-
directives.push({
|
|
389
|
-
name,
|
|
390
|
-
tokens: appendCspTokens([...defaultSrc.tokens], additions),
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function appendToExistingCspDirective(
|
|
395
|
-
directives: CspDirective[],
|
|
396
|
-
name: string,
|
|
397
|
-
additions: readonly string[],
|
|
398
|
-
): void {
|
|
399
|
-
const existing = findCspDirective(directives, name);
|
|
400
|
-
if (!existing) return;
|
|
401
|
-
existing.tokens = appendCspTokens(existing.tokens, additions);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function ensureCspDirective(
|
|
405
|
-
directives: CspDirective[],
|
|
406
|
-
name: string,
|
|
407
|
-
tokens: readonly string[],
|
|
408
|
-
): void {
|
|
409
|
-
if (findCspDirective(directives, name)) return;
|
|
410
|
-
directives.push({ name, tokens: [...tokens] });
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function hasStrictNonceScriptPolicy(tokens: readonly string[]): boolean {
|
|
414
|
-
return tokens.some(
|
|
415
|
-
(token) => token === "'strict-dynamic'" || token.startsWith("'nonce-"),
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
function appendToScriptCspDirective(
|
|
420
|
-
directives: CspDirective[],
|
|
421
|
-
name: string,
|
|
422
|
-
additions: readonly string[],
|
|
423
|
-
): boolean {
|
|
424
|
-
const existing = findCspDirective(directives, name);
|
|
425
|
-
if (existing) {
|
|
426
|
-
if (hasStrictNonceScriptPolicy(existing.tokens)) return false;
|
|
427
|
-
existing.tokens = appendCspTokens(existing.tokens, additions);
|
|
428
|
-
return true;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const defaultSrc = findCspDirective(directives, "default-src");
|
|
432
|
-
if (!defaultSrc || hasStrictNonceScriptPolicy(defaultSrc.tokens)) {
|
|
433
|
-
return false;
|
|
434
|
-
}
|
|
435
|
-
directives.push({
|
|
436
|
-
name,
|
|
437
|
-
tokens: appendCspTokens([...defaultSrc.tokens], additions),
|
|
438
|
-
});
|
|
439
|
-
return true;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function appendToEffectiveScriptElementCspDirective(
|
|
443
|
-
directives: CspDirective[],
|
|
444
|
-
additions: readonly string[],
|
|
445
|
-
): boolean {
|
|
446
|
-
const scriptSrcElem = findCspDirective(directives, "script-src-elem");
|
|
447
|
-
if (scriptSrcElem) {
|
|
448
|
-
if (hasStrictNonceScriptPolicy(scriptSrcElem.tokens)) return false;
|
|
449
|
-
scriptSrcElem.tokens = appendCspTokens(scriptSrcElem.tokens, additions);
|
|
450
|
-
return true;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return appendToScriptCspDirective(directives, "script-src", additions);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function augmentExistingEnforcedCspForFrameworkScripts(
|
|
457
|
-
policy: string,
|
|
458
|
-
options: {
|
|
459
|
-
gaScriptSrcTokens: readonly string[];
|
|
460
|
-
gaEnabled: boolean;
|
|
461
|
-
},
|
|
462
|
-
): string {
|
|
463
|
-
// Multiple CSP headers are surfaced by Headers.get() as one comma-joined
|
|
464
|
-
// string. CSP is not a comma-list header, so serializing a parsed combined
|
|
465
|
-
// value would turn two policies into one invalid policy. Leave those headers
|
|
466
|
-
// app-owned; a comma inside a source/report URL is still safe to parse.
|
|
467
|
-
if (hasCommaJoinedCspPolicies(policy)) return policy;
|
|
468
|
-
|
|
469
|
-
const directives = parseCsp(policy);
|
|
470
|
-
if (!directives.length) return policy;
|
|
471
|
-
|
|
472
|
-
if (options.gaEnabled) {
|
|
473
|
-
const addedScriptElement = appendToEffectiveScriptElementCspDirective(
|
|
474
|
-
directives,
|
|
475
|
-
options.gaScriptSrcTokens,
|
|
476
|
-
);
|
|
477
|
-
if (addedScriptElement) {
|
|
478
|
-
appendToExistingOrDefaultCspDirective(
|
|
479
|
-
directives,
|
|
480
|
-
"connect-src",
|
|
481
|
-
GA_CSP_CONNECT_HOSTS,
|
|
482
|
-
);
|
|
483
|
-
appendToExistingOrDefaultCspDirective(
|
|
484
|
-
directives,
|
|
485
|
-
"img-src",
|
|
486
|
-
GA_CSP_IMG_HOSTS,
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
ensureCspDirective(directives, "object-src", ["'none'"]);
|
|
492
|
-
ensureCspDirective(directives, "base-uri", ["'self'"]);
|
|
493
|
-
|
|
494
|
-
return serializeCsp(directives);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
function augmentExistingReportOnlyCspForFrameworkScripts(
|
|
498
|
-
policy: string,
|
|
499
|
-
options: {
|
|
500
|
-
scriptSrcTokens: readonly string[];
|
|
501
|
-
gaEnabled: boolean;
|
|
502
|
-
},
|
|
503
|
-
): string {
|
|
504
|
-
if (hasCommaJoinedCspPolicies(policy)) return policy;
|
|
505
|
-
|
|
506
|
-
const directives = parseCsp(policy);
|
|
507
|
-
if (!directives.length) return policy;
|
|
508
|
-
|
|
509
|
-
appendToExistingOrDefaultCspDirective(
|
|
510
|
-
directives,
|
|
511
|
-
"script-src",
|
|
512
|
-
options.scriptSrcTokens,
|
|
513
|
-
);
|
|
514
|
-
appendToExistingCspDirective(
|
|
515
|
-
directives,
|
|
516
|
-
"script-src-elem",
|
|
517
|
-
options.scriptSrcTokens,
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
if (options.gaEnabled) {
|
|
521
|
-
appendToExistingOrDefaultCspDirective(
|
|
522
|
-
directives,
|
|
523
|
-
"connect-src",
|
|
524
|
-
GA_CSP_CONNECT_HOSTS,
|
|
525
|
-
);
|
|
526
|
-
appendToExistingOrDefaultCspDirective(
|
|
527
|
-
directives,
|
|
528
|
-
"img-src",
|
|
529
|
-
GA_CSP_IMG_HOSTS,
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
return serializeCsp(directives);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Apply a Content-Security-Policy header to HTML document responses.
|
|
254
|
+
* Strip document-level CSP from app HTML responses.
|
|
538
255
|
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
* hijacking all relative URLs in the document (a common attack target when
|
|
545
|
-
* user-controlled content reaches the HTML).
|
|
546
|
-
*
|
|
547
|
-
* A third directive, `script-src`, is emitted via `Content-Security-Policy-
|
|
548
|
-
* Report-Only` rather than enforced when the app has no existing document CSP.
|
|
549
|
-
* The framework injects inline scripts for analytics, Sentry, and template
|
|
550
|
-
* setup, and hosted apps need Google Tag Manager to load without noisy CSP
|
|
551
|
-
* diagnostics. The report-only policy is intentionally permissive for scripts:
|
|
552
|
-
* it includes `'unsafe-inline'` plus the known GA/GTM loader hosts.
|
|
553
|
-
*
|
|
554
|
-
* If an app or host already sends an enforced CSP with `script-src`,
|
|
555
|
-
* `script-src-elem`, `connect-src`, `img-src`, or `default-src`, we merge only
|
|
556
|
-
* GA-specific allowances into existing host/hash policies. Strict nonce or
|
|
557
|
-
* `strict-dynamic` script policies stay app-owned because blindly appending
|
|
558
|
-
* hashes or hosts would widen the policy without reliably loading our injected
|
|
559
|
-
* scripts.
|
|
560
|
-
*
|
|
561
|
-
* Templates additionally render a theme-init inline script whose exact content
|
|
562
|
-
* varies by template (default theme param, custom docs variant, etc.) and which
|
|
563
|
-
* is rendered by React Router, not this handler, so its hash is not available
|
|
564
|
-
* here. Shipping script-src as Report-Only surfaces the remaining violations
|
|
565
|
-
* without breaking template customisations; teams can graduate to enforcement
|
|
566
|
-
* once their hashes are enumerated.
|
|
567
|
-
*
|
|
568
|
-
* Skipped in development (`NODE_ENV !== 'production'`) so HMR eval and Vite
|
|
569
|
-
* dev-server injects are never blocked. Set `AGENT_NATIVE_DISABLE_DOC_CSP=1`
|
|
570
|
-
* to opt out in production for a template with exotic needs.
|
|
256
|
+
* Hosted templates inject framework bootstrap scripts, analytics, Sentry config,
|
|
257
|
+
* and app-owned inline scripts whose exact bytes vary by build/template. Any
|
|
258
|
+
* shared CSP header, even Report-Only, can block or noisily report Google Tag
|
|
259
|
+
* Manager and those bootstraps. Extension iframes and webviews keep their own
|
|
260
|
+
* route-specific sandboxes; normal app documents deliberately do not emit CSP.
|
|
571
261
|
*/
|
|
572
|
-
function
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
// script-src as Report-Only: keep this deliberately loose so the framework's
|
|
577
|
-
// injected analytics and template bootstrap scripts do not look blocked in
|
|
578
|
-
// browser diagnostics.
|
|
579
|
-
const sentryBody = extractScriptBody(sentryScript);
|
|
580
|
-
const sentryHash = sentryBody ? computeInlineScriptHash(sentryBody) : null;
|
|
581
|
-
const gaInlineBody = getGaInlineConfigScriptBody();
|
|
582
|
-
const gaHash = gaInlineBody ? computeInlineScriptHash(gaInlineBody) : null;
|
|
583
|
-
const gaHosts = gaInlineBody ? [...GA_CSP_SCRIPT_HOSTS] : [];
|
|
584
|
-
const gaScriptSrcTokens = [
|
|
585
|
-
"'unsafe-inline'",
|
|
586
|
-
...(gaHash ? [gaHash] : []),
|
|
587
|
-
...gaHosts,
|
|
588
|
-
];
|
|
589
|
-
const scriptSrcTokens = [
|
|
590
|
-
"'self'",
|
|
591
|
-
"'unsafe-inline'",
|
|
592
|
-
...(sentryHash ? [sentryHash] : []),
|
|
593
|
-
...(gaHash ? [gaHash] : []),
|
|
594
|
-
...gaHosts,
|
|
595
|
-
];
|
|
596
|
-
|
|
597
|
-
const cspAugmentOptions = {
|
|
598
|
-
scriptSrcTokens,
|
|
599
|
-
gaScriptSrcTokens,
|
|
600
|
-
gaEnabled: Boolean(gaInlineBody),
|
|
601
|
-
};
|
|
602
|
-
const existing = headers.get("content-security-policy") ?? "";
|
|
603
|
-
if (!existing) {
|
|
604
|
-
headers.set(
|
|
605
|
-
"content-security-policy",
|
|
606
|
-
"object-src 'none'; base-uri 'self'",
|
|
607
|
-
);
|
|
608
|
-
} else {
|
|
609
|
-
headers.set(
|
|
610
|
-
"content-security-policy",
|
|
611
|
-
augmentExistingEnforcedCspForFrameworkScripts(
|
|
612
|
-
existing,
|
|
613
|
-
cspAugmentOptions,
|
|
614
|
-
),
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const scriptSrc = `script-src ${scriptSrcTokens.join(" ")}`;
|
|
619
|
-
const existingRo = headers.get("content-security-policy-report-only") ?? "";
|
|
620
|
-
if (!existingRo) {
|
|
621
|
-
headers.set("content-security-policy-report-only", scriptSrc);
|
|
622
|
-
} else {
|
|
623
|
-
headers.set(
|
|
624
|
-
"content-security-policy-report-only",
|
|
625
|
-
augmentExistingReportOnlyCspForFrameworkScripts(
|
|
626
|
-
existingRo,
|
|
627
|
-
cspAugmentOptions,
|
|
628
|
-
),
|
|
629
|
-
);
|
|
630
|
-
}
|
|
262
|
+
function removeDocumentCsp(headers: Headers): void {
|
|
263
|
+
headers.delete("content-security-policy");
|
|
264
|
+
headers.delete("content-security-policy-report-only");
|
|
631
265
|
}
|
|
632
266
|
|
|
633
267
|
function isFrameworkOrAssetPath(pathname: string): boolean {
|
|
@@ -665,7 +299,15 @@ async function rewriteMountedResponse(
|
|
|
665
299
|
}
|
|
666
300
|
|
|
667
301
|
const contentType = headers.get("content-type") ?? "";
|
|
668
|
-
if (!contentType.toLowerCase().includes("text/html")
|
|
302
|
+
if (!contentType.toLowerCase().includes("text/html")) {
|
|
303
|
+
return new Response(response.body, {
|
|
304
|
+
status: response.status,
|
|
305
|
+
statusText: response.statusText,
|
|
306
|
+
headers,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
removeDocumentCsp(headers);
|
|
310
|
+
if (!response.body) {
|
|
669
311
|
return new Response(response.body, {
|
|
670
312
|
status: response.status,
|
|
671
313
|
statusText: response.statusText,
|
|
@@ -675,7 +317,6 @@ async function rewriteMountedResponse(
|
|
|
675
317
|
|
|
676
318
|
const html = await response.text();
|
|
677
319
|
headers.delete("content-length");
|
|
678
|
-
applyDocumentCsp(headers, sentryClientConfigScript);
|
|
679
320
|
return new Response(
|
|
680
321
|
injectHeadScript(
|
|
681
322
|
injectDefaultSocialImageMeta(
|
|
@@ -349,8 +349,6 @@ function loomEmbedResponse(embedUrl: string): Response {
|
|
|
349
349
|
"Cache-Control": "private, max-age=0, no-store",
|
|
350
350
|
"Referrer-Policy": "no-referrer",
|
|
351
351
|
"X-Content-Type-Options": "nosniff",
|
|
352
|
-
"Content-Security-Policy":
|
|
353
|
-
"default-src 'none'; frame-src https://www.loom.com; style-src 'unsafe-inline'",
|
|
354
352
|
},
|
|
355
353
|
});
|
|
356
354
|
}
|
|
@@ -279,7 +279,6 @@ export async function renderPublicForm(event: H3Event) {
|
|
|
279
279
|
|
|
280
280
|
const headers: Record<string, string> = {
|
|
281
281
|
"Content-Type": "text/html; charset=utf-8",
|
|
282
|
-
"Content-Security-Policy": "frame-ancestors *",
|
|
283
282
|
};
|
|
284
283
|
if (status === 200) {
|
|
285
284
|
// Public form SSR is anonymous HTML and follows the same framework-level
|