Skip to content

Commit 83432cd

Browse files
authored
fix(examples): align stylesheet and telemetry loading with CSP (#1095)
* fix(examples): align stylesheet and telemetry loading with CSP * fix(ci): redeploy examples when HTML preparation changes
1 parent 38f8e87 commit 83432cd

10 files changed

Lines changed: 79 additions & 9 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,7 +1082,7 @@ jobs:
10821082
10831083
changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
10841084
deploy_relevant=false
1085-
if printf '%s\n' "$changed_files" | grep -E '^(\.github/workflows/ci\.yml|runtime-parent-origins\.json|vercel\.(json|examples\.json)|apps/website/.*|cockpit/.*|examples/chat/.*|libs/.*|scripts/(assemble-examples|demo-middleware|generate-runtime-parent-origins|langgraph-proxy|rate-limit)\.ts|scripts/assemble-demo\.ts)$' >/dev/null; then
1085+
if printf '%s\n' "$changed_files" | grep -E '^(\.github/workflows/ci\.yml|runtime-parent-origins\.json|vercel\.(json|examples\.json)|apps/website/.*|cockpit/.*|examples/chat/.*|libs/.*|scripts/(assemble-examples|prepare-example-html|demo-middleware|generate-runtime-parent-origins|langgraph-proxy|rate-limit)\.ts|scripts/assemble-demo\.ts)$' >/dev/null; then
10861086
deploy_relevant=true
10871087
fi
10881088
@@ -1101,7 +1101,7 @@ jobs:
11011101
if printf '%s\n' "$changed_files" | grep -E '^cockpit/.*/angular/' >/dev/null; then
11021102
examples_changed=true
11031103
fi
1104-
if printf '%s\n' "$changed_files" | grep -E '^(runtime-parent-origins\.json|vercel\.examples\.json|scripts/(assemble-examples|examples-middleware|generate-runtime-parent-origins|langgraph-proxy|upstash-rate-limit)\.ts)$' >/dev/null; then
1104+
if printf '%s\n' "$changed_files" | grep -E '^(runtime-parent-origins\.json|vercel\.examples\.json|scripts/(assemble-examples|prepare-example-html|examples-middleware|generate-runtime-parent-origins|langgraph-proxy|upstash-rate-limit)\.ts)$' >/dev/null; then
11051105
examples_changed=true
11061106
fi
11071107
if printf '%s\n' "$changed_files" | grep -E '^(\.github/workflows/ci\.yml|vercel\.json|apps/website/.*)$' >/dev/null; then

apps/website/e2e/platform-production-smoke.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,11 @@ test.describe('Production: unified runtime embedding policy', () => {
200200
}
201201
}
202202
expect(policy).toContain(
203-
"connect-src 'self' https: http://localhost:* http://127.0.0.1:* http://[::1]:*"
203+
"connect-src 'self' https: http://localhost:* http://127.0.0.1:*"
204204
);
205+
expect(policy).not.toContain('http://[::1]:*');
206+
expect(policy).toContain("script-src 'self';");
207+
expect(await response.text()).not.toContain('onload="this.media=');
205208
expect(frameAncestors).not.toContain('*');
206209
expect(frameAncestors).not.toContain('cockpit.threadplane.ai');
207210
expect(headers['referrer-policy']).toBe('origin');

libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ describe('CockpitTelemetryService', () => {
8282
bootstrap: { distinctID: 'd1' },
8383
autocapture: false,
8484
capture_pageview: false,
85+
advanced_disable_flags: true,
86+
disable_surveys: true,
87+
disable_session_recording: true,
88+
disable_external_dependency_loading: true,
8589
}),
8690
);
8791
});

libs/cockpit-telemetry/src/lib/cockpit-telemetry.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ export class CockpitTelemetryService {
2424
bootstrap: { distinctID: this.config.distinctId },
2525
autocapture: false,
2626
capture_pageview: false,
27+
// This client only sends explicit lifecycle events. Optional downloaded
28+
// features are unnecessary and conflict with the embedded runtime CSP.
29+
advanced_disable_flags: true,
30+
disable_surveys: true,
31+
disable_session_recording: true,
32+
disable_external_dependency_loading: true,
2733
});
2834

2935
this.subscribeChat();

