@cfxlabsinc/b2b-services
    Preparing search index...

    Module @cfxlabsinc/controllers

    controllers

    Common utilities for API controllers and their HTTP runtimes.

    Please do not place any functional/business common stuff in here. This is purely for non-functional or cross-functional components.

    Every app serves the same declarative route tables (ApiRoutes objects in each *-api package's src/interface/) through the Lambda runtime:

    Import Runtime
    @cfxlabsinc/controllers Shared vocabulary — error schemas, route types, TypeBox helpers
    @cfxlabsinc/controllers/lambda createLambdaHandler — the request pipeline
    @cfxlabsinc/controllers/http serveHttpnode:http dev shell over the same handler
    @cfxlabsinc/controllers/openapi buildOpenApiDocument — build-time spec assembly

    The runtime replaced Fastify + @fastify/aws-lambda + AJV + fast-json-stringify with a router over event.path, TypeBox's JIT Validator, and a small reply shim. It cut roughly 150ms off every invocation on the first apps that moved. Fastify is gone from the repo: no package depends on it, and the spec generator assembles the OpenAPI document itself rather than reading it out of @fastify/swagger.

    src/index.ts must never re-export ./lambda, ./http or ./openapi, and nothing under src/lambda/ may import Fastify or src/http/. Those are separate esbuild entry points, which is what keeps build-time and dev-only code out of every production Lambda bundle. test/packageBoundaries.test.ts enforces all three; a type-only import type { FastifyReply } trips it too, because it signals the wrong dependency direction even though the build erases it.

    Three things bite, and none of them fail loudly:

    1. logger() is not request-scoped. It returns a console-backed logger with no invocation keys, kept for event consumers and service internals that are handed no logger of their own. Anything reached from a route should take request.log — the per-request Powertools child logger, which carries the request id and whatever appendKeys put on it.
    2. Handlers receive { params, query, headers, body, customerId, log, event }. The first four names match Fastify's FastifyRequest, so most handler bodies port unchanged. customerId and log are injected explicitly — there is no ambient context, deliberately: a Lambda environment serves one invocation at a time, but the dev shell interleaves, so ambient state would be correct in prod and wrong locally.
    3. Declare a response schema for every status your handler sends. augmentRouteSchema injects default: TInternalError on every route, so a payload returned at an undeclared status is cleaned against that schema and stripped. Returning 201 while declaring only 200 yields an empty body.
    4. cleanResponse mutates its payload and returns the same reference — Validator.Clean() works in place. Do not read or log a response payload after it has been serialised.

    maintenanceService.register() is handled for you: createLambdaHandler calls it once per execution environment. You do not need a cold-start hook.

    These were verified request-by-request against the Fastify stack while both existed. They are intentional; do not "fix" them into parity.

    • 404 messages omit the query string. Fastify built the message from request.url; the Lambda path uses event.path. GET /v1/x?a=1 now yields Cannot route to GET /v1/x. Echoing a client-controlled query string into an error body is an unnecessary reflection surface.
    • Validation error wording differs. AJV and TypeBox phrase individual errors[].message entries differently. The envelope ({code, message, errors}) and each entry's shape ({keyword, instancePath, schemaPath, params, message}) are identical.
    • Auth path exemptions match the path, not the URL. The Fastify auth plugin tested its ignore regex against request.url, so it exempted GET /v1/withdrawal?x=/health from authentication. authenticate() tests path. This is a security improvement.
    • The router tolerates inconsistent parameter names at the same tree position (/v1/identity/{id} alongside /v1/identity/{identityId}/documents), which find-my-way rejects at registration. Parameters are stored per route.

    GET and DELETE validation failures used to return INTERNAL. augmentRouteSchema injected a 400 schema only for POST/PATCH/PUT, so a GET/DELETE 400 body fell through to the injected default: TInternalError. Because that schema declares code/message as T.Literal, fast-json-stringify substitutes the literals rather than validating against them — so the response was {"code":"INTERNAL", …} with the field-level errors[] discarded, under a 400 status. It affected the 31 of 41 GET/DELETE operations that did not declare their own 400 (organization-api 17, identity-api 11, deposit-api 3). A base 400 is now injected for every method.

    The Idempotency-Key header schema enforces nothing. augmentRouteSchema injects the key capitalised while the runtime lowercases all headers, so the schema property never matches — true of the Fastify path before it as well. Behaviour is unaffected because idempotency.ts does its own case-insensitive lookup. A real fix has to keep canonical casing in the published OpenAPI spec while matching case-insensitively at runtime, so it spans both the runtime and openapi/.

    ApiRoutes
    HttpMethod
    OpenApiPaths
    RouteSchema
    SentryDiagnostics
    ServerLogger
    TProductLimits
    TUsage
    TIdempotencyKeyInProgressError
    TIdempotencyReusedError
    TInternalError
    TInvalidRequestError
    TNotFoundError
    TProductLimits
    TUnauthorizedError
    TUsage
    augmentRouteSchema
    defaultSentryIntegrations
    describeError
    initSentry
    logger
    parseListQueryParam
    parseOrderByQueryParam
    parsePublicKeyQueryParam
    parseTimestampQueryParam
    sentryDiagnostics
    TServiceError
    verifyHmacSignature
    withApiIdempotency