build(licenses): every build carries its third-party notices, checked against a committed manifest - #318
Merged
Conversation
… against a committed manifest Part of #301, the first of its two pull requests. No interface changes; the screen that shows the notices comes in the second. - Every build now ships the notices: THIRD_PARTY_NOTICES.txt in the web and PWA builds (the PWA precaches it), and the same text inside the portable file's HTML, in <template id="third-party-notices">. Each shipped component appears with its version, SPDX licence, copyright lines and the full text of every licence and NOTICE file; a text that several components share is printed once. The header keeps "Copyright © 2026 Hanrim. All rights reserved." and calls the list "Third-party open-source licenses"; Loop Studio itself is not described as open source. - scripts/third-party-notices/vite-plugin.mjs gathers what ships from the build itself: every chunk's module ids (read before the portable build inlines them), Vite's own build.license list as evidence (then removed from the output, since it misses fonts referenced from CSS and is empty in the single-file build), the @fontsource packages our CSS names, the emitted font files' origins, and the virtual modules of the Vite and Rolldown runtime. - The portable copy is text, never markup: `&`, `<` and `>` are escaped, so no licence text can close the template or add an element; the writer refuses U+0000 and CR, which an HTML parser would drop or rewrite; it is inserted before the last </body> by slicing, so `$&` in a text stays literal. Read back with `template.content.textContent`, it is byte-identical to the web build's THIRD_PARTY_NOTICES.txt. A unit test feeds the writer a text built to break out (`</template><script>`, an onerror image, a second template); an e2e test reads the real file's DOM against the manifest and parses the hostile text in Chromium: one template, one text node, nothing runs. core.mjs and licenses/README.md state the contract for the licence screen: render as text, never through innerHTML or dangerouslySetInnerHTML. - licenses/registry.json lists what the graph cannot show: the Vite runtime (its whole LICENSE.md, "MIT + bundled third-party notices"), the Rolldown runtime (its own MIT LICENSE and THIRD-PARTY-LICENSE, each pinned as its own input with what it covers), and the PWA service worker's six Workbox packages, idb, the workbox-build template and the Apache-2.0 loader of @trickfilm400/rollup-plugin-off-main-thread (no NOTICE file; its copyright line comes from the loader's header). It also holds that loader's licence review. - licenses/third-party-manifest.json pins each build: web and portable 28 components, PWA 37, with every licence file's SHA-256, NOTICE files (none today), copyright lines and the SHA-256 of the whole text. A build that ships anything else fails and prints the difference; `npm run licenses:update` rewrites the manifest offline from three real builds, for review. Two runs give byte-identical output. Neither the manifest nor the notices may hold a local absolute path; the build and the check fail if they do. - scripts/third-party-notices/core.mjs holds the rules, tested on made-up packages in scripts/third-party-notices.test.mjs (49 tests): only MIT, ISC, BSD-3-Clause and OFL-1.1 pass by themselves; Apache-2.0 only with a review entry; GPL-family licences never; UNKNOWN, a missing licence file, a missing copyright line, an unresolved registry item and an OR expression without a recorded choice fail; a new, removed or upgraded package, a changed licence text and an added NOTICE file each show as a difference. - scripts/check-third-party-notices.mjs runs after the builds in CI: the notices are in each output and match the manifest, the portable copy is escaped and pinned to the web file's bytes, Vite's licence JSON is not deployed, sw.js precaches the file, every service-worker item's marker is in sw.js or the Workbox chunk, and no unlisted Workbox package is. - Declared internal; the version stays 0.17.1.
Deploying cozy-loop-studio with
|
| Latest commit: |
b366461
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://9968c212.cozy-loop-studio.pages.dev |
| Branch Preview URL: | https://feat-third-party-notices.cozy-loop-studio.pages.dev |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #301 — the first of its two pull requests: the manifest, the generator, the notices in every build, and the build checks. No interface changes; the screen that shows the notices comes in the second.
What ships now
npm run build)THIRD_PARTY_NOTICES.txtnpm run build:portable)<template id="third-party-notices">npm run build:pwa, and the Cloudflare Production build)THIRD_PARTY_NOTICES.txt, precached by the service workerzustandships in two versions) plus the Vite and Rolldown runtime helpers. The two texts are byte-identical.idb, theworkbox-buildtemplate and the Apache-2.0 loader of@trickfilm400/rollup-plugin-off-main-thread.LICENSE.mdis included: its own MIT licence, then the licences of the code it bundles. Rolldown's own MITLICENSEand itsTHIRD-PARTY-LICENSEare separate inputs, each pinned by its own SHA-256 and labelled with what it covers.LICENSEorlicensefield is added.How the list is made
scripts/third-party-notices/vite-plugin.mjsreads the build itself: every chunk's module ids (before the portable build inlines its chunks), the@fontsourcepackages our CSS names, the emitted font files' origins, and the virtual modules of the Vite and Rolldown runtime.build.licenselist is used as evidence and then removed from the output. On its own it is not enough: it misses@fontsource/ibm-plex-sans-thai(whose files our CSS names directly), it cannot see the service worker, and it is empty in the single-file portable build.licenses/registry.jsonlists what the module graph cannot show, with the evidence that proves each item is in the output, and the review of the one Apache-2.0 component. It has no NOTICE file; its copyright line comes from the loader's own header.node_modules, the lockfile-installed packages and the repository. Two runs ofnpm run licenses:updategave a byte-identical manifest.The portable copy is text, never markup
&,<and>are escaped, so the template's content holds no<: whatever a licence text says, it cannot close the template, open a<script>or add any element. The writer refuses U+0000 and CR, which an HTML parser would drop or rewrite, and inserts the template before the last</body>by slicing, so a$&in a text stays literal.template.content.textContent, the DOM gives back the exact bytes of the web build'sTHIRD_PARTY_NOTICES.txt; the manifest pins the same SHA-256 for both builds.</template><script>…, anonerrorimage, a second template,$&). A new test ine2e/portable-file.spec.tsreads the real file's DOM against the manifest, then parses that hostile text in Chromium: one template, one text node, nothing runs.core.mjsandlicenses/README.mdfix the contract for the licence screen in the second pull request: the notices are rendered as text, never throughinnerHTMLordangerouslySetInnerHTML.What fails the build
licenses/third-party-manifest.jsonpins each build: components, versions, SPDX, every licence and NOTICE file with its SHA-256, copyright lines, and the SHA-256 of the whole text. A build that ships anything else fails and prints the difference. The rules, inscripts/third-party-notices/core.mjsand tested inscripts/third-party-notices.test.mjs(49 tests):UNKNOWNor a missing licence, a missing licence file, a missing copyright line, anORexpression without a recorded choice, and a registry item marked unresolved fail;After the builds,
npm run check:third-party-notices(a new CI step, and on the Cloudflare Production path) checks that the notices are in each output and match the manifest, that the portable copy is escaped and pinned to the web file's bytes, that Vite's licence list is not deployed, thatsw.jsprecaches the file, that every service-worker item's marker is insw.jsor the Workbox chunk, and that no unlisted Workbox package ships.Verification (local)
npx tsc -b, oxlint (39 warnings, the existing baseline), 3,077 unit tests and all check scripts pass.CF_PAGES_BRANCH=main) build, andcheck:third-party-notices,check:pwa-closureandcheck:no-pwapass.<, a stray&, an altered text, no template or two templates each failed the post-build check. In Chromium, the hostile text left unescaped built a<script>and an<img>and ran; escaped, it built nothing.Not in this pull request