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

scout-sf

v1.0.0

Published

TypeScript security framework for Node/Express APIs: input sanitization & validation, JWT auth, multi-tenant isolation, rate limiting, and Guardian threat detection. Alpha — feedback welcome.

Readme

scout-sf

Built in. Not bolted on.

scout-sf is an application-layer security framework for Node / TypeScript APIs. It wraps your functions with input sanitization, runtime validation, a call-integrity check, and an automatic audit trail (the DataLog) — and ships Express middleware for identity context, multi-tenant isolation, role-based access control, rate limiting, and Guardian behavioral monitoring.

Security posture (pre-release · v0.2.x). Read this before trusting the green numbers. Scout is defense-in-depth, not a replacement for TLS, parameterized queries / an ORM, JWT signature verification, Postgres RLS, or a WAF. The function-level pipeline (sanitize → validate → integrity → encode → DataLog) is strong and heavily tested. The risk is concentrated in the layer tested least — HTTP / auth / multi-tenant / deploy. All assessment to date is author self-test, not an independent third-party review. Test counts describe the strong layer; they are not a "0 critical" claim. See pentest/ for the honest status.

0.2.0 — cleaned up. This release removed accreted and domain-specific code to get back to a focused core: the compliance-regime modules, the NEC calculation engine, the message-bus specializations (SecureBus / DatabaseBus / BusFactory), and the runtime error-code registry were all removed. What remains is the pipeline plus the capability modules below.


Install

npm install scout-sf

Requirements: Node.js 18+, TypeScript 5.0+.


Quick start

import S from 'scout-sf'

// S.S — the audited path: name-first, rules required, full DataLog.
const createUser = S.S('createUser',
    function createUser(P: { name: string; age: number }): string {
        return `${P.name} (${P.age})`
    },
    {
        name: { type: 'string', minLength: 1, maxLength: 100 },
        age:  { type: 'number', min: 0, max: 150 },
    }
)

// Synchronous by default — no await needed.
createUser({ name: 'Ada', age: 36 })   // → "Ada (36)"
createUser({ name: 'Ada', age: 999 })  // → throws: value 999 exceeds max 150

Scout is synchronous by default and needs no setup to start wrapping functions. The only feature that needs an adapter is rate limiting (see below), and in development (NODE_ENV !== 'production') Scout auto-installs an in-memory one.


What Scout does on every call

0. Guardian check    — behavioral anomaly score checked; blocked users rejected (opt-in)
1. Category check    — business params cannot contain identity or security fields
2. Sanitize          — HTML encoding, SQL-injection rejection, unicode normalization
3. Validate          — runtime type, range, enum, and pattern checking
4. Rate limit        — per-user or per-function (opt-in)
5. Execute           — your function runs with sanitized, validated params
6. Integrity check   — SHA-256 entry/exit call key (input-mutation guard; see note)
7. Output validate   — return type verified when declared
8. Encode            — result encoded before returning
9. DataLog           — every step recorded with WHO called WHAT and WHEN

About step 6. Scout hashes the sanitized params on entry (from a deep clone) and again on exit. A mismatch means the wrapped function mutated its own input — a real correctness bug caught at runtime. It is an input-mutation guard, not tamper detection: any in-process code can recompute a matching key, so it does not defend against an in-process attacker. Treat it as defense-in-depth on your own code.


S.S and S.L — capability model

Scout's modes are named by capability, not by compliance regime.

  • S.S is the audited path. Use it for any operation that needs an audit trail and full attribution. Rules are required, the function must be named (first argument), and every call is fully logged. This produces the evidence trail a SOC 2 assessment draws from when the DataLog is persisted — it does not, by itself, make you compliant.
  • S.L is the light path. Internal tooling, analytics, non-sensitive logic. Rules optional, types-only DataLog.
const a = S.S('myFn', (P: { x: number }): number => P.x, { x: { type: 'number' } })   // name-first
const b = S.L('myFn', (P: { x: number }): number => P.x)                               // name-first
const c = S.L(function myFn(P: { x: number }): number { return P.x })                  // function-first

| Feature | S.S (audited) | S.L (light) | |-------------------|--------------------|------------------------| | Rules | Required | Optional | | Name | Required (1st arg) | Optional | | DataLog detail | Full values | Types only | | Rate limiting | Opt-in | Opt-in | | Output validation | ✓ | ✓ | | Call integrity | ✓ | ✓ |

Both are synchronous by default; both return Promise<R> (and must be awaited) only when you pass { rateLimit } or { guardian: true }.


Three param categories

Developers only ever write business params. Identity and security are assembled automatically and are structurally separate — they cannot be spoofed or forgotten.

// BUSINESS  — you write these
// IDENTITY  — from the auth session: userId, companyId, deviceId  (never written by you)
// SECURITY  — assembled by Scout: jwtHash, timestamp, callKey      (never written by you)

Passing an identity field (userId, companyId) as a business param throws immediately — no silent identity override is possible.


Identity — WHO context

Scout reads identity from the session automatically; you never pass userId/companyId.

import S, { setSession, clearSession } from 'scout-sf'

// Backend: set in middleware after JWT verification. Pass `next` so identity is bound
// to THIS request via AsyncLocalStorage (never a shared global).
setSession({ identity: { userId: 'u_123', companyId: 'c_456' }, jwt: token }, next)

// Frontend (single session per process): set after login, clear on logout.
setSession({ identity: { userId: 'u_123', companyId: 'c_456' }, jwt: token })
clearSession()

Rules

S.S('fn', fn, {
    name:   { type: 'string', minLength: 1, maxLength: 100 },
    age:    { type: 'number', min: 0, max: 150 },
    status: { type: 'string', values: ['active', 'inactive'] },   // enum
    email:  { type: 'string', pattern: /^[^@]+@[^@]+$/, patternError: 'invalid email' },
})

