@geekmidas/events 10.0.0-alpha.39 → 10.0.0-alpha.40

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/pgboss/errors.ts","../../src/pgboss/PgBossConnection.ts","../../src/pgboss/PgBossPublisher.ts","../../src/pgboss/PgBossSubscriber.ts"],"mappings":";;;;qBAKa,yBAAyB;EACrC;;;;UCFgB;EAChB;EACA;;qBAGY,4BAA4B;UAKpB;WAJX,OAAI,mBAAA;UACL;UACA;EAER,YAAoB,QAAQ;;;;;SAMf,qBACZ,2BACE,QAAQ;EAgBL,WAAW;EA2BX,SAAS;EAUf;UAIQ;;;;MAQJ,YAAY;;;;qBCnFJ,gBAAgB,iBAAiB,4CAClC,eAAe;UAEN;EAApB,YAAoB,YAAY;;;;;SAMnB,qBACZ,iBAAiB,iCAChB,2BAA2B,QAAQ,gBAAgB;EAO/C,QAAQ,UAAU,aAAa;EAwB/B,SAAS;;;;UC1CC;EAChB;EACA;;qBAGY,iBAAiB,iBAAiB,4CACnC,gBAAgB;UAGlB;UACA;EAFT,YACS,YAAY,kBACZ,UAAS;;;;;SAOL,qBACZ,iBAAiB,iCAChB,2BAA2B,QAAQ,iBAAiB;EAoBhD,UACL,UAAU,oBACV,WAAW,SAAS,aAAa,gBAC/B"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/pgboss/errors.ts","../../src/pgboss/PgBossConnection.ts","../../src/pgboss/PgBossPublisher.ts","../../src/pgboss/PgBossSubscriber.ts"],"mappings":";;;;qBAKa,yBAAyB;EACrC;;;qBASY,oCAAoC;WAC3B;EAArB,YAAqB;;;;UCZL;EAChB;EACA;;qBAGY,4BAA4B;UAKpB;WAJX,OAAI,mBAAA;UACL;UACA;EAER,YAAoB,QAAQ;;;;;SAMf,qBACZ,2BACE,QAAQ;EAgBL,WAAW;EA2BX,SAAS;EAUf;UAIQ;;;;MAQJ,YAAY;;;;UCnFA;;;;;;EAMhB;;;wBAIe,WAAW,eAAe;qBAI7B,gBAAgB,iBAAiB,4CAClC,eAAe;UAGjB;UACA;EAFT,YACS,YAAY,kBACZ,UAAS;;;;;SAOL,qBACZ,iBAAiB,iCAEjB,0BACA,UAAS,yBACP,QAAQ,gBAAgB;EAOrB,QAAQ,UAAU,aAAa;EAsC/B,SAAS;;;;UC3EC;EAChB;EACA;;;;;;;;EAQA;;EAEA;;qBAGY,iBAAiB,iBAAiB,4CACnC,gBAAgB;UAGlB;UACA;EAFT,YACS,YAAY,kBACZ,UAAS;;;;;SAOL,qBACZ,iBAAiB,iCAChB,2BAA2B,QAAQ,iBAAiB;EAoBhD,UACL,UAAU,oBACV,WAAW,SAAS,aAAa,gBAC/B"}
@@ -8,6 +8,15 @@ var PgBossNotStarted = class extends Error {
8
8
  this.name = "PgBossNotStarted";
9
9
  }
10
10
  };
11
+ /** A topic subscription with no name to give the subscriber's own queue. */
12
+ var PgBossSubscriptionNeedsName = class extends Error {
13
+ topic;
14
+ constructor(topic) {
15
+ super(`Subscribing to topic '${topic}' on pg-boss needs a subscription name: each subscriber drains a queue of its own, and that queue is named for it. Pass \`subscription\`.`);
16
+ this.topic = topic;
17
+ this.name = "PgBossSubscriptionNeedsName";
18
+ }
19
+ };
11
20
  //#endregion
12
21
  //#region src/pgboss/PgBossConnection.ts
