diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 0149f6a..d233934 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -29,6 +29,8 @@ jobs:
with:
node-version: 20
cache: npm
+ - name: Check package.json file references
+ run: node scripts/check-referenced-files.mjs
- run: npm ci
- run: npm run lint
- run: npm run typecheck
diff --git a/README.md b/README.md
index edfc7bd..993ae99 100644
--- a/README.md
+++ b/README.md
@@ -77,13 +77,13 @@ could reach. Pin both with SRI so a bad CDN day fails closed instead of open:
```html
```
diff --git a/dist/fortify.cjs.js b/dist/fortify.cjs.js
index e044502..151edb8 100644
--- a/dist/fortify.cjs.js
+++ b/dist/fortify.cjs.js
@@ -1,4 +1,4 @@
-/*! DOMFortify 0.5.0 | (c) Cure53 and contributors | (MPL-2.0 OR Apache-2.0) */
+/*! DOMFortify 1.0.0 | (c) Cure53 and contributors | (MPL-2.0 OR Apache-2.0) */
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
@@ -82,7 +82,7 @@ function urlMatches(pattern, url) {
* - Fails closed: no sanitizer means sinks throw, never leak.
* - Only covers Trusted Types sinks; inline handlers / style / URL props stay open.
*/
-const VERSION = '0.5.0';
+const VERSION = '1.0.0';
// Natives captured up front, so later prototype pollution or clobbering can't swap them out.
const root = typeof globalThis !== 'undefined' ? globalThis : window;
const doc = typeof document !== 'undefined' ? document : undefined;
diff --git a/dist/fortify.es.mjs b/dist/fortify.es.mjs
index 30eae02..be24e7f 100644
--- a/dist/fortify.es.mjs
+++ b/dist/fortify.es.mjs
@@ -1,4 +1,4 @@
-/*! DOMFortify 0.5.0 | (c) Cure53 and contributors | (MPL-2.0 OR Apache-2.0) */
+/*! DOMFortify 1.0.0 | (c) Cure53 and contributors | (MPL-2.0 OR Apache-2.0) */
// Cached up front so later prototype pollution or clobbering can't swap hasOwnProperty out.
const hasOwn = Object.prototype.hasOwnProperty;
/** True only for an own (non-inherited) property, so a polluted prototype is never consulted. */
@@ -78,7 +78,7 @@ function urlMatches(pattern, url) {
* - Fails closed: no sanitizer means sinks throw, never leak.
* - Only covers Trusted Types sinks; inline handlers / style / URL props stay open.
*/
-const VERSION = '0.5.0';
+const VERSION = '1.0.0';
// Natives captured up front, so later prototype pollution or clobbering can't swap them out.
const root = typeof globalThis !== 'undefined' ? globalThis : window;
const doc = typeof document !== 'undefined' ? document : undefined;
diff --git a/dist/fortify.js b/dist/fortify.js
index 6aaff18..9c0522b 100644
--- a/dist/fortify.js
+++ b/dist/fortify.js
@@ -1,4 +1,4 @@
-/*! DOMFortify 0.5.0 | (c) Cure53 and contributors | (MPL-2.0 OR Apache-2.0) */
+/*! DOMFortify 1.0.0 | (c) Cure53 and contributors | (MPL-2.0 OR Apache-2.0) */
(function () {
'use strict';
@@ -81,7 +81,7 @@
* - Fails closed: no sanitizer means sinks throw, never leak.
* - Only covers Trusted Types sinks; inline handlers / style / URL props stay open.
*/
- const VERSION = '0.5.0';
+ const VERSION = '1.0.0';
// Natives captured up front, so later prototype pollution or clobbering can't swap them out.
const root = typeof globalThis !== 'undefined' ? globalThis : window;
const doc = typeof document !== 'undefined' ? document : undefined;
diff --git a/dist/fortify.min.js b/dist/fortify.min.js
index e8800b8..fd10706 100644
--- a/dist/fortify.min.js
+++ b/dist/fortify.min.js
@@ -1,3 +1,3 @@
-/*! DOMFortify 0.5.0 | (c) Cure53 and contributors | (MPL-2.0 OR Apache-2.0) */
-!function(){"use strict";const t=Object.prototype.hasOwnProperty;function e(e,n){return null!=e&&t.call(e,n)}function n(t,n){return e(t,n)?t[n]:void 0}function r(t){return String(t).slice(0,80)}function i(t){try{return String(t?.message)}catch{return"unknown error"}}function o(t,e){if(null==t)return!1;const n=Array.isArray(t)?t:[t];for(let t=0;t{if(!n)return r("sanitizer-unavailable",{sink:"createHTML"}),null;if(o)return c;try{return o=!0,t.sanitize(c,e)}catch(t){return r("sanitize-threw",{error:i(t)}),null}finally{o=!1}}}function p(t,e,n){return o=>{if(e){let r;try{r=e(o)}catch(e){return n("script-hook-threw",{sink:t,error:i(e)}),null}if("string"==typeof r)return n("script-sink-allowed",{sink:t}),r}return n("script-sink-refused",{sink:t,sample:r(o)}),null}}const h=Object.freeze({init:function(r={}){if(s)return f;s=!0;const h=n(r,"ON_VIOLATION"),O="function"==typeof h?(t,e)=>{try{h(t,e)}catch{}}:()=>{},v={version:"0.5.0",ttSupported:!!l,enforcementActive:!1,defaultPolicyOwned:!1,sanitizerReady:!1,excluded:!1,metaInjected:!1,protected:!1,reason:""},m=(t,e)=>(v.protected=v.defaultPolicyOwned&&v.enforcementActive&&v.sanitizerReady,v.reason=t,f=Object.freeze({...v}),e&&O(e,f),f);try{const s=a&&void 0!==a.href?String(a.href):"";if(o(n(r,"EXCLUDE"),s))return v.excluded=!0,m("URL matched EXCLUDE; DOMFortify is intentionally inactive on this page.","excluded-by-url");const f=n(r,"INCLUDE");if(null!=f&&!o(f,s))return v.excluded=!0,m("URL is outside INCLUDE scope; DOMFortify is intentionally inactive on this page.","outside-include-scope");if(!l||"function"!=typeof l.createPolicy)return m("Trusted Types not supported; library is inert. Sinks are NOT routed.","tt-unsupported");const h=function(t,e){const r=n(t,"URL_CONFIG");if(!Array.isArray(r))return null;for(let t=0;th&&e(h,t)?h[t]:n(r,t);if(!0===n(r,"INJECT_META")){const t=(T=n(r,"META_DIRECTIVE"),w="function"==typeof g("SANITIZER"),"string"==typeof T&&T?T:`require-trusted-types-for 'script'; trusted-types ${w?"default":"default dompurify"};`);v.metaInjected=function(t){if(!u)return!1;const e=u,n='\r\n]/g,"")+'">';if("loading"===e.readyState&&"function"==typeof e.write)try{return e.write(n),!0}catch{}try{const n=e.createElement("meta");n.setAttribute("http-equiv","Content-Security-Policy"),n.setAttribute("content",t),(e.head||e.documentElement).appendChild(n)}catch{}return!1}(t),O("meta-injection-attempted",{directive:t,written:v.metaInjected})}v.enforcementActive=function(){try{return u.createElement("div").innerHTML="x",!1}catch{return!0}}();let b=g("SANITIZER");void 0===b&&(b=c.DOMPurify);const L=d(b),A=g("SANITIZER_CONFIG"),I=A&&"object"==typeof A?function(e){const n={};for(const r in e)t.call(e,r)&&"__proto__"!==r&&"constructor"!==r&&"prototype"!==r&&(n[r]=e[r]);return n}(A):void 0,E=g("ALLOW_SCRIPT"),R=g("ALLOW_SCRIPT_URL"),k="function"==typeof E?E:null,P="function"==typeof R?R:null;let S=!1;if(L){const t=function(t,e){try{return"string"==typeof t.sanitize("x",e)?{ready:!0,error:null}:{ready:!1,error:"sanitize() did not return a string"}}catch(t){return{ready:!1,error:i(t)}}}(L,I);S=t.ready,t.ready||O("sanitizer-smoketest-failed",{error:t.error})}v.sanitizerReady=S;const z={createHTML:y(L,I,S,O),createScript:p("createScript",k,O),createScriptURL:p("createScriptURL",P,O)};if(l.defaultPolicy)return m("A default Trusted Types policy already exists; DOMFortify did NOT install and cannot vouch for it. Load DOMFortify first, inline in .","preexisting-default-policy");let D;try{D=l.createPolicy("default",z)}catch(t){return m(`createPolicy("default") threw (${i(t)}); another default policy won the race.`,"default-policy-lost")}return l.defaultPolicy&&l.defaultPolicy!==D?m('Our policy was created but is not the active default (allow-duplicates race lost). Remove "allow-duplicates" from the trusted-types directive.',"default-policy-not-active"):(v.defaultPolicyOwned=!0,v.enforcementActive?S?m(`Active: HTML sinks sanitized, script sinks ${k||P?"partly allowed by hooks":"refused"}.`):m("Enforcement active and slot locked, but the sanitizer is unavailable - HTML sinks will THROW (failing closed). Bundle DOMPurify and load it before DOMFortify.","failing-closed"):m("Default policy installed and slot locked, but TT enforcement is NOT active - sinks are not routed. Deliver require-trusted-types-for (header preferred).","enforcement-inactive"))}catch(t){return m(`init() hit an unexpected error (${i(t)}); failing closed.`,"failing-closed")}var T,w},status:function(){return f}});"undefined"!=typeof window&&(window.DOMFortify=h,h.init(window.DOMFortifyConfig||{}))}();
+/*! DOMFortify 1.0.0 | (c) Cure53 and contributors | (MPL-2.0 OR Apache-2.0) */
+!function(){"use strict";const t=Object.prototype.hasOwnProperty;function e(e,n){return null!=e&&t.call(e,n)}function n(t,n){return e(t,n)?t[n]:void 0}function r(t){return String(t).slice(0,80)}function i(t){try{return String(t?.message)}catch{return"unknown error"}}function o(t,e){if(null==t)return!1;const n=Array.isArray(t)?t:[t];for(let t=0;t{if(!n)return r("sanitizer-unavailable",{sink:"createHTML"}),null;if(o)return c;try{return o=!0,t.sanitize(c,e)}catch(t){return r("sanitize-threw",{error:i(t)}),null}finally{o=!1}}}function p(t,e,n){return o=>{if(e){let r;try{r=e(o)}catch(e){return n("script-hook-threw",{sink:t,error:i(e)}),null}if("string"==typeof r)return n("script-sink-allowed",{sink:t}),r}return n("script-sink-refused",{sink:t,sample:r(o)}),null}}const h=Object.freeze({init:function(r={}){if(s)return f;s=!0;const h=n(r,"ON_VIOLATION"),O="function"==typeof h?(t,e)=>{try{h(t,e)}catch{}}:()=>{},v={version:"1.0.0",ttSupported:!!l,enforcementActive:!1,defaultPolicyOwned:!1,sanitizerReady:!1,excluded:!1,metaInjected:!1,protected:!1,reason:""},m=(t,e)=>(v.protected=v.defaultPolicyOwned&&v.enforcementActive&&v.sanitizerReady,v.reason=t,f=Object.freeze({...v}),e&&O(e,f),f);try{const s=a&&void 0!==a.href?String(a.href):"";if(o(n(r,"EXCLUDE"),s))return v.excluded=!0,m("URL matched EXCLUDE; DOMFortify is intentionally inactive on this page.","excluded-by-url");const f=n(r,"INCLUDE");if(null!=f&&!o(f,s))return v.excluded=!0,m("URL is outside INCLUDE scope; DOMFortify is intentionally inactive on this page.","outside-include-scope");if(!l||"function"!=typeof l.createPolicy)return m("Trusted Types not supported; library is inert. Sinks are NOT routed.","tt-unsupported");const h=function(t,e){const r=n(t,"URL_CONFIG");if(!Array.isArray(r))return null;for(let t=0;th&&e(h,t)?h[t]:n(r,t);if(!0===n(r,"INJECT_META")){const t=(T=n(r,"META_DIRECTIVE"),w="function"==typeof g("SANITIZER"),"string"==typeof T&&T?T:`require-trusted-types-for 'script'; trusted-types ${w?"default":"default dompurify"};`);v.metaInjected=function(t){if(!u)return!1;const e=u,n='\r\n]/g,"")+'">';if("loading"===e.readyState&&"function"==typeof e.write)try{return e.write(n),!0}catch{}try{const n=e.createElement("meta");n.setAttribute("http-equiv","Content-Security-Policy"),n.setAttribute("content",t),(e.head||e.documentElement).appendChild(n)}catch{}return!1}(t),O("meta-injection-attempted",{directive:t,written:v.metaInjected})}v.enforcementActive=function(){try{return u.createElement("div").innerHTML="x",!1}catch{return!0}}();let b=g("SANITIZER");void 0===b&&(b=c.DOMPurify);const L=d(b),A=g("SANITIZER_CONFIG"),I=A&&"object"==typeof A?function(e){const n={};for(const r in e)t.call(e,r)&&"__proto__"!==r&&"constructor"!==r&&"prototype"!==r&&(n[r]=e[r]);return n}(A):void 0,E=g("ALLOW_SCRIPT"),R=g("ALLOW_SCRIPT_URL"),k="function"==typeof E?E:null,P="function"==typeof R?R:null;let S=!1;if(L){const t=function(t,e){try{return"string"==typeof t.sanitize("x",e)?{ready:!0,error:null}:{ready:!1,error:"sanitize() did not return a string"}}catch(t){return{ready:!1,error:i(t)}}}(L,I);S=t.ready,t.ready||O("sanitizer-smoketest-failed",{error:t.error})}v.sanitizerReady=S;const z={createHTML:y(L,I,S,O),createScript:p("createScript",k,O),createScriptURL:p("createScriptURL",P,O)};if(l.defaultPolicy)return m("A default Trusted Types policy already exists; DOMFortify did NOT install and cannot vouch for it. Load DOMFortify first, inline in .","preexisting-default-policy");let D;try{D=l.createPolicy("default",z)}catch(t){return m(`createPolicy("default") threw (${i(t)}); another default policy won the race.`,"default-policy-lost")}return l.defaultPolicy&&l.defaultPolicy!==D?m('Our policy was created but is not the active default (allow-duplicates race lost). Remove "allow-duplicates" from the trusted-types directive.',"default-policy-not-active"):(v.defaultPolicyOwned=!0,v.enforcementActive?S?m(`Active: HTML sinks sanitized, script sinks ${k||P?"partly allowed by hooks":"refused"}.`):m("Enforcement active and slot locked, but the sanitizer is unavailable - HTML sinks will THROW (failing closed). Bundle DOMPurify and load it before DOMFortify.","failing-closed"):m("Default policy installed and slot locked, but TT enforcement is NOT active - sinks are not routed. Deliver require-trusted-types-for (header preferred).","enforcement-inactive"))}catch(t){return m(`init() hit an unexpected error (${i(t)}); failing closed.`,"failing-closed")}var T,w},status:function(){return f}});"undefined"!=typeof window&&(window.DOMFortify=h,h.init(window.DOMFortifyConfig||{}))}();
//# sourceMappingURL=fortify.min.js.map
diff --git a/docs/maintaining.md b/docs/maintaining.md
index 30a22e6..d9d9164 100644
--- a/docs/maintaining.md
+++ b/docs/maintaining.md
@@ -17,8 +17,8 @@ Covers the Branch-Protection and Code-Review checks:
## Secrets
-- `NPM_TOKEN` - a granular npm automation token scoped to publish this package only. Keep 2FA on the
- npm account. Publishing runs from `publish.yml` via OIDC with `--provenance`.
+- npm publishing is done **manually** by a maintainer (`npm publish` from the release tag); there is
+ no publish workflow and no `NPM_TOKEN` repo secret. Keep 2FA on the npm account.
- `SCORECARD_TOKEN` - a fine-grained PAT so the Scorecard workflow can read branch-protection status
on a public repo. See the
[scorecard-action docs](https://github.com/ossf/scorecard-action#authentication-with-fine-grained-pat-optional).
@@ -33,6 +33,16 @@ Covers the Branch-Protection and Code-Review checks:
## Releasing
-1. Bump the version in `package.json` (the build injects it in place of `__VERSION__`).
-2. Create a GitHub Release / tag. `publish.yml` builds, tests, and publishes with provenance.
-3. Publish the SRI hashes for `dist/fortify.min.js` in the release notes so integrators can pin them.
+1. Bump the version in `package.json` (the build injects it in place of `__VERSION__`), sync the
+ lockfile (`npm install --package-lock-only`), run `npm run build`, and commit the rebuilt `dist`
+ - CI (`Verify committed dist is in sync with src`) fails otherwise. If DOMPurify shipped, bump
+ the `dompurify` devDependency and refresh the pinned version and SRI hash in the README first.
+2. Land that via PR, then create a GitHub Release / tag on the release commit. On publish,
+ `sign-release.yml` checks out the tag, rebuilds, and attaches Sigstore bundles, and
+ `slsa-provenance.yml` attests build provenance for the same bytes.
+3. Publish manually from a clean checkout of the tag: `npm ci && npm publish`
+ (`prepublishOnly` rebuilds `dist`; the build is reproducible, so the published bytes match the
+ committed and attested ones).
+4. Publish the SRI hashes for `dist/fortify.min.js` in the release notes so integrators can pin
+ them (`openssl dgst -sha384 -binary dist/fortify.min.js | openssl base64 -A`), and verify the
+ hash in the README's CDN snippet matches the published artifact.
diff --git a/package-lock.json b/package-lock.json
index 0e7c32e..5c8abd0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "domfortify",
- "version": "0.5.0",
+ "version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "domfortify",
- "version": "0.5.0",
+ "version": "1.0.0",
"license": "(MPL-2.0 OR Apache-2.0)",
"devDependencies": {
"@playwright/test": "^1.49.0",
@@ -14,7 +14,7 @@
"@rollup/plugin-terser": "^1.0.0",
"@rollup/plugin-typescript": "^12.1.1",
"angular": "1.8.3",
- "dompurify": "^3.4.11",
+ "dompurify": "^3.4.12",
"fast-check": "^4.8.0",
"jquery": "3.4.1",
"nyc": "^18.0.0",
@@ -1206,9 +1206,9 @@
}
},
"node_modules/dompurify": {
- "version": "3.4.11",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
- "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
+ "version": "3.4.12",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
+ "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
"dev": true,
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
diff --git a/package.json b/package.json
index 0374122..81614c0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "domfortify",
- "version": "0.5.0",
+ "version": "1.0.0",
"description": "Retrofit Trusted Types onto a legacy page: claim the realm's default policy so old DOM-XSS sinks get sanitized without touching the code.",
"license": "(MPL-2.0 OR Apache-2.0)",
"homepage": "https://github.com/cure53/DOMFortify",
@@ -59,6 +59,7 @@
"prebuild": "rimraf dist",
"build": "rollup -c config/rollup.config.mjs",
"typecheck": "tsc -p tsconfig.json --noEmit",
+ "check:files": "node scripts/check-referenced-files.mjs",
"lint": "prettier --check \"{src,test,config,scripts}/**/*.{ts,js,mjs}\"",
"format": "prettier --write \"{src,test,config,scripts}/**/*.{ts,js,mjs}\" \"**/*.md\"",
"test:node": "node test/node-runner.mjs",
@@ -75,7 +76,7 @@
"@rollup/plugin-terser": "^1.0.0",
"@rollup/plugin-typescript": "^12.1.1",
"angular": "1.8.3",
- "dompurify": "^3.4.11",
+ "dompurify": "^3.4.12",
"fast-check": "^4.8.0",
"jquery": "3.4.1",
"nyc": "^18.0.0",
diff --git a/scripts/check-referenced-files.mjs b/scripts/check-referenced-files.mjs
new file mode 100644
index 0000000..40f3575
--- /dev/null
+++ b/scripts/check-referenced-files.mjs
@@ -0,0 +1,53 @@
+#!/usr/bin/env node
+/**
+ * Guard: every in-repo file referenced by a package.json script must exist in the tree.
+ *
+ * Catches the 0.5.0 slip where build:cov and coverage referenced config/rollup.coverage.config.mjs
+ * and scripts/coverage.mjs, but those two files were never committed - so a clean checkout (CI
+ * included) had a broken `npm run coverage`. Pure Node built-ins, no dependencies.
+ *
+ * Narrow by design. It inspects script command strings only, and flags a token as a must-exist
+ * reference when it looks like a concrete source file: a relative path with a source extension,
+ * not a glob, and not under a built/output directory. That covers entry points passed to `node`,
+ * `rollup -c`, `tsc -p`, `playwright --config`, and the like, while deliberately ignoring rimraf
+ * targets (dist/, coverage/, .nyc_output), prettier globs, and flags. It does not parse `files`,
+ * `exports`, `bin`, or other packaging fields - those point at built output and have a better home
+ * in `publint` / `npm pack`, without this guard's build-order traps.
+ */
+import { existsSync, readFileSync } from 'node:fs';
+import { dirname, join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
+
+const SOURCE_EXT = /\.(?:mjs|cjs|js|ts|json)$/;
+const GLOB = /[*?{}[\]]/;
+const BUILT_OR_OUTPUT = /^(?:dist|coverage|node_modules|\.nyc_output)\//;
+
+const misses = [];
+for (const [name, command] of Object.entries(pkg.scripts ?? {})) {
+ for (let token of command.split(/\s+/)) {
+ // --flag=path keeps the path side; bare flags are dropped.
+ if (token.startsWith('-')) {
+ const eq = token.indexOf('=');
+ if (eq === -1) continue;
+ token = token.slice(eq + 1);
+ }
+ token = token.replace(/^['"]|['"]$/g, '');
+
+ if (!token || token.includes('://') || token.startsWith('/')) continue;
+ if (GLOB.test(token) || BUILT_OR_OUTPUT.test(token) || !SOURCE_EXT.test(token)) continue;
+
+ if (!existsSync(join(root, token))) misses.push({ name, token });
+ }
+}
+
+if (misses.length > 0) {
+ console.error('package.json references files that are not in the tree:');
+ for (const { name, token } of misses) console.error(` scripts.${name} -> ${token}`);
+ console.error('\nCommit the missing file(s), or remove the reference.');
+ process.exit(1);
+}
+
+console.log('check-referenced-files: all script-referenced source files exist.');
diff --git a/test/e2e/deployment.spec.ts b/test/e2e/deployment.spec.ts
index 0d6c058..9aba0fc 100644
--- a/test/e2e/deployment.spec.ts
+++ b/test/e2e/deployment.spec.ts
@@ -155,7 +155,7 @@ for (const v of VECTORS) {
});
}
-// --- Legacy-library regression: 0.4.0 must not break when heavy libraries are on the page ---------
+// --- Legacy-library regression: DOMFortify must not break when heavy libraries are on the page ----
// jQuery and AngularJS run internal innerHTML (and AngularJS Function/eval) as they load under
// enforcement, AFTER DOMFortify has claimed the default policy. The libraries themselves may not fully
// initialise under Trusted Types - AngularJS needs Function/eval, which DOMFortify refuses by design,