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

@cross-deck/node

v1.7.0

Published

Crossdeck server SDK for Node.js — verified subscriptions, entitlements, server-side error capture, and product telemetry in one client.

Readme

@cross-deck/node

The Crossdeck server SDK for Node.js — one install, three pillars: errors, analytics, entitlements.

npm install @cross-deck/node

Quick start

import { CrossdeckServer } from "@cross-deck/node";

const crossdeck = new CrossdeckServer({
  secretKey: process.env.CROSSDECK_SECRET_KEY!,
  appId: "app_node_xxxxxxxxxxxx",
  // env is inferred from the key prefix: cd_sk_test_… → sandbox, cd_sk_live_… → production
});

// Optional: validate the key at boot (recommended for serverless cold-starts)
await crossdeck.heartbeat();

// USP 1 — manual error capture
try {
  await processOrder(orderId);
} catch (err) {
  crossdeck.captureError(err, { context: { orderId } });
  throw err;
}

// USP 2 — analytics
crossdeck.track({
  name: "checkout.completed",
  developerUserId: "user_847",
  properties: { plan: "pro", revenue: 9_900 },
});

// USP 3 — entitlement gating (synchronous after first warm)
await crossdeck.getEntitlements({ userId: "user_847" });
if (crossdeck.isEntitled({ userId: "user_847" }, "pro")) {
  // grant access
}

Three USPs, one SDK

USP 1 — Errors

Auto-wired by default: process.on('uncaughtException'), process.on('unhandledRejection'), and globalThis.fetch wrap (5xx + network failures). Plus the full manual surface:

// Manual capture from try/catch
crossdeck.captureError(err, {
  context: { jobId },
  tags: { flow: "checkout" },
  level: "error", // "error" | "warning" | "info"
});

// Non-error signals (Sentry pattern)
crossdeck.captureMessage("deprecated path hit", "warning");

// Pin tags + context to all subsequent errors
crossdeck.setTag("release", process.env.K_REVISION);
crossdeck.setContext("region", { az: "us-east-1a" });

// Add breadcrumbs (last 50 attached to every error report)
crossdeck.addBreadcrumb({
  timestamp: Date.now(),
  category: "custom",
  message: "user.opened_paywall",
});

// Pre-send hook for app-specific PII scrubbing
crossdeck.setErrorBeforeSend((err) => {
  if (err.message.includes("auth-token=")) return null;
  return err;
});

Stack frames are parsed (V8 + Firefox/Safari formats), fingerprinted via djb2 over message + top-3 in-app frames, attached with the breadcrumb buffer + your context + tags. Rate-limited per fingerprint (default 5/min), session-capped (default 100/process). Frames inside node_modules/, node:, internal/, or @cross-deck/node are marked not-in-app and excluded from fingerprints.

To opt out (e.g. if you have a separate error tracker):

new CrossdeckServer({ secretKey, errorCapture: false });

USP 2 — Analytics

track() enqueues synchronously into a durable retry-with-jitter queue with per-batch Idempotency-Key reuse on retry. Flush-on-exit drains before the process terminates — critical for Cloud Functions / Lambda where the runtime freezes the process and any pending events would otherwise vanish.

crossdeck.track({
  name: "paywall_shown",
  developerUserId: "user_847",
  properties: { variant: "v3" },
});

// Super-properties (Mixpanel pattern) — carried on every subsequent event
crossdeck.register({ serviceVersion: process.env.K_REVISION });
crossdeck.unregister("oldField");

// Group analytics — attach $groups.<type> for B2B dashboard pivots
crossdeck.group("org", "acme_inc");
crossdeck.group("team", "design", { headcount: 12 });

// Bulk imports — synchronous POST, returns IngestResponse
await crossdeck.ingest([
  { name: "job.completed", crossdeckCustomerId: "cdcust_x", properties: { durationMs: 1200 } },
  { name: "job.completed", crossdeckCustomerId: "cdcust_y", properties: { durationMs: 950 } },
]);

// Drain the queue (call at end of Lambda/CF invocations)
await crossdeck.flush();

