n8nnpmciintegrationapitypescript

A Verified n8n Community Node: the rules the docs don't emphasize

Douglas Haruo 10 min 9/10/2026

Packaging an API as an n8n community node, a community plugin published on npm, is a one-day project. The surface is small: a node class, a credential (where the user pastes the API key), an SVG. What is not a one-day project is doing it the way n8n’s verification requires. The requirements change the package’s architecture, and three of them barely show up in tutorials.

This is the field note of n8n-nodes-tamperlens, the node that exposes the Tamperlens API as three operations (Inspect, Metadata, Compare). It hit npm on 2026-08-11, in two versions on the same day (I’ll explain why), and was submitted to the Creator Portal three days later. What follows are the rules that apply to any API you want to package. And also the afternoon that npm’s trusted publishing cost, with the mistakes in the order they happened.


The three rules that change the design

A verified node is one that has passed n8n’s official review. Since 2026-05-01, it must satisfy three conditions that are not cosmetic:

  1. Publishing via GitHub Actions with provenance, the package’s proof of origin. Publishing from a local machine is rejected. This is not a process preference: it is what guarantees the package on npm was built from the public repository, by an auditable workflow.
  2. The node may touch no files and no environment variables. No fs.readFile on a path the user typed. The practical consequence: the input is the item’s binary data, never a file path.
  3. Passing npx @n8n/scan-community-package. It is a static scanner that rejects specific patterns, and it is worth running before publishing, not after.

There is a fourth rule the ecosystem assumes without writing it in bold: zero runtime dependencies. The node’s package.json has devDependencies and a peerDependencies: { "n8n-workflow": "*" }, and no dependencies. That reverberates through the entire codebase, as becomes clear with the first operation that needs an upload.

Zero deps in practice: multipart by hand

Two of the operations (inspect, metadata) send the raw buffer as the body. The Content-Type comes from the binary’s mimeType, falling back to application/octet-stream. Simple.

The third (compare) sends two files, and two files call for multipart/form-data, the format that packs several files into one request. The reflex is to install the form-data package. With zero deps, you can’t. So the multipart is assembled by hand, in about thirty lines: a buildMultipart() that concatenates boundary, part headers, and buffers.

Hand-rolling multipart has two known traps, and both became tests:

  • A predictable boundary. The boundary is generated with 24 base-36 characters, and the generator accepts an injectable RNG, a swappable random-number generator. It is not for security: it is so the test can pin it and assert the body byte for byte.
  • Filename as a header-injection vector. The file name comes from the workflow item, that is, from outside. An escapeQuotes() handles quotes and backslashes, and the corresponding test asserts that a malicious filename cannot break out of the part header.

The architecture detail that made those tests cheap: the helpers (buildMultipart, joinUrl, escapeQuotes) live in a module with no n8n types at all. They test with plain node --test, no harness, no n8n runtime mock. The node class stays thin. The testable logic lives outside it.

Binary instead of paths: the rule that protects the user

The “touch no files” rule looks like bureaucracy. Consider what a malicious node would do with a file-path field: read ~/.ssh/id_rsa and ship it to an endpoint. Hence the input is assertBinaryData + getBinaryDataBuffer: the node only sees what the workflow has already loaded as the item’s binary.

For anyone packaging an API, this defines the contract: your node receives buffers and your endpoint receives buffers. Any story of “point at the file on disk” dies at the design stage. The side effect is good: the same node works identically on n8n Cloud, where disk doesn’t even exist the way the user imagines.

The credential test that costs nothing

Every n8n credential can declare a test endpoint: the little “Test” button the user presses after pasting the API key. The naive choice is to point it at the main operation. Except if the API is metered, every credential test burns quota.

The solution was to point the test at an endpoint that is authenticated but not metered. In this case, the receipt-verification endpoint: it returns 401 with a wrong key and 200 with a valid one, without consuming anything from the monthly allowance. If your API has no such endpoint, it is worth creating a GET /auth/check just for this. The credential test is the user’s first interaction with your API, and “testing charged me” is a terrible first interaction.

The scanner rejects things that look right

@n8n/scan-community-package ran over 0.1.1 and flagged three things. Hence 0.1.2 on the same day:

  1. A credential without an icon. The credential needs its own icon reference, with light/dark variants.
  2. A node icon without a dark variant. The SVG needs the tamperlens.svg / tamperlens.dark.svg pair. In this case, the variant only changes the stroke color.
  3. A raw throw error inside the catch. The code had a conditional rethrow (if (error instanceof NodeOperationError) throw error;) before wrapping the rest in NodeOperationError. The scanner counts the rethrow as a “raw throw” and rejects it. The fix was to always wrap, with itemIndex in the context.

