{"version":3,"file":"adapter-http-fastify-B248RJtm.mjs","sources":["../src/graphql/gateway/adapter-http-fastify.ts"],"sourcesContent":["import fastifyCors from \"@fastify/cors\";\nimport fastifyFormbody from \"@fastify/formbody\";\nimport fastifyMiddie from \"@fastify/middie\";\nimport devcert from \"devcert\";\nimport Fastify from \"fastify\";\nimport type { FastifyInstance, FastifyReply, FastifyRequest } from \"fastify\";\nimport type { CorsOptions } from \"cors\";\nimport fs from \"node:fs\";\nimport http from \"node:http\";\nimport https from \"node:https\";\nimport nodePath from \"node:path\";\nimport { match, type MatchFunction, type ParamData } from \"path-to-regexp\";\nimport type { FetchHandler, IHttpAdapter, TlsOptions } from \"./types.js\";\n\n/**\n * Normalises a route path for path-to-regexp v8:\n * - Collapses duplicate slashes (e.g. \"//explorer\" → \"/explorer\")\n * - Converts legacy optional-param syntax \":param?\" → \"{/:param}?\"\n */\nfunction normalizePath(path: string): string {\n  return path.replace(/\\/+/g, \"/\").replace(/:(\\w+)\\?/g, \"{/:$1}\");\n}\n\n/** Parses body-limit strings like \"50mb\" to bytes. */\nfunction parseBodyLimit(limit: string): number {\n  const m = /^(\\d+(?:\\.\\d+)?)\\s*(b|kb|mb|gb)?$/i.exec(limit.trim());\n  if (!m) return 52_428_800; // 50 MB fallback\n  const n = parseFloat(m[1]);\n  const units: Record<string, number> = {\n    b: 1,\n    kb: 1_024,\n    mb: 1_048_576,\n    gb: 1_073_741_824,\n  };\n  return Math.round(n * (units[(m[2] ?? \"b\").toLowerCase()] ?? 1));\n}\n\ntype FetchEntry = {\n  handler: FetchHandler;\n  matcher: MatchFunction<ParamData>;\n  prefix: boolean;\n};\n\ntype GetEntry = {\n  handler: (r: Request) => Response | Promise<Response>;\n  matcher: MatchFunction<ParamData>;\n};\n\ntype NodeHandler = (\n  req: http.IncomingMessage,\n  res: http.ServerResponse,\n  body?: unknown,\n) => void;\n\ntype NodeEntry = {\n  method: \"DELETE\" | \"GET\" | \"HEAD\" | \"POST\" | \"PUT\";\n  matcher: MatchFunction<ParamData>;\n  handler: NodeHandler;\n};\n\n// Pre-listen configuration ops (need the Fastify instance to apply).\ntype SetupOp =\n  | { kind: \"cors\"; options?: CorsOptions; bodyLimit: number }\n  | { kind: \"middie\"; middleware: unknown };\n\nexport class FastifyHttpAdapter implements IHttpAdapter {\n  // Dispatch maps — populated at any time (before or after listen).\n  readonly #fetchRoutes: FetchEntry[] = [];\n  readonly #getRoutes: Map<string, GetEntry> = new Map();\n  // Iterated in registration order on dispatch; entries carry their own\n  // path-to-regexp matcher so parameterised paths (e.g. \"/attachments/:hash\")\n  // resolve correctly and populate `req.params`.\n  readonly #nodeRoutes: NodeEntry[] = [];\n\n  // Ops that need the Fastify instance (CORS config, Connect middleware).\n  readonly #setupOps: SetupOp[] = [];\n\n  #instance: FastifyInstance | undefined;\n\n  get handle(): unknown {\n    return this.#instance;\n  }\n\n  setupMiddleware({\n    corsOptions,\n    bodyLimit = \"50mb\",\n  }: {\n    corsOptions?: CorsOptions;\n    bodyLimit?: string;\n  }): void {\n    this.#setupOps.push({\n      kind: \"cors\",\n      options: corsOptions,\n      bodyLimit: parseBodyLimit(bodyLimit),\n    });\n  }\n\n  mount(\n    path: string,\n    handler: FetchHandler,\n    { exact = false }: { exact?: boolean } = {},\n  ): void {\n    this.#fetchRoutes.push({\n      handler,\n      // exact=false → exact path match; exact=true → prefix match.\n      matcher: match(normalizePath(path), { end: !exact }),\n      prefix: exact,\n    });\n  }\n\n  getRoute(\n    path: string,\n    handler: (request: Request) => Response | Promise<Response>,\n  ): void {\n    this.#getRoutes.set(path, { handler, matcher: match(normalizePath(path)) });\n  }\n\n  mountNodeRoute(\n    method: \"DELETE\" | \"GET\" | \"HEAD\" | \"POST\" | \"PUT\",\n    path: string,\n    handler: NodeHandler,\n  ): void {\n    this.#nodeRoutes.push({\n      method,\n      matcher: match(normalizePath(path)),\n      handler,\n    });\n  }\n\n  mountRawMiddleware(middleware: unknown): void {\n    if (this.#instance) {\n      // @fastify/middie is always registered, so .use() is available post-listen.\n      (\n        this.#instance as FastifyInstance & {\n          use(middleware: unknown): FastifyInstance;\n        }\n      ).use(middleware);\n    } else {\n      this.#setupOps.push({ kind: \"middie\", middleware });\n    }\n  }\n\n  setupSentryErrorHandler(sentry: object): void {\n    if (!this.#instance) return;\n    const s = sentry as {\n      setupFastifyErrorHandler(app: FastifyInstance): void;\n    };\n    s.setupFastifyErrorHandler(this.#instance);\n  }\n\n  async listen(port: number, tls?: TlsOptions): Promise<http.Server> {\n    let httpServer: http.Server;\n\n    if (tls === true) {\n      const { cert, key } = (await devcert.certificateFor(\"localhost\")) as {\n        cert: Buffer;\n        key: Buffer;\n      };\n      if (!cert || !key) throw new Error(\"Invalid certificate generated\");\n      httpServer = https.createServer({ cert, key });\n    } else if (tls && \"keyPath\" in tls) {\n      const cwd = process.cwd();\n      httpServer = https.createServer({\n        key: fs.readFileSync(nodePath.join(cwd, tls.keyPath)),\n        cert: fs.readFileSync(nodePath.join(cwd, tls.certPath)),\n      });\n    } else if (tls && \"cert\" in tls) {\n      httpServer = https.createServer({ cert: tls.cert, key: tls.key });\n    } else {\n      httpServer = http.createServer();\n    }\n\n    const corsOp = this.#setupOps.find(\n      (op): op is Extract<SetupOp, { kind: \"cors\" }> => op.kind === \"cors\",\n    );\n    const bodyLimit = corsOp?.bodyLimit ?? 52_428_800;\n\n    const instance = Fastify({\n      serverFactory: (handler) => {\n        httpServer.on(\"request\", handler);\n        return httpServer;\n      },\n      bodyLimit,\n      logger: false,\n    });\n\n    this.#instance = instance;\n\n    // Always register middie first so .use() is available post-listen.\n    await instance.register(fastifyMiddie);\n    await instance.register(fastifyFormbody);\n\n    for (const op of this.#setupOps) {\n      if (op.kind === \"cors\") {\n        await instance.register(\n          fastifyCors,\n          op.options as Parameters<typeof fastifyCors>[1],\n        );\n      } else {\n        (\n          instance as FastifyInstance & {\n            use(middleware: unknown): FastifyInstance;\n          }\n        ).use(op.middleware);\n      }\n    }\n\n    // Single catch-all route — all dispatching is done via the Maps above so\n    // that routes registered after listen() are picked up automatically.\n    // OPTIONS is excluded because @fastify/cors registers its own OPTIONS /*\n    // handler for preflight; including it here would cause a duplicate-route\n    // conflict at startup.\n    instance.route({\n      method: [\"DELETE\", \"GET\", \"HEAD\", \"PATCH\", \"POST\", \"PUT\"],\n      url: \"/*\",\n      handler: (req, reply) => this.#dispatch(req, reply),\n    });\n\n    await instance.ready();\n\n    return new Promise<http.Server>((resolve, reject) => {\n      httpServer.once(\"error\", reject);\n      httpServer.listen(port, () => {\n        httpServer.off(\"error\", reject);\n        resolve(httpServer);\n      });\n    });\n  }\n\n  #dispatch(req: FastifyRequest, reply: FastifyReply): void | Promise<void> {\n    const pathname = new URL(req.url, \"http://localhost\").pathname;\n    const method = req.method.toUpperCase() as\n      | \"DELETE\"\n      | \"GET\"\n      | \"HEAD\"\n      | \"POST\"\n      | \"PUT\";\n\n    // 1. Node routes — path-to-regexp match + method, handler manages raw\n    // response. Attach decoded params onto `req.raw` so downstream handlers\n    // can read them via the same `req.params` API the Express adapter exposes.\n    for (const entry of this.#nodeRoutes) {\n      if (entry.method !== method) continue;\n      const result = entry.matcher(pathname);\n      if (!result) continue;\n      (req.raw as http.IncomingMessage & { params?: ParamData }).params =\n        result.params;\n      reply.hijack();\n      entry.handler(req.raw, reply.raw, req.body);\n      return;\n    }\n\n    // 2. GET-specific routes (health, explorer, etc.).\n    if (method === \"GET\") {\n      for (const entry of this.#getRoutes.values()) {\n        if (entry.matcher(pathname)) {\n          return this.#serveGetEntry(entry, req, reply);\n        }\n      }\n    }\n\n    // 3. Fetch routes (GraphQL handlers, SSE, etc.).\n    // Iterate in reverse so that the last-mounted handler wins when the same\n    // path is mounted more than once (e.g. supergraph remount after reload).\n    for (let i = this.#fetchRoutes.length - 1; i >= 0; i--) {\n      const entry = this.#fetchRoutes[i]!;\n      if (entry.matcher(pathname)) {\n        return serveFetchHandler(entry.handler, req, reply);\n      }\n    }\n\n    void reply.status(404).send({\n      message: `Route ${req.method}:${pathname} not found`,\n      error: \"Not Found\",\n      statusCode: 404,\n    });\n  }\n\n  async #serveGetEntry(\n    entry: GetEntry,\n    req: FastifyRequest,\n    reply: FastifyReply,\n  ): Promise<void> {\n    const url = buildUrl(req);\n    const headers = buildHeaders(req);\n    const fetchReq = new Request(url, { method: \"GET\", headers });\n    const response = await entry.handler(fetchReq);\n    writeResponse(reply, response);\n    return reply.send(await response.text());\n  }\n}\n\nasync function serveFetchHandler(\n  handler: FetchHandler,\n  req: FastifyRequest,\n  reply: FastifyReply,\n): Promise<void> {\n  const url = buildUrl(req);\n  const headers = buildHeaders(req);\n\n  let body: string | undefined;\n  if (req.method !== \"GET\" && req.method !== \"HEAD\" && req.body !== undefined) {\n    body = JSON.stringify(req.body);\n  }\n\n  const fetchRequest = new Request(url, { method: req.method, headers, body });\n  const response = await handler(fetchRequest);\n  writeResponse(reply, response);\n  return reply.send(await response.text());\n}\n\nfunction buildUrl(req: FastifyRequest): string {\n  const host = req.headers.host ?? \"localhost\";\n  return `${req.protocol}://${host}${req.url}`;\n}\n\nfunction buildHeaders(req: FastifyRequest): Headers {\n  const headers = new Headers();\n  for (const [key, value] of Object.entries(req.headers)) {\n    if (typeof value === \"string\") {\n      headers.set(key, value);\n    } else if (Array.isArray(value)) {\n      headers.set(key, value.join(\", \"));\n    }\n  }\n  return headers;\n}\n\nfunction writeResponse(reply: FastifyReply, response: Response): void {\n  void reply.status(response.status);\n  response.headers.forEach((value, key) => {\n    void reply.header(key, value);\n  });\n}\n\nexport function createFastifyHttpAdapter(): { adapter: IHttpAdapter } {\n  return { adapter: new FastifyHttpAdapter() };\n}\n"],"names":["#fetchRoutes","#getRoutes","#nodeRoutes","#setupOps","#instance","nodePath","#dispatch","#serveGetEntry"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,SAAS,cAAc,MAAsB;AAC3C,QAAO,KAAK,QAAQ,QAAQ,IAAI,CAAC,QAAQ,aAAa,SAAS;;;AAIjE,SAAS,eAAe,OAAuB;CAC7C,MAAM,IAAI,qCAAqC,KAAK,MAAM,MAAM,CAAC;AACjE,KAAI,CAAC,EAAG,QAAO;CACf,MAAM,IAAI,WAAW,EAAE,GAAG;AAO1B,QAAO,KAAK,MAAM,KANoB;EACpC,GAAG;EACH,IAAI;EACJ,IAAI;EACJ,IAAI;EACL,EAC6B,EAAE,MAAM,KAAK,aAAa,KAAK,GAAG;;AA+BlE,IAAa,qBAAb,MAAwD;CAEtD,eAAsC,EAAE;CACxC,6BAA6C,IAAI,KAAK;CAItD,cAAoC,EAAE;CAGtC,YAAgC,EAAE;CAElC;CAEA,IAAI,SAAkB;AACpB,SAAO,MAAA;;CAGT,gBAAgB,EACd,aACA,YAAY,UAIL;AACP,QAAA,SAAe,KAAK;GAClB,MAAM;GACN,SAAS;GACT,WAAW,eAAe,UAAU;GACrC,CAAC;;CAGJ,MACE,MACA,SACA,EAAE,QAAQ,UAA+B,EAAE,EACrC;AACN,QAAA,YAAkB,KAAK;GACrB;GAEA,SAAS,MAAM,cAAc,KAAK,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC;GACpD,QAAQ;GACT,CAAC;;CAGJ,SACE,MACA,SACM;AACN,QAAA,UAAgB,IAAI,MAAM;GAAE;GAAS,SAAS,MAAM,cAAc,KAAK,CAAC;GAAE,CAAC;;CAG7E,eACE,QACA,MACA,SACM;AACN,QAAA,WAAiB,KAAK;GACpB;GACA,SAAS,MAAM,cAAc,KAAK,CAAC;GACnC;GACD,CAAC;;CAGJ,mBAAmB,YAA2B;AAC5C,MAAI,MAAA,SAGA,OAAA,SAGA,IAAI,WAAW;MAEjB,OAAA,SAAe,KAAK;GAAE,MAAM;GAAU;GAAY,CAAC;;CAIvD,wBAAwB,QAAsB;AAC5C,MAAI,CAAC,MAAA,SAAgB;AACX,SAGR,yBAAyB,MAAA,SAAe;;CAG5C,MAAM,OAAO,MAAc,KAAwC;EACjE,IAAI;AAEJ,MAAI,QAAQ,MAAM;GAChB,MAAM,EAAE,MAAM,QAAS,MAAM,QAAQ,eAAe,YAAY;AAIhE,OAAI,CAAC,QAAQ,CAAC,IAAK,OAAM,IAAI,MAAM,gCAAgC;AACnE,gBAAa,MAAM,aAAa;IAAE;IAAM;IAAK,CAAC;aACrC,OAAO,aAAa,KAAK;GAClC,MAAM,MAAM,QAAQ,KAAK;AACzB,gBAAa,MAAM,aAAa;IAC9B,KAAK,GAAG,aAAaK,KAAS,KAAK,KAAK,IAAI,QAAQ,CAAC;IACrD,MAAM,GAAG,aAAaA,KAAS,KAAK,KAAK,IAAI,SAAS,CAAC;IACxD,CAAC;aACO,OAAO,UAAU,IAC1B,cAAa,MAAM,aAAa;GAAE,MAAM,IAAI;GAAM,KAAK,IAAI;GAAK,CAAC;MAEjE,cAAa,KAAK,cAAc;EAQlC,MAAM,WAAW,QAAQ;GACvB,gBAAgB,YAAY;AAC1B,eAAW,GAAG,WAAW,QAAQ;AACjC,WAAO;;GAET,WAVa,MAAA,SAAe,MAC3B,OAAiD,GAAG,SAAS,OAC/D,EACyB,aAAa;GAQrC,QAAQ;GACT,CAAC;AAEF,QAAA,WAAiB;AAGjB,QAAM,SAAS,SAAS,cAAc;AACtC,QAAM,SAAS,SAAS,gBAAgB;AAExC,OAAK,MAAM,MAAM,MAAA,SACf,KAAI,GAAG,SAAS,OACd,OAAM,SAAS,SACb,aACA,GAAG,QACJ;MAGC,UAGA,IAAI,GAAG,WAAW;AASxB,WAAS,MAAM;GACb,QAAQ;IAAC;IAAU;IAAO;IAAQ;IAAS;IAAQ;IAAM;GACzD,KAAK;GACL,UAAU,KAAK,UAAU,MAAA,SAAe,KAAK,MAAM;GACpD,CAAC;AAEF,QAAM,SAAS,OAAO;AAEtB,SAAO,IAAI,SAAsB,SAAS,WAAW;AACnD,cAAW,KAAK,SAAS,OAAO;AAChC,cAAW,OAAO,YAAY;AAC5B,eAAW,IAAI,SAAS,OAAO;AAC/B,YAAQ,WAAW;KACnB;IACF;;CAGJ,UAAU,KAAqB,OAA2C;EACxE,MAAM,WAAW,IAAI,IAAI,IAAI,KAAK,mBAAmB,CAAC;EACtD,MAAM,SAAS,IAAI,OAAO,aAAa;AAUvC,OAAK,MAAM,SAAS,MAAA,YAAkB;AACpC,OAAI,MAAM,WAAW,OAAQ;GAC7B,MAAM,SAAS,MAAM,QAAQ,SAAS;AACtC,OAAI,CAAC,OAAQ;AACZ,OAAI,IAAsD,SACzD,OAAO;AACT,SAAM,QAAQ;AACd,SAAM,QAAQ,IAAI,KAAK,MAAM,KAAK,IAAI,KAAK;AAC3C;;AAIF,MAAI,WAAW;QACR,MAAM,SAAS,MAAA,UAAgB,QAAQ,CAC1C,KAAI,MAAM,QAAQ,SAAS,CACzB,QAAO,MAAA,cAAoB,OAAO,KAAK,MAAM;;AAQnD,OAAK,IAAI,IAAI,MAAA,YAAkB,SAAS,GAAG,KAAK,GAAG,KAAK;GACtD,MAAM,QAAQ,MAAA,YAAkB;AAChC,OAAI,MAAM,QAAQ,SAAS,CACzB,QAAO,kBAAkB,MAAM,SAAS,KAAK,MAAM;;AAIlD,QAAM,OAAO,IAAI,CAAC,KAAK;GAC1B,SAAS,SAAS,IAAI,OAAO,GAAG,SAAS;GACzC,OAAO;GACP,YAAY;GACb,CAAC;;CAGJ,OAAA,cACE,OACA,KACA,OACe;EACf,MAAM,MAAM,SAAS,IAAI;EACzB,MAAM,UAAU,aAAa,IAAI;EACjC,MAAM,WAAW,IAAI,QAAQ,KAAK;GAAE,QAAQ;GAAO;GAAS,CAAC;EAC7D,MAAM,WAAW,MAAM,MAAM,QAAQ,SAAS;AAC9C,gBAAc,OAAO,SAAS;AAC9B,SAAO,MAAM,KAAK,MAAM,SAAS,MAAM,CAAC;;;AAI5C,eAAe,kBACb,SACA,KACA,OACe;CACf,MAAM,MAAM,SAAS,IAAI;CACzB,MAAM,UAAU,aAAa,IAAI;CAEjC,IAAI;AACJ,KAAI,IAAI,WAAW,SAAS,IAAI,WAAW,UAAU,IAAI,SAAS,KAAA,EAChE,QAAO,KAAK,UAAU,IAAI,KAAK;CAIjC,MAAM,WAAW,MAAM,QADF,IAAI,QAAQ,KAAK;EAAE,QAAQ,IAAI;EAAQ;EAAS;EAAM,CAAC,CAChC;AAC5C,eAAc,OAAO,SAAS;AAC9B,QAAO,MAAM,KAAK,MAAM,SAAS,MAAM,CAAC;;AAG1C,SAAS,SAAS,KAA6B;CAC7C,MAAM,OAAO,IAAI,QAAQ,QAAQ;AACjC,QAAO,GAAG,IAAI,SAAS,KAAK,OAAO,IAAI;;AAGzC,SAAS,aAAa,KAA8B;CAClD,MAAM,UAAU,IAAI,SAAS;AAC7B,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,IAAI,QAAQ,CACpD,KAAI,OAAO,UAAU,SACnB,SAAQ,IAAI,KAAK,MAAM;UACd,MAAM,QAAQ,MAAM,CAC7B,SAAQ,IAAI,KAAK,MAAM,KAAK,KAAK,CAAC;AAGtC,QAAO;;AAGT,SAAS,cAAc,OAAqB,UAA0B;AAC/D,OAAM,OAAO,SAAS,OAAO;AAClC,UAAS,QAAQ,SAAS,OAAO,QAAQ;AAClC,QAAM,OAAO,KAAK,MAAM;GAC7B;;AAGJ,SAAgB,2BAAsD;AACpE,QAAO,EAAE,SAAS,IAAI,oBAAoB,EAAE","debug_id":"4fb1d468-f1ff-54b7-91b9-0b7da8b78c2d"}