Skip to content

Commit 262ed09

Browse files
committed
fix(examples): align stylesheet and telemetry loading with CSP
1 parent 38f8e87 commit 262ed09

8 files changed

Lines changed: 64 additions & 7 deletions

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/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)