@rotorsoft/act-http 1.0.0 → 1.2.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/README.md +127 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/api/actor.d.ts +20 -0
- package/dist/@types/api/actor.d.ts.map +1 -0
- package/dist/@types/api/errors.d.ts +73 -0
- package/dist/@types/api/errors.d.ts.map +1 -0
- package/dist/@types/api/idempotency.d.ts +36 -0
- package/dist/@types/api/idempotency.d.ts.map +1 -0
- package/dist/@types/api/index.d.ts +39 -0
- package/dist/@types/api/index.d.ts.map +1 -0
- package/dist/@types/receiver/check.d.ts +66 -0
- package/dist/@types/receiver/check.d.ts.map +1 -0
- package/dist/@types/receiver/express/index.d.ts +51 -0
- package/dist/@types/receiver/express/index.d.ts.map +1 -0
- package/dist/@types/receiver/extract.d.ts +24 -0
- package/dist/@types/receiver/extract.d.ts.map +1 -0
- package/dist/@types/receiver/fastify/index.d.ts +55 -0
- package/dist/@types/receiver/fastify/index.d.ts.map +1 -0
- package/dist/@types/receiver/hono/index.d.ts +60 -0
- package/dist/@types/receiver/hono/index.d.ts.map +1 -0
- package/dist/@types/receiver/index.d.ts +39 -0
- package/dist/@types/receiver/index.d.ts.map +1 -0
- package/dist/@types/receiver/start.d.ts +48 -0
- package/dist/@types/receiver/start.d.ts.map +1 -0
- package/dist/@types/receiver/trpc/index.d.ts +16 -0
- package/dist/@types/receiver/trpc/index.d.ts.map +1 -0
- package/dist/@types/receiver/verify.d.ts +57 -0
- package/dist/@types/receiver/verify.d.ts.map +1 -0
- package/dist/@types/webhook/classify.d.ts +59 -0
- package/dist/@types/webhook/classify.d.ts.map +1 -0
- package/dist/@types/webhook/index.d.ts +3 -2
- package/dist/@types/webhook/index.d.ts.map +1 -1
- package/dist/@types/webhook/sign.d.ts +25 -0
- package/dist/@types/webhook/sign.d.ts.map +1 -0
- package/dist/@types/webhook/types.d.ts +61 -20
- package/dist/@types/webhook/types.d.ts.map +1 -1
- package/dist/api/index.cjs +85 -0
- package/dist/api/index.cjs.map +1 -0
- package/dist/api/index.js +62 -0
- package/dist/api/index.js.map +1 -0
- package/dist/chunk-F7VWYZ37.js +29 -0
- package/dist/chunk-F7VWYZ37.js.map +1 -0
- package/dist/chunk-NOIXOF2I.js +78 -0
- package/dist/chunk-NOIXOF2I.js.map +1 -0
- package/dist/dist-NWMJQI4E.js +647 -0
- package/dist/dist-NWMJQI4E.js.map +1 -0
- package/dist/receiver/express/index.cjs +128 -0
- package/dist/receiver/express/index.cjs.map +1 -0
- package/dist/receiver/express/index.js +33 -0
- package/dist/receiver/express/index.js.map +1 -0
- package/dist/receiver/fastify/index.cjs +120 -0
- package/dist/receiver/fastify/index.cjs.map +1 -0
- package/dist/receiver/fastify/index.js +25 -0
- package/dist/receiver/fastify/index.js.map +1 -0
- package/dist/receiver/hono/index.cjs +123 -0
- package/dist/receiver/hono/index.cjs.map +1 -0
- package/dist/receiver/hono/index.js +8 -0
- package/dist/receiver/hono/index.js.map +1 -0
- package/dist/receiver/index.cjs +2943 -0
- package/dist/receiver/index.cjs.map +1 -0
- package/dist/receiver/index.js +2162 -0
- package/dist/receiver/index.js.map +1 -0
- package/dist/receiver/trpc/index.cjs +126 -0
- package/dist/receiver/trpc/index.cjs.map +1 -0
- package/dist/receiver/trpc/index.js +31 -0
- package/dist/receiver/trpc/index.js.map +1 -0
- package/dist/webhook/index.cjs +66 -6
- package/dist/webhook/index.cjs.map +1 -1
- package/dist/webhook/index.js +62 -6
- package/dist/webhook/index.js.map +1 -1
- package/package.json +52 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../node_modules/.pnpm/@hono+node-server@1.19.14_hono@4.12.23/node_modules/@hono/node-server/dist/index.mjs"],"sourcesContent":["// src/server.ts\nimport { createServer as createServerHTTP } from \"http\";\n\n// src/listener.ts\nimport { Http2ServerRequest as Http2ServerRequest2, constants as h2constants } from \"http2\";\n\n// src/request.ts\nimport { Http2ServerRequest } from \"http2\";\nimport { Readable } from \"stream\";\nvar RequestError = class extends Error {\n constructor(message, options) {\n super(message, options);\n this.name = \"RequestError\";\n }\n};\nvar toRequestError = (e) => {\n if (e instanceof RequestError) {\n return e;\n }\n return new RequestError(e.message, { cause: e });\n};\nvar GlobalRequest = global.Request;\nvar Request = class extends GlobalRequest {\n constructor(input, options) {\n if (typeof input === \"object\" && getRequestCache in input) {\n input = input[getRequestCache]();\n }\n if (typeof options?.body?.getReader !== \"undefined\") {\n ;\n options.duplex ??= \"half\";\n }\n super(input, options);\n }\n};\nvar newHeadersFromIncoming = (incoming) => {\n const headerRecord = [];\n const rawHeaders = incoming.rawHeaders;\n for (let i = 0; i < rawHeaders.length; i += 2) {\n const { [i]: key, [i + 1]: value } = rawHeaders;\n if (key.charCodeAt(0) !== /*:*/\n 58) {\n headerRecord.push([key, value]);\n }\n }\n return new Headers(headerRecord);\n};\nvar wrapBodyStream = Symbol(\"wrapBodyStream\");\nvar newRequestFromIncoming = (method, url, headers, incoming, abortController) => {\n const init = {\n method,\n headers,\n signal: abortController.signal\n };\n if (method === \"TRACE\") {\n init.method = \"GET\";\n const req = new Request(url, init);\n Object.defineProperty(req, \"method\", {\n get() {\n return \"TRACE\";\n }\n });\n return req;\n }\n if (!(method === \"GET\" || method === \"HEAD\")) {\n if (\"rawBody\" in incoming && incoming.rawBody instanceof Buffer) {\n init.body = new ReadableStream({\n start(controller) {\n controller.enqueue(incoming.rawBody);\n controller.close();\n }\n });\n } else if (incoming[wrapBodyStream]) {\n let reader;\n init.body = new ReadableStream({\n async pull(controller) {\n try {\n reader ||= Readable.toWeb(incoming).getReader();\n const { done, value } = await reader.read();\n if (done) {\n controller.close();\n } else {\n controller.enqueue(value);\n }\n } catch (error) {\n controller.error(error);\n }\n }\n });\n } else {\n init.body = Readable.toWeb(incoming);\n }\n }\n return new Request(url, init);\n};\nvar getRequestCache = Symbol(\"getRequestCache\");\nvar requestCache = Symbol(\"requestCache\");\nvar incomingKey = Symbol(\"incomingKey\");\nvar urlKey = Symbol(\"urlKey\");\nvar headersKey = Symbol(\"headersKey\");\nvar abortControllerKey = Symbol(\"abortControllerKey\");\nvar getAbortController = Symbol(\"getAbortController\");\nvar requestPrototype = {\n get method() {\n return this[incomingKey].method || \"GET\";\n },\n get url() {\n return this[urlKey];\n },\n get headers() {\n return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]);\n },\n [getAbortController]() {\n this[getRequestCache]();\n return this[abortControllerKey];\n },\n [getRequestCache]() {\n this[abortControllerKey] ||= new AbortController();\n return this[requestCache] ||= newRequestFromIncoming(\n this.method,\n this[urlKey],\n this.headers,\n this[incomingKey],\n this[abortControllerKey]\n );\n }\n};\n[\n \"body\",\n \"bodyUsed\",\n \"cache\",\n \"credentials\",\n \"destination\",\n \"integrity\",\n \"mode\",\n \"redirect\",\n \"referrer\",\n \"referrerPolicy\",\n \"signal\",\n \"keepalive\"\n].forEach((k) => {\n Object.defineProperty(requestPrototype, k, {\n get() {\n return this[getRequestCache]()[k];\n }\n });\n});\n[\"arrayBuffer\", \"blob\", \"clone\", \"formData\", \"json\", \"text\"].forEach((k) => {\n Object.defineProperty(requestPrototype, k, {\n value: function() {\n return this[getRequestCache]()[k]();\n }\n });\n});\nObject.defineProperty(requestPrototype, Symbol.for(\"nodejs.util.inspect.custom\"), {\n value: function(depth, options, inspectFn) {\n const props = {\n method: this.method,\n url: this.url,\n headers: this.headers,\n nativeRequest: this[requestCache]\n };\n return `Request (lightweight) ${inspectFn(props, { ...options, depth: depth == null ? null : depth - 1 })}`;\n }\n});\nObject.setPrototypeOf(requestPrototype, Request.prototype);\nvar newRequest = (incoming, defaultHostname) => {\n const req = Object.create(requestPrototype);\n req[incomingKey] = incoming;\n const incomingUrl = incoming.url || \"\";\n if (incomingUrl[0] !== \"/\" && // short-circuit for performance. most requests are relative URL.\n (incomingUrl.startsWith(\"http://\") || incomingUrl.startsWith(\"https://\"))) {\n if (incoming instanceof Http2ServerRequest) {\n throw new RequestError(\"Absolute URL for :path is not allowed in HTTP/2\");\n }\n try {\n const url2 = new URL(incomingUrl);\n req[urlKey] = url2.href;\n } catch (e) {\n throw new RequestError(\"Invalid absolute URL\", { cause: e });\n }\n return req;\n }\n const host = (incoming instanceof Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname;\n if (!host) {\n throw new RequestError(\"Missing host header\");\n }\n let scheme;\n if (incoming instanceof Http2ServerRequest) {\n scheme = incoming.scheme;\n if (!(scheme === \"http\" || scheme === \"https\")) {\n throw new RequestError(\"Unsupported scheme\");\n }\n } else {\n scheme = incoming.socket && incoming.socket.encrypted ? \"https\" : \"http\";\n }\n const url = new URL(`${scheme}://${host}${incomingUrl}`);\n if (url.hostname.length !== host.length && url.hostname !== host.replace(/:\\d+$/, \"\")) {\n throw new RequestError(\"Invalid host header\");\n }\n req[urlKey] = url.href;\n return req;\n};\n\n// src/response.ts\nvar responseCache = Symbol(\"responseCache\");\nvar getResponseCache = Symbol(\"getResponseCache\");\nvar cacheKey = Symbol(\"cache\");\nvar GlobalResponse = global.Response;\nvar Response2 = class _Response {\n #body;\n #init;\n [getResponseCache]() {\n delete this[cacheKey];\n return this[responseCache] ||= new GlobalResponse(this.#body, this.#init);\n }\n constructor(body, init) {\n let headers;\n this.#body = body;\n if (init instanceof _Response) {\n const cachedGlobalResponse = init[responseCache];\n if (cachedGlobalResponse) {\n this.#init = cachedGlobalResponse;\n this[getResponseCache]();\n return;\n } else {\n this.#init = init.#init;\n headers = new Headers(init.#init.headers);\n }\n } else {\n this.#init = init;\n }\n if (typeof body === \"string\" || typeof body?.getReader !== \"undefined\" || body instanceof Blob || body instanceof Uint8Array) {\n ;\n this[cacheKey] = [init?.status || 200, body, headers || init?.headers];\n }\n }\n get headers() {\n const cache = this[cacheKey];\n if (cache) {\n if (!(cache[2] instanceof Headers)) {\n cache[2] = new Headers(\n cache[2] || { \"content-type\": \"text/plain; charset=UTF-8\" }\n );\n }\n return cache[2];\n }\n return this[getResponseCache]().headers;\n }\n get status() {\n return this[cacheKey]?.[0] ?? this[getResponseCache]().status;\n }\n get ok() {\n const status = this.status;\n return status >= 200 && status < 300;\n }\n};\n[\"body\", \"bodyUsed\", \"redirected\", \"statusText\", \"trailers\", \"type\", \"url\"].forEach((k) => {\n Object.defineProperty(Response2.prototype, k, {\n get() {\n return this[getResponseCache]()[k];\n }\n });\n});\n[\"arrayBuffer\", \"blob\", \"clone\", \"formData\", \"json\", \"text\"].forEach((k) => {\n Object.defineProperty(Response2.prototype, k, {\n value: function() {\n return this[getResponseCache]()[k]();\n }\n });\n});\nObject.defineProperty(Response2.prototype, Symbol.for(\"nodejs.util.inspect.custom\"), {\n value: function(depth, options, inspectFn) {\n const props = {\n status: this.status,\n headers: this.headers,\n ok: this.ok,\n nativeResponse: this[responseCache]\n };\n return `Response (lightweight) ${inspectFn(props, { ...options, depth: depth == null ? null : depth - 1 })}`;\n }\n});\nObject.setPrototypeOf(Response2, GlobalResponse);\nObject.setPrototypeOf(Response2.prototype, GlobalResponse.prototype);\n\n// src/utils.ts\nasync function readWithoutBlocking(readPromise) {\n return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(void 0))]);\n}\nfunction writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) {\n const cancel = (error) => {\n reader.cancel(error).catch(() => {\n });\n };\n writable.on(\"close\", cancel);\n writable.on(\"error\", cancel);\n (currentReadPromise ?? reader.read()).then(flow, handleStreamError);\n return reader.closed.finally(() => {\n writable.off(\"close\", cancel);\n writable.off(\"error\", cancel);\n });\n function handleStreamError(error) {\n if (error) {\n writable.destroy(error);\n }\n }\n function onDrain() {\n reader.read().then(flow, handleStreamError);\n }\n function flow({ done, value }) {\n try {\n if (done) {\n writable.end();\n } else if (!writable.write(value)) {\n writable.once(\"drain\", onDrain);\n } else {\n return reader.read().then(flow, handleStreamError);\n }\n } catch (e) {\n handleStreamError(e);\n }\n }\n}\nfunction writeFromReadableStream(stream, writable) {\n if (stream.locked) {\n throw new TypeError(\"ReadableStream is locked.\");\n } else if (writable.destroyed) {\n return;\n }\n return writeFromReadableStreamDefaultReader(stream.getReader(), writable);\n}\nvar buildOutgoingHttpHeaders = (headers) => {\n const res = {};\n if (!(headers instanceof Headers)) {\n headers = new Headers(headers ?? void 0);\n }\n const cookies = [];\n for (const [k, v] of headers) {\n if (k === \"set-cookie\") {\n cookies.push(v);\n } else {\n res[k] = v;\n }\n }\n if (cookies.length > 0) {\n res[\"set-cookie\"] = cookies;\n }\n res[\"content-type\"] ??= \"text/plain; charset=UTF-8\";\n return res;\n};\n\n// src/utils/response/constants.ts\nvar X_ALREADY_SENT = \"x-hono-already-sent\";\n\n// src/globals.ts\nimport crypto from \"crypto\";\nif (typeof global.crypto === \"undefined\") {\n global.crypto = crypto;\n}\n\n// src/listener.ts\nvar outgoingEnded = Symbol(\"outgoingEnded\");\nvar incomingDraining = Symbol(\"incomingDraining\");\nvar DRAIN_TIMEOUT_MS = 500;\nvar MAX_DRAIN_BYTES = 64 * 1024 * 1024;\nvar drainIncoming = (incoming) => {\n const incomingWithDrainState = incoming;\n if (incoming.destroyed || incomingWithDrainState[incomingDraining]) {\n return;\n }\n incomingWithDrainState[incomingDraining] = true;\n if (incoming instanceof Http2ServerRequest2) {\n try {\n ;\n incoming.stream?.close?.(h2constants.NGHTTP2_NO_ERROR);\n } catch {\n }\n return;\n }\n let bytesRead = 0;\n const cleanup = () => {\n clearTimeout(timer);\n incoming.off(\"data\", onData);\n incoming.off(\"end\", cleanup);\n incoming.off(\"error\", cleanup);\n };\n const forceClose = () => {\n cleanup();\n const socket = incoming.socket;\n if (socket && !socket.destroyed) {\n socket.destroySoon();\n }\n };\n const timer = setTimeout(forceClose, DRAIN_TIMEOUT_MS);\n timer.unref?.();\n const onData = (chunk) => {\n bytesRead += chunk.length;\n if (bytesRead > MAX_DRAIN_BYTES) {\n forceClose();\n }\n };\n incoming.on(\"data\", onData);\n incoming.on(\"end\", cleanup);\n incoming.on(\"error\", cleanup);\n incoming.resume();\n};\nvar handleRequestError = () => new Response(null, {\n status: 400\n});\nvar handleFetchError = (e) => new Response(null, {\n status: e instanceof Error && (e.name === \"TimeoutError\" || e.constructor.name === \"TimeoutError\") ? 504 : 500\n});\nvar handleResponseError = (e, outgoing) => {\n const err = e instanceof Error ? e : new Error(\"unknown error\", { cause: e });\n if (err.code === \"ERR_STREAM_PREMATURE_CLOSE\") {\n console.info(\"The user aborted a request.\");\n } else {\n console.error(e);\n if (!outgoing.headersSent) {\n outgoing.writeHead(500, { \"Content-Type\": \"text/plain\" });\n }\n outgoing.end(`Error: ${err.message}`);\n outgoing.destroy(err);\n }\n};\nvar flushHeaders = (outgoing) => {\n if (\"flushHeaders\" in outgoing && outgoing.writable) {\n outgoing.flushHeaders();\n }\n};\nvar responseViaCache = async (res, outgoing) => {\n let [status, body, header] = res[cacheKey];\n let hasContentLength = false;\n if (!header) {\n header = { \"content-type\": \"text/plain; charset=UTF-8\" };\n } else if (header instanceof Headers) {\n hasContentLength = header.has(\"content-length\");\n header = buildOutgoingHttpHeaders(header);\n } else if (Array.isArray(header)) {\n const headerObj = new Headers(header);\n hasContentLength = headerObj.has(\"content-length\");\n header = buildOutgoingHttpHeaders(headerObj);\n } else {\n for (const key in header) {\n if (key.length === 14 && key.toLowerCase() === \"content-length\") {\n hasContentLength = true;\n break;\n }\n }\n }\n if (!hasContentLength) {\n if (typeof body === \"string\") {\n header[\"Content-Length\"] = Buffer.byteLength(body);\n } else if (body instanceof Uint8Array) {\n header[\"Content-Length\"] = body.byteLength;\n } else if (body instanceof Blob) {\n header[\"Content-Length\"] = body.size;\n }\n }\n outgoing.writeHead(status, header);\n if (typeof body === \"string\" || body instanceof Uint8Array) {\n outgoing.end(body);\n } else if (body instanceof Blob) {\n outgoing.end(new Uint8Array(await body.arrayBuffer()));\n } else {\n flushHeaders(outgoing);\n await writeFromReadableStream(body, outgoing)?.catch(\n (e) => handleResponseError(e, outgoing)\n );\n }\n ;\n outgoing[outgoingEnded]?.();\n};\nvar isPromise = (res) => typeof res.then === \"function\";\nvar responseViaResponseObject = async (res, outgoing, options = {}) => {\n if (isPromise(res)) {\n if (options.errorHandler) {\n try {\n res = await res;\n } catch (err) {\n const errRes = await options.errorHandler(err);\n if (!errRes) {\n return;\n }\n res = errRes;\n }\n } else {\n res = await res.catch(handleFetchError);\n }\n }\n if (cacheKey in res) {\n return responseViaCache(res, outgoing);\n }\n const resHeaderRecord = buildOutgoingHttpHeaders(res.headers);\n if (res.body) {\n const reader = res.body.getReader();\n const values = [];\n let done = false;\n let currentReadPromise = void 0;\n if (resHeaderRecord[\"transfer-encoding\"] !== \"chunked\") {\n let maxReadCount = 2;\n for (let i = 0; i < maxReadCount; i++) {\n currentReadPromise ||= reader.read();\n const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => {\n console.error(e);\n done = true;\n });\n if (!chunk) {\n if (i === 1) {\n await new Promise((resolve) => setTimeout(resolve));\n maxReadCount = 3;\n continue;\n }\n break;\n }\n currentReadPromise = void 0;\n if (chunk.value) {\n values.push(chunk.value);\n }\n if (chunk.done) {\n done = true;\n break;\n }\n }\n if (done && !(\"content-length\" in resHeaderRecord)) {\n resHeaderRecord[\"content-length\"] = values.reduce((acc, value) => acc + value.length, 0);\n }\n }\n outgoing.writeHead(res.status, resHeaderRecord);\n values.forEach((value) => {\n ;\n outgoing.write(value);\n });\n if (done) {\n outgoing.end();\n } else {\n if (values.length === 0) {\n flushHeaders(outgoing);\n }\n await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise);\n }\n } else if (resHeaderRecord[X_ALREADY_SENT]) {\n } else {\n outgoing.writeHead(res.status, resHeaderRecord);\n outgoing.end();\n }\n ;\n outgoing[outgoingEnded]?.();\n};\nvar getRequestListener = (fetchCallback, options = {}) => {\n const autoCleanupIncoming = options.autoCleanupIncoming ?? true;\n if (options.overrideGlobalObjects !== false && global.Request !== Request) {\n Object.defineProperty(global, \"Request\", {\n value: Request\n });\n Object.defineProperty(global, \"Response\", {\n value: Response2\n });\n }\n return async (incoming, outgoing) => {\n let res, req;\n try {\n req = newRequest(incoming, options.hostname);\n let incomingEnded = !autoCleanupIncoming || incoming.method === \"GET\" || incoming.method === \"HEAD\";\n if (!incomingEnded) {\n ;\n incoming[wrapBodyStream] = true;\n incoming.on(\"end\", () => {\n incomingEnded = true;\n });\n if (incoming instanceof Http2ServerRequest2) {\n ;\n outgoing[outgoingEnded] = () => {\n if (!incomingEnded) {\n setTimeout(() => {\n if (!incomingEnded) {\n setTimeout(() => {\n drainIncoming(incoming);\n });\n }\n });\n }\n };\n }\n outgoing.on(\"finish\", () => {\n if (!incomingEnded) {\n drainIncoming(incoming);\n }\n });\n }\n outgoing.on(\"close\", () => {\n const abortController = req[abortControllerKey];\n if (abortController) {\n if (incoming.errored) {\n req[abortControllerKey].abort(incoming.errored.toString());\n } else if (!outgoing.writableFinished) {\n req[abortControllerKey].abort(\"Client connection prematurely closed.\");\n }\n }\n if (!incomingEnded) {\n setTimeout(() => {\n if (!incomingEnded) {\n setTimeout(() => {\n drainIncoming(incoming);\n });\n }\n });\n }\n });\n res = fetchCallback(req, { incoming, outgoing });\n if (cacheKey in res) {\n return responseViaCache(res, outgoing);\n }\n } catch (e) {\n if (!res) {\n if (options.errorHandler) {\n res = await options.errorHandler(req ? e : toRequestError(e));\n if (!res) {\n return;\n }\n } else if (!req) {\n res = handleRequestError();\n } else {\n res = handleFetchError(e);\n }\n } else {\n return handleResponseError(e, outgoing);\n }\n }\n try {\n return await responseViaResponseObject(res, outgoing, options);\n } catch (e) {\n return handleResponseError(e, outgoing);\n }\n };\n};\n\n// src/server.ts\nvar createAdaptorServer = (options) => {\n const fetchCallback = options.fetch;\n const requestListener = getRequestListener(fetchCallback, {\n hostname: options.hostname,\n overrideGlobalObjects: options.overrideGlobalObjects,\n autoCleanupIncoming: options.autoCleanupIncoming\n });\n const createServer = options.createServer || createServerHTTP;\n const server = createServer(options.serverOptions || {}, requestListener);\n return server;\n};\nvar serve = (options, listeningListener) => {\n const server = createAdaptorServer(options);\n server.listen(options?.port ?? 3e3, options.hostname, () => {\n const serverInfo = server.address();\n listeningListener && listeningListener(serverInfo);\n });\n return server;\n};\nexport {\n RequestError,\n createAdaptorServer,\n getRequestListener,\n serve\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/receiver/express/index.ts
|
|
21
|
+
var express_exports = {};
|
|
22
|
+
__export(express_exports, {
|
|
23
|
+
webhookMiddleware: () => webhookMiddleware
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(express_exports);
|
|
26
|
+
|
|
27
|
+
// src/receiver/extract.ts
|
|
28
|
+
function extractIdempotencyKey(headers) {
|
|
29
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
30
|
+
if (name.toLowerCase() !== "idempotency-key") continue;
|
|
31
|
+
if (Array.isArray(value)) return void 0;
|
|
32
|
+
if (value === "") return void 0;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/receiver/verify.ts
|
|
39
|
+
var import_node_crypto = require("crypto");
|
|
40
|
+
function verifyWebhook(headers, body, secret, options) {
|
|
41
|
+
const maxAgeSeconds = options?.maxAgeSeconds ?? 300;
|
|
42
|
+
const now = options?.now ?? Math.floor(Date.now() / 1e3);
|
|
43
|
+
const signature = pickHeader(headers, "x-webhook-signature");
|
|
44
|
+
if (!signature) return { ok: false, reason: "missing-signature" };
|
|
45
|
+
const timestampStr = pickHeader(headers, "x-webhook-timestamp");
|
|
46
|
+
if (!timestampStr) return { ok: false, reason: "missing-timestamp" };
|
|
47
|
+
const timestamp = Number.parseInt(timestampStr, 10);
|
|
48
|
+
if (Number.isNaN(timestamp) || String(timestamp) !== timestampStr) {
|
|
49
|
+
return { ok: false, reason: "missing-timestamp" };
|
|
50
|
+
}
|
|
51
|
+
const delta = now - timestamp;
|
|
52
|
+
if (delta > maxAgeSeconds) return { ok: false, reason: "stale" };
|
|
53
|
+
if (delta < -maxAgeSeconds) return { ok: false, reason: "future" };
|
|
54
|
+
if (!signature.startsWith("sha256=")) {
|
|
55
|
+
return { ok: false, reason: "bad-signature" };
|
|
56
|
+
}
|
|
57
|
+
const providedHex = signature.slice("sha256=".length);
|
|
58
|
+
if (!/^[0-9a-fA-F]{64}$/.test(providedHex)) {
|
|
59
|
+
return { ok: false, reason: "bad-signature" };
|
|
60
|
+
}
|
|
61
|
+
const expectedHex = (0, import_node_crypto.createHmac)("sha256", secret).update(`${timestampStr}.${body}`).digest("hex");
|
|
62
|
+
const a = Buffer.from(providedHex, "hex");
|
|
63
|
+
const b = Buffer.from(expectedHex, "hex");
|
|
64
|
+
if (!(0, import_node_crypto.timingSafeEqual)(a, b)) {
|
|
65
|
+
return { ok: false, reason: "bad-signature" };
|
|
66
|
+
}
|
|
67
|
+
return { ok: true };
|
|
68
|
+
}
|
|
69
|
+
function pickHeader(headers, lowerName) {
|
|
70
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
71
|
+
if (name.toLowerCase() !== lowerName) continue;
|
|
72
|
+
if (Array.isArray(value) || value === void 0 || value === "") {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/receiver/check.ts
|
|
81
|
+
async function checkWebhook(headers, body, options) {
|
|
82
|
+
if (options.secret !== void 0) {
|
|
83
|
+
const verification = verifyWebhook(
|
|
84
|
+
headers,
|
|
85
|
+
body,
|
|
86
|
+
options.secret,
|
|
87
|
+
options.verify
|
|
88
|
+
);
|
|
89
|
+
if (!verification.ok) {
|
|
90
|
+
return { ok: false, status: 401, reason: verification.reason };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const key = extractIdempotencyKey(headers);
|
|
94
|
+
if (!key) return { ok: false, status: 400, reason: "missing-key" };
|
|
95
|
+
const claimed = await options.store.claim(key);
|
|
96
|
+
return { ok: true, key, deduped: !claimed };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/receiver/express/index.ts
|
|
100
|
+
function webhookMiddleware(options) {
|
|
101
|
+
return async function check(req, res, next) {
|
|
102
|
+
const rawBody = bufferOrString(req.body);
|
|
103
|
+
const result = await checkWebhook(
|
|
104
|
+
req.headers,
|
|
105
|
+
rawBody,
|
|
106
|
+
options
|
|
107
|
+
);
|
|
108
|
+
if (!result.ok) {
|
|
109
|
+
res.status(result.status).json({ error: result.reason });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
req.idempotency = {
|
|
113
|
+
key: result.key,
|
|
114
|
+
deduped: result.deduped
|
|
115
|
+
};
|
|
116
|
+
next();
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function bufferOrString(body) {
|
|
120
|
+
if (typeof body === "string") return body;
|
|
121
|
+
if (body instanceof Uint8Array) return Buffer.from(body).toString("utf8");
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
125
|
+
0 && (module.exports = {
|
|
126
|
+
webhookMiddleware
|
|
127
|
+
});
|
|
128
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/receiver/express/index.ts","../../../src/receiver/extract.ts","../../../src/receiver/verify.ts","../../../src/receiver/check.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/receiver/express\n *\n * Express adapter for the receiver-side webhook check.\n *\n * Usage:\n *\n * ```ts\n * import express from \"express\";\n * import { webhookMiddleware } from \"@rotorsoft/act-http/receiver/express\";\n * import { InMemoryIdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n *\n * const app = express();\n * const dedup = new InMemoryIdempotencyStore();\n *\n * // Raw body capture required when signing is enabled.\n * app.use(express.raw({ type: \"application/json\" }));\n *\n * app.post(\n * \"/webhooks/orders\",\n * webhookMiddleware({ store: dedup, secret: process.env.WEBHOOK_SECRET }),\n * (req, res) => {\n * const { key, deduped } = (req as any).idempotency;\n * if (deduped) return res.json({ status: \"dedup-skipped\", key });\n * // ... process the inbound event ...\n * res.json({ status: \"processed\", key });\n * }\n * );\n * ```\n *\n * On failure: responds with the framework-idiomatic JSON shape\n * `{ error: <reason> }` at status 400 (missing-key) or 401\n * (verification failures), and does not call `next()`. On success:\n * attaches `req.idempotency = { key, deduped }` and calls `next()`.\n *\n * **Raw body requirement**: when `secret` is configured, mount\n * `express.raw({ type: \"application/json\" })` (or whatever\n * content-type your webhooks use) ahead of the receiver middleware.\n * The middleware reads `req.body` as a `Buffer | string` and converts\n * to a UTF-8 string for hashing. Skip when unsigned.\n */\nimport type { NextFunction, Request, RequestHandler, Response } from \"express\";\nimport { type CheckWebhookOptions, checkWebhook } from \"../check.js\";\n\n/**\n * Build an Express middleware that verifies the request signature\n * (when `secret` is set), enforces `Idempotency-Key`, and claims the\n * key on the configured store. See the module-level docs for usage.\n */\nexport function webhookMiddleware(\n options: CheckWebhookOptions\n): RequestHandler {\n return async function check(\n req: Request,\n res: Response,\n next: NextFunction\n ): Promise<void> {\n const rawBody = bufferOrString(req.body);\n const result = await checkWebhook(\n req.headers as Record<string, string | string[] | undefined>,\n rawBody,\n options\n );\n if (!result.ok) {\n res.status(result.status).json({ error: result.reason });\n return;\n }\n (\n req as Request & { idempotency: { key: string; deduped: boolean } }\n ).idempotency = {\n key: result.key,\n deduped: result.deduped,\n };\n next();\n };\n}\n\nfunction bufferOrString(body: unknown): string {\n if (typeof body === \"string\") return body;\n if (body instanceof Uint8Array) return Buffer.from(body).toString(\"utf8\");\n return \"\";\n}\n","/**\n * Pull the `Idempotency-Key` header from a Node-style headers bag,\n * case-insensitive. Returns `undefined` when any of the following\n * carries no usable key:\n *\n * - the header is missing\n * - its value is an array (ambiguous — can't pick one without a\n * policy the receiver hasn't declared)\n * - its value is the empty string (carries no idempotency\n * information; structurally equivalent to \"no header at all\")\n *\n * Pair with `IdempotencyStore.claim` from\n * `@rotorsoft/act-ops/idempotency`: extract the key from the inbound\n * request, claim it on the store, return a `deduped` marker when the\n * claim fails. The framework-agnostic middleware that wires these\n * together lands in #744.\n *\n * Validation beyond \"is there a usable key?\" (length bounds, format\n * checks, normalization) is intentionally out of scope. Receivers\n * picking a policy can layer it on top — or, when #744 ships, opt\n * into the middleware's opinionated defaults.\n */\nexport function extractIdempotencyKey(\n headers: Record<string, string | string[] | undefined>\n): string | undefined {\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() !== \"idempotency-key\") continue;\n if (Array.isArray(value)) return undefined;\n if (value === \"\") return undefined;\n return value;\n }\n return undefined;\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Outcome of {@link verifyWebhook}. Either the request signature\n * checks out, or one of five distinct failure reasons applies. Each\n * reason maps to an operator-meaningful telemetry bucket — separated\n * deliberately so dashboards can distinguish \"client lost its secret\"\n * from \"client clock is wrong\" from \"this is a replay attack.\"\n */\nexport type VerifyResult =\n | { ok: true }\n | {\n ok: false;\n reason:\n | \"missing-signature\"\n | \"missing-timestamp\"\n | \"stale\"\n | \"future\"\n | \"bad-signature\";\n };\n\n/** Options for {@link verifyWebhook}. */\nexport type VerifyOptions = {\n /**\n * Maximum acceptable timestamp drift in either direction, in\n * seconds. Default: 300 (±5 minutes) — matches Stripe / GitHub /\n * Slack conventions. Tightening narrows the replay window;\n * loosening accommodates clients with worse clock sync.\n */\n maxAgeSeconds?: number;\n /**\n * Current Unix-seconds time. Exposed for tests; production\n * callers should leave it undefined so wall-clock is used.\n */\n now?: number;\n};\n\n/**\n * Verify an inbound webhook's signature and timestamp against the\n * shared secret. Pair with the sender side: configure\n * `webhook({ secret })` from `@rotorsoft/act-http/webhook`.\n *\n * Returns `{ ok: true }` on success or `{ ok: false; reason }` on\n * failure. The reasons are:\n *\n * - `missing-signature` — no `X-Webhook-Signature` header, value\n * was an array, or value was empty.\n * - `missing-timestamp` — no `X-Webhook-Timestamp` header, value\n * was empty, or value isn't a parseable integer.\n * - `stale` — timestamp older than `maxAgeSeconds` from `now`.\n * - `future` — timestamp more than `maxAgeSeconds` ahead of `now`.\n * - `bad-signature` — signature header didn't start with `sha256=`,\n * wasn't 64 hex chars, or the recomputed HMAC didn't match\n * (constant-time compare).\n *\n * The signed payload is `${timestamp}.${body}`, so `body` must be\n * the **raw request body bytes**. Any pre-parse normalization\n * (whitespace trimming, JSON re-stringification) would change the\n * hash and reject every otherwise-valid request. Framework adapters\n * in #744 will provide the raw body alongside the parsed one.\n *\n * Uses Node's `crypto.timingSafeEqual` for the final comparison to\n * avoid signature-equality timing attacks.\n */\nexport function verifyWebhook(\n headers: Record<string, string | string[] | undefined>,\n body: string,\n secret: string,\n options?: VerifyOptions\n): VerifyResult {\n const maxAgeSeconds = options?.maxAgeSeconds ?? 300;\n const now = options?.now ?? Math.floor(Date.now() / 1000);\n\n const signature = pickHeader(headers, \"x-webhook-signature\");\n if (!signature) return { ok: false, reason: \"missing-signature\" };\n\n const timestampStr = pickHeader(headers, \"x-webhook-timestamp\");\n if (!timestampStr) return { ok: false, reason: \"missing-timestamp\" };\n const timestamp = Number.parseInt(timestampStr, 10);\n if (Number.isNaN(timestamp) || String(timestamp) !== timestampStr) {\n return { ok: false, reason: \"missing-timestamp\" };\n }\n\n const delta = now - timestamp;\n if (delta > maxAgeSeconds) return { ok: false, reason: \"stale\" };\n if (delta < -maxAgeSeconds) return { ok: false, reason: \"future\" };\n\n if (!signature.startsWith(\"sha256=\")) {\n return { ok: false, reason: \"bad-signature\" };\n }\n const providedHex = signature.slice(\"sha256=\".length);\n if (!/^[0-9a-fA-F]{64}$/.test(providedHex)) {\n return { ok: false, reason: \"bad-signature\" };\n }\n\n const expectedHex = createHmac(\"sha256\", secret)\n .update(`${timestampStr}.${body}`)\n .digest(\"hex\");\n\n const a = Buffer.from(providedHex, \"hex\");\n const b = Buffer.from(expectedHex, \"hex\");\n if (!timingSafeEqual(a, b)) {\n return { ok: false, reason: \"bad-signature\" };\n }\n\n return { ok: true };\n}\n\nfunction pickHeader(\n headers: Record<string, string | string[] | undefined>,\n lowerName: string\n): string | undefined {\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() !== lowerName) continue;\n if (Array.isArray(value) || value === undefined || value === \"\") {\n return undefined;\n }\n return value;\n }\n return undefined;\n}\n","import type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\nimport { extractIdempotencyKey } from \"./extract.js\";\nimport { type VerifyOptions, verifyWebhook } from \"./verify.js\";\n\n/**\n * Failure reasons returned by {@link checkWebhook}. The shape splits\n * `missing-key` (a client error, mapped to HTTP 400) from the five\n * verification failures (authentication errors, HTTP 401) so each\n * maps to its own telemetry bucket.\n */\nexport type CheckFailureReason =\n | \"missing-key\"\n | \"missing-signature\"\n | \"missing-timestamp\"\n | \"stale\"\n | \"future\"\n | \"bad-signature\";\n\n/**\n * Outcome of {@link checkWebhook}. Either the request passed every\n * configured check and carries a usable idempotency key, or it\n * failed one of them and the framework adapter should reply with the\n * corresponding HTTP status.\n */\nexport type CheckResult =\n | { ok: false; status: 400 | 401; reason: CheckFailureReason }\n | { ok: true; key: string; deduped: boolean };\n\n/** Options for {@link checkWebhook}. */\nexport type CheckWebhookOptions = {\n /** Idempotency store the framework-agnostic core claims the key on. */\n store: IdempotencyStore;\n /**\n * Optional HMAC-SHA256 secret. When set, the request's\n * `X-Webhook-Signature` and `X-Webhook-Timestamp` headers are\n * verified before the dedup claim. When omitted, signature\n * verification is skipped (unsigned receivers).\n */\n secret?: string;\n /**\n * Verification options forwarded to {@link verifyWebhook}. Only\n * meaningful when `secret` is set. Defaults to a ±300-second\n * timestamp window.\n */\n verify?: VerifyOptions;\n};\n\n/**\n * Framework-agnostic receiver check: verify the signature (when a\n * secret is configured), extract the `Idempotency-Key`, and claim\n * it on the store. Returns the request's fate as a discriminated\n * union the per-framework adapter translates into the framework's\n * idiomatic 4xx response or context injection.\n *\n * **Order of checks** (matters):\n *\n * 1. Verify signature + timestamp window (when `secret` is set).\n * Rejecting bad signatures *before* extracting and claiming the\n * key keeps attacker-supplied keys out of the dedup store —\n * otherwise a flood of spoofed POSTs would pollute the LRU.\n * 2. Extract the `Idempotency-Key`. Missing → reject with 400.\n * 3. Claim the key on the store. If already seen, return\n * `{ ok: true; deduped: true }` so the framework adapter can\n * short-circuit the handler without re-running side effects.\n *\n * The dedup store may be sync (`InMemoryIdempotencyStore`) or async\n * (durable adapters like a future `PostgresIdempotencyStore`); the\n * core awaits unconditionally so both shapes compose cleanly.\n */\nexport async function checkWebhook(\n headers: Record<string, string | string[] | undefined>,\n body: string,\n options: CheckWebhookOptions\n): Promise<CheckResult> {\n if (options.secret !== undefined) {\n const verification = verifyWebhook(\n headers,\n body,\n options.secret,\n options.verify\n );\n if (!verification.ok) {\n return { ok: false, status: 401, reason: verification.reason };\n }\n }\n\n const key = extractIdempotencyKey(headers);\n if (!key) return { ok: false, status: 400, reason: \"missing-key\" };\n\n const claimed = await options.store.claim(key);\n return { ok: true, key, deduped: !claimed };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkWebhook
|
|
3
|
+
} from "../../chunk-NOIXOF2I.js";
|
|
4
|
+
|
|
5
|
+
// src/receiver/express/index.ts
|
|
6
|
+
function webhookMiddleware(options) {
|
|
7
|
+
return async function check(req, res, next) {
|
|
8
|
+
const rawBody = bufferOrString(req.body);
|
|
9
|
+
const result = await checkWebhook(
|
|
10
|
+
req.headers,
|
|
11
|
+
rawBody,
|
|
12
|
+
options
|
|
13
|
+
);
|
|
14
|
+
if (!result.ok) {
|
|
15
|
+
res.status(result.status).json({ error: result.reason });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
req.idempotency = {
|
|
19
|
+
key: result.key,
|
|
20
|
+
deduped: result.deduped
|
|
21
|
+
};
|
|
22
|
+
next();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function bufferOrString(body) {
|
|
26
|
+
if (typeof body === "string") return body;
|
|
27
|
+
if (body instanceof Uint8Array) return Buffer.from(body).toString("utf8");
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
webhookMiddleware
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/receiver/express/index.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/receiver/express\n *\n * Express adapter for the receiver-side webhook check.\n *\n * Usage:\n *\n * ```ts\n * import express from \"express\";\n * import { webhookMiddleware } from \"@rotorsoft/act-http/receiver/express\";\n * import { InMemoryIdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n *\n * const app = express();\n * const dedup = new InMemoryIdempotencyStore();\n *\n * // Raw body capture required when signing is enabled.\n * app.use(express.raw({ type: \"application/json\" }));\n *\n * app.post(\n * \"/webhooks/orders\",\n * webhookMiddleware({ store: dedup, secret: process.env.WEBHOOK_SECRET }),\n * (req, res) => {\n * const { key, deduped } = (req as any).idempotency;\n * if (deduped) return res.json({ status: \"dedup-skipped\", key });\n * // ... process the inbound event ...\n * res.json({ status: \"processed\", key });\n * }\n * );\n * ```\n *\n * On failure: responds with the framework-idiomatic JSON shape\n * `{ error: <reason> }` at status 400 (missing-key) or 401\n * (verification failures), and does not call `next()`. On success:\n * attaches `req.idempotency = { key, deduped }` and calls `next()`.\n *\n * **Raw body requirement**: when `secret` is configured, mount\n * `express.raw({ type: \"application/json\" })` (or whatever\n * content-type your webhooks use) ahead of the receiver middleware.\n * The middleware reads `req.body` as a `Buffer | string` and converts\n * to a UTF-8 string for hashing. Skip when unsigned.\n */\nimport type { NextFunction, Request, RequestHandler, Response } from \"express\";\nimport { type CheckWebhookOptions, checkWebhook } from \"../check.js\";\n\n/**\n * Build an Express middleware that verifies the request signature\n * (when `secret` is set), enforces `Idempotency-Key`, and claims the\n * key on the configured store. See the module-level docs for usage.\n */\nexport function webhookMiddleware(\n options: CheckWebhookOptions\n): RequestHandler {\n return async function check(\n req: Request,\n res: Response,\n next: NextFunction\n ): Promise<void> {\n const rawBody = bufferOrString(req.body);\n const result = await checkWebhook(\n req.headers as Record<string, string | string[] | undefined>,\n rawBody,\n options\n );\n if (!result.ok) {\n res.status(result.status).json({ error: result.reason });\n return;\n }\n (\n req as Request & { idempotency: { key: string; deduped: boolean } }\n ).idempotency = {\n key: result.key,\n deduped: result.deduped,\n };\n next();\n };\n}\n\nfunction bufferOrString(body: unknown): string {\n if (typeof body === \"string\") return body;\n if (body instanceof Uint8Array) return Buffer.from(body).toString(\"utf8\");\n return \"\";\n}\n"],"mappings":";;;;;AAkDO,SAAS,kBACd,SACgB;AAChB,SAAO,eAAe,MACpB,KACA,KACA,MACe;AACf,UAAM,UAAU,eAAe,IAAI,IAAI;AACvC,UAAM,SAAS,MAAM;AAAA,MACnB,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AACA,QAAI,CAAC,OAAO,IAAI;AACd,UAAI,OAAO,OAAO,MAAM,EAAE,KAAK,EAAE,OAAO,OAAO,OAAO,CAAC;AACvD;AAAA,IACF;AACA,IACE,IACA,cAAc;AAAA,MACd,KAAK,OAAO;AAAA,MACZ,SAAS,OAAO;AAAA,IAClB;AACA,SAAK;AAAA,EACP;AACF;AAEA,SAAS,eAAe,MAAuB;AAC7C,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,MAAI,gBAAgB,WAAY,QAAO,OAAO,KAAK,IAAI,EAAE,SAAS,MAAM;AACxE,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/receiver/fastify/index.ts
|
|
21
|
+
var fastify_exports = {};
|
|
22
|
+
__export(fastify_exports, {
|
|
23
|
+
webhookMiddleware: () => webhookMiddleware
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(fastify_exports);
|
|
26
|
+
|
|
27
|
+
// src/receiver/extract.ts
|
|
28
|
+
function extractIdempotencyKey(headers) {
|
|
29
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
30
|
+
if (name.toLowerCase() !== "idempotency-key") continue;
|
|
31
|
+
if (Array.isArray(value)) return void 0;
|
|
32
|
+
if (value === "") return void 0;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/receiver/verify.ts
|
|
39
|
+
var import_node_crypto = require("crypto");
|
|
40
|
+
function verifyWebhook(headers, body, secret, options) {
|
|
41
|
+
const maxAgeSeconds = options?.maxAgeSeconds ?? 300;
|
|
42
|
+
const now = options?.now ?? Math.floor(Date.now() / 1e3);
|
|
43
|
+
const signature = pickHeader(headers, "x-webhook-signature");
|
|
44
|
+
if (!signature) return { ok: false, reason: "missing-signature" };
|
|
45
|
+
const timestampStr = pickHeader(headers, "x-webhook-timestamp");
|
|
46
|
+
if (!timestampStr) return { ok: false, reason: "missing-timestamp" };
|
|
47
|
+
const timestamp = Number.parseInt(timestampStr, 10);
|
|
48
|
+
if (Number.isNaN(timestamp) || String(timestamp) !== timestampStr) {
|
|
49
|
+
return { ok: false, reason: "missing-timestamp" };
|
|
50
|
+
}
|
|
51
|
+
const delta = now - timestamp;
|
|
52
|
+
if (delta > maxAgeSeconds) return { ok: false, reason: "stale" };
|
|
53
|
+
if (delta < -maxAgeSeconds) return { ok: false, reason: "future" };
|
|
54
|
+
if (!signature.startsWith("sha256=")) {
|
|
55
|
+
return { ok: false, reason: "bad-signature" };
|
|
56
|
+
}
|
|
57
|
+
const providedHex = signature.slice("sha256=".length);
|
|
58
|
+
if (!/^[0-9a-fA-F]{64}$/.test(providedHex)) {
|
|
59
|
+
return { ok: false, reason: "bad-signature" };
|
|
60
|
+
}
|
|
61
|
+
const expectedHex = (0, import_node_crypto.createHmac)("sha256", secret).update(`${timestampStr}.${body}`).digest("hex");
|
|
62
|
+
const a = Buffer.from(providedHex, "hex");
|
|
63
|
+
const b = Buffer.from(expectedHex, "hex");
|
|
64
|
+
if (!(0, import_node_crypto.timingSafeEqual)(a, b)) {
|
|
65
|
+
return { ok: false, reason: "bad-signature" };
|
|
66
|
+
}
|
|
67
|
+
return { ok: true };
|
|
68
|
+
}
|
|
69
|
+
function pickHeader(headers, lowerName) {
|
|
70
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
71
|
+
if (name.toLowerCase() !== lowerName) continue;
|
|
72
|
+
if (Array.isArray(value) || value === void 0 || value === "") {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/receiver/check.ts
|
|
81
|
+
async function checkWebhook(headers, body, options) {
|
|
82
|
+
if (options.secret !== void 0) {
|
|
83
|
+
const verification = verifyWebhook(
|
|
84
|
+
headers,
|
|
85
|
+
body,
|
|
86
|
+
options.secret,
|
|
87
|
+
options.verify
|
|
88
|
+
);
|
|
89
|
+
if (!verification.ok) {
|
|
90
|
+
return { ok: false, status: 401, reason: verification.reason };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const key = extractIdempotencyKey(headers);
|
|
94
|
+
if (!key) return { ok: false, status: 400, reason: "missing-key" };
|
|
95
|
+
const claimed = await options.store.claim(key);
|
|
96
|
+
return { ok: true, key, deduped: !claimed };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/receiver/fastify/index.ts
|
|
100
|
+
function webhookMiddleware(options) {
|
|
101
|
+
return async function check(request, reply) {
|
|
102
|
+
const req = request;
|
|
103
|
+
const rawBody = req.rawBody ?? "";
|
|
104
|
+
const result = await checkWebhook(
|
|
105
|
+
req.headers,
|
|
106
|
+
rawBody,
|
|
107
|
+
options
|
|
108
|
+
);
|
|
109
|
+
if (!result.ok) {
|
|
110
|
+
await reply.status(result.status).send({ error: result.reason });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
req.idempotency = { key: result.key, deduped: result.deduped };
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
117
|
+
0 && (module.exports = {
|
|
118
|
+
webhookMiddleware
|
|
119
|
+
});
|
|
120
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/receiver/fastify/index.ts","../../../src/receiver/extract.ts","../../../src/receiver/verify.ts","../../../src/receiver/check.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/receiver/fastify\n *\n * Fastify adapter for the receiver-side webhook check.\n *\n * Usage:\n *\n * ```ts\n * import Fastify from \"fastify\";\n * import { webhookMiddleware } from \"@rotorsoft/act-http/receiver/fastify\";\n * import { InMemoryIdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n *\n * const app = Fastify();\n * const dedup = new InMemoryIdempotencyStore();\n *\n * app.post(\n * \"/webhooks/orders\",\n * {\n * preHandler: webhookMiddleware({\n * store: dedup,\n * secret: process.env.WEBHOOK_SECRET,\n * }),\n * },\n * async (request, reply) => {\n * const { key, deduped } = (request as any).idempotency;\n * if (deduped) return { status: \"dedup-skipped\", key };\n * // ... process the inbound event ...\n * return { status: \"processed\", key };\n * }\n * );\n * ```\n *\n * On failure: replies with `{ error: <reason> }` at status 400\n * (missing-key) or 401 (verification failures). On success: attaches\n * `request.idempotency = { key, deduped }` and lets the route handler\n * run.\n *\n * **Raw body requirement**: when `secret` is configured, register a\n * content-type parser that preserves the raw body string. Fastify's\n * default JSON parser eats the bytes — register a custom parser via\n * `app.addContentTypeParser(\"application/json\", { parseAs: \"string\" }, …)`\n * and stash the string on `request.rawBody` (Fastify pattern). The\n * middleware reads `request.rawBody` for hashing. Skip when unsigned.\n */\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\nimport { type CheckWebhookOptions, checkWebhook } from \"../check.js\";\n\ntype WebhookRequest = FastifyRequest & {\n rawBody?: string;\n idempotency?: { key: string; deduped: boolean };\n};\n\n/**\n * Build a Fastify `preHandler` hook that verifies the request\n * signature (when `secret` is set), enforces `Idempotency-Key`, and\n * claims the key on the configured store. See the module-level docs\n * for usage.\n */\nexport function webhookMiddleware(\n options: CheckWebhookOptions\n): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {\n return async function check(\n request: FastifyRequest,\n reply: FastifyReply\n ): Promise<void> {\n const req = request as WebhookRequest;\n const rawBody = req.rawBody ?? \"\";\n const result = await checkWebhook(\n req.headers as Record<string, string | string[] | undefined>,\n rawBody,\n options\n );\n if (!result.ok) {\n await reply.status(result.status).send({ error: result.reason });\n return;\n }\n req.idempotency = { key: result.key, deduped: result.deduped };\n };\n}\n","/**\n * Pull the `Idempotency-Key` header from a Node-style headers bag,\n * case-insensitive. Returns `undefined` when any of the following\n * carries no usable key:\n *\n * - the header is missing\n * - its value is an array (ambiguous — can't pick one without a\n * policy the receiver hasn't declared)\n * - its value is the empty string (carries no idempotency\n * information; structurally equivalent to \"no header at all\")\n *\n * Pair with `IdempotencyStore.claim` from\n * `@rotorsoft/act-ops/idempotency`: extract the key from the inbound\n * request, claim it on the store, return a `deduped` marker when the\n * claim fails. The framework-agnostic middleware that wires these\n * together lands in #744.\n *\n * Validation beyond \"is there a usable key?\" (length bounds, format\n * checks, normalization) is intentionally out of scope. Receivers\n * picking a policy can layer it on top — or, when #744 ships, opt\n * into the middleware's opinionated defaults.\n */\nexport function extractIdempotencyKey(\n headers: Record<string, string | string[] | undefined>\n): string | undefined {\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() !== \"idempotency-key\") continue;\n if (Array.isArray(value)) return undefined;\n if (value === \"\") return undefined;\n return value;\n }\n return undefined;\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Outcome of {@link verifyWebhook}. Either the request signature\n * checks out, or one of five distinct failure reasons applies. Each\n * reason maps to an operator-meaningful telemetry bucket — separated\n * deliberately so dashboards can distinguish \"client lost its secret\"\n * from \"client clock is wrong\" from \"this is a replay attack.\"\n */\nexport type VerifyResult =\n | { ok: true }\n | {\n ok: false;\n reason:\n | \"missing-signature\"\n | \"missing-timestamp\"\n | \"stale\"\n | \"future\"\n | \"bad-signature\";\n };\n\n/** Options for {@link verifyWebhook}. */\nexport type VerifyOptions = {\n /**\n * Maximum acceptable timestamp drift in either direction, in\n * seconds. Default: 300 (±5 minutes) — matches Stripe / GitHub /\n * Slack conventions. Tightening narrows the replay window;\n * loosening accommodates clients with worse clock sync.\n */\n maxAgeSeconds?: number;\n /**\n * Current Unix-seconds time. Exposed for tests; production\n * callers should leave it undefined so wall-clock is used.\n */\n now?: number;\n};\n\n/**\n * Verify an inbound webhook's signature and timestamp against the\n * shared secret. Pair with the sender side: configure\n * `webhook({ secret })` from `@rotorsoft/act-http/webhook`.\n *\n * Returns `{ ok: true }` on success or `{ ok: false; reason }` on\n * failure. The reasons are:\n *\n * - `missing-signature` — no `X-Webhook-Signature` header, value\n * was an array, or value was empty.\n * - `missing-timestamp` — no `X-Webhook-Timestamp` header, value\n * was empty, or value isn't a parseable integer.\n * - `stale` — timestamp older than `maxAgeSeconds` from `now`.\n * - `future` — timestamp more than `maxAgeSeconds` ahead of `now`.\n * - `bad-signature` — signature header didn't start with `sha256=`,\n * wasn't 64 hex chars, or the recomputed HMAC didn't match\n * (constant-time compare).\n *\n * The signed payload is `${timestamp}.${body}`, so `body` must be\n * the **raw request body bytes**. Any pre-parse normalization\n * (whitespace trimming, JSON re-stringification) would change the\n * hash and reject every otherwise-valid request. Framework adapters\n * in #744 will provide the raw body alongside the parsed one.\n *\n * Uses Node's `crypto.timingSafeEqual` for the final comparison to\n * avoid signature-equality timing attacks.\n */\nexport function verifyWebhook(\n headers: Record<string, string | string[] | undefined>,\n body: string,\n secret: string,\n options?: VerifyOptions\n): VerifyResult {\n const maxAgeSeconds = options?.maxAgeSeconds ?? 300;\n const now = options?.now ?? Math.floor(Date.now() / 1000);\n\n const signature = pickHeader(headers, \"x-webhook-signature\");\n if (!signature) return { ok: false, reason: \"missing-signature\" };\n\n const timestampStr = pickHeader(headers, \"x-webhook-timestamp\");\n if (!timestampStr) return { ok: false, reason: \"missing-timestamp\" };\n const timestamp = Number.parseInt(timestampStr, 10);\n if (Number.isNaN(timestamp) || String(timestamp) !== timestampStr) {\n return { ok: false, reason: \"missing-timestamp\" };\n }\n\n const delta = now - timestamp;\n if (delta > maxAgeSeconds) return { ok: false, reason: \"stale\" };\n if (delta < -maxAgeSeconds) return { ok: false, reason: \"future\" };\n\n if (!signature.startsWith(\"sha256=\")) {\n return { ok: false, reason: \"bad-signature\" };\n }\n const providedHex = signature.slice(\"sha256=\".length);\n if (!/^[0-9a-fA-F]{64}$/.test(providedHex)) {\n return { ok: false, reason: \"bad-signature\" };\n }\n\n const expectedHex = createHmac(\"sha256\", secret)\n .update(`${timestampStr}.${body}`)\n .digest(\"hex\");\n\n const a = Buffer.from(providedHex, \"hex\");\n const b = Buffer.from(expectedHex, \"hex\");\n if (!timingSafeEqual(a, b)) {\n return { ok: false, reason: \"bad-signature\" };\n }\n\n return { ok: true };\n}\n\nfunction pickHeader(\n headers: Record<string, string | string[] | undefined>,\n lowerName: string\n): string | undefined {\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() !== lowerName) continue;\n if (Array.isArray(value) || value === undefined || value === \"\") {\n return undefined;\n }\n return value;\n }\n return undefined;\n}\n","import type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\nimport { extractIdempotencyKey } from \"./extract.js\";\nimport { type VerifyOptions, verifyWebhook } from \"./verify.js\";\n\n/**\n * Failure reasons returned by {@link checkWebhook}. The shape splits\n * `missing-key` (a client error, mapped to HTTP 400) from the five\n * verification failures (authentication errors, HTTP 401) so each\n * maps to its own telemetry bucket.\n */\nexport type CheckFailureReason =\n | \"missing-key\"\n | \"missing-signature\"\n | \"missing-timestamp\"\n | \"stale\"\n | \"future\"\n | \"bad-signature\";\n\n/**\n * Outcome of {@link checkWebhook}. Either the request passed every\n * configured check and carries a usable idempotency key, or it\n * failed one of them and the framework adapter should reply with the\n * corresponding HTTP status.\n */\nexport type CheckResult =\n | { ok: false; status: 400 | 401; reason: CheckFailureReason }\n | { ok: true; key: string; deduped: boolean };\n\n/** Options for {@link checkWebhook}. */\nexport type CheckWebhookOptions = {\n /** Idempotency store the framework-agnostic core claims the key on. */\n store: IdempotencyStore;\n /**\n * Optional HMAC-SHA256 secret. When set, the request's\n * `X-Webhook-Signature` and `X-Webhook-Timestamp` headers are\n * verified before the dedup claim. When omitted, signature\n * verification is skipped (unsigned receivers).\n */\n secret?: string;\n /**\n * Verification options forwarded to {@link verifyWebhook}. Only\n * meaningful when `secret` is set. Defaults to a ±300-second\n * timestamp window.\n */\n verify?: VerifyOptions;\n};\n\n/**\n * Framework-agnostic receiver check: verify the signature (when a\n * secret is configured), extract the `Idempotency-Key`, and claim\n * it on the store. Returns the request's fate as a discriminated\n * union the per-framework adapter translates into the framework's\n * idiomatic 4xx response or context injection.\n *\n * **Order of checks** (matters):\n *\n * 1. Verify signature + timestamp window (when `secret` is set).\n * Rejecting bad signatures *before* extracting and claiming the\n * key keeps attacker-supplied keys out of the dedup store —\n * otherwise a flood of spoofed POSTs would pollute the LRU.\n * 2. Extract the `Idempotency-Key`. Missing → reject with 400.\n * 3. Claim the key on the store. If already seen, return\n * `{ ok: true; deduped: true }` so the framework adapter can\n * short-circuit the handler without re-running side effects.\n *\n * The dedup store may be sync (`InMemoryIdempotencyStore`) or async\n * (durable adapters like a future `PostgresIdempotencyStore`); the\n * core awaits unconditionally so both shapes compose cleanly.\n */\nexport async function checkWebhook(\n headers: Record<string, string | string[] | undefined>,\n body: string,\n options: CheckWebhookOptions\n): Promise<CheckResult> {\n if (options.secret !== undefined) {\n const verification = verifyWebhook(\n headers,\n body,\n options.secret,\n options.verify\n );\n if (!verification.ok) {\n return { ok: false, status: 401, reason: verification.reason };\n }\n }\n\n const key = extractIdempotencyKey(headers);\n if (!key) return { ok: false, status: 400, reason: \"missing-key\" };\n\n const claimed = await options.store.claim(key);\n return { ok: true, key, deduped: !claimed };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkWebhook
|
|
3
|
+
} from "../../chunk-NOIXOF2I.js";
|
|
4
|
+
|
|
5
|
+
// src/receiver/fastify/index.ts
|
|
6
|
+
function webhookMiddleware(options) {
|
|
7
|
+
return async function check(request, reply) {
|
|
8
|
+
const req = request;
|
|
9
|
+
const rawBody = req.rawBody ?? "";
|
|
10
|
+
const result = await checkWebhook(
|
|
11
|
+
req.headers,
|
|
12
|
+
rawBody,
|
|
13
|
+
options
|
|
14
|
+
);
|
|
15
|
+
if (!result.ok) {
|
|
16
|
+
await reply.status(result.status).send({ error: result.reason });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
req.idempotency = { key: result.key, deduped: result.deduped };
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
webhookMiddleware
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/receiver/fastify/index.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/receiver/fastify\n *\n * Fastify adapter for the receiver-side webhook check.\n *\n * Usage:\n *\n * ```ts\n * import Fastify from \"fastify\";\n * import { webhookMiddleware } from \"@rotorsoft/act-http/receiver/fastify\";\n * import { InMemoryIdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n *\n * const app = Fastify();\n * const dedup = new InMemoryIdempotencyStore();\n *\n * app.post(\n * \"/webhooks/orders\",\n * {\n * preHandler: webhookMiddleware({\n * store: dedup,\n * secret: process.env.WEBHOOK_SECRET,\n * }),\n * },\n * async (request, reply) => {\n * const { key, deduped } = (request as any).idempotency;\n * if (deduped) return { status: \"dedup-skipped\", key };\n * // ... process the inbound event ...\n * return { status: \"processed\", key };\n * }\n * );\n * ```\n *\n * On failure: replies with `{ error: <reason> }` at status 400\n * (missing-key) or 401 (verification failures). On success: attaches\n * `request.idempotency = { key, deduped }` and lets the route handler\n * run.\n *\n * **Raw body requirement**: when `secret` is configured, register a\n * content-type parser that preserves the raw body string. Fastify's\n * default JSON parser eats the bytes — register a custom parser via\n * `app.addContentTypeParser(\"application/json\", { parseAs: \"string\" }, …)`\n * and stash the string on `request.rawBody` (Fastify pattern). The\n * middleware reads `request.rawBody` for hashing. Skip when unsigned.\n */\nimport type { FastifyReply, FastifyRequest } from \"fastify\";\nimport { type CheckWebhookOptions, checkWebhook } from \"../check.js\";\n\ntype WebhookRequest = FastifyRequest & {\n rawBody?: string;\n idempotency?: { key: string; deduped: boolean };\n};\n\n/**\n * Build a Fastify `preHandler` hook that verifies the request\n * signature (when `secret` is set), enforces `Idempotency-Key`, and\n * claims the key on the configured store. See the module-level docs\n * for usage.\n */\nexport function webhookMiddleware(\n options: CheckWebhookOptions\n): (request: FastifyRequest, reply: FastifyReply) => Promise<void> {\n return async function check(\n request: FastifyRequest,\n reply: FastifyReply\n ): Promise<void> {\n const req = request as WebhookRequest;\n const rawBody = req.rawBody ?? \"\";\n const result = await checkWebhook(\n req.headers as Record<string, string | string[] | undefined>,\n rawBody,\n options\n );\n if (!result.ok) {\n await reply.status(result.status).send({ error: result.reason });\n return;\n }\n req.idempotency = { key: result.key, deduped: result.deduped };\n };\n}\n"],"mappings":";;;;;AA2DO,SAAS,kBACd,SACiE;AACjE,SAAO,eAAe,MACpB,SACA,OACe;AACf,UAAM,MAAM;AACZ,UAAM,UAAU,IAAI,WAAW;AAC/B,UAAM,SAAS,MAAM;AAAA,MACnB,IAAI;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AACA,QAAI,CAAC,OAAO,IAAI;AACd,YAAM,MAAM,OAAO,OAAO,MAAM,EAAE,KAAK,EAAE,OAAO,OAAO,OAAO,CAAC;AAC/D;AAAA,IACF;AACA,QAAI,cAAc,EAAE,KAAK,OAAO,KAAK,SAAS,OAAO,QAAQ;AAAA,EAC/D;AACF;","names":[]}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/receiver/hono/index.ts
|
|
21
|
+
var hono_exports = {};
|
|
22
|
+
__export(hono_exports, {
|
|
23
|
+
webhookMiddleware: () => webhookMiddleware
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(hono_exports);
|
|
26
|
+
|
|
27
|
+
// src/receiver/extract.ts
|
|
28
|
+
function extractIdempotencyKey(headers) {
|
|
29
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
30
|
+
if (name.toLowerCase() !== "idempotency-key") continue;
|
|
31
|
+
if (Array.isArray(value)) return void 0;
|
|
32
|
+
if (value === "") return void 0;
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/receiver/verify.ts
|
|
39
|
+
var import_node_crypto = require("crypto");
|
|
40
|
+
function verifyWebhook(headers, body, secret, options) {
|
|
41
|
+
const maxAgeSeconds = options?.maxAgeSeconds ?? 300;
|
|
42
|
+
const now = options?.now ?? Math.floor(Date.now() / 1e3);
|
|
43
|
+
const signature = pickHeader(headers, "x-webhook-signature");
|
|
44
|
+
if (!signature) return { ok: false, reason: "missing-signature" };
|
|
45
|
+
const timestampStr = pickHeader(headers, "x-webhook-timestamp");
|
|
46
|
+
if (!timestampStr) return { ok: false, reason: "missing-timestamp" };
|
|
47
|
+
const timestamp = Number.parseInt(timestampStr, 10);
|
|
48
|
+
if (Number.isNaN(timestamp) || String(timestamp) !== timestampStr) {
|
|
49
|
+
return { ok: false, reason: "missing-timestamp" };
|
|
50
|
+
}
|
|
51
|
+
const delta = now - timestamp;
|
|
52
|
+
if (delta > maxAgeSeconds) return { ok: false, reason: "stale" };
|
|
53
|
+
if (delta < -maxAgeSeconds) return { ok: false, reason: "future" };
|
|
54
|
+
if (!signature.startsWith("sha256=")) {
|
|
55
|
+
return { ok: false, reason: "bad-signature" };
|
|
56
|
+
}
|
|
57
|
+
const providedHex = signature.slice("sha256=".length);
|
|
58
|
+
if (!/^[0-9a-fA-F]{64}$/.test(providedHex)) {
|
|
59
|
+
return { ok: false, reason: "bad-signature" };
|
|
60
|
+
}
|
|
61
|
+
const expectedHex = (0, import_node_crypto.createHmac)("sha256", secret).update(`${timestampStr}.${body}`).digest("hex");
|
|
62
|
+
const a = Buffer.from(providedHex, "hex");
|
|
63
|
+
const b = Buffer.from(expectedHex, "hex");
|
|
64
|
+
if (!(0, import_node_crypto.timingSafeEqual)(a, b)) {
|
|
65
|
+
return { ok: false, reason: "bad-signature" };
|
|
66
|
+
}
|
|
67
|
+
return { ok: true };
|
|
68
|
+
}
|
|
69
|
+
function pickHeader(headers, lowerName) {
|
|
70
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
71
|
+
if (name.toLowerCase() !== lowerName) continue;
|
|
72
|
+
if (Array.isArray(value) || value === void 0 || value === "") {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/receiver/check.ts
|
|
81
|
+
async function checkWebhook(headers, body, options) {
|
|
82
|
+
if (options.secret !== void 0) {
|
|
83
|
+
const verification = verifyWebhook(
|
|
84
|
+
headers,
|
|
85
|
+
body,
|
|
86
|
+
options.secret,
|
|
87
|
+
options.verify
|
|
88
|
+
);
|
|
89
|
+
if (!verification.ok) {
|
|
90
|
+
return { ok: false, status: 401, reason: verification.reason };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const key = extractIdempotencyKey(headers);
|
|
94
|
+
if (!key) return { ok: false, status: 400, reason: "missing-key" };
|
|
95
|
+
const claimed = await options.store.claim(key);
|
|
96
|
+
return { ok: true, key, deduped: !claimed };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/receiver/hono/index.ts
|
|
100
|
+
function webhookMiddleware(options) {
|
|
101
|
+
return async function check(c, next) {
|
|
102
|
+
const headers = headersBag(c.req.raw.headers);
|
|
103
|
+
const rawBody = await c.req.text();
|
|
104
|
+
const result = await checkWebhook(headers, rawBody, options);
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
return c.json({ error: result.reason }, result.status);
|
|
107
|
+
}
|
|
108
|
+
c.set("idempotency", { key: result.key, deduped: result.deduped });
|
|
109
|
+
await next();
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function headersBag(headers) {
|
|
113
|
+
const out = {};
|
|
114
|
+
headers.forEach((value, key) => {
|
|
115
|
+
out[key] = value;
|
|
116
|
+
});
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
120
|
+
0 && (module.exports = {
|
|
121
|
+
webhookMiddleware
|
|
122
|
+
});
|
|
123
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/receiver/hono/index.ts","../../../src/receiver/extract.ts","../../../src/receiver/verify.ts","../../../src/receiver/check.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/receiver/hono\n *\n * Hono adapter for the receiver-side webhook check.\n *\n * Usage:\n *\n * ```ts\n * import { Hono } from \"hono\";\n * import { webhookMiddleware } from \"@rotorsoft/act-http/receiver/hono\";\n * import { InMemoryIdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n *\n * const app = new Hono();\n * const dedup = new InMemoryIdempotencyStore();\n *\n * app.post(\n * \"/webhooks/orders\",\n * webhookMiddleware({ store: dedup, secret: process.env.WEBHOOK_SECRET }),\n * async (c) => {\n * const idem = c.get(\"idempotency\") as { key: string; deduped: boolean };\n * if (idem.deduped) return c.json({ status: \"dedup-skipped\", key: idem.key });\n * // ... process the inbound event ...\n * return c.json({ status: \"processed\", key: idem.key });\n * }\n * );\n * ```\n *\n * On failure: returns `c.json({ error: <reason> }, status)` directly\n * (Hono short-circuits when middleware returns a Response). On\n * success: stashes `c.set(\"idempotency\", { key, deduped })` and\n * continues with `await next()`.\n *\n * **Raw body**: Hono exposes `await c.req.text()` natively, which\n * the middleware reads when `secret` is configured. No extra setup\n * needed.\n */\nimport type { MiddlewareHandler } from \"hono\";\nimport { type CheckWebhookOptions, checkWebhook } from \"../check.js\";\n\n/**\n * Variables this middleware contributes to the Hono context. The\n * generic on the returned {@link MiddlewareHandler} threads it\n * through so route handlers downstream of `app.post(..., webhookMiddleware(...), handler)`\n * see `c.get(\"idempotency\")` typed without a manual cast.\n */\nexport type WebhookVariables = {\n idempotency: { key: string; deduped: boolean };\n};\n\n/**\n * Build a Hono middleware that verifies the request signature (when\n * `secret` is set), enforces `Idempotency-Key`, and claims the key\n * on the configured store. See the module-level docs for usage.\n */\nexport function webhookMiddleware(\n options: CheckWebhookOptions\n): MiddlewareHandler<{ Variables: WebhookVariables }> {\n return async function check(c, next) {\n const headers = headersBag(c.req.raw.headers);\n const rawBody = await c.req.text();\n const result = await checkWebhook(headers, rawBody, options);\n if (!result.ok) {\n return c.json({ error: result.reason }, result.status);\n }\n c.set(\"idempotency\", { key: result.key, deduped: result.deduped });\n await next();\n };\n}\n\nfunction headersBag(\n headers: Headers\n): Record<string, string | string[] | undefined> {\n const out: Record<string, string | string[] | undefined> = {};\n headers.forEach((value, key) => {\n out[key] = value;\n });\n return out;\n}\n","/**\n * Pull the `Idempotency-Key` header from a Node-style headers bag,\n * case-insensitive. Returns `undefined` when any of the following\n * carries no usable key:\n *\n * - the header is missing\n * - its value is an array (ambiguous — can't pick one without a\n * policy the receiver hasn't declared)\n * - its value is the empty string (carries no idempotency\n * information; structurally equivalent to \"no header at all\")\n *\n * Pair with `IdempotencyStore.claim` from\n * `@rotorsoft/act-ops/idempotency`: extract the key from the inbound\n * request, claim it on the store, return a `deduped` marker when the\n * claim fails. The framework-agnostic middleware that wires these\n * together lands in #744.\n *\n * Validation beyond \"is there a usable key?\" (length bounds, format\n * checks, normalization) is intentionally out of scope. Receivers\n * picking a policy can layer it on top — or, when #744 ships, opt\n * into the middleware's opinionated defaults.\n */\nexport function extractIdempotencyKey(\n headers: Record<string, string | string[] | undefined>\n): string | undefined {\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() !== \"idempotency-key\") continue;\n if (Array.isArray(value)) return undefined;\n if (value === \"\") return undefined;\n return value;\n }\n return undefined;\n}\n","import { createHmac, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Outcome of {@link verifyWebhook}. Either the request signature\n * checks out, or one of five distinct failure reasons applies. Each\n * reason maps to an operator-meaningful telemetry bucket — separated\n * deliberately so dashboards can distinguish \"client lost its secret\"\n * from \"client clock is wrong\" from \"this is a replay attack.\"\n */\nexport type VerifyResult =\n | { ok: true }\n | {\n ok: false;\n reason:\n | \"missing-signature\"\n | \"missing-timestamp\"\n | \"stale\"\n | \"future\"\n | \"bad-signature\";\n };\n\n/** Options for {@link verifyWebhook}. */\nexport type VerifyOptions = {\n /**\n * Maximum acceptable timestamp drift in either direction, in\n * seconds. Default: 300 (±5 minutes) — matches Stripe / GitHub /\n * Slack conventions. Tightening narrows the replay window;\n * loosening accommodates clients with worse clock sync.\n */\n maxAgeSeconds?: number;\n /**\n * Current Unix-seconds time. Exposed for tests; production\n * callers should leave it undefined so wall-clock is used.\n */\n now?: number;\n};\n\n/**\n * Verify an inbound webhook's signature and timestamp against the\n * shared secret. Pair with the sender side: configure\n * `webhook({ secret })` from `@rotorsoft/act-http/webhook`.\n *\n * Returns `{ ok: true }` on success or `{ ok: false; reason }` on\n * failure. The reasons are:\n *\n * - `missing-signature` — no `X-Webhook-Signature` header, value\n * was an array, or value was empty.\n * - `missing-timestamp` — no `X-Webhook-Timestamp` header, value\n * was empty, or value isn't a parseable integer.\n * - `stale` — timestamp older than `maxAgeSeconds` from `now`.\n * - `future` — timestamp more than `maxAgeSeconds` ahead of `now`.\n * - `bad-signature` — signature header didn't start with `sha256=`,\n * wasn't 64 hex chars, or the recomputed HMAC didn't match\n * (constant-time compare).\n *\n * The signed payload is `${timestamp}.${body}`, so `body` must be\n * the **raw request body bytes**. Any pre-parse normalization\n * (whitespace trimming, JSON re-stringification) would change the\n * hash and reject every otherwise-valid request. Framework adapters\n * in #744 will provide the raw body alongside the parsed one.\n *\n * Uses Node's `crypto.timingSafeEqual` for the final comparison to\n * avoid signature-equality timing attacks.\n */\nexport function verifyWebhook(\n headers: Record<string, string | string[] | undefined>,\n body: string,\n secret: string,\n options?: VerifyOptions\n): VerifyResult {\n const maxAgeSeconds = options?.maxAgeSeconds ?? 300;\n const now = options?.now ?? Math.floor(Date.now() / 1000);\n\n const signature = pickHeader(headers, \"x-webhook-signature\");\n if (!signature) return { ok: false, reason: \"missing-signature\" };\n\n const timestampStr = pickHeader(headers, \"x-webhook-timestamp\");\n if (!timestampStr) return { ok: false, reason: \"missing-timestamp\" };\n const timestamp = Number.parseInt(timestampStr, 10);\n if (Number.isNaN(timestamp) || String(timestamp) !== timestampStr) {\n return { ok: false, reason: \"missing-timestamp\" };\n }\n\n const delta = now - timestamp;\n if (delta > maxAgeSeconds) return { ok: false, reason: \"stale\" };\n if (delta < -maxAgeSeconds) return { ok: false, reason: \"future\" };\n\n if (!signature.startsWith(\"sha256=\")) {\n return { ok: false, reason: \"bad-signature\" };\n }\n const providedHex = signature.slice(\"sha256=\".length);\n if (!/^[0-9a-fA-F]{64}$/.test(providedHex)) {\n return { ok: false, reason: \"bad-signature\" };\n }\n\n const expectedHex = createHmac(\"sha256\", secret)\n .update(`${timestampStr}.${body}`)\n .digest(\"hex\");\n\n const a = Buffer.from(providedHex, \"hex\");\n const b = Buffer.from(expectedHex, \"hex\");\n if (!timingSafeEqual(a, b)) {\n return { ok: false, reason: \"bad-signature\" };\n }\n\n return { ok: true };\n}\n\nfunction pickHeader(\n headers: Record<string, string | string[] | undefined>,\n lowerName: string\n): string | undefined {\n for (const [name, value] of Object.entries(headers)) {\n if (name.toLowerCase() !== lowerName) continue;\n if (Array.isArray(value) || value === undefined || value === \"\") {\n return undefined;\n }\n return value;\n }\n return undefined;\n}\n","import type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\nimport { extractIdempotencyKey } from \"./extract.js\";\nimport { type VerifyOptions, verifyWebhook } from \"./verify.js\";\n\n/**\n * Failure reasons returned by {@link checkWebhook}. The shape splits\n * `missing-key` (a client error, mapped to HTTP 400) from the five\n * verification failures (authentication errors, HTTP 401) so each\n * maps to its own telemetry bucket.\n */\nexport type CheckFailureReason =\n | \"missing-key\"\n | \"missing-signature\"\n | \"missing-timestamp\"\n | \"stale\"\n | \"future\"\n | \"bad-signature\";\n\n/**\n * Outcome of {@link checkWebhook}. Either the request passed every\n * configured check and carries a usable idempotency key, or it\n * failed one of them and the framework adapter should reply with the\n * corresponding HTTP status.\n */\nexport type CheckResult =\n | { ok: false; status: 400 | 401; reason: CheckFailureReason }\n | { ok: true; key: string; deduped: boolean };\n\n/** Options for {@link checkWebhook}. */\nexport type CheckWebhookOptions = {\n /** Idempotency store the framework-agnostic core claims the key on. */\n store: IdempotencyStore;\n /**\n * Optional HMAC-SHA256 secret. When set, the request's\n * `X-Webhook-Signature` and `X-Webhook-Timestamp` headers are\n * verified before the dedup claim. When omitted, signature\n * verification is skipped (unsigned receivers).\n */\n secret?: string;\n /**\n * Verification options forwarded to {@link verifyWebhook}. Only\n * meaningful when `secret` is set. Defaults to a ±300-second\n * timestamp window.\n */\n verify?: VerifyOptions;\n};\n\n/**\n * Framework-agnostic receiver check: verify the signature (when a\n * secret is configured), extract the `Idempotency-Key`, and claim\n * it on the store. Returns the request's fate as a discriminated\n * union the per-framework adapter translates into the framework's\n * idiomatic 4xx response or context injection.\n *\n * **Order of checks** (matters):\n *\n * 1. Verify signature + timestamp window (when `secret` is set).\n * Rejecting bad signatures *before* extracting and claiming the\n * key keeps attacker-supplied keys out of the dedup store —\n * otherwise a flood of spoofed POSTs would pollute the LRU.\n * 2. Extract the `Idempotency-Key`. Missing → reject with 400.\n * 3. Claim the key on the store. If already seen, return\n * `{ ok: true; deduped: true }` so the framework adapter can\n * short-circuit the handler without re-running side effects.\n *\n * The dedup store may be sync (`InMemoryIdempotencyStore`) or async\n * (durable adapters like a future `PostgresIdempotencyStore`); the\n * core awaits unconditionally so both shapes compose cleanly.\n */\nexport async function checkWebhook(\n headers: Record<string, string | string[] | undefined>,\n body: string,\n options: CheckWebhookOptions\n): Promise<CheckResult> {\n if (options.secret !== undefined) {\n const verification = verifyWebhook(\n headers,\n body,\n options.secret,\n options.verify\n );\n if (!verification.ok) {\n return { ok: false, status: 401, reason: verification.reason };\n }\n }\n\n const key = extractIdempotencyKey(headers);\n if (!key) return { ok: false, status: 400, reason: \"missing-key\" };\n\n const claimed = await options.store.claim(key);\n return { ok: true, key, deduped: !claimed };\n}\n"],"mappings":"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","names":[]}
|