Multi-tenant servers: register() is process-scoped, not per-request. In a single Node process handling requests for many tenants, registering { tenant: "acme" } taints every subsequent event from that process — including ones serving other tenants. For per-request properties, pass them on the track() call itself.

Framework adapters (@cross-deck/node/auto-events)

Plug Crossdeck into your existing framework with a single middleware/wrap call. Auto-emits request.handled / function.invoked / function.completed / function.failed events, captures uncaught errors with request context, and (on Lambda + Firebase) awaits flush() before the handler returns.

import {
  crossdeckExpress,
  crossdeckExpressErrorHandler,
  wrapLambdaHandler,
  wrapFunction,
} from "@cross-deck/node/auto-events";

// Express 4 + 5
app.use(crossdeckExpress(crossdeck, {
  getIdentity: (req) => ({ developerUserId: req.user?.id }),
}));
// ... routes ...
app.use(crossdeckExpressErrorHandler(crossdeck)); // register LAST

// AWS Lambda + Vercel Functions (which run on Lambda underneath)
export const handler = wrapLambdaHandler(crossdeck, async (event, ctx) => {
  return { statusCode: 200, body: "ok" };
});

// Firebase Functions v1 + v2, Cloud Run (generic shape-preserving wrap)
export const myFunction = onRequest(
  wrapFunction(crossdeck, async (req, res) => {
    res.send("ok");
  }),
);

USP 3 — Entitlements

Per-customer TTL cache (default 60s). Hot-path entitlement gates become synchronous memory reads after the first warm. Bounded by maxCustomers (default 10,000) with LRU eviction for long-running multi-tenant servers.

// Warm the cache (records userId → customerId alias)
await crossdeck.getEntitlements({ userId: "user_847" });

// Synchronous gate — memory read within TTL, no HTTP
if (crossdeck.isEntitled({ userId: "user_847" }, "pro")) {
  // grant access
}

// Full snapshot for callers needing source / validUntil
const ents = crossdeck.listEntitlements({ userId: "user_847" });

// Subscribe to cache mutations (e.g. push to connected clients)
const unsubscribe = crossdeck.onEntitlementsChange((customerId, ents) => {
  // ...
});

// Server-side manual overrides
await crossdeck.grantEntitlement({
  customerId: "cdcust_123",
  entitlementKey: "pro",
  duration: "P30D",
  reason: "Support recovery after billing incident",
});
await crossdeck.revokeEntitlement({
  customerId: "cdcust_123",
  entitlementKey: "pro",
  reason: "Chargeback",
});

Webhook signature verification

Stripe-compatible HMAC-SHA256 with constant-time comparison + replay window. Supports multi-secret rotation.

import { verifyWebhookSignature } from "@cross-deck/node";
import express from "express";

app.post("/crossdeck-webhook", express.raw({ type: "application/json" }), (req, res) => {
  try {
    const event = verifyWebhookSignature(
      req.body.toString("utf8"),
      req.headers["crossdeck-signature"],
      [process.env.CROSSDECK_WEBHOOK_SECRET, process.env.CROSSDECK_WEBHOOK_SECRET_OLD],
      // 5-min default replay window
    );
    handleCrossdeckEvent(event);
    res.sendStatus(200);
  } catch (err) {
    res.sendStatus(401);
  }
});

For test fixtures that need to mint signed webhooks against the same scheme, signWebhookPayload(payload, secret, timestampSec) is exported.

Cross-cutting

Runtime info

Auto-detected at construction. Attached to every event + error as runtime.* properties:

| Detected platform | Trigger env var | Surfaces as runtime.host | |---|---|---| | AWS Lambda + Vercel Functions | AWS_LAMBDA_FUNCTION_NAME | aws-lambda | | Azure Functions | FUNCTIONS_WORKER_RUNTIME + WEBSITE_INSTANCE_ID | azure-functions | | Google App Engine | GAE_APPLICATION | google-app-engine | | Firebase Functions v2 / Cloud Functions Gen 2 | K_SERVICE + FIREBASE_CONFIG | firebase-functions-v2 | | Firebase Functions v1 | FUNCTION_NAME + FUNCTION_REGION | firebase-functions-v1 | | Google Cloud Run | K_SERVICE + K_REVISION (no Firebase) | cloud-run | | Vercel | VERCEL === "1" | vercel | | Netlify Functions | NETLIFY === "true" | netlify | | Heroku | DYNO | heroku | | Render | RENDER === "true" | render | | Railway | RAILWAY_ENVIRONMENT | railway | | Fly.io | FLY_APP_NAME | fly | | Generic Kubernetes | KUBERNETES_SERVICE_HOST | kubernetes | | Plain Node | (fallback) | node |