The third item is the instructive one: the rejected pattern is defensible in application code. In a verified node, the rule is syntactic and non-negotiable. So run the scanner before the first publish, because every run of it after publishing is a new version on npm.

The error handling that remained is n8n’s idiomatic pattern and worth copying: try/catch per item. With continueOnFail() on, the error becomes { json: { error } } and the workflow continues. With it off, it becomes a NodeOperationError with the item’s index. On both paths the output carries pairedItem, so n8n knows which input item each output came from.

The OIDC afternoon: four mistakes until token-less worked

The part that pays the most per hour of suffering. Publishing with provenance via trusted publishing (no token in the repository) is the right path. The road to it, on 2026-08-11, took five commits in forty minutes:

  1. npm 10 does not do the OIDC exchange, which authenticates the CI without a token. The Node 22 from setup-node ships npm 10, and that version does not implement the token-less exchange. The symptom is not a message about OIDC: it is the registry returning 404 on the unauthenticated PUT.
  2. registry-url in setup-node sabotages OIDC. With registry-url configured, setup-node writes an .npmrc with the placeholder ${NODE_AUTH_TOKEN}. npm presents that placeholder as if it were a real token, preempting the OIDC exchange (issue actions/setup-node#1551). The fix is counter-intuitive: remove the registry-url.
  3. An explicit --provenance breaks trusted publishing. With trusted publishing, provenance is automatic. Passing the flag by hand routes the publish through the token-signing path, which does not exist, and yields ENEEDAUTH.
  4. npm 12.0.x regressed the exchange. Even with everything right, the npm 12.0.x of the time returned ENEEDAUTH with a valid configuration.

The final state, which works and is pinned in the workflow: setup-node without registry-url, npm install -g npm@11 (neither 10 nor 12), npm publish --access public with no flag and no NODE_AUTH_TOKEN. On the job, just permissions: id-token: write. And two seatbelts outside the YAML: prepublishOnly: npm test in the package.json, and a test script that builds before testing. That way a publish never leaves without a green suite over the real dist/.

If you set up the same pipeline: copy the final state, but keep the symptom list. 404 on the PUT, ENEEDAUTH, and “works locally, fails in CI” are three disguises of the same problem, and none of them mentions OIDC in the error text.

Distribution channel? Measuring before claiming

The thesis that justified the node was “marketplace as a distribution channel”. The honest reading, recorded in internal docs before this post: the ecosystem had 5,834 community nodes indexed in January 2026 and about 25 verified ones. An unverified node is a needle in a haystack with no good search. Verification is what changes the shelf.

That is why the operational conclusion was different: the node is hygiene, not a channel. It needs to exist for the product to be taken seriously by people who live inside n8n. Its maintenance cost is near zero (zero deps helps again: no dependabot waking the repo every week). But the real distribution bet is verification, submitted to the Creator Portal on 2026-08-14, with a review window of up to four weeks. Along with it came a self-imposed rule worth adopting: do not touch the repository while the review is open, because every new version resets the queue.


The checklist, for your API

If you are going to package an API as a community node with verification ambitions:

  1. Zero dependencies. Multipart by hand if needed; pure helpers in a module without n8n types, tested with node --test.
  2. Binary, never file paths. assertBinaryData + getBinaryDataBuffer; the item’s mimeType becomes the Content-Type.
  3. Credential test on an authenticated, unmetered endpoint. If one doesn’t exist, create it.
  4. Run npx @n8n/scan-community-package before the first publish. Icons with dark variants (node and credential), no raw throws, errors with itemIndex and pairedItem on both paths.
  5. Token-less trusted publishing: no registry-url, npm pinned to a version that does the OIDC exchange, no manual --provenance, id-token: write, prepublishOnly running the suite.
  6. Treat the node as hygiene. Verification is the distribution bet, and the node itself is the entry condition. Freeze the repo while the review is open.

None of this is hard the second time. The goal of this post is to be someone’s second time.

Need a custom technical project?

Architecture, TypeScript, APIs and automation, from prototype to production. The person answering your email is the one writing the code, and the deadline I promise is the one I can meet.

Send me a message →