Shorthand strings cover the common cases: 'string', 'string:1-50', 'number', 'number:0-1000', 'boolean', 'enum:a,b,c'. Built-in PATTERNS (lettersOnly, slug, phone, zipCode, …) are available for pattern.

Less verbose: array & composite shorthand

When every param maps to a built-in rule by the same name, pass an array — Scout expands it and infers P from the rule names, so the (P: {...}) annotation is no longer needed:

const VOLTDROP = S.S('voltdrop',
    (P) => (P.phase === 3 ? 1.732 : 2) * P.length * P.amps / (P.awg || 1),
    ['amps', 'awg', 'length', 'phase'],     // P inferred: { amps, awg, length, phase: number }
    { output: 'number', rateLimit: '1000/min' }
)

Combine a reusable rule set with per-function extras using the composite array-of-arrays form (duplicate fields throw at definition time):

const ELECTRICAL = ['amps', 'awg', 'length'] as const
S.S('createEstimate', fn, [ELECTRICAL, { jobName: 'string:1-200' }], { output: 'number' })

Full reference: Shorthand API.

Custom rules

import { defineRules, addRules } from 'scout-sf'

const myRules = defineRules({
    jobName:   { type: 'string', minLength: 1, maxLength: 200 },
    tradeType: { type: 'string', values: ['electrical', 'plumbing', 'hvac'] },
})
addRules(myRules)                                  // register once at startup
S.S('fn', fn, { jobName: 'jobName' })              // then reference by name

tableToRule(record) turns a lookup table's keys into an enum rule.


ScoutMiddleware — Express on-ramp

import express from 'express'
import { scoutStack, scoutErrorHandler } from 'scout-sf'

const app = express()
app.use(express.json())

app.use(scoutStack({ jwtSecret: process.env.SCOUT_JWT_SECRET, sessionRequired: true }))
// ... your routes — req.body is sanitized, session is set, res.json is encoded ...
app.use(scoutErrorHandler())   // mount last

Individual middleware (scoutLogger, scoutBodySanitize, scoutSession, scoutResponseEncode, scoutErrorHandler) can be mounted separately.


Application-layer access control

These are the app-layer complement to network/DB controls — defense-in-depth, not a replacement for Postgres RLS.

import { requireTenant, assertTenantOwnership } from 'scout-sf'   // multi-tenant isolation
import { requireRole, requirePermission }       from 'scout-sf'   // RBAC: owner > admin > member > viewer

ScoutGuardian — behavioral monitoring (opt-in)

Guardian accumulates a per-user anomaly score from repeated validation failures, injection attempts, and integrity violations; past a threshold the user is warned or blocked. It is opt-in (a no-op until an adapter is set) and fail-open (adapter errors never block a call).

import { setGuardianAdapter, InMemoryGuardianAdapter, RedisGuardianAdapter } from 'scout-sf'

setGuardianAdapter(new InMemoryGuardianAdapter())                 // dev / tests
setGuardianAdapter(new RedisGuardianAdapter(redis), { blockThreshold: 200 })   // prod

DataLog & persistence

import { getLogs, clearLogs } from 'scout-sf'
getLogs()    // DataLog[] — every wrapped call this session (WHO/WHAT/WHEN + every step)

For a durable audit trail, flush the DataLog to Postgres with DrizzleDataLogStore + startAuditFlusher (in-process logs are not an audit log).


Rate limiting

import { setRateLimitAdapter, RedisRateLimitAdapter } from 'scout-sf'
setRateLimitAdapter(new RedisRateLimitAdapter(redis))            // prod, set once at startup

const fn = S.S('fn', impl, rules, { rateLimit: '100/min' })     // opt-in; fn becomes async

Error diagnostics

import { ScoutError, SCOUT_ERROR_CODES } from 'scout-sf'

try { fn(params) } catch (err) {
    if (ScoutError.isScoutError(err)) {
        err.code      // 'SCOUT_SQL_INJECTION'
        err.title; err.reason; err.fix; err.docsLink
    }
}

SQL sanitization is a blocklist, not a defense

Scout rejects common SQL-injection patterns at input, but this is defense-in-depth, not a substitute for parameterized queries or an ORM. Always use prepared statements.


What Scout is not

Scout supplements, it does not replace:

  • TLS / HTTPS — encrypt data in transit at the network layer
  • Parameterized queries / ORM — the primary SQL-injection defense
  • JWT signature verification — Scout reads session format, not crypto signatures
  • Row-level security — use Postgres RLS for multi-tenant data isolation
  • Network firewalls / WAFs — Scout operates at the function-call level

Package surface

scout-sf ships a single entry point — everything is a named import from 'scout-sf'.

| Symbol | Contents | |--------|----------| | S (default), S.S / S.L | Core pipeline — sanitize, validate, integrity, encode, DataLog | | setSession, rules, defineRules, ScoutError | Session/identity, rule helpers, error diagnostics | | scoutStack, scoutErrorHandler, … | Express middleware on-ramp | | requireTenant, assertTenantOwnership | Multi-tenant isolation | | requireRole, requirePermission | Role-based access control | | ScoutGuardian exports | Behavioral monitoring, anomaly scoring | | DrizzleDataLogStore, startAuditFlusher | Drizzle-backed audit-log persistence | | scoutBus, connectScoutToBus | In-process module event bus |

Not exported (roadmap): ScoutAuth (email + OAuth login) exists in the repo but is not part of the published barrel; feed your own verified identity to setSession().


Shorthand API · Custom Rules Guide · Error Reference · Integration