7.6 KiB
Capability Spec Delta: rules-hono (enterprise-rules-migration)
Package: @suaveplan/rules-hono
Status: PROPOSED
Every requirement carries a
**Verified by:**line at authoring time. (AGENTS.md §13.)
ADDED Requirements
Requirement: ruleMiddleware evaluates a single rule per request
The package SHALL expose ruleMiddleware<TContext>(rule, options) returning a Hono MiddlewareHandler. The middleware SHALL invoke options.extractData? to build the evaluation context (defaulting to a context derived from c.req), call options.engine ?? getDefaultEngine() to evaluate, store the EvaluationResult in c.set("ruleResult", result), and call options.onDenied? (or short-circuit with 403) when the rule fails.
Scenario: passes through to next handler when rule passes
- WHEN a Hono app uses
ruleMiddleware(passingRule)and a request is dispatched - THEN the next handler is invoked and
c.var.ruleResult.passed === true
Scenario: 403 short-circuit when rule fails
- WHEN a Hono app uses
ruleMiddleware(failingRule)and noonDeniedis supplied - THEN the response status is 403 and the next handler is not invoked
Verified by: src/middleware/rule.test.ts::"passes through on rule pass" AND src/middleware/rule.test.ts::"403 short-circuits on rule fail"
Requirement: validationMiddleware validates request body, query, params, and headers
The package SHALL expose validationMiddleware(options) accepting per-source schemas under body / query / params / headers. The middleware SHALL collect errors per source as HonoValidationError, call options.onError? (or short-circuit with 400 returning a JSON body) when any errors exist, and honour abortEarly to stop at the first failing source.
Scenario: body schema rejects malformed JSON
- WHEN a request with
Content-Type: application/jsonand a body that fails the body schema is dispatched - THEN the response status is 400 and the body lists every failing field with
source: "body"
Scenario: abortEarly stops at first failing source
- WHEN body and query both fail and
abortEarly: trueis set - THEN the response body lists only
bodyerrors
Verified by: src/middleware/validation.test.ts::"body schema rejects malformed JSON" AND src/middleware/validation.test.ts::"abortEarly stops at first failing source"
Requirement: accessControl evaluates a ruleset with caching
The package SHALL expose accessControl<TContext>(options) taking a single rule or a ruleset, optionally an injectable cache: { get(k): Result | undefined; set(k, v, ttl): void } keyed by options.cacheKey?(c). On pass, the result SHALL be stored in c.set("accessResult", result); on fail, options.onDenied? runs or a 403 is returned.
Scenario: cache hit short-circuits evaluation
- WHEN the cache returns a passed
EvaluationResultfor the computed key - THEN the rule engine is not invoked and the next handler runs with
c.var.accessResultpopulated from cache
Scenario: 403 on ruleset denial includes errors
- WHEN a ruleset evaluates to
passed: false - THEN the response status is 403 and the JSON body includes the
errorsarray
Verified by: src/middleware/access-control.test.ts::"cache hit short-circuits engine" AND src/middleware/access-control.test.ts::"403 includes ruleset errors"
Requirement: responseValidation validates outgoing JSON bodies
The package SHALL expose responseValidation(options) that runs AFTER the downstream handler. The middleware SHALL parse the response body when content-type is application/json, validate it against options.schema, and call options.onError? (or replace the response with a 500 and a JSON envelope) when validation fails.
Scenario: invalid response body replaced with 500 envelope
- WHEN the downstream handler returns JSON that fails the schema
- THEN the final response has status 500 and a JSON body with
errors
Verified by: src/middleware/response-validation.test.ts::"invalid body replaced with 500"
Requirement: rateLimitRule returns 429 when its rule fails
The package SHALL expose rateLimitRule<TContext>(options) evaluating options.rules against a context built by options.extractContext. On failure, the response SHALL be 429 with a Retry-After header when options.retryAfterSeconds? is provided, optionally allowing options.onLimited? to customise the response.
Scenario: 429 with Retry-After when over limit
- WHEN the rule fails and
retryAfterSeconds: 60is configured - THEN the response status is 429 and
Retry-After: 60
Verified by: src/middleware/rate-limit-rule.test.ts::"429 with Retry-After"
Requirement: combineRules chains middlewares with early-response short-circuit
The package SHALL expose combineRules(...middlewares) returning a single MiddlewareHandler. The combined middleware SHALL run each child sequentially, short-circuit on the first child that writes a response (no further children run), and pass control to the next handler if all children pass.
Scenario: first early response short-circuits
- WHEN the first child sends a 403 response
- THEN the second child is not invoked
Scenario: all-pass chains through
- WHEN every child passes through
- THEN the next handler runs and observes context variables set by each child
Verified by: src/middleware/combine.test.ts::"first response short-circuits" AND src/middleware/combine.test.ts::"all-pass chains through"
Requirement: Default engine is lazy and per-process
The package SHALL expose getDefaultEngine() returning a process-singleton RuleEngine constructed on first call (no module-level instantiation). Tests SHALL be able to reset the singleton via resetDefaultEngine() (exported under the ./testing subpath only).
Scenario: first call constructs the singleton
- WHEN
getDefaultEngine()is called twice - THEN the second call returns the identical reference (
===)
Scenario: resetDefaultEngine forces reconstruction
- WHEN
resetDefaultEngine()is called, thengetDefaultEngine()returns a fresh instance - THEN the new instance is not
===to the previous one
Verified by: src/engine/default.test.ts::"singleton identity" AND src/engine/default.test.ts::"resetDefaultEngine forces reconstruction"
Requirement: All middleware options are validated via Zod at construction
Every middleware factory SHALL Zod-validate its options argument at call time and raise RulesError with code: "RULES.PLUGIN_FAILED" (re-used from the rules core for option-validation failures) when validation rejects.
Scenario: validationMiddleware rejects malformed options
- WHEN
validationMiddleware({ body: "not-a-schema" } as any)is called - THEN the call throws
RulesErrorcode: "RULES.PLUGIN_FAILED"
Verified by: src/middleware/options.spec-conformance.test.ts::"rejects malformed options"
Requirement: OTel surface emits from middleware paths
The package SHALL emit rules_hono.middleware.<name>.duration_ms{outcome} and rules_hono.middleware.<name>_total{outcome} for each of the six middleware factories, asserted via an in-memory MeterProvider.
Scenario: every middleware emits its duration histogram
- WHEN the full test suite runs against an in-memory
MeterProvider - THEN each of the six middlewares has at least one recorded
duration_msmeasurement
Verified by: src/middleware/otel.spec-conformance.test.ts::"every middleware emits duration_ms"