npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

n8n-nodes-gatekeeper

v0.1.0

Published

Human approval gates for n8n. Nothing executes without a signed approval, and tampered tickets are refused.

Readme

Gatekeeper

Provider-neutral approval and execution gating for n8n workflows.

Gatekeeper exists so that automation which touches something outside the workflow cannot act on its own. Every consequential action passes through an explicit human approval, an authenticated ticket, and a deployment-configured execution adapter.

What this package is, and is not

It is:

  • an authorization layer: nothing consequential runs without an approved ticket
  • an authenticated integrity mechanism for the ticket that was approved
  • a provider-neutral boundary between "this action is authorised" and "this action is performed"

It is not:

  • a production integration. No account, billing, or messaging provider is bundled or contacted
  • something that suspends, resumes, or deactivates anything on its own
  • an audit database. An audit_event record is produced and returned by the workflow; it is not written to durable storage
  • a guarantee of exactly-once external execution (see Idempotency and Concurrency limitation)

The human approval invariant

The action that executes is the action that was approved. This is enforced, not documented as intent.

  1. A request creates a ticket, signed with HMAC-SHA-256.
  2. The ticket is persisted as the authoritative record.
  3. A human approves by ticket_id only. The approver does not supply the ticket.
  4. The approval gate reloads the authoritative ticket, re-verifies its MAC, and checks expiry and lifecycle state.
  5. The execution boundary reloads the ticket again and re-verifies action, payload, state, MAC, expiry, and approver against the stored record.
  6. Only then is the configured adapter called.

Caller-supplied substitutions are rejected at these boundaries:

| Attempted substitution | Result | |---|---| | action | refused, does not match the authorised action | | payload | refused, does not match the authorised payload | | expires_at | ignored, the stored value governs | | decided_by | refused, does not match the authorised approver | | state | refused, not APPROVED | | consumed_at | refused, already consumed | | ticket_id | refused, no authoritative record | | endpoint, url, host, target, webhook | refused, execution-control field | | api_key, token, secret, credential | refused, execution-control field |

Authenticated ticket

Integrity uses HMAC-SHA-256, a keyed MAC. The key is supplied by a credential-backed node and is never written into the workflow JSON, the ticket, or any output.

This is an authenticated MAC, not an asymmetric digital signature. It is not a signature: the same symmetric key both produces and verifies it, and anyone holding the key can produce a valid MAC. Its tamper-detection property depends entirely on keeping that key secret. Protect the credential as you would any signing key.

The canonical authenticated form covers eight fields, in this order:

ticket_id, action, payload, requested_by, requested_at, expires_at, state, decided_by

payload is canonicalised with recursively sorted keys, so key ordering can never change the MAC. The same eight-field form is used at every stage: ticket creation, the approval transition, the execution verification, and the consume transition. There is one canonicalisation scheme, not several.

decided_by is the eighth field. It is null at creation and becomes authoritative at the moment of approval, at which point it is written into the record and re-authenticated with it. A caller cannot restate who approved. The execution boundary verifies that the decided_by in the request matches the authoritative stored approver; a mismatched or forged approver identity is refused.

MAC comparison is constant-time (crypto.timingSafeEqual).

Ticket lifecycle

PENDING_APPROVAL  ->  APPROVED  ->  CONSUMED

Terminal refusal conditions: REJECTED, EXPIRED, CONSUMED, malformed, unknown, or tampered.

A ticket is single-use. Once consumed, a second approval is refused, and a second consumption is refused.

Expiry

Expiry is enforced twice:

  1. at the approval gate, and
  2. again immediately before the external adapter is called

Both checks read the authoritative stored expires_at. A malformed expiry is refused rather than assumed valid, and a caller cannot supply a later expiry to extend an authorisation. The invariant is:

authoritative ticket -> HMAC -> approval -> expiry -> execution -> expiry again -> execute

Execution adapter architecture

Gatekeeper deliberately separates three concerns that are usually conflated:

authorization              may this action happen?
        |
execution configuration   which deployment, which destination, which actions?
        |
external execution        the adapter performs the action

The production adapter registry is intentionally empty:

const LOCAL_ADAPTERS = {};

An unconfigured deployment fails closed. It refuses to execute rather than simulating success. A host supplies a real adapter by registering an implementation under __gatekeeperAdapterRegistry, or by adding one to the local table.

The execution destination is read only from the operator-configured adapter node. It is never taken from the ticket, the payload, the approval request, or any caller input. The adapter receives a request containing only the authorised action, the authoritative payload, the authoritative approver, the idempotency key, and the configured target.

Execution result semantics

The result distinguishes these separately, and they must not be conflated:

| Field | Meaning | |---|---| | authorization_succeeded | the ticket was approved, authenticated and current | | execution_attempted | the adapter boundary was reached and invoked | | external_call_made | the adapter reports it actually contacted something | | execution_succeeded | the adapter reported success | | action_performed | success and an external call was made | | adapter | which configured adapter ran | | idempotency_key | the authoritative ticket id |

