@cdmx/wappler_ag_grid 2.1.7 โ†’ 2.1.8

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/README.md CHANGED
@@ -2,6 +2,60 @@
2
2
 
3
3
  **Major Update:** This release upgrades AG Grid to v35.1.0, bringing continued performance improvements and new features.
4
4
 
5
+ ## ๐Ÿ”’ What's New in v2.1.8
6
+
7
+ **`style-src` without `'unsafe-inline'`, via a page nonce.** AG Grid v35 injects its theme CSS as
8
+ `<style>` elements at runtime, so any policy without `'unsafe-inline'` broke the grid. This release:
9
+
10
+ - ships `ag-grid-community.min.noStyle.js` under the existing `js/ag-grid-community.min.js` name.
11
+ The with-styles build injected 14 `<style>` blocks at script load that cannot take a nonce, and
12
+ AG Grid deleted them again the moment a grid initialised (they only serve legacy-theme mode, which
13
+ this module never uses). Nothing visible changes and existing `<script>` links keep working โ€”
14
+ update the module in each project and the file is overwritten.
15
+ - reads a page nonce and passes it to AG Grid (`styleNonce`) and to the module's own three
16
+ `<style>` blocks. The nonce is page-global, as AG Grid requires; it is taken from
17
+ `<meta name="csp-nonce" content="โ€ฆ">` or from any `<script nonce="โ€ฆ">` already on the page. Pages
18
+ without a nonce behave exactly as before.
19
+
20
+ To use it, generate a fresh random nonce **per response** (a fixed value is equivalent to
21
+ `'unsafe-inline'`) and put it in both places:
22
+
23
+ ```html
24
+ <meta name="csp-nonce" content="<nonce>"> <!-- layout <head> -->
25
+ ```
26
+ ```
27
+ Content-Security-Policy: style-src 'self' 'nonce-<nonce>'; img-src 'self' data:; font-src 'self' data:
28
+ ```
29
+
30
+ `img-src data:` covers AG Grid's SVG icons, `font-src data:` the icon fonts embedded in the theme
31
+ CSS; `script-src` needs nothing from the module (`'unsafe-eval'` only with `enable_cell_expressions`).
32
+
33
+ Server side:
34
+
35
+ - **Express / Wappler NodeJS** โ€” middleware registered before the page routes (in Wappler, a custom
36
+ routes file under `extensions/server_connect/routes/` receives the Express `app`):
37
+ ```js
38
+ const crypto = require('crypto');
39
+ app.use((req, res, next) => {
40
+ res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
41
+ res.setHeader('Content-Security-Policy',
42
+ `style-src 'self' 'nonce-${res.locals.cspNonce}'; img-src 'self' data:; font-src 'self' data:`);
43
+ next();
44
+ });
45
+ ```
46
+ and `<meta name="csp-nonce" content="<%= cspNonce %>">` in the EJS layout.
47
+ - **nginx** โ€” `set $csp_nonce $request_id;`, `add_header Content-Security-Policy "style-src 'self'
48
+ 'nonce-$csp_nonce'; img-src 'self' data:; font-src 'self' data:" always;`, and inject it into the
49
+ HTML with `sub_filter '__CSP_NONCE__' $csp_nonce; sub_filter_once off;` against
50
+ `<meta name="csp-nonce" content="__CSP_NONCE__">` in the layout (`sub_filter` needs uncompressed
51
+ upstream HTML: `proxy_set_header Accept-Encoding "";`).
52
+ - **PHP** โ€” `$nonce = base64_encode(random_bytes(16));` then `header("Content-Security-Policy: โ€ฆ
53
+ 'nonce-$nonce' โ€ฆ");` and `<meta name="csp-nonce" content="<?= $nonce ?>">`.
54
+
55
+ Regression coverage: `tests/15-csp.html` is served under `style-src 'self' 'nonce-csp15'` with no
56
+ `'unsafe-inline'` and asserts zero violations, every `<style>` nonced, and the layout,
57
+ `vert_center_cell_data` and `fixed_horizontal_scroll` styles applied.
58
+
5
59
  ## ๐Ÿ”’ What's New in v2.1.7
