@nymbalhq/http 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -246,13 +246,17 @@ function rateLimitResponse(decision, ctx) {
246
246
  }
247
247
 
248
248
  // src/fastify-adapter.ts
249
+ var STREAM_END_BOUND_MS = 500;
249
250
  var FastifyHttpAdapter = class {
250
251
  #app;
251
252
  #logger;
252
253
  #guard;
253
254
  #middlewares = [];
254
255
  #defaultHeaders = {};
256
+ /** Ends each open stream for shutdown; a stream removes itself once its close callbacks have run. */
257
+ #openStreams = /* @__PURE__ */ new Set();
255
258
  #started = false;
259
+ #stopping = false;
256
260
  #boundPort = 0;
257
261
  constructor(options) {
258
262
  this.#logger = options.logger;
@@ -274,6 +278,10 @@ var FastifyHttpAdapter = class {
274
278
  if (config?.routeOptions?.rawBody === true) req.rawBody = buffer;
275
279
  parseJson(req, buffer.toString("utf8"), done);
276
280
  });
281
+ this.#app.addHook("preClose", async () => {
282
+ this.#stopping = true;
283
+ for (const endStream of this.#openStreams) endStream();
284
+ });
277
285
  void this.#app.register(fastifyCookie);
278
286
  if (options.cors && options.cors.allowedOrigins.length > 0) {
279
287
  void this.#app.register(fastifyCors, {
@@ -331,7 +339,16 @@ var FastifyHttpAdapter = class {
331
339
  } catch (err) {
332
340
  return this.#applyEnvelope(reply, this.#renderError(err, ctx));
333
341
  }
334
- reply.raw.writeHead(200, {
342
+ if (this.#stopping) {
343
+ return this.#applyEnvelope(reply, {
344
+ status: 503,
345
+ headers: { connection: "close" },
346
+ body: { error: { code: "unavailable", message: "Server is shutting down" } }
347
+ });
348
+ }
349
+ const raw = reply.raw;
350
+ const socket = req.raw.socket;
351
+ raw.writeHead(200, {
335
352
  ...this.#defaultHeaders,
336
353
  "content-type": "text/event-stream",
337
354
  "cache-control": "no-cache, no-transform",
@@ -340,14 +357,29 @@ var FastifyHttpAdapter = class {
340
357
  });
341
358
  reply.hijack();
342
359
  const closeCallbacks = [];
360
+ let ending = false;
343
361
  let closed = false;
344
- const close = () => {
345
- if (closed) return;
346
- closed = true;
362
+ const end = (then) => {
363
+ if (ending) return;
364
+ ending = true;
347
365
  try {
348
- reply.raw.end();
366
+ raw.end(then);
349
367
  } catch {
350
368
  }
369
+ };
370
+ const endForShutdown = () => {
371
+ const bound = setTimeout(() => socket.destroy(), STREAM_END_BOUND_MS);
372
+ bound.unref();
373
+ end(() => {
374
+ clearTimeout(bound);
375
+ socket.destroy();
376
+ });
377
+ };
378
+ const close = () => {
379
+ end();
380
+ if (closed) return;
381
+ closed = true;
382
+ this.#openStreams.delete(endForShutdown);
351
383
  for (const cb of closeCallbacks) {
352
384
  try {
353
385
  cb();
@@ -358,9 +390,10 @@ var FastifyHttpAdapter = class {
358
390
  };
359
391
  req.raw.on("close", close);
360
392
  req.raw.on("error", close);
393
+ this.#openStreams.add(endForShutdown);
361
394
  const stream = {
362
395
  send(event) {
363
- if (closed) return;
396
+ if (ending) return;
364
397
  let line = "";
365
398
  if (event.id !== void 0) line += `id: ${event.id}
366
399
  `;
@@ -370,7 +403,7 @@ var FastifyHttpAdapter = class {
370
403
  line += `data: ${data}
371
404
 
372
405
  `;
373
- reply.raw.write(line);
406
+ raw.write(line);
374
407
  },
375
408
  close,
376
409
  onClose(cb) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fastify-adapter.ts","../src/security-middleware.ts","../src/rate-limit.ts","../src/routes/health.ts","../src/routes/products.ts","../src/server.ts"],"sourcesContent":["import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify'\nimport fastifyCookie from '@fastify/cookie'\nimport fastifyCors from '@fastify/cors'\nimport { v7 as uuidv7 } from 'uuid'\nimport type {\n HttpAdapter,\n HttpMethod,\n Logger,\n MiddlewareHandler,\n RequestContext,\n ResponseEnvelope,\n RouteHandler,\n RouteOptions,\n SseEvent,\n StreamContext,\n StreamHandler,\n} from '@nymbalhq/types'\nimport { HttpError, NymbalError } from '@nymbalhq/types'\nimport type { NymbalConfig } from '@nymbalhq/config'\nimport { createSecurityGuard, type SecurityGuard } from './security-middleware.js'\n\nexport interface CorsOptions {\n allowedOrigins: string[]\n credentials: boolean\n}\n\nexport interface FastifyAdapterOptions {\n logger: Logger\n /** The guard every route passes before its middlewares and handler: rate limit, then CSRF on unsafe methods. */\n security: NymbalConfig['security']\n /** Whether `X-Forwarded-For` sets the request IP. Off unless a trusted proxy sits in front. */\n trustProxy?: boolean\n cors?: CorsOptions\n}\n\nexport class FastifyHttpAdapter implements HttpAdapter {\n readonly #app: FastifyInstance\n readonly #logger: Logger\n readonly #guard: SecurityGuard\n readonly #middlewares: MiddlewareHandler[] = []\n readonly #defaultHeaders: Record<string, string> = {}\n #started = false\n #boundPort = 0\n\n constructor(options: FastifyAdapterOptions) {\n this.#logger = options.logger\n this.#guard = createSecurityGuard(options.security)\n this.#app = Fastify({\n logger: false,\n trustProxy: options.trustProxy ?? false,\n disableRequestLogging: true,\n genReqId: () => uuidv7(),\n })\n // A signed webhook is verified against the bytes sent, so a route registered with `rawBody` keeps them. The\n // body is still parsed by Fastify's own JSON parser, with its poisoning settings and its 400s.\n const parseJson = this.#app.getDefaultJsonParser(\n this.#app.initialConfig.onProtoPoisoning ?? 'error',\n this.#app.initialConfig.onConstructorPoisoning ?? 'error',\n )\n this.#app.removeContentTypeParser('application/json')\n this.#app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => {\n const buffer = body as Buffer\n const config = req.routeOptions.config as { routeOptions?: RouteOptions } | undefined\n if (config?.routeOptions?.rawBody === true) req.rawBody = buffer\n parseJson(req, buffer.toString('utf8'), done)\n })\n void this.#app.register(fastifyCookie)\n if (options.cors && options.cors.allowedOrigins.length > 0) {\n void this.#app.register(fastifyCors, {\n origin: options.cors.allowedOrigins,\n credentials: options.cors.credentials,\n methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'],\n })\n }\n // The token a client on another origin of the same site echoes in `x-csrf-token`: it cannot read this cookie.\n this.registerRoute('GET', '/api/csrf', (ctx) => this.#guard.issueToken(ctx))\n }\n\n get boundPort(): number {\n return this.#boundPort\n }\n\n registerRoute(\n method: HttpMethod,\n path: string,\n handler: RouteHandler,\n options?: RouteOptions,\n ): void {\n if (this.#started) {\n throw new Error('Cannot register routes after the server has started')\n }\n const routeConfig: {\n method: HttpMethod\n url: string\n handler: (req: FastifyRequest, reply: FastifyReply) => Promise<FastifyReply>\n config: { routeOptions: RouteOptions }\n bodyLimit?: number\n } = {\n method,\n url: path,\n handler: async (req, reply) => {\n const ctx = this.#buildContext(req, options?.rawBody ?? false)\n const refused = this.#guard.admit(ctx, options)\n if (refused) return this.#applyEnvelope(reply, refused)\n let envelope: ResponseEnvelope\n try {\n envelope = await this.#runMiddlewaresThen(ctx, handler)\n } catch (err) {\n envelope = this.#renderError(err, ctx)\n }\n return this.#applyEnvelope(reply, this.#guard.finish(ctx, envelope))\n },\n config: { routeOptions: options ?? {} },\n }\n this.#app.route(routeConfig)\n }\n\n registerStreamRoute(\n method: 'GET',\n path: string,\n handler: StreamHandler,\n options?: RouteOptions,\n ): void {\n if (this.#started) {\n throw new Error('Cannot register routes after the server has started')\n }\n this.#app.route({\n method,\n url: path,\n handler: async (req, reply) => {\n const ctx = this.#buildContext(req, false)\n const refused = this.#guard.admit(ctx, options)\n if (refused) return this.#applyEnvelope(reply, refused)\n try {\n for (const mw of this.#middlewares) {\n const out = await mw(ctx)\n if (out) {\n return this.#applyEnvelope(reply, out)\n }\n }\n } catch (err) {\n return this.#applyEnvelope(reply, this.#renderError(err, ctx))\n }\n reply.raw.writeHead(200, {\n ...this.#defaultHeaders,\n 'content-type': 'text/event-stream',\n 'cache-control': 'no-cache, no-transform',\n connection: 'keep-alive',\n 'x-accel-buffering': 'no',\n })\n reply.hijack()\n const closeCallbacks: Array<() => void> = []\n let closed = false\n const close = (): void => {\n if (closed) return\n closed = true\n try {\n reply.raw.end()\n } catch {\n // ignore\n }\n for (const cb of closeCallbacks) {\n try {\n cb()\n } catch (err) {\n ctx.logger.error({ err }, 'stream close callback failed')\n }\n }\n }\n req.raw.on('close', close)\n req.raw.on('error', close)\n const stream: StreamContext = {\n send(event: SseEvent): void {\n if (closed) return\n let line = ''\n if (event.id !== undefined) line += `id: ${event.id}\\n`\n if (event.event !== undefined) line += `event: ${event.event}\\n`\n const data = typeof event.data === 'string' ? event.data : JSON.stringify(event.data)\n line += `data: ${data}\\n\\n`\n reply.raw.write(line)\n },\n close,\n onClose(cb: () => void): void {\n closeCallbacks.push(cb)\n },\n }\n try {\n await handler(ctx, stream)\n } catch (err) {\n ctx.logger.error({ err }, 'stream handler threw')\n close()\n }\n return reply\n },\n config: { routeOptions: options ?? {} },\n })\n }\n\n registerMiddleware(middleware: MiddlewareHandler): void {\n if (this.#started) {\n throw new Error('Cannot register middleware after the server has started')\n }\n this.#middlewares.push(middleware)\n }\n\n setDefaultHeaders(headers: Record<string, string>): void {\n for (const [k, v] of Object.entries(headers)) {\n this.#defaultHeaders[k.toLowerCase()] = v\n }\n }\n\n async start(port: number, host = '0.0.0.0'): Promise<void> {\n await this.#app.listen({ port, host })\n this.#boundPort = (this.#app.server.address() as { port: number }).port\n this.#started = true\n this.#logger.info({ port: this.#boundPort, host }, 'HTTP server listening')\n }\n\n async stop(): Promise<void> {\n if (!this.#started) return\n await this.#app.close()\n this.#started = false\n }\n\n async #runMiddlewaresThen(ctx: RequestContext, handler: RouteHandler): Promise<ResponseEnvelope> {\n for (const mw of this.#middlewares) {\n const out = await mw(ctx)\n if (out) return out\n }\n return handler(ctx)\n }\n\n #buildContext(req: FastifyRequest, includeRawBody: boolean): RequestContext {\n const correlationId = (req.headers['x-correlation-id'] as string | undefined) ?? req.id\n const logger = this.#logger.child({ requestId: req.id, correlationId })\n const ctx: RequestContext = {\n method: req.method as HttpMethod,\n path: req.routeOptions?.url ?? req.url,\n url: req.url,\n params: (req.params ?? {}) as Record<string, string>,\n query: (req.query ?? {}) as Record<string, string | string[] | undefined>,\n body: req.body,\n headers: req.headers,\n cookies: (req.cookies ?? {}) as Record<string, string>,\n requestId: req.id,\n correlationId,\n logger,\n ip: req.ip,\n }\n if (includeRawBody && Buffer.isBuffer(req.rawBody)) {\n ctx.rawBody = req.rawBody\n }\n return ctx\n }\n\n #applyEnvelope(reply: FastifyReply, envelope: ResponseEnvelope): FastifyReply {\n reply.status(envelope.status)\n for (const [name, value] of Object.entries(this.#defaultHeaders)) {\n reply.header(name, value)\n }\n if (envelope.headers) {\n for (const [name, value] of Object.entries(envelope.headers)) {\n reply.header(name, value)\n }\n }\n if (envelope.cookies) {\n for (const cookie of envelope.cookies) {\n reply.setCookie(cookie.name, cookie.value, {\n path: cookie.path ?? '/',\n ...(cookie.domain !== undefined && { domain: cookie.domain }),\n ...(cookie.maxAge !== undefined && { maxAge: cookie.maxAge }),\n httpOnly: cookie.httpOnly ?? false,\n secure: cookie.secure ?? false,\n ...(cookie.sameSite !== undefined && { sameSite: cookie.sameSite }),\n })\n }\n }\n if (envelope.body === undefined) {\n return reply.send()\n }\n return reply.send(envelope.body)\n }\n\n #renderError(err: unknown, ctx: RequestContext): ResponseEnvelope {\n if (err instanceof HttpError) {\n ctx.logger.warn({ err: err.toJSON() }, err.message)\n return {\n status: err.status,\n body: { error: { code: err.code, message: err.message } },\n }\n }\n if (err instanceof NymbalError) {\n ctx.logger.error({ err: err.toJSON() }, err.message)\n return {\n status: 500,\n body: { error: { code: err.code, message: err.message } },\n }\n }\n const message = err instanceof Error ? err.message : String(err)\n ctx.logger.error({ err }, message)\n return {\n status: 500,\n body: { error: { code: 'internal', message: 'Internal server error' } },\n }\n }\n}\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n rawBody?: Buffer\n }\n}\n","import { randomBytes, timingSafeEqual } from 'node:crypto'\nimport type { NymbalConfig } from '@nymbalhq/config'\nimport type {\n RequestContext,\n ResponseCookie,\n ResponseEnvelope,\n RouteOptions,\n} from '@nymbalhq/types'\nimport { BUILT_IN_RATE_LIMITS, RateLimiter } from './rate-limit.js'\n\nexport const CSRF_COOKIE = 'nymbal.csrf'\nexport const CSRF_HEADER = 'x-csrf-token'\nconst UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nexport function buildSecurityHeaders(security: NymbalConfig['security']): Record<string, string> {\n const headers: Record<string, string> = {}\n if (security.headers.hsts) {\n headers['strict-transport-security'] = 'max-age=31536000; includeSubDomains'\n }\n const csp = security.headers.contentSecurityPolicy\n const cspValue =\n csp === 'strict'\n ? \"default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'\"\n : csp === 'relaxed'\n ? \"default-src 'self' https:; img-src * data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'\"\n : csp === 'off'\n ? null\n : csp\n if (cspValue) headers['content-security-policy'] = cspValue\n headers['referrer-policy'] = security.headers.referrerPolicy\n headers['x-frame-options'] = security.headers.xFrameOptions === 'deny' ? 'DENY' : 'SAMEORIGIN'\n headers['x-content-type-options'] = 'nosniff'\n return headers\n}\n\nexport interface SecurityGuard {\n /**\n * Runs before any middleware or handler: the rate limit, then CSRF on unsafe methods unless the route opted out.\n * Returns the refusal, or null when the request may proceed.\n */\n admit(ctx: RequestContext, options?: RouteOptions): ResponseEnvelope | null\n /** Runs on the handler's answer: security headers, the CSRF cookie, and the outcome of a failure-counted login. */\n finish(ctx: RequestContext, envelope: ResponseEnvelope): ResponseEnvelope\n /** `GET /api/csrf`: the request's CSRF cookie, or a new one set on the response. */\n issueToken(ctx: RequestContext): ResponseEnvelope\n reset(): void\n}\n\n/** Counts failures only, keyed by IP and by account, so an office behind one NAT is not blocked by its own logins. */\nconst LOGIN_PATH = '/api/auth/login'\n\nfunction csrfCookie(token: string): ResponseCookie {\n return {\n name: CSRF_COOKIE,\n value: token,\n path: '/',\n httpOnly: false,\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n }\n}\n\nfunction loginKeys(ctx: RequestContext): string[] {\n const email = (ctx.body as { email?: unknown } | undefined)?.email\n return typeof email === 'string' && email !== ''\n ? [ctx.ip, `account:${email.toLowerCase()}`]\n : [ctx.ip]\n}\n\nfunction isLogin(ctx: RequestContext): boolean {\n return ctx.method === 'POST' && ctx.path === LOGIN_PATH\n}\n\nexport function createSecurityGuard(security: NymbalConfig['security']): SecurityGuard {\n const rateLimiter = new RateLimiter({ ...BUILT_IN_RATE_LIMITS, ...security.rateLimit })\n const baseHeaders = buildSecurityHeaders(security)\n\n function applyHeaders(envelope: ResponseEnvelope): ResponseEnvelope {\n return {\n ...envelope,\n headers: { ...baseHeaders, ...(envelope.headers ?? {}) },\n }\n }\n\n return {\n admit(ctx, options = {}) {\n const decision = isLogin(ctx)\n ? rateLimiter.full(ctx.path, loginKeys(ctx))\n : rateLimiter.check(ctx.ip, ctx.path)\n if (decision && !decision.allowed) {\n return applyHeaders(rateLimitResponse(decision, ctx))\n }\n\n if (security.csrf && options.csrf !== false && UNSAFE_METHODS.has(ctx.method)) {\n if (!verifyCsrf(ctx)) {\n ctx.logger.warn({ method: ctx.method, path: ctx.path }, 'CSRF validation failed')\n return applyHeaders({\n status: 403,\n body: { error: { code: 'csrf.invalid', message: 'Invalid or missing CSRF token' } },\n })\n }\n }\n return null\n },\n\n finish(ctx, envelope) {\n if (isLogin(ctx)) {\n const keys = loginKeys(ctx)\n // Success clears the account only: the IP bucket expires with its window, so logging in to one's own\n // account cannot reset a spray across other accounts.\n if (envelope.status >= 200 && envelope.status < 300) {\n rateLimiter.clear(\n ctx.path,\n keys.filter((key) => key.startsWith('account:')),\n )\n } else rateLimiter.hit(ctx.path, keys)\n }\n const finished = applyHeaders(envelope)\n const setsCsrf = finished.cookies?.some((cookie) => cookie.name === CSRF_COOKIE) ?? false\n if (security.csrf && !ctx.cookies[CSRF_COOKIE] && !setsCsrf) {\n finished.cookies = [...(finished.cookies ?? []), csrfCookie(newToken())]\n }\n return finished\n },\n\n issueToken(ctx) {\n const existing = ctx.cookies[CSRF_COOKIE]\n if (existing) return { status: 200, body: { data: { token: existing } } }\n const token = newToken()\n return { status: 200, body: { data: { token } }, cookies: [csrfCookie(token)] }\n },\n\n reset() {\n rateLimiter.reset()\n },\n }\n}\n\nfunction newToken(): string {\n return randomBytes(32).toString('hex')\n}\n\nfunction verifyCsrf(ctx: RequestContext): boolean {\n const cookieToken = ctx.cookies[CSRF_COOKIE]\n const rawHeader = ctx.headers[CSRF_HEADER]\n const headerToken = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader\n if (!cookieToken || !headerToken) return false\n if (cookieToken.length !== headerToken.length) return false\n try {\n return timingSafeEqual(Buffer.from(cookieToken), Buffer.from(headerToken))\n } catch {\n return false\n }\n}\n\nfunction rateLimitResponse(\n decision: { action: string; retryAfter?: number },\n ctx: RequestContext,\n): ResponseEnvelope {\n ctx.logger.info({ ip: ctx.ip, path: ctx.path, action: decision.action }, 'Rate limit triggered')\n const status = decision.action === 'challenge' ? 403 : 429\n const headers: Record<string, string> = {}\n if (decision.retryAfter) headers['retry-after'] = String(decision.retryAfter)\n return {\n status,\n headers,\n body: {\n error: {\n code: `rate_limit.${decision.action}`,\n message: 'Too many requests',\n ...(decision.retryAfter !== undefined && { retryAfter: decision.retryAfter }),\n },\n },\n }\n}\n","import type { RateLimitAction } from '@nymbalhq/types'\n\nexport interface ParsedRule {\n pattern: string\n regex: RegExp\n requests: number\n windowMs: number\n action: RateLimitAction\n}\n\nexport interface RateLimitDecision {\n allowed: boolean\n action: RateLimitAction\n retryAfter?: number\n}\n\ntype RateLimitRules = Record<string, { requests: number; window: string; action: RateLimitAction }>\n\n/**\n * Applied under the configured rules: a configured key with the same pattern replaces its default, and the defaults\n * stay when other keys are configured.\n */\nexport const BUILT_IN_RATE_LIMITS: RateLimitRules = {\n '/api/auth/login': { requests: 10, window: '15m', action: 'block' },\n '/api/auth/register': { requests: 10, window: '1h', action: 'block' },\n}\n\ninterface Bucket {\n count: number\n resetAt: number\n}\n\n/** The map size that triggers the first sweep of expired buckets. */\nconst PRUNE_THRESHOLD = 1024\n\nexport class RateLimiter {\n readonly #rules: ParsedRule[]\n readonly #buckets = new Map<string, Bucket>()\n #pruneAt = PRUNE_THRESHOLD\n\n constructor(rules: RateLimitRules) {\n this.#rules = Object.entries(rules).map(([pattern, rule]) => ({\n pattern,\n regex: compilePathPattern(pattern),\n requests: rule.requests,\n windowMs: parseWindow(rule.window),\n action: rule.action,\n }))\n }\n\n check(ip: string, path: string): RateLimitDecision | null {\n const rule = this.#ruleFor(path)\n if (!rule) return null\n const key = `${rule.pattern}|${ip}`\n const now = Date.now()\n const bucket = this.#buckets.get(key)\n if (!bucket || bucket.resetAt <= now) {\n this.#open(key, now, rule.windowMs)\n return { allowed: true, action: rule.action }\n }\n bucket.count += 1\n if (bucket.count > rule.requests) {\n return { allowed: false, action: rule.action, retryAfter: retryAfter(bucket, now) }\n }\n return { allowed: true, action: rule.action }\n }\n\n /**\n * For a route that counts failures only: refuses when any of `keys` (an IP, an account) already holds the rule's\n * full count, without counting this request. `hit` and `clear` record the outcome once the handler has answered.\n */\n full(path: string, keys: string[]): RateLimitDecision | null {\n const rule = this.#ruleFor(path)\n if (!rule) return null\n const now = Date.now()\n for (const key of keys) {\n const bucket = this.#buckets.get(`${rule.pattern}|${key}`)\n if (bucket && bucket.resetAt > now && bucket.count >= rule.requests) {\n return { allowed: false, action: rule.action, retryAfter: retryAfter(bucket, now) }\n }\n }\n return { allowed: true, action: rule.action }\n }\n\n hit(path: string, keys: string[]): void {\n const rule = this.#ruleFor(path)\n if (!rule) return\n const now = Date.now()\n for (const key of keys) {\n const bucketKey = `${rule.pattern}|${key}`\n const bucket = this.#buckets.get(bucketKey)\n if (!bucket || bucket.resetAt <= now) {\n this.#open(bucketKey, now, rule.windowMs)\n } else {\n bucket.count += 1\n }\n }\n }\n\n clear(path: string, keys: string[]): void {\n const rule = this.#ruleFor(path)\n if (!rule) return\n for (const key of keys) this.#buckets.delete(`${rule.pattern}|${key}`)\n }\n\n reset(): void {\n this.#buckets.clear()\n this.#pruneAt = PRUNE_THRESHOLD\n }\n\n /**\n * Starts a bucket. Keys include caller-chosen values (an account email), so expired buckets are swept once the map\n * reaches `#pruneAt`; the next sweep waits until the map doubles past what survived, which keeps the cost amortised\n * constant per insert even when every bucket is live.\n */\n #open(key: string, now: number, windowMs: number): void {\n if (this.#buckets.size >= this.#pruneAt) {\n for (const [k, b] of this.#buckets) if (b.resetAt <= now) this.#buckets.delete(k)\n this.#pruneAt = Math.max(PRUNE_THRESHOLD, this.#buckets.size * 2)\n }\n this.#buckets.set(key, { count: 1, resetAt: now + windowMs })\n }\n\n #ruleFor(path: string): ParsedRule | undefined {\n return this.#rules.find((r) => r.regex.test(path))\n }\n}\n\nfunction retryAfter(bucket: Bucket, now: number): number {\n return Math.max(1, Math.ceil((bucket.resetAt - now) / 1000))\n}\n\nfunction parseWindow(window: string): number {\n const match = /^(\\d+)(ms|s|m|h)$/.exec(window)\n if (!match) throw new Error(`Invalid rate-limit window: ${window}`)\n const n = Number(match[1])\n switch (match[2]) {\n case 'ms':\n return n\n case 's':\n return n * 1000\n case 'm':\n return n * 60_000\n case 'h':\n return n * 3_600_000\n default:\n throw new Error(`Unreachable unit: ${match[2]}`)\n }\n}\n\nfunction compilePathPattern(pattern: string): RegExp {\n const escaped = pattern\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*\\*/g, '__DOUBLESTAR__')\n .replace(/\\*/g, '[^/]+')\n .replace(/__DOUBLESTAR__/g, '.*')\n return new RegExp(`^${escaped}$`)\n}\n","import type { RouteHandler } from '@nymbalhq/types'\n\nexport function createHealthRoute(version: string): RouteHandler {\n return () => ({\n status: 200,\n body: { status: 'ok', version, timestamp: new Date().toISOString() },\n })\n}\n","import type { DocumentStoreAdapter, RouteHandler } from '@nymbalhq/types'\n\nexport interface ProductListItem {\n id: string\n slug: string\n title: string\n description: string\n priceMinor: number\n currency: string\n categoryId: string\n imageUrl: string\n}\n\nexport function createProductsRoute(\n documentStore: DocumentStoreAdapter,\n storeId: string,\n): RouteHandler {\n return async (ctx) => {\n const rawLimit = ctx.query.limit\n const limitValue = Array.isArray(rawLimit) ? rawLimit[0] : rawLimit\n const limit = limitValue ? Math.min(100, Math.max(1, Number(limitValue) || 20)) : 50\n const rawCursor = ctx.query.cursor\n const cursorValue = Array.isArray(rawCursor) ? rawCursor[0] : rawCursor\n\n const result = await documentStore.query<ProductListItem>('products', {\n partitionKey: { field: 'storeId', value: storeId },\n limit,\n ...(cursorValue !== undefined && { cursor: cursorValue }),\n ascending: true,\n })\n return {\n status: 200,\n body: { items: result.items, nextCursor: result.nextCursor },\n }\n }\n}\n","import type { NymbalConfig } from '@nymbalhq/config'\nimport type {\n DocumentStoreAdapter,\n HttpAdapter,\n HttpMethod,\n Logger,\n MiddlewareHandler,\n RouteHandler,\n RouteOptions,\n} from '@nymbalhq/types'\nimport { FastifyHttpAdapter } from './fastify-adapter.js'\nimport { createHealthRoute } from './routes/health.js'\nimport { createProductsRoute } from './routes/products.js'\n\nexport interface HttpServerDeps {\n config: NymbalConfig\n logger: Logger\n documentStore: DocumentStoreAdapter\n version: string\n}\n\nexport interface HttpServer {\n adapter: HttpAdapter\n registerRoute(\n method: HttpMethod,\n path: string,\n handler: RouteHandler,\n options?: RouteOptions,\n ): void\n registerMiddleware(middleware: MiddlewareHandler): void\n start(): Promise<{ port: number; host: string }>\n stop(): Promise<void>\n}\n\nexport function createHttpServer(deps: HttpServerDeps): HttpServer {\n const { config, logger, documentStore, version } = deps\n if (config.http.adapter !== 'fastify') {\n throw new Error(`Unsupported http.adapter: ${config.http.adapter}`)\n }\n const adapter = new FastifyHttpAdapter({\n logger,\n cors: config.http.cors,\n security: config.security,\n trustProxy: config.http.trustProxy,\n })\n\n const server: HttpServer = {\n adapter,\n registerRoute(method, path, handler, options) {\n adapter.registerRoute(method, path, handler, options)\n },\n registerMiddleware(middleware) {\n adapter.registerMiddleware(middleware)\n },\n async start() {\n await adapter.start(config.http.port, config.http.host)\n return { port: adapter.boundPort, host: config.http.host }\n },\n async stop() {\n await adapter.stop()\n },\n }\n\n // Default routes — available without any user route registration.\n server.registerRoute('GET', '/health', createHealthRoute(version))\n // The commerce routes (including `/api/products`) are registered by\n // `platform.attachHttp(...)` — we intentionally do not pre-register them\n // here to avoid FST_ERR_DUPLICATED_ROUTE conflicts when the kernel is wired.\n void createProductsRoute\n void documentStore\n void config\n\n return server\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fastify-adapter.ts","../src/security-middleware.ts","../src/rate-limit.ts","../src/routes/health.ts","../src/routes/products.ts","../src/server.ts"],"sourcesContent":["import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify'\nimport fastifyCookie from '@fastify/cookie'\nimport fastifyCors from '@fastify/cors'\nimport { v7 as uuidv7 } from 'uuid'\nimport type {\n HttpAdapter,\n HttpMethod,\n Logger,\n MiddlewareHandler,\n RequestContext,\n ResponseEnvelope,\n RouteHandler,\n RouteOptions,\n SseEvent,\n StreamContext,\n StreamHandler,\n} from '@nymbalhq/types'\nimport { HttpError, NymbalError } from '@nymbalhq/types'\nimport type { NymbalConfig } from '@nymbalhq/config'\nimport { createSecurityGuard, type SecurityGuard } from './security-middleware.js'\n\nexport interface CorsOptions {\n allowedOrigins: string[]\n credentials: boolean\n}\n\nexport interface FastifyAdapterOptions {\n logger: Logger\n /** The guard every route passes before its middlewares and handler: rate limit, then CSRF on unsafe methods. */\n security: NymbalConfig['security']\n /** Whether `X-Forwarded-For` sets the request IP. Off unless a trusted proxy sits in front. */\n trustProxy?: boolean\n cors?: CorsOptions\n}\n\n/** How long a stream ended at shutdown may take to flush before its socket is destroyed regardless. */\nconst STREAM_END_BOUND_MS = 500\n\nexport class FastifyHttpAdapter implements HttpAdapter {\n readonly #app: FastifyInstance\n readonly #logger: Logger\n readonly #guard: SecurityGuard\n readonly #middlewares: MiddlewareHandler[] = []\n readonly #defaultHeaders: Record<string, string> = {}\n /** Ends each open stream for shutdown; a stream removes itself once its close callbacks have run. */\n readonly #openStreams = new Set<() => void>()\n #started = false\n #stopping = false\n #boundPort = 0\n\n constructor(options: FastifyAdapterOptions) {\n this.#logger = options.logger\n this.#guard = createSecurityGuard(options.security)\n this.#app = Fastify({\n logger: false,\n trustProxy: options.trustProxy ?? false,\n disableRequestLogging: true,\n genReqId: () => uuidv7(),\n })\n // A signed webhook is verified against the bytes sent, so a route registered with `rawBody` keeps them. The\n // body is still parsed by Fastify's own JSON parser, with its poisoning settings and its 400s.\n const parseJson = this.#app.getDefaultJsonParser(\n this.#app.initialConfig.onProtoPoisoning ?? 'error',\n this.#app.initialConfig.onConstructorPoisoning ?? 'error',\n )\n this.#app.removeContentTypeParser('application/json')\n this.#app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => {\n const buffer = body as Buffer\n const config = req.routeOptions.config as { routeOptions?: RouteOptions } | undefined\n if (config?.routeOptions?.rawBody === true) req.rawBody = buffer\n parseJson(req, buffer.toString('utf8'), done)\n })\n // Fastify's `close()` waits for every open connection, and a stream never ends by itself. `preClose` runs after\n // Fastify answers new requests 503 and before `server.close()`, so no stream can hold the close once it has run.\n this.#app.addHook('preClose', async () => {\n this.#stopping = true\n for (const endStream of this.#openStreams) endStream()\n })\n void this.#app.register(fastifyCookie)\n if (options.cors && options.cors.allowedOrigins.length > 0) {\n void this.#app.register(fastifyCors, {\n origin: options.cors.allowedOrigins,\n credentials: options.cors.credentials,\n methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'],\n })\n }\n // The token a client on another origin of the same site echoes in `x-csrf-token`: it cannot read this cookie.\n this.registerRoute('GET', '/api/csrf', (ctx) => this.#guard.issueToken(ctx))\n }\n\n get boundPort(): number {\n return this.#boundPort\n }\n\n registerRoute(\n method: HttpMethod,\n path: string,\n handler: RouteHandler,\n options?: RouteOptions,\n ): void {\n if (this.#started) {\n throw new Error('Cannot register routes after the server has started')\n }\n const routeConfig: {\n method: HttpMethod\n url: string\n handler: (req: FastifyRequest, reply: FastifyReply) => Promise<FastifyReply>\n config: { routeOptions: RouteOptions }\n bodyLimit?: number\n } = {\n method,\n url: path,\n handler: async (req, reply) => {\n const ctx = this.#buildContext(req, options?.rawBody ?? false)\n const refused = this.#guard.admit(ctx, options)\n if (refused) return this.#applyEnvelope(reply, refused)\n let envelope: ResponseEnvelope\n try {\n envelope = await this.#runMiddlewaresThen(ctx, handler)\n } catch (err) {\n envelope = this.#renderError(err, ctx)\n }\n return this.#applyEnvelope(reply, this.#guard.finish(ctx, envelope))\n },\n config: { routeOptions: options ?? {} },\n }\n this.#app.route(routeConfig)\n }\n\n registerStreamRoute(\n method: 'GET',\n path: string,\n handler: StreamHandler,\n options?: RouteOptions,\n ): void {\n if (this.#started) {\n throw new Error('Cannot register routes after the server has started')\n }\n this.#app.route({\n method,\n url: path,\n handler: async (req, reply) => {\n const ctx = this.#buildContext(req, false)\n const refused = this.#guard.admit(ctx, options)\n if (refused) return this.#applyEnvelope(reply, refused)\n try {\n for (const mw of this.#middlewares) {\n const out = await mw(ctx)\n if (out) {\n return this.#applyEnvelope(reply, out)\n }\n }\n } catch (err) {\n return this.#applyEnvelope(reply, this.#renderError(err, ctx))\n }\n // Routed before Fastify began closing, but its middlewares finished after the open streams were ended.\n if (this.#stopping) {\n return this.#applyEnvelope(reply, {\n status: 503,\n headers: { connection: 'close' },\n body: { error: { code: 'unavailable', message: 'Server is shutting down' } },\n })\n }\n const raw = reply.raw\n // Taken now: a finished response detaches from its socket before `end`'s callback runs.\n const socket = req.raw.socket\n raw.writeHead(200, {\n ...this.#defaultHeaders,\n 'content-type': 'text/event-stream',\n 'cache-control': 'no-cache, no-transform',\n connection: 'keep-alive',\n 'x-accel-buffering': 'no',\n })\n reply.hijack()\n const closeCallbacks: Array<() => void> = []\n let ending = false\n let closed = false\n const end = (then?: () => void): void => {\n if (ending) return\n ending = true\n try {\n raw.end(then)\n } catch {\n // ignore\n }\n }\n // A clean end, so the client sees a complete response, then the keep-alive socket goes too: it would hold\n // `server.close()` open. The bound covers a socket whose end never flushes.\n const endForShutdown = (): void => {\n const bound = setTimeout(() => socket.destroy(), STREAM_END_BOUND_MS)\n bound.unref()\n end(() => {\n clearTimeout(bound)\n socket.destroy()\n })\n }\n const close = (): void => {\n end()\n if (closed) return\n closed = true\n this.#openStreams.delete(endForShutdown)\n for (const cb of closeCallbacks) {\n try {\n cb()\n } catch (err) {\n ctx.logger.error({ err }, 'stream close callback failed')\n }\n }\n }\n req.raw.on('close', close)\n req.raw.on('error', close)\n this.#openStreams.add(endForShutdown)\n const stream: StreamContext = {\n send(event: SseEvent): void {\n if (ending) return\n let line = ''\n if (event.id !== undefined) line += `id: ${event.id}\\n`\n if (event.event !== undefined) line += `event: ${event.event}\\n`\n const data = typeof event.data === 'string' ? event.data : JSON.stringify(event.data)\n line += `data: ${data}\\n\\n`\n raw.write(line)\n },\n close,\n onClose(cb: () => void): void {\n closeCallbacks.push(cb)\n },\n }\n try {\n await handler(ctx, stream)\n } catch (err) {\n ctx.logger.error({ err }, 'stream handler threw')\n close()\n }\n return reply\n },\n config: { routeOptions: options ?? {} },\n })\n }\n\n registerMiddleware(middleware: MiddlewareHandler): void {\n if (this.#started) {\n throw new Error('Cannot register middleware after the server has started')\n }\n this.#middlewares.push(middleware)\n }\n\n setDefaultHeaders(headers: Record<string, string>): void {\n for (const [k, v] of Object.entries(headers)) {\n this.#defaultHeaders[k.toLowerCase()] = v\n }\n }\n\n async start(port: number, host = '0.0.0.0'): Promise<void> {\n await this.#app.listen({ port, host })\n this.#boundPort = (this.#app.server.address() as { port: number }).port\n this.#started = true\n this.#logger.info({ port: this.#boundPort, host }, 'HTTP server listening')\n }\n\n async stop(): Promise<void> {\n if (!this.#started) return\n await this.#app.close()\n this.#started = false\n }\n\n async #runMiddlewaresThen(ctx: RequestContext, handler: RouteHandler): Promise<ResponseEnvelope> {\n for (const mw of this.#middlewares) {\n const out = await mw(ctx)\n if (out) return out\n }\n return handler(ctx)\n }\n\n #buildContext(req: FastifyRequest, includeRawBody: boolean): RequestContext {\n const correlationId = (req.headers['x-correlation-id'] as string | undefined) ?? req.id\n const logger = this.#logger.child({ requestId: req.id, correlationId })\n const ctx: RequestContext = {\n method: req.method as HttpMethod,\n path: req.routeOptions?.url ?? req.url,\n url: req.url,\n params: (req.params ?? {}) as Record<string, string>,\n query: (req.query ?? {}) as Record<string, string | string[] | undefined>,\n body: req.body,\n headers: req.headers,\n cookies: (req.cookies ?? {}) as Record<string, string>,\n requestId: req.id,\n correlationId,\n logger,\n ip: req.ip,\n }\n if (includeRawBody && Buffer.isBuffer(req.rawBody)) {\n ctx.rawBody = req.rawBody\n }\n return ctx\n }\n\n #applyEnvelope(reply: FastifyReply, envelope: ResponseEnvelope): FastifyReply {\n reply.status(envelope.status)\n for (const [name, value] of Object.entries(this.#defaultHeaders)) {\n reply.header(name, value)\n }\n if (envelope.headers) {\n for (const [name, value] of Object.entries(envelope.headers)) {\n reply.header(name, value)\n }\n }\n if (envelope.cookies) {\n for (const cookie of envelope.cookies) {\n reply.setCookie(cookie.name, cookie.value, {\n path: cookie.path ?? '/',\n ...(cookie.domain !== undefined && { domain: cookie.domain }),\n ...(cookie.maxAge !== undefined && { maxAge: cookie.maxAge }),\n httpOnly: cookie.httpOnly ?? false,\n secure: cookie.secure ?? false,\n ...(cookie.sameSite !== undefined && { sameSite: cookie.sameSite }),\n })\n }\n }\n if (envelope.body === undefined) {\n return reply.send()\n }\n return reply.send(envelope.body)\n }\n\n #renderError(err: unknown, ctx: RequestContext): ResponseEnvelope {\n if (err instanceof HttpError) {\n ctx.logger.warn({ err: err.toJSON() }, err.message)\n return {\n status: err.status,\n body: { error: { code: err.code, message: err.message } },\n }\n }\n if (err instanceof NymbalError) {\n ctx.logger.error({ err: err.toJSON() }, err.message)\n return {\n status: 500,\n body: { error: { code: err.code, message: err.message } },\n }\n }\n const message = err instanceof Error ? err.message : String(err)\n ctx.logger.error({ err }, message)\n return {\n status: 500,\n body: { error: { code: 'internal', message: 'Internal server error' } },\n }\n }\n}\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n rawBody?: Buffer\n }\n}\n","import { randomBytes, timingSafeEqual } from 'node:crypto'\nimport type { NymbalConfig } from '@nymbalhq/config'\nimport type {\n RequestContext,\n ResponseCookie,\n ResponseEnvelope,\n RouteOptions,\n} from '@nymbalhq/types'\nimport { BUILT_IN_RATE_LIMITS, RateLimiter } from './rate-limit.js'\n\nexport const CSRF_COOKIE = 'nymbal.csrf'\nexport const CSRF_HEADER = 'x-csrf-token'\nconst UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])\n\nexport function buildSecurityHeaders(security: NymbalConfig['security']): Record<string, string> {\n const headers: Record<string, string> = {}\n if (security.headers.hsts) {\n headers['strict-transport-security'] = 'max-age=31536000; includeSubDomains'\n }\n const csp = security.headers.contentSecurityPolicy\n const cspValue =\n csp === 'strict'\n ? \"default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'\"\n : csp === 'relaxed'\n ? \"default-src 'self' https:; img-src * data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'\"\n : csp === 'off'\n ? null\n : csp\n if (cspValue) headers['content-security-policy'] = cspValue\n headers['referrer-policy'] = security.headers.referrerPolicy\n headers['x-frame-options'] = security.headers.xFrameOptions === 'deny' ? 'DENY' : 'SAMEORIGIN'\n headers['x-content-type-options'] = 'nosniff'\n return headers\n}\n\nexport interface SecurityGuard {\n /**\n * Runs before any middleware or handler: the rate limit, then CSRF on unsafe methods unless the route opted out.\n * Returns the refusal, or null when the request may proceed.\n */\n admit(ctx: RequestContext, options?: RouteOptions): ResponseEnvelope | null\n /** Runs on the handler's answer: security headers, the CSRF cookie, and the outcome of a failure-counted login. */\n finish(ctx: RequestContext, envelope: ResponseEnvelope): ResponseEnvelope\n /** `GET /api/csrf`: the request's CSRF cookie, or a new one set on the response. */\n issueToken(ctx: RequestContext): ResponseEnvelope\n reset(): void\n}\n\n/** Counts failures only, keyed by IP and by account, so an office behind one NAT is not blocked by its own logins. */\nconst LOGIN_PATH = '/api/auth/login'\n\nfunction csrfCookie(token: string): ResponseCookie {\n return {\n name: CSRF_COOKIE,\n value: token,\n path: '/',\n httpOnly: false,\n sameSite: 'lax',\n secure: process.env.NODE_ENV === 'production',\n }\n}\n\nfunction loginKeys(ctx: RequestContext): string[] {\n const email = (ctx.body as { email?: unknown } | undefined)?.email\n return typeof email === 'string' && email !== ''\n ? [ctx.ip, `account:${email.toLowerCase()}`]\n : [ctx.ip]\n}\n\nfunction isLogin(ctx: RequestContext): boolean {\n return ctx.method === 'POST' && ctx.path === LOGIN_PATH\n}\n\nexport function createSecurityGuard(security: NymbalConfig['security']): SecurityGuard {\n const rateLimiter = new RateLimiter({ ...BUILT_IN_RATE_LIMITS, ...security.rateLimit })\n const baseHeaders = buildSecurityHeaders(security)\n\n function applyHeaders(envelope: ResponseEnvelope): ResponseEnvelope {\n return {\n ...envelope,\n headers: { ...baseHeaders, ...(envelope.headers ?? {}) },\n }\n }\n\n return {\n admit(ctx, options = {}) {\n const decision = isLogin(ctx)\n ? rateLimiter.full(ctx.path, loginKeys(ctx))\n : rateLimiter.check(ctx.ip, ctx.path)\n if (decision && !decision.allowed) {\n return applyHeaders(rateLimitResponse(decision, ctx))\n }\n\n if (security.csrf && options.csrf !== false && UNSAFE_METHODS.has(ctx.method)) {\n if (!verifyCsrf(ctx)) {\n ctx.logger.warn({ method: ctx.method, path: ctx.path }, 'CSRF validation failed')\n return applyHeaders({\n status: 403,\n body: { error: { code: 'csrf.invalid', message: 'Invalid or missing CSRF token' } },\n })\n }\n }\n return null\n },\n\n finish(ctx, envelope) {\n if (isLogin(ctx)) {\n const keys = loginKeys(ctx)\n // Success clears the account only: the IP bucket expires with its window, so logging in to one's own\n // account cannot reset a spray across other accounts.\n if (envelope.status >= 200 && envelope.status < 300) {\n rateLimiter.clear(\n ctx.path,\n keys.filter((key) => key.startsWith('account:')),\n )\n } else rateLimiter.hit(ctx.path, keys)\n }\n const finished = applyHeaders(envelope)\n const setsCsrf = finished.cookies?.some((cookie) => cookie.name === CSRF_COOKIE) ?? false\n if (security.csrf && !ctx.cookies[CSRF_COOKIE] && !setsCsrf) {\n finished.cookies = [...(finished.cookies ?? []), csrfCookie(newToken())]\n }\n return finished\n },\n\n issueToken(ctx) {\n const existing = ctx.cookies[CSRF_COOKIE]\n if (existing) return { status: 200, body: { data: { token: existing } } }\n const token = newToken()\n return { status: 200, body: { data: { token } }, cookies: [csrfCookie(token)] }\n },\n\n reset() {\n rateLimiter.reset()\n },\n }\n}\n\nfunction newToken(): string {\n return randomBytes(32).toString('hex')\n}\n\nfunction verifyCsrf(ctx: RequestContext): boolean {\n const cookieToken = ctx.cookies[CSRF_COOKIE]\n const rawHeader = ctx.headers[CSRF_HEADER]\n const headerToken = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader\n if (!cookieToken || !headerToken) return false\n if (cookieToken.length !== headerToken.length) return false\n try {\n return timingSafeEqual(Buffer.from(cookieToken), Buffer.from(headerToken))\n } catch {\n return false\n }\n}\n\nfunction rateLimitResponse(\n decision: { action: string; retryAfter?: number },\n ctx: RequestContext,\n): ResponseEnvelope {\n ctx.logger.info({ ip: ctx.ip, path: ctx.path, action: decision.action }, 'Rate limit triggered')\n const status = decision.action === 'challenge' ? 403 : 429\n const headers: Record<string, string> = {}\n if (decision.retryAfter) headers['retry-after'] = String(decision.retryAfter)\n return {\n status,\n headers,\n body: {\n error: {\n code: `rate_limit.${decision.action}`,\n message: 'Too many requests',\n ...(decision.retryAfter !== undefined && { retryAfter: decision.retryAfter }),\n },\n },\n }\n}\n","import type { RateLimitAction } from '@nymbalhq/types'\n\nexport interface ParsedRule {\n pattern: string\n regex: RegExp\n requests: number\n windowMs: number\n action: RateLimitAction\n}\n\nexport interface RateLimitDecision {\n allowed: boolean\n action: RateLimitAction\n retryAfter?: number\n}\n\ntype RateLimitRules = Record<string, { requests: number; window: string; action: RateLimitAction }>\n\n/**\n * Applied under the configured rules: a configured key with the same pattern replaces its default, and the defaults\n * stay when other keys are configured.\n */\nexport const BUILT_IN_RATE_LIMITS: RateLimitRules = {\n '/api/auth/login': { requests: 10, window: '15m', action: 'block' },\n '/api/auth/register': { requests: 10, window: '1h', action: 'block' },\n}\n\ninterface Bucket {\n count: number\n resetAt: number\n}\n\n/** The map size that triggers the first sweep of expired buckets. */\nconst PRUNE_THRESHOLD = 1024\n\nexport class RateLimiter {\n readonly #rules: ParsedRule[]\n readonly #buckets = new Map<string, Bucket>()\n #pruneAt = PRUNE_THRESHOLD\n\n constructor(rules: RateLimitRules) {\n this.#rules = Object.entries(rules).map(([pattern, rule]) => ({\n pattern,\n regex: compilePathPattern(pattern),\n requests: rule.requests,\n windowMs: parseWindow(rule.window),\n action: rule.action,\n }))\n }\n\n check(ip: string, path: string): RateLimitDecision | null {\n const rule = this.#ruleFor(path)\n if (!rule) return null\n const key = `${rule.pattern}|${ip}`\n const now = Date.now()\n const bucket = this.#buckets.get(key)\n if (!bucket || bucket.resetAt <= now) {\n this.#open(key, now, rule.windowMs)\n return { allowed: true, action: rule.action }\n }\n bucket.count += 1\n if (bucket.count > rule.requests) {\n return { allowed: false, action: rule.action, retryAfter: retryAfter(bucket, now) }\n }\n return { allowed: true, action: rule.action }\n }\n\n /**\n * For a route that counts failures only: refuses when any of `keys` (an IP, an account) already holds the rule's\n * full count, without counting this request. `hit` and `clear` record the outcome once the handler has answered.\n */\n full(path: string, keys: string[]): RateLimitDecision | null {\n const rule = this.#ruleFor(path)\n if (!rule) return null\n const now = Date.now()\n for (const key of keys) {\n const bucket = this.#buckets.get(`${rule.pattern}|${key}`)\n if (bucket && bucket.resetAt > now && bucket.count >= rule.requests) {\n return { allowed: false, action: rule.action, retryAfter: retryAfter(bucket, now) }\n }\n }\n return { allowed: true, action: rule.action }\n }\n\n hit(path: string, keys: string[]): void {\n const rule = this.#ruleFor(path)\n if (!rule) return\n const now = Date.now()\n for (const key of keys) {\n const bucketKey = `${rule.pattern}|${key}`\n const bucket = this.#buckets.get(bucketKey)\n if (!bucket || bucket.resetAt <= now) {\n this.#open(bucketKey, now, rule.windowMs)\n } else {\n bucket.count += 1\n }\n }\n }\n\n clear(path: string, keys: string[]): void {\n const rule = this.#ruleFor(path)\n if (!rule) return\n for (const key of keys) this.#buckets.delete(`${rule.pattern}|${key}`)\n }\n\n reset(): void {\n this.#buckets.clear()\n this.#pruneAt = PRUNE_THRESHOLD\n }\n\n /**\n * Starts a bucket. Keys include caller-chosen values (an account email), so expired buckets are swept once the map\n * reaches `#pruneAt`; the next sweep waits until the map doubles past what survived, which keeps the cost amortised\n * constant per insert even when every bucket is live.\n */\n #open(key: string, now: number, windowMs: number): void {\n if (this.#buckets.size >= this.#pruneAt) {\n for (const [k, b] of this.#buckets) if (b.resetAt <= now) this.#buckets.delete(k)\n this.#pruneAt = Math.max(PRUNE_THRESHOLD, this.#buckets.size * 2)\n }\n this.#buckets.set(key, { count: 1, resetAt: now + windowMs })\n }\n\n #ruleFor(path: string): ParsedRule | undefined {\n return this.#rules.find((r) => r.regex.test(path))\n }\n}\n\nfunction retryAfter(bucket: Bucket, now: number): number {\n return Math.max(1, Math.ceil((bucket.resetAt - now) / 1000))\n}\n\nfunction parseWindow(window: string): number {\n const match = /^(\\d+)(ms|s|m|h)$/.exec(window)\n if (!match) throw new Error(`Invalid rate-limit window: ${window}`)\n const n = Number(match[1])\n switch (match[2]) {\n case 'ms':\n return n\n case 's':\n return n * 1000\n case 'm':\n return n * 60_000\n case 'h':\n return n * 3_600_000\n default:\n throw new Error(`Unreachable unit: ${match[2]}`)\n }\n}\n\nfunction compilePathPattern(pattern: string): RegExp {\n const escaped = pattern\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*\\*/g, '__DOUBLESTAR__')\n .replace(/\\*/g, '[^/]+')\n .replace(/__DOUBLESTAR__/g, '.*')\n return new RegExp(`^${escaped}$`)\n}\n","import type { RouteHandler } from '@nymbalhq/types'\n\nexport function createHealthRoute(version: string): RouteHandler {\n return () => ({\n status: 200,\n body: { status: 'ok', version, timestamp: new Date().toISOString() },\n })\n}\n","import type { DocumentStoreAdapter, RouteHandler } from '@nymbalhq/types'\n\nexport interface ProductListItem {\n id: string\n slug: string\n title: string\n description: string\n priceMinor: number\n currency: string\n categoryId: string\n imageUrl: string\n}\n\nexport function createProductsRoute(\n documentStore: DocumentStoreAdapter,\n storeId: string,\n): RouteHandler {\n return async (ctx) => {\n const rawLimit = ctx.query.limit\n const limitValue = Array.isArray(rawLimit) ? rawLimit[0] : rawLimit\n const limit = limitValue ? Math.min(100, Math.max(1, Number(limitValue) || 20)) : 50\n const rawCursor = ctx.query.cursor\n const cursorValue = Array.isArray(rawCursor) ? rawCursor[0] : rawCursor\n\n const result = await documentStore.query<ProductListItem>('products', {\n partitionKey: { field: 'storeId', value: storeId },\n limit,\n ...(cursorValue !== undefined && { cursor: cursorValue }),\n ascending: true,\n })\n return {\n status: 200,\n body: { items: result.items, nextCursor: result.nextCursor },\n }\n }\n}\n","import type { NymbalConfig } from '@nymbalhq/config'\nimport type {\n DocumentStoreAdapter,\n HttpAdapter,\n HttpMethod,\n Logger,\n MiddlewareHandler,\n RouteHandler,\n RouteOptions,\n} from '@nymbalhq/types'\nimport { FastifyHttpAdapter } from './fastify-adapter.js'\nimport { createHealthRoute } from './routes/health.js'\nimport { createProductsRoute } from './routes/products.js'\n\nexport interface HttpServerDeps {\n config: NymbalConfig\n logger: Logger\n documentStore: DocumentStoreAdapter\n version: string\n}\n\nexport interface HttpServer {\n adapter: HttpAdapter\n registerRoute(\n method: HttpMethod,\n path: string,\n handler: RouteHandler,\n options?: RouteOptions,\n ): void\n registerMiddleware(middleware: MiddlewareHandler): void\n start(): Promise<{ port: number; host: string }>\n stop(): Promise<void>\n}\n\nexport function createHttpServer(deps: HttpServerDeps): HttpServer {\n const { config, logger, documentStore, version } = deps\n if (config.http.adapter !== 'fastify') {\n throw new Error(`Unsupported http.adapter: ${config.http.adapter}`)\n }\n const adapter = new FastifyHttpAdapter({\n logger,\n cors: config.http.cors,\n security: config.security,\n trustProxy: config.http.trustProxy,\n })\n\n const server: HttpServer = {\n adapter,\n registerRoute(method, path, handler, options) {\n adapter.registerRoute(method, path, handler, options)\n },\n registerMiddleware(middleware) {\n adapter.registerMiddleware(middleware)\n },\n async start() {\n await adapter.start(config.http.port, config.http.host)\n return { port: adapter.boundPort, host: config.http.host }\n },\n async stop() {\n await adapter.stop()\n },\n }\n\n // Default routes — available without any user route registration.\n server.registerRoute('GET', '/health', createHealthRoute(version))\n // The commerce routes (including `/api/products`) are registered by\n // `platform.attachHttp(...)` — we intentionally do not pre-register them\n // here to avoid FST_ERR_DUPLICATED_ROUTE conflicts when the kernel is wired.\n void createProductsRoute\n void documentStore\n void config\n\n return server\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nymbalhq/http",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "Framework-agnostic HTTP abstraction with Fastify adapter",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,8 +31,8 @@
31
31
  "@fastify/cors": "^8.5.0",
32
32
  "fastify": "^4.28.1",
33
33
  "uuid": "^10.0.0",
34
- "@nymbalhq/config": "0.2.0",
35
- "@nymbalhq/types": "0.2.0"
34
+ "@nymbalhq/config": "1.0.0",
35
+ "@nymbalhq/types": "1.0.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/uuid": "^10.0.0",