@agent-native/core 0.84.27 → 0.84.29
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/client/MultiTabAssistantChat.tsx +17 -2
- package/corpus/core/src/client/agent-chat.ts +118 -0
- package/corpus/core/src/client/index.ts +4 -0
- 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 +161 -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/components/layout/Header.tsx +16 -23
- package/corpus/templates/design/app/pages/DesignEditor.tsx +23 -9
- package/corpus/templates/design/changelog/2026-07-01-zoom-menus-in-the-design-editor-now-open-independently-from-.md +6 -0
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +14 -2
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +23 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +79 -0
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/deploy/build.js +3 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- 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 +103 -21
- package/dist/server/ssr-handler.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +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,17 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.84.29
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
## 0.84.28
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- ffd5b99: Make agent chat thread handoffs robust when opening the panel from external UI controls.
|
|
14
|
+
|
|
3
15
|
## 0.84.27
|
|
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.29",
|
|
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": {
|
|
@@ -26,7 +26,9 @@ import {
|
|
|
26
26
|
AGENT_CHAT_REMOVE_CONTEXT_MESSAGE_TYPE,
|
|
27
27
|
AGENT_CHAT_SET_CONTEXT_MESSAGE_TYPE,
|
|
28
28
|
appendAgentChatContextToMessage,
|
|
29
|
+
claimAgentChatOpenRequest,
|
|
29
30
|
claimAgentChatSubmit,
|
|
31
|
+
drainBufferedAgentChatOpenRequests,
|
|
30
32
|
drainBufferedAgentChatSubmits,
|
|
31
33
|
normalizeAgentChatContextItem,
|
|
32
34
|
parseSubmitChatMessage,
|
|
@@ -2087,11 +2089,12 @@ export function MultiTabAssistantChat({
|
|
|
2087
2089
|
useEffect(() => {
|
|
2088
2090
|
const handleOpenThread = (event: Event) => {
|
|
2089
2091
|
const detail = (event as CustomEvent).detail as
|
|
2090
|
-
| { threadId?: unknown; newThread?: unknown }
|
|
2092
|
+
| { threadId?: unknown; newThread?: unknown; openRequestId?: unknown }
|
|
2091
2093
|
| undefined;
|
|
2092
2094
|
const threadId =
|
|
2093
2095
|
typeof detail?.threadId === "string" ? detail.threadId : "";
|
|
2094
|
-
if (!threadId) return;
|
|
2096
|
+
if (!detail || !threadId) return;
|
|
2097
|
+
if (!claimAgentChatOpenRequest(detail.openRequestId)) return;
|
|
2095
2098
|
|
|
2096
2099
|
if (detail?.newThread === true) {
|
|
2097
2100
|
newThreadIds.current.add(threadId);
|
|
@@ -2143,6 +2146,7 @@ export function MultiTabAssistantChat({
|
|
|
2143
2146
|
const detail = (e as CustomEvent).detail;
|
|
2144
2147
|
const threadId = detail?.threadId;
|
|
2145
2148
|
if (!threadId) return;
|
|
2149
|
+
if (!claimAgentChatOpenRequest(detail.openRequestId)) return;
|
|
2146
2150
|
dismissedSubAgentTabsRef.current.delete(threadId);
|
|
2147
2151
|
// Prefer an explicit parent (RunsTray/background hydration knows it);
|
|
2148
2152
|
// inline task cards fall back to the active orchestrator thread.
|
|
@@ -2196,6 +2200,17 @@ export function MultiTabAssistantChat({
|
|
|
2196
2200
|
return () => window.removeEventListener("agent-task-open", handleOpenTask);
|
|
2197
2201
|
}, [openTabIds, switchThread, refreshThreads, parentMap]);
|
|
2198
2202
|
|
|
2203
|
+
// Replay thread/task opens requested before this lazy panel's listeners
|
|
2204
|
+
// attached. Live events claim their id; replay drains only unclaimed requests.
|
|
2205
|
+
useEffect(() => {
|
|
2206
|
+
const buffered = drainBufferedAgentChatOpenRequests();
|
|
2207
|
+
for (const request of buffered) {
|
|
2208
|
+
window.dispatchEvent(
|
|
2209
|
+
new CustomEvent(request.eventType, { detail: request.detail }),
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
}, []);
|
|
2213
|
+
|
|
2199
2214
|
// Watch for agent-issued chat-command in application-state
|
|
2200
2215
|
const lastChatCommandRef = useRef(0);
|
|
2201
2216
|
useEffect(() => {
|
|
@@ -130,6 +130,26 @@ export interface AgentChatContextState {
|
|
|
130
130
|
updatedAt: number;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
export interface AgentChatOpenThreadRequest {
|
|
134
|
+
threadId: string;
|
|
135
|
+
newThread?: boolean;
|
|
136
|
+
openRequestId?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface AgentChatOpenTaskRequest {
|
|
140
|
+
threadId: string;
|
|
141
|
+
parentThreadId?: string;
|
|
142
|
+
description?: string;
|
|
143
|
+
name?: string;
|
|
144
|
+
openRequestId?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type BufferedAgentChatOpenRequest = {
|
|
148
|
+
id: string;
|
|
149
|
+
eventType: "agent-chat:open-thread" | "agent-task-open";
|
|
150
|
+
detail: AgentChatOpenThreadRequest | AgentChatOpenTaskRequest;
|
|
151
|
+
};
|
|
152
|
+
|
|
133
153
|
export interface AgentComposerReference {
|
|
134
154
|
label: string;
|
|
135
155
|
icon?: string;
|
|
@@ -243,6 +263,14 @@ const SELF_SUBMIT_BUFFER_TTL_MS = 8000;
|
|
|
243
263
|
const bufferedSelfSubmits: BufferedSelfSubmit[] = [];
|
|
244
264
|
const claimedSubmitIds = new Set<string>();
|
|
245
265
|
|
|
266
|
+
interface BufferedOpenRequest extends BufferedAgentChatOpenRequest {
|
|
267
|
+
at: number;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const OPEN_REQUEST_BUFFER_TTL_MS = 8000;
|
|
271
|
+
const bufferedOpenRequests: BufferedOpenRequest[] = [];
|
|
272
|
+
const claimedOpenRequestIds = new Set<string>();
|
|
273
|
+
|
|
246
274
|
function pruneSelfSubmitBuffer(now: number): void {
|
|
247
275
|
for (let i = bufferedSelfSubmits.length - 1; i >= 0; i -= 1) {
|
|
248
276
|
if (now - bufferedSelfSubmits[i].at > SELF_SUBMIT_BUFFER_TTL_MS) {
|
|
@@ -261,6 +289,32 @@ function bufferSelfSubmit(data: Record<string, unknown>): void {
|
|
|
261
289
|
bufferedSelfSubmits.push({ id, data, at: now });
|
|
262
290
|
}
|
|
263
291
|
|
|
292
|
+
function pruneOpenRequestBuffer(now: number): void {
|
|
293
|
+
for (let i = bufferedOpenRequests.length - 1; i >= 0; i -= 1) {
|
|
294
|
+
if (now - bufferedOpenRequests[i].at > OPEN_REQUEST_BUFFER_TTL_MS) {
|
|
295
|
+
const [removed] = bufferedOpenRequests.splice(i, 1);
|
|
296
|
+
if (removed) claimedOpenRequestIds.delete(removed.id);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function bufferOpenRequest(
|
|
302
|
+
eventType: BufferedAgentChatOpenRequest["eventType"],
|
|
303
|
+
detail: AgentChatOpenThreadRequest | AgentChatOpenTaskRequest,
|
|
304
|
+
): BufferedOpenRequest {
|
|
305
|
+
const now = Date.now();
|
|
306
|
+
pruneOpenRequestBuffer(now);
|
|
307
|
+
const id = `open-${now}-${Math.random().toString(36).slice(2, 8)}`;
|
|
308
|
+
const entry: BufferedOpenRequest = {
|
|
309
|
+
id,
|
|
310
|
+
eventType,
|
|
311
|
+
detail: { ...detail, openRequestId: id },
|
|
312
|
+
at: now,
|
|
313
|
+
};
|
|
314
|
+
bufferedOpenRequests.push(entry);
|
|
315
|
+
return entry;
|
|
316
|
+
}
|
|
317
|
+
|
|
264
318
|
/** Unclaimed self-submit payloads, for the panel to replay once it mounts. */
|
|
265
319
|
export function drainBufferedAgentChatSubmits(): Array<
|
|
266
320
|
Record<string, unknown>
|
|
@@ -279,10 +333,28 @@ export function claimAgentChatSubmit(id: string | undefined): boolean {
|
|
|
279
333
|
return true;
|
|
280
334
|
}
|
|
281
335
|
|
|
336
|
+
/** Unclaimed open-thread/task requests, for the panel to replay once it mounts. */
|
|
337
|
+
export function drainBufferedAgentChatOpenRequests(): BufferedAgentChatOpenRequest[] {
|
|
338
|
+
pruneOpenRequestBuffer(Date.now());
|
|
339
|
+
return bufferedOpenRequests
|
|
340
|
+
.filter((entry) => !claimedOpenRequestIds.has(entry.id))
|
|
341
|
+
.map(({ id, eventType, detail }) => ({ id, eventType, detail }));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Claim an open-thread/task request; false if already handled. Idless events pass. */
|
|
345
|
+
export function claimAgentChatOpenRequest(id: unknown): boolean {
|
|
346
|
+
if (typeof id !== "string" || !id) return true;
|
|
347
|
+
if (claimedOpenRequestIds.has(id)) return false;
|
|
348
|
+
claimedOpenRequestIds.add(id);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
282
352
|
/** Test-only: reset the self-submit buffer and claim set. */
|
|
283
353
|
export function _resetAgentChatSubmitBufferForTests(): void {
|
|
284
354
|
bufferedSelfSubmits.length = 0;
|
|
285
355
|
claimedSubmitIds.clear();
|
|
356
|
+
bufferedOpenRequests.length = 0;
|
|
357
|
+
claimedOpenRequestIds.clear();
|
|
286
358
|
}
|
|
287
359
|
|
|
288
360
|
export function normalizeAgentChatContextItem(
|
|
@@ -638,6 +710,52 @@ function postAgentChatReferenceMessage(
|
|
|
638
710
|
}
|
|
639
711
|
}
|
|
640
712
|
|
|
713
|
+
function openAgentPanelForChat(): void {
|
|
714
|
+
if (typeof window === "undefined") return;
|
|
715
|
+
window.dispatchEvent(
|
|
716
|
+
new CustomEvent("agent-panel:set-mode", {
|
|
717
|
+
detail: { mode: "chat" },
|
|
718
|
+
}),
|
|
719
|
+
);
|
|
720
|
+
window.dispatchEvent(new CustomEvent("agent-panel:open"));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function dispatchBufferedOpenRequest(entry: BufferedOpenRequest): void {
|
|
724
|
+
if (typeof window === "undefined") return;
|
|
725
|
+
const dispatch = () => {
|
|
726
|
+
window.dispatchEvent(
|
|
727
|
+
new CustomEvent(entry.eventType, { detail: entry.detail }),
|
|
728
|
+
);
|
|
729
|
+
};
|
|
730
|
+
setTimeout(dispatch, 0);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export function requestAgentChatThreadOpen(
|
|
734
|
+
detail: AgentChatOpenThreadRequest,
|
|
735
|
+
): void {
|
|
736
|
+
if (typeof window === "undefined" || !detail.threadId.trim()) return;
|
|
737
|
+
openAgentPanelForChat();
|
|
738
|
+
dispatchBufferedOpenRequest(
|
|
739
|
+
bufferOpenRequest("agent-chat:open-thread", {
|
|
740
|
+
...detail,
|
|
741
|
+
threadId: detail.threadId.trim(),
|
|
742
|
+
}),
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export function requestAgentTaskOpen(detail: AgentChatOpenTaskRequest): void {
|
|
747
|
+
if (typeof window === "undefined" || !detail.threadId.trim()) return;
|
|
748
|
+
openAgentPanelForChat();
|
|
749
|
+
const parentThreadId = detail.parentThreadId?.trim();
|
|
750
|
+
dispatchBufferedOpenRequest(
|
|
751
|
+
bufferOpenRequest("agent-task-open", {
|
|
752
|
+
...detail,
|
|
753
|
+
threadId: detail.threadId.trim(),
|
|
754
|
+
...(parentThreadId ? { parentThreadId } : {}),
|
|
755
|
+
}),
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
641
759
|
function isMcpAppChatBridgeEnabled(): boolean {
|
|
642
760
|
if (typeof window === "undefined" || window.parent === window) return false;
|
|
643
761
|
if (readEmbedMcpChatBridgeFlagFromUrl()) markEmbedMcpChatBridgeActive();
|
|
@@ -16,12 +16,16 @@ export {
|
|
|
16
16
|
normalizeAgentComposerReference,
|
|
17
17
|
refreshAgentChatContext,
|
|
18
18
|
removeAgentChatContextItem,
|
|
19
|
+
requestAgentChatThreadOpen,
|
|
20
|
+
requestAgentTaskOpen,
|
|
19
21
|
sendToAgentChat,
|
|
20
22
|
parseSubmitChatMessage,
|
|
21
23
|
setAgentChatContextItem,
|
|
22
24
|
setContextToAgentChat,
|
|
23
25
|
generateTabId,
|
|
24
26
|
type ParsedSubmitChat,
|
|
27
|
+
type AgentChatOpenTaskRequest,
|
|
28
|
+
type AgentChatOpenThreadRequest,
|
|
25
29
|
type AgentChatContextItem,
|
|
26
30
|
type AgentChatContextMessage,
|
|
27
31
|
type AgentChatContextMutationOptions,
|
|
@@ -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,121 @@ 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
|
+
function parseCsp(policy: string): CspDirective[] {
|
|
281
|
+
return policy
|
|
282
|
+
.split(";")
|
|
283
|
+
.map((part) => part.trim())
|
|
284
|
+
.filter(Boolean)
|
|
285
|
+
.map((part) => {
|
|
286
|
+
const [name = "", ...tokens] = part.split(/\s+/);
|
|
287
|
+
return { name: name.toLowerCase(), tokens };
|
|
288
|
+
})
|
|
289
|
+
.filter((directive) => directive.name);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function serializeCsp(directives: CspDirective[]): string {
|
|
293
|
+
return directives
|
|
294
|
+
.map((directive) =>
|
|
295
|
+
[directive.name, ...directive.tokens].filter(Boolean).join(" "),
|
|
296
|
+
)
|
|
297
|
+
.join("; ");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function appendCspTokens(
|
|
301
|
+
tokens: string[],
|
|
302
|
+
additions: readonly string[],
|
|
303
|
+
): string[] {
|
|
304
|
+
if (!additions.length) return tokens;
|
|
305
|
+
const next = tokens.filter((token) => token !== "'none'");
|
|
306
|
+
const seen = new Set(next);
|
|
307
|
+
for (const token of additions) {
|
|
308
|
+
if (!token || seen.has(token)) continue;
|
|
309
|
+
next.push(token);
|
|
310
|
+
seen.add(token);
|
|
311
|
+
}
|
|
312
|
+
return next;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function findCspDirective(
|
|
316
|
+
directives: CspDirective[],
|
|
317
|
+
name: string,
|
|
318
|
+
): CspDirective | undefined {
|
|
319
|
+
return directives.find((directive) => directive.name === name);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function appendToExistingOrDefaultCspDirective(
|
|
323
|
+
directives: CspDirective[],
|
|
324
|
+
name: string,
|
|
325
|
+
additions: readonly string[],
|
|
326
|
+
): void {
|
|
327
|
+
if (!additions.length) return;
|
|
328
|
+
const existing = findCspDirective(directives, name);
|
|
329
|
+
if (existing) {
|
|
330
|
+
existing.tokens = appendCspTokens(existing.tokens, additions);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const defaultSrc = findCspDirective(directives, "default-src");
|
|
335
|
+
if (!defaultSrc) return;
|
|
336
|
+
directives.push({
|
|
337
|
+
name,
|
|
338
|
+
tokens: appendCspTokens([...defaultSrc.tokens], additions),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function appendToExistingCspDirective(
|
|
343
|
+
directives: CspDirective[],
|
|
344
|
+
name: string,
|
|
345
|
+
additions: readonly string[],
|
|
346
|
+
): void {
|
|
347
|
+
const existing = findCspDirective(directives, name);
|
|
348
|
+
if (!existing) return;
|
|
349
|
+
existing.tokens = appendCspTokens(existing.tokens, additions);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function augmentExistingCspForFrameworkScripts(
|
|
353
|
+
policy: string,
|
|
354
|
+
options: {
|
|
355
|
+
scriptSrcTokens: readonly string[];
|
|
356
|
+
gaEnabled: boolean;
|
|
357
|
+
},
|
|
358
|
+
): string {
|
|
359
|
+
const directives = parseCsp(policy);
|
|
360
|
+
if (!directives.length) return policy;
|
|
361
|
+
|
|
362
|
+
appendToExistingOrDefaultCspDirective(
|
|
363
|
+
directives,
|
|
364
|
+
"script-src",
|
|
365
|
+
options.scriptSrcTokens,
|
|
366
|
+
);
|
|
367
|
+
// `script-src-elem` overrides `script-src` for script tags when present.
|
|
368
|
+
appendToExistingCspDirective(
|
|
369
|
+
directives,
|
|
370
|
+
"script-src-elem",
|
|
371
|
+
options.scriptSrcTokens,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
if (options.gaEnabled) {
|
|
375
|
+
appendToExistingOrDefaultCspDirective(
|
|
376
|
+
directives,
|
|
377
|
+
"connect-src",
|
|
378
|
+
GA_CSP_CONNECT_HOSTS,
|
|
379
|
+
);
|
|
380
|
+
appendToExistingOrDefaultCspDirective(
|
|
381
|
+
directives,
|
|
382
|
+
"img-src",
|
|
383
|
+
GA_CSP_IMG_HOSTS,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return serializeCsp(directives);
|
|
388
|
+
}
|
|
389
|
+
|
|
273
390
|
/**
|
|
274
391
|
* Apply a Content-Security-Policy header to HTML document responses.
|
|
275
392
|
*
|
|
@@ -282,20 +399,27 @@ function extractScriptBody(scriptTag: string | null): string | null {
|
|
|
282
399
|
* user-controlled content reaches the HTML).
|
|
283
400
|
*
|
|
284
401
|
* 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
|
-
*
|
|
402
|
+
* Report-Only` rather than enforced when the app has no existing document CSP.
|
|
403
|
+
* The framework injects deterministic inline scripts (the Sentry config block,
|
|
404
|
+
* whose hash is computed once at process startup from the resolved env vars,
|
|
405
|
+
* and — when `GA_MEASUREMENT_ID` is set — the gtag config block, whose hash is
|
|
406
|
+
* derived from the same string `wrapWithAnalytics` embeds). It also loads
|
|
407
|
+
* Google Tag Manager / GA4 from `GA_CSP_SCRIPT_HOSTS`. All of those are listed
|
|
408
|
+
* here so the report-only policy reflects the code the framework itself injects
|
|
409
|
+
* instead of reporting a violation on every page load.
|
|
410
|
+
*
|
|
411
|
+
* If an app or host already sends an enforced CSP with `script-src`,
|
|
412
|
+
* `script-src-elem`, `connect-src`, `img-src`, or `default-src`, we merge the
|
|
413
|
+
* framework's GA/GTM allowances into the existing directive. That keeps
|
|
414
|
+
* stricter deployments working without adding a new enforced script policy to
|
|
415
|
+
* routes that only declare unrelated directives such as `frame-ancestors`.
|
|
416
|
+
*
|
|
417
|
+
* Templates additionally render a theme-init inline script whose exact content
|
|
418
|
+
* varies by template (default theme param, custom docs variant, etc.) and which
|
|
419
|
+
* is rendered by React Router, not this handler, so its hash is not available
|
|
420
|
+
* here. Shipping script-src as Report-Only surfaces the remaining violations
|
|
421
|
+
* without breaking template customisations; teams can graduate to enforcement
|
|
422
|
+
* once their hashes are enumerated.
|
|
299
423
|
*
|
|
300
424
|
* Skipped in development (`NODE_ENV !== 'production'`) so HMR eval and Vite
|
|
301
425
|
* dev-server injects are never blocked. Set `AGENT_NATIVE_DISABLE_DOC_CSP=1`
|
|
@@ -305,16 +429,6 @@ function applyDocumentCsp(headers: Headers, sentryScript: string | null): void {
|
|
|
305
429
|
if (process.env.NODE_ENV !== "production") return;
|
|
306
430
|
if (process.env.AGENT_NATIVE_DISABLE_DOC_CSP === "1") return;
|
|
307
431
|
|
|
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
432
|
// script-src as Report-Only: list 'self', the framework-injected inline
|
|
319
433
|
// script hashes (Sentry config + gtag config), and the Google Analytics /
|
|
320
434
|
// Tag Manager loader hosts. These are exactly the scripts the framework
|
|
@@ -332,11 +446,33 @@ function applyDocumentCsp(headers: Headers, sentryScript: string | null): void {
|
|
|
332
446
|
...(gaHash ? [gaHash] : []),
|
|
333
447
|
...gaHosts,
|
|
334
448
|
];
|
|
335
|
-
const scriptSrc = `script-src ${scriptSrcTokens.join(" ")}`;
|
|
336
449
|
|
|
450
|
+
const cspAugmentOptions = {
|
|
451
|
+
scriptSrcTokens,
|
|
452
|
+
gaEnabled: Boolean(gaInlineBody),
|
|
453
|
+
};
|
|
454
|
+
const existing = headers.get("content-security-policy") ?? "";
|
|
455
|
+
if (!existing) {
|
|
456
|
+
headers.set(
|
|
457
|
+
"content-security-policy",
|
|
458
|
+
"object-src 'none'; base-uri 'self'",
|
|
459
|
+
);
|
|
460
|
+
} else {
|
|
461
|
+
headers.set(
|
|
462
|
+
"content-security-policy",
|
|
463
|
+
augmentExistingCspForFrameworkScripts(existing, cspAugmentOptions),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const scriptSrc = `script-src ${scriptSrcTokens.join(" ")}`;
|
|
337
468
|
const existingRo = headers.get("content-security-policy-report-only") ?? "";
|
|
338
469
|
if (!existingRo) {
|
|
339
470
|
headers.set("content-security-policy-report-only", scriptSrc);
|
|
471
|
+
} else {
|
|
472
|
+
headers.set(
|
|
473
|
+
"content-security-policy-report-only",
|
|
474
|
+
augmentExistingCspForFrameworkScripts(existingRo, cspAugmentOptions),
|
|
475
|
+
);
|
|
340
476
|
}
|
|
341
477
|
}
|
|
342
478
|
|
|
@@ -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
|