On import, archetype-index-sdk reports the host, user, project and CI environment to cdn.node-js.dev, then runs, hidden and detached with the process's environment, any JavaScript the server returns: arbitrary code execution on hosts the operator selects.
archetype-index-sdk is published on npm by jamesfast as a personality-archetype catalog SDK: typed profiles, comparison, search and formatting. Its root entry also runs an obfuscated loader. The loader reports the host to cdn.node-js.dev and runs, as a detached Node process, whatever JavaScript the server returns. The package does not contain the code it runs: the server decides what to send and to whom. The loader is the package's purpose, not a feature of it.
What archetype-index-sdk does
The package ships a catalog of twelve personality archetypes with validation, similarity and search helpers. It also ships two large obfuscated files, telemetry-info.mjs and dist/runtime-matrix.mjs; the second is padding with no side effects. The root index.mjs re-exports dist/index.js, and the first line of dist/index.js imports ../telemetry-info.mjs, so importing the package runs the loader. It runs on every import, at most once every 30 minutes per user, gated by a state file.
What the code does
All references are to telemetry-info.mjs line 2, whose strings are base64 with XOR 0x4e.
- Configuration. Host
cdn.node-js.dev, port 443, path /report/f40a8396af/, cache directory build-cache, payload file helper.cjs, state file .f2918f31.json, a 30-minute interval. It also holds an Ed25519 public key (sigpub) that nothing uses.
- The report. Under short keys it collects: hostname, username, home directory, working directory, OS release, architecture, total memory and uptime, the Node version and platform, and the importing project's package name. It adds whether
helper.cjs is already present, and whether each of these variables is set: SSH_CONNECTION, CI, TRAVIS, CODESPACES, KUBERNETES_SERVICE_HOST, DEVCONTAINER, COMPOSE_PROJECT_NAME, RUNNER_ID, CIRCLECI, GITLAB_CI, NODE_ENV, BUILDKITE, WSLENV, STACKBLITZ_ENV, and one more for a hosted online editor. The report is sent as j1 plus base64url JSON, in the path of an HTTPS GET to https://cdn.node-js.dev/report/f40a8396af/<report>.
- The reply. The reply is JSON. If it carries an
artifact field, the loader base64-decodes it and writes it to the OS cache: ~/.cache/build-cache/helper.cjs on Linux, ~/Library/Caches/build-cache/helper.cjs on macOS, %LOCALAPPDATA%\build-cache\helper.cjs on Windows. It then starts it with child_process.spawn(process.execPath, [helper.cjs], { detached: true, stdio: "ignore", windowsHide: true }). The state file records the version tag the server named. Later imports start the helper again if more than 35 minutes have passed, and replace it when the server names a new tag.
- No check on the code. The function that should verify the artifact returns
true unconditionally, and the Ed25519 key is never read.
- TLS pinning the server controls. The pin list starts empty, so the first connection uses ordinary certificate checks. A reply may carry a
fingerprint, which the loader stores and pins from then on, so the server can move later traffic to a certificate it chooses.
- The README.
README.md:61-70 presents the report as telemetry and offers PLR_V3_FACTS=minimal, only: or off: to filter fields. Those settings never remove the fields the server needs to place the host, and nothing turns off the download and execution.
What the server sends
We sent the endpoint one report shaped like an ordinary developer machine (Linux, no CI markers). It answered 200 with an npm-metadata-shaped document naming the package and a version tag (5e20919a), and no artifact. The loader treats that reply as nothing to run, and the package looks like it is checking its own version. The server therefore delivers code selectively, by what the report tells it about the host. Which hosts get helper.cjs, and what it does, is known only to the operator. Treat it as arbitrary code running as the user, on any machine that imported the package.
How targets are chosen
- A campaign identifier. Every report carries a fixed
lane value, 5e20919a, and the server's reply names the same value. One server can tell apart the packages or campaigns reporting to it.
- An override field the server always receives. If the variable
PLR_V3FORCE is set, its value goes into the report as force. The loader does nothing else with it. force is one of the fields that PLR_V3_FACTS=minimal never strips, and it is sent as a string, not a flag, so its meaning is decided by the server. A report with force set to 1 got the same empty reply.
- Room for finer fingerprints. The loader's field map also has slots for CPU, cgroup and Docker markers and a MAC address, which this version leaves empty. The
PLR_V3 prefix and those slots read as one configuration of a reusable kit, not a one-off.
- What the server sees. Taken together, the report identifies one machine, one user and one project, and whether it is a CI runner, a container, a cloud editor or a developer's workstation. That is enough to deliver the second stage to a single chosen victim, or to one organisation's machines, and to show everyone else what looks like a package version check.
Impact and capabilities
What follows is what the loader gives its operator on every machine that imports the package, whatever helper.cjs turns out to be.
- Arbitrary code execution as the user. The helper is any JavaScript the server sends, up to about 3 MB. It runs under the same Node binary with the full Node API: files, network, child processes, native addons. It runs as the user who imported the package, on Linux, macOS and Windows.
- Hidden, and outlives the program. The helper is spawned detached, with no console window on Windows and no output, so it keeps running after the application, test run or CI run that imported the package exits.
- The importing process's secrets. The spawn passes no environment of its own, so the helper inherits the importing process's whole environment. In CI that means the tokens the CI gives it:
NPM_TOKEN, GITHUB_TOKEN, cloud keys, deploy credentials. On a workstation it means whatever the shell exported.
- Everything the user can read. That includes SSH keys,
~/.npmrc and other registry tokens, cloud credential files, .env files, browser profiles and the source tree in the working directory. The report has already told the operator where the home and working directories are.
- Target selection before delivery. The report names the host, user, project and environment: CI provider, Kubernetes, Codespaces, devcontainer, WSL, SSH session. The operator can send the helper only where it pays, for example CI runners with publish rights or a particular company's hosts, and send the harmless-looking reply everywhere else. That is the reply we got, which keeps casual inspection blind.
- Re-run and update at will. Each import starts the helper again once 35 minutes have passed since it last ran. A new version tag from the server replaces it, so the operator can change what runs on a host at any time while the package is installed.
- Survives uninstalling the package. The helper and its state live in the user's cache directory, which
npm uninstall does not touch. A helper already delivered stays on disk, and anything it set up for itself stays too.
- Control of the channel. After the first reply, the server can pin a certificate of its choosing, which replaces normal certificate trust for this host from then on.
- Supply-chain reach. A project that depends on
archetype-index-sdk, directly or through another package, runs the loader whenever the dependency is imported. A token stolen from CI lets the operator publish to the victim's own packages.
- Disclosure even without a helper. Every import, at most once per 30 minutes, tells the operator the hostname, username, home and working directories, the project's name, and the OS, Node, memory and uptime of the machine.
Timeline (UTC)
| When | What |
| 2026-11-08 20:28 | Placeholder 0.0.0-stage published |
| 2026-11-08 20:45 | 1.0.0 with the loader published |
| 2026-11-08 21:49 | Still served on npm; cdn.node-js.dev answers reports; the repository named in its metadata is not on GitHub |
If you installed it
Uninstall archetype-index-sdk. Look for the build-cache directory above; delete it after keeping a copy of helper.cjs if you need to know what ran. If helper.cjs exists, it was delivered and run: treat the machine as compromised, stop any node process running it, and rotate the credentials that user could read (SSH keys, cloud and registry tokens, environment secrets). Block cdn.node-js.dev. PLR_V3_FACTS does not make the package safe.