Repository navigation
ci: publish to npm with provenance attestation - #223
Merged
Merged
Conversation
Add id-token: write (plus explicit contents: read for actions/checkout) to the publish-npm job and pass --provenance to npm publish, so releases carry a signed SLSA build attestation linking the tarball to this workflow run.
TurboDocx DOCX Diff Report
Summary
🚀 Powered by TurboDocx | html-to-docxAutomated DOCX regression testing • Catch document generation bugs before they ship • 100% open source Generated by TurboDocx DOCX Diff workflow • Learn more |
nicolasiscoding
marked this pull request as ready for review
August 10, 2026 11:40
nicolasiscoding
added a commit
that referenced
this pull request
Sep 10, 2026
* ci: bump JS actions to Node-24-bundled versions * docs: refresh agent-skill callout to match TurboDocx SDK READMEs Bring the README's quickstart-skill section in line with the pattern now used across the TurboDocx SDK READMEs: - Rename the 'Agent Skill' badge to 'Agent Skills' (matches the agentskills.io brand name) and bump its accent purple to 8A2BE2 - Rename the 'skills.sh' badge to 'Quickstart Skill' and point the click-through at the canonical source repo (github.com/TurboDocx/ quickstart) instead of skills.sh's profile page - Replace the 'Install via AI Agent Skill' heading with the 'Skip the boilerplate' tagline used on the SDK READMEs - Tighten the intro copy and keep the 4-bullet 'The skill will' explanation that's specific to this package * feat: zero-config browser imports (browser ESM build + exports) + Next.js example (#205) * feat(browser): add browser ESM build + exports map for zero-config bundler imports Bundlers (Next.js/Turbopack, Vite, webpack) resolved the package browser field to the IIFE build, which exposes no ES module default export, so import HTMLtoDOCX from '@turbodocx/html-to-docx' failed with 'not a function' (Next) / 'does not provide an export named default' (Vite, issues #203 #179). Add dist/html-to-docx.browser.esm.js (node polyfills bundled, sharp stubbed, real default export) and an exports map: browser condition points bundlers at it, node condition pins server/edge to esm/umd, script/CDN keep the IIFE. All 618 unit tests pass. * docs(example): add zero-config Next.js browser example Minimal Next.js 16 App Router (TypeScript) app that generates a .docx entirely in the browser via a plain import of @turbodocx/html-to-docx. Browser-verified end to end (produces a valid ~25KB .docx). * fix(browser): exclude sharp from browser ESM build The browser ESM build relied solely on a resolveId stub to drop the optional native `sharp` dependency, but sharp is loaded via `require('sharp')` inside a try/catch in an ES module. @rollup/plugin-commonjs leaves try/catch requires in mixed ES modules untransformed, so the stub never fired and a bare `require('sharp')` survived into dist/html-to-docx.browser.esm.js. Downstream bundlers (Next.js/webpack) then statically resolved it and pulled in sharp's Node-native deps (detect-libc -> child_process), breaking the browser build. Add a transform hook that neutralizes the require in first-party source so no sharp specifier remains for any consumer to follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(example): add tabbed simple/complex examples Add a "Complex" tab to the Next.js example alongside the existing simple demo. The complex document showcases nested lists, a table, an inline base64 image, a code block, the full inline-formatting pipeline (bold/italic/underline/ strikethrough/code/links/sup/sub), and color, font-size, and font-family variations. Example HTML moves into app/examples.ts to keep the component readable; the component gains a tab bar that loads each example into the editable textarea. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Nicolas <nicolas@turbodocx.com> Co-authored-by: Kushal <kushalkumargupta4@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * fix(security): close runtime Dependabot advisories (form-data, postcss) (#216) Same runtime-security set as the reverted main PR (#214), now targeting develop. - form-data (high, GHSA-hmw2-7cc7-3qxx): 4.0.5 -> 4.0.6 via root overrides. - postcss (medium, <8.5.10): 8.4.31 -> 8.5.19 in the nextjs-example lockfile via a scoped override. Both transitive infra libs, not imported by src; semver-safe version-only bumps. Dev-scope alerts (js-yaml x2, @babel/core) left for a follow-up tranche. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): bump the npm_and_yarn group across 2 directories with 4 updates (#220) Bumps the npm_and_yarn group with 3 updates in the / directory: [axios](https://github.com/axios/axios), [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) and [fast-uri](https://github.com/fastify/fast-uri). Bumps the npm_and_yarn group with 1 update in the /example/nextjs-example directory: [next](https://github.com/vercel/next.js). Updates `axios` from 1.16.0 to 1.18.0 - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](axios/axios@v1.16.0...v1.18.0) Updates `@babel/core` from 7.28.4 to 7.29.6 - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.29.6/packages/babel-core) Updates `fast-uri` from 3.1.2 to 3.1.4 - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](fastify/fast-uri@v3.1.2...v3.1.4) Updates `next` from 16.2.7 to 16.2.11 - [Release notes](https://github.com/vercel/next.js/releases) - [Commits](vercel/next.js@v16.2.7...v16.2.11) --- updated-dependencies: - dependency-name: axios dependency-version: 1.18.0 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: "@babel/core" dependency-version: 7.29.6 dependency-type: direct:development dependency-group: npm_and_yarn - dependency-name: fast-uri dependency-version: 3.1.4 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: next dependency-version: 16.2.11 dependency-type: direct:production dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump brace-expansion (#222) Bumps the npm_and_yarn group with 1 update in the / directory: [brace-expansion](https://github.com/juliangruber/brace-expansion). Updates `brace-expansion` from 1.1.14 to 1.1.18 - [Release notes](https://github.com/juliangruber/brace-expansion/releases) - [Commits](juliangruber/brace-expansion@v1.1.14...v1.1.18) --- updated-dependencies: - dependency-name: brace-expansion dependency-version: 1.1.18 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#224) Bumps the npm_and_yarn group with 2 updates in the / directory: [nanoid](https://github.com/ai/nanoid) and [fast-uri](https://github.com/fastify/fast-uri). Updates `nanoid` from 3.3.11 to 3.3.17 - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](ai/nanoid@3.3.11...3.3.17) Updates `fast-uri` from 3.1.4 to 3.1.5 - [Release notes](https://github.com/fastify/fast-uri/releases) - [Commits](fastify/fast-uri@v3.1.4...v3.1.5) --- updated-dependencies: - dependency-name: nanoid dependency-version: 3.3.17 dependency-type: direct:production dependency-group: npm_and_yarn - dependency-name: fast-uri dependency-version: 3.1.5 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ci: publish to npm with provenance attestation (#223) Add id-token: write (plus explicit contents: read for actions/checkout) to the publish-npm job and pass --provenance to npm publish, so releases carry a signed SLSA build attestation linking the tarball to this workflow run. Co-authored-by: Nicolas Fry <nicolas@turbodocx.com> * fix(security): clear open Dependabot advisories (js-yaml, sharp, postcss, nanoid) (#226) Root (dev-scope, via commitlint): - js-yaml 3.14.2 -> 3.15.1, 4.1.1 -> 4.3.1 example/nextjs-example: - sharp 0.34.5 -> 0.35.3 (GHSA-f88m-g3jw-g9cj, inherited libvips CVEs; pulls the @img/sharp-* platform binaries forward with it) - postcss 8.5.19 -> 8.5.25, nanoid 3.3.12 -> 3.3.17 The example sits below the repo .npmrc, so it does not inherit the min-release-age=7 hardening — postcss/nanoid were pinned by hand to versions published before the cutoff rather than taking 8.5.26 / 3.3.18. npm audit --package-lock-only: example 0 vulnerabilities; root down to image-size only, which has no patched release (advisory covers <= 2.0.2 and 2.0.2 is latest). Build-time image probing on trusted input; tracked, not fixed. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(deps): move from image-size to probe-image-size (#227) * chore(deps): move from image-size to probe-image-size `image-size` is archived and is flagged by vulnerability scanners for infinite-loop DoS on ICNS/HEIF/JP2/JXL inputs (SNYK-JS-IMAGESIZE-17295814). Replace it with `probe-image-size`, which covers every raster format we actually embed in a DOCX (png, jpeg, gif, bmp, webp, tiff, psd, ico, avif, svg) and is still maintained. Import from `probe-image-size/sync` rather than the package root: the root entry pulls in `needle` (and with it http/https/zlib/iconv-lite/sax), which would land in the bundled browser builds. We only ever probe Buffers, so the sync entry is all we need. The call sites are unchanged — `sizeOf(buffer)` still yields `{ width, height }` and every one of them is already wrapped in a try/catch with a falsy-dimension guard, which also covers `probe.sync` returning `null` instead of throwing on unrecognised input. Co-authored-by: Eric Lefevre-Ardant <eric.lefevre-ardant@dilitrust.com> * fix(image): normalise measured dimensions to pixels `probe-image-size` reports the raw declared number alongside the unit it was written in, where `image-size` converted to pixels internally. Every caller treats the result as pixels and multiplies by 9525 to get EMUs, so an SVG declared `width="72pt"` rendered at 72px instead of 96 — a 25% shrink. Add `measureImage()`, which measures and converts in one place using the existing `SVG_UNIT_TO_PIXEL_CONVERSIONS` table, and route all three call sites through it. Raster formats always report `px`, so they pass through untouched. The conversion table already existed and was used for reading SVG attributes as text; it simply was not wired into the measurement path. This also removes the last reason for `probe-image-size` to be imported anywhere but `utils/image.js`, so there is now one place that can produce a dimension and no way to get an unconverted one out of it. Reachable in practice: the `isSVG` short-circuit only exists in `buildImage`, while the two `sizeOf` call sites in `xml-builder` had no such guard, so an SVG reached measurement whenever it stayed an SVG in the media part — `svgHandling: "native"`, or `"convert"` falling back because sharp is unavailable. Tests lock the conversion against measurements taken from `image-size@2.0.2`, so this is a parity check against the previous rendering rather than a restatement of the new implementation. Co-authored-by: Eric Lefevre-Ardant <eric.lefevre-ardant@dilitrust.com> --------- Co-authored-by: Eric Lefevre-Ardant <eric.lefevre-ardant@dilitrust.com> * chore(deps): bump nanoid to 3.3.18 (GHSA high — infinite loop on size 0) (#231) Co-authored-by: Nicolas Fry <nicolas@turbodocx.com> * fix: V-001 security vulnerability (#225) Automated security fix generated by OrbisAI Security * fix(build): correct repository.url owner casing (TurboDocx) for npm provenance npm provenance verification is case-sensitive and expects the package.json repository.url to match the canonical GitHub slug TurboDocx/html-to-docx. The lowercase turbodocx value caused publish to fail with E422 'Error verifying sigstore provenance bundle'. * fix(deps): bump nanoid 3.3.17 -> 3.3.18 in nextjs example (#232) Resolves Dependabot alert (GHSA, high) for nanoid <3.3.18 in example/nextjs-example. postcss requests nanoid ^3.3.16, so a targeted npm update --package-lock-only picks up the patch (no override needed). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): patch-duty security bumps — fast-uri, browserslist, baseline-browser-mapping (#238) * chore(deps): patch-duty security bumps — fast-uri, browserslist, baseline-browser-mapping Weekly patch duty. Closes all open Dependabot alerts (all [development] scope — this is a published library, so devDeps do not ship to consumers): - fast-uri -> 3.1.6 (high, 3 advisory ranges) - browserslist -> 4.28.7 (high) - baseline-browser-mapping -> 2.11.0 (medium) All three are transitive; pinned via overrides. Supersedes the standalone Dependabot PR #236 (fast-uri). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016amyscuSg6tbhYVvuQJ2S3 * ci(docx-diff): actually test PR code + fix the report output path Two defects made the docx-diff check non-functional: 1. The "Checkout PR branch" step had no `ref`, so under pull_request_target it checked out the BASE branch — the diff compared base-vs-base and never tested PR changes. Now checks out `pull_request.head.sha`, symmetric with the baseline step's `base.sha`. 2. The diff step ran with `working-directory: current` and wrote `../diff-report.md`, which the output-path guard added in #233 rejects (must be within cwd) — so it exited 1 on every run since. Now runs from the workspace root with the report inside cwd; the downstream read/comment/upload steps already expect it there. Verified locally: built the example DOCX from develop and from this branch (DETERMINISTIC_IDS=true) and ran the diff — 71 identical files, 0 changed/new/deleted. So this branch's dependency bump is not a DOCX regression. Note: pull_request_target runs the BASE branch's workflow, so this fix only takes effect for future PRs once merged to develop; #238's own docx-diff check keeps running the old (broken) base-branch workflow and stays red until then. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016amyscuSg6tbhYVvuQJ2S3 --------- Co-authored-by: Nicolas Fry <nicolas@turbodocx.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * ci(docx-diff): split untrusted build from the write-token comment + harden the diff script Addresses the code-review findings on the docx-diff harness. Security (blocker): the previous fix ran `npm ci` + `npm test` on untrusted PR head code under `pull_request_target` with a `pull-requests: write` token — a token-exfiltration / RCE vector for fork PRs. Split into two workflows: - docx-diff.yml now triggers on `pull_request` with `contents: read` only (no secrets, no write token), checks out the PR merge ref with `persist-credentials: false`, builds baseline + current DOCX, runs the diff, and uploads the report (+ PR number) as an artifact. It never comments. - docx-diff-comment.yml (new) triggers on `workflow_run` in the trusted base context, holds `pull-requests: write`, downloads the artifact, and posts the comment. It never checks out or runs PR code. diff-docx.js hardening: - `--output` with no value now errors gracefully instead of throwing a TypeError on `path.resolve(undefined)`. - the cwd-containment guard resolves symlinks on the target's parent dir (fs.realpathSync) so a symlinked --output can't escape the working directory. Verified: node --check passes; standalone assertions cover the no-value, in-cwd, `../` escape, and symlinked-parent cases; both workflow YAMLs parse. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016amyscuSg6tbhYVvuQJ2S3 * fix(image): convert SVG `ex` units to pixels instead of silently treating them as px (#241) `measureImage` normalizes probe-image-size's declared value + unit to pixels via SVG_UNIT_TO_PIXEL_CONVERSIONS, but the table had no `ex` entry. probe-image-size can emit `ex` (its unit regex is `in|mm|cm|pt|pc|px|em|ex`), so an SVG sized in `ex` fell through the `|| 1` fallback and was measured as if it were pixels — rendering the image ~8x too small (a `width="72ex"` SVG came out 72px instead of 576px). Add `ex: 8` (1ex = 0.5em, half the table's assumed 16px em), matching the CSS default and the existing em/rem convention. TDD: added a failing `ex` parity case to tests/image-measurement.test.js (72ex -> 576px), watched it fail (received 72), then added the table entry. Full jest suite green (636 tests). Claude-Session: https://claude.ai/code/session_016amyscuSg6tbhYVvuQJ2S3 Co-authored-by: Nicolas Fry <nicolas@turbodocx.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * docs(readme): add TurboQuote to the TurboDocx ecosystem table Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016amyscuSg6tbhYVvuQJ2S3 * ci(docx-diff): resolve comment target trusted-side + harden output guard Follow-up hardening from the v1.23.0 security + code review of the docx-diff split. - docx-diff-comment.yml: resolve the target PR from the build run's head SHA (listPullRequestsAssociatedWithCommit) instead of from the artifact. The build runs untrusted PR code and could tamper with an uploaded pr-number.txt to redirect the bot comment at an arbitrary PR; the head SHA comes from the trusted workflow_run event. Only the comment body (the PR's own diff) still comes from the artifact and lands solely on that PR — bounded, not a redirect. Dropped the now-unused pr-number.txt step/artifact from docx-diff.yml. - diff-docx.js: the containment guard now also refuses a symlinked target file (lstat) in addition to resolving the parent dir, so a symlinked --output can't redirect the write outside cwd — matching what the comment claims. Verified: node --check, both workflow YAMLs parse, guard assertions cover in-cwd / ../ escape / symlinked-target / symlinked-parent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016amyscuSg6tbhYVvuQJ2S3 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Nicolas <nicolas@turbodocx.com> Co-authored-by: Kushal <kushalkumargupta4@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Amit Sharma <amit.sharma@turbodocx.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Eric Lefevre-Ardant <eric.lefevre-ardant@dilitrust.com> Co-authored-by: Anupam Mediratta <mediratta@gmail.com>
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.
Why
npm can attach a signed provenance attestation to a published package: a SLSA statement, recorded in the public Sigstore transparency log, that ties the published tarball back to the exact GitHub Actions workflow run and commit that produced it. Consumers can then verify it with
npm audit signatures.The n8n community-node registry rejected another one of our packages specifically because it was published without provenance. That package has since been fixed and re-released with an attestation, and we are now applying the same one-line change across the public TurboDocx repos that publish to npm.
What changed
One file,
.github/workflows/npm-publish.yml:permissionsblock topublish-npm:contents: readis included deliberately. Declaring apermissionsblock zeroes every scope not listed, so omitting it would breakactions/checkout.--provenanceto the publish step:npm publish --access public --provenance.Nothing else. No
permissionsblock was added at the workflow level or to thebuildjob.Node 24 (already pinned in this workflow) ships npm 11.x, comfortably above the npm >= 9.5 that provenance requires, and this repo is public, so both preconditions are already satisfied.
publishConfig.provenancewas deliberately NOT added"publishConfig": { "provenance": true }inpackage.jsonwould make a bare localnpm publishfail. That is only desirable if GitHub Actions is genuinely the sole publisher, so I checked before adding it:npm view @turbodocx/html-to-docx versions --json— 30 published versions,1.9.1through1.22.0.gh run list --workflow=npm-publish.yml -L 100— 20 runs total in the queryable run history, the oldest beingv1.13.5(2025-08-15).Result: every version from 1.13.5 through 1.22.0 maps cleanly to a successful workflow run (each publish timestamp lands a minute or two after its run started). But for 1.9.1 through 1.13.4 — published 2024-05-22 through 2025-06-05 — no corresponding run exists in the queryable run history. I can't tell whether those were published by hand or whether the run records have simply aged out, and I'm not going to assert either. Since the evidence doesn't establish that Actions has always been the only publisher,
publishConfigis omitted here.That costs us nothing that matters: the
--provenanceflag is what actually produces the attestation.publishConfig.provenanceis only a guardrail against an accidental local publish — and not even an enforcement boundary, since npm gives CLI flags precedence overpublishConfig, sonpm publish --provenance=falsewould still push an unattested tarball. If we later want the guardrail, it can be added in a follow-up.No version bump, no release
This PR does not bump the version and does not cut a release. Provenance is attached at publish time, so there is nothing to verify after merging — this simply makes the next release attested.
Whenever someone next cuts a release, that publish should produce an attestation, and it can be confirmed with:
which should report a
provenanceentry withpredicateTypeofhttps://slsa.dev/provenance/v1.Two notes for the reviewer
add-npm-provenance, carrying an equivalent commit from December 2025 that never got a PR and has since fallen far behind. This PR supersedes it, and that branch can be deleted.main, notdevelop(v1.22.0is reachable frommainonly). This PR targetsdevelop, the default branch and where all other active PRs go, so the change takes effect on the first release cut after the nextdevelop->mainpromotion.Risk
Low, and non-destructive if it goes wrong. npm builds and signs the provenance statement inside
buildMetadata, which must return before the registry PUT. So if OIDC or attestation generation fails, the publish aborts before anything reaches the registry — nothing gets published and the version number stays reusable.Worth noting for reviewers:
npm publish --dry-runcannot exercise this path (npm short-circuits before the publish internals run), so the real check is the next release.