Every detected platform exposes serviceName, serviceVersion, region, instanceId where available. Override via constructor:

new CrossdeckServer({
  secretKey,
  serviceName: "my-fn",
  serviceVersion: process.env.K_REVISION,
  appVersion: "1.2.3", // attached to events as `appVersion`
});

Diagnostics

const d = crossdeck.diagnostics();
// {
//   sdkVersion, baseUrl, secretKeyPrefix (masked), env,
//   runtime: { nodeVersion, platform, host, region, serviceName, ... },
//   events: { buffered, dropped, inFlight, consecutiveFailures, ... },
//   errors: { sessionCount, fingerprintsTracked, handlersInstalled },
//   entitlements: { count, ttlMs, lastUpdated, listenerErrors },
// }

Useful for /health and /metrics endpoints exposed to your platform.

Debug mode

new CrossdeckServer({ secretKey, debug: true });

Emits NorthStar §16 debug signals to console.info:

  • sdk.configured — boot confirmation
  • sdk.first_event_sent — proves wire connectivity
  • sdk.flush_retry_scheduled — surfaces flush failures + retry delay
  • sdk.flush_on_exit_started / sdk.flush_on_exit_completed — drain lifecycle
  • sdk.entitlement_cache_warm / sdk.entitlement_cache_used — cache observability
  • sdk.webhook_verified — signature verification confirmation
  • sdk.sensitive_property_warning — flagged property names on track()
  • sdk.runtime_detected — host platform detection

PII scrub utility

Opt-in regex-based scrub for email + card-number-shaped substrings. Use before forwarding caller-supplied properties:

import { scrubPiiFromProperties } from "@cross-deck/node";

crossdeck.track({
  name: "checkout.failed",
  developerUserId,
  properties: scrubPiiFromProperties({
    url: req.url, // /users/[email protected]/ → /users/[email]/
    failedCardLast4: payload.card_number, // 4242 4242 4242 4242 → [card]
  }),
});

Configuration

All options on new CrossdeckServer({...}):

{
  secretKey: string;            // required — `cd_sk_test_…` (sandbox) | `cd_sk_live_…` (production)
  baseUrl?: string;             // default "https://api.cross-deck.com/v1"
  timeoutMs?: number;           // default 15_000, 0 disables
  appId?: string;               // optional metadata on event envelope
  sdkVersion?: string;          // override the version reported on the wire

  // USP 1
  errorCapture?: boolean | Partial<ErrorCaptureConfig>;
    // false to disable; partial object to override specific hooks
    // (onUncaughtException, onUnhandledRejection, wrapFetch, etc.)

  // USP 2
  eventFlushBatchSize?: number; // default 20
  eventFlushIntervalMs?: number;// default 1500
  flushOnExit?: boolean;        // default true — beforeExit + SIGTERM + SIGINT drain
  flushOnExitTimeoutMs?: number;// default 2000

  // USP 3
  entitlementCacheTtlMs?: number; // default 60_000, 0 disables

  // Cross-cutting
  serviceName?: string;         // overrides env-detected
  serviceVersion?: string;      // overrides env-detected
  appVersion?: string;          // attached as `appVersion` on events
  debug?: boolean;              // default false
  breadcrumbsMaxSize?: number;  // default 50

  // Bank-grade SDK extras (QA-review v2)
  testMode?: boolean;           // default false — short-circuits HTTP to synthetic responses
  onRequest?: (info) => void;   // fires on every request (incl. retries)
  onResponse?: (info) => void;  // fires on every response
  httpRetries?: {               // idempotent GET retry policy
    maxAttempts?: number;       // default 3 (1 initial + 2 retries)
    retryableStatuses?: number[]; // default [408, 500, 502, 503, 504]
  };
  runtimeToken?: string;        // override the User-Agent runtime token
}