scripts/assemble-examples.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from 'fs';
2020
import { resolve } from 'path';
2121
import { capabilities as registryCapabilities } from '@threadplane/cockpit-registry';
22+
import { prepareExampleHtml } from './prepare-example-html';
2223
import {
2324
GENERATED_RUNTIME_PARENT_ORIGINS_MODULE,
2425
generateRuntimeParentOriginPolicy,
@@ -97,10 +98,7 @@ for (const cap of capabilities) {
9798
const indexPath = resolve(dest, 'index.html');
9899
if (existsSync(indexPath)) {
99100
const html = readFileSync(indexPath, 'utf-8');
100-
const fixed = html.replace(
101-
'<base href="/">',
102-
`<base href="/${cap.product}/${cap.topic}/">`
103-
);
101+
const fixed = prepareExampleHtml(html, cap.product, cap.topic);
104102
writeFileSync(indexPath, fixed);
105103
}
106104

scripts/ci-workflow.spec.mjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,19 @@ describe('CI workflow', () => {
247247
);
248248
});
249249

250+
it('redeploys examples when their HTML preparation changes', async () => {
251+
const deployJob = await readDeployJob();
252+
const path = 'scripts/prepare-example-html.ts';
253+
const preflight = deployJob.match(/grep -E '([^']+)' >\/dev\/null/);
254+
assert.match(path, new RegExp(preflight?.[1] ?? '(?!)'));
255+
const examplesDetection = deployJob.slice(
256+
deployJob.indexOf('Check if examples changed'),
257+
deployJob.indexOf('- uses: actions/setup-node')
258+
);
259+
assert.ok([...examplesDetection.matchAll(/grep -E '([^']+)'/g)]
260+
.some((match) => new RegExp(match[1]).test(path)));
261+
});
262+
250263
it('isolates langgraph coverage before the remaining bounded library tests', async () => {
251264
const libraryJob = await readLibraryJob();
252265

scripts/generate-runtime-parent-origins.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ describe('generate runtime parent origins', () => {
9696
'Referrer-Policy': 'origin',
9797
});
9898
expect(policy.deploymentHeaders['Content-Security-Policy']).toContain(
99-
"connect-src 'self' https: http://localhost:* http://127.0.0.1:* http://[::1]:*"
99+
"connect-src 'self' https: http://localhost:* http://127.0.0.1:*"
100100
);
101+
expect(policy.deploymentHeaders['Content-Security-Policy']).not.toContain('[::1]');
102+
expect(policy.deploymentHeaders['Content-Security-Policy']).toContain("script-src 'self';");
101103
expect(JSON.stringify(policy)).not.toMatch(
102104
/apiKey|authorization|secret|token/i
103105
);

scripts/generate-runtime-parent-origins.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const RUNTIME_CONNECT_SOURCES = [
88
'https:',
99
'http://localhost:*',
1010
'http://127.0.0.1:*',
11-
'http://[::1]:*',
1211
] as const;
1312

1413
export interface RuntimeParentOriginPolicy {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { prepareExampleHtml } from './prepare-example-html';
3+
4+
describe('assembled example HTML under script-src self', () => {
5+
it('activates Angular deferred CSS without executing an inline handler', () => {
6+
const html = `<base href="/"><link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.css"></noscript>`;
7+
const prepared = prepareExampleHtml(html, 'ag-ui', 'interrupts');
8+
expect(prepared).toContain('<base href="/ag-ui/interrupts/">');
9+
expect(prepared).toContain(
10+
'<link rel="stylesheet" href="styles.css" media="all">'
11+
);
12+
expect(prepared).not.toContain('onload=');
13+
expect(prepared).toContain(
14+
'<noscript><link rel="stylesheet" href="styles.css"></noscript>'
15+
);
16+
expect(prepareExampleHtml(prepared, 'ag-ui', 'interrupts')).toBe(prepared);
17+
});
18+
19+
it('preserves genuine print styles and script assets', () => {
20+
const html =
21+
'<link rel="stylesheet" href="print.css" media="print"><script src="main.js" type="module"></script>';
22+
expect(prepareExampleHtml(html, 'chat', 'threads')).toBe(html);
23+
});
24+
});

scripts/prepare-example-html.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/** Prepare Angular output for subpath hosting under script-src 'self'. */
2+
export function prepareExampleHtml(
3+
html: string,
4+
product: string,
5+
topic: string
6+
): string {
7+
return html
8+
.replace('<base href="/">', `<base href="/${product}/${topic}/">`)
9+
.replace(/<link\b[^>]*>/g, (tag) => {
10+
// Angular's critical-CSS optimization defers the full stylesheet with an
11+
// inline handler. Load it normally because the runtime CSP forbids handlers.
12+
if (
13+
!tag.includes('rel="stylesheet"') ||
14+
!tag.includes('onload="this.media=\'all\'"')
15+
)
16+
return tag;
17+
return tag
18+
.replace('media="print"', 'media="all"')
19+
.replace(' onload="this.media=\'all\'"', '');
20+
});
21+
}

0 commit comments

Comments
 (0)