action_performed is true only when the adapter reports both success and a real external call. An adapter that is registered but does nothing will report execution_succeeded: false and action_performed: false.

A failed execution does not consume the ticket. The graph routes failures to a separate branch so the authorisation remains inspectable rather than being silently spent on a call that did not happen.

Idempotency

idempotency_key is derived from the authoritative ticket_id, never from caller input.

Provider idempotency is required. Gatekeeper sends the key; whether a second identical request is suppressed depends entirely on the external provider honouring it. Providers that ignore the key can double-execute.

Known crash window

If the external call succeeds and the process dies before the local consume write, the ticket remains APPROVED and a retry will attempt execution again. Mitigation is the idempotency key, and that is the only mitigation. A datastore with conditional writes would be the structural fix.

Concurrency limitation

The current implementation stores tickets with $getWorkflowStaticData('global'), which is a per-instance read-modify-write and is not transactional.

Two consequences, neither solved:

  • Simultaneous approvals of the same ticket_id can both read PENDING_APPROVAL before either writes, and both succeed.
  • Simultaneous executions of the same ticket carry the same risk.

Do not rely on this implementation for atomic single-use enforcement across concurrent workers. Hardening options, none implemented here:

  • serialise per ticket_id
  • a datastore with conditional / compare-and-set writes
  • a provider-side idempotency guarantee

Audit

Each authorised decision produces an audit_event object recording the ticket id, action, requester, authoritative approver, decision, MAC, and timestamp. It is returned in the workflow output and is part of the adapter request.

This is not durable audit storage. Nothing is written to an audit database or long-term log by these workflows. If you need a durable audit trail, an operator-supplied adapter or a datastore node must write it. Do not assume the audit_event in the output is retained anywhere.

Workflow 02: suspension and resumption authorisation

Workflow 02 is a second, separate authorisation path. It does not suspend or resume anything by itself.

suspension sweep:  overdue account
                      -> suspension ticket (PENDING_APPROVAL, nothing happens)
                      -> human approval
                      -> SUSPEND_WORKFLOW adapter

payment sweep:     account with payment_status CLEARED
                      -> resume ticket (PENDING_APPROVAL, nothing happens)
                      -> human approval
                      -> RESUME_WORKFLOW adapter

Key properties:

  • Creating a ticket does not suspend or resume. The ticket output states suspended: false / resumed: false.
  • Suspension and resumption are separate actions with separate tickets. Approving one never authorises the other; a cross-action attempt is refused.
  • Each requires its own independent approval.
  • No production suspension or resumption provider is bundled. Registering an adapter is the operator's responsibility.
  • Payment status is an input boundary. It is supplied to the workflow, not fetched from a payment provider, and the workflow marks it INPUT_BOUNDARY_UNVERIFIED. Only an exact CLEARED qualifies; anything else produces no resume ticket, and a ticket cannot be built without it.
  • Only accounts that are themselves suspended and cleared become candidates.

Fail-closed behaviour

These conditions refuse rather than continue:

  • missing or unknown ticket_id
  • missing approver identity
  • missing or invalid HMAC, including a store edited without re-signing
  • malformed expires_at
  • expired ticket
  • non-APPROVED state
  • already-consumed ticket
  • action not in the operator allowlist
  • missing adapter configuration
  • missing execution target
  • missing or registered-but-unimplemented adapter
  • missing or mismatched authoritative payload
  • execution-control fields in the decision or payload
  • caller-supplied decided_by that differs from the stored approver
  • caller-supplied expiry that would extend an authorisation

The adapter boundary fails closed on missing configuration before it can attempt anything.

Installation

  1. Import a workflow file into n8n (Workflows > Import from File).
  2. Create a credential of type gatekeeperHmacSecret holding a strong random key, and assign it to the Resolve HMAC Secret nodes.
  3. Configure the Resolve Execution Adapter node with your adapter_id, target, and allowed_actions.
  4. Register an implementation for your adapter under __gatekeeperAdapterRegistry, or accept that execution will refuse.
  5. Wire the approval decision into the gate: POST { ticket_id, decision } to gatekeeper/approval from wherever your human approver lives.

Steps 3 and 4 are the operator's responsibility. Without them the workflow authorises and then declines to act, by design.

Testing

The suite is Python unittest with no third-party dependencies.

  • Behavioural tests execute the real JavaScript embedded in the workflow files through Node, so the shipped logic is what is under test rather than a re-implementation.
  • Structural tests inspect wiring, node references, credential wiring and documentation claims.

Current state: 132 tests. Structural checks confirm both workflow files parse, node ids are unique, every connection endpoint and $() reference resolves, the canonical form is identical at every site, and no secret, URL or bare createHash is present.

Known limitations

  • No production adapter is bundled; execution refuses until one is configured.
  • $getWorkflowStaticData is not transactional, so concurrent approvals and executions can race.
  • Crash between external success and local consume is mitigated only by provider idempotency.
  • audit_event is returned, not durably stored.
  • Payment status in workflow 02 is unverified caller input, not a provider response.

License

MIT