Error model

Stripe-style subclass hierarchy. Use instanceof for typed narrowing in your catch blocks.

import {
  CrossdeckError,
  CrossdeckAuthenticationError,
  CrossdeckRateLimitError,
  CrossdeckNetworkError,
  isCrossdeckErrorCode,
} from "@cross-deck/node";

try {
  await crossdeck.heartbeat();
} catch (err) {
  if (err instanceof CrossdeckAuthenticationError) {
    // 401 path — bad/revoked secret key, or bad webhook signature
  } else if (err instanceof CrossdeckRateLimitError) {
    // 429 — back off for err.retryAfterMs
  } else if (err instanceof CrossdeckNetworkError) {
    // fetch failed / aborted / timed out — likely transient
  } else if (err instanceof CrossdeckError) {
    if (isCrossdeckErrorCode(err.code) && err.code === "invalid_secret_key") {
      // narrowed to the catalogue's literal union
    }
    console.error(err.type, err.code, err.requestId);
  }
}

Subclasses: CrossdeckAuthenticationError, CrossdeckPermissionError, CrossdeckValidationError, CrossdeckRateLimitError, CrossdeckNetworkError, CrossdeckInternalError, CrossdeckConfigurationError. All extend CrossdeckError. Constructed automatically by the SDK — you never need to instantiate them yourself.

CrossdeckErrorCode is the literal union of every documented code in CROSSDECK_ERROR_CODES. Use isCrossdeckErrorCode to narrow string to the union for type-safe comparisons (catches misspelled codes at compile time).

err.toJSON() is implemented — your structured logger sees type, code, requestId, status, retryAfterMs, and stack instead of just name + message:

logger.error({ err }, "crossdeck request failed");
// → { err: { name: "CrossdeckRateLimitError", type: "rate_limit_error",
//             code: "too_many_requests", retryAfterMs: 30000, ... } }

Every entry in CROSSDECK_ERROR_CODES carries { code, type, description, resolution, retryable } — render-able in dashboards and AI assistants.

Reliability + lifecycle

Idempotent GET retry

Read methods (getEntitlements, getCustomerEntitlements, getAuditEntry, heartbeat) automatically retry on 408 + 5xx (except 501) and on network failures. Default 3 attempts with exponential backoff + full jitter. Honours server Retry-After. Configurable per-instance:

new CrossdeckServer({
  secretKey,
  httpRetries: { maxAttempts: 5 }, // up to 5 attempts
});

POST methods (track/ingest/syncPurchases/grantEntitlement/revokeEntitlement) DO NOT auto-retry at the HTTP layer. Retries happen via the event queue with per-batch Idempotency-Key reuse — the server can dedupe replays.

v1.4.0 — syncPurchases deterministic key. The Idempotency-Key on syncPurchases is derived from the request body (UUID-shaped SHA-256 of crossdeck:purchases/sync:<rail>:<jws|token>). Two retries of the same Apple transaction land on the same key, so the backend short-circuits with idempotent_replay: true instead of double-processing. Override via options.idempotencyKey only when an outer orchestrator needs a different idempotency window.

AbortSignal — caller-controlled cancellation

Every async method accepts a final RequestOptions? with { signal, timeoutMs }:

const ctrl = new AbortController();
const flight = crossdeck.heartbeat({ signal: ctrl.signal });
setTimeout(() => ctrl.abort(), 100);
try {
  await flight;
} catch (err) {
  if (err instanceof CrossdeckNetworkError && err.code === "request_aborted") {
    // caller-cancelled
  }
}

PII scrubber (v1.4.0 — parity with Web/RN/Swift)

Every track() payload runs through scrubPiiFromProperties before enqueue — email-shaped and card-number-shaped substrings are rewritten to <email> / <card> sentinels recursively across nested objects + arrays. Default: on. Pre-v1.4.0 the Node SDK was the only one that skipped this, shipping payloads UNREDACTED despite the README promising parity.