6
60
 
7
61
  **Content Security Policy compliance.** The status toggle column (rendered when a grid has a
@@ -15,22 +69,29 @@ The renderer now emits `data-status-col` / `data-status-value` / `data-status-id
15
69
  a single delegated `click` listener on the grid container dispatches the events. Behaviour is
16
70
  unchanged; no configuration or view changes are required.
17
71
 
18
- - **No inline event handlers** are emitted anywhere in the grid, so `script-src` needs neither
19
- `'unsafe-inline'` nor per-row hashes.
20
- - The three runtime `<style>` blocks (cell centering, sticky horizontal scrollbar, pagination
21
- layout) are static and unchanged, and can be allowed by hash in `style-src`:
22
- ```
23
- 'sha256-rW177u2hd7RHx4qSlJmD1nI6607/SZ01lrewgfHuj/M=' cell centering
24
- 'sha256-Et0Yl8993s0IRJOeeDT++DYDRSXLRDRvbgCj9zG2J9A=' sticky horizontal scrollbar
25
- 'sha256-6oQ3cIavZD3cp6igHrAN3HjrosWY41/7+bYLY+je9F0=' pagination panel
26
- ```
27
- These must be recomputed if that CSS text is ever edited.
72
+ - **No inline event handlers** on any documented rendering path, so `script-src` needs neither
73
+ `'unsafe-inline'` nor per-row hashes. The one exception is the legacy `clickCellRenderer`, which
74
+ is only reachable by passing raw AG Grid column definitions through `column_defs` with
75
+ `cellRenderer: 'clickCellRenderer'`; it still emits `onclick=`/`style=` and cannot be used under
76
+ a strict policy.
28
77
  - Toggle values are now attribute-escaped rather than interpolated into a JavaScript string
29
78
  literal, closing a quote-breakout in the previous renderer.
30
79
 
31
80
  Regression coverage lives in `tests/04-selection.html` (`window.__statusCheck`), which asserts the
32
81
  grid emits zero `[onclick]` attributes and that the toggle still dispatches its events.
33
82
 
83
+ ### Alternative without a nonce: hash allow-list
84
+
85
+ If a per-request nonce is not possible on your stack, every `<style>` block AG Grid and this module
86
+ inject can be allowed by `'sha256-โ€ฆ'` hash instead. `node tests/csp-hashes.cjs` harvests them from
87
+ the test scenarios and prints the `style-src` directive (helmet array form too); `--verify` reloads
88
+ every scenario under that header and exits 1 on any violation. Caveats, which are why the nonce is
89
+ the recommended route: the hashes are bound to the exact bytes of `ag-grid-community.min.js` and
90
+ `dmx-ag-grid.js` (regenerate after **any** upgrade of either), AG Grid injects component CSS lazily
91
+ so a feature the scenarios don't exercise can need one more hash (that component renders unstyled and
92
+ the console names the hash), and hashes cover `<style>` elements only โ€” inline `style="โ€ฆ"` attributes
93
+ in your own pages still need `style-src-attr` handling.
94
+
34
95
  ## ๐Ÿš€ What's New in v2.1.1
35
96
 
36
97
  1. **๐Ÿ”ฅ AG Grid v35.1.0** - Latest version with all community features
@@ -3862,7 +3862,7 @@
3862
3862
  "allowed_children": {},
3863
3863
  "copyFiles": [
3864
3864
  {
3865
- "src": "../../../node_modules/ag-grid-community/dist/ag-grid-community.min.js",
3865
+ "src": "../../../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js",
3866
3866
  "dst": "js/ag-grid-community.min.js"
3867
3867
  },
3868
3868
  {
package/dmx-ag-grid.js CHANGED
@@ -621,6 +621,10 @@ dmx.Component('ag-grid', {
621
621
 
622
622
  const gridThemeClass = options.dark_mode ? `${options.grid_theme}-dark` : options.grid_theme;
623
623
  this.$node.innerHTML = `<div id=${options.id}-grid class="${gridThemeClass}"></div>`;
624
+ // CSP: nonce for every <style> AG Grid and this module inject. Page-global, read from
625
+ // <meta name="csp-nonce" content="โ€ฆ"> or any nonce'd <script>; undefined when the page has none.
626
+ const cspNonce = (document.querySelector('meta[name="csp-nonce"]') || {}).content
627
+ || (document.querySelector('script[nonce]') || {}).nonce || undefined;
624
628
 
625
629
  // Apply theme mode using data attribute
626
630
  const themeContainer = this.$node.querySelector(`#${options.id}-grid`);
@@ -1823,6 +1827,7 @@ dmx.Component('ag-grid', {
1823
1827
  localeText = AG_GRID_LOCALE_ES
1824
1828
  }
1825
1829
  const gridOptions = {
1830
+ styleNonce: cspNonce,
1826
1831
  ...(idFieldPresent ? { getRowId: params => String(params.data.id) } : {}),
1827
1832
  columnDefs: (groupedColumnDefs && groupedColumnDefs.length > 0) ? groupedColumnDefs : columnDefs,
1828
1833
  getRowStyle: options.rstyles ? createRowStyleFunction(options.rstyles): undefined,
@@ -2153,6 +2158,7 @@ dmx.Component('ag-grid', {
2153
2158
  const gridElement = document.getElementById(options.id+'-grid');
2154
2159
  if (options.vert_center_cell_data) {
2155
2160
  const styleElement = document.createElement('style');
2161
+ if (cspNonce) styleElement.setAttribute('nonce', cspNonce);
2156
2162
  if (options.vert_center_cell_data) {
2157
2163
  styleElement.textContent += `
2158
2164
  .ag-cell {
@@ -2250,6 +2256,7 @@ dmx.Component('ag-grid', {
2250
2256
  if (options.fixed_horizontal_scroll) {
2251
2257
  const styleElement = document.createElement('style');
2252
2258
  styleElement.id = 'hovering-bar-style';
2259
+ if (cspNonce) styleElement.setAttribute('nonce', cspNonce);
2253
2260
  const agRootWrapper = gridElement.querySelector('.ag-root-wrapper');
2254
2261
  const bodyHorizontalScrollElement = gridElement.querySelector('.ag-body-horizontal-scroll');
2255
2262
  const rootWrapperWidth = agRootWrapper.clientWidth;
@@ -2961,6 +2968,7 @@ dmx.Component('ag-grid', {
2961
2968
  `;
2962
2969
 
2963
2970
  const paginationPanelStyle = document.createElement('style');
2971
+ if (cspNonce) paginationPanelStyle.setAttribute('nonce', cspNonce);
2964
2972
  paginationPanelStyle.innerHTML = paginationPanelCss;
2965
2973
  document.head.appendChild(paginationPanelStyle);
2966
2974
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cdmx/wappler_ag_grid",
3
- "version": "2.1.7",
3
+ "version": "2.1.8",
4
4
  "type": "module",
5
5
  "description": "App Connect module for AG Grid v35 - Advanced data grid with enhanced editing, filtering, and tree data capabilities.",
6
6
  "license": "MIT",
@@ -14,7 +14,7 @@
14
14
  <script src="./lib/version-banner.js" defer></script>
15
15
  <script src="./lib/test-helpers.js" defer></script>
16
16
  <script src="./lib/page-template.js" defer></script>
17
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
17
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
18
18
  <script src="../dmx-ag-grid.js" defer></script>
19
19
  <script src="./data/sample-data.js" defer></script>
20
20
  </head>
@@ -17,7 +17,7 @@
17
17
  <script src="./lib/version-banner.js" defer></script>
18
18
  <script src="./lib/test-helpers.js" defer></script>
19
19
  <script src="./lib/page-template.js" defer></script>
20
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
20
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
21
21
  <script src="../node_modules/@ag-grid-community/locale/dist/umd/@ag-grid-community/locale.min.js" defer></script>
22
22
  <script src="../ru.js" defer></script>
23
23
  <script src="../dmx-ag-grid.js" defer></script>
@@ -12,7 +12,7 @@
12
12
  <script src="./lib/version-banner.js" defer></script>
13
13
  <script src="./lib/test-helpers.js" defer></script>
14
14
  <script src="./lib/page-template.js" defer></script>
15
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
15
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
16
16
  <script src="../dmx-ag-grid.js" defer></script>
17
17
  <script src="./data/sample-data.js" defer></script>
18
18
  </head>
@@ -11,7 +11,7 @@
11
11
  <script src="./lib/version-banner.js" defer></script>
12
12
  <script src="./lib/test-helpers.js" defer></script>
13
13
  <script src="./lib/page-template.js" defer></script>
14
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
14
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
15
15
  <script src="../dmx-ag-grid.js" defer></script>
16
16
  <script src="./data/sample-data.js" defer></script>
17
17
  </head>
@@ -12,7 +12,7 @@
12
12
  <script src="./lib/version-banner.js" defer></script>
13
13
  <script src="./lib/test-helpers.js" defer></script>
14
14
  <script src="./lib/page-template.js" defer></script>
15
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
15
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
16
16
  <script src="../dmx-ag-grid.js" defer></script>
17
17
  <script src="./data/sample-data.js" defer></script>
18
18
  </head>
@@ -11,7 +11,7 @@
11
11
  <script src="./lib/version-banner.js" defer></script>
12
12
  <script src="./lib/test-helpers.js" defer></script>
13
13
  <script src="./lib/page-template.js" defer></script>
14
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
14
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
15
15
  <script src="../node_modules/pdfmake/build/pdfmake.min.js" defer></script>
16
16
  <script src="../node_modules/pdfmake/build/vfs_fonts.js" defer></script>
17
17
  <script src="../node_modules/exceljs/dist/exceljs.min.js" defer></script>
@@ -11,7 +11,7 @@
11
11
  <script src="./lib/version-banner.js" defer></script>
12
12
  <script src="./lib/test-helpers.js" defer></script>
13
13
  <script src="./lib/page-template.js" defer></script>
14
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
14
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
15
15
  <script src="../node_modules/papaparse/papaparse.min.js" defer></script>
16
16
  <script src="../node_modules/read-excel-file/bundle/read-excel-file.min.js" defer></script>
17
17
  <script src="../dmx-ag-grid.js" defer></script>
@@ -11,7 +11,7 @@
11
11
  <script src="./lib/version-banner.js" defer></script>
12
12
  <script src="./lib/test-helpers.js" defer></script>
13
13
  <script src="./lib/page-template.js" defer></script>
14
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
14
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
15
15
  <script src="../dmx-ag-grid.js" defer></script>
16
16
  <script src="./data/sample-data.js" defer></script>
17
17
  </head>
@@ -11,7 +11,7 @@
11
11
  <script src="./lib/version-banner.js" defer></script>
12
12
  <script src="./lib/test-helpers.js" defer></script>
13
13
  <script src="./lib/page-template.js" defer></script>
14
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
14
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
15
15
  <script src="../dmx-ag-grid.js" defer></script>
16
16
  <script src="./data/sample-data.js" defer></script>
17
17
  </head>
@@ -11,7 +11,7 @@
11
11
  <script src="./lib/version-banner.js" defer></script>
12
12
  <script src="./lib/test-helpers.js" defer></script>
13
13
  <script src="./lib/page-template.js" defer></script>
14
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
14
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
15
15
  <script src="../dmx-ag-grid.js" defer></script>
16
16
  <script src="./data/sample-data.js" defer></script>
17
17
  </head>
@@ -11,7 +11,7 @@
11
11
  <script src="./lib/version-banner.js" defer></script>
12
12
  <script src="./lib/test-helpers.js" defer></script>
13
13
  <script src="./lib/page-template.js" defer></script>
14
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
14
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
15
15
  <script src="../dmx-ag-grid.js" defer></script>
16
16
  <script src="./data/sample-data.js" defer></script>
17
17
  <style>
@@ -11,7 +11,7 @@
11
11
  <script src="./lib/version-banner.js" defer></script>
12
12
  <script src="./lib/test-helpers.js" defer></script>
13
13
  <script src="./lib/page-template.js" defer></script>
14
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
14
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
15
15
  <script src="../dmx-ag-grid.js" defer></script>
16
16
  <script src="./data/sample-data.js" defer></script>
17
17
  </head>
@@ -11,7 +11,7 @@
11
11
  <script src="./lib/version-banner.js" defer></script>
12
12
  <script src="./lib/test-helpers.js" defer></script>
13
13
  <script src="./lib/page-template.js" defer></script>
14
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
14
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
15
15
  <script src="../dmx-ag-grid.js" defer></script>
16
16
  <script src="./data/sample-data.js" defer></script>
17
17
 
@@ -311,6 +311,10 @@
311
311
  if (!employees.length) return setTimeout(runChecks, 200);
312
312
  // Wait an extra cycle so the legacy-form re-render lands before we count rows.
313
313
  if (!document.querySelector('#rsLegacy-grid .ag-row [col-id]')) return setTimeout(runChecks, 200);
314
+ // cellStyle lands a tick after the rows: wait for a styled salary cell (capped so a real
315
+ // regression still surfaces as a failed count rather than a page that never publishes).
316
+ runChecks.tries = (runChecks.tries || 0) + 1;
317
+ if (runChecks.tries < 25 && !document.querySelector('#csCell-grid [col-id="salary"][style*="color"]')) return setTimeout(runChecks, 200);
314
318
 
315
319
  // Expected hand-counts derived from sample-data.js
316
320
  const vipRows = employees.filter(r => r.rating >= 4.7 && r.active === true).length;
@@ -12,7 +12,7 @@
12
12
  <script src="./lib/version-banner.js" defer></script>
13
13
  <script src="./lib/test-helpers.js" defer></script>
14
14
  <script src="./lib/page-template.js" defer></script>
15
- <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.js" defer></script>
15
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
16
16
  <script src="../dmx-ag-grid.js" defer></script>
17
17
  <script src="./data/sample-data.js" defer></script>
18
18
  </head>
@@ -0,0 +1,98 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <!-- Strict policy: no 'unsafe-inline' anywhere. Every <style> AG Grid or the module injects must carry the nonce. -->
6
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'nonce-csp15'; style-src 'self' 'nonce-csp15'; img-src 'self' data:; font-src 'self' data:">
7
+ <meta name="csp-nonce" content="csp15">
8
+ <title>15 ยท CSP nonce</title>
9
+ <script nonce="csp15">
10
+ // Must be installed before any deferred script runs so every violation is counted.
11
+ window.__cspViolations = [];
12
+ document.addEventListener('securitypolicyviolation', e => window.__cspViolations.push(e.violatedDirective + ' ' + (e.sourceFile || '').split('/').pop() + ':' + e.lineNumber));
13
+ </script>
14
+ <link rel="stylesheet" href="./lib/test-suite.css">
15
+ <link rel="stylesheet" href="../node_modules/ag-grid-community/styles/ag-theme-alpine.css">
16
+ <link rel="stylesheet" href="../switch-toggle-slider.css">
17
+ <script src="./dmxAppConnect/dmxAppConnect.js" defer></script>
18
+ <script src="./lib/version-banner.js" defer></script>
19
+ <script src="./lib/test-helpers.js" defer></script>
20
+ <script src="./lib/page-template.js" defer></script>
21
+ <script src="../node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js" defer></script>
22
+ <script src="../dmx-ag-grid.js" defer></script>
23
+ <script src="./data/sample-data.js" defer></script>
24
+ </head>
25
+ <body is="dmx-app" id="page15">
26
+
27
+ <script nonce="csp15">
28
+ window.addEventListener('DOMContentLoaded', () => {
29
+ TestPage.header({
30
+ title: '15 ยท CSP nonce',
31
+ summary: "Page served under style-src 'self' 'nonce-csp15' with no 'unsafe-inline'. The module reads the nonce from &lt;meta name=\"csp-nonce\"&gt; and passes it to AG Grid (styleNonce) and its own &lt;style&gt; blocks."
32
+ });
33
+ });
34
+ </script>
35
+
36
+ <main class="test-main">
37
+ <dmx-test-data id="rows" source="employees"></dmx-test-data>
38
+ <dmx-test-data id="statusData" source="statusRows"></dmx-test-data>
39
+
40
+ <section class="feature">
41
+ <h2>Grid under a nonce-only style-src</h2>
42
+ <p class="desc">
43
+ Pagination, <code>vert_center_cell_data</code>, <code>fixed_horizontal_scroll</code> and the status toggle โ€”
44
+ every path where the module itself creates a <code>&lt;style&gt;</code>. The puppeteer runner reads
45
+ <code>window.__cspCheck</code>.
46
+ </p>
47
+ <div class="grid-wrap">
48
+ <dmx-ag-grid
49
+ id="cspGrid"
50
+ dmx-bind:data="rows.value"
51
+ grid_theme="ag-theme-alpine"
52
+ dmx-bind:pagination="true"
53
+ dmx-bind:pagination_page_size="10"
54
+ dmx-bind:vert_center_cell_data="true"
55
+ dmx-bind:fixed_horizontal_scroll="true">
56
+ </dmx-ag-grid>
57
+ </div>
58
+ <div class="grid-wrap">
59
+ <dmx-ag-grid id="statusGrid" dmx-bind:data="statusData.value" grid_theme="ag-theme-alpine" dmx-bind:row_status_event="true" dmx-bind:pagination="false"></dmx-ag-grid>
60
+ </div>
61
+ <pre class="log" id="csp-check"></pre>
62
+ </section>
63
+ </main>
64
+
65
+ <script nonce="csp15">
66
+ function runCspChecks(tries) {
67
+ tries = tries || 0;
68
+ if (document.body.dataset.gridsReady !== 'true') {
69
+ if (tries > 50) return publish({ grids_ready: { expect: true, actual: false } });
70
+ return setTimeout(() => runCspChecks(tries + 1), 200);
71
+ }
72
+ setTimeout(() => {
73
+ const root = document.querySelector('#cspGrid-grid');
74
+ const wrap = root && root.querySelector('.ag-root-wrapper');
75
+ const cell = root && root.querySelector('.ag-cell');
76
+ const styles = [...document.querySelectorAll('style')];
77
+ publish({
78
+ csp_violations: { expect: 0, actual: window.__cspViolations.length },
79
+ style_elements_seen: { expect: true, actual: styles.length > 0 },
80
+ all_styles_nonced: { expect: 0, actual: styles.filter(s => s.nonce !== 'csp15').length },
81
+ layout_intact: { expect: 'flex', actual: wrap ? getComputedStyle(wrap).display : 'no-wrapper' },
82
+ vcenter_applied: { expect: 'flex', actual: cell ? getComputedStyle(cell).display : 'no-cell' },
83
+ hscroll_fixed: { expect: 'fixed', actual: (el => el ? getComputedStyle(el).position : 'none')(root && root.querySelector('.ag-body-horizontal-scroll')) }
84
+ });
85
+ if (window.__cspViolations.length) console.warn('CSP violations:', window.__cspViolations);
86
+ }, 400);
87
+ }
88
+ function publish(checks) {
89
+ const failed = Object.entries(checks).filter(([_, v]) => v.actual !== v.expect);
90
+ window.__cspCheck = { checks, ok: failed.length === 0, failed: failed.map(([k]) => k) };
91
+ document.getElementById('csp-check').textContent = Object.entries(checks)
92
+ .map(([k, v]) => (v.actual === v.expect ? 'โœ“' : 'โœ—') + ' ' + k + ' โ€” expected ' + v.expect + ', got ' + v.actual)
93
+ .join('\n');
94
+ }
95
+ runCspChecks();
96
+ </script>
97
+ </body>
98
+ </html>