@agent-native/core 0.84.28 → 0.84.30
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 +13 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/chat/tool-call-display.tsx +50 -8
- package/corpus/core/src/client/sse-event-processor.ts +17 -4
- package/corpus/core/src/deploy/build.ts +5 -0
- package/corpus/core/src/server/analytics.ts +29 -7
- package/corpus/core/src/server/ssr-handler.ts +305 -25
- package/corpus/core/src/vite/client.ts +3 -0
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +324 -24
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +11 -0
- package/corpus/templates/analytics/app/i18n-data.ts +110 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +45 -2
- package/corpus/templates/design/changelog/2026-07-01-design-previews-refresh-immediately-after-agent-screen-edits.md +6 -0
- package/dist/client/chat/tool-call-display.d.ts +1 -0
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +22 -5
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/sse-event-processor.d.ts.map +1 -1
- package/dist/client/sse-event-processor.js +13 -4
- package/dist/client/sse-event-processor.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.js +3 -0
- package/dist/deploy/build.js.map +1 -1
- 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 +8 -8
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/analytics.d.ts +10 -2
- package/dist/server/analytics.d.ts.map +1 -1
- package/dist/server/analytics.js +26 -5
- package/dist/server/analytics.js.map +1 -1
- package/dist/server/ssr-handler.d.ts.map +1 -1
- package/dist/server/ssr-handler.js +206 -21
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +1 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +1 -1
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.84.30
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 80e618a: Fix hosted Google Analytics / Tag Manager injection by baking the measurement id into Nitro server bundles and merging the required GA/GTM script, connect, and image hosts into existing stricter document CSPs.
|
|
8
|
+
- 80e618a: Improve chat tool-preparation UX by hiding zero-byte progress, using clearer preparation/writing copy, and showing a delayed long-running update hint.
|
|
9
|
+
|
|
10
|
+
## 0.84.29
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 5f60aaa: Fix hosted Google Analytics / Tag Manager injection by baking the measurement id into Nitro server bundles and merging the required GA/GTM script, connect, and image hosts into existing stricter document CSPs.
|
|
15
|
+
|
|
3
16
|
## 0.84.28
|
|
4
17
|
|
|
5
18
|
### 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.30",
|
|
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": {
|
|
@@ -70,6 +70,43 @@ export const ApprovalContext = React.createContext<ApprovalContextValue | null>(
|
|
|
70
70
|
null,
|
|
71
71
|
);
|
|
72
72
|
|
|
73
|
+
export const TOOL_LONG_RUNNING_HINT_DELAY_MS = 45_000;
|
|
74
|
+
|
|
75
|
+
function ToolLongRunningHintShell({
|
|
76
|
+
toolName,
|
|
77
|
+
isRunning,
|
|
78
|
+
children,
|
|
79
|
+
}: {
|
|
80
|
+
toolName: string;
|
|
81
|
+
isRunning: boolean;
|
|
82
|
+
children: React.ReactNode;
|
|
83
|
+
}) {
|
|
84
|
+
const [showLongRunningHint, setShowLongRunningHint] = useState(false);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!isRunning) {
|
|
88
|
+
setShowLongRunningHint(false);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setShowLongRunningHint(false);
|
|
92
|
+
const timeout = window.setTimeout(() => {
|
|
93
|
+
setShowLongRunningHint(true);
|
|
94
|
+
}, TOOL_LONG_RUNNING_HINT_DELAY_MS);
|
|
95
|
+
return () => window.clearTimeout(timeout);
|
|
96
|
+
}, [isRunning, toolName]);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<>
|
|
100
|
+
{children}
|
|
101
|
+
{isRunning && showLongRunningHint && (
|
|
102
|
+
<div className="mt-0.5 px-2.5 text-[11px] leading-snug text-muted-foreground/80">
|
|
103
|
+
Still working. Large updates can take a minute or two.
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
73
110
|
// ─── Tool-payload formatting ──────────────────────────────────────────────────
|
|
74
111
|
|
|
75
112
|
type ToolDetailSection = "input" | "result";
|
|
@@ -435,38 +472,43 @@ export function ToolCallDisplay({
|
|
|
435
472
|
// These must be separate components so hook order in ToolCallDisplayGeneric
|
|
436
473
|
// is always stable (no conditional hook calls).
|
|
437
474
|
const toolKind = structuredMeta?.toolKind as string | undefined;
|
|
475
|
+
const wrapToolDisplay = (children: React.ReactNode) => (
|
|
476
|
+
<ToolLongRunningHintShell toolName={toolName} isRunning={isRunning}>
|
|
477
|
+
{children}
|
|
478
|
+
</ToolLongRunningHintShell>
|
|
479
|
+
);
|
|
438
480
|
if (toolKind === "bash") {
|
|
439
|
-
return (
|
|
481
|
+
return wrapToolDisplay(
|
|
440
482
|
<BashCell
|
|
441
483
|
meta={
|
|
442
484
|
structuredMeta as unknown as Parameters<typeof BashCell>[0]["meta"]
|
|
443
485
|
}
|
|
444
486
|
output={result}
|
|
445
487
|
isRunning={isRunning}
|
|
446
|
-
|
|
488
|
+
/>,
|
|
447
489
|
);
|
|
448
490
|
}
|
|
449
491
|
if (toolKind === "edit") {
|
|
450
|
-
return (
|
|
492
|
+
return wrapToolDisplay(
|
|
451
493
|
<EditCell
|
|
452
494
|
meta={
|
|
453
495
|
structuredMeta as unknown as Parameters<typeof EditCell>[0]["meta"]
|
|
454
496
|
}
|
|
455
497
|
isRunning={isRunning}
|
|
456
|
-
|
|
498
|
+
/>,
|
|
457
499
|
);
|
|
458
500
|
}
|
|
459
501
|
if (toolKind === "write") {
|
|
460
|
-
return (
|
|
502
|
+
return wrapToolDisplay(
|
|
461
503
|
<WriteCell
|
|
462
504
|
meta={
|
|
463
505
|
structuredMeta as unknown as Parameters<typeof WriteCell>[0]["meta"]
|
|
464
506
|
}
|
|
465
507
|
isRunning={isRunning}
|
|
466
|
-
|
|
508
|
+
/>,
|
|
467
509
|
);
|
|
468
510
|
}
|
|
469
|
-
return (
|
|
511
|
+
return wrapToolDisplay(
|
|
470
512
|
<ToolCallDisplayGeneric
|
|
471
513
|
toolName={toolName}
|
|
472
514
|
argsText={argsText}
|
|
@@ -477,7 +519,7 @@ export function ToolCallDisplay({
|
|
|
477
519
|
isRunning={isRunning}
|
|
478
520
|
approval={approval}
|
|
479
521
|
repeatCount={repeatCount}
|
|
480
|
-
|
|
522
|
+
/>,
|
|
481
523
|
);
|
|
482
524
|
}
|
|
483
525
|
|
|
@@ -205,13 +205,26 @@ function baseActivityLabel(ev: SSEEvent, tool?: string): string {
|
|
|
205
205
|
return humanizeToolLabelText(ev.label ?? "Working", tool);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
function preparationActivityLabel(
|
|
209
|
+
tool: string | undefined,
|
|
210
|
+
progressBytes: number | undefined,
|
|
211
|
+
): string {
|
|
212
|
+
const action = humanizeToolName(tool);
|
|
213
|
+
if (progressBytes === undefined) {
|
|
214
|
+
return `Starting ${action}...`;
|
|
215
|
+
}
|
|
216
|
+
if (progressBytes <= 0) {
|
|
217
|
+
return `Preparing ${action}...`;
|
|
218
|
+
}
|
|
219
|
+
return `Writing ${action}... (${formatProgressBytes(progressBytes)} prepared)`;
|
|
220
|
+
}
|
|
221
|
+
|
|
208
222
|
function visibleActivityLabel(ev: SSEEvent, tool?: string): string {
|
|
209
|
-
const label = baseActivityLabel(ev, tool);
|
|
210
223
|
const progressBytes = activityProgressBytes(ev);
|
|
211
|
-
if (
|
|
212
|
-
return
|
|
224
|
+
if (isPreparingActionActivity(ev)) {
|
|
225
|
+
return preparationActivityLabel(tool, progressBytes);
|
|
213
226
|
}
|
|
214
|
-
return
|
|
227
|
+
return baseActivityLabel(ev, tool);
|
|
215
228
|
}
|
|
216
229
|
|
|
217
230
|
function findPendingToolCallIndex(
|
|
@@ -2886,6 +2886,11 @@ export default bundle;
|
|
|
2886
2886
|
virtual: {
|
|
2887
2887
|
"virtual:agents-bundle": agentsBundleModuleSource,
|
|
2888
2888
|
},
|
|
2889
|
+
replace: {
|
|
2890
|
+
"process.env.AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID": JSON.stringify(
|
|
2891
|
+
process.env.GA_MEASUREMENT_ID?.trim() || "",
|
|
2892
|
+
),
|
|
2893
|
+
},
|
|
2889
2894
|
// Replace browser-only renderers (Excalidraw/Mermaid) with an inert proxy in
|
|
2890
2895
|
// the server bundle. Without this, Nitro's Rolldown build pulls the real
|
|
2891
2896
|
// Excalidraw into a shared vendor chunk imported statically by the SSR render
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* - `GA_MEASUREMENT_ID` — Google Analytics 4 measurement ID
|
|
5
5
|
*
|
|
6
6
|
* Netlify configuration-file env vars are build-time only for serverless
|
|
7
|
-
* functions, so the Vite
|
|
8
|
-
*
|
|
7
|
+
* functions, so the Vite/Nitro build paths also bake this public value into
|
|
8
|
+
* SSR bundles.
|
|
9
9
|
*
|
|
10
10
|
* Amplitude and Sentry are initialized client-side via their npm packages
|
|
11
11
|
* (see `packages/core/src/client/analytics.ts`). Only GA requires script
|
|
@@ -28,14 +28,17 @@ function normalizeMeasurementId(value: string | undefined): string | null {
|
|
|
28
28
|
return trimmed ? trimmed : null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function getViteBakedGaMeasurementId(): string | undefined {
|
|
32
|
+
return typeof __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__ === "string"
|
|
33
|
+
? __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__
|
|
34
|
+
: undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
31
37
|
function getGaMeasurementId(): string | null {
|
|
32
38
|
return (
|
|
33
39
|
normalizeMeasurementId(process.env.GA_MEASUREMENT_ID) ||
|
|
34
|
-
normalizeMeasurementId(
|
|
35
|
-
|
|
36
|
-
? __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__
|
|
37
|
-
: undefined,
|
|
38
|
-
)
|
|
40
|
+
normalizeMeasurementId(process.env.AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID) ||
|
|
41
|
+
normalizeMeasurementId(getViteBakedGaMeasurementId())
|
|
39
42
|
);
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -51,6 +54,25 @@ export const GA_CSP_SCRIPT_HOSTS = [
|
|
|
51
54
|
"https://www.google-analytics.com",
|
|
52
55
|
] as const;
|
|
53
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
|
+
|
|
54
76
|
/**
|
|
55
77
|
* The exact JS body (no surrounding `<script>` tags) of the inline gtag config
|
|
56
78
|
* block injected next to the gtag.js loader. Returned so the SSR handler can
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
withAgentNativeSocialImageCacheBuster,
|
|
32
32
|
} from "../shared/social-meta.js";
|
|
33
33
|
import {
|
|
34
|
+
GA_CSP_CONNECT_HOSTS,
|
|
35
|
+
GA_CSP_IMG_HOSTS,
|
|
34
36
|
GA_CSP_SCRIPT_HOSTS,
|
|
35
37
|
getGaInlineConfigScriptBody,
|
|
36
38
|
} from "./analytics.js";
|
|
@@ -270,6 +272,256 @@ function extractScriptBody(scriptTag: string | null): string | null {
|
|
|
270
272
|
return scriptTag.slice(start, end);
|
|
271
273
|
}
|
|
272
274
|
|
|
275
|
+
type CspDirective = {
|
|
276
|
+
name: string;
|
|
277
|
+
tokens: string[];
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const CSP_DIRECTIVES_WITH_VALUE_TOKENS = new Set([
|
|
281
|
+
"base-uri",
|
|
282
|
+
"block-all-mixed-content",
|
|
283
|
+
"child-src",
|
|
284
|
+
"connect-src",
|
|
285
|
+
"default-src",
|
|
286
|
+
"fenced-frame-src",
|
|
287
|
+
"font-src",
|
|
288
|
+
"form-action",
|
|
289
|
+
"frame-ancestors",
|
|
290
|
+
"frame-src",
|
|
291
|
+
"img-src",
|
|
292
|
+
"manifest-src",
|
|
293
|
+
"media-src",
|
|
294
|
+
"navigate-to",
|
|
295
|
+
"object-src",
|
|
296
|
+
"plugin-types",
|
|
297
|
+
"prefetch-src",
|
|
298
|
+
"referrer",
|
|
299
|
+
"reflected-xss",
|
|
300
|
+
"require-sri-for",
|
|
301
|
+
"require-trusted-types-for",
|
|
302
|
+
"report-to",
|
|
303
|
+
"report-uri",
|
|
304
|
+
"sandbox",
|
|
305
|
+
"script-src",
|
|
306
|
+
"script-src-attr",
|
|
307
|
+
"script-src-elem",
|
|
308
|
+
"style-src",
|
|
309
|
+
"style-src-attr",
|
|
310
|
+
"style-src-elem",
|
|
311
|
+
"trusted-types",
|
|
312
|
+
"upgrade-insecure-requests",
|
|
313
|
+
"webrtc",
|
|
314
|
+
"worker-src",
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
function hasCommaJoinedCspPolicies(policy: string): boolean {
|
|
318
|
+
let commaIndex = policy.indexOf(",");
|
|
319
|
+
while (commaIndex !== -1) {
|
|
320
|
+
const afterComma = policy.slice(commaIndex + 1);
|
|
321
|
+
const directive = /^\s+([a-z][a-z0-9-]*)(?=\s|;|$)/i.exec(afterComma)?.[1];
|
|
322
|
+
if (
|
|
323
|
+
directive &&
|
|
324
|
+
CSP_DIRECTIVES_WITH_VALUE_TOKENS.has(directive.toLowerCase())
|
|
325
|
+
) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
commaIndex = policy.indexOf(",", commaIndex + 1);
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parseCsp(policy: string): CspDirective[] {
|
|
334
|
+
return policy
|
|
335
|
+
.split(";")
|
|
336
|
+
.map((part) => part.trim())
|
|
337
|
+
.filter(Boolean)
|
|
338
|
+
.map((part) => {
|
|
339
|
+
const [name = "", ...tokens] = part.split(/\s+/);
|
|
340
|
+
return { name: name.toLowerCase(), tokens };
|
|
341
|
+
})
|
|
342
|
+
.filter((directive) => directive.name);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function serializeCsp(directives: CspDirective[]): string {
|
|
346
|
+
return directives
|
|
347
|
+
.map((directive) =>
|
|
348
|
+
[directive.name, ...directive.tokens].filter(Boolean).join(" "),
|
|
349
|
+
)
|
|
350
|
+
.join("; ");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function appendCspTokens(
|
|
354
|
+
tokens: string[],
|
|
355
|
+
additions: readonly string[],
|
|
356
|
+
): string[] {
|
|
357
|
+
if (!additions.length) return tokens;
|
|
358
|
+
const next = tokens.filter((token) => token !== "'none'");
|
|
359
|
+
const seen = new Set(next);
|
|
360
|
+
for (const token of additions) {
|
|
361
|
+
if (!token || seen.has(token)) continue;
|
|
362
|
+
next.push(token);
|
|
363
|
+
seen.add(token);
|
|
364
|
+
}
|
|
365
|
+
return next;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function findCspDirective(
|
|
369
|
+
directives: CspDirective[],
|
|
370
|
+
name: string,
|
|
371
|
+
): CspDirective | undefined {
|
|
372
|
+
return directives.find((directive) => directive.name === name);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function appendToExistingOrDefaultCspDirective(
|
|
376
|
+
directives: CspDirective[],
|
|
377
|
+
name: string,
|
|
378
|
+
additions: readonly string[],
|
|
379
|
+
): void {
|
|
380
|
+
if (!additions.length) return;
|
|
381
|
+
const existing = findCspDirective(directives, name);
|
|
382
|
+
if (existing) {
|
|
383
|
+
existing.tokens = appendCspTokens(existing.tokens, additions);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const defaultSrc = findCspDirective(directives, "default-src");
|
|
388
|
+
if (!defaultSrc) return;
|
|
389
|
+
directives.push({
|
|
390
|
+
name,
|
|
391
|
+
tokens: appendCspTokens([...defaultSrc.tokens], additions),
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function appendToExistingCspDirective(
|
|
396
|
+
directives: CspDirective[],
|
|
397
|
+
name: string,
|
|
398
|
+
additions: readonly string[],
|
|
399
|
+
): void {
|
|
400
|
+
const existing = findCspDirective(directives, name);
|
|
401
|
+
if (!existing) return;
|
|
402
|
+
existing.tokens = appendCspTokens(existing.tokens, additions);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function hasStrictNonceScriptPolicy(tokens: readonly string[]): boolean {
|
|
406
|
+
return tokens.some(
|
|
407
|
+
(token) => token === "'strict-dynamic'" || token.startsWith("'nonce-"),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function appendToScriptCspDirective(
|
|
412
|
+
directives: CspDirective[],
|
|
413
|
+
name: string,
|
|
414
|
+
additions: readonly string[],
|
|
415
|
+
): boolean {
|
|
416
|
+
const existing = findCspDirective(directives, name);
|
|
417
|
+
if (existing) {
|
|
418
|
+
if (hasStrictNonceScriptPolicy(existing.tokens)) return false;
|
|
419
|
+
existing.tokens = appendCspTokens(existing.tokens, additions);
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const defaultSrc = findCspDirective(directives, "default-src");
|
|
424
|
+
if (!defaultSrc || hasStrictNonceScriptPolicy(defaultSrc.tokens)) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
directives.push({
|
|
428
|
+
name,
|
|
429
|
+
tokens: appendCspTokens([...defaultSrc.tokens], additions),
|
|
430
|
+
});
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function appendToEffectiveScriptElementCspDirective(
|
|
435
|
+
directives: CspDirective[],
|
|
436
|
+
additions: readonly string[],
|
|
437
|
+
): boolean {
|
|
438
|
+
const scriptSrcElem = findCspDirective(directives, "script-src-elem");
|
|
439
|
+
if (scriptSrcElem) {
|
|
440
|
+
if (hasStrictNonceScriptPolicy(scriptSrcElem.tokens)) return false;
|
|
441
|
+
scriptSrcElem.tokens = appendCspTokens(scriptSrcElem.tokens, additions);
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return appendToScriptCspDirective(directives, "script-src", additions);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function augmentExistingEnforcedCspForFrameworkScripts(
|
|
449
|
+
policy: string,
|
|
450
|
+
options: {
|
|
451
|
+
gaScriptSrcTokens: readonly string[];
|
|
452
|
+
gaEnabled: boolean;
|
|
453
|
+
},
|
|
454
|
+
): string {
|
|
455
|
+
// Multiple CSP headers are surfaced by Headers.get() as one comma-joined
|
|
456
|
+
// string. CSP is not a comma-list header, so serializing a parsed combined
|
|
457
|
+
// value would turn two policies into one invalid policy. Leave those headers
|
|
458
|
+
// app-owned; a comma inside a source/report URL is still safe to parse.
|
|
459
|
+
if (hasCommaJoinedCspPolicies(policy)) return policy;
|
|
460
|
+
|
|
461
|
+
const directives = parseCsp(policy);
|
|
462
|
+
if (!directives.length) return policy;
|
|
463
|
+
|
|
464
|
+
if (options.gaEnabled) {
|
|
465
|
+
const addedScriptElement = appendToEffectiveScriptElementCspDirective(
|
|
466
|
+
directives,
|
|
467
|
+
options.gaScriptSrcTokens,
|
|
468
|
+
);
|
|
469
|
+
if (addedScriptElement) {
|
|
470
|
+
appendToExistingOrDefaultCspDirective(
|
|
471
|
+
directives,
|
|
472
|
+
"connect-src",
|
|
473
|
+
GA_CSP_CONNECT_HOSTS,
|
|
474
|
+
);
|
|
475
|
+
appendToExistingOrDefaultCspDirective(
|
|
476
|
+
directives,
|
|
477
|
+
"img-src",
|
|
478
|
+
GA_CSP_IMG_HOSTS,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return serializeCsp(directives);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function augmentExistingReportOnlyCspForFrameworkScripts(
|
|
487
|
+
policy: string,
|
|
488
|
+
options: {
|
|
489
|
+
scriptSrcTokens: readonly string[];
|
|
490
|
+
gaEnabled: boolean;
|
|
491
|
+
},
|
|
492
|
+
): string {
|
|
493
|
+
if (hasCommaJoinedCspPolicies(policy)) return policy;
|
|
494
|
+
|
|
495
|
+
const directives = parseCsp(policy);
|
|
496
|
+
if (!directives.length) return policy;
|
|
497
|
+
|
|
498
|
+
appendToExistingOrDefaultCspDirective(
|
|
499
|
+
directives,
|
|
500
|
+
"script-src",
|
|
501
|
+
options.scriptSrcTokens,
|
|
502
|
+
);
|
|
503
|
+
appendToExistingCspDirective(
|
|
504
|
+
directives,
|
|
505
|
+
"script-src-elem",
|
|
506
|
+
options.scriptSrcTokens,
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
if (options.gaEnabled) {
|
|
510
|
+
appendToExistingOrDefaultCspDirective(
|
|
511
|
+
directives,
|
|
512
|
+
"connect-src",
|
|
513
|
+
GA_CSP_CONNECT_HOSTS,
|
|
514
|
+
);
|
|
515
|
+
appendToExistingOrDefaultCspDirective(
|
|
516
|
+
directives,
|
|
517
|
+
"img-src",
|
|
518
|
+
GA_CSP_IMG_HOSTS,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return serializeCsp(directives);
|
|
523
|
+
}
|
|
524
|
+
|
|
273
525
|
/**
|
|
274
526
|
* Apply a Content-Security-Policy header to HTML document responses.
|
|
275
527
|
*
|
|
@@ -282,20 +534,28 @@ function extractScriptBody(scriptTag: string | null): string | null {
|
|
|
282
534
|
* user-controlled content reaches the HTML).
|
|
283
535
|
*
|
|
284
536
|
* A third directive, `script-src`, is emitted via `Content-Security-Policy-
|
|
285
|
-
* Report-Only` rather than enforced
|
|
286
|
-
* scripts (the Sentry config block,
|
|
287
|
-
* startup from the resolved env vars,
|
|
288
|
-
* the gtag config block, whose hash is
|
|
289
|
-
* `wrapWithAnalytics` embeds). It also loads
|
|
290
|
-
* `GA_CSP_SCRIPT_HOSTS`. All of those are listed
|
|
291
|
-
* reflects the code the framework itself injects
|
|
292
|
-
* violation on every page load.
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
537
|
+
* Report-Only` rather than enforced when the app has no existing document CSP.
|
|
538
|
+
* The framework injects deterministic inline scripts (the Sentry config block,
|
|
539
|
+
* whose hash is computed once at process startup from the resolved env vars,
|
|
540
|
+
* and — when `GA_MEASUREMENT_ID` is set — the gtag config block, whose hash is
|
|
541
|
+
* derived from the same string `wrapWithAnalytics` embeds). It also loads
|
|
542
|
+
* Google Tag Manager / GA4 from `GA_CSP_SCRIPT_HOSTS`. All of those are listed
|
|
543
|
+
* here so the report-only policy reflects the code the framework itself injects
|
|
544
|
+
* instead of reporting a violation on every page load.
|
|
545
|
+
*
|
|
546
|
+
* If an app or host already sends an enforced CSP with `script-src`,
|
|
547
|
+
* `script-src-elem`, `connect-src`, `img-src`, or `default-src`, we merge only
|
|
548
|
+
* GA-specific allowances into existing host/hash policies. Strict nonce or
|
|
549
|
+
* `strict-dynamic` script policies stay app-owned because blindly appending
|
|
550
|
+
* hashes or hosts would widen the policy without reliably loading our injected
|
|
551
|
+
* scripts.
|
|
552
|
+
*
|
|
553
|
+
* Templates additionally render a theme-init inline script whose exact content
|
|
554
|
+
* varies by template (default theme param, custom docs variant, etc.) and which
|
|
555
|
+
* is rendered by React Router, not this handler, so its hash is not available
|
|
556
|
+
* here. Shipping script-src as Report-Only surfaces the remaining violations
|
|
557
|
+
* without breaking template customisations; teams can graduate to enforcement
|
|
558
|
+
* once their hashes are enumerated.
|
|
299
559
|
*
|
|
300
560
|
* Skipped in development (`NODE_ENV !== 'production'`) so HMR eval and Vite
|
|
301
561
|
* dev-server injects are never blocked. Set `AGENT_NATIVE_DISABLE_DOC_CSP=1`
|
|
@@ -305,16 +565,6 @@ function applyDocumentCsp(headers: Headers, sentryScript: string | null): void {
|
|
|
305
565
|
if (process.env.NODE_ENV !== "production") return;
|
|
306
566
|
if (process.env.AGENT_NATIVE_DISABLE_DOC_CSP === "1") return;
|
|
307
567
|
|
|
308
|
-
// object-src / base-uri: enforced; neither directive mentions scripts, so
|
|
309
|
-
// they are safe even when a template's inline script hashes are unknown.
|
|
310
|
-
const existing = headers.get("content-security-policy") ?? "";
|
|
311
|
-
if (!existing) {
|
|
312
|
-
headers.set(
|
|
313
|
-
"content-security-policy",
|
|
314
|
-
"object-src 'none'; base-uri 'self'",
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
568
|
// script-src as Report-Only: list 'self', the framework-injected inline
|
|
319
569
|
// script hashes (Sentry config + gtag config), and the Google Analytics /
|
|
320
570
|
// Tag Manager loader hosts. These are exactly the scripts the framework
|
|
@@ -326,17 +576,47 @@ function applyDocumentCsp(headers: Headers, sentryScript: string | null): void {
|
|
|
326
576
|
const gaInlineBody = getGaInlineConfigScriptBody();
|
|
327
577
|
const gaHash = gaInlineBody ? computeInlineScriptHash(gaInlineBody) : null;
|
|
328
578
|
const gaHosts = gaInlineBody ? [...GA_CSP_SCRIPT_HOSTS] : [];
|
|
579
|
+
const gaScriptSrcTokens = [...(gaHash ? [gaHash] : []), ...gaHosts];
|
|
329
580
|
const scriptSrcTokens = [
|
|
330
581
|
"'self'",
|
|
331
582
|
...(sentryHash ? [sentryHash] : []),
|
|
332
583
|
...(gaHash ? [gaHash] : []),
|
|
333
584
|
...gaHosts,
|
|
334
585
|
];
|
|
335
|
-
const scriptSrc = `script-src ${scriptSrcTokens.join(" ")}`;
|
|
336
586
|
|
|
587
|
+
const cspAugmentOptions = {
|
|
588
|
+
scriptSrcTokens,
|
|
589
|
+
gaScriptSrcTokens,
|
|
590
|
+
gaEnabled: Boolean(gaInlineBody),
|
|
591
|
+
};
|
|
592
|
+
const existing = headers.get("content-security-policy") ?? "";
|
|
593
|
+
if (!existing) {
|
|
594
|
+
headers.set(
|
|
595
|
+
"content-security-policy",
|
|
596
|
+
"object-src 'none'; base-uri 'self'",
|
|
597
|
+
);
|
|
598
|
+
} else {
|
|
599
|
+
headers.set(
|
|
600
|
+
"content-security-policy",
|
|
601
|
+
augmentExistingEnforcedCspForFrameworkScripts(
|
|
602
|
+
existing,
|
|
603
|
+
cspAugmentOptions,
|
|
604
|
+
),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const scriptSrc = `script-src ${scriptSrcTokens.join(" ")}`;
|
|
337
609
|
const existingRo = headers.get("content-security-policy-report-only") ?? "";
|
|
338
610
|
if (!existingRo) {
|
|
339
611
|
headers.set("content-security-policy-report-only", scriptSrc);
|
|
612
|
+
} else {
|
|
613
|
+
headers.set(
|
|
614
|
+
"content-security-policy-report-only",
|
|
615
|
+
augmentExistingReportOnlyCspForFrameworkScripts(
|
|
616
|
+
existingRo,
|
|
617
|
+
cspAugmentOptions,
|
|
618
|
+
),
|
|
619
|
+
);
|
|
340
620
|
}
|
|
341
621
|
}
|
|
342
622
|
|
|
@@ -2086,6 +2086,9 @@ function createAgentNativeConfig(
|
|
|
2086
2086
|
__AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__: JSON.stringify(
|
|
2087
2087
|
process.env.GA_MEASUREMENT_ID?.trim() || "",
|
|
2088
2088
|
),
|
|
2089
|
+
"process.env.AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID": JSON.stringify(
|
|
2090
|
+
process.env.GA_MEASUREMENT_ID?.trim() || "",
|
|
2091
|
+
),
|
|
2089
2092
|
// Framework route warmup controls how SSR `.data` routes are fetched:
|
|
2090
2093
|
// ordinary fetches keep them CDN-cacheable, while native prefetch headers
|
|
2091
2094
|
// can be refused before the CDN/origin sees the request. Keep this value
|