Opt out only for regulator-required audit trails where the raw value must be preserved:

new CrossdeckServer({ secretKey, scrubPii: false });

Blast radius: every track() payload — event names with embedded emails, trait values, group memberships, error context blobs — ships verbatim to Crossdeck and downstream warehouses / analytics exports. Document the decision at the call site.

Shutdown — flush before exit (v1.4.0 contract)

The server holds a buffered event queue. A clean teardown MUST flush the buffer before dropping it, otherwise events queued between the last flush and shutdown are silently lost.

Three teardown paths, three contracts:

| Method | Flushes? | Use when | | ------ | -------- | -------- | | await server.shutdown() | YES — awaits internal flush() then tears down | Default. Use this in graceful-shutdown handlers. | | await using server = ... + [Symbol.asyncDispose] | YES — equivalent to await server.shutdown() | TC39 explicit-resource-management blocks. | | server.shutdownSync() / using + [Symbol.dispose] | NO — drops the buffer | ONLY when the runtime cannot await (signal handlers, process.exit fallthrough). |

// Graceful shutdown (recommended)
process.on("SIGTERM", async () => {
  await server.shutdown();
  process.exit(0);
});

// Explicit-resource-management (Node 20+ / TS 5.2+)
{
  await using server = new CrossdeckServer({ secretKey });
  // ... use server ...
} // [Symbol.asyncDispose] fires here, awaits flush

shutdownSync() (and the sync [Symbol.dispose] that wraps it) logs a console.warn with the dropped-event count whenever the buffer is non-empty at sync-teardown time — silent loss is incompatible with the bank-grade contract.

EventEmitter — internal events

CrossdeckServer extends EventEmitter. Subscribe to internal lifecycle events with typed listeners:

crossdeck.on("queue.flush_failed", ({ error, attempt, nextRetryMs }) => {
  metrics.increment("crossdeck.flush_failed", { attempt });
});
crossdeck.on("error.captured", ({ fingerprint, kind, message }) => {
  // forward to your other observability tools
});
crossdeck.on("sdk.shutdown", ({ reason }) => {
  // last-chance cleanup
});

Events: queue.flush_succeeded, queue.flush_failed, queue.dropped, queue.buffer_changed, error.captured, entitlements.warmed, sdk.shutdown.

Health probes — Kubernetes / load balancers

crossdeck.isReady();   // synchronous: false on sustained retry storm or buffer pressure
await crossdeck.awaitReady(2000); // backpressure-aware wait
crossdeck.getHealth(); // full snapshot

// Express health endpoint
app.get("/healthz", (_req, res) => {
  const h = crossdeck.getHealth();
  res.status(h.healthy ? 200 : 503).json(h);
});

Explicit resource management

TC39 using / await using syntax (Node 20+, TS 5.2+):

{
  using crossdeck = new CrossdeckServer({ secretKey });
  // ... use crossdeck ...
} // crossdeck[Symbol.dispose]() runs — handlers cleaned up

async function lambdaHandler(event) {
  await using crossdeck = new CrossdeckServer({ secretKey });
  crossdeck.track({ name: "handler.invoked", developerUserId: event.userId });
  // ... do work ...
} // crossdeck[Symbol.asyncDispose]() runs — awaits flush() then cleans up

testMode — caller tests without mocking fetch

const crossdeck = new CrossdeckServer({
  secretKey: "cd_sk_test_test",
  testMode: true,
});
// Every call returns a synthetic success shape — no network.
// Use crossdeck.on("entitlements.warmed", ...) etc. to assert behaviour.

onRequest / onResponse hooks

new CrossdeckServer({
  secretKey,
  onRequest: (info) => debug.log({ method: info.method, url: info.url, attempt: info.attempt }),
  onResponse: (info) => metrics.histogram("crossdeck.request_ms", info.durationMs),
});

Synchronous, errors swallowed — telemetry must never break the request pipeline.

Bulk entitlement ops

