@geekmidas/constructs 10.0.0-alpha.123 → 10.0.0-alpha.125

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.
Files changed (183) hide show
  1. package/dist/{Construct-CFW0zjdD.d.cts → Construct-127M7yrl.d.cts} +3 -2
  2. package/dist/{Construct-B7AXn0xH.d.mts.map → Construct-127M7yrl.d.cts.map} +1 -1
  3. package/dist/{Construct-CKUA-CTA.mjs → Construct-CGuc_Ijk.mjs} +2 -1
  4. package/dist/{Construct-CKUA-CTA.mjs.map → Construct-CGuc_Ijk.mjs.map} +1 -1
  5. package/dist/{Construct-DEOVauyA.cjs → Construct-DorKS0Ua.cjs} +2 -1
  6. package/dist/{Construct-DEOVauyA.cjs.map → Construct-DorKS0Ua.cjs.map} +1 -1
  7. package/dist/{Construct-B7AXn0xH.d.mts → Construct-DySlwh8o.d.mts} +3 -2
  8. package/dist/{Construct-CFW0zjdD.d.cts.map → Construct-DySlwh8o.d.mts.map} +1 -1
  9. package/dist/{Cron-bdGRSROY.d.cts → Cron-CFkpEys6.d.cts} +4 -4
  10. package/dist/{Cron-bdGRSROY.d.cts.map → Cron-CFkpEys6.d.cts.map} +1 -1
  11. package/dist/{Cron-i4Uetezh.d.mts → Cron-CQoDSu-n.d.mts} +4 -4
  12. package/dist/{Cron-i4Uetezh.d.mts.map → Cron-CQoDSu-n.d.mts.map} +1 -1
  13. package/dist/{CronBuilder-9H4BnD6W.d.mts → CronBuilder-1W-G_i15.d.mts} +5 -5
  14. package/dist/{CronBuilder-9H4BnD6W.d.mts.map → CronBuilder-1W-G_i15.d.mts.map} +1 -1
  15. package/dist/{CronBuilder-D3sF9tS_.mjs → CronBuilder-BJ5LoadF.mjs} +3 -3
  16. package/dist/{CronBuilder-D3sF9tS_.mjs.map → CronBuilder-BJ5LoadF.mjs.map} +1 -1
  17. package/dist/{CronBuilder-BXTG10u5.cjs → CronBuilder-B_NSP-Y5.cjs} +3 -3
  18. package/dist/{CronBuilder-BXTG10u5.cjs.map → CronBuilder-B_NSP-Y5.cjs.map} +1 -1
  19. package/dist/{CronBuilder-B_24LZpn.d.cts → CronBuilder-OaCRRFVk.d.cts} +5 -5
  20. package/dist/{CronBuilder-B_24LZpn.d.cts.map → CronBuilder-OaCRRFVk.d.cts.map} +1 -1
  21. package/dist/{Endpoint-DgrpG15P.d.cts → Endpoint-BEh-jW-f.d.cts} +4 -4
  22. package/dist/{Endpoint-CeFkSwh5.d.mts.map → Endpoint-BEh-jW-f.d.cts.map} +1 -1
  23. package/dist/{Endpoint-yhR2T2Wt.cjs → Endpoint-BuGFNFSB.cjs} +3 -3
  24. package/dist/{Endpoint-yhR2T2Wt.cjs.map → Endpoint-BuGFNFSB.cjs.map} +1 -1
  25. package/dist/{Endpoint-CeFkSwh5.d.mts → Endpoint-DTXDggNl.d.mts} +4 -4
  26. package/dist/{Endpoint-DgrpG15P.d.cts.map → Endpoint-DTXDggNl.d.mts.map} +1 -1
  27. package/dist/{Endpoint-gPXhHKhJ.mjs → Endpoint-VdaYKc2g.mjs} +3 -3
  28. package/dist/{Endpoint-gPXhHKhJ.mjs.map → Endpoint-VdaYKc2g.mjs.map} +1 -1
  29. package/dist/{EndpointFactory-Cc863XJS.d.cts → EndpointFactory-B2dCS9dy.d.cts} +4 -4
  30. package/dist/{EndpointFactory-Dbw9ugkX.d.mts.map → EndpointFactory-B2dCS9dy.d.cts.map} +1 -1
  31. package/dist/{EndpointFactory-CSrAyazj.cjs → EndpointFactory-C6pVda4y.cjs} +4 -4
  32. package/dist/{EndpointFactory-CSrAyazj.cjs.map → EndpointFactory-C6pVda4y.cjs.map} +1 -1
  33. package/dist/{EndpointFactory-c2ZugGig.mjs → EndpointFactory-CUpqgtaH.mjs} +4 -4
  34. package/dist/{EndpointFactory-c2ZugGig.mjs.map → EndpointFactory-CUpqgtaH.mjs.map} +1 -1
  35. package/dist/{EndpointFactory-Dbw9ugkX.d.mts → EndpointFactory-D2HXcUrJ.d.mts} +4 -4
  36. package/dist/{EndpointFactory-Cc863XJS.d.cts.map → EndpointFactory-D2HXcUrJ.d.mts.map} +1 -1
  37. package/dist/{Function-B0uZhZnU.d.cts → Function-BIEsY0Hm.d.cts} +2 -2
  38. package/dist/{Function-B0uZhZnU.d.cts.map → Function-BIEsY0Hm.d.cts.map} +1 -1
  39. package/dist/{Function-B_9R7Zy1.d.mts → Function-DfMzTGS4.d.mts} +2 -2
  40. package/dist/{Function-B_9R7Zy1.d.mts.map → Function-DfMzTGS4.d.mts.map} +1 -1
  41. package/dist/{HonoEndpointAdaptor-CN1jjUBJ.cjs → HonoEndpointAdaptor-BBTzbP38.cjs} +3 -3
  42. package/dist/{HonoEndpointAdaptor-CN1jjUBJ.cjs.map → HonoEndpointAdaptor-BBTzbP38.cjs.map} +1 -1
  43. package/dist/{HonoEndpointAdaptor-euH1oSdY.mjs → HonoEndpointAdaptor-BR_LyaFE.mjs} +3 -3
  44. package/dist/{HonoEndpointAdaptor-euH1oSdY.mjs.map → HonoEndpointAdaptor-BR_LyaFE.mjs.map} +1 -1
  45. package/dist/ObjectSubscriberBuilder-HmBwFn2o.d.mts +129 -0
  46. package/dist/ObjectSubscriberBuilder-HmBwFn2o.d.mts.map +1 -0
  47. package/dist/ObjectSubscriberBuilder-l-64yUiF.d.cts +129 -0
  48. package/dist/ObjectSubscriberBuilder-l-64yUiF.d.cts.map +1 -0
  49. package/dist/ProcessorBuilder-B2_stA8f.d.mts +216 -0
  50. package/dist/ProcessorBuilder-B2_stA8f.d.mts.map +1 -0
  51. package/dist/ProcessorBuilder-C62bIOwj.d.cts +216 -0
  52. package/dist/ProcessorBuilder-C62bIOwj.d.cts.map +1 -0
  53. package/dist/{Queue-B-dDXyjF.d.mts → Queue-BnK3UH8r.d.mts} +3 -3
  54. package/dist/{Queue-B-dDXyjF.d.mts.map → Queue-BnK3UH8r.d.mts.map} +1 -1
  55. package/dist/{Queue-JbIjVtTf.d.cts → Queue-C4rijiM7.d.cts} +3 -3
  56. package/dist/{Queue-JbIjVtTf.d.cts.map → Queue-C4rijiM7.d.cts.map} +1 -1
  57. package/dist/{Queue-BhuofM3k.cjs → Queue-D4Iaidrs.cjs} +3 -3
  58. package/dist/{Queue-BhuofM3k.cjs.map → Queue-D4Iaidrs.cjs.map} +1 -1
  59. package/dist/{Queue-cmhqEBOB.mjs → Queue-u8w8_HpE.mjs} +3 -3
  60. package/dist/{Queue-cmhqEBOB.mjs.map → Queue-u8w8_HpE.mjs.map} +1 -1
  61. package/dist/{QueueBuilder-DXu1qxT1.mjs → QueueBuilder-2wP5lEC8.mjs} +2 -2
  62. package/dist/{QueueBuilder-DXu1qxT1.mjs.map → QueueBuilder-2wP5lEC8.mjs.map} +1 -1
  63. package/dist/{QueueBuilder-48W-q8In.d.mts → QueueBuilder-B3vL1ED2.d.mts} +2 -2
  64. package/dist/{QueueBuilder-48W-q8In.d.mts.map → QueueBuilder-B3vL1ED2.d.mts.map} +1 -1
  65. package/dist/{QueueBuilder-MfpkJ3R9.d.cts → QueueBuilder-BcPBQSSX.d.cts} +2 -2
  66. package/dist/{QueueBuilder-MfpkJ3R9.d.cts.map → QueueBuilder-BcPBQSSX.d.cts.map} +1 -1
  67. package/dist/{QueueBuilder-DdU2klPu.cjs → QueueBuilder-BxsZ9o5z.cjs} +2 -2
  68. package/dist/{QueueBuilder-DdU2klPu.cjs.map → QueueBuilder-BxsZ9o5z.cjs.map} +1 -1
  69. package/dist/{Subscriber-CEt7kHkc.d.cts → Subscriber-BdCtfFXL.d.cts} +15 -3
  70. package/dist/{Subscriber-CEt7kHkc.d.cts.map → Subscriber-BdCtfFXL.d.cts.map} +1 -1
  71. package/dist/{Subscriber-dsxc5LKa.d.mts → Subscriber-DDo4HFNn.d.mts} +15 -3
  72. package/dist/{Subscriber-dsxc5LKa.d.mts.map → Subscriber-DDo4HFNn.d.mts.map} +1 -1
  73. package/dist/{SubscriberBuilder-DRXfxWn_.mjs → SubscriberBuilder-3msj1U2w.mjs} +99 -3
  74. package/dist/SubscriberBuilder-3msj1U2w.mjs.map +1 -0
  75. package/dist/{SubscriberBuilder-M2EAgQhC.cjs → SubscriberBuilder-CQst6Iz3.cjs} +110 -2
  76. package/dist/SubscriberBuilder-CQst6Iz3.cjs.map +1 -0
  77. package/dist/{Topic-D2R0r24C.mjs → Topic-DN2gYJ-p.mjs} +3 -3
  78. package/dist/{Topic-D2R0r24C.mjs.map → Topic-DN2gYJ-p.mjs.map} +1 -1
  79. package/dist/{Topic-DwS3QO0m.cjs → Topic-Dp9Q7Lj2.cjs} +3 -3
  80. package/dist/{Topic-DwS3QO0m.cjs.map → Topic-Dp9Q7Lj2.cjs.map} +1 -1
  81. package/dist/adaptors/aws.cjs +4 -4
  82. package/dist/adaptors/aws.cjs.map +1 -1
  83. package/dist/adaptors/aws.d.cts +5 -5
  84. package/dist/adaptors/aws.d.mts +5 -5
  85. package/dist/adaptors/aws.mjs +5 -5
  86. package/dist/adaptors/aws.mjs.map +1 -1
  87. package/dist/adaptors/hono.cjs +1 -1
  88. package/dist/adaptors/hono.d.cts +1 -1
  89. package/dist/adaptors/hono.d.mts +1 -1
  90. package/dist/adaptors/hono.mjs +1 -1
  91. package/dist/adaptors/testing.cjs +114 -7
  92. package/dist/adaptors/testing.cjs.map +1 -1
  93. package/dist/adaptors/testing.d.cts +25 -4
  94. package/dist/adaptors/testing.d.cts.map +1 -1
  95. package/dist/adaptors/testing.d.mts +25 -4
  96. package/dist/adaptors/testing.d.mts.map +1 -1
  97. package/dist/adaptors/testing.mjs +114 -7
  98. package/dist/adaptors/testing.mjs.map +1 -1
  99. package/dist/crons/index.cjs +2 -2
  100. package/dist/crons/index.d.cts +2 -2
  101. package/dist/crons/index.d.mts +2 -2
  102. package/dist/crons/index.mjs +2 -2
  103. package/dist/endpoints/index.cjs +3 -3
  104. package/dist/endpoints/index.d.cts +3 -3
  105. package/dist/endpoints/index.d.mts +3 -3
  106. package/dist/endpoints/index.mjs +3 -3
  107. package/dist/file-server.cjs +101 -31
  108. package/dist/file-server.cjs.map +1 -1
  109. package/dist/file-server.d.cts +95 -0
  110. package/dist/file-server.d.cts.map +1 -1
  111. package/dist/file-server.d.mts +95 -0
  112. package/dist/file-server.d.mts.map +1 -1
  113. package/dist/file-server.mjs +101 -33
  114. package/dist/file-server.mjs.map +1 -1
  115. package/dist/functions/index.cjs +1 -1
  116. package/dist/functions/index.d.cts +2 -2
  117. package/dist/functions/index.d.mts +2 -2
  118. package/dist/functions/index.mjs +1 -1
  119. package/dist/{functions-CTI-hDem.mjs → functions-BvIDQKAA.mjs} +3 -3
  120. package/dist/{functions-CTI-hDem.mjs.map → functions-BvIDQKAA.mjs.map} +1 -1
  121. package/dist/{functions-D7O5SGVc.cjs → functions-DLF6-IK8.cjs} +3 -3
  122. package/dist/{functions-D7O5SGVc.cjs.map → functions-DLF6-IK8.cjs.map} +1 -1
  123. package/dist/{index-Cd2w-DRL.d.cts → index-BtzMzhMA.d.cts} +3 -3
  124. package/dist/{index-Cd2w-DRL.d.cts.map → index-BtzMzhMA.d.cts.map} +1 -1
  125. package/dist/{index-aBIcAiH5.d.mts → index-DuriFjYD.d.mts} +3 -3
  126. package/dist/{index-aBIcAiH5.d.mts.map → index-DuriFjYD.d.mts.map} +1 -1
  127. package/dist/index.cjs +1 -1
  128. package/dist/index.d.cts +1 -1
  129. package/dist/index.d.mts +1 -1
  130. package/dist/index.mjs +1 -1
  131. package/dist/pattern-C1CEcglD.mjs +241 -0
  132. package/dist/pattern-C1CEcglD.mjs.map +1 -0
  133. package/dist/pattern-QFaUIr77.cjs +288 -0
  134. package/dist/pattern-QFaUIr77.cjs.map +1 -0
  135. package/dist/processors/index.cjs +214 -0
  136. package/dist/processors/index.cjs.map +1 -0
  137. package/dist/processors/index.d.cts +78 -0
  138. package/dist/processors/index.d.cts.map +1 -0
  139. package/dist/processors/index.d.mts +78 -0
  140. package/dist/processors/index.d.mts.map +1 -0
  141. package/dist/processors/index.mjs +193 -0
  142. package/dist/processors/index.mjs.map +1 -0
  143. package/dist/queue/index.cjs +2 -2
  144. package/dist/queue/index.d.cts +2 -2
  145. package/dist/queue/index.d.mts +2 -2
  146. package/dist/queue/index.mjs +2 -2
  147. package/dist/rest-api.cjs +1 -1
  148. package/dist/rest-api.d.cts +2 -2
  149. package/dist/rest-api.d.mts +2 -2
  150. package/dist/rest-api.mjs +1 -1
  151. package/dist/{runSubscriber-D8nLXIv1.cjs → runSubscriber-B1Yg4dID.cjs} +5 -3
  152. package/dist/runSubscriber-B1Yg4dID.cjs.map +1 -0
  153. package/dist/{runSubscriber-BHvJ8POr.mjs → runSubscriber-JCgkeYbE.mjs} +5 -3
  154. package/dist/runSubscriber-JCgkeYbE.mjs.map +1 -0
  155. package/dist/subscribers/index.cjs +31 -13
  156. package/dist/subscribers/index.cjs.map +1 -1
  157. package/dist/subscribers/index.d.cts +27 -4
  158. package/dist/subscribers/index.d.cts.map +1 -1
  159. package/dist/subscribers/index.d.mts +27 -4
  160. package/dist/subscribers/index.d.mts.map +1 -1
  161. package/dist/subscribers/index.mjs +30 -14
  162. package/dist/subscribers/index.mjs.map +1 -1
  163. package/dist/topic/index.cjs +1 -1
  164. package/dist/topic/index.d.cts +1 -1
  165. package/dist/topic/index.d.mts +1 -1
  166. package/dist/topic/index.mjs +1 -1
  167. package/dist/worker.cjs +25 -4
  168. package/dist/worker.cjs.map +1 -1
  169. package/dist/worker.d.cts +26 -7
  170. package/dist/worker.d.cts.map +1 -1
  171. package/dist/worker.d.mts +26 -7
  172. package/dist/worker.d.mts.map +1 -1
  173. package/dist/worker.mjs +25 -4
  174. package/dist/worker.mjs.map +1 -1
  175. package/package.json +41 -31
  176. package/dist/SubscriberBuilder-BRa_nrmH.d.cts +0 -67
  177. package/dist/SubscriberBuilder-BRa_nrmH.d.cts.map +0 -1
  178. package/dist/SubscriberBuilder-D9bkrYzH.d.mts +0 -67
  179. package/dist/SubscriberBuilder-D9bkrYzH.d.mts.map +0 -1
  180. package/dist/SubscriberBuilder-DRXfxWn_.mjs.map +0 -1
  181. package/dist/SubscriberBuilder-M2EAgQhC.cjs.map +0 -1
  182. package/dist/runSubscriber-BHvJ8POr.mjs.map +0 -1
  183. package/dist/runSubscriber-D8nLXIv1.cjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/processors/runProcessor.ts","../../src/processors/AWSLambdaProcessor.ts"],"sourcesContent":["import type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport { runWithRequestContext, ServiceDiscovery } from '@geekmidas/services';\nimport type { StorageClient } from '@geekmidas/storage';\nimport { defaultEnvParser } from '../endpoints/surfaceEnv';\nimport {\n\tDERIVED_PREFIX,\n\ttype Processor,\n\ttype ProcessorCache,\n\ttype SourceObject,\n} from './Processor';\nimport { fillPattern, matchPattern } from './pattern';\n\n/** What a processor answers, for whichever server or function carries it. */\nexport interface ProcessorResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody?: Uint8Array | string;\n}\n\n/** The object a request addressed is not in the bucket. */\nexport class ProcessorSourceMissing extends Error {\n\tconstructor(readonly key: string) {\n\t\tsuper(`'${key}' is not in the bucket, so there is nothing to process.`);\n\t\tthis.name = 'ProcessorSourceMissing';\n\t}\n}\n\n/** What a refusal or a miss may be kept for: long enough to absorb a burst. */\nconst SHORT = 'public, max-age=60';\n\n/** The header a response carries, by how long it may be kept. */\nexport function cacheControl(cache: ProcessorCache | undefined): string {\n\tif (cache === 'immutable') return 'public, max-age=31536000, immutable';\n\tif (cache === 'none') return 'no-store';\n\treturn `public, max-age=${cache?.maxAge ?? 3600}`;\n}\n\n/** Where a processor keeps the derivative for one request. */\nexport function derivedKey(\n\tprocessor: Processor<string, any, any, any>,\n\tparams: Readonly<Record<string, string>>,\n\tkey: string,\n): string {\n\treturn `${DERIVED_PREFIX}${fillPattern(processor.parsed, params, key).slice(1)}`;\n}\n\nexport interface ProcessorRequestOptions {\n\tserviceDiscovery: ServiceDiscovery<any>;\n\t/** How the bucket is read through a presigned address. */\n\tfetch?: typeof fetch;\n}\n\n/**\n * Answer one request with the first processor whose pattern matches it, or\n * `undefined` when none does.\n *\n * In order: the parameters are parsed by the processor's schema (400 when it\n * refuses them); the key must be one of the file server's open paths (404\n * otherwise, as an unsigned read of it would be); a derivative already in the\n * bucket is answered without running anything; and only then is the handler\n * run, its `immutable` result written through for next time.\n */\nexport async function handleProcessorRequest(\n\tprocessors: readonly Processor<string, any, any, any>[],\n\trequest: { method: string; path: string },\n\toptions: ProcessorRequestOptions,\n): Promise<ProcessorResponse | undefined> {\n\tconst method = request.method.toUpperCase();\n\n\tfor (const processor of processors) {\n\t\tconst match = matchPattern(processor.parsed, request.path);\n\t\tif (!match) continue;\n\n\t\tif (method !== 'GET' && method !== 'HEAD') {\n\t\t\treturn text(405, 'Method Not Allowed', 'no-store', {\n\t\t\t\tallow: 'GET, HEAD',\n\t\t\t});\n\t\t}\n\n\t\treturn runWithRequestContext(\n\t\t\t{\n\t\t\t\tlogger: processor.logger,\n\t\t\t\trequestId: `processor-${Date.now()}`,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t\toperation: `processor ${processor.pattern}`,\n\t\t\t},\n\t\t\t() => run(processor, match, options),\n\t\t);\n\t}\n\n\treturn undefined;\n}\n\n/**\n * A request handler for a set of processors — what a generated entry serves\n * — with its services discovered from the default environment parser: the\n * process's environment and the credentials `gkm` injected.\n */\nexport function processorServer(\n\tprocessors: readonly Processor<string, any, any, any>[],\n\tenvParser: EnvironmentParser<{}> = defaultEnvParser(),\n): (request: {\n\tmethod: string;\n\tpath: string;\n}) => Promise<ProcessorResponse | undefined> {\n\tconst serviceDiscovery = ServiceDiscovery.getInstance(envParser);\n\treturn (request) =>\n\t\thandleProcessorRequest(processors, request, { serviceDiscovery });\n}\n\nasync function run(\n\tprocessor: Processor<string, any, any, any>,\n\tmatch: { params: Record<string, string>; key: string },\n\toptions: ProcessorRequestOptions,\n): Promise<ProcessorResponse> {\n\tconst logger = processor.logger as Logger;\n\tconst fetcher = options.fetch ?? fetch;\n\tconst { key } = match;\n\n\tconst schema = processor.options.paramsSchema;\n\tlet params: unknown = match.params;\n\tif (schema) {\n\t\tconst parsed = await schema['~standard'].validate(match.params);\n\t\tif (parsed.issues) return text(400, 'Bad Request', SHORT);\n\t\tparams = parsed.value;\n\t}\n\n\t// A derivative is never itself a source, and a private object is never\n\t// processed: a processor serves exactly what an unsigned read would.\n\tif (key.startsWith(DERIVED_PREFIX) || !processor.options.serves(key)) {\n\t\treturn text(404, 'Not Found', SHORT);\n\t}\n\n\tconst services: Record<string, unknown> =\n\t\tawait options.serviceDiscovery.register(processor.services);\n\tconst storage = services[\n\t\tprocessor.options.origin.serviceName\n\t] as StorageClient;\n\n\tconst derived = derivedKey(processor, match.params, key);\n\tconst kept = await read(storage, derived, fetcher);\n\tif (kept) {\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\theaders: {\n\t\t\t\t'content-type': kept.contentType ?? 'application/octet-stream',\n\t\t\t\t'cache-control': cacheControl('immutable'),\n\t\t\t\t'x-gkm-processor': 'kept',\n\t\t\t},\n\t\t\tbody: new Uint8Array(await kept.response.arrayBuffer()),\n\t\t};\n\t}\n\n\tlet source: Response | undefined;\n\tconst load = async (): Promise<Response> => {\n\t\tif (source) return source;\n\t\tconst found = await read(storage, key, fetcher);\n\t\tif (!found) throw new ProcessorSourceMissing(key);\n\t\tsource = found.response;\n\t\treturn source;\n\t};\n\tconst object: SourceObject = {\n\t\tkey,\n\t\tbytes: async () => new Uint8Array(await (await load()).arrayBuffer()),\n\t\tstream: async () => {\n\t\t\tconst body = (await load()).body;\n\t\t\tif (!body) throw new ProcessorSourceMissing(key);\n\t\t\treturn body;\n\t\t},\n\t\tcontentType: async () =>\n\t\t\t(await load()).headers.get('content-type') ?? undefined,\n\t};\n\n\tlet result: Awaited<ReturnType<typeof processor.handler>>;\n\ttry {\n\t\tresult = await processor.handler({\n\t\t\tparams,\n\t\t\tkey,\n\t\t\tobject,\n\t\t\tlogger,\n\t\t\tservices,\n\t\t} as never);\n\t} catch (error) {\n\t\tif (error instanceof ProcessorSourceMissing) {\n\t\t\treturn text(404, 'Not Found', SHORT);\n\t\t}\n\t\tlogger.error({ err: error, key }, 'Processor failed');\n\t\treturn text(500, 'Internal Server Error', 'no-store');\n\t}\n\n\tconst body =\n\t\ttypeof result.body === 'string'\n\t\t\t? new TextEncoder().encode(result.body)\n\t\t\t: result.body;\n\n\t// Kept only when it can never change: anything else would be served from\n\t// the bucket long after its handler would have answered differently.\n\tif (result.cache === 'immutable') {\n\t\tawait storage\n\t\t\t.upload(derived, Buffer.from(body), result.contentType)\n\t\t\t.catch((error: unknown) =>\n\t\t\t\tlogger.warn({ err: error, key: derived }, 'Could not keep derivative'),\n\t\t\t);\n\t}\n\n\treturn {\n\t\tstatus: 200,\n\t\theaders: {\n\t\t\t'content-type': result.contentType,\n\t\t\t'cache-control': cacheControl(result.cache),\n\t\t\t'x-gkm-processor': 'made',\n\t\t},\n\t\tbody,\n\t};\n}\n\n/** An object through a presigned address, or nothing when it is not there. */\nasync function read(\n\tstorage: StorageClient,\n\tkey: string,\n\tfetcher: typeof fetch,\n): Promise<{ response: Response; contentType?: string } | undefined> {\n\tconst url = await storage.getDownloadURL({ path: key });\n\tconst response = await fetcher(url);\n\tif (!response.ok) {\n\t\tawait response.body?.cancel().catch(() => undefined);\n\t\treturn undefined;\n\t}\n\tconst contentType = response.headers.get('content-type') ?? undefined;\n\treturn { response, ...(contentType ? { contentType } : {}) };\n}\n\nfunction text(\n\tstatus: number,\n\tbody: string,\n\tcache: string,\n\textra: Record<string, string> = {},\n): ProcessorResponse {\n\treturn {\n\t\tstatus,\n\t\theaders: {\n\t\t\t'content-type': 'text/plain; charset=utf-8',\n\t\t\t'cache-control': cache,\n\t\t\t...extra,\n\t\t},\n\t\tbody,\n\t};\n}\n","import type { EnvironmentParser } from '@geekmidas/envkit';\nimport { defaultEnvParser } from '../endpoints/surfaceEnv';\nimport type { Processor } from './Processor';\nimport { processorServer } from './runProcessor';\n\n/** The parts of a Lambda function URL event a processor reads. */\nexport interface FunctionUrlEvent {\n\trawPath: string;\n\trequestContext: { http: { method: string } };\n}\n\n/** What a function URL answers with. */\nexport interface FunctionUrlResult {\n\tstatusCode: number;\n\theaders: Record<string, string>;\n\tbody: string;\n\tisBase64Encoded: boolean;\n}\n\n/**\n * A processor as a Lambda behind a function URL — the CDN's origin for its\n * pattern on AWS. The URL takes IAM auth and the CDN signs each request, so\n * the function cannot be called around the cache.\n */\nexport class AWSLambdaProcessor {\n\tprivate readonly serve: ReturnType<typeof processorServer>;\n\n\tconstructor(\n\t\tprocessor: Processor<string, any, any, any>,\n\t\tenvParser: EnvironmentParser<{}> = defaultEnvParser(),\n\t) {\n\t\tthis.serve = processorServer([processor], envParser);\n\t}\n\n\thandler = async (event: FunctionUrlEvent): Promise<FunctionUrlResult> => {\n\t\tconst response = await this.serve({\n\t\t\tmethod: event.requestContext.http.method,\n\t\t\tpath: event.rawPath,\n\t\t});\n\t\tif (!response) {\n\t\t\treturn {\n\t\t\t\tstatusCode: 404,\n\t\t\t\theaders: { 'cache-control': 'public, max-age=60' },\n\t\t\t\tbody: '',\n\t\t\t\tisBase64Encoded: false,\n\t\t\t};\n\t\t}\n\t\tconst body = response.body ?? '';\n\t\treturn {\n\t\t\tstatusCode: response.status,\n\t\t\theaders: response.headers,\n\t\t\tbody:\n\t\t\t\ttypeof body === 'string' ? body : Buffer.from(body).toString('base64'),\n\t\t\tisBase64Encoded: typeof body !== 'string',\n\t\t};\n\t};\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_Queue = require("../Queue-BhuofM3k.cjs");
3
- const require_QueueBuilder = require("../QueueBuilder-DdU2klPu.cjs");
2
+ const require_Queue = require("../Queue-D4Iaidrs.cjs");
3
+ const require_QueueBuilder = require("../QueueBuilder-BxsZ9o5z.cjs");
4
4
  exports.Queue = require_Queue.Queue;
5
5
  exports.QueueNeedsMessage = require_QueueBuilder.QueueNeedsMessage;
6
6
  exports.QueueNeedsName = require_QueueBuilder.QueueNeedsName;
@@ -1,3 +1,3 @@
1
- import { i as QueueMessage, n as QueueContext, r as QueueHandler, t as Queue } from "../Queue-JbIjVtTf.cjs";
2
- import { n as QueueNeedsMessage, r as QueueNeedsName } from "../QueueBuilder-MfpkJ3R9.cjs";
1
+ import { i as QueueMessage, n as QueueContext, r as QueueHandler, t as Queue } from "../Queue-C4rijiM7.cjs";
2
+ import { n as QueueNeedsMessage, r as QueueNeedsName } from "../QueueBuilder-BcPBQSSX.cjs";
3
3
  export { Queue, type QueueContext, type QueueHandler, type QueueMessage, QueueNeedsMessage, QueueNeedsName };
@@ -1,3 +1,3 @@
1
- import { i as QueueMessage, n as QueueContext, r as QueueHandler, t as Queue } from "../Queue-B-dDXyjF.mjs";
2
- import { n as QueueNeedsMessage, r as QueueNeedsName } from "../QueueBuilder-48W-q8In.mjs";
1
+ import { i as QueueMessage, n as QueueContext, r as QueueHandler, t as Queue } from "../Queue-BnK3UH8r.mjs";
2
+ import { n as QueueNeedsMessage, r as QueueNeedsName } from "../QueueBuilder-B3vL1ED2.mjs";
3
3
  export { Queue, type QueueContext, type QueueHandler, type QueueMessage, QueueNeedsMessage, QueueNeedsName };
@@ -1,3 +1,3 @@
1
- import { t as Queue } from "../Queue-cmhqEBOB.mjs";
2
- import { n as QueueNeedsMessage, r as QueueNeedsName } from "../QueueBuilder-DXu1qxT1.mjs";
1
+ import { t as Queue } from "../Queue-u8w8_HpE.mjs";
2
+ import { n as QueueNeedsMessage, r as QueueNeedsName } from "../QueueBuilder-2wP5lEC8.mjs";
3
3
  export { Queue, QueueNeedsMessage, QueueNeedsName };
package/dist/rest-api.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_construct_interface = require("./construct-interface.cjs");
3
- const require_EndpointFactory = require("./EndpointFactory-CSrAyazj.cjs");
3
+ const require_EndpointFactory = require("./EndpointFactory-C6pVda4y.cjs");
4
4
  const require_surfaceEnv = require("./surfaceEnv-CNimK76p.cjs");
5
5
  let _geekmidas_manifest = require("@geekmidas/manifest");
6
6
  let _geekmidas_logger_console = require("@geekmidas/logger/console");
@@ -1,7 +1,7 @@
1
1
  import { i as Declarable, t as Authenticator } from "./construct-interface-B1AfqRl4.cjs";
2
2
  import { n as Telemetry } from "./telemetry-B48RLjyk.cjs";
3
- import { D as BuiltInSecuritySchemeId } from "./Endpoint-DgrpG15P.cjs";
4
- import { t as EndpointFactory } from "./EndpointFactory-Cc863XJS.cjs";
3
+ import { D as BuiltInSecuritySchemeId } from "./Endpoint-BEh-jW-f.cjs";
4
+ import { t as EndpointFactory } from "./EndpointFactory-B2dCS9dy.cjs";
5
5
  import { EnvironmentParser } from "@geekmidas/envkit";
6
6
  import { Logger } from "@geekmidas/logger";
7
7
  import { ConstructName, Declaration, Dependency } from "@geekmidas/manifest";
@@ -1,7 +1,7 @@
1
1
  import { i as Declarable, t as Authenticator } from "./construct-interface-ZvYQbivO.mjs";
2
2
  import { n as Telemetry } from "./telemetry-BEoBn6GO.mjs";
3
- import { D as BuiltInSecuritySchemeId } from "./Endpoint-CeFkSwh5.mjs";
4
- import { t as EndpointFactory } from "./EndpointFactory-Dbw9ugkX.mjs";
3
+ import { D as BuiltInSecuritySchemeId } from "./Endpoint-DTXDggNl.mjs";
4
+ import { t as EndpointFactory } from "./EndpointFactory-D2HXcUrJ.mjs";
5
5
  import { ConstructName, Declaration, Dependency } from "@geekmidas/manifest";
6
6
  import { EnvironmentParser } from "@geekmidas/envkit";
7
7
  import { Logger } from "@geekmidas/logger";
package/dist/rest-api.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { edgeTo } from "./construct-interface.mjs";
2
- import { p as getSecurityScheme, t as EndpointFactory } from "./EndpointFactory-c2ZugGig.mjs";
2
+ import { p as getSecurityScheme, t as EndpointFactory } from "./EndpointFactory-CUpqgtaH.mjs";
3
3
  import { n as envParserFor } from "./surfaceEnv-wMT5QzgZ.mjs";
4
4
  import { canonicalId, provideKey } from "@geekmidas/manifest";
5
5
  import { DEFAULT_LOGGER } from "@geekmidas/logger/console";
@@ -10,15 +10,17 @@ var SubscriberOutputInvalid = class extends Error {
10
10
  }
11
11
  };
