chore(demo): zero CVE advisories — replace EOL demo toolchains and fix all seven demos - #430
Conversation
Clears 14 of the 28 remaining advisories, all of which were reachable only
through `react-scripts@5.0.1` — unmaintained since 2022 and never supported
on the React 19 this demo already uses.
Forcing the transitive versions was tried first and cannot work:
`webpack-dev-server` 5 removed `onBeforeSetupMiddleware` /
`onAfterSetupMiddleware`, and CRA's `webpackDevServer.config.js` passes both,
so `yarn start` would fail schema validation. `svgo` 2 dropped the v1 API that
`@svgr/plugin-svgo@5` calls. Only `resolve-url-loader` and `jsdom` could be
forced, leaving 8 behind — so the toolchain itself had to go.
Gone with react-scripts: `webpack-dev-server`, `postcss@7.0.39`, `svgo@1.3.2`,
`uuid@8.3.2`, `@tootallnate/once`.
The app source is untouched apart from one import. What made this fit in a
config rather than a rewrite:
- `include: /\.(js|jsx)$/` plus an explicit `@babel/preset-react` — Vite's
esbuild pass only treats `.jsx`/`.tsx` as JSX, so JSX inside `.js` needs
babel to own the transform. Doing it in babel also keeps raw JSX visible to
`babel-plugin-macros`, which twin.macro's `css={...}` prop support (7 uses
here) depends on.
- `vite-plugin-svgr` with `exportType: 'named'` / `namedExport: 'ReactComponent'`
keeps all 15 `import {ReactComponent as X} from './x.svg'` imports working.
CRA also allowed a default import for the URL of the same file; that is one
site, now `?url`, with `?url` excluded from svgr.
- `resolve.alias` reproduces `jsconfig.json`'s `baseUrl: src`, so imports like
`components/misc/Layouts.js` still resolve.
- `optimizeDeps.esbuildOptions.loader` stops the dependency scanner choking on
JSX-in-.js, which otherwise skipped pre-bundling on every dev start.
`public/index.html` became the root `index.html` (Vite's entry) with
`%PUBLIC_URL%` dropped; every meta tag is preserved verbatim.
Verified, dev server and production build both driven in a real browser:
- `yarn build` — 521 modules, 184 kB gzip (CRA produced 190 kB)
- `yarn start` — renders, 0 page errors, 0 console errors, 0 failed requests
- `yarn preview` on the built output — same, 0 errors
- advisories 28 -> 14
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… advisories
Brings the repo to **zero open advisories** (from 28), with every demo still
building and rendering.
Remix v2 is in maintenance and pins the vulnerable versions exactly, so nothing
could be forced past it: `@remix-run/react` pins `react-router@6.30.4` and
`turbo-stream@2.4.1`, `react-router-dom@6.30.4` has no patched release at all,
and `@remix-run/dev` drags in `@vanilla-extract/integration` (vite 5.4.21),
`remark-mdx-frontmatter` (estree-util-value-to-estree) and the legacy esbuild
compiler. React Router is the supported successor and its tree is clean —
`react-router` depends only on `cookie-es`, `@react-router/node` has no undici.
Per app: `@remix-run/*` -> `react-router` / `@react-router/{node,serve,dev}`,
`remix.config.js` -> `react-router.config.ts`, a `vite.config.ts` using the
React Router vite plugin, and an explicit `app/routes.ts`. The stock
`entry.client.tsx` / `entry.server.tsx` are dropped — they carried no
customisation, so the framework defaults now apply. `cssBundleHref` and
`LiveReload` no longer exist and are gone; `json()` became `data()`.
Two defects this surfaced, neither of them caused by the version bumps:
1. Both demos read the SDK constructor as `ConvertSDK.default`, which only
holds under the CJS interop the old esbuild compiler produced. Under
Vite/Rollup the default import *is* the constructor, so this threw
"is not a constructor" at runtime — client-side in the provider, server-side
in the loader. Now `ConvertSDK.default ?? ConvertSDK`, correct either way.
2. The root `"path-to-regexp": "^0.1.12"` resolution was over-broad: it
collapsed *every* descriptor onto 0.1.13, including the `^8.x` that Express
5's `router` needs, so `react-router-serve` died with
"pathRegexp.match is not a function". Removed — `@nestjs/*` and `express@4`
already declare `^0.1.12` themselves, so they still resolve to the patched
0.1.13 while Express 5 now gets 8.4.2. No path-to-regexp advisory returns.
Also `"undici": ">=7.29.0 <8"` — miniflare (wrangler) pins 7.28.0 exactly and
7.29.0 is the patch. Upper bound is deliberate: an unbounded range resolved to
undici 8.10.0, a major jump miniflare does not ask for.
Verified — advisories 28 -> 0, and every demo driven in a real browser:
- `yarn build` root: 50 bundles; `yarn lint` js-sdk: clean
- 546 mocha tests, 47 browser tests: passing
- demo/reactjs, demo/remixjs-client-side, demo/remixjs-server-side: render with
0 page errors, 0 console errors, 0 failed requests
- demo/cloudflare-workers: dry-run deploy, 259.01 KiB — byte-identical to before
- demo/nextjs, demo/nestjs: build clean
- `yarn install --immutable` passes; peerDependencies byte-identical to main
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ne command
Fixes three demos that did not actually run, which the previous two commits
claimed were verified. They were not: I checked the built output and a
render, never `yarn demo:*:start` from a clean tree, so I missed all three.
1. **React Router 8 requires Node >=22.22.0.** Local Node is 22.20.0, so
`react-router dev` printed "Oops, Node v22.20.0 detected" and the dev server
never came up cleanly. React Router 7.18.2 requires only Node >=20.0.0 and
has the same clean dependency tree (`cookie`, `set-cookie-parser` — no
turbo-stream, no undici), so advisories stay at zero. Both demos pinned to
^7.18.2.
2. **Every demo needs a workspace build first.** `@convertcom/js-sdk` resolves
`main` to ./lib/index.js, which only exists after `yarn build`, so a fresh
checkout failed with a bare
`Cannot find package '.../@convertcom/js-sdk/lib/index.js'` — no hint at the
cause. New `demo:prepare` (scripts/ensure-sdk-built.mjs) builds once when the
entry points are missing and is a no-op afterwards; every `demo:*:start` now
depends on it. This was equally broken on main.
3. **The cloudflare demo never started its origin.** The Worker proxies to
ORIGIN_URL (localhost:8888) but `demo:cloudflare:start` only ran
`wrangler dev`, so every request 500'd with "Network connection lost". Its
`start` now brings up origin and worker together via `concurrently`. Also
pre-existing on main.
Verified end-to-end in headless Chrome, launched through the root script, with
server stdout and browser console captured for each — all seven demos, zero
console errors, zero page errors, zero failed requests, no >=400 responses:
reactjs (Vite) FF JS SDK / Events / Statistics / Pricing renders
remixjs-client-side renders, first load clean
remixjs-server-side renders, loader runs, "Trigger Goal" present
cloudflare-workers edge experiments applied — "Variation 1", captions
differ per variation, across /, /events,
/statistics, /pricing
nodejs renders, feature-flag card present
nextjs renders
nestjs responds, "SDK Ready" in stdout
Baseline comparison against pristine main, same harness: the remix client demo
threw `ReferenceError: process is not defined` there and does not here, and
body text is byte-identical on both demos — so no content regression.
SDK unchanged and re-verified: 50 bundles, js-sdk lint clean, 546 mocha tests,
47 browser tests, `yarn install --immutable` passing, peerDependencies
byte-identical to main.
Advisories remain 0. Note `yarn npm audit` still prints 8 rows marked
"moderate" — every one is a `(deprecation)` notice (glob, inflight, lodash.get,
lodash.isequal, rimraf, sourcemap-codec, tsconfck, tslint), not a CVE. None has
a GHSA and Dependabot does not count them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cket
I had verified "the page renders without errors" and called it done. That is not
what these demos are for — they exist as evidence the SDK works across each
framework. Reading each README made the real acceptance criteria obvious, and
three of them were not met.
**Documented ports were broken.** Each README names a port — reactjs 3002,
nodejs 3003, nestjs 3004, nextjs 3005, remix client 3006, remix server 3007 —
and each demo reads it from `.env`, which is gitignored and therefore absent
after a clone. Every demo fell back to a framework default, collided on :3000,
and did not match its own docs. My Vite config had also hardcoded reactjs to
3000. `demo:prepare` now seeds each `.env` from its `.env.example`, and the Vite
configs honour PORT with the documented value as the default.
**The remix demos bucketed nothing.** Both logged `convertVariations: []` — on
main too — so they demonstrated the SDK doing precisely nothing. Two causes,
found in the SDK's own DEBUG output rather than by guessing:
- they asked for `location: "global"`, which matches no location in the project.
`#statistics-location` does (as the nodejs and cloudflare demos use).
- the client demo called `createContext(userId)` with no visitor properties and
no default segments, so `#adv-audience` filtered every experience out. It now
passes `{mobile: true}` and `setDefaultSegments({country: 'US'})`, matching
the nodejs and server-side demos.
Both now log a real variation and the debug trail reads
`Audience adv-audience rule matched` -> `Experience rules matched` ->
`Visitor is bucketed for variation #1003142551`.
**The server demo's goal did not exist.** "Trigger Goal" posted
`goalId: "add-to-cart"`, and the SDK answered `DataManager.convert() Goal not
found`. The project's goal is `button-primary-click` (as nodejs and reactjs
use). The click now emits a `conversion` event.
Also worth recording, because it cost real time: the tracking POST returns 403
under Playwright. That is not a demo bug — Convert filters bots at ingest and
rejects the default `HeadlessChrome` user agent. Verified directly against the
endpoint: identical payload returns 403 with a headless UA and 200 with a real
Chrome UA. Any browser-driven check of these demos has to send a realistic UA or
it will report a false failure.
Verified per demo on its documented port, launched via the root script, with
full stdout and browser console captured — all seven pass with zero stdout
errors, zero console errors, zero page errors, zero failed requests, no >=400:
reactjs :3002 renders
nodejs :3003 /statistics shows variation 1003142550-original-page
nestjs :3004 responds, SDK Ready
nextjs :3005 renders
remix client :3006 convertVariations: [Object]
remix server :3007 convertVariations: [Object], goal click -> conversion
cloudflare :8787 edge experiments applied on /, /events,
/statistics, /pricing
SDK re-verified: 50 bundles, lint clean, 546 mocha, 47 browser,
`yarn install --immutable` passing, peerDependencies byte-identical to main.
CVE advisories 0. `yarn npm audit` still prints 8 rows marked moderate; all 8
are `(deprecation)` notices, not CVEs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
There was a problem hiding this comment.
Review — PR #430 chore(demo): zero CVE advisories — replace EOL demo toolchains and fix all seven demos
Repo convertcom/javascript-sdk · HEAD 581a2d801a7820b2468a3b5e217d05e0d0ab7424 · base main @ fe46798d592b6633190451de4ae2f123aa04fd13 (PR is exactly 4 commits on top, no divergence)
1. Verdict
APPROVED WITH NON-BLOCKING FINDINGS
The two migrations are done correctly: packages/** is byte-untouched, the removal of the path-to-regexp resolution fixes a live breakage rather than merely being safe, every SVG/alias/JSX edge case of the CRA→Vite move is actually covered, and the React Router 7 config reproduces what the deleted Remix entry files did. Nothing here breaks a demo or reintroduces a vulnerability. What remains is documentation the PR invalidated but did not update, a resolution upper bound that over-reaches, and two engines fields that are now wrong.
2. Non-blocking findings
1. [IMPORTANT] Both Remix READMEs still document a build command that no longer exists and an output path that is never produced.
demo/remixjs-client-side/README.md:1,3,51,54 and demo/remixjs-server-side/README.md:1,3,51,54
Failure scenario: a user follows the "DIY" deployment section, runs remix build — after this PR there is no remix binary anywhere in the tree (@remix-run/dev has 0 entries in yarn.lock) — then deploys build/ and public/build/. react-router build emits build/client + build/server; public/build/ is never created. The start script this PR wrote (react-router-serve ./build/server/index.js) confirms the new layout. These are the only two stale references to the removed toolchain left in the repo (verified by grepping all *.md/json/js/mjs/ts/tsx for react-scripts|remix build|remix dev|remix-serve|entry.server|remix.config).
Fix: in both files replace remix build → react-router build, replace the build/ + public/build/ bullets with build/client + build/server, and retitle away from "Welcome to Remix!" / the Remix docs link.
2. [IMPORTANT] demo/reactjs/README.md still presents the demo as Create React App.
demo/reactjs/README.md:3,43,47,53,57,61,65,69,73
yarn start, yarn build and port 3002 are all still accurate, so nothing hard-breaks — but the README tells the reader the project is CRA and points at CRA docs for deployment, advanced configuration, code splitting and a "yarn build fails to minify" troubleshooting page that no longer applies. It also promises "You will also see any lint errors in the console", which the Vite dev server does not do. The new preview script is undocumented.
Fix: drop the CRA framing/links, document yarn preview.
3. [IMPORTANT] The undici resolution's <8 upper bound forces a major downgrade on a consumer that was already patched.
package.json:85 — "undici": ">=7.29.0 <8"
Evidence: in the HEAD lock exactly two packages depend on undici — miniflare@5.20260801.0-alpha (declares the exact pin 7.28.0) and node-gyp@latest (declares ^8.4.1, pulled in by fsevents). The lock now contains a single undici entry, "undici@npm:>=7.29.0 <8" → 7.29.0, so both are rewritten. Per the GitHub advisory DB (gh api /advisories?ecosystem=npm&affects=undici), the relevant advisories — including the high-severity GHSA-4cwx-7wf7-3272 — are patched at 7.29.0 and 8.9.0. The base lock resolved node-gyp's copy to 8.10.0, i.e. already unaffected. So the <8 cap buys zero security and costs a major-version downgrade (8.10.0 → 7.29.0) for the one consumer that didn't need touching. Practical impact today is small (node-gyp only runs if fsevents is rebuilt from source), but the cap will also silently downgrade any future legitimate undici@8 consumer.
Fix: scope it to the actually-vulnerable consumer — "miniflare/undici": ">=7.29.0 <8" — leaving node-gyp on 8.x.
4. [IMPORTANT] Both Remix demos still declare engines.node: ">=18.0.0", which their new dependencies cannot satisfy.
demo/remixjs-client-side/package.json:42-44 and demo/remixjs-server-side/package.json:42-44
Verified engines of the deps this PR added: react-router@7.18.2 → >=20.0.0; @react-router/dev@7.18.2 → >=20.0.0; @react-router/node@7.18.2 / @react-router/serve@7.18.2 → >=20.0.0; vite@7.3.5 → ^20.19.0 || >=22.12.0. Failure scenario: a contributor on Node 18 trusts the field, installs, and gets engine warnings plus a toolchain that is not supported at runtime.
Fix: bump both to >=20.19.0.
5. [IMPORTANT] Port wiring: two demos don't actually read the .env that demo:prepare seeds, and no demo pins its port.
demo/reactjs/vite.config.js:50-51; demo/nestjs/src/main.ts:9; scripts/ensure-sdk-built.mjs:5-9
Per the Vite docs (Config → Using Environment Variables in Config), Vite deliberately does not load .env into process.env when evaluating the config file — loadEnv is required. So Number(process.env.PORT) || 3002 in vite.config.js always takes the 3002 fallback, and the seeded demo/reactjs/.env has no effect: a user who edits PORT there sees nothing change. demo/nestjs has no dotenv/ConfigModule at all and likewise falls back to its hardcoded 3004. The four other demos genuinely do read .env (demo/nodejs/app.js:8-9 runs dotenv.config() before bin/www:15 reads PORT; nextjs and both remix demos go through dotenv-cli, declared at package.json:30). The observable outcome — documented ports, no :3000 collision — is achieved everywhere, so this is a configurability and claim-accuracy issue, not a break. Secondary: no config sets strictPort, so if 3006 is busy the remix client demo silently slides onto 3007, the remix server demo's documented port.
Fix: use loadEnv in demo/reactjs/vite.config.js (or run vite under dotenv-cli like the other demos) and add strictPort: true to the three Vite configs; either that, or narrow the comment in scripts/ensure-sdk-built.mjs:5-9 so it doesn't claim every demo reads its port from .env.
6. [minor] Six resolutions are now dead. package.json:60-85 — pbkdf2, cipher-base, sha.js, elliptic, http-proxy-middleware, nth-check have zero matching entries in the HEAD lock; they were all react-scripts-tree pins. Harmless (Yarn ignores unmatched resolutions), but this PR is the dependency-hygiene PR and removed only path-to-regexp.
7. [minor] The PR description's justification for the path-to-regexp removal is wrong on the facts, though right on the conclusion. @nestjs/core@11 and @nestjs/platform-express@11 declare path-to-regexp: 8.4.2, not ^0.1.12. More importantly the removal is stronger than "safe": the base lock had a single entry, "path-to-regexp@npm:^0.1.12" → 0.1.13, meaning the old blanket resolution collapsed every consumer — nestjs (8.4.2), express 5's router (^8.0.0), wrangler (6.3.0), serve-handler (3.3.0) — onto the API-incompatible 0.1.x. Worth correcting in the description so the next reader doesn't inherit the wrong reason for a load-bearing change.
4. Claims verified
| PR-body claim | What was run / read | Outcome |
|---|---|---|
packages/** untouched; peerDependencies byte-identical in all 11 packages |
git diff --stat fe46798 HEAD -- packages/ → empty |
Confirmed |
demo:prepare seeds every demo .env from .env.example |
git ls-files → 6 tracked .env.example (all but cloudflare-workers); .gitignore ignores .env, not .env*, so the examples stay tracked; ports in them are exactly 3002/3003/3004/3005/3006/3007, matching every README and the root README |
Confirmed |
…and each demo actually reads its port from that .env |
Vite docs (config env loading) + demo/nodejs/app.js:8/bin/www:15, demo/nestjs/src/main.ts:9, dotenv-cli at package.json:30 of nextjs + both remix demos |
Refuted for 2 of 6 (reactjs, nestjs use hardcoded fallbacks); documented ports still honored everywhere — see finding 5 |
Demos need a workspace build; main is ./lib/index.js |
packages/js-sdk/package.json (main/module/browser); scripts/ensure-sdk-built.mjs checks enums, types, js-sdk, cloudflare — the first two and the last two in root build order, so a partial build is always detected; execSync throws on failure so the && chain won't start a demo on a broken build |
Confirmed, script is sound |
ConvertSDK.default ?? ConvertSDK needed because bundlers interop differently |
Vite 7 tryResolveBrowserEntry (dist/node/chunks/config.js): with a browser string entry and a module field, Vite reads the browser entry and, if hasESMSyntax() is false (a UMD), returns data.module instead → client resolves lib/index.mjs, whose default export is the constructor (.default undefined). Remix v2's esbuild had no such heuristic → UMD → CJS interop → .default |
Confirmed — the ?? is genuinely required in both directions, and this is also why the "linked workspace CJS needs optimizeDeps.include" trap does not fire |
Removing the root path-to-regexp resolution is safe, no advisory returns |
HEAD lock resolves 0.1.13 (express 4 ~0.1.12), 3.3.0 (serve-handler), 6.3.0 (wrangler), 8.4.2 (nestjs, express 5 router) — every one at or above the patched version for GHSA-9wv6-86v2-598j (0.1.10/1.9.0/3.3.0/6.3.0/8.0.0) and GHSA-rhx6-c78j-4q9w (0.1.12) |
Confirmed; stated reason inaccurate — finding 7 |
undici pinned |
HEAD lock: single entry → 7.29.0; advisory DB patch points 7.29.0 / 8.9.0 | Confirmed it clears miniflare's vulnerable exact-pinned 7.28.0; the <8 cap over-reaches — finding 3 |
| React Router 7.18.2 not 8 because 8 needs Node ≥ 22.22.0 | npm view react-router@8 engines → 8.2.0/8.3.0: node >=22.22.0 |
Confirmed |
RR7's defaults cover the deleted entry.client.tsx / entry.server.tsx |
Read @react-router/dev@7.18.2/dist/config/defaults/entry.client.tsx and entry.server.node.tsx: identical startTransition + StrictMode + hydrateRoot(document, …); same isbot → onAllReady vs onShellReady split, same 5 s abort, plus HEAD handling and timeout cleanup the deleted files lacked; onError sets 500 as the server demo's version did |
Confirmed — nothing lost. isbot must stay declared (the default entry imports it) |
routes.ts maps the existing route files; ssr mode is right |
git ls-files → each demo has exactly one route file, app/routes/_index.tsx, and index('routes/_index.tsx') resolves relative to app/; Remix v2 always SSR'd (neither old remix.config.js set otherwise), so ssr: true in both is correct — including the "client-side" demo, whose client-ness is the SDK running in useEffect, not SPA mode |
Confirmed |
| RR7 API surface used by the diff | Extracted react-router@7.18.2: MetaFunction, LoaderFunction, ActionFunction in the index.d.ts export list; declare function data<D>(…) in data-CjO11-hU.d.ts and data in the runtime export block of index.mjs; @react-router/serve bin is react-router-serve and its own usage string is react-router-serve build/server/index.js — exactly the new start script |
Confirmed |
Removing browserNodeBuiltinsPolyfill is safe |
packages/utils/src/http-client.ts:160-169: the require('url'/'http'/'https'/'querystring') calls sit in a try/catch reached only when window and fetch are both absent (old Node). Vite doesn't statically resolve bare require() in ESM, so no build error and the branch never runs in a browser |
Confirmed |
CRA SVG behaviour reproduced, incl. node_modules and ?url |
Installed @rollup/pluginutils@5.3.0 in a scratch dir and ran createFilter('**/*.svg','**/*.svg?url'): true for an in-project abs path, true for …/node_modules/feather-icons/dist/icons/menu.svg, false for …svg?url |
Confirmed empirically — feather-icons named imports keep working, ?url falls through to Vite's asset pipeline |
JSX-in-.js handled |
@vitejs/plugin-react runs babel with enforce: 'pre', so @babel/preset-react transforms JSX before vite:esbuild (whose filter is .ts/.mts/.jsx/.tsx and never touches plain .js anyway); babel-plugin-macros is a plugin so it runs before the preset and twin.macro sees raw JSX; optimizeDeps.esbuildOptions.loader['.js']='jsx' covers the dep scanner. demo/reactjs/babel-plugin-macros.config.js resolves via cosmiconfig from src/**, and its ./src/tailwind.config.js is cwd-relative — cwd is demo/reactjs under cd demo/reactjs && yarn start |
Confirmed by construction; the runtime css={…} output (7 usages) is browser-only — see §5 |
demo:cloudflare:start starts the origin the Worker proxies to |
concurrently@^9.2.1 is declared in the demo's devDeps; -k prevents orphans; origin/server.js:11 listens on 8888 and wrangler.toml ORIGIN_URL = http://localhost:8888, wrangler's own default is 8787 — no clash, matches the demo README |
Confirmed |
| Bucketing fixes are consistent across demos | button-primary-click matches demo/nodejs/routes/pricing.js:5 and demo/reactjs/src/landing/PricingPage.js:12; location: 'statistics' matches demo/nodejs/routes/statistics.js:13; createContext(userId, {mobile:true}) + setDefaultSegments({country:'US'}) now match all seven demos; setDefaultSegments exists on ContextInterface (packages/js-sdk/src/interfaces/context.ts:54) |
Confirmed |
| Zero CVE advisories | yarn npm audit needs an install state, which was not created. Targeted checks instead: react-scripts has 0 lock entries and the old @remix-run/* tree went 13 entries → 1 (the whole vulnerable webpack/svgo tree is gone; the survivor is @remix-run/node-fetch-server@0.13.3, a dependency of the new @react-router/serve@7.18.2, with no advisories in the GitHub DB). The newly-introduced toolchain resolves to vite 7.3.6 (advisories GHSA-fx2h-pf6j-xcff high and GHSA-v6wh-96g9-6wx3 cover >=7.0.0, <=7.3.4 — hence the deliberate ^7.3.5 floor) and esbuild 0.28.1 (GHSA-gv7w-rqvm-qjhr high covers >=0.17.0, <0.28.1) |
Directionally confirmed for everything reachable; the repo-wide claim is unverifiable without an install |
| CI green means the demos work | .github/workflows/qa.yml runs only packages/js-sdk lint/build/mocha/playwright — no demo is built, typechecked or started in CI. The one real signal is yarn install --immutable, which proves the lockfile is consistent with all 33 changed manifests |
Context, not a defect — the green tick carries no information about the demo fixes |
5. What this review did not cover
- The manual browser verification. "All seven demos launched on documented ports, zero console/stdout/network errors" rests on a run that cannot be reproduced here. No
yarn install,yarn build,vite build,react-router build,wrangler dev, or demo start was run. Everything above is static analysis plus first-party package sources and the GitHub advisory API. - twin.macro's
css={…}prop at runtime. 7 usages across 4 files. The babel ordering that makes it possible is verified; whether the emitted styles are visually identical to the CRA build needs a browser. wrangler devunderundici@7.29.0instead of the7.28.0miniflare pins exactly. Low risk (patch-level within 7.x), but it is a deliberate upstream pin being overridden and only a real run proves it.yarn npm audit— Yarn 4 requires a restored install state, so the repo-wide "zero advisories" number is unconfirmed. Checks were targeted at the packages the description makes claims about plus the newly added build tooling.yarn typecheckin either Remix demo (react-router typegen && tsc) — not wired into CI and not run here; the hand-writtenConvertSDKInstanceinterface inConvert.tsxanduseLoaderData<ConvertLoaderData>()are plausible but untypechecked.- Lint. The repo's lint scripts cover
packages/*only; no lint gate applies to any file in this diff, so there was nothing meaningful to run. - Staleness, as distinct from partial builds.
scripts/ensure-sdk-built.mjsdetects a missing or partial build. A complete-but-stalelib/(older thansrc/) is not detected and the demo will run against it. Minor for a demo bootstrap, and out of the PR's stated scope.
Provenance
- Review pass:
convert-code-revieweragent (Opus), independent of the main session, reading a detached worktree at581a2d80— 76 tool calls. - Verification pass: the main session independently spot-checked the reviewer's load-bearing claims against disk before this report was accepted, per
.claude/rules/verify-subagent-claims.md. Checked and confirmed:packages/**empty diff;.gitignore:3is.env(not.env*); all six tracked.env.examplefiles and their exact ports;demo/reactjs/vite.config.js:50-51;demo/nestjs/src/main.ts:9plus zerodotenvin its manifest; both remixenginesblocks at:42-44;package.json:85undici; HEAD-vs-BASEpath-to-regexplock entries (1 collapsed entry → 4 correct ones) andundicientries (3 → 1, incl. the 8.10.0 → 7.29.0 downgrade); all six dead resolutions at zero lock entries;vite 7.3.6/esbuild 0.28.1;@nestjs/core@11.1.28declaringpath-to-regexp: npm:8.4.2; both remix READMEs' staleremix build/public/build/lines; the reactjs README CRA lines; zeroprocess.env/REACT_APP_/%PUBLIC_URL%in the reactjs demo; one route file per remix demo; zerostrictPort; and a full read ofscripts/ensure-sdk-built.mjs. One imprecision found: the reviewer stated all@remix-run/*entries were gone; one remains, and it is a dependency of the new toolchain with no advisories — the conclusion is unaffected. Corrected inline above. - CI at this sha: 6/6 green —
pass-qa (22), SonarCloud, CodeQL, Analyze (actions), Analyze (javascript), auto-approve.
JosephSamirL
left a comment
There was a problem hiding this comment.
Approved via /convert:approve. An independent code review ran through /convert:review, and this issues the B-G4 human marker at 581a2d8.

Takes the repo to zero CVE advisories (from 28 after #429) and fixes the seven
demos, which are the evidence that the SDK works across each framework.
Every remaining advisory sat behind a build tool that is end-of-life and pins the vulnerable
versions exactly, so nothing could be forced past it. Replacing those tools with their supported
successors removes the vulnerable trees outright rather than suppressing them.
demo/reactjs: Create React App → Viteundicipinned to its patchWhy the tools had to be replaced, not forced
Verified against the installed code rather than assumed:
webpack-dev-server5 removedonBeforeSetupMiddleware/onAfterSetupMiddleware, and CRA'sown
webpackDevServer.config.jspasses both — forcing it fails schema validation onyarn start.svgo2 dropped the v1 API@svgr/plugin-svgo@5calls. Onlyresolve-url-loaderandjsdomcould be forced, leaving 8 behind.@remix-run/reactpinsreact-router@6.30.4andturbo-stream@2.4.1exactly, andreact-router-dom@6.30.4has no patched release in existence.React Router 7.18.2 (not 8) because 8 requires Node >=22.22.0 and local Node is 22.20.0;
7.18.2 needs only >=20 and has the same clean tree —
react-routerdepends oncookieandset-cookie-parser,@react-router/nodehas no undici.The demos were not doing their job
Reading each README gave the real acceptance criteria. Three were unmet, two of them on
mainas well:
Documented ports were broken. Each README names a port — reactjs 3002, nodejs 3003,
nestjs 3004, nextjs 3005, remix client 3006, remix server 3007 — read from
.env, which isgitignored and absent after a clone. Every demo fell back to a framework default and collided on
:3000.
demo:preparenow seeds each.envfrom its.env.example.The remix demos bucketed nothing — both logged
convertVariations: []. Diagnosed from theSDK's own DEBUG output: they asked for
location: "global", which matches no location in theproject, and the client demo called
createContext(userId)with no visitor properties or defaultsegments, so
#adv-audiencefiltered every experience out. With{mobile: true}andsetDefaultSegments({country: 'US'})— matching the nodejs demo — the trail now readsAudience adv-audience rule matched→Experience rules matched→Visitor is bucketed for variation #1003142551.The server demo's goal did not exist. "Trigger Goal" posted
add-to-cart; the SDK answeredDataManager.convert() Goal not found. The project's goal isbutton-primary-click. The clicknow emits a
conversionevent.Two more pre-existing blockers, fixed here: every demo needs a workspace build first
(
@convertcom/js-sdkresolvesmainto./lib/index.js, so a fresh checkout died with a bareCannot find package), anddemo:cloudflare:startnever started the origin the Worker proxiesto, so every request 500'd with "Network connection lost".
Two defects this surfaced in the demo code
ConvertSDK.default, which only holds under theCJS interop the old esbuild compiler produced. Under Vite/Rollup the default import is the
constructor, so this threw "is not a constructor" at runtime. Now
ConvertSDK.default ?? ConvertSDK, correct either way."path-to-regexp": "^0.1.12"resolution was over-broad — it collapsed everydescriptor onto 0.1.13, including the
^8.xExpress 5'srouterneeds, soreact-router-servedied withpathRegexp.match is not a function. Removed:@nestjs/*andexpress@4already declare^0.1.12themselves, so they still resolve to the patched 0.1.13while Express 5 gets 8.4.2. No path-to-regexp advisory returns.
Verification at
581a2d8Each demo launched via its root script on its documented port, with server stdout and browser
console/network captured, driven in headless Chrome:
/statistics→1003142550-original-pageSDK ReadyconvertVariations: [Object]convertVariations: [Object], goal click →conversion/,/events,/statistics,/pricingZero stdout errors, zero console errors, zero page errors, zero failed requests, no >=400 in any
of the seven.
SDK:
yarn install --immutablepasses, root build 50 bundles,packages/js-sdklint clean,546 mocha tests, 47 browser tests.
peerDependenciesbyte-identical tomainin all 11packages — no published package changes. Every commit is
chore(...), so release-please cutsnothing.
Known, not fixed here
yarn lintis red inpackages/types(generatedsrc/config/types.gen.ts).Pre-existing on
main;pass-qaonly lintspackages/js-sdk, so CI has never exercised it.yarn buildrewrites each package's sourcepackage.json(generate-rollup-config.mjspinspeerDependenciesto sibling workspace versions), so any build leaves a dirty tree. That churnwas stripped before committing.
opting into the
v8_*flags changes runtime behaviour.yarn npm auditstill prints 8 rows markedmoderate; all 8 are(deprecation)notices(glob, inflight, lodash.get, lodash.isequal, rimraf, sourcemap-codec, tsconfck, tslint), not
CVEs. None carries a GHSA.
🤖 Generated with Claude Code