// Grant `pro_q1_bonus` to a list of customers, bounded concurrency
const results = await crossdeck.bulkGrantEntitlement(
  customerIds.map((customerId) => ({
    customerId,
    entitlementKey: "pro_q1_bonus",
    duration: "P30D",
    reason: "Q1 promo",
  })),
  { maxConcurrency: 10 },
);

const succeeded = results.filter((r) => r.ok);
const failed = results.filter((r) => !r.ok);
// Partial failures preserved as { ok: false, error }

Symmetric bulkRevokeEntitlement(revokes[], options?).

Bank-grade contracts

The SDK ships its own contracts registry — every behavioural guarantee the SDK makes (per-user cache isolation, deterministic Idempotency-Key, queue durability, etc.) lives in contracts/**/*.json at the monorepo root and is bundled into every release. The customer's lockfile pins SDK code + contracts atomically — drift between what the SDK does and what it claims is structurally impossible. See contracts/README.md for the full architecture.

CrossdeckContracts — typed access to the bundled registry

import { CrossdeckContracts } from "@cross-deck/node";

CrossdeckContracts.all();                              // enforced contracts only
CrossdeckContracts.allIncludingHistorical();           // + proposed + retired
CrossdeckContracts.byId("idempotency-key-deterministic");
CrossdeckContracts.byPillar("revenue");
CrossdeckContracts.withStatus("proposed");
CrossdeckContracts.findByTestName("rail namespacing prevents cross-rail collisions");
CrossdeckContracts.sdkVersion;        // "1.5.0"
CrossdeckContracts.bundledIn;         // "@cross-deck/[email protected]"

The Contract type is exported alongside; the binary-stability promise is documented in contracts/README.md.

crossdeckServer.reportContractFailure(input) — surface contract test failures

When a contract test asserts and fails — in your CI, a dogfood run, or a customer integration test — fire a typed crossdeck.contract_failed event over the Crossdeck reliability channel. This is one-way operational telemetry to the Crossdeck operations team (Privacy Policy §6, "Flow B"); it never enters your track() pipeline, never shows in your dashboard, never bills against your event quota. The wire shape is schema-locked at contracts/diagnostics/contract-failed-payload-schema-lock.json:

import { CrossdeckServer } from "@cross-deck/node";

const cd = new CrossdeckServer({ secretKey: process.env.CROSSDECK_SECRET_KEY! });

cd.reportContractFailure({
  contractId: "idempotency-key-deterministic",
  failureReason: "expected cross-SDK oracle to match canonical vector, got drift",
  runContext: process.env.CI ? "ci" : "dogfood",
  runId: process.env.GITHUB_RUN_ID ?? crypto.randomUUID(),
  testRef: {
    file: "tests/idempotency-key.test.ts",
    name: "apple JWS produces the canonical pinned UUID across all 5 SDKs",
  },
});

No new endpoint, no special ingest path — the event lands in the same pipeline every other server-side track() call does. It surfaces immediately in the dashboard's live event feed, the breakdown chart (group by contract_id, sdk_platform), and any alert rule with event = crossdeck.contract_failed.

Properties stamped on the wire:

| Property | Source | |----------|--------| | contract_id | caller | | sdk_version, sdk_platform | auto-stamped (@cross-deck/node ships sdk_platform: "node") | | failure_reason, run_context, run_id | caller | | test_file, test_name | set when testRef is provided | | device_class | optional, set by caller (categorical bucket — e.g. "linux-server", "container", "lambda") |

The wire shape is schema-locked at contracts/diagnostics/contract-failed-payload-schema-lock.json; per-SDK assertion tests gate it on every release. Free-form extra keys are not accepted — adding a field requires an amendment to the schema-lock contract first.

For per-test-framework hooks see contracts/README.md § Reporting contract failures.

Node version

Node 18+. Uses the platform fetch and node:crypto — zero runtime dependencies.

Bundle

dist/index.cjs + dist/index.mjs (main entry) + dist/auto-events/index.cjs + dist/auto-events/index.mjs (framework adapters subpath). Strict TypeScript, full .d.ts for both entries, source maps included.

License

MIT