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

@smithy-hono/adapter-aws

v0.2.6

Published

AWS (DynamoDB + Secrets Manager) runtime adapter for @smithy-hono/security-core. Implements the four storage interfaces (strongly-consistent rate-limit + nonce via DynamoDB conditional writes / CAS) plus platform glue (forwarded-proto / client-IP resolver

Readme

@smithy-hono/adapter-aws

AWS runtime adapter for @smithy-hono/security-core. Implements the four storage interfaces (ARCH-03) against AWS backends — DynamoDB for the session / rate-limit / nonce stores, Secrets Manager for HMAC signing keys — plus the platform glue (forwarded-proto resolver, client-IP resolver, CloudWatch logger).

Rate-limit and nonce stores are strongly consistent (plan 11 mandate): DynamoDB conditional writes / optimistic-concurrency CAS, so a single logical bucket never overspends and a nonce is accepted exactly once.

Dependency discipline (ARCH-01)

This package does not import @aws-sdk/*. The stores depend only on a narrow structural DynamoTablePort; the real port talks to DynamoDB through a structural DynamoSendLike client, and the SecretProvider through a structural SecretsSourceLike. So it typechecks and runs its full test suite with no AWS SDK installed. The SDK packages are listed as optional peer dependencies; install them only in your consumer service:

@aws-sdk/client-dynamodb        (optional peer)
@aws-sdk/lib-dynamodb           (optional peer — recommended: DocumentClient)
@aws-sdk/client-secrets-manager (optional peer)

Public exports

| Export | Purpose | |--------|---------| | DynamoSessionStore | SessionStore over the port | | DynamoRateLimitStore | RateLimitStore (strongly-consistent token bucket via CAS) | | DynamoNonceStore | NonceStore (first-write-wins conditional put) | | SecretsManagerSecretProvider | SecretProvider over Secrets Manager | | createDynamoTablePort(client, tableName) | real DynamoDB port | | computeTokenBucket(state, cost, spec, nowMs) | pure token-bucket math | | awsForwardedProto(opts?) | TransportConfig.forwardedProtoHeader resolver | | awsClientIp(opts?) | RateLimitConfig.clientIp resolver | | createConsoleLogger(sink?) | structured Logger → CloudWatch | | types | DynamoTablePort, ItemKey, DynamoSendLike, SecretsSourceLike, DynamoSecretProviderOptions, BucketState, BucketResult, PK_ATTR/TTL_ATTR/VERSION_ATTR |

./test-support entry: FakeDynamoTablePort, FakeSecretsSource — in-process fakes with the same conditional/CAS atomicity, for running the conformance suite locally.

Structural client interfaces the consumer satisfies

interface DynamoSendLike   { send(command: unknown): Promise<{ Item?: Record<string, unknown> } & Record<string, unknown>> }
interface SecretsSourceLike { getSecretString(secretId: string): Promise<string | null> }

Required DynamoDB table schema

One physical table backs all three stores (keys are namespaced: sess: / rl: / nonce:).

| Attribute | Type | Role | |-----------|------|------| | pk | String | Partition key (the only key attribute; no sort key) | | ttl | Number | TTL attribute (epoch SECONDS) — enable DynamoDB TTL on it | | version | Number | Optimistic-concurrency token — managed by the port; do not touch |

DynamoDB TTL deletion is eventual (can lag the deadline by up to ~48h). The stores therefore carry a precise expiresAtMs (epoch millis) and perform an in-code expiry check on read — they never trust the backend to have swept a dead row. ttl exists only to let DynamoDB reclaim storage.

Wiring the real port (DocumentClient recommended)

Because this package cannot import @aws-sdk/lib-dynamodb, createDynamoTablePort hands .send a tagged plain command input ({ __command: 'Put'|'Get'|'Update'| 'Delete', ...input }). Supply a tiny DynamoSendLike that turns the tag into the real command:

import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import {
  DynamoDBDocumentClient, PutCommand, GetCommand, UpdateCommand, DeleteCommand,
} from '@aws-sdk/lib-dynamodb'
import {
  createDynamoTablePort, DynamoSessionStore, DynamoRateLimitStore, DynamoNonceStore,
} from '@smithy-hono/adapter-aws'

const doc = DynamoDBDocumentClient.from(new DynamoDBClient({}))
const sendLike = {
  send(cmd: any) {
    const { __command, ...input } = cmd
    const C = { Put: PutCommand, Get: GetCommand, Update: UpdateCommand, Delete: DeleteCommand }[__command]
    return doc.send(new C(input))
  },
}

const port = createDynamoTablePort(sendLike, process.env.SECURITY_TABLE!)
const stores = {
  session:   new DynamoSessionStore(port),
  rateLimit: new DynamoRateLimitStore(port),
  nonce:     new DynamoNonceStore(port),
}

Reads use ConsistentRead: true and writes use ConditionExpression, satisfying the strong-consistency mandate. The CAS path retries on ConditionalCheckFailedException (bounded); under exhausted contention the rate-limit store fails safe by denying (never overspends).

Wiring the SecretProvider

import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'
import { SecretsManagerSecretProvider } from '@smithy-hono/adapter-aws'

const sm = new SecretsManagerClient({})
const source = {
  async getSecretString(secretId: string) {
    const out = await sm.send(new GetSecretValueCommand({ SecretId: secretId }))
    return out.SecretString ?? null
  },
}

const secrets = new SecretsManagerSecretProvider(source, {
  keyIdToSecretId:      { 'k-2026-06': 'prod/sig/k-2026-06', 'k-2026-05': 'prod/sig/k-2026-05' },
  clientToCurrentKeyId: { 'svc-orders': 'k-2026-06' },
})

Key material encoding: the Secrets Manager secret string is the raw HMAC key encoded as base64. It is imported via crypto.subtle.importKey('raw', bytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']) — verify-only, non-extractable, cached per keyId. Keys never appear in code or config (SIGN-06). During rotation, map both the current and previous keyId to their secrets (SIGN-05); getSigningKey returns null for an unknown/retired keyId.

Wiring the glue

import { awsForwardedProto, awsClientIp, createConsoleLogger } from '@smithy-hono/adapter-aws'

const config = {
  // ...security-core config...
  forwardedProtoHeader: awsForwardedProto(),  // reads X-Forwarded-Proto
  clientIp:             awsClientIp(),         // leftmost X-Forwarded-For
  logger:               createConsoleLogger(), // JSON → CloudWatch
}
  • forwarded-proto: defaults to X-Forwarded-Proto (ALB / API Gateway REST set it). API Gateway HTTP API (payload v2.0) may instead carry the scheme in the request context / cloudfront-forwarded-proto — pass awsForwardedProto({ headerName: 'cloudfront-forwarded-proto' }) or supply your own resolver reading the hono/aws-lambda request context. Absent → undefinedassertHttps rejects (fail closed).
  • client IP: leftmost X-Forwarded-For. Trusted-hop assumption: this is correct only because AWS edges (ALB / API Gateway) normalize the header; the value is client-spoofable behind an untrusted edge (e.g. a bare Lambda Function URL). Use awsClientIp({ trustedHopsFromRight: n }) to take the Nth-from-right entry your trusted proxy appended, or your own resolver using the API Gateway request-context source IP.
  • logger: one JSON line per record via console.* → CloudWatch Logs (queryable with Logs Insights).

Local vs. CI validation split

Validated here (this package's vitest, root tooling only — no AWS):

  • All three stores pass the security-core conformance suite against the FakeDynamoTablePort (same conditional/CAS atomicity), including the no-overspend (rate-limit) and exactly-once (nonce) strong-consistency assertions.
  • computeTokenBucket pure math (allow/deny/refill/reset/retry-after).
  • The real createDynamoTablePort over a structural .send mock that evaluates the attribute_not_exists(pk) / version = :expected ConditionExpressions — proving the conditional-put false return and the CAS retry/version logic.
  • The glue resolvers and the Secrets Manager provider (HMAC import + verify).
  • A lightweight simulation of the Lambda base64 event-body decode (below).

Live DynamoDB (src/live.dynamodb.test.ts, gated on DYNAMODB_ENDPOINT) runs the SAME conformance suites through createDynamoTablePort against a real DynamoDB (DynamoDB Local) — validating the conditional writes / version-CAS server-side (no-overspend, exactly-once). Run it locally with ./scripts/verify-live.sh (DynamoDB Local in Docker) or in CI via .github/workflows/live-conformance.yml (DynamoDB service container).

Still deferred (needs a real AWS account, not just an engine):

  • True cross-container writer races (multiple Lambdas hammering one bucket/nonce) and DynamoDB TTL sweep timing — distributed/eventual behaviors only a real deployment shows; the live engine test exercises the CAS path and the in-code expiry guard.
  • Secrets Manager fetch/caching against the real service.

ARCH-08 Lambda raw-body — validated via the real adapter

The raw-body spike (plan/security/11a-rawbody-spike-findings.md) proved readRawBody(c) (= c.req.arrayBuffer()) on Node and reasoned it correct on Workers, but flagged one Lambda-specific transform that cannot be exercised without a real/SAM-local invoke: hono/aws-lambda decodes API Gateway's (possibly isBase64Encoded) event body and builds a Web Request whose body is the decoded bytes — the bytes the client signed, which the SIGN-07 verifier must hash.

This package now exercises that transform two ways: src/lambdaRawBody.test.ts unit-simulates the base64 → decoded-bytes → Request math, and src/lambdaRawBody.real.test.ts drives the real hono/aws-lambda handle() with a base64 API Gateway event and asserts readRawBody(c) sees the decoded bytes (hash matches) while c.req.json() still parses — the actual adapter decode path, no Docker or AWS needed (hono/aws-lambda is a subpath of hono). The only thing left is a genuinely deployed API Gateway → Lambda invoke (deploy smoke, not adapter logic).

Audit sink, metrics & retention (OPS-05, LOG-08/10/12)

security-core emits two structured streams that this package gives an AWS transport, both as one JSON line per record via console.log. On Lambda, console output lands in CloudWatch Logs (queryable with Logs Insights):

  • createConsoleAuditSink()config.audit. A concrete AuditSink for the typed audit trail (auth.failure, authz.deny, ratelimit.trip, sig.fail, session.*, key.rotate). Lines tagged kind: "audit".
  • createConsoleMetricsSink()config.metrics. A concrete MetricsSink for the LOG-08 operational signals (http.5xx, ratelimit.saturation, cert.expiry). Lines tagged kind: "metric".
import {
  createConsoleAuditSink,
  createConsoleMetricsSink,
} from '@smithy-hono/adapter-aws'
import { ChainedAuditSink } from '@smithy-hono/security-core'

const config = {
  // ...stores, logger, etc.
  audit: createConsoleAuditSink({ base: { service: 'todo-api' } }),
  metrics: createConsoleMetricsSink({ base: { service: 'todo-api' } }),
}
// Opt-in LOG-12 tamper-evidence (default off):
//   audit: new ChainedAuditSink(createConsoleAuditSink())

Both factories accept an injectable { sink } (a { log } console-like) so tests can capture lines; it defaults to the ambient console.

Point it at a real destination (deploy-config, no code change): add a CloudWatch Logs subscription filter that fans the lines, routed on the kind discriminator — kind: "audit" to Kinesis Data Streams / Firehose → S3 with Object-Lock (the WORM / 1-yr audit baseline) or a SIEM, distinctly from kind: "metric" and ordinary request logs. Turn the metric lines into a http.5xx rate, a ratelimit.saturation rate, and a cert.expiry gauge with CloudWatch metric filters / alarms. Optional: emit metrics as CloudWatch EMF (a drop-in MetricsSink that formats the embedded-metric envelope) so CloudWatch auto-extracts the metrics.

Cert-expiry hook (LOG-08/TLS-05): ACM/ALB terminates TLS, so core cannot read the cert. A scheduled EventBridge → Lambda computes seconds-remaining and calls emitCertExpiry(metricsSink, { subject, secondsRemaining }) (ACM already publishes DaysToExpiry; this keeps the signal in one pipeline).

Retention (deploy-config baseline): 1 year for audit (kind: "audit"), 90 days for request logs / metrics — enforced at the destination (S3 lifecycle / Object-Lock retention, log-group retention, SIEM policy), never in code, and overridable per deployment. Sampling MAY apply to request logs but MUST NOT apply to audit events (LOG-06).

Build / test

npm run build       # tsc -p tsconfig.build.json → dist
npm test            # vitest run
npm run typecheck   # tsc --noEmit