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

@traffic-lights/events

v0.3.2

Published

Traffic Lights Node.js SDK — emit business events to Traffic Lights over HTTPS. Server-side only.

Readme

Traffic Lights Node.js SDK

@traffic-lights/events — emit business events to Traffic Lights over HTTPS.

🛑 SERVER-SIDE ONLY. Never ship this to a browser. The API key travels in a request header on every call, so anything that reaches an end user's device reaches their developer tools. RestTransport refuses a non-HTTPS baseUrl for the same reason, and the package imports node:crypto, so it is Node-only by construction rather than by convention.

Requires Node 18 or later.

One package. REST is the default and the single contract. EventBridge is an internal adapter behind the /eventbridge subpath export — importing it is a deliberate act visible in the import line, which is what keeps SAI's "REST is the contract" ruling from being decorative.

Three ways to integrate

1 · Node server-side — the normal path

npm install @traffic-lights/events
import { emitEvent } from '@traffic-lights/events';

// TL_API_BASE_URL and TL_API_KEY from the environment, or pass them explicitly.
const eventId = await emitEvent('billing.close', 'FLOW_CALLBACK', {
  runID: 'MEC-20260925T000000Z',
  taskID: '20006510',
  status: 'SUCCESS',
});

2 · Inside the Traffic Lights AWS account — the EventBridge adapter

Not a peer transport: REST is the contract, and this exists for in-account emitters only. You inject your own AWS client, because this package does not depend on the AWS SDK and must not start.

import { EventBridgeTransport } from '@traffic-lights/events/eventbridge';
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';

const tr = new EventBridgeTransport({
  client: new EventBridgeClient({}), eventBusName: 'tl-bus', PutEventsCommand,
});
await tr.emit({ source: 'sai.tl.acme.orders', 'detail-type': 'FLOW_CALLBACK', detail });

3 · Google Apps Script — no npm, no bundler

Apps Script cannot install packages, so there is nothing to import: the contract is one HTTPS POST and this is the whole client. Runnable as-is — paste it in, set the two script properties, run tlEmit.

/**
 * Traffic Lights — emit one event from Google Apps Script.
 * Script properties required: TL_API_BASE_URL, TL_API_KEY.
 */
function tlEmit(detailType, detail, idempotencyKey) {
  var props   = PropertiesService.getScriptProperties();
  var baseUrl = props.getProperty('TL_API_BASE_URL');
  var apiKey  = props.getProperty('TL_API_KEY');
  if (!baseUrl || !apiKey) {
    throw new Error('Set TL_API_BASE_URL and TL_API_KEY in Script Properties.');
  }
  if (!detail || !detail.runID) {
    // The correlation key is never generated for you: a synthesised id correlates nothing
    // while looking like it does.
    throw new Error('detail.runID is required.');
  }

  // 🛑 ONE KEY PER EVENT, NOT PER ATTEMPT. Apps Script retries on its own; without a stable key
  //    a retried timeout becomes a SECOND event. Pass your own for anything re-runnable.
  var key = idempotencyKey || Utilities.getUuid();

  var res = UrlFetchApp.fetch(baseUrl.replace(/\/+$/, '') + '/api/v1/events', {
    method: 'post',
    contentType: 'application/json',
    headers: { 'X-Api-Key': apiKey, 'Idempotency-Key': key },
    payload: JSON.stringify({
      source: 'sai.tl.appsscript',
      'detail-type': detailType,
      detail: detail,
    }),
    muteHttpExceptions: true,   // so a refusal is readable instead of an opaque throw
  });

  var code = res.getResponseCode();
  var body = {};
  try { body = JSON.parse(res.getContentText() || '{}'); } catch (e) { /* keep {} */ }

  if (code !== 202) {
    // ⚠️ THE SERVER'S REASON IS THE POINT. A 422 saying only "rejected" sends you to your JSON
    //    when the real cause may be that the run is not awaiting a callback.
    throw new Error('Traffic Lights refused (' + code + '): ' +
                    (body.reason || '') + ' ' + (body.detail || res.getContentText()));
  }
  return body.eventId;
}

function demo() {
  Logger.log(tlEmit('FLOW_CALLBACK', {
    runID: 'MEC-20260925T000000Z', taskID: '20006510', status: 'SUCCESS',
  }));
}

⚠️ A non-202 is a refusal, including a 200. The contract says 202; a 200 means something other than this endpoint answered — a proxy, a redirect target, a router that matched a different route — and treating it as success is how a caller believes an event landed somewhere it did not.

Types

./types/*.d.ts are generated from the JSDoc in src/. The events-intake contract is a separate, generated artefact — @traffic-lights/events/api-types — derived from the api-tlcore OpenAPI spec, so the request body and the 202 response cannot drift from the server silently:

import type { EventEnvelopeIn, EventAccepted, RefusalReason }
  from '@traffic-lights/events/api-types';

⚠️ RefusalReason is derived from the server's own refusal vocabulary rather than from the spec, because the spec does not carry it — its only documented error response is a generic validation error. Regenerate with make gen-types; make check-types-drift refuses a publish when the committed artefact no longer matches the contract.

import { RestTransport, validateEnvelope } from '@traffic-lights/events';
import { EventBridgeTransport } from '@traffic-lights/events/eventbridge';  // deliberate

The envelope is defined once

../schema/envelope.schema.json, beside the Python SDK. Neither language restates it, and ../fixtures/ holds golden envelopes that both suites load and must agree on, envelope for envelope. That diff is the contract test; a fixture only one language reads proves nothing.

🛑 take, takeOne and complete REFUSE today

They throw NotWiredError naming what they wait on — emit waits on TL-701, the executor-queue operations on [TL]'s auth/contract report. ⚠️ They do not return an empty array or {ok:true}. An empty work list is byte-identical to "the queue is empty", so a stub would let a caller build against a transport that has never spoken to anything, with green tests. A refusal cannot be mistaken for a working call. emit still validates first, so the envelope contract is exercised today.

The runID spelling is one constant

RUN_ID_KEY in src/envelope.js, and it is runID.

🛑 CORRECTED 2026-09-28 ([TL]). This paragraph said the ruling was PENDING and that the shipped Python wheel "accepts runId and explicitly refuses runID" — the OPPOSITE of what ships. It was written 2026-09-25 against wheel 2.0.0; SAI ruled on 2026-09-26, one day later, and the ruling went the other way.

MEASURED 2026-09-28 against the artefacts, not the source:

wheel traffic-lights-events 2.4.1 (installed)   RUN_ID_KEY   = "runID"   RUN_ID_ALIAS  = "runId"
deployed api-tlcore-QA, app/events_intake_core  RUN_ID_KEY, TASK_ID_KEY = "runID", "taskID"
                                                RUN_ID_ALIAS, TASK_ID_ALIAS = "runId", "taskId"

⇒ runID is canonical. runId is a DEPRECATED ALIAS — accepted for one minor version, normalised away before the event reaches the bus, and logged so the lapse is visible. Both spellings work on the wire today; only one is correct, and the alias is removed next minor.

⚠️ WHY THIS IS RECORDED RATHER THAN QUIETLY REWRITTEN. A stale line that merely goes out of date costs a reader some confusion. This one was an instruction in the imperative, pointing at the spelling the estate had just ruled against — and it was believed: it was quoted back as evidence during the 2026-09-28 marketing-site verification. A caution that names a deployed artefact's behaviour is a measurable claim; measure it on the way past.

No credentials, ever

RestTransport takes getAuthToken: () => ..., a callback, and refuses at construction if handed token/apiKey/secret/password as a value. The SDK holds no credential. EventBridgeTransport takes a client the host constructs, so this package declares no @aws-sdk dependency and stays installable where that would not resolve.

Why the source is typed JavaScript rather than .ts

tsc runs in checkJs + emitDeclarationOnly mode: the source is strict-typechecked and .d.ts files are generated from it, so TS consumers get full types from one artefact instead of a .ts that compiles into a .js nobody reads. ⚠️ The load-bearing consequence: npm test runs under node --test with ZERO dependencies, so the cross-language contract test executes in CI today rather than waiting on an npm ci. A contract test that cannot run yet is a promise, not a gate.

npm test        # node --test test/   — no install required
npm run typecheck
npm run build   # emits types/

From the repo root: make sdk-js-test, make sdk-js-typecheck, make sdk-js-build, make sdk-envelope-parity.