12
12
  /** The events in an SQS or SNS batch (a Lambda event's `Records`) that `subscriber` subscribed to. */
13
- function subscribedEvents(subscriber, rawEvent, logger) {
13
+ async function subscribedEvents(subscriber, rawEvent, logger) {
14
14
  const events = [];
15
15
  const records = rawEvent.Records ?? [];
16
16
  const first = records[0];
17
17
  const parse = first?.eventSource === "aws:sqs" ? (record) => parseSQSRecord(record) : first?.EventSource === "aws:sns" ? (record) => parseSNSRecord(record) : void 0;
18
18
  if (!parse) return events;
19
+ const toObjectEvents = subscriber.bucketName ? (await import("@geekmidas/storage/events")).toObjectEvents : void 0;
19
20
  for (const record of records) try {
20
21
  const event = parse(record);
21
- if (isSubscribed(subscriber, event)) events.push(event);
22
+ const parsed = toObjectEvents && !(typeof event === "object" && event?.type) ? toObjectEvents(event) : [event];
23
+ for (const one of parsed) if (isSubscribed(subscriber, one)) events.push(one);
22
24
  } catch (error) {
23
25
  logger.error({
24
26
  err: error,
@@ -123,4 +125,4 @@ Object.defineProperty(exports, "subscriberContext", {
123
125
  }
124
126
  });
125
127
 
126
- //# sourceMappingURL=runSubscriber-D8nLXIv1.cjs.map
128
+ //# sourceMappingURL=runSubscriber-B1Yg4dID.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runSubscriber-B1Yg4dID.cjs","names":["ServiceDiscovery"],"sources":["../src/subscribers/runSubscriber.ts"],"sourcesContent":["/**\n * What a subscriber does with a batch of records, however they arrived: parse\n * them into events, keep the ones it subscribed to, and run its handler with\n * its services and database.\n *\n * Shared by the Lambda adaptor, which wraps it in middy, and the SNS push\n * adaptor, which a server runs and which must not need middy to load.\n */\n\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport { ServiceDiscovery } from '@geekmidas/services';\nimport type { SNSEvent, SNSEventRecord, SQSEvent, SQSRecord } from 'aws-lambda';\nimport type { Subscriber } from './Subscriber';\n\ntype AnySubscriber = Subscriber<any, any, any, any, any, any, any>;\n\n/** The subscriber's output did not match its `.output()` schema. */\nexport class SubscriberOutputInvalid extends Error {\n\tconstructor(readonly issues: ReadonlyArray<unknown>) {\n\t\tsuper(\n\t\t\t\"Subscriber output validation failed: the handler's result does not match its .output() schema.\",\n\t\t);\n\t\tthis.name = 'SubscriberOutputInvalid';\n\t}\n}\n\n/** The events in an SQS or SNS batch (a Lambda event's `Records`) that `subscriber` subscribed to. */\nexport async function subscribedEvents(\n\tsubscriber: AnySubscriber,\n\trawEvent: { Records?: readonly unknown[] },\n\tlogger: Logger,\n): Promise<any[]> {\n\tconst events: any[] = [];\n\tconst records = rawEvent.Records ?? [];\n\tconst first = records[0] as any;\n\tconst parse =\n\t\tfirst?.eventSource === 'aws:sqs'\n\t\t\t? (record: any) => parseSQSRecord(record)\n\t\t\t: first?.EventSource === 'aws:sns'\n\t\t\t\t? (record: any) => parseSNSRecord(record)\n\t\t\t\t: undefined;\n\tif (!parse) return events;\n\n\t// A bucket's subscriber is sent what the storage sends — a notification\n\t// of records — and reads it as object events. Loaded only then: an app\n\t// with no bucket never installs the storage package.\n\tconst toObjectEvents = subscriber.bucketName\n\t\t? (await import('@geekmidas/storage/events')).toObjectEvents\n\t\t: undefined;\n\n\tfor (const record of records) {\n\t\ttry {\n\t\t\tconst event = parse(record);\n\t\t\tconst parsed =\n\t\t\t\ttoObjectEvents && !(typeof event === 'object' && event?.type)\n\t\t\t\t\t? toObjectEvents(event)\n\t\t\t\t\t: [event];\n\t\t\tfor (const one of parsed) {\n\t\t\t\tif (isSubscribed(subscriber, one)) events.push(one);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlogger.error({ err: error, record }, 'Failed to parse record');\n\t\t}\n\t}\n\treturn events;\n}\n\n/** The subscriber's services and database, registered as functions do. */\nexport async function subscriberContext(\n\tsubscriber: AnySubscriber,\n\tenvParser: EnvironmentParser<{}>,\n): Promise<{ services: Record<string, unknown>; db: unknown }> {\n\tconst discovery = ServiceDiscovery.getInstance(envParser);\n\tconst services =\n\t\tsubscriber.services.length > 0\n\t\t\t? await discovery.register(subscriber.services)\n\t\t\t: {};\n\tconst service = subscriber.databaseService;\n\tconst db = service\n\t\t? (await discovery.register([service]))[service.serviceName]\n\t\t: undefined;\n\treturn { services, db };\n}\n\n/** Runs the handler on `events`, and checks its output against its schema. */\nexport async function runSubscriber(\n\tsubscriber: AnySubscriber,\n\tcontext: {\n\t\tevents: any[];\n\t\tservices: Record<string, unknown>;\n\t\tdb: unknown;\n\t\tlogger: Logger;\n\t},\n): Promise<unknown> {\n\tif (context.events.length === 0) {\n\t\tcontext.logger.info('No subscribed events to process');\n\t\treturn { batchItemFailures: [] };\n\t}\n\n\tconst { events, services, db, logger } = context;\n\tconst result = await subscriber.handler({\n\t\tevents,\n\t\tservices,\n\t\tlogger,\n\t\tdb,\n\t} as any);\n\n\tif (subscriber.outputSchema && result) {\n\t\tconst validation =\n\t\t\tawait subscriber.outputSchema['~standard'].validate(result);\n\t\tif (validation.issues) {\n\t\t\tcontext.logger.error(\n\t\t\t\t{ issues: validation.issues },\n\t\t\t\t'Subscriber output validation failed',\n\t\t\t);\n\t\t\tthrow new SubscriberOutputInvalid(validation.issues);\n\t\t}\n\t\treturn validation.value;\n\t}\n\n\treturn result;\n}\n\nfunction isSubscribed(subscriber: AnySubscriber, event: any): boolean {\n\t// No event type (raw string/non-object) — always include\n\tif (typeof event !== 'object' || !event?.type) return true;\n\t// No filter configured — accept all\n\tif (!subscriber.subscribedEvents) return true;\n\treturn subscriber.subscribedEvents.includes(event.type);\n}\n\nfunction parseSNSRecord(record: SNSEventRecord): any {\n\tconst message = safeJsonParse(record.Sns.Message);\n\tconst messageType = record.Sns.MessageAttributes?.type?.Value;\n\n\t// Not JSON — wrap raw string with type from MessageAttributes if available\n\tif (message === null) {\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: record.Sns.Message }\n\t\t\t: record.Sns.Message;\n\t}\n\n\tif (message.type) {\n\t\treturn message; // Full event format: { type, payload }\n\t}\n\n\t// Payload-only format: type is in MessageAttributes\n\treturn messageType ? { type: messageType, payload: message } : message;\n}\n\nfunction parseSQSRecord(record: SQSRecord): any {\n\tconst body = safeJsonParse(record.body);\n\n\t// Not JSON — return raw body as-is\n\tif (body === null) {\n\t\treturn record.body;\n\t}\n\n\t// Check if this is an SNS message wrapped in SQS\n\tif (body.Type === 'Notification' && body.Message) {\n\t\tconst snsMessage = safeJsonParse(body.Message);\n\t\tconst messageType = body.MessageAttributes?.type?.Value;\n\n\t\t// SNS Message not JSON — wrap with type from MessageAttributes if available\n\t\tif (snsMessage === null) {\n\t\t\treturn messageType\n\t\t\t\t? { type: messageType, payload: body.Message }\n\t\t\t\t: body.Message;\n\t\t}\n\n\t\tif (snsMessage.type) {\n\t\t\treturn snsMessage; // Full event format: { type, payload }\n\t\t}\n\n\t\t// Payload-only format: type is in MessageAttributes\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: snsMessage }\n\t\t\t: snsMessage;\n\t}\n\n\t// Direct SQS message\n\treturn body;\n}\n\nfunction safeJsonParse(value: string): any | null {\n\ttry {\n\t\treturn JSON.parse(value);\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
@@ -10,15 +10,17 @@ var SubscriberOutputInvalid = class extends Error {
10
10
  }
11
11
  };
12
12
  /** The events in an SQS or SNS batch (a Lambda event's `Records`) that `subscriber` subscribed to. */
13
- function subscribedEvents(subscriber, rawEvent, logger) {
13
+ async function subscribedEvents(subscriber, rawEvent, logger) {
14
14
  const events = [];
15
15
  const records = rawEvent.Records ?? [];
16
16
  const first = records[0];
17
17
  const parse = first?.eventSource === "aws:sqs" ? (record) => parseSQSRecord(record) : first?.EventSource === "aws:sns" ? (record) => parseSNSRecord(record) : void 0;
18
18
  if (!parse) return events;
19
+ const toObjectEvents = subscriber.bucketName ? (await import("@geekmidas/storage/events")).toObjectEvents : void 0;
19
20
  for (const record of records) try {
20
21
  const event = parse(record);
21
- if (isSubscribed(subscriber, event)) events.push(event);
22
+ const parsed = toObjectEvents && !(typeof event === "object" && event?.type) ? toObjectEvents(event) : [event];
23
+ for (const one of parsed) if (isSubscribed(subscriber, one)) events.push(one);
22
24
  } catch (error) {
23
25
  logger.error({
24
26
  err: error,
@@ -106,4 +108,4 @@ function safeJsonParse(value) {
106
108
  //#endregion
107
109
  export { subscribedEvents as n, subscriberContext as r, runSubscriber as t };
108
110
 
109
- //# sourceMappingURL=runSubscriber-BHvJ8POr.mjs.map
111
+ //# sourceMappingURL=runSubscriber-JCgkeYbE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runSubscriber-JCgkeYbE.mjs","names":[],"sources":["../src/subscribers/runSubscriber.ts"],"sourcesContent":["/**\n * What a subscriber does with a batch of records, however they arrived: parse\n * them into events, keep the ones it subscribed to, and run its handler with\n * its services and database.\n *\n * Shared by the Lambda adaptor, which wraps it in middy, and the SNS push\n * adaptor, which a server runs and which must not need middy to load.\n */\n\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport { ServiceDiscovery } from '@geekmidas/services';\nimport type { SNSEvent, SNSEventRecord, SQSEvent, SQSRecord } from 'aws-lambda';\nimport type { Subscriber } from './Subscriber';\n\ntype AnySubscriber = Subscriber<any, any, any, any, any, any, any>;\n\n/** The subscriber's output did not match its `.output()` schema. */\nexport class SubscriberOutputInvalid extends Error {\n\tconstructor(readonly issues: ReadonlyArray<unknown>) {\n\t\tsuper(\n\t\t\t\"Subscriber output validation failed: the handler's result does not match its .output() schema.\",\n\t\t);\n\t\tthis.name = 'SubscriberOutputInvalid';\n\t}\n}\n\n/** The events in an SQS or SNS batch (a Lambda event's `Records`) that `subscriber` subscribed to. */\nexport async function subscribedEvents(\n\tsubscriber: AnySubscriber,\n\trawEvent: { Records?: readonly unknown[] },\n\tlogger: Logger,\n): Promise<any[]> {\n\tconst events: any[] = [];\n\tconst records = rawEvent.Records ?? [];\n\tconst first = records[0] as any;\n\tconst parse =\n\t\tfirst?.eventSource === 'aws:sqs'\n\t\t\t? (record: any) => parseSQSRecord(record)\n\t\t\t: first?.EventSource === 'aws:sns'\n\t\t\t\t? (record: any) => parseSNSRecord(record)\n\t\t\t\t: undefined;\n\tif (!parse) return events;\n\n\t// A bucket's subscriber is sent what the storage sends — a notification\n\t// of records — and reads it as object events. Loaded only then: an app\n\t// with no bucket never installs the storage package.\n\tconst toObjectEvents = subscriber.bucketName\n\t\t? (await import('@geekmidas/storage/events')).toObjectEvents\n\t\t: undefined;\n\n\tfor (const record of records) {\n\t\ttry {\n\t\t\tconst event = parse(record);\n\t\t\tconst parsed =\n\t\t\t\ttoObjectEvents && !(typeof event === 'object' && event?.type)\n\t\t\t\t\t? toObjectEvents(event)\n\t\t\t\t\t: [event];\n\t\t\tfor (const one of parsed) {\n\t\t\t\tif (isSubscribed(subscriber, one)) events.push(one);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tlogger.error({ err: error, record }, 'Failed to parse record');\n\t\t}\n\t}\n\treturn events;\n}\n\n/** The subscriber's services and database, registered as functions do. */\nexport async function subscriberContext(\n\tsubscriber: AnySubscriber,\n\tenvParser: EnvironmentParser<{}>,\n): Promise<{ services: Record<string, unknown>; db: unknown }> {\n\tconst discovery = ServiceDiscovery.getInstance(envParser);\n\tconst services =\n\t\tsubscriber.services.length > 0\n\t\t\t? await discovery.register(subscriber.services)\n\t\t\t: {};\n\tconst service = subscriber.databaseService;\n\tconst db = service\n\t\t? (await discovery.register([service]))[service.serviceName]\n\t\t: undefined;\n\treturn { services, db };\n}\n\n/** Runs the handler on `events`, and checks its output against its schema. */\nexport async function runSubscriber(\n\tsubscriber: AnySubscriber,\n\tcontext: {\n\t\tevents: any[];\n\t\tservices: Record<string, unknown>;\n\t\tdb: unknown;\n\t\tlogger: Logger;\n\t},\n): Promise<unknown> {\n\tif (context.events.length === 0) {\n\t\tcontext.logger.info('No subscribed events to process');\n\t\treturn { batchItemFailures: [] };\n\t}\n\n\tconst { events, services, db, logger } = context;\n\tconst result = await subscriber.handler({\n\t\tevents,\n\t\tservices,\n\t\tlogger,\n\t\tdb,\n\t} as any);\n\n\tif (subscriber.outputSchema && result) {\n\t\tconst validation =\n\t\t\tawait subscriber.outputSchema['~standard'].validate(result);\n\t\tif (validation.issues) {\n\t\t\tcontext.logger.error(\n\t\t\t\t{ issues: validation.issues },\n\t\t\t\t'Subscriber output validation failed',\n\t\t\t);\n\t\t\tthrow new SubscriberOutputInvalid(validation.issues);\n\t\t}\n\t\treturn validation.value;\n\t}\n\n\treturn result;\n}\n\nfunction isSubscribed(subscriber: AnySubscriber, event: any): boolean {\n\t// No event type (raw string/non-object) — always include\n\tif (typeof event !== 'object' || !event?.type) return true;\n\t// No filter configured — accept all\n\tif (!subscriber.subscribedEvents) return true;\n\treturn subscriber.subscribedEvents.includes(event.type);\n}\n\nfunction parseSNSRecord(record: SNSEventRecord): any {\n\tconst message = safeJsonParse(record.Sns.Message);\n\tconst messageType = record.Sns.MessageAttributes?.type?.Value;\n\n\t// Not JSON — wrap raw string with type from MessageAttributes if available\n\tif (message === null) {\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: record.Sns.Message }\n\t\t\t: record.Sns.Message;\n\t}\n\n\tif (message.type) {\n\t\treturn message; // Full event format: { type, payload }\n\t}\n\n\t// Payload-only format: type is in MessageAttributes\n\treturn messageType ? { type: messageType, payload: message } : message;\n}\n\nfunction parseSQSRecord(record: SQSRecord): any {\n\tconst body = safeJsonParse(record.body);\n\n\t// Not JSON — return raw body as-is\n\tif (body === null) {\n\t\treturn record.body;\n\t}\n\n\t// Check if this is an SNS message wrapped in SQS\n\tif (body.Type === 'Notification' && body.Message) {\n\t\tconst snsMessage = safeJsonParse(body.Message);\n\t\tconst messageType = body.MessageAttributes?.type?.Value;\n\n\t\t// SNS Message not JSON — wrap with type from MessageAttributes if available\n\t\tif (snsMessage === null) {\n\t\t\treturn messageType\n\t\t\t\t? { type: messageType, payload: body.Message }\n\t\t\t\t: body.Message;\n\t\t}\n\n\t\tif (snsMessage.type) {\n\t\t\treturn snsMessage; // Full event format: { type, payload }\n\t\t}\n\n\t\t// Payload-only format: type is in MessageAttributes\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: snsMessage }\n\t\t\t: snsMessage;\n\t}\n\n\t// Direct SQS message\n\treturn body;\n}\n\nfunction safeJsonParse(value: string): any | null {\n\ttry {\n\t\treturn JSON.parse(value);\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_SubscriberBuilder = require("../SubscriberBuilder-M2EAgQhC.cjs");
3
- const require_runSubscriber = require("../runSubscriber-D8nLXIv1.cjs");
2
+ const require_SubscriberBuilder = require("../SubscriberBuilder-CQst6Iz3.cjs");
3
+ const require_runSubscriber = require("../runSubscriber-B1Yg4dID.cjs");
4
4
  let _geekmidas_events = require("@geekmidas/events");
5
5
  let _geekmidas_services = require("@geekmidas/services");
6
6
  let _geekmidas_errors = require("@geekmidas/errors");
@@ -24,7 +24,7 @@ var SnsPushSubscriberAdaptor = class {
24
24
  this.subscriber = subscriber;
25
25
  this.options = options;
26
26
  }
27
- async handle(body) {
27
+ async handle(body, request = {}) {
28
28
  const message = body;
29
29
  if (typeof message?.Type !== "string" || !message.TopicArn) return refuse(400, "not an SNS message");
30
30
  if (message.TopicArn !== this.options.topicArn) return refuse(403, `not subscribed to ${message.TopicArn}`);
@@ -39,6 +39,14 @@ var SnsPushSubscriberAdaptor = class {
39
39
  }
40
40
  switch (message.Type) {
41
41
  case "SubscriptionConfirmation":
42
+ if (this.options.confirm) {
43
+ await this.options.confirm(message);
44
+ this.subscriber.logger.info({ topicArn: message.TopicArn }, "Confirmed SNS subscription");
45
+ return {
46
+ status: 200,
47
+ body: { status: "confirmed" }
48
+ };
49
+ }
42
50
  await (0, _geekmidas_events_sns.confirmSnsSubscription)(this.options.emulatorEndpoint && message.SubscribeURL ? {
43
51
  ...message,
44
52
  SubscribeURL: throughEndpoint(message.SubscribeURL, this.options.emulatorEndpoint)
@@ -48,15 +56,23 @@ var SnsPushSubscriberAdaptor = class {
48
56
  status: 200,
49
57
  body: { status: "confirmed" }
50
58
  };
51
- case "Notification": try {
52
- await this.notify(message);
53
- return {
54
- status: 200,
55
- body: { status: "ok" }
56
- };
57
- } catch (error) {
58
- return refuse(500, error.name);
59
- }
59
+ case "Notification":
60
+ if (this.options.subscriptionArn && request.subscriptionArn !== this.options.subscriptionArn) {
61
+ this.subscriber.logger.warn({
62
+ topicArn: message.TopicArn,
63
+ subscriptionArn: request.subscriptionArn
64
+ }, "Refused an SNS notification from a subscription the deploy did not make");
65
+ return refuse(403, "not the recorded subscription");
66
+ }
67
+ try {
68
+ await this.notify(message);
69
+ return {
70
+ status: 200,
71
+ body: { status: "ok" }
72
+ };
73
+ } catch (error) {
74
+ return refuse(500, error.name);
75
+ }
60
76
  default: return {
61
77
  status: 200,
62
78
  body: { status: "ignored" }
@@ -81,7 +97,7 @@ var SnsPushSubscriberAdaptor = class {
81
97
  async run(message, logger) {
82
98
  try {
83
99
  const { services, db } = await require_runSubscriber.subscriberContext(this.subscriber, this.envParser);
84
- const events = require_runSubscriber.subscribedEvents(this.subscriber, (0, _geekmidas_events_sns.toSnsEvent)(message), logger);
100
+ const events = await require_runSubscriber.subscribedEvents(this.subscriber, (0, _geekmidas_events_sns.toSnsEvent)(message), logger);
85
101
  await require_runSubscriber.runSubscriber(this.subscriber, {
86
102
  events,
87
103
  services,
@@ -112,6 +128,8 @@ function refuse(status, error) {
112
128
  };
113
129
  }
114
130
  //#endregion
131
+ exports.ObjectSubscriberBuilder = require_SubscriberBuilder.ObjectSubscriberBuilder;
132
+ exports.ObjectSubscriberHasNoEvents = require_SubscriberBuilder.ObjectSubscriberHasNoEvents;
115
133
  exports.SnsPushSubscriberAdaptor = SnsPushSubscriberAdaptor;
116
134
  exports.Subscriber = require_SubscriberBuilder.Subscriber;
117
135
  exports.SubscriberBuilder = require_SubscriberBuilder.SubscriberBuilder;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["verifySnsMessage","confirmSnsSubscription","runWithRequestContext","consumeTraced","carrierFromAttributes","subscriberContext","subscribedEvents","toSnsEvent","runSubscriber","wrapError"],"sources":["../../src/subscribers/SnsPushSubscriberAdaptor.ts"],"sourcesContent":["import type { EnvironmentParser } from '@geekmidas/envkit';\nimport { wrapError } from '@geekmidas/errors';\nimport { carrierFromAttributes, consumeTraced } from '@geekmidas/events';\nimport {\n\tconfirmSnsSubscription,\n\ttype SnsHttpMessage,\n\ttoSnsEvent,\n\tverifySnsMessage,\n} from '@geekmidas/events/sns';\nimport { runWithRequestContext } from '@geekmidas/services';\nimport {\n\trunSubscriber,\n\tsubscribedEvents,\n\tsubscriberContext,\n} from './runSubscriber';\nimport type { Subscriber } from './Subscriber';\n\nexport interface SnsPushOptions {\n\t/** The topic this subscriber is subscribed to. Anything else is refused. */\n\ttopicArn: string;\n\t/**\n\t * Verify each message's signature. Off only against an emulator, which\n\t * signs nothing — decided by the connection string's endpoint, never by\n\t * anything the request says about itself.\n\t */\n\tverify: boolean;\n\t/**\n\t * The emulator's address, as this server was configured with it. An\n\t * emulator names itself in `SubscribeURL` by the port it listens on inside\n\t * its container, which is not the port published to the host — so against\n\t * one, the confirmation is sent here instead. From configuration, never\n\t * from the request.\n\t */\n\temulatorEndpoint?: string;\n}\n\n/** What to answer SNS with. A non-2xx tells SNS to retry. */\nexport interface SnsPushResponse {\n\tstatus: 200 | 400 | 403 | 500;\n\tbody: { status: string; error?: string };\n}\n\n/**\n * A topic subscriber run by SNS pushing to an HTTP endpoint, rather than by a\n * poller.\n *\n * The notification is read as the Lambda event SNS would have invoked a\n * function with, and run through the same parsing, services and handler as the\n * Lambda adaptor — without its middy wrapper, which is Lambda's alone: a\n * server serving this route has no reason to install `@middy/core`.\n */\nexport class SnsPushSubscriberAdaptor {\n\tconstructor(\n\t\tprivate readonly envParser: EnvironmentParser<{}>,\n\t\treadonly subscriber: Subscriber<any, any, any, any, any, any, any>,\n\t\tprivate readonly options: SnsPushOptions,\n\t) {}\n\n\tasync handle(body: unknown): Promise<SnsPushResponse> {\n\t\tconst message = body as SnsHttpMessage;\n\t\tif (typeof message?.Type !== 'string' || !message.TopicArn) {\n\t\t\treturn refuse(400, 'not an SNS message');\n\t\t}\n\t\t// Checked before the signature: a genuine message for another topic is\n\t\t// still not this subscriber's to confirm or to handle.\n\t\tif (message.TopicArn !== this.options.topicArn) {\n\t\t\treturn refuse(403, `not subscribed to ${message.TopicArn}`);\n\t\t}\n\n\t\tif (this.options.verify) {\n\t\t\ttry {\n\t\t\t\tawait verifySnsMessage(message);\n\t\t\t} catch (error) {\n\t\t\t\tthis.subscriber.logger.warn(\n\t\t\t\t\t{ err: error, topicArn: message.TopicArn },\n\t\t\t\t\t'Refused an SNS message that did not verify',\n\t\t\t\t);\n\t\t\t\treturn refuse(403, (error as Error).name);\n\t\t\t}\n\t\t}\n\n\t\tswitch (message.Type) {\n\t\t\tcase 'SubscriptionConfirmation':\n\t\t\t\tawait confirmSnsSubscription(\n\t\t\t\t\tthis.options.emulatorEndpoint && message.SubscribeURL\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t...message,\n\t\t\t\t\t\t\t\tSubscribeURL: throughEndpoint(\n\t\t\t\t\t\t\t\t\tmessage.SubscribeURL,\n\t\t\t\t\t\t\t\t\tthis.options.emulatorEndpoint,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: message,\n\t\t\t\t);\n\t\t\t\tthis.subscriber.logger.info(\n\t\t\t\t\t{ topicArn: message.TopicArn },\n\t\t\t\t\t'Confirmed SNS subscription',\n\t\t\t\t);\n\t\t\t\treturn { status: 200, body: { status: 'confirmed' } };\n\t\t\tcase 'Notification':\n\t\t\t\ttry {\n\t\t\t\t\tawait this.notify(message);\n\t\t\t\t\treturn { status: 200, body: { status: 'ok' } };\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Already logged. A 500 makes SNS retry, which is the point of\n\t\t\t\t\t// answering at all.\n\t\t\t\t\treturn refuse(500, (error as Error).name);\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn { status: 200, body: { status: 'ignored' } };\n\t\t}\n\t}\n\n\tprivate async notify(message: SnsHttpMessage): Promise<void> {\n\t\tconst requestId = message.MessageId;\n\t\tconst logger = this.subscriber.logger.child({ requestId });\n\n\t\tawait runWithRequestContext(\n\t\t\t{\n\t\t\t\tlogger,\n\t\t\t\trequestId,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t\toperation: `subscriber ${this.subscriber.topicName ?? 'sns'}`,\n\t\t\t},\n\t\t\t// A CONSUMER span, child of the publisher's: SNS hands the trace\n\t\t\t// context on in the message attributes it was published with.\n\t\t\t() =>\n\t\t\t\tconsumeTraced(\n\t\t\t\t\t{\n\t\t\t\t\t\tsystem: 'aws_sns',\n\t\t\t\t\t\tdestination:\n\t\t\t\t\t\t\tthis.subscriber.topicName ??\n\t\t\t\t\t\t\tmessage.TopicArn.split(':').pop() ??\n\t\t\t\t\t\t\tmessage.TopicArn,\n\t\t\t\t\t\ttype: message.MessageAttributes?.type?.Value,\n\t\t\t\t\t\tmessageId: message.MessageId,\n\t\t\t\t\t},\n\t\t\t\t\tcarrierFromAttributes(message.MessageAttributes),\n\t\t\t\t\t() => this.run(message, logger),\n\t\t\t\t),\n\t\t);\n\t}\n\n\tprivate async run(\n\t\tmessage: SnsHttpMessage,\n\t\tlogger: Subscriber<any, any, any, any, any, any, any>['logger'],\n\t): Promise<void> {\n\t\ttry {\n\t\t\tconst { services, db } = await subscriberContext(\n\t\t\t\tthis.subscriber,\n\t\t\t\tthis.envParser,\n\t\t\t);\n\t\t\tconst events = subscribedEvents(\n\t\t\t\tthis.subscriber,\n\t\t\t\ttoSnsEvent(message),\n\t\t\t\tlogger,\n\t\t\t);\n\t\t\tawait runSubscriber(this.subscriber, {\n\t\t\t\tevents,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as object, 'Error processing subscriber');\n\t\t\tthrow wrapError(error);\n\t\t}\n\t}\n}\n\n/** `url`, sent to `endpoint`'s origin instead of its own. */\nfunction throughEndpoint(url: string, endpoint: string): string {\n\tconst target = new URL(url);\n\tconst origin = new URL(endpoint);\n\ttarget.protocol = origin.protocol;\n\ttarget.host = origin.host;\n\treturn target.toString();\n}\n\nfunction refuse(\n\tstatus: SnsPushResponse['status'],\n\terror: string,\n): SnsPushResponse {\n\treturn { status, body: { status: 'refused', error } };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["verifySnsMessage","confirmSnsSubscription","runWithRequestContext","consumeTraced","carrierFromAttributes","subscriberContext","subscribedEvents","toSnsEvent","runSubscriber","wrapError"],"sources":["../../src/subscribers/SnsPushSubscriberAdaptor.ts"],"sourcesContent":["import type { EnvironmentParser } from '@geekmidas/envkit';\nimport { wrapError } from '@geekmidas/errors';\nimport { carrierFromAttributes, consumeTraced } from '@geekmidas/events';\nimport {\n\tconfirmSnsSubscription,\n\ttype SnsHttpMessage,\n\ttoSnsEvent,\n\tverifySnsMessage,\n} from '@geekmidas/events/sns';\nimport { runWithRequestContext } from '@geekmidas/services';\nimport {\n\trunSubscriber,\n\tsubscribedEvents,\n\tsubscriberContext,\n} from './runSubscriber';\nimport type { Subscriber } from './Subscriber';\n\nexport interface SnsPushOptions {\n\t/** The topic this subscriber is subscribed to. Anything else is refused. */\n\ttopicArn: string;\n\t/**\n\t * Verify each message's signature. Off only against an emulator, which\n\t * signs nothing — decided by the connection string's endpoint, never by\n\t * anything the request says about itself.\n\t */\n\tverify: boolean;\n\t/**\n\t * The emulator's address, as this server was configured with it. An\n\t * emulator names itself in `SubscribeURL` by the port it listens on inside\n\t * its container, which is not the port published to the host — so against\n\t * one, the confirmation is sent here instead. From configuration, never\n\t * from the request.\n\t */\n\temulatorEndpoint?: string;\n\t/**\n\t * The subscription the deploy made for this route, as it recorded it\n\t * (`GKM_SNS_SUBSCRIPTIONS`). A notification naming another one in its\n\t * `x-amz-sns-subscription-arn` header is refused — a subscription someone\n\t * added by hand, pointing at this route.\n\t *\n\t * Defence in depth, not authentication: SNS does not sign its headers.\n\t * What authenticates a message is its signature and its `TopicArn`.\n\t */\n\tsubscriptionArn?: string;\n\t/**\n\t * How a confirmation is confirmed. By visiting its `SubscribeURL` when\n\t * unset — what an emulator takes. Against SNS itself, the server confirms\n\t * through the API with `AuthenticateOnUnsubscribe`, so the `UnsubscribeURL`\n\t * every notification carries cannot cut the route off.\n\t */\n\tconfirm?: (message: SnsHttpMessage) => Promise<unknown>;\n}\n\n/** What SNS said about a request, outside its signed body. */\nexport interface SnsPushRequest {\n\t/** The `x-amz-sns-subscription-arn` header. */\n\tsubscriptionArn?: string;\n}\n\n/** What to answer SNS with. A non-2xx tells SNS to retry. */\nexport interface SnsPushResponse {\n\tstatus: 200 | 400 | 403 | 500;\n\tbody: { status: string; error?: string };\n}\n\n/**\n * A topic subscriber run by SNS pushing to an HTTP endpoint, rather than by a\n * poller.\n *\n * The notification is read as the Lambda event SNS would have invoked a\n * function with, and run through the same parsing, services and handler as the\n * Lambda adaptor — without its middy wrapper, which is Lambda's alone: a\n * server serving this route has no reason to install `@middy/core`.\n */\nexport class SnsPushSubscriberAdaptor {\n\tconstructor(\n\t\tprivate readonly envParser: EnvironmentParser<{}>,\n\t\treadonly subscriber: Subscriber<any, any, any, any, any, any, any>,\n\t\tprivate readonly options: SnsPushOptions,\n\t) {}\n\n\tasync handle(\n\t\tbody: unknown,\n\t\trequest: SnsPushRequest = {},\n\t): Promise<SnsPushResponse> {\n\t\tconst message = body as SnsHttpMessage;\n\t\tif (typeof message?.Type !== 'string' || !message.TopicArn) {\n\t\t\treturn refuse(400, 'not an SNS message');\n\t\t}\n\t\t// Checked before the signature: a genuine message for another topic is\n\t\t// still not this subscriber's to confirm or to handle.\n\t\tif (message.TopicArn !== this.options.topicArn) {\n\t\t\treturn refuse(403, `not subscribed to ${message.TopicArn}`);\n\t\t}\n\n\t\tif (this.options.verify) {\n\t\t\ttry {\n\t\t\t\tawait verifySnsMessage(message);\n\t\t\t} catch (error) {\n\t\t\t\tthis.subscriber.logger.warn(\n\t\t\t\t\t{ err: error, topicArn: message.TopicArn },\n\t\t\t\t\t'Refused an SNS message that did not verify',\n\t\t\t\t);\n\t\t\t\treturn refuse(403, (error as Error).name);\n\t\t\t}\n\t\t}\n\n\t\tswitch (message.Type) {\n\t\t\tcase 'SubscriptionConfirmation':\n\t\t\t\tif (this.options.confirm) {\n\t\t\t\t\tawait this.options.confirm(message);\n\t\t\t\t\tthis.subscriber.logger.info(\n\t\t\t\t\t\t{ topicArn: message.TopicArn },\n\t\t\t\t\t\t'Confirmed SNS subscription',\n\t\t\t\t\t);\n\t\t\t\t\treturn { status: 200, body: { status: 'confirmed' } };\n\t\t\t\t}\n\t\t\t\tawait confirmSnsSubscription(\n\t\t\t\t\tthis.options.emulatorEndpoint && message.SubscribeURL\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t...message,\n\t\t\t\t\t\t\t\tSubscribeURL: throughEndpoint(\n\t\t\t\t\t\t\t\t\tmessage.SubscribeURL,\n\t\t\t\t\t\t\t\t\tthis.options.emulatorEndpoint,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: message,\n\t\t\t\t);\n\t\t\t\tthis.subscriber.logger.info(\n\t\t\t\t\t{ topicArn: message.TopicArn },\n\t\t\t\t\t'Confirmed SNS subscription',\n\t\t\t\t);\n\t\t\t\treturn { status: 200, body: { status: 'confirmed' } };\n\t\t\tcase 'Notification':\n\t\t\t\tif (\n\t\t\t\t\tthis.options.subscriptionArn &&\n\t\t\t\t\trequest.subscriptionArn !== this.options.subscriptionArn\n\t\t\t\t) {\n\t\t\t\t\tthis.subscriber.logger.warn(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttopicArn: message.TopicArn,\n\t\t\t\t\t\t\tsubscriptionArn: request.subscriptionArn,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'Refused an SNS notification from a subscription the deploy did not make',\n\t\t\t\t\t);\n\t\t\t\t\treturn refuse(403, 'not the recorded subscription');\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tawait this.notify(message);\n\t\t\t\t\treturn { status: 200, body: { status: 'ok' } };\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Already logged. A 500 makes SNS retry, which is the point of\n\t\t\t\t\t// answering at all.\n\t\t\t\t\treturn refuse(500, (error as Error).name);\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn { status: 200, body: { status: 'ignored' } };\n\t\t}\n\t}\n\n\tprivate async notify(message: SnsHttpMessage): Promise<void> {\n\t\tconst requestId = message.MessageId;\n\t\tconst logger = this.subscriber.logger.child({ requestId });\n\n\t\tawait runWithRequestContext(\n\t\t\t{\n\t\t\t\tlogger,\n\t\t\t\trequestId,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t\toperation: `subscriber ${this.subscriber.topicName ?? 'sns'}`,\n\t\t\t},\n\t\t\t// A CONSUMER span, child of the publisher's: SNS hands the trace\n\t\t\t// context on in the message attributes it was published with.\n\t\t\t() =>\n\t\t\t\tconsumeTraced(\n\t\t\t\t\t{\n\t\t\t\t\t\tsystem: 'aws_sns',\n\t\t\t\t\t\tdestination:\n\t\t\t\t\t\t\tthis.subscriber.topicName ??\n\t\t\t\t\t\t\tmessage.TopicArn.split(':').pop() ??\n\t\t\t\t\t\t\tmessage.TopicArn,\n\t\t\t\t\t\ttype: message.MessageAttributes?.type?.Value,\n\t\t\t\t\t\tmessageId: message.MessageId,\n\t\t\t\t\t},\n\t\t\t\t\tcarrierFromAttributes(message.MessageAttributes),\n\t\t\t\t\t() => this.run(message, logger),\n\t\t\t\t),\n\t\t);\n\t}\n\n\tprivate async run(\n\t\tmessage: SnsHttpMessage,\n\t\tlogger: Subscriber<any, any, any, any, any, any, any>['logger'],\n\t): Promise<void> {\n\t\ttry {\n\t\t\tconst { services, db } = await subscriberContext(\n\t\t\t\tthis.subscriber,\n\t\t\t\tthis.envParser,\n\t\t\t);\n\t\t\tconst events = await subscribedEvents(\n\t\t\t\tthis.subscriber,\n\t\t\t\ttoSnsEvent(message),\n\t\t\t\tlogger,\n\t\t\t);\n\t\t\tawait runSubscriber(this.subscriber, {\n\t\t\t\tevents,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as object, 'Error processing subscriber');\n\t\t\tthrow wrapError(error);\n\t\t}\n\t}\n}\n\n/** `url`, sent to `endpoint`'s origin instead of its own. */\nfunction throughEndpoint(url: string, endpoint: string): string {\n\tconst target = new URL(url);\n\tconst origin = new URL(endpoint);\n\ttarget.protocol = origin.protocol;\n\ttarget.host = origin.host;\n\treturn target.toString();\n}\n\nfunction refuse(\n\tstatus: SnsPushResponse['status'],\n\terror: string,\n): SnsPushResponse {\n\treturn { status, body: { status: 'refused', error } };\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
- import { t as Subscriber } from "../Subscriber-CEt7kHkc.cjs";
2
- import { t as SubscriberBuilder } from "../SubscriberBuilder-BRa_nrmH.cjs";
1
+ import { t as Subscriber } from "../Subscriber-BdCtfFXL.cjs";
2
+ import { a as ObjectSubscriberHasNoEvents, i as ObjectSubscriberHandler, n as ObjectSubscriberBuilder, o as SubscriberBuilder, r as ObjectSubscriberContext, t as ObjectSubscriber } from "../ObjectSubscriberBuilder-l-64yUiF.cjs";
3
3
  import { EnvironmentParser } from "@geekmidas/envkit";
4
+ import { SnsHttpMessage } from "@geekmidas/events/sns";
4
5
  //#region src/subscribers/SnsPushSubscriberAdaptor.d.ts
5
6
  interface SnsPushOptions {
6
7
  /** The topic this subscriber is subscribed to. Anything else is refused. */
@@ -19,6 +20,28 @@ interface SnsPushOptions {
19
20
  * from the request.
20
21
  */
21
22
  emulatorEndpoint?: string;
23
+ /**
24
+ * The subscription the deploy made for this route, as it recorded it
25
+ * (`GKM_SNS_SUBSCRIPTIONS`). A notification naming another one in its
26
+ * `x-amz-sns-subscription-arn` header is refused — a subscription someone
27
+ * added by hand, pointing at this route.
28
+ *
29
+ * Defence in depth, not authentication: SNS does not sign its headers.
30
+ * What authenticates a message is its signature and its `TopicArn`.
31
+ */
32
+ subscriptionArn?: string;
33
+ /**
34
+ * How a confirmation is confirmed. By visiting its `SubscribeURL` when
35
+ * unset — what an emulator takes. Against SNS itself, the server confirms
36
+ * through the API with `AuthenticateOnUnsubscribe`, so the `UnsubscribeURL`
37
+ * every notification carries cannot cut the route off.
38
+ */
39
+ confirm?: (message: SnsHttpMessage) => Promise<unknown>;
40
+ }
41
+ /** What SNS said about a request, outside its signed body. */
42
+ interface SnsPushRequest {
43
+ /** The `x-amz-sns-subscription-arn` header. */
44
+ subscriptionArn?: string;
22
45
  }
23
46
  /** What to answer SNS with. A non-2xx tells SNS to retry. */
24
47
  interface SnsPushResponse {
@@ -42,10 +65,10 @@ export declare class SnsPushSubscriberAdaptor {
42
65
  readonly subscriber: Subscriber<any, any, any, any, any, any, any>;
43
66
  private readonly options;
44
67
  constructor(envParser: EnvironmentParser<{}>, subscriber: Subscriber<any, any, any, any, any, any, any>, options: SnsPushOptions);
45
- handle(body: unknown): Promise<SnsPushResponse>;
68
+ handle(body: unknown, request?: SnsPushRequest): Promise<SnsPushResponse>;
46
69
  private notify;
47
70
  private run;
48
71
  }
49
72
  //#endregion
50
- export { type SnsPushOptions, type SnsPushResponse, Subscriber, SubscriberBuilder };
73
+ export { type ObjectSubscriber, ObjectSubscriberBuilder, type ObjectSubscriberContext, type ObjectSubscriberHandler, ObjectSubscriberHasNoEvents, type SnsPushOptions, type SnsPushRequest, type SnsPushResponse, Subscriber, SubscriberBuilder };
51
74
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/subscribers/SnsPushSubscriberAdaptor.ts"],"mappings":";;;;UAiBiB;;EAEhB;;;;;;EAMA;;;;;;;;EAQA;;;UAIgB;EAChB;EACA;IAAQ;IAAgB;;;;;;;;;;;;qBAYZ;mBAEM;WACR,YAAY;mBACJ;EAHlB,YACkB,WAAW,uBACnB,YAAY,+CACJ,SAAS;EAGrB,OAAO,gBAAgB,QAAQ;UAuDvB;UA8BA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/subscribers/SnsPushSubscriberAdaptor.ts"],"mappings":";;;;;UAiBiB;;EAEhB;;;;;;EAMA;;;;;;;;EAQA;;;;;;;;;;EAUA;;;;;;;EAOA,WAAW,SAAS,mBAAmB;;;UAIvB;;EAEhB;;;UAIgB;EAChB;EACA;IAAQ;IAAgB;;;;;;;;;;;;qBAYZ;mBAEM;WACR,YAAY;mBACJ;EAHlB,YACkB,WAAW,uBACnB,YAAY,+CACJ,SAAS;EAGrB,OACL,eACA,UAAS,iBACP,QAAQ;UA4EG;UA8BA"}
@@ -1,6 +1,7 @@
1
- import { t as Subscriber } from "../Subscriber-dsxc5LKa.mjs";
2
- import { t as SubscriberBuilder } from "../SubscriberBuilder-D9bkrYzH.mjs";
1
+ import { t as Subscriber } from "../Subscriber-DDo4HFNn.mjs";
2
+ import { a as ObjectSubscriberHasNoEvents, i as ObjectSubscriberHandler, n as ObjectSubscriberBuilder, o as SubscriberBuilder, r as ObjectSubscriberContext, t as ObjectSubscriber } from "../ObjectSubscriberBuilder-HmBwFn2o.mjs";
3
3
  import { EnvironmentParser } from "@geekmidas/envkit";
4
+ import { SnsHttpMessage } from "@geekmidas/events/sns";
4
5
  //#region src/subscribers/SnsPushSubscriberAdaptor.d.ts
5
6
  interface SnsPushOptions {
6
7
  /** The topic this subscriber is subscribed to. Anything else is refused. */
@@ -19,6 +20,28 @@ interface SnsPushOptions {
19
20
  * from the request.
20
21
  */
21
22
  emulatorEndpoint?: string;
23
+ /**
24
+ * The subscription the deploy made for this route, as it recorded it
25
+ * (`GKM_SNS_SUBSCRIPTIONS`). A notification naming another one in its
26
+ * `x-amz-sns-subscription-arn` header is refused — a subscription someone
27
+ * added by hand, pointing at this route.
28
+ *
29
+ * Defence in depth, not authentication: SNS does not sign its headers.
30
+ * What authenticates a message is its signature and its `TopicArn`.
31
+ */
32
+ subscriptionArn?: string;
33
+ /**
34
+ * How a confirmation is confirmed. By visiting its `SubscribeURL` when
35
+ * unset — what an emulator takes. Against SNS itself, the server confirms
36
+ * through the API with `AuthenticateOnUnsubscribe`, so the `UnsubscribeURL`
37
+ * every notification carries cannot cut the route off.
38
+ */
39
+ confirm?: (message: SnsHttpMessage) => Promise<unknown>;
40
+ }
41
+ /** What SNS said about a request, outside its signed body. */
42
+ interface SnsPushRequest {
43
+ /** The `x-amz-sns-subscription-arn` header. */
44
+ subscriptionArn?: string;
22
45
  }
23
46
  /** What to answer SNS with. A non-2xx tells SNS to retry. */
24
47
  interface SnsPushResponse {
@@ -42,10 +65,10 @@ export declare class SnsPushSubscriberAdaptor {
42
65
  readonly subscriber: Subscriber<any, any, any, any, any, any, any>;
43
66
  private readonly options;
44
67
  constructor(envParser: EnvironmentParser<{}>, subscriber: Subscriber<any, any, any, any, any, any, any>, options: SnsPushOptions);
45
- handle(body: unknown): Promise<SnsPushResponse>;
68
+ handle(body: unknown, request?: SnsPushRequest): Promise<SnsPushResponse>;
46
69
  private notify;
47
70
  private run;
48
71
  }
49
72
  //#endregion
50
- export { type SnsPushOptions, type SnsPushResponse, Subscriber, SubscriberBuilder };
73
+ export { type ObjectSubscriber, ObjectSubscriberBuilder, type ObjectSubscriberContext, type ObjectSubscriberHandler, ObjectSubscriberHasNoEvents, type SnsPushOptions, type SnsPushRequest, type SnsPushResponse, Subscriber, SubscriberBuilder };
51
74
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/subscribers/SnsPushSubscriberAdaptor.ts"],"mappings":";;;;UAiBiB;;EAEhB;;;;;;EAMA;;;;;;;;EAQA;;;UAIgB;EAChB;EACA;IAAQ;IAAgB;;;;;;;;;;;;qBAYZ;mBAEM;WACR,YAAY;mBACJ;EAHlB,YACkB,WAAW,uBACnB,YAAY,+CACJ,SAAS;EAGrB,OAAO,gBAAgB,QAAQ;UAuDvB;UA8BA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/subscribers/SnsPushSubscriberAdaptor.ts"],"mappings":";;;;;UAiBiB;;EAEhB;;;;;;EAMA;;;;;;;;EAQA;;;;;;;;;;EAUA;;;;;;;EAOA,WAAW,SAAS,mBAAmB;;;UAIvB;;EAEhB;;;UAIgB;EAChB;EACA;IAAQ;IAAgB;;;;;;;;;;;;qBAYZ;mBAEM;WACR,YAAY;mBACJ;EAHlB,YACkB,WAAW,uBACnB,YAAY,+CACJ,SAAS;EAGrB,OACL,eACA,UAAS,iBACP,QAAQ;UA4EG;UA8BA"}
@@ -1,5 +1,5 @@
1
- import { n as Subscriber, t as SubscriberBuilder } from "../SubscriberBuilder-DRXfxWn_.mjs";
2
- import { n as subscribedEvents, r as subscriberContext, t as runSubscriber } from "../runSubscriber-BHvJ8POr.mjs";
1
+ import { i as ObjectSubscriberHasNoEvents, n as Subscriber, r as ObjectSubscriberBuilder, t as SubscriberBuilder } from "../SubscriberBuilder-3msj1U2w.mjs";
2
+ import { n as subscribedEvents, r as subscriberContext, t as runSubscriber } from "../runSubscriber-JCgkeYbE.mjs";
3
3
  import { carrierFromAttributes, consumeTraced } from "@geekmidas/events";
4
4
  import { runWithRequestContext } from "@geekmidas/services";
5
5
  import { wrapError } from "@geekmidas/errors";
@@ -23,7 +23,7 @@ var SnsPushSubscriberAdaptor = class {
23
23
  this.subscriber = subscriber;
24
24
  this.options = options;
25
25
  }
26
- async handle(body) {
26
+ async handle(body, request = {}) {
27
27
  const message = body;
28
28
  if (typeof message?.Type !== "string" || !message.TopicArn) return refuse(400, "not an SNS message");
29
29
  if (message.TopicArn !== this.options.topicArn) return refuse(403, `not subscribed to ${message.TopicArn}`);
@@ -38,6 +38,14 @@ var SnsPushSubscriberAdaptor = class {
38
38
  }
39
39
  switch (message.Type) {
40
40
  case "SubscriptionConfirmation":
41
+ if (this.options.confirm) {
42
+ await this.options.confirm(message);
43
+ this.subscriber.logger.info({ topicArn: message.TopicArn }, "Confirmed SNS subscription");
44
+ return {
45
+ status: 200,
46
+ body: { status: "confirmed" }
47
+ };
48
+ }
41
49
  await confirmSnsSubscription(this.options.emulatorEndpoint && message.SubscribeURL ? {
42
50
  ...message,
43
51
  SubscribeURL: throughEndpoint(message.SubscribeURL, this.options.emulatorEndpoint)
@@ -47,15 +55,23 @@ var SnsPushSubscriberAdaptor = class {
47
55
  status: 200,
48
56
  body: { status: "confirmed" }
49
57
  };
50
- case "Notification": try {
51
- await this.notify(message);
52
- return {
53
- status: 200,
54
- body: { status: "ok" }
55
- };
56
- } catch (error) {
57
- return refuse(500, error.name);
58
- }
58
+ case "Notification":
59
+ if (this.options.subscriptionArn && request.subscriptionArn !== this.options.subscriptionArn) {
60
+ this.subscriber.logger.warn({
61
+ topicArn: message.TopicArn,
62
+ subscriptionArn: request.subscriptionArn
63
+ }, "Refused an SNS notification from a subscription the deploy did not make");
64
+ return refuse(403, "not the recorded subscription");
65
+ }
66
+ try {
67
+ await this.notify(message);
68
+ return {
69
+ status: 200,
70
+ body: { status: "ok" }
71
+ };
72
+ } catch (error) {
73
+ return refuse(500, error.name);
74
+ }
59
75
  default: return {
60
76
  status: 200,
61
77
  body: { status: "ignored" }
@@ -80,7 +96,7 @@ var SnsPushSubscriberAdaptor = class {
80
96
  async run(message, logger) {
81
97
  try {
82
98
  const { services, db } = await subscriberContext(this.subscriber, this.envParser);
83
- const events = subscribedEvents(this.subscriber, toSnsEvent(message), logger);
99
+ const events = await subscribedEvents(this.subscriber, toSnsEvent(message), logger);
84
100
  await runSubscriber(this.subscriber, {
85
101
  events,
86
102
  services,
@@ -111,6 +127,6 @@ function refuse(status, error) {
111
127
  };
112
128
  }
113
129
  //#endregion
114
- export { SnsPushSubscriberAdaptor, Subscriber, SubscriberBuilder };
130
+ export { ObjectSubscriberBuilder, ObjectSubscriberHasNoEvents, SnsPushSubscriberAdaptor, Subscriber, SubscriberBuilder };
115
131
 
116
132
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/subscribers/SnsPushSubscriberAdaptor.ts"],"sourcesContent":["import type { EnvironmentParser } from '@geekmidas/envkit';\nimport { wrapError } from '@geekmidas/errors';\nimport { carrierFromAttributes, consumeTraced } from '@geekmidas/events';\nimport {\n\tconfirmSnsSubscription,\n\ttype SnsHttpMessage,\n\ttoSnsEvent,\n\tverifySnsMessage,\n} from '@geekmidas/events/sns';\nimport { runWithRequestContext } from '@geekmidas/services';\nimport {\n\trunSubscriber,\n\tsubscribedEvents,\n\tsubscriberContext,\n} from './runSubscriber';\nimport type { Subscriber } from './Subscriber';\n\nexport interface SnsPushOptions {\n\t/** The topic this subscriber is subscribed to. Anything else is refused. */\n\ttopicArn: string;\n\t/**\n\t * Verify each message's signature. Off only against an emulator, which\n\t * signs nothing — decided by the connection string's endpoint, never by\n\t * anything the request says about itself.\n\t */\n\tverify: boolean;\n\t/**\n\t * The emulator's address, as this server was configured with it. An\n\t * emulator names itself in `SubscribeURL` by the port it listens on inside\n\t * its container, which is not the port published to the host — so against\n\t * one, the confirmation is sent here instead. From configuration, never\n\t * from the request.\n\t */\n\temulatorEndpoint?: string;\n}\n\n/** What to answer SNS with. A non-2xx tells SNS to retry. */\nexport interface SnsPushResponse {\n\tstatus: 200 | 400 | 403 | 500;\n\tbody: { status: string; error?: string };\n}\n\n/**\n * A topic subscriber run by SNS pushing to an HTTP endpoint, rather than by a\n * poller.\n *\n * The notification is read as the Lambda event SNS would have invoked a\n * function with, and run through the same parsing, services and handler as the\n * Lambda adaptor — without its middy wrapper, which is Lambda's alone: a\n * server serving this route has no reason to install `@middy/core`.\n */\nexport class SnsPushSubscriberAdaptor {\n\tconstructor(\n\t\tprivate readonly envParser: EnvironmentParser<{}>,\n\t\treadonly subscriber: Subscriber<any, any, any, any, any, any, any>,\n\t\tprivate readonly options: SnsPushOptions,\n\t) {}\n\n\tasync handle(body: unknown): Promise<SnsPushResponse> {\n\t\tconst message = body as SnsHttpMessage;\n\t\tif (typeof message?.Type !== 'string' || !message.TopicArn) {\n\t\t\treturn refuse(400, 'not an SNS message');\n\t\t}\n\t\t// Checked before the signature: a genuine message for another topic is\n\t\t// still not this subscriber's to confirm or to handle.\n\t\tif (message.TopicArn !== this.options.topicArn) {\n\t\t\treturn refuse(403, `not subscribed to ${message.TopicArn}`);\n\t\t}\n\n\t\tif (this.options.verify) {\n\t\t\ttry {\n\t\t\t\tawait verifySnsMessage(message);\n\t\t\t} catch (error) {\n\t\t\t\tthis.subscriber.logger.warn(\n\t\t\t\t\t{ err: error, topicArn: message.TopicArn },\n\t\t\t\t\t'Refused an SNS message that did not verify',\n\t\t\t\t);\n\t\t\t\treturn refuse(403, (error as Error).name);\n\t\t\t}\n\t\t}\n\n\t\tswitch (message.Type) {\n\t\t\tcase 'SubscriptionConfirmation':\n\t\t\t\tawait confirmSnsSubscription(\n\t\t\t\t\tthis.options.emulatorEndpoint && message.SubscribeURL\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t...message,\n\t\t\t\t\t\t\t\tSubscribeURL: throughEndpoint(\n\t\t\t\t\t\t\t\t\tmessage.SubscribeURL,\n\t\t\t\t\t\t\t\t\tthis.options.emulatorEndpoint,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: message,\n\t\t\t\t);\n\t\t\t\tthis.subscriber.logger.info(\n\t\t\t\t\t{ topicArn: message.TopicArn },\n\t\t\t\t\t'Confirmed SNS subscription',\n\t\t\t\t);\n\t\t\t\treturn { status: 200, body: { status: 'confirmed' } };\n\t\t\tcase 'Notification':\n\t\t\t\ttry {\n\t\t\t\t\tawait this.notify(message);\n\t\t\t\t\treturn { status: 200, body: { status: 'ok' } };\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Already logged. A 500 makes SNS retry, which is the point of\n\t\t\t\t\t// answering at all.\n\t\t\t\t\treturn refuse(500, (error as Error).name);\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn { status: 200, body: { status: 'ignored' } };\n\t\t}\n\t}\n\n\tprivate async notify(message: SnsHttpMessage): Promise<void> {\n\t\tconst requestId = message.MessageId;\n\t\tconst logger = this.subscriber.logger.child({ requestId });\n\n\t\tawait runWithRequestContext(\n\t\t\t{\n\t\t\t\tlogger,\n\t\t\t\trequestId,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t\toperation: `subscriber ${this.subscriber.topicName ?? 'sns'}`,\n\t\t\t},\n\t\t\t// A CONSUMER span, child of the publisher's: SNS hands the trace\n\t\t\t// context on in the message attributes it was published with.\n\t\t\t() =>\n\t\t\t\tconsumeTraced(\n\t\t\t\t\t{\n\t\t\t\t\t\tsystem: 'aws_sns',\n\t\t\t\t\t\tdestination:\n\t\t\t\t\t\t\tthis.subscriber.topicName ??\n\t\t\t\t\t\t\tmessage.TopicArn.split(':').pop() ??\n\t\t\t\t\t\t\tmessage.TopicArn,\n\t\t\t\t\t\ttype: message.MessageAttributes?.type?.Value,\n\t\t\t\t\t\tmessageId: message.MessageId,\n\t\t\t\t\t},\n\t\t\t\t\tcarrierFromAttributes(message.MessageAttributes),\n\t\t\t\t\t() => this.run(message, logger),\n\t\t\t\t),\n\t\t);\n\t}\n\n\tprivate async run(\n\t\tmessage: SnsHttpMessage,\n\t\tlogger: Subscriber<any, any, any, any, any, any, any>['logger'],\n\t): Promise<void> {\n\t\ttry {\n\t\t\tconst { services, db } = await subscriberContext(\n\t\t\t\tthis.subscriber,\n\t\t\t\tthis.envParser,\n\t\t\t);\n\t\t\tconst events = subscribedEvents(\n\t\t\t\tthis.subscriber,\n\t\t\t\ttoSnsEvent(message),\n\t\t\t\tlogger,\n\t\t\t);\n\t\t\tawait runSubscriber(this.subscriber, {\n\t\t\t\tevents,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as object, 'Error processing subscriber');\n\t\t\tthrow wrapError(error);\n\t\t}\n\t}\n}\n\n/** `url`, sent to `endpoint`'s origin instead of its own. */\nfunction throughEndpoint(url: string, endpoint: string): string {\n\tconst target = new URL(url);\n\tconst origin = new URL(endpoint);\n\ttarget.protocol = origin.protocol;\n\ttarget.host = origin.host;\n\treturn target.toString();\n}\n\nfunction refuse(\n\tstatus: SnsPushResponse['status'],\n\terror: string,\n): SnsPushResponse {\n\treturn { status, body: { status: 'refused', error } };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAmDA,IAAa,2BAAb,MAAsC;CAEnB;CACR;CACQ;CAHlB,YACC,WACA,YACA,SACC;EAHgB,KAAA,YAAA;EACR,KAAA,aAAA;EACQ,KAAA,UAAA;CACf;CAEH,MAAM,OAAO,MAAyC;EACrD,MAAM,UAAU;EAChB,IAAI,OAAO,SAAS,SAAS,YAAY,CAAC,QAAQ,UACjD,OAAO,OAAO,KAAK,oBAAoB;EAIxC,IAAI,QAAQ,aAAa,KAAK,QAAQ,UACrC,OAAO,OAAO,KAAK,qBAAqB,QAAQ,UAAU;EAG3D,IAAI,KAAK,QAAQ,QAChB,IAAI;GACH,MAAM,iBAAiB,OAAO;EAC/B,SAAS,OAAO;GACf,KAAK,WAAW,OAAO,KACtB;IAAE,KAAK;IAAO,UAAU,QAAQ;GAAS,GACzC,4CACD;GACA,OAAO,OAAO,KAAM,MAAgB,IAAI;EACzC;EAGD,QAAQ,QAAQ,MAAhB;GACC,KAAK;IACJ,MAAM,uBACL,KAAK,QAAQ,oBAAoB,QAAQ,eACtC;KACA,GAAG;KACH,cAAc,gBACb,QAAQ,cACR,KAAK,QAAQ,gBACd;IACD,IACC,OACJ;IACA,KAAK,WAAW,OAAO,KACtB,EAAE,UAAU,QAAQ,SAAS,GAC7B,4BACD;IACA,OAAO;KAAE,QAAQ;KAAK,MAAM,EAAE,QAAQ,YAAY;IAAE;GACrD,KAAK,gBACJ,IAAI;IACH,MAAM,KAAK,OAAO,OAAO;IACzB,OAAO;KAAE,QAAQ;KAAK,MAAM,EAAE,QAAQ,KAAK;IAAE;GAC9C,SAAS,OAAO;IAGf,OAAO,OAAO,KAAM,MAAgB,IAAI;GACzC;GACD,SACC,OAAO;IAAE,QAAQ;IAAK,MAAM,EAAE,QAAQ,UAAU;GAAE;EACpD;CACD;CAEA,MAAc,OAAO,SAAwC;EAC5D,MAAM,YAAY,QAAQ;EAC1B,MAAM,SAAS,KAAK,WAAW,OAAO,MAAM,EAAE,UAAU,CAAC;EAEzD,MAAM,sBACL;GACC;GACA;GACA,WAAW,KAAK,IAAI;GACpB,WAAW,cAAc,KAAK,WAAW,aAAa;EACvD,SAIC,cACC;GACC,QAAQ;GACR,aACC,KAAK,WAAW,aAChB,QAAQ,SAAS,MAAM,GAAG,CAAC,CAAC,IAAI,KAChC,QAAQ;GACT,MAAM,QAAQ,mBAAmB,MAAM;GACvC,WAAW,QAAQ;EACpB,GACA,sBAAsB,QAAQ,iBAAiB,SACzC,KAAK,IAAI,SAAS,MAAM,CAC/B,CACF;CACD;CAEA,MAAc,IACb,SACA,QACgB;EAChB,IAAI;GACH,MAAM,EAAE,UAAU,OAAO,MAAM,kBAC9B,KAAK,YACL,KAAK,SACN;GACA,MAAM,SAAS,iBACd,KAAK,YACL,WAAW,OAAO,GAClB,MACD;GACA,MAAM,cAAc,KAAK,YAAY;IACpC;IACA;IACA;IACA;GACD,CAAC;EACF,SAAS,OAAO;GACf,OAAO,MAAM,OAAiB,6BAA6B;GAC3D,MAAM,UAAU,KAAK;EACtB;CACD;AACD;;AAGA,SAAS,gBAAgB,KAAa,UAA0B;CAC/D,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,SAAS,IAAI,IAAI,QAAQ;CAC/B,OAAO,WAAW,OAAO;CACzB,OAAO,OAAO,OAAO;CACrB,OAAO,OAAO,SAAS;AACxB;AAEA,SAAS,OACR,QACA,OACkB;CAClB,OAAO;EAAE;EAAQ,MAAM;GAAE,QAAQ;GAAW;EAAM;CAAE;AACrD"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/subscribers/SnsPushSubscriberAdaptor.ts"],"sourcesContent":["import type { EnvironmentParser } from '@geekmidas/envkit';\nimport { wrapError } from '@geekmidas/errors';\nimport { carrierFromAttributes, consumeTraced } from '@geekmidas/events';\nimport {\n\tconfirmSnsSubscription,\n\ttype SnsHttpMessage,\n\ttoSnsEvent,\n\tverifySnsMessage,\n} from '@geekmidas/events/sns';\nimport { runWithRequestContext } from '@geekmidas/services';\nimport {\n\trunSubscriber,\n\tsubscribedEvents,\n\tsubscriberContext,\n} from './runSubscriber';\nimport type { Subscriber } from './Subscriber';\n\nexport interface SnsPushOptions {\n\t/** The topic this subscriber is subscribed to. Anything else is refused. */\n\ttopicArn: string;\n\t/**\n\t * Verify each message's signature. Off only against an emulator, which\n\t * signs nothing — decided by the connection string's endpoint, never by\n\t * anything the request says about itself.\n\t */\n\tverify: boolean;\n\t/**\n\t * The emulator's address, as this server was configured with it. An\n\t * emulator names itself in `SubscribeURL` by the port it listens on inside\n\t * its container, which is not the port published to the host — so against\n\t * one, the confirmation is sent here instead. From configuration, never\n\t * from the request.\n\t */\n\temulatorEndpoint?: string;\n\t/**\n\t * The subscription the deploy made for this route, as it recorded it\n\t * (`GKM_SNS_SUBSCRIPTIONS`). A notification naming another one in its\n\t * `x-amz-sns-subscription-arn` header is refused — a subscription someone\n\t * added by hand, pointing at this route.\n\t *\n\t * Defence in depth, not authentication: SNS does not sign its headers.\n\t * What authenticates a message is its signature and its `TopicArn`.\n\t */\n\tsubscriptionArn?: string;\n\t/**\n\t * How a confirmation is confirmed. By visiting its `SubscribeURL` when\n\t * unset — what an emulator takes. Against SNS itself, the server confirms\n\t * through the API with `AuthenticateOnUnsubscribe`, so the `UnsubscribeURL`\n\t * every notification carries cannot cut the route off.\n\t */\n\tconfirm?: (message: SnsHttpMessage) => Promise<unknown>;\n}\n\n/** What SNS said about a request, outside its signed body. */\nexport interface SnsPushRequest {\n\t/** The `x-amz-sns-subscription-arn` header. */\n\tsubscriptionArn?: string;\n}\n\n/** What to answer SNS with. A non-2xx tells SNS to retry. */\nexport interface SnsPushResponse {\n\tstatus: 200 | 400 | 403 | 500;\n\tbody: { status: string; error?: string };\n}\n\n/**\n * A topic subscriber run by SNS pushing to an HTTP endpoint, rather than by a\n * poller.\n *\n * The notification is read as the Lambda event SNS would have invoked a\n * function with, and run through the same parsing, services and handler as the\n * Lambda adaptor — without its middy wrapper, which is Lambda's alone: a\n * server serving this route has no reason to install `@middy/core`.\n */\nexport class SnsPushSubscriberAdaptor {\n\tconstructor(\n\t\tprivate readonly envParser: EnvironmentParser<{}>,\n\t\treadonly subscriber: Subscriber<any, any, any, any, any, any, any>,\n\t\tprivate readonly options: SnsPushOptions,\n\t) {}\n\n\tasync handle(\n\t\tbody: unknown,\n\t\trequest: SnsPushRequest = {},\n\t): Promise<SnsPushResponse> {\n\t\tconst message = body as SnsHttpMessage;\n\t\tif (typeof message?.Type !== 'string' || !message.TopicArn) {\n\t\t\treturn refuse(400, 'not an SNS message');\n\t\t}\n\t\t// Checked before the signature: a genuine message for another topic is\n\t\t// still not this subscriber's to confirm or to handle.\n\t\tif (message.TopicArn !== this.options.topicArn) {\n\t\t\treturn refuse(403, `not subscribed to ${message.TopicArn}`);\n\t\t}\n\n\t\tif (this.options.verify) {\n\t\t\ttry {\n\t\t\t\tawait verifySnsMessage(message);\n\t\t\t} catch (error) {\n\t\t\t\tthis.subscriber.logger.warn(\n\t\t\t\t\t{ err: error, topicArn: message.TopicArn },\n\t\t\t\t\t'Refused an SNS message that did not verify',\n\t\t\t\t);\n\t\t\t\treturn refuse(403, (error as Error).name);\n\t\t\t}\n\t\t}\n\n\t\tswitch (message.Type) {\n\t\t\tcase 'SubscriptionConfirmation':\n\t\t\t\tif (this.options.confirm) {\n\t\t\t\t\tawait this.options.confirm(message);\n\t\t\t\t\tthis.subscriber.logger.info(\n\t\t\t\t\t\t{ topicArn: message.TopicArn },\n\t\t\t\t\t\t'Confirmed SNS subscription',\n\t\t\t\t\t);\n\t\t\t\t\treturn { status: 200, body: { status: 'confirmed' } };\n\t\t\t\t}\n\t\t\t\tawait confirmSnsSubscription(\n\t\t\t\t\tthis.options.emulatorEndpoint && message.SubscribeURL\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t...message,\n\t\t\t\t\t\t\t\tSubscribeURL: throughEndpoint(\n\t\t\t\t\t\t\t\t\tmessage.SubscribeURL,\n\t\t\t\t\t\t\t\t\tthis.options.emulatorEndpoint,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: message,\n\t\t\t\t);\n\t\t\t\tthis.subscriber.logger.info(\n\t\t\t\t\t{ topicArn: message.TopicArn },\n\t\t\t\t\t'Confirmed SNS subscription',\n\t\t\t\t);\n\t\t\t\treturn { status: 200, body: { status: 'confirmed' } };\n\t\t\tcase 'Notification':\n\t\t\t\tif (\n\t\t\t\t\tthis.options.subscriptionArn &&\n\t\t\t\t\trequest.subscriptionArn !== this.options.subscriptionArn\n\t\t\t\t) {\n\t\t\t\t\tthis.subscriber.logger.warn(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttopicArn: message.TopicArn,\n\t\t\t\t\t\t\tsubscriptionArn: request.subscriptionArn,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t'Refused an SNS notification from a subscription the deploy did not make',\n\t\t\t\t\t);\n\t\t\t\t\treturn refuse(403, 'not the recorded subscription');\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tawait this.notify(message);\n\t\t\t\t\treturn { status: 200, body: { status: 'ok' } };\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Already logged. A 500 makes SNS retry, which is the point of\n\t\t\t\t\t// answering at all.\n\t\t\t\t\treturn refuse(500, (error as Error).name);\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn { status: 200, body: { status: 'ignored' } };\n\t\t}\n\t}\n\n\tprivate async notify(message: SnsHttpMessage): Promise<void> {\n\t\tconst requestId = message.MessageId;\n\t\tconst logger = this.subscriber.logger.child({ requestId });\n\n\t\tawait runWithRequestContext(\n\t\t\t{\n\t\t\t\tlogger,\n\t\t\t\trequestId,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t\toperation: `subscriber ${this.subscriber.topicName ?? 'sns'}`,\n\t\t\t},\n\t\t\t// A CONSUMER span, child of the publisher's: SNS hands the trace\n\t\t\t// context on in the message attributes it was published with.\n\t\t\t() =>\n\t\t\t\tconsumeTraced(\n\t\t\t\t\t{\n\t\t\t\t\t\tsystem: 'aws_sns',\n\t\t\t\t\t\tdestination:\n\t\t\t\t\t\t\tthis.subscriber.topicName ??\n\t\t\t\t\t\t\tmessage.TopicArn.split(':').pop() ??\n\t\t\t\t\t\t\tmessage.TopicArn,\n\t\t\t\t\t\ttype: message.MessageAttributes?.type?.Value,\n\t\t\t\t\t\tmessageId: message.MessageId,\n\t\t\t\t\t},\n\t\t\t\t\tcarrierFromAttributes(message.MessageAttributes),\n\t\t\t\t\t() => this.run(message, logger),\n\t\t\t\t),\n\t\t);\n\t}\n\n\tprivate async run(\n\t\tmessage: SnsHttpMessage,\n\t\tlogger: Subscriber<any, any, any, any, any, any, any>['logger'],\n\t): Promise<void> {\n\t\ttry {\n\t\t\tconst { services, db } = await subscriberContext(\n\t\t\t\tthis.subscriber,\n\t\t\t\tthis.envParser,\n\t\t\t);\n\t\t\tconst events = await subscribedEvents(\n\t\t\t\tthis.subscriber,\n\t\t\t\ttoSnsEvent(message),\n\t\t\t\tlogger,\n\t\t\t);\n\t\t\tawait runSubscriber(this.subscriber, {\n\t\t\t\tevents,\n\t\t\t\tservices,\n\t\t\t\tlogger,\n\t\t\t\tdb,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as object, 'Error processing subscriber');\n\t\t\tthrow wrapError(error);\n\t\t}\n\t}\n}\n\n/** `url`, sent to `endpoint`'s origin instead of its own. */\nfunction throughEndpoint(url: string, endpoint: string): string {\n\tconst target = new URL(url);\n\tconst origin = new URL(endpoint);\n\ttarget.protocol = origin.protocol;\n\ttarget.host = origin.host;\n\treturn target.toString();\n}\n\nfunction refuse(\n\tstatus: SnsPushResponse['status'],\n\terror: string,\n): SnsPushResponse {\n\treturn { status, body: { status: 'refused', error } };\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_Topic = require("../Topic-DwS3QO0m.cjs");
2
+ const require_Topic = require("../Topic-Dp9Q7Lj2.cjs");
3
3
  exports.Topic = require_Topic.Topic;
@@ -1,2 +1,2 @@
1
- import { c as Topic, d as TopicOptions, l as TopicEvents, u as TopicMessage } from "../Construct-CFW0zjdD.cjs";
1
+ import { c as Topic, d as TopicOptions, l as TopicEvents, u as TopicMessage } from "../Construct-127M7yrl.cjs";
2
2
  export { Topic, type TopicEvents, type TopicMessage, type TopicOptions };
@@ -1,2 +1,2 @@
1
- import { c as Topic, d as TopicOptions, l as TopicEvents, u as TopicMessage } from "../Construct-B7AXn0xH.mjs";
1
+ import { c as Topic, d as TopicOptions, l as TopicEvents, u as TopicMessage } from "../Construct-DySlwh8o.mjs";
2
2
  export { Topic, type TopicEvents, type TopicMessage, type TopicOptions };
@@ -1,2 +1,2 @@
1
- import { t as Topic } from "../Topic-D2R0r24C.mjs";
1
+ import { t as Topic } from "../Topic-DN2gYJ-p.mjs";
2
2
  export { Topic };