13
22
  var PgBossConnection = class PgBossConnection {
@@ -78,24 +87,38 @@ var PgBossConnection = class PgBossConnection {
78
87
  };
79
88
  //#endregion
80
89
  //#region src/pgboss/PgBossPublisher.ts
90
+ /** The pg-boss event a topic's message type is published as. */
91
+ function topicEvent(topic, type) {
92
+ return `${topic}/${type}`;
93
+ }
81
94
  var PgBossPublisher = class PgBossPublisher {
82
95
  connection;
83
- constructor(connection) {
96
+ options;
97
+ constructor(connection, options = {}) {
84
98
  this.connection = connection;
99
+ this.options = options;
85
100
  }
86
101
  /**
87
102
  * Create a PgBossPublisher from a connection string.
88
103
  * Format: pgboss://user:pass@host:5432/database?schema=pgboss
89
104
  */
90
- static async fromConnectionString(connectionString) {
105
+ static async fromConnectionString(connectionString, options = {}) {
91
106
  const { PgBossConnection } = await import("../PgBossConnection-DjoqT84r.mjs");
92
107
  const connection = await PgBossConnection.fromConnectionString(connectionString);
93
- return new PgBossPublisher(connection);
108
+ return new PgBossPublisher(connection, options);
94
109
  }
95
110
  async publish(messages) {
96
111
  if (!this.connection.isConnected()) await this.connection.connect();
97
112
  const boss = this.connection.instance;
98
113
  if (!boss) throw new PgBossNotStarted();
114
+ const { topic } = this.options;
115
+ if (topic) {
116
+ for (const m of messages) await boss.publish(topicEvent(topic, m.type), {
117
+ type: m.type,
118
+ payload: m.payload
119
+ });
120
+ return;
121
+ }
99
122
  const groups = /* @__PURE__ */ new Map();
100
123
  for (const m of messages) {
101
124
  const list = groups.get(m.type) ?? [];
@@ -136,12 +159,27 @@ var PgBossSubscriber = class PgBossSubscriber {
136
159
  if (!this.connection.isConnected()) await this.connection.connect();
137
160
  const boss = this.connection.instance;
138
161
  if (!boss) throw new PgBossNotStarted();
162
+ const work = {
163
+ ...this.options.batchSize && { batchSize: this.options.batchSize },
164
+ ...this.options.pollingIntervalSeconds && { pollingIntervalSeconds: this.options.pollingIntervalSeconds }
165
+ };
166
+ const { topic, subscription } = this.options;
167
+ if (topic) {
168
+ if (!subscription) throw new PgBossSubscriptionNeedsName(topic);
169
+ const queue = topicEvent(topic, subscription);
170
+ await boss.createQueue(queue);
171
+ for (const type of messages) await boss.subscribe(topicEvent(topic, type), queue);
172
+ await boss.work(queue, work, async (jobs) => {
173
+ for (const job of jobs) await listener({
174
+ type: job.data.type,
175
+ payload: job.data.payload
176
+ });
177
+ });
178
+ return;
179
+ }
139
180
  for (const messageType of messages) {
140
181
  await boss.createQueue(messageType);
141
- await boss.work(messageType, {
142
- ...this.options.batchSize && { batchSize: this.options.batchSize },
143
- ...this.options.pollingIntervalSeconds && { pollingIntervalSeconds: this.options.pollingIntervalSeconds }
144
- }, async (jobs) => {
182
+ await boss.work(messageType, work, async (jobs) => {
145
183
  for (const job of jobs) await listener({
146
184
  type: job.name,
147
185
  payload: job.data
@@ -151,6 +189,6 @@ var PgBossSubscriber = class PgBossSubscriber {
151
189
  }
152
190
  };
153
191
  //#endregion
154
- export { PgBossConnection, PgBossNotStarted, PgBossPublisher, PgBossSubscriber };
192
+ export { PgBossConnection, PgBossNotStarted, PgBossPublisher, PgBossSubscriber, PgBossSubscriptionNeedsName, topicEvent };
155
193
 
156
194
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/pgboss/errors.ts","../../src/pgboss/PgBossConnection.ts","../../src/pgboss/PgBossPublisher.ts","../../src/pgboss/PgBossSubscriber.ts"],"sourcesContent":["// Beside the adapter but apart from its connection module, which imports\n// the broker's client: the publisher and subscriber load that lazily, and\n// importing an error class from it would load it eagerly.\n\n/** pg-boss connected but its instance never started. */\nexport class PgBossNotStarted extends Error {\n\tconstructor() {\n\t\tsuper(\n\t\t\t'pg-boss did not start. Check the database in the connection string is reachable and the role may create the pg-boss schema.',\n\t\t);\n\t\tthis.name = 'PgBossNotStarted';\n\t}\n}\n","import { PgBoss } from 'pg-boss';\nimport type { EventConnection } from '../types';\nimport { EventPublisherType } from '../types';\n\nexport interface PgBossConnectionConfig {\n\tconnectionString: string;\n\tschema?: string;\n}\n\nexport class PgBossConnection implements EventConnection {\n\treadonly type = EventPublisherType.PgBoss;\n\tprivate boss?: PgBoss;\n\tprivate connecting?: Promise<void>;\n\n\tconstructor(private config: PgBossConnectionConfig) {}\n\n\t/**\n\t * Create a PgBossConnection from a connection string.\n\t * Format: pgboss://user:pass@host:5432/database?schema=pgboss\n\t */\n\tstatic async fromConnectionString(\n\t\tconnectionString: string,\n\t): Promise<PgBossConnection> {\n\t\tconst url = new URL(connectionString);\n\t\tconst params = url.searchParams;\n\n\t\tconst postgresUrl = `postgres://${url.username ? `${url.username}:${url.password}@` : ''}${url.host}${url.pathname}`;\n\n\t\tconst config: PgBossConnectionConfig = {\n\t\t\tconnectionString: postgresUrl,\n\t\t\tschema: params.get('schema') || undefined,\n\t\t};\n\n\t\tconst connection = new PgBossConnection(config);\n\t\tawait connection.connect();\n\t\treturn connection;\n\t}\n\n\tasync connect(): Promise<void> {\n\t\tif (this.boss) return;\n\n\t\tif (this.connecting) {\n\t\t\tawait this.connecting;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.connecting = (async () => {\n\t\t\ttry {\n\t\t\t\tconst boss = new PgBoss({\n\t\t\t\t\tconnectionString: this.config.connectionString,\n\t\t\t\t\t...(this.config.schema && { schema: this.config.schema }),\n\t\t\t\t});\n\n\t\t\t\tawait boss.start();\n\t\t\t\tthis.boss = boss;\n\t\t\t} catch (error) {\n\t\t\t\tthis.cleanup();\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t})();\n\n\t\tawait this.connecting;\n\t\tthis.connecting = undefined;\n\t}\n\n\tasync close(): Promise<void> {\n\t\ttry {\n\t\t\tawait this.boss?.stop({ graceful: true });\n\t\t} catch {\n\t\t\t// Ignore errors during close\n\t\t} finally {\n\t\t\tthis.cleanup();\n\t\t}\n\t}\n\n\tisConnected(): boolean {\n\t\treturn !!this.boss;\n\t}\n\n\tprivate cleanup(): void {\n\t\tthis.boss = undefined;\n\t\tthis.connecting = undefined;\n\t}\n\n\t/**\n\t * Get the underlying pg-boss instance for publishing/subscribing\n\t */\n\tget instance(): PgBoss | undefined {\n\t\treturn this.boss;\n\t}\n}\n","import type { EventPublisher, PublishableMessage } from '../types';\nimport { PgBossNotStarted } from './errors';\nimport type { PgBossConnection } from './PgBossConnection';\n\nexport class PgBossPublisher<TMessage extends PublishableMessage<string, any>>\n\timplements EventPublisher<TMessage>\n{\n\tconstructor(private connection: PgBossConnection) {}\n\n\t/**\n\t * Create a PgBossPublisher from a connection string.\n\t * Format: pgboss://user:pass@host:5432/database?schema=pgboss\n\t */\n\tstatic async fromConnectionString<\n\t\tTMessage extends PublishableMessage<string, any>,\n\t>(connectionString: string): Promise<PgBossPublisher<TMessage>> {\n\t\tconst { PgBossConnection } = await import('./PgBossConnection');\n\t\tconst connection =\n\t\t\tawait PgBossConnection.fromConnectionString(connectionString);\n\t\treturn new PgBossPublisher<TMessage>(connection);\n\t}\n\n\tasync publish(messages: TMessage[]): Promise<void> {\n\t\tif (!this.connection.isConnected()) {\n\t\t\tawait this.connection.connect();\n\t\t}\n\n\t\tconst boss = this.connection.instance;\n\t\tif (!boss) {\n\t\t\tthrow new PgBossNotStarted();\n\t\t}\n\n\t\t// Group jobs by queue name (v11+ requires per-queue insert calls)\n\t\tconst groups = new Map<string, { data: TMessage['payload'] }[]>();\n\t\tfor (const m of messages) {\n\t\t\tconst list = groups.get(m.type) ?? [];\n\t\t\tlist.push({ data: m.payload });\n\t\t\tgroups.set(m.type, list);\n\t\t}\n\n\t\tfor (const [name, jobs] of groups) {\n\t\t\tawait boss.createQueue(name);\n\t\t\tawait boss.insert(name, jobs);\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\t// Publisher doesn't own the connection\n\t}\n}\n","import type { EventSubscriber, PublishableMessage } from '../types';\nimport { PgBossNotStarted } from './errors';\nimport type { PgBossConnection } from './PgBossConnection';\n\nexport interface PgBossSubscriberOptions {\n\tbatchSize?: number;\n\tpollingIntervalSeconds?: number;\n}\n\nexport class PgBossSubscriber<TMessage extends PublishableMessage<string, any>>\n\timplements EventSubscriber<TMessage>\n{\n\tconstructor(\n\t\tprivate connection: PgBossConnection,\n\t\tprivate options: PgBossSubscriberOptions = {},\n\t) {}\n\n\t/**\n\t * Create a PgBossSubscriber from a connection string.\n\t * Format: pgboss://user:pass@host:5432/database?schema=pgboss&batchSize=5\n\t */\n\tstatic async fromConnectionString<\n\t\tTMessage extends PublishableMessage<string, any>,\n\t>(connectionString: string): Promise<PgBossSubscriber<TMessage>> {\n\t\tconst url = new URL(connectionString);\n\t\tconst params = url.searchParams;\n\n\t\tconst { PgBossConnection } = await import('./PgBossConnection');\n\t\tconst connection =\n\t\t\tawait PgBossConnection.fromConnectionString(connectionString);\n\n\t\tconst options: PgBossSubscriberOptions = {\n\t\t\tbatchSize: params.get('batchSize')\n\t\t\t\t? Number.parseInt(params.get('batchSize')!, 10)\n\t\t\t\t: undefined,\n\t\t\tpollingIntervalSeconds: params.get('pollingIntervalSeconds')\n\t\t\t\t? Number.parseInt(params.get('pollingIntervalSeconds')!, 10)\n\t\t\t\t: undefined,\n\t\t};\n\n\t\treturn new PgBossSubscriber<TMessage>(connection, options);\n\t}\n\n\tasync subscribe(\n\t\tmessages: TMessage['type'][],\n\t\tlistener: (payload: TMessage) => Promise<void>,\n\t): Promise<void> {\n\t\tif (!this.connection.isConnected()) {\n\t\t\tawait this.connection.connect();\n\t\t}\n\n\t\tconst boss = this.connection.instance;\n\t\tif (!boss) {\n\t\t\tthrow new PgBossNotStarted();\n\t\t}\n\n\t\tfor (const messageType of messages) {\n\t\t\tawait boss.createQueue(messageType);\n\t\t\tawait boss.work(\n\t\t\t\tmessageType,\n\t\t\t\t{\n\t\t\t\t\t...(this.options.batchSize && {\n\t\t\t\t\t\tbatchSize: this.options.batchSize,\n\t\t\t\t\t}),\n\t\t\t\t\t...(this.options.pollingIntervalSeconds && {\n\t\t\t\t\t\tpollingIntervalSeconds: this.options.pollingIntervalSeconds,\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\tasync (jobs) => {\n\t\t\t\t\tfor (const job of jobs) {\n\t\t\t\t\t\tconst fullMessage = {\n\t\t\t\t\t\t\ttype: job.name,\n\t\t\t\t\t\t\tpayload: job.data,\n\t\t\t\t\t\t} as TMessage;\n\n\t\t\t\t\t\tawait listener(fullMessage);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/pgboss/errors.ts","../../src/pgboss/PgBossConnection.ts","../../src/pgboss/PgBossPublisher.ts","../../src/pgboss/PgBossSubscriber.ts"],"sourcesContent":["// Beside the adapter but apart from its connection module, which imports\n// the broker's client: the publisher and subscriber load that lazily, and\n// importing an error class from it would load it eagerly.\n\n/** pg-boss connected but its instance never started. */\nexport class PgBossNotStarted extends Error {\n\tconstructor() {\n\t\tsuper(\n\t\t\t'pg-boss did not start. Check the database in the connection string is reachable and the role may create the pg-boss schema.',\n\t\t);\n\t\tthis.name = 'PgBossNotStarted';\n\t}\n}\n\n/** A topic subscription with no name to give the subscriber's own queue. */\nexport class PgBossSubscriptionNeedsName extends Error {\n\tconstructor(readonly topic: string) {\n\t\tsuper(\n\t\t\t`Subscribing to topic '${topic}' on pg-boss needs a subscription name: ` +\n\t\t\t\t'each subscriber drains a queue of its own, and that queue is named ' +\n\t\t\t\t'for it. Pass `subscription`.',\n\t\t);\n\t\tthis.name = 'PgBossSubscriptionNeedsName';\n\t}\n}\n","import { PgBoss } from 'pg-boss';\nimport type { EventConnection } from '../types';\nimport { EventPublisherType } from '../types';\n\nexport interface PgBossConnectionConfig {\n\tconnectionString: string;\n\tschema?: string;\n}\n\nexport class PgBossConnection implements EventConnection {\n\treadonly type = EventPublisherType.PgBoss;\n\tprivate boss?: PgBoss;\n\tprivate connecting?: Promise<void>;\n\n\tconstructor(private config: PgBossConnectionConfig) {}\n\n\t/**\n\t * Create a PgBossConnection from a connection string.\n\t * Format: pgboss://user:pass@host:5432/database?schema=pgboss\n\t */\n\tstatic async fromConnectionString(\n\t\tconnectionString: string,\n\t): Promise<PgBossConnection> {\n\t\tconst url = new URL(connectionString);\n\t\tconst params = url.searchParams;\n\n\t\tconst postgresUrl = `postgres://${url.username ? `${url.username}:${url.password}@` : ''}${url.host}${url.pathname}`;\n\n\t\tconst config: PgBossConnectionConfig = {\n\t\t\tconnectionString: postgresUrl,\n\t\t\tschema: params.get('schema') || undefined,\n\t\t};\n\n\t\tconst connection = new PgBossConnection(config);\n\t\tawait connection.connect();\n\t\treturn connection;\n\t}\n\n\tasync connect(): Promise<void> {\n\t\tif (this.boss) return;\n\n\t\tif (this.connecting) {\n\t\t\tawait this.connecting;\n\t\t\treturn;\n\t\t}\n\n\t\tthis.connecting = (async () => {\n\t\t\ttry {\n\t\t\t\tconst boss = new PgBoss({\n\t\t\t\t\tconnectionString: this.config.connectionString,\n\t\t\t\t\t...(this.config.schema && { schema: this.config.schema }),\n\t\t\t\t});\n\n\t\t\t\tawait boss.start();\n\t\t\t\tthis.boss = boss;\n\t\t\t} catch (error) {\n\t\t\t\tthis.cleanup();\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t})();\n\n\t\tawait this.connecting;\n\t\tthis.connecting = undefined;\n\t}\n\n\tasync close(): Promise<void> {\n\t\ttry {\n\t\t\tawait this.boss?.stop({ graceful: true });\n\t\t} catch {\n\t\t\t// Ignore errors during close\n\t\t} finally {\n\t\t\tthis.cleanup();\n\t\t}\n\t}\n\n\tisConnected(): boolean {\n\t\treturn !!this.boss;\n\t}\n\n\tprivate cleanup(): void {\n\t\tthis.boss = undefined;\n\t\tthis.connecting = undefined;\n\t}\n\n\t/**\n\t * Get the underlying pg-boss instance for publishing/subscribing\n\t */\n\tget instance(): PgBoss | undefined {\n\t\treturn this.boss;\n\t}\n}\n","import type { EventPublisher, PublishableMessage } from '../types';\nimport { PgBossNotStarted } from './errors';\nimport type { PgBossConnection } from './PgBossConnection';\n\nexport interface PgBossPublisherOptions {\n\t/**\n\t * Publishing to a topic: each message is fanned out to every subscriber's\n\t * own queue rather than inserted into one shared queue. Without it, a\n\t * message goes to the queue named by its type — a work queue, one consumer.\n\t */\n\ttopic?: string;\n}\n\n/** The pg-boss event a topic's message type is published as. */\nexport function topicEvent(topic: string, type: string): string {\n\treturn `${topic}/${type}`;\n}\n\nexport class PgBossPublisher<TMessage extends PublishableMessage<string, any>>\n\timplements EventPublisher<TMessage>\n{\n\tconstructor(\n\t\tprivate connection: PgBossConnection,\n\t\tprivate options: PgBossPublisherOptions = {},\n\t) {}\n\n\t/**\n\t * Create a PgBossPublisher from a connection string.\n\t * Format: pgboss://user:pass@host:5432/database?schema=pgboss\n\t */\n\tstatic async fromConnectionString<\n\t\tTMessage extends PublishableMessage<string, any>,\n\t>(\n\t\tconnectionString: string,\n\t\toptions: PgBossPublisherOptions = {},\n\t): Promise<PgBossPublisher<TMessage>> {\n\t\tconst { PgBossConnection } = await import('./PgBossConnection');\n\t\tconst connection =\n\t\t\tawait PgBossConnection.fromConnectionString(connectionString);\n\t\treturn new PgBossPublisher<TMessage>(connection, options);\n\t}\n\n\tasync publish(messages: TMessage[]): Promise<void> {\n\t\tif (!this.connection.isConnected()) {\n\t\t\tawait this.connection.connect();\n\t\t}\n\n\t\tconst boss = this.connection.instance;\n\t\tif (!boss) {\n\t\t\tthrow new PgBossNotStarted();\n\t\t}\n\n\t\t// A topic: pg-boss copies the job into every queue subscribed to the\n\t\t// event — one per subscriber — so each subscriber sees each message.\n\t\t// The type rides in the data, because the job is named for the queue.\n\t\tconst { topic } = this.options;\n\t\tif (topic) {\n\t\t\tfor (const m of messages) {\n\t\t\t\tawait boss.publish(topicEvent(topic, m.type), {\n\t\t\t\t\ttype: m.type,\n\t\t\t\t\tpayload: m.payload,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Group jobs by queue name (v11+ requires per-queue insert calls)\n\t\tconst groups = new Map<string, { data: TMessage['payload'] }[]>();\n\t\tfor (const m of messages) {\n\t\t\tconst list = groups.get(m.type) ?? [];\n\t\t\tlist.push({ data: m.payload });\n\t\t\tgroups.set(m.type, list);\n\t\t}\n\n\t\tfor (const [name, jobs] of groups) {\n\t\t\tawait boss.createQueue(name);\n\t\t\tawait boss.insert(name, jobs);\n\t\t}\n\t}\n\n\tasync close(): Promise<void> {\n\t\t// Publisher doesn't own the connection\n\t}\n}\n","import type { EventSubscriber, PublishableMessage } from '../types';\nimport { PgBossNotStarted, PgBossSubscriptionNeedsName } from './errors';\nimport type { PgBossConnection } from './PgBossConnection';\nimport { topicEvent } from './PgBossPublisher';\n\nexport interface PgBossSubscriberOptions {\n\tbatchSize?: number;\n\tpollingIntervalSeconds?: number;\n\t/**\n\t * Subscribing to a topic. Each subscriber then drains a queue of its own,\n\t * `<topic>/<subscription>`, bound to the events it names — so every\n\t * subscriber sees every message, and replicas of one subscriber share it.\n\t * Without it, messages are drained from the queue named by their type:\n\t * work shared among whoever drains it.\n\t */\n\ttopic?: string;\n\t/** The subscriber's own name on the topic. Required with `topic`. */\n\tsubscription?: string;\n}\n\nexport class PgBossSubscriber<TMessage extends PublishableMessage<string, any>>\n\timplements EventSubscriber<TMessage>\n{\n\tconstructor(\n\t\tprivate connection: PgBossConnection,\n\t\tprivate options: PgBossSubscriberOptions = {},\n\t) {}\n\n\t/**\n\t * Create a PgBossSubscriber from a connection string.\n\t * Format: pgboss://user:pass@host:5432/database?schema=pgboss&batchSize=5\n\t */\n\tstatic async fromConnectionString<\n\t\tTMessage extends PublishableMessage<string, any>,\n\t>(connectionString: string): Promise<PgBossSubscriber<TMessage>> {\n\t\tconst url = new URL(connectionString);\n\t\tconst params = url.searchParams;\n\n\t\tconst { PgBossConnection } = await import('./PgBossConnection');\n\t\tconst connection =\n\t\t\tawait PgBossConnection.fromConnectionString(connectionString);\n\n\t\tconst options: PgBossSubscriberOptions = {\n\t\t\tbatchSize: params.get('batchSize')\n\t\t\t\t? Number.parseInt(params.get('batchSize')!, 10)\n\t\t\t\t: undefined,\n\t\t\tpollingIntervalSeconds: params.get('pollingIntervalSeconds')\n\t\t\t\t? Number.parseInt(params.get('pollingIntervalSeconds')!, 10)\n\t\t\t\t: undefined,\n\t\t};\n\n\t\treturn new PgBossSubscriber<TMessage>(connection, options);\n\t}\n\n\tasync subscribe(\n\t\tmessages: TMessage['type'][],\n\t\tlistener: (payload: TMessage) => Promise<void>,\n\t): Promise<void> {\n\t\tif (!this.connection.isConnected()) {\n\t\t\tawait this.connection.connect();\n\t\t}\n\n\t\tconst boss = this.connection.instance;\n\t\tif (!boss) {\n\t\t\tthrow new PgBossNotStarted();\n\t\t}\n\n\t\tconst work = {\n\t\t\t...(this.options.batchSize && { batchSize: this.options.batchSize }),\n\t\t\t...(this.options.pollingIntervalSeconds && {\n\t\t\t\tpollingIntervalSeconds: this.options.pollingIntervalSeconds,\n\t\t\t}),\n\t\t};\n\n\t\tconst { topic, subscription } = this.options;\n\t\tif (topic) {\n\t\t\tif (!subscription) throw new PgBossSubscriptionNeedsName(topic);\n\n\t\t\tconst queue = topicEvent(topic, subscription);\n\t\t\tawait boss.createQueue(queue);\n\t\t\tfor (const type of messages) {\n\t\t\t\tawait boss.subscribe(topicEvent(topic, type), queue);\n\t\t\t}\n\t\t\tawait boss.work<{ type: string; payload: unknown }>(\n\t\t\t\tqueue,\n\t\t\t\twork,\n\t\t\t\tasync (jobs) => {\n\t\t\t\t\tfor (const job of jobs) {\n\t\t\t\t\t\tawait listener({\n\t\t\t\t\t\t\ttype: job.data.type,\n\t\t\t\t\t\t\tpayload: job.data.payload,\n\t\t\t\t\t\t} as TMessage);\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const messageType of messages) {\n\t\t\tawait boss.createQueue(messageType);\n\t\t\tawait boss.work(messageType, work, async (jobs) => {\n\t\t\t\tfor (const job of jobs) {\n\t\t\t\t\tconst fullMessage = {\n\t\t\t\t\t\ttype: job.name,\n\t\t\t\t\t\tpayload: job.data,\n\t\t\t\t\t} as TMessage;\n\n\t\t\t\t\tawait listener(fullMessage);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n}\n"],"mappings":"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"}
@@ -3,7 +3,259 @@ require("../types-BcZjLxVV.cjs");
3
3
  const require_SQSSubscriber = require("../SQSSubscriber-DtRUaZB3.cjs");
4
4
  const require_sns_snsUrl = require("./snsUrl.cjs");
5
5
  let _aws_sdk_client_sqs = require("@aws-sdk/client-sqs");
6
+ let node_crypto = require("node:crypto");
6
7
  let _aws_sdk_client_sns = require("@aws-sdk/client-sns");
8
+ //#region src/sns/push.ts
9
+ /** The fields signed, in the order SNS signs them, per message type. */
10
+ const SIGNED_FIELDS = {
11
+ Notification: [
12
+ "Message",
13
+ "MessageId",
14
+ "Subject",
15
+ "Timestamp",
16
+ "TopicArn",
17
+ "Type"
18
+ ],
19
+ SubscriptionConfirmation: [
20
+ "Message",
21
+ "MessageId",
22
+ "SubscribeURL",
23
+ "Timestamp",
24
+ "Token",
25
+ "TopicArn",
26
+ "Type"
27
+ ],
28
+ UnsubscribeConfirmation: [
29
+ "Message",
30
+ "MessageId",
31
+ "SubscribeURL",
32
+ "Timestamp",
33
+ "Token",
34
+ "TopicArn",
35
+ "Type"
36
+ ]
37
+ };
38
+ /**
39
+ * Where SNS serves its signing certificates. Anywhere else is a certificate
40
+ * the sender chose, which would let the sender sign its own forgeries.
41
+ */
42
+ const SNS_CERTIFICATE_HOST = /^sns\.[a-z0-9-]+\.amazonaws\.com(\.cn)?$/;
43
+ const certificates = /* @__PURE__ */ new Map();
44
+ async function defaultFetchCertificate(url) {
45
+ const response = await fetch(url);
46
+ if (!response.ok) throw new SnsCertificateUnavailable(url, response.status);
47
+ return response.text();
48
+ }
49
+ /**
50
+ * A URL, or undefined for a value that is not one at all — what an emulator
51
+ * sends — so it is untrusted rather than a crash. (`URL.parse` is Node 22.1+.)
52
+ */
53
+ function parseUrl(value) {
54
+ try {
55
+ return new URL(String(value));
56
+ } catch {
57
+ return;
58
+ }
59
+ }
60
+ /** The string SNS signed for this message. */
61
+ function snsStringToSign(message) {
62
+ const fields = SIGNED_FIELDS[message.Type];
63
+ if (!fields) throw new SnsMessageTypeUnknown(String(message.Type));
64
+ return fields.filter((field) => message[field] !== void 0).map((field) => `${field}\n${message[field]}\n`).join("");
65
+ }
66
+ /**
67
+ * Check that SNS sent this message: the certificate is SNS's own, and the
68
+ * signature over the message's signed fields verifies against it.
69
+ *
70
+ * Throws rather than returning false, so a caller cannot forget to look.
71
+ */
72
+ async function verifySnsMessage(message, options = {}) {
73
+ const url = parseUrl(message.SigningCertURL);
74
+ if (url?.protocol !== "https:" || !SNS_CERTIFICATE_HOST.test(url.hostname)) throw new SnsCertificateUntrusted(message.SigningCertURL);
75
+ const algorithm = message.SignatureVersion === "1" ? "RSA-SHA1" : message.SignatureVersion === "2" ? "RSA-SHA256" : void 0;
76
+ if (!algorithm) throw new SnsSignatureVersionUnknown(message.SignatureVersion);
77
+ const fetchCertificate = options.fetchCertificate ?? defaultFetchCertificate;
78
+ let certificate = certificates.get(message.SigningCertURL);
79
+ if (!certificate || options.fetchCertificate) {
80
+ certificate = fetchCertificate(message.SigningCertURL);
81
+ if (!options.fetchCertificate) {
82
+ certificates.set(message.SigningCertURL, certificate);
83
+ certificate.catch(() => certificates.delete(message.SigningCertURL));
84
+ }
85
+ }
86
+ const verifier = (0, node_crypto.createVerify)(algorithm);
87
+ verifier.update(snsStringToSign(message), "utf8");
88
+ if (!verifier.verify(await certificate, message.Signature, "base64")) throw new SnsSignatureInvalid(message.MessageId, message.TopicArn);
89
+ }
90
+ /**
91
+ * Confirm a subscription by visiting the URL SNS sent. Until this happens SNS
92
+ * delivers nothing to the endpoint.
93
+ */
94
+ async function confirmSnsSubscription(message, request = fetch) {
95
+ if (message.Type !== "SubscriptionConfirmation" || !message.SubscribeURL) throw new SnsNotAConfirmation(message.Type);
96
+ const response = await request(message.SubscribeURL);
97
+ if (!response.ok) throw new SnsConfirmationFailed(message.TopicArn, response.status);
98
+ }
99
+ /**
100
+ * Subscribe an HTTP endpoint to the connection's topic, converging on one
101
+ * confirmed subscription with the filter asked for — so it is safe on every
102
+ * start:
103
+ *
104
+ * - **none yet** — subscribed; SNS sends a confirmation to the endpoint.
105
+ * - **confirmed** — its filter and dead-letter queue are brought up to date.
106
+ * Subscribing again with different attributes is an error on SNS, and the
107
+ * events a subscriber names change.
108
+ * - **stuck pending** — a confirmation that never landed (the endpoint was
109
+ * down, or answered wrong). Subscribing the same endpoint again is a no-op
110
+ * on an emulator, so it is dropped and subscribed afresh, which sends a new
111
+ * confirmation.
112
+ */
113
+ async function subscribeHttpEndpoint(connection, options) {
114
+ const protocol = new URL(options.endpoint).protocol.replace(":", "");
115
+ if (protocol !== "http" && protocol !== "https") throw new SnsEndpointNotHttp(options.endpoint);
116
+ const attributes = {
117
+ FilterPolicyScope: "MessageAttributes",
118
+ FilterPolicy: JSON.stringify({ type: [...options.events] })
119
+ };
120
+ if (options.deadLetterQueueArn) attributes.RedrivePolicy = JSON.stringify({ deadLetterTargetArn: options.deadLetterQueueArn });
121
+ const client = connection.snsClient;
122
+ const existing = await existingSubscription(connection, options.endpoint);
123
+ if (existing) {
124
+ const { Attributes = {} } = await client.send(new _aws_sdk_client_sns.GetSubscriptionAttributesCommand({ SubscriptionArn: existing }));
125
+ if (Attributes.PendingConfirmation === "true") await client.send(new _aws_sdk_client_sns.UnsubscribeCommand({ SubscriptionArn: existing }));
126
+ else {
127
+ for (const [name, value] of Object.entries(attributes)) {
128
+ if (Attributes[name] === value) continue;
129
+ await client.send(new _aws_sdk_client_sns.SetSubscriptionAttributesCommand({
130
+ SubscriptionArn: existing,
131
+ AttributeName: name,
132
+ AttributeValue: value
133
+ }));
134
+ }
135
+ return existing;
136
+ }
137
+ }
138
+ const { SubscriptionArn } = await client.send(new _aws_sdk_client_sns.SubscribeCommand({
139
+ TopicArn: connection.topicArn,
140
+ Protocol: protocol,
141
+ Endpoint: options.endpoint,
142
+ Attributes: attributes,
143
+ ReturnSubscriptionArn: true
144
+ }));
145
+ return SubscriptionArn;
146
+ }
147
+ /**
148
+ * The ARN of the endpoint's subscription to the topic, if it has one SNS will
149
+ * name. (Real SNS lists a pending one as `PendingConfirmation`, with no ARN to
150
+ * act on — and resends its confirmation on a new Subscribe anyway.)
151
+ */
152
+ async function existingSubscription(connection, endpoint) {
153
+ let NextToken;
154
+ do {
155
+ const page = await connection.snsClient.send(new _aws_sdk_client_sns.ListSubscriptionsByTopicCommand({
156
+ TopicArn: connection.topicArn,
157
+ NextToken
158
+ }));
159
+ const match = page.Subscriptions?.find((s) => s.Endpoint === endpoint && s.SubscriptionArn?.startsWith("arn:"));
160
+ if (match) return match.SubscriptionArn;
161
+ NextToken = page.NextToken;
162
+ } while (NextToken);
163
+ }
164
+ /**
165
+ * A pushed notification as the Lambda event SNS would have invoked a function
166
+ * with, so it can be handed to the same adaptor.
167
+ */
168
+ function toSnsEvent(message, subscriptionArn = "") {
169
+ return { Records: [{
170
+ EventSource: "aws:sns",
171
+ EventVersion: "1.0",
172
+ EventSubscriptionArn: subscriptionArn,
173
+ Sns: {
174
+ Type: message.Type,
175
+ MessageId: message.MessageId,
176
+ TopicArn: message.TopicArn,
177
+ Subject: message.Subject ?? null,
178
+ Message: message.Message,
179
+ Timestamp: message.Timestamp,
180
+ SignatureVersion: message.SignatureVersion,
181
+ Signature: message.Signature,
182
+ SigningCertUrl: message.SigningCertURL,
183
+ UnsubscribeUrl: message.UnsubscribeURL ?? "",
184
+ MessageAttributes: message.MessageAttributes ?? {}
185
+ }
186
+ }] };
187
+ }
188
+ var SnsCertificateUntrusted = class extends Error {
189
+ url;
190
+ constructor(url) {
191
+ super(`The SNS message names a signing certificate at '${url}', which is not an https URL on sns.<region>.amazonaws.com. Only SNS can send to this endpoint; reject the request.`);
192
+ this.url = url;
193
+ this.name = "SnsCertificateUntrusted";
194
+ }
195
+ };
196
+ var SnsCertificateUnavailable = class extends Error {
197
+ url;
198
+ status;
199
+ constructor(url, status) {
200
+ super(`Fetching the SNS signing certificate at '${url}' answered ${status}. The message cannot be verified, so it is not handled; SNS will retry.`);
201
+ this.url = url;
202
+ this.status = status;
203
+ this.name = "SnsCertificateUnavailable";
204
+ }
205
+ };
206
+ var SnsSignatureVersionUnknown = class extends Error {
207
+ version;
208
+ constructor(version) {
209
+ super(`SNS signature version '${version}' is not one this endpoint verifies (it knows '1' and '2'). Reject the request.`);
210
+ this.version = version;
211
+ this.name = "SnsSignatureVersionUnknown";
212
+ }
213
+ };
214
+ var SnsSignatureInvalid = class extends Error {
215
+ messageId;
216
+ topicArn;
217
+ constructor(messageId, topicArn) {
218
+ super(`SNS message '${messageId}' from '${topicArn}' does not verify against its signing certificate. It was not sent by SNS, or was altered on the way; reject it.`);
219
+ this.messageId = messageId;
220
+ this.topicArn = topicArn;
221
+ this.name = "SnsSignatureInvalid";
222
+ }
223
+ };
224
+ var SnsMessageTypeUnknown = class extends Error {
225
+ type;
226
+ constructor(type) {
227
+ super(`'${type}' is not an SNS message type (Notification, SubscriptionConfirmation, UnsubscribeConfirmation). Reject it.`);
228
+ this.type = type;
229
+ this.name = "SnsMessageTypeUnknown";
230
+ }
231
+ };
232
+ var SnsNotAConfirmation = class extends Error {
233
+ type;
234
+ constructor(type) {
235
+ super(`Only a SubscriptionConfirmation carries a URL to confirm; this is a '${type}'.`);
236
+ this.type = type;
237
+ this.name = "SnsNotAConfirmation";
238
+ }
239
+ };
240
+ var SnsConfirmationFailed = class extends Error {
241
+ topicArn;
242
+ status;
243
+ constructor(topicArn, status) {
244
+ super(`Confirming the subscription to '${topicArn}' answered ${status}. SNS will deliver nothing until it is confirmed; it resends the confirmation, or subscribe the endpoint again.`);
245
+ this.topicArn = topicArn;
246
+ this.status = status;
247
+ this.name = "SnsConfirmationFailed";
248
+ }
249
+ };
250
+ var SnsEndpointNotHttp = class extends Error {
251
+ endpoint;
252
+ constructor(endpoint) {
253
+ super(`'${endpoint}' is not an http(s) URL. SNS pushes to an HTTP endpoint; subscribe a queue for anything else.`);
254
+ this.endpoint = endpoint;
255
+ this.name = "SnsEndpointNotHttp";
256
+ }
257
+ };
258
+ //#endregion
7
259
  //#region src/sns/SNSConnection.ts
8
260
  var SNSConnection = class SNSConnection {
9
261
  type = "sns";
@@ -250,12 +502,25 @@ exports.SNSConnection = SNSConnection;
250
502
  exports.SNSConnection$1 = SNSConnection;
251
503
  exports.SNSPublisher = SNSPublisher;
252
504
  exports.SNSSubscriber = SNSSubscriber;
505
+ exports.SnsCertificateUnavailable = SnsCertificateUnavailable;
506
+ exports.SnsCertificateUntrusted = SnsCertificateUntrusted;
507
+ exports.SnsConfirmationFailed = SnsConfirmationFailed;
508
+ exports.SnsEndpointNotHttp = SnsEndpointNotHttp;
509
+ exports.SnsMessageTypeUnknown = SnsMessageTypeUnknown;
510
+ exports.SnsNotAConfirmation = SnsNotAConfirmation;
253
511
  exports.SnsQueueMissing = SnsQueueMissing;
512
+ exports.SnsSignatureInvalid = SnsSignatureInvalid;
513
+ exports.SnsSignatureVersionUnknown = SnsSignatureVersionUnknown;
514
+ exports.confirmSnsSubscription = confirmSnsSubscription;
515
+ exports.snsStringToSign = snsStringToSign;
254
516
  Object.defineProperty(exports, "snsUrl", {
255
517
  enumerable: true,
256
518
  get: function() {
257
519
  return require_sns_snsUrl.snsUrl_exports;
258
520
  }
259
521
  });
522
+ exports.subscribeHttpEndpoint = subscribeHttpEndpoint;
523
+ exports.toSnsEvent = toSnsEvent;
524
+ exports.verifySnsMessage = verifySnsMessage;
260
525
 
261
526
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["SNSClient","PublishCommand","SQSClient","SQSConnection","SQSSubscriber","UnsubscribeCommand","DeleteQueueCommand","CreateQueueCommand","GetQueueAttributesCommand","SetQueueAttributesCommand","SubscribeCommand"],"sources":["../../src/sns/SNSConnection.ts","../../src/sns/SNSPublisher.ts","../../src/sns/SNSSubscriber.ts"],"sourcesContent":["import { SNSClient, type SNSClientConfig } from '@aws-sdk/client-sns';\nimport type { EventConnection } from '../types';\nimport { EventPublisherType } from '../types';\nimport * as snsUrl from './snsUrl';\n\nexport interface SNSConnectionConfig {\n\ttopicArn: string;\n\tregion?: string;\n\tendpoint?: string; // Custom endpoint (e.g., for LocalStack)\n\tcredentials?: {\n\t\taccessKeyId: string;\n\t\tsecretAccessKey: string;\n\t\tsessionToken?: string;\n\t};\n}\n\nexport class SNSConnection implements EventConnection {\n\treadonly type = EventPublisherType.SNS;\n\tprivate client: SNSClient;\n\tprivate connected = false;\n\tprivate config: SNSConnectionConfig;\n\n\tconstructor(config: SNSConnectionConfig) {\n\t\tthis.config = config;\n\n\t\tconst clientConfig: SNSClientConfig = {\n\t\t\tregion: config.region,\n\t\t\tendpoint: config.endpoint,\n\t\t\tcredentials: config.credentials,\n\t\t};\n\n\t\tthis.client = new SNSClient(clientConfig);\n\t}\n\n\t/**\n\t * Create an SNSConnection from a connection string\n\t * Format: sns://?topicArn=arn:aws:sns:region:account:topic&region=us-east-1&endpoint=http://localhost:4566\n\t */\n\tstatic async fromConnectionString(\n\t\tconnectionString: string,\n\t): Promise<SNSConnection> {\n\t\t// Through the codec, not a second hand-rolled parse: the deploy target\n\t\t// composes these with `snsUrl.build`, and a string it cannot read back is\n\t\t// exactly the drift one shared module rules out.\n\t\tconst { topicArn, region, endpoint } = snsUrl.parse(connectionString);\n\t\tconst params = new URL(connectionString).searchParams;\n\n\t\tconst config: SNSConnectionConfig = {\n\t\t\ttopicArn,\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t};\n\n\t\t// Credentials are not part of the address — see `snsUrl` — but a caller\n\t\t// may still pin them here, so they are read beside it rather than in it.\n\t\tconst accessKeyId = params.get('accessKeyId');\n\t\tconst secretAccessKey = params.get('secretAccessKey');\n\t\tif (accessKeyId && secretAccessKey) {\n\t\t\tconfig.credentials = {\n\t\t\t\taccessKeyId,\n\t\t\t\tsecretAccessKey,\n\t\t\t\tsessionToken: params.get('sessionToken') || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst connection = new SNSConnection(config);\n\t\tawait connection.connect();\n\t\treturn connection;\n\t}\n\n\tasync connect(): Promise<void> {\n\t\t// SNS doesn't have a traditional connection, but we mark as connected\n\t\tthis.connected = true;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.client.destroy();\n\t\tthis.connected = false;\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.connected;\n\t}\n\n\t/**\n\t * Get the underlying SNS client\n\t */\n\tget snsClient(): SNSClient {\n\t\treturn this.client;\n\t}\n\n\t/**\n\t * Get the topic ARN for this connection\n\t */\n\tget topicArn(): string {\n\t\treturn this.config.topicArn;\n\t}\n}\n","import { PublishCommand } from '@aws-sdk/client-sns';\nimport type { EventPublisher, PublishableMessage } from '../types';\nimport type { SNSConnection } from './SNSConnection';\n\nexport type SNSPublisherOptions = {};\n\nexport class SNSPublisher<TMessage extends PublishableMessage<string, any>>\n\timplements EventPublisher<TMessage>\n{\n\tconstructor(\n\t\tprivate connection: SNSConnection,\n\t\t_options: SNSPublisherOptions = {},\n\t) {}\n\n\t/**\n\t * Create an SNSPublisher from a connection string\n\t * Format: sns://?topicArn=arn:aws:sns:region:account:topic&region=us-east-1&endpoint=http://localhost:4566\n\t */\n\tstatic async fromConnectionString<\n\t\tTMessage extends PublishableMessage<string, any>,\n\t>(connectionString: string): Promise<SNSPublisher<TMessage>> {\n\t\tconst { SNSConnection } = await import('./SNSConnection');\n\t\tconst connection =\n\t\t\tawait SNSConnection.fromConnectionString(connectionString);\n\n\t\treturn new SNSPublisher<TMessage>(connection);\n\t}\n\n\tasync publish(messages: TMessage[]): Promise<void> {\n\t\tif (messages.length === 0) return;\n\n\t\tif (!this.connection.isConnected()) {\n\t\t\tawait this.connection.connect();\n\t\t}\n\n\t\t// Publish all messages\n\t\tawait Promise.all(messages.map((message) => this.publishMessage(message)));\n\t}\n\n\tprivate async publishMessage(message: TMessage): Promise<void> {\n\t\tconst command = new PublishCommand({\n\t\t\tTopicArn: this.connection.topicArn,\n\t\t\tMessage: JSON.stringify({\n\t\t\t\ttype: message.type,\n\t\t\t\tpayload: message.payload,\n\t\t\t}),\n\t\t\tMessageAttributes: {\n\t\t\t\ttype: {\n\t\t\t\t\tDataType: 'String',\n\t\t\t\t\tStringValue: message.type,\n\t\t\t\t},\n\t\t\t},\n\t\t});\n\n\t\tawait this.connection.snsClient.send(command);\n\t}\n\n\tasync close(): Promise<void> {\n\t\t// Publisher doesn't own the connection\n\t\t// Connection should be closed by whoever created it\n\t}\n}\n","import { SubscribeCommand, UnsubscribeCommand } from '@aws-sdk/client-sns';\nimport {\n\tCreateQueueCommand,\n\tDeleteQueueCommand,\n\tGetQueueAttributesCommand,\n\tSetQueueAttributesCommand,\n\tSQSClient,\n} from '@aws-sdk/client-sqs';\nimport { SQSConnection } from '../sqs/SQSConnection';\nimport { SQSSubscriber } from '../sqs/SQSSubscriber';\nimport type { EventSubscriber, PublishableMessage } from '../types';\nimport type { SNSConnection } from './SNSConnection';\n\nexport interface SNSSubscriberOptions {\n\tqueueName?: string; // Optional queue name (defaults to auto-generated)\n\tcreateQueue?: boolean; // Auto-create SQS queue (default: true)\n\tdeleteQueueOnClose?: boolean; // Delete queue when subscriber closes (default: false for named queues, true for auto-generated)\n\twaitTimeSeconds?: number; // SQS long polling (default: 20)\n\tmaxMessages?: number; // Max messages per poll (default: 10)\n}\n\n/**\n * SNS Subscriber - receives push-based messages from SNS via SQS queue\n *\n * Architecture:\n * SNS Topic → (push) → SQS Queue → Subscriber\n *\n * This subscriber automatically:\n * 1. Creates an SQS queue (or uses existing one)\n * 2. Subscribes the queue to the SNS topic\n * 3. Receives messages pushed from SNS to the queue\n * 4. Filters messages by topic ARN and message type\n * 5. Processes and acknowledges messages\n *\n * Note: While SQS long-polling is used internally to receive messages,\n * this is an implementation detail of the SNS push delivery mechanism.\n * Messages are pushed from SNS to SQS, and we receive those pushed messages.\n */\nexport class SNSSubscriber<TMessage extends PublishableMessage<string, any>>\n\timplements EventSubscriber<TMessage>\n{\n\tprivate sqsClient: SQSClient;\n\tprivate queueUrl?: string;\n\tprivate queueArn?: string;\n\tprivate subscriptionArn?: string;\n\tprivate sqsSubscriber?: SQSSubscriber<TMessage>;\n\tprivate options: Required<SNSSubscriberOptions>;\n\n\tconstructor(\n\t\tprivate connection: SNSConnection,\n\t\toptions: SNSSubscriberOptions = {},\n\t) {\n\t\tthis.options = {\n\t\t\tqueueName:\n\t\t\t\toptions.queueName ||\n\t\t\t\t`sns-sub-${Date.now()}-${Math.random().toString(36).substring(7)}`,\n\t\t\tcreateQueue: options.createQueue ?? true,\n\t\t\tdeleteQueueOnClose: options.deleteQueueOnClose ?? !options.queueName, // Delete auto-generated queues\n\t\t\twaitTimeSeconds: options.waitTimeSeconds ?? 20,\n\t\t\tmaxMessages: options.maxMessages ?? 10,\n\t\t};\n\n\t\t// Create SQS client with same config as SNS\n\t\tthis.sqsClient = new SQSClient({\n\t\t\tregion: (this.connection as any).config.region,\n\t\t\tendpoint: (this.connection as any).config.endpoint,\n\t\t\tcredentials: (this.connection as any).config.credentials,\n\t\t});\n\t}\n\n\t/**\n\t * Create an SNSSubscriber from a connection string\n\t * Format: sns://?topicArn=...&queueName=myqueue&region=us-east-1\n\t */\n\tstatic async fromConnectionString<\n\t\tTMessage extends PublishableMessage<string, any>,\n\t>(connectionString: string): Promise<SNSSubscriber<TMessage>> {\n\t\tconst url = new URL(connectionString);\n\t\tconst params = url.searchParams;\n\n\t\tconst { SNSConnection } = await import('./SNSConnection');\n\t\tconst connection =\n\t\t\tawait SNSConnection.fromConnectionString(connectionString);\n\n\t\tconst options: SNSSubscriberOptions = {\n\t\t\tqueueName: params.get('queueName') || undefined,\n\t\t\tcreateQueue: params.get('createQueue') !== 'false',\n\t\t\tdeleteQueueOnClose: params.get('deleteQueueOnClose') === 'true',\n\t\t\twaitTimeSeconds: params.get('waitTimeSeconds')\n\t\t\t\t? Number.parseInt(params.get('waitTimeSeconds')!, 10)\n\t\t\t\t: undefined,\n\t\t};\n\n\t\treturn new SNSSubscriber<TMessage>(connection, options);\n\t}\n\n\tasync subscribe(\n\t\tmessages: TMessage['type'][],\n\t\tlistener: (payload: TMessage) => Promise<void>,\n\t): Promise<void> {\n\t\t// Setup SQS queue and SNS subscription\n\t\tawait this.setupQueue();\n\t\tawait this.subscribeToTopic();\n\n\t\t// Create SQS connection for the subscriber\n\t\tconst sqsConnection = new SQSConnection({\n\t\t\tqueueUrl: this.queueUrl!,\n\t\t\tregion: (this.connection as any).config.region,\n\t\t\tendpoint: (this.connection as any).config.endpoint,\n\t\t\tcredentials: (this.connection as any).config.credentials,\n\t\t});\n\t\tawait sqsConnection.connect();\n\n\t\t// Use SQSSubscriber to receive messages, with topic filtering\n\t\tthis.sqsSubscriber = new SQSSubscriber<TMessage>(sqsConnection, {\n\t\t\twaitTimeSeconds: this.options.waitTimeSeconds,\n\t\t\tmaxMessages: this.options.maxMessages,\n\t\t\texpectedTopicArn: this.connection.topicArn,\n\t\t});\n\n\t\tawait this.sqsSubscriber.subscribe(messages, listener);\n\t}\n\n\t/**\n\t * Stop receiving messages and clean up resources\n\t */\n\tasync stop(): Promise<void> {\n\t\t// Stop SQS subscriber\n\t\tif (this.sqsSubscriber) {\n\t\t\tthis.sqsSubscriber.stop();\n\t\t}\n\n\t\t// Unsubscribe from topic\n\t\tif (this.subscriptionArn) {\n\t\t\ttry {\n\t\t\t\tconst command = new UnsubscribeCommand({\n\t\t\t\t\tSubscriptionArn: this.subscriptionArn,\n\t\t\t\t});\n\t\t\t\tawait this.connection.snsClient.send(command);\n\t\t\t} catch (_error) {}\n\t\t}\n\n\t\t// Delete queue if configured\n\t\tif (this.queueUrl && this.options.deleteQueueOnClose) {\n\t\t\ttry {\n\t\t\t\tconst command = new DeleteQueueCommand({\n\t\t\t\t\tQueueUrl: this.queueUrl,\n\t\t\t\t});\n\t\t\t\tawait this.sqsClient.send(command);\n\t\t\t} catch (_error) {}\n\t\t}\n\n\t\tthis.sqsClient.destroy();\n\t}\n\n\tprivate async setupQueue(): Promise<void> {\n\t\tif (this.options.createQueue) {\n\t\t\t// Create queue\n\t\t\tconst createCommand = new CreateQueueCommand({\n\t\t\t\tQueueName: this.options.queueName,\n\t\t\t});\n\t\t\tconst response = await this.sqsClient.send(createCommand);\n\t\t\tthis.queueUrl = response.QueueUrl!;\n\t\t} else {\n\t\t\tthrow new SnsQueueMissing(this.options.queueName);\n\t\t}\n\n\t\t// Get queue ARN\n\t\tconst attrsCommand = new GetQueueAttributesCommand({\n\t\t\tQueueUrl: this.queueUrl,\n\t\t\tAttributeNames: ['QueueArn'],\n\t\t});\n\t\tconst attrsResponse = await this.sqsClient.send(attrsCommand);\n\t\tthis.queueArn = attrsResponse.Attributes?.QueueArn!;\n\n\t\t// Set queue policy to allow SNS\n\t\tawait this.setQueuePolicy();\n\t}\n\n\tprivate async setQueuePolicy(): Promise<void> {\n\t\tconst policy = {\n\t\t\tVersion: '2012-10-17',\n\t\t\tStatement: [\n\t\t\t\t{\n\t\t\t\t\tEffect: 'Allow',\n\t\t\t\t\tPrincipal: '*',\n\t\t\t\t\tAction: 'SQS:SendMessage',\n\t\t\t\t\tResource: this.queueArn,\n\t\t\t\t\tCondition: {\n\t\t\t\t\t\tArnEquals: {\n\t\t\t\t\t\t\t'aws:SourceArn': this.connection.topicArn,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\n\t\tconst command = new SetQueueAttributesCommand({\n\t\t\tQueueUrl: this.queueUrl,\n\t\t\tAttributes: {\n\t\t\t\tPolicy: JSON.stringify(policy),\n\t\t\t},\n\t\t});\n\n\t\tawait this.sqsClient.send(command);\n\t}\n\n\tprivate async subscribeToTopic(): Promise<void> {\n\t\tconst command = new SubscribeCommand({\n\t\t\tTopicArn: this.connection.topicArn,\n\t\t\tProtocol: 'sqs',\n\t\t\tEndpoint: this.queueArn,\n\t\t});\n\n\t\tconst response = await this.connection.snsClient.send(command);\n\t\tthis.subscriptionArn = response.SubscriptionArn!;\n\t}\n}\n\n/**\n * A subscriber asked to use a queue it may not create. The SQS queue behind an\n * SNS subscription has to exist first, or `createQueue` has to allow it.\n */\nexport class SnsQueueMissing extends Error {\n\tconstructor(readonly queueName: string | undefined) {\n\t\tsuper(\n\t\t\t`The SNS subscriber's queue${queueName ? ` \"${queueName}\"` : ''} is not created here: pass createQueue: true, or create the queue first.`,\n\t\t);\n\t\tthis.name = 'SnsQueueMissing';\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["createVerify","GetSubscriptionAttributesCommand","UnsubscribeCommand","SetSubscriptionAttributesCommand","SubscribeCommand","ListSubscriptionsByTopicCommand","SNSClient","PublishCommand","SQSClient","SQSConnection","SQSSubscriber","UnsubscribeCommand","DeleteQueueCommand","CreateQueueCommand","GetQueueAttributesCommand","SetQueueAttributesCommand","SubscribeCommand"],"sources":["../../src/sns/push.ts","../../src/sns/SNSConnection.ts","../../src/sns/SNSPublisher.ts","../../src/sns/SNSSubscriber.ts"],"sourcesContent":["import { createVerify } from 'node:crypto';\nimport {\n\tGetSubscriptionAttributesCommand,\n\tListSubscriptionsByTopicCommand,\n\tSetSubscriptionAttributesCommand,\n\tSubscribeCommand,\n\ttype SubscribeCommandInput,\n\tUnsubscribeCommand,\n} from '@aws-sdk/client-sns';\nimport type { SNSConnection } from './SNSConnection';\n\n/**\n * SNS delivering to an HTTP(S) endpoint, rather than to a queue that is polled.\n *\n * SNS POSTs each message to the subscribed URL as JSON. That is the whole\n * transport: no queue in between, no poller, and the endpoint is handed the\n * same envelope a Lambda subscription receives inside `Records[].Sns` — so the\n * handler that runs deployed can be the one that runs here.\n *\n * The endpoint is public, so a message is trusted only once its signature\n * checks out against a certificate SNS itself serves. Anything else that POSTs\n * there is somebody's guess at the shape.\n */\n\n/** A message SNS POSTs to a subscribed HTTP endpoint. */\nexport interface SnsHttpMessage {\n\tType: 'Notification' | 'SubscriptionConfirmation' | 'UnsubscribeConfirmation';\n\tMessageId: string;\n\tTopicArn: string;\n\tMessage: string;\n\tTimestamp: string;\n\tSignatureVersion: string;\n\tSignature: string;\n\tSigningCertURL: string;\n\tSubject?: string;\n\t/** Confirmation messages only. */\n\tToken?: string;\n\t/** Confirmation messages only: GET it to confirm. */\n\tSubscribeURL?: string;\n\t/** Notifications only. */\n\tUnsubscribeURL?: string;\n\tMessageAttributes?: Record<string, { Type: string; Value: string }>;\n}\n\n/** The fields signed, in the order SNS signs them, per message type. */\nconst SIGNED_FIELDS: Record<SnsHttpMessage['Type'], (keyof SnsHttpMessage)[]> =\n\t{\n\t\tNotification: [\n\t\t\t'Message',\n\t\t\t'MessageId',\n\t\t\t'Subject',\n\t\t\t'Timestamp',\n\t\t\t'TopicArn',\n\t\t\t'Type',\n\t\t],\n\t\tSubscriptionConfirmation: [\n\t\t\t'Message',\n\t\t\t'MessageId',\n\t\t\t'SubscribeURL',\n\t\t\t'Timestamp',\n\t\t\t'Token',\n\t\t\t'TopicArn',\n\t\t\t'Type',\n\t\t],\n\t\tUnsubscribeConfirmation: [\n\t\t\t'Message',\n\t\t\t'MessageId',\n\t\t\t'SubscribeURL',\n\t\t\t'Timestamp',\n\t\t\t'Token',\n\t\t\t'TopicArn',\n\t\t\t'Type',\n\t\t],\n\t};\n\n/**\n * Where SNS serves its signing certificates. Anywhere else is a certificate\n * the sender chose, which would let the sender sign its own forgeries.\n */\nconst SNS_CERTIFICATE_HOST = /^sns\\.[a-z0-9-]+\\.amazonaws\\.com(\\.cn)?$/;\n\nexport interface VerifySnsMessageOptions {\n\t/**\n\t * Fetch the PEM at a certificate URL. `fetch` by default; replaceable so a\n\t * test signs with its own key instead of SNS's.\n\t */\n\tfetchCertificate?: (url: string) => Promise<string>;\n}\n\nconst certificates = new Map<string, Promise<string>>();\n\nasync function defaultFetchCertificate(url: string): Promise<string> {\n\tconst response = await fetch(url);\n\tif (!response.ok) throw new SnsCertificateUnavailable(url, response.status);\n\treturn response.text();\n}\n\n/**\n * A URL, or undefined for a value that is not one at all — what an emulator\n * sends — so it is untrusted rather than a crash. (`URL.parse` is Node 22.1+.)\n */\nfunction parseUrl(value: unknown): URL | undefined {\n\ttry {\n\t\treturn new URL(String(value));\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** The string SNS signed for this message. */\nexport function snsStringToSign(message: SnsHttpMessage): string {\n\tconst fields = SIGNED_FIELDS[message.Type];\n\tif (!fields) throw new SnsMessageTypeUnknown(String(message.Type));\n\n\treturn fields\n\t\t.filter((field) => message[field] !== undefined)\n\t\t.map((field) => `${field}\\n${message[field]}\\n`)\n\t\t.join('');\n}\n\n/**\n * Check that SNS sent this message: the certificate is SNS's own, and the\n * signature over the message's signed fields verifies against it.\n *\n * Throws rather than returning false, so a caller cannot forget to look.\n */\nexport async function verifySnsMessage(\n\tmessage: SnsHttpMessage,\n\toptions: VerifySnsMessageOptions = {},\n): Promise<void> {\n\tconst url = parseUrl(message.SigningCertURL);\n\tif (url?.protocol !== 'https:' || !SNS_CERTIFICATE_HOST.test(url.hostname)) {\n\t\tthrow new SnsCertificateUntrusted(message.SigningCertURL);\n\t}\n\n\tconst algorithm =\n\t\tmessage.SignatureVersion === '1'\n\t\t\t? 'RSA-SHA1'\n\t\t\t: message.SignatureVersion === '2'\n\t\t\t\t? 'RSA-SHA256'\n\t\t\t\t: undefined;\n\tif (!algorithm) {\n\t\tthrow new SnsSignatureVersionUnknown(message.SignatureVersion);\n\t}\n\n\tconst fetchCertificate = options.fetchCertificate ?? defaultFetchCertificate;\n\tlet certificate = certificates.get(message.SigningCertURL);\n\tif (!certificate || options.fetchCertificate) {\n\t\tcertificate = fetchCertificate(message.SigningCertURL);\n\t\t// Only SNS's real fetch is cached: a test's key belongs to that test.\n\t\tif (!options.fetchCertificate) {\n\t\t\tcertificates.set(message.SigningCertURL, certificate);\n\t\t\tcertificate.catch(() => certificates.delete(message.SigningCertURL));\n\t\t}\n\t}\n\n\tconst verifier = createVerify(algorithm);\n\tverifier.update(snsStringToSign(message), 'utf8');\n\tif (!verifier.verify(await certificate, message.Signature, 'base64')) {\n\t\tthrow new SnsSignatureInvalid(message.MessageId, message.TopicArn);\n\t}\n}\n\n/**\n * Confirm a subscription by visiting the URL SNS sent. Until this happens SNS\n * delivers nothing to the endpoint.\n */\nexport async function confirmSnsSubscription(\n\tmessage: SnsHttpMessage,\n\trequest: (url: string) => Promise<{ ok: boolean; status: number }> = fetch,\n): Promise<void> {\n\tif (message.Type !== 'SubscriptionConfirmation' || !message.SubscribeURL) {\n\t\tthrow new SnsNotAConfirmation(message.Type);\n\t}\n\n\tconst response = await request(message.SubscribeURL);\n\tif (!response.ok) {\n\t\tthrow new SnsConfirmationFailed(message.TopicArn, response.status);\n\t}\n}\n\nexport interface SubscribeHttpEndpointOptions {\n\t/** The URL SNS POSTs to — `http:` or `https:`, which picks the protocol. */\n\tendpoint: string;\n\t/**\n\t * The event types this endpoint takes. Becomes the subscription's filter\n\t * policy on the `type` attribute `SNSPublisher` sets, so SNS sends nothing\n\t * else — fan-out to every subscriber, each seeing only what it named.\n\t */\n\tevents: readonly string[];\n\t/**\n\t * An SQS queue's ARN for what SNS gives up delivering. SNS retries an HTTP\n\t * endpoint for a limited time and then drops the message, so without one an\n\t * outage longer than that loses events.\n\t */\n\tdeadLetterQueueArn?: string;\n}\n\n/**\n * Subscribe an HTTP endpoint to the connection's topic, converging on one\n * confirmed subscription with the filter asked for — so it is safe on every\n * start:\n *\n * - **none yet** — subscribed; SNS sends a confirmation to the endpoint.\n * - **confirmed** — its filter and dead-letter queue are brought up to date.\n * Subscribing again with different attributes is an error on SNS, and the\n * events a subscriber names change.\n * - **stuck pending** — a confirmation that never landed (the endpoint was\n * down, or answered wrong). Subscribing the same endpoint again is a no-op\n * on an emulator, so it is dropped and subscribed afresh, which sends a new\n * confirmation.\n */\nexport async function subscribeHttpEndpoint(\n\tconnection: SNSConnection,\n\toptions: SubscribeHttpEndpointOptions,\n): Promise<string | undefined> {\n\tconst protocol = new URL(options.endpoint).protocol.replace(':', '');\n\tif (protocol !== 'http' && protocol !== 'https') {\n\t\tthrow new SnsEndpointNotHttp(options.endpoint);\n\t}\n\n\tconst attributes: Record<string, string> = {\n\t\tFilterPolicyScope: 'MessageAttributes',\n\t\tFilterPolicy: JSON.stringify({ type: [...options.events] }),\n\t};\n\tif (options.deadLetterQueueArn) {\n\t\tattributes.RedrivePolicy = JSON.stringify({\n\t\t\tdeadLetterTargetArn: options.deadLetterQueueArn,\n\t\t});\n\t}\n\n\tconst client = connection.snsClient;\n\tconst existing = await existingSubscription(connection, options.endpoint);\n\tif (existing) {\n\t\tconst { Attributes = {} } = await client.send(\n\t\t\tnew GetSubscriptionAttributesCommand({ SubscriptionArn: existing }),\n\t\t);\n\t\tif (Attributes.PendingConfirmation === 'true') {\n\t\t\tawait client.send(new UnsubscribeCommand({ SubscriptionArn: existing }));\n\t\t} else {\n\t\t\tfor (const [name, value] of Object.entries(attributes)) {\n\t\t\t\tif (Attributes[name] === value) continue;\n\t\t\t\tawait client.send(\n\t\t\t\t\tnew SetSubscriptionAttributesCommand({\n\t\t\t\t\t\tSubscriptionArn: existing,\n\t\t\t\t\t\tAttributeName: name,\n\t\t\t\t\t\tAttributeValue: value,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn existing;\n\t\t}\n\t}\n\n\tconst { SubscriptionArn } = await client.send(\n\t\tnew SubscribeCommand({\n\t\t\tTopicArn: connection.topicArn,\n\t\t\tProtocol: protocol,\n\t\t\tEndpoint: options.endpoint,\n\t\t\tAttributes: attributes as SubscribeCommandInput['Attributes'],\n\t\t\tReturnSubscriptionArn: true,\n\t\t}),\n\t);\n\treturn SubscriptionArn;\n}\n\n/**\n * The ARN of the endpoint's subscription to the topic, if it has one SNS will\n * name. (Real SNS lists a pending one as `PendingConfirmation`, with no ARN to\n * act on — and resends its confirmation on a new Subscribe anyway.)\n */\nasync function existingSubscription(\n\tconnection: SNSConnection,\n\tendpoint: string,\n): Promise<string | undefined> {\n\tlet NextToken: string | undefined;\n\tdo {\n\t\tconst page = await connection.snsClient.send(\n\t\t\tnew ListSubscriptionsByTopicCommand({\n\t\t\t\tTopicArn: connection.topicArn,\n\t\t\t\tNextToken,\n\t\t\t}),\n\t\t);\n\t\tconst match = page.Subscriptions?.find(\n\t\t\t(s) => s.Endpoint === endpoint && s.SubscriptionArn?.startsWith('arn:'),\n\t\t);\n\t\tif (match) return match.SubscriptionArn;\n\t\tNextToken = page.NextToken;\n\t} while (NextToken);\n\treturn undefined;\n}\n\n/**\n * A pushed notification as the Lambda event SNS would have invoked a function\n * with, so it can be handed to the same adaptor.\n */\nexport function toSnsEvent(message: SnsHttpMessage, subscriptionArn = '') {\n\treturn {\n\t\tRecords: [\n\t\t\t{\n\t\t\t\tEventSource: 'aws:sns',\n\t\t\t\tEventVersion: '1.0',\n\t\t\t\tEventSubscriptionArn: subscriptionArn,\n\t\t\t\tSns: {\n\t\t\t\t\tType: message.Type,\n\t\t\t\t\tMessageId: message.MessageId,\n\t\t\t\t\tTopicArn: message.TopicArn,\n\t\t\t\t\tSubject: message.Subject ?? null,\n\t\t\t\t\tMessage: message.Message,\n\t\t\t\t\tTimestamp: message.Timestamp,\n\t\t\t\t\tSignatureVersion: message.SignatureVersion,\n\t\t\t\t\tSignature: message.Signature,\n\t\t\t\t\t// Lambda spells these two differently from the HTTP envelope.\n\t\t\t\t\tSigningCertUrl: message.SigningCertURL,\n\t\t\t\t\tUnsubscribeUrl: message.UnsubscribeURL ?? '',\n\t\t\t\t\tMessageAttributes: message.MessageAttributes ?? {},\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t};\n}\n\nexport class SnsCertificateUntrusted extends Error {\n\tconstructor(readonly url: string) {\n\t\tsuper(\n\t\t\t`The SNS message names a signing certificate at '${url}', which is not ` +\n\t\t\t\t'an https URL on sns.<region>.amazonaws.com. Only SNS can send to ' +\n\t\t\t\t'this endpoint; reject the request.',\n\t\t);\n\t\tthis.name = 'SnsCertificateUntrusted';\n\t}\n}\n\nexport class SnsCertificateUnavailable extends Error {\n\tconstructor(\n\t\treadonly url: string,\n\t\treadonly status: number,\n\t) {\n\t\tsuper(\n\t\t\t`Fetching the SNS signing certificate at '${url}' answered ${status}. ` +\n\t\t\t\t'The message cannot be verified, so it is not handled; SNS will retry.',\n\t\t);\n\t\tthis.name = 'SnsCertificateUnavailable';\n\t}\n}\n\nexport class SnsSignatureVersionUnknown extends Error {\n\tconstructor(readonly version: string) {\n\t\tsuper(\n\t\t\t`SNS signature version '${version}' is not one this endpoint verifies ` +\n\t\t\t\t\"(it knows '1' and '2'). Reject the request.\",\n\t\t);\n\t\tthis.name = 'SnsSignatureVersionUnknown';\n\t}\n}\n\nexport class SnsSignatureInvalid extends Error {\n\tconstructor(\n\t\treadonly messageId: string,\n\t\treadonly topicArn: string,\n\t) {\n\t\tsuper(\n\t\t\t`SNS message '${messageId}' from '${topicArn}' does not verify against ` +\n\t\t\t\t'its signing certificate. It was not sent by SNS, or was altered ' +\n\t\t\t\t'on the way; reject it.',\n\t\t);\n\t\tthis.name = 'SnsSignatureInvalid';\n\t}\n}\n\nexport class SnsMessageTypeUnknown extends Error {\n\tconstructor(readonly type: string) {\n\t\tsuper(\n\t\t\t`'${type}' is not an SNS message type (Notification, ` +\n\t\t\t\t'SubscriptionConfirmation, UnsubscribeConfirmation). Reject it.',\n\t\t);\n\t\tthis.name = 'SnsMessageTypeUnknown';\n\t}\n}\n\nexport class SnsNotAConfirmation extends Error {\n\tconstructor(readonly type: string) {\n\t\tsuper(\n\t\t\t`Only a SubscriptionConfirmation carries a URL to confirm; this is a ` +\n\t\t\t\t`'${type}'.`,\n\t\t);\n\t\tthis.name = 'SnsNotAConfirmation';\n\t}\n}\n\nexport class SnsConfirmationFailed extends Error {\n\tconstructor(\n\t\treadonly topicArn: string,\n\t\treadonly status: number,\n\t) {\n\t\tsuper(\n\t\t\t`Confirming the subscription to '${topicArn}' answered ${status}. ` +\n\t\t\t\t'SNS will deliver nothing until it is confirmed; it resends the ' +\n\t\t\t\t'confirmation, or subscribe the endpoint again.',\n\t\t);\n\t\tthis.name = 'SnsConfirmationFailed';\n\t}\n}\n\nexport class SnsEndpointNotHttp extends Error {\n\tconstructor(readonly endpoint: string) {\n\t\tsuper(\n\t\t\t`'${endpoint}' is not an http(s) URL. SNS pushes to an HTTP endpoint; ` +\n\t\t\t\t'subscribe a queue for anything else.',\n\t\t);\n\t\tthis.name = 'SnsEndpointNotHttp';\n\t}\n}\n","import { SNSClient, type SNSClientConfig } from '@aws-sdk/client-sns';\nimport type { EventConnection } from '../types';\nimport { EventPublisherType } from '../types';\nimport * as snsUrl from './snsUrl';\n\nexport interface SNSConnectionConfig {\n\ttopicArn: string;\n\tregion?: string;\n\tendpoint?: string; // Custom endpoint (e.g., for LocalStack)\n\tcredentials?: {\n\t\taccessKeyId: string;\n\t\tsecretAccessKey: string;\n\t\tsessionToken?: string;\n\t};\n}\n\nexport class SNSConnection implements EventConnection {\n\treadonly type = EventPublisherType.SNS;\n\tprivate client: SNSClient;\n\tprivate connected = false;\n\tprivate config: SNSConnectionConfig;\n\n\tconstructor(config: SNSConnectionConfig) {\n\t\tthis.config = config;\n\n\t\tconst clientConfig: SNSClientConfig = {\n\t\t\tregion: config.region,\n\t\t\tendpoint: config.endpoint,\n\t\t\tcredentials: config.credentials,\n\t\t};\n\n\t\tthis.client = new SNSClient(clientConfig);\n\t}\n\n\t/**\n\t * Create an SNSConnection from a connection string\n\t * Format: sns://?topicArn=arn:aws:sns:region:account:topic&region=us-east-1&endpoint=http://localhost:4566\n\t */\n\tstatic async fromConnectionString(\n\t\tconnectionString: string,\n\t): Promise<SNSConnection> {\n\t\t// Through the codec, not a second hand-rolled parse: the deploy target\n\t\t// composes these with `snsUrl.build`, and a string it cannot read back is\n\t\t// exactly the drift one shared module rules out.\n\t\tconst { topicArn, region, endpoint } = snsUrl.parse(connectionString);\n\t\tconst params = new URL(connectionString).searchParams;\n\n\t\tconst config: SNSConnectionConfig = {\n\t\t\ttopicArn,\n\t\t\tregion,\n\t\t\tendpoint,\n\t\t};\n\n\t\t// Credentials are not part of the address — see `snsUrl` — but a caller\n\t\t// may still pin them here, so they are read beside it rather than in it.\n\t\tconst accessKeyId = params.get('accessKeyId');\n\t\tconst secretAccessKey = params.get('secretAccessKey');\n\t\tif (accessKeyId && secretAccessKey) {\n\t\t\tconfig.credentials = {\n\t\t\t\taccessKeyId,\n\t\t\t\tsecretAccessKey,\n\t\t\t\tsessionToken: params.get('sessionToken') || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst connection = new SNSConnection(config);\n\t\tawait connection.connect();\n\t\treturn connection;\n\t}\n\n\tasync connect(): Promise<void> {\n\t\t// SNS doesn't have a traditional connection, but we mark as connected\n\t\tthis.connected = true;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.client.destroy();\n\t\tthis.connected = false;\n\t}\n\n\tisConnected(): boolean {\n\t\treturn this.connected;\n\t}\n\n\t/**\n\t * Get the underlying SNS client\n\t */\n\tget snsClient(): SNSClient {\n\t\treturn this.client;\n\t}\n\n\t/**\n\t * Get the topic ARN for this connection\n\t */\n\tget topicArn(): string {\n\t\treturn this.config.topicArn;\n\t}\n}\n","import { PublishCommand } from '@aws-sdk/client-sns';\nimport type { EventPublisher, PublishableMessage } from '../types';\nimport type { SNSConnection } from './SNSConnection';\n\nexport type SNSPublisherOptions = {};\n\nexport class SNSPublisher<TMessage extends PublishableMessage<string, any>>\n\timplements EventPublisher<TMessage>\n{\n\tconstructor(\n\t\tprivate connection: SNSConnection,\n\t\t_options: SNSPublisherOptions = {},\n\t) {}\n\n\t/**\n\t * Create an SNSPublisher from a connection string\n\t * Format: sns://?topicArn=arn:aws:sns:region:account:topic&region=us-east-1&endpoint=http://localhost:4566\n\t */\n\tstatic async fromConnectionString<\n\t\tTMessage extends PublishableMessage<string, any>,\n\t>(connectionString: string): Promise<SNSPublisher<TMessage>> {\n\t\tconst { SNSConnection } = await import('./SNSConnection');\n\t\tconst connection =\n\t\t\tawait SNSConnection.fromConnectionString(connectionString);\n\n\t\treturn new SNSPublisher<TMessage>(connection);\n\t}\n\n\tasync publish(messages: TMessage[]): Promise<void> {\n\t\tif (messages.length === 0) return;\n\n\t\tif (!this.connection.isConnected()) {\n\t\t\tawait this.connection.connect();\n\t\t}\n\n\t\t// Publish all messages\n\t\tawait Promise.all(messages.map((message) => this.publishMessage(message)));\n\t}\n\n\tprivate async publishMessage(message: TMessage): Promise<void> {\n\t\tconst command = new PublishCommand({\n\t\t\tTopicArn: this.connection.topicArn,\n\t\t\tMessage: JSON.stringify({\n\t\t\t\ttype: message.type,\n\t\t\t\tpayload: message.payload,\n\t\t\t}),\n\t\t\tMessageAttributes: {\n\t\t\t\ttype: {\n\t\t\t\t\tDataType: 'String',\n\t\t\t\t\tStringValue: message.type,\n\t\t\t\t},\n\t\t\t},\n\t\t});\n\n\t\tawait this.connection.snsClient.send(command);\n\t}\n\n\tasync close(): Promise<void> {\n\t\t// Publisher doesn't own the connection\n\t\t// Connection should be closed by whoever created it\n\t}\n}\n","import { SubscribeCommand, UnsubscribeCommand } from '@aws-sdk/client-sns';\nimport {\n\tCreateQueueCommand,\n\tDeleteQueueCommand,\n\tGetQueueAttributesCommand,\n\tSetQueueAttributesCommand,\n\tSQSClient,\n} from '@aws-sdk/client-sqs';\nimport { SQSConnection } from '../sqs/SQSConnection';\nimport { SQSSubscriber } from '../sqs/SQSSubscriber';\nimport type { EventSubscriber, PublishableMessage } from '../types';\nimport type { SNSConnection } from './SNSConnection';\n\nexport interface SNSSubscriberOptions {\n\tqueueName?: string; // Optional queue name (defaults to auto-generated)\n\tcreateQueue?: boolean; // Auto-create SQS queue (default: true)\n\tdeleteQueueOnClose?: boolean; // Delete queue when subscriber closes (default: false for named queues, true for auto-generated)\n\twaitTimeSeconds?: number; // SQS long polling (default: 20)\n\tmaxMessages?: number; // Max messages per poll (default: 10)\n}\n\n/**\n * SNS Subscriber - receives push-based messages from SNS via SQS queue\n *\n * Architecture:\n * SNS Topic → (push) → SQS Queue → Subscriber\n *\n * This subscriber automatically:\n * 1. Creates an SQS queue (or uses existing one)\n * 2. Subscribes the queue to the SNS topic\n * 3. Receives messages pushed from SNS to the queue\n * 4. Filters messages by topic ARN and message type\n * 5. Processes and acknowledges messages\n *\n * Note: While SQS long-polling is used internally to receive messages,\n * this is an implementation detail of the SNS push delivery mechanism.\n * Messages are pushed from SNS to SQS, and we receive those pushed messages.\n */\nexport class SNSSubscriber<TMessage extends PublishableMessage<string, any>>\n\timplements EventSubscriber<TMessage>\n{\n\tprivate sqsClient: SQSClient;\n\tprivate queueUrl?: string;\n\tprivate queueArn?: string;\n\tprivate subscriptionArn?: string;\n\tprivate sqsSubscriber?: SQSSubscriber<TMessage>;\n\tprivate options: Required<SNSSubscriberOptions>;\n\n\tconstructor(\n\t\tprivate connection: SNSConnection,\n\t\toptions: SNSSubscriberOptions = {},\n\t) {\n\t\tthis.options = {\n\t\t\tqueueName:\n\t\t\t\toptions.queueName ||\n\t\t\t\t`sns-sub-${Date.now()}-${Math.random().toString(36).substring(7)}`,\n\t\t\tcreateQueue: options.createQueue ?? true,\n\t\t\tdeleteQueueOnClose: options.deleteQueueOnClose ?? !options.queueName, // Delete auto-generated queues\n\t\t\twaitTimeSeconds: options.waitTimeSeconds ?? 20,\n\t\t\tmaxMessages: options.maxMessages ?? 10,\n\t\t};\n\n\t\t// Create SQS client with same config as SNS\n\t\tthis.sqsClient = new SQSClient({\n\t\t\tregion: (this.connection as any).config.region,\n\t\t\tendpoint: (this.connection as any).config.endpoint,\n\t\t\tcredentials: (this.connection as any).config.credentials,\n\t\t});\n\t}\n\n\t/**\n\t * Create an SNSSubscriber from a connection string\n\t * Format: sns://?topicArn=...&queueName=myqueue&region=us-east-1\n\t */\n\tstatic async fromConnectionString<\n\t\tTMessage extends PublishableMessage<string, any>,\n\t>(connectionString: string): Promise<SNSSubscriber<TMessage>> {\n\t\tconst url = new URL(connectionString);\n\t\tconst params = url.searchParams;\n\n\t\tconst { SNSConnection } = await import('./SNSConnection');\n\t\tconst connection =\n\t\t\tawait SNSConnection.fromConnectionString(connectionString);\n\n\t\tconst options: SNSSubscriberOptions = {\n\t\t\tqueueName: params.get('queueName') || undefined,\n\t\t\tcreateQueue: params.get('createQueue') !== 'false',\n\t\t\tdeleteQueueOnClose: params.get('deleteQueueOnClose') === 'true',\n\t\t\twaitTimeSeconds: params.get('waitTimeSeconds')\n\t\t\t\t? Number.parseInt(params.get('waitTimeSeconds')!, 10)\n\t\t\t\t: undefined,\n\t\t};\n\n\t\treturn new SNSSubscriber<TMessage>(connection, options);\n\t}\n\n\tasync subscribe(\n\t\tmessages: TMessage['type'][],\n\t\tlistener: (payload: TMessage) => Promise<void>,\n\t): Promise<void> {\n\t\t// Setup SQS queue and SNS subscription\n\t\tawait this.setupQueue();\n\t\tawait this.subscribeToTopic();\n\n\t\t// Create SQS connection for the subscriber\n\t\tconst sqsConnection = new SQSConnection({\n\t\t\tqueueUrl: this.queueUrl!,\n\t\t\tregion: (this.connection as any).config.region,\n\t\t\tendpoint: (this.connection as any).config.endpoint,\n\t\t\tcredentials: (this.connection as any).config.credentials,\n\t\t});\n\t\tawait sqsConnection.connect();\n\n\t\t// Use SQSSubscriber to receive messages, with topic filtering\n\t\tthis.sqsSubscriber = new SQSSubscriber<TMessage>(sqsConnection, {\n\t\t\twaitTimeSeconds: this.options.waitTimeSeconds,\n\t\t\tmaxMessages: this.options.maxMessages,\n\t\t\texpectedTopicArn: this.connection.topicArn,\n\t\t});\n\n\t\tawait this.sqsSubscriber.subscribe(messages, listener);\n\t}\n\n\t/**\n\t * Stop receiving messages and clean up resources\n\t */\n\tasync stop(): Promise<void> {\n\t\t// Stop SQS subscriber\n\t\tif (this.sqsSubscriber) {\n\t\t\tthis.sqsSubscriber.stop();\n\t\t}\n\n\t\t// Unsubscribe from topic\n\t\tif (this.subscriptionArn) {\n\t\t\ttry {\n\t\t\t\tconst command = new UnsubscribeCommand({\n\t\t\t\t\tSubscriptionArn: this.subscriptionArn,\n\t\t\t\t});\n\t\t\t\tawait this.connection.snsClient.send(command);\n\t\t\t} catch (_error) {}\n\t\t}\n\n\t\t// Delete queue if configured\n\t\tif (this.queueUrl && this.options.deleteQueueOnClose) {\n\t\t\ttry {\n\t\t\t\tconst command = new DeleteQueueCommand({\n\t\t\t\t\tQueueUrl: this.queueUrl,\n\t\t\t\t});\n\t\t\t\tawait this.sqsClient.send(command);\n\t\t\t} catch (_error) {}\n\t\t}\n\n\t\tthis.sqsClient.destroy();\n\t}\n\n\tprivate async setupQueue(): Promise<void> {\n\t\tif (this.options.createQueue) {\n\t\t\t// Create queue\n\t\t\tconst createCommand = new CreateQueueCommand({\n\t\t\t\tQueueName: this.options.queueName,\n\t\t\t});\n\t\t\tconst response = await this.sqsClient.send(createCommand);\n\t\t\tthis.queueUrl = response.QueueUrl!;\n\t\t} else {\n\t\t\tthrow new SnsQueueMissing(this.options.queueName);\n\t\t}\n\n\t\t// Get queue ARN\n\t\tconst attrsCommand = new GetQueueAttributesCommand({\n\t\t\tQueueUrl: this.queueUrl,\n\t\t\tAttributeNames: ['QueueArn'],\n\t\t});\n\t\tconst attrsResponse = await this.sqsClient.send(attrsCommand);\n\t\tthis.queueArn = attrsResponse.Attributes?.QueueArn!;\n\n\t\t// Set queue policy to allow SNS\n\t\tawait this.setQueuePolicy();\n\t}\n\n\tprivate async setQueuePolicy(): Promise<void> {\n\t\tconst policy = {\n\t\t\tVersion: '2012-10-17',\n\t\t\tStatement: [\n\t\t\t\t{\n\t\t\t\t\tEffect: 'Allow',\n\t\t\t\t\tPrincipal: '*',\n\t\t\t\t\tAction: 'SQS:SendMessage',\n\t\t\t\t\tResource: this.queueArn,\n\t\t\t\t\tCondition: {\n\t\t\t\t\t\tArnEquals: {\n\t\t\t\t\t\t\t'aws:SourceArn': this.connection.topicArn,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\n\t\tconst command = new SetQueueAttributesCommand({\n\t\t\tQueueUrl: this.queueUrl,\n\t\t\tAttributes: {\n\t\t\t\tPolicy: JSON.stringify(policy),\n\t\t\t},\n\t\t});\n\n\t\tawait this.sqsClient.send(command);\n\t}\n\n\tprivate async subscribeToTopic(): Promise<void> {\n\t\tconst command = new SubscribeCommand({\n\t\t\tTopicArn: this.connection.topicArn,\n\t\t\tProtocol: 'sqs',\n\t\t\tEndpoint: this.queueArn,\n\t\t});\n\n\t\tconst response = await this.connection.snsClient.send(command);\n\t\tthis.subscriptionArn = response.SubscriptionArn!;\n\t}\n}\n\n/**\n * A subscriber asked to use a queue it may not create. The SQS queue behind an\n * SNS subscription has to exist first, or `createQueue` has to allow it.\n */\nexport class SnsQueueMissing extends Error {\n\tconstructor(readonly queueName: string | undefined) {\n\t\tsuper(\n\t\t\t`The SNS subscriber's queue${queueName ? ` \"${queueName}\"` : ''} is not created here: pass createQueue: true, or create the queue first.`,\n\t\t);\n\t\tthis.name = 'SnsQueueMissing';\n\t}\n}\n"],"mappings":"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"}