@geekmidas/events 10.0.0-alpha.4 → 10.0.0-alpha.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PgBossConnection-CywKoAZU.cjs +2 -0
- package/dist/PgBossConnection-DjoqT84r.mjs +2 -0
- package/dist/RabbitMQConnection-C9eccBL8.cjs +2 -0
- package/dist/RabbitMQConnection-CAp7VqyT.mjs +2 -0
- package/dist/SNSConnection-BWDC1wAF.cjs +2 -0
- package/dist/SNSConnection-ezQvVrDv.mjs +2 -0
- package/dist/{SQSSubscriber-BMWZ3s5q.mjs → SQSSubscriber-BXLE_1L3.mjs} +73 -7
- package/dist/SQSSubscriber-BXLE_1L3.mjs.map +1 -0
- package/dist/{SQSSubscriber-YXPlg2rm.cjs → SQSSubscriber-DtRUaZB3.cjs} +91 -14
- package/dist/SQSSubscriber-DtRUaZB3.cjs.map +1 -0
- package/dist/basic/index.cjs +7 -8
- package/dist/basic/index.cjs.map +1 -1
- package/dist/basic/index.d.cts +4 -10
- package/dist/basic/index.d.cts.map +1 -1
- package/dist/basic/index.d.mts +4 -10
- package/dist/basic/index.d.mts.map +1 -1
- package/dist/basic/index.mjs +5 -6
- package/dist/basic/index.mjs.map +1 -1
- package/dist/index.cjs +43 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -11
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +26 -11
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +42 -51
- package/dist/index.mjs.map +1 -1
- package/dist/pgboss/index.cjs +145 -28
- package/dist/pgboss/index.cjs.map +1 -1
- package/dist/pgboss/index.d.cts +39 -12
- package/dist/pgboss/index.d.cts.map +1 -1
- package/dist/pgboss/index.d.mts +39 -12
- package/dist/pgboss/index.d.mts.map +1 -1
- package/dist/pgboss/index.mjs +140 -28
- package/dist/pgboss/index.mjs.map +1 -1
- package/dist/rabbitmq/index.cjs +135 -22
- package/dist/rabbitmq/index.cjs.map +1 -1
- package/dist/rabbitmq/index.d.cts +11 -10
- package/dist/rabbitmq/index.d.cts.map +1 -1
- package/dist/rabbitmq/index.d.mts +11 -10
- package/dist/rabbitmq/index.d.mts.map +1 -1
- package/dist/rabbitmq/index.mjs +130 -22
- package/dist/rabbitmq/index.mjs.map +1 -1
- package/dist/{chunk-CsX-DzYB.cjs → rolldown-runtime-C0BPl7ul.cjs} +17 -16
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/sns/index.cjs +371 -34
- package/dist/sns/index.cjs.map +1 -1
- package/dist/sns/index.d.cts +167 -11
- package/dist/sns/index.d.cts.map +1 -1
- package/dist/sns/index.d.mts +167 -11
- package/dist/sns/index.d.mts.map +1 -1
- package/dist/sns/index.mjs +340 -18
- package/dist/sns/index.mjs.map +1 -1
- package/dist/sns/snsUrl.cjs +91 -6
- package/dist/sns/snsUrl.cjs.map +1 -0
- package/dist/sns/snsUrl.d.cts +57 -2
- package/dist/sns/snsUrl.d.cts.map +1 -0
- package/dist/sns/snsUrl.d.mts +57 -2
- package/dist/sns/snsUrl.d.mts.map +1 -0
- package/dist/sns/snsUrl.mjs +81 -2
- package/dist/sns/snsUrl.mjs.map +1 -0
- package/dist/sqs/index.cjs +34 -24
- package/dist/sqs/index.cjs.map +1 -1
- package/dist/sqs/index.d.cts +19 -11
- package/dist/sqs/index.d.cts.map +1 -1
- package/dist/sqs/index.d.mts +19 -11
- package/dist/sqs/index.d.mts.map +1 -1
- package/dist/sqs/index.mjs +22 -15
- package/dist/sqs/index.mjs.map +1 -1
- package/dist/sqs/sqsUrl.cjs +92 -6
- package/dist/sqs/sqsUrl.cjs.map +1 -0
- package/dist/sqs/sqsUrl.d.cts +56 -2
- package/dist/sqs/sqsUrl.d.cts.map +1 -0
- package/dist/sqs/sqsUrl.d.mts +56 -2
- package/dist/sqs/sqsUrl.d.mts.map +1 -0
- package/dist/sqs/sqsUrl.mjs +82 -2
- package/dist/sqs/sqsUrl.mjs.map +1 -0
- package/dist/types-BcZjLxVV.cjs +51 -0
- package/dist/types-BcZjLxVV.cjs.map +1 -0
- package/dist/types-Bk5JM6MP.mjs +40 -0
- package/dist/types-Bk5JM6MP.mjs.map +1 -0
- package/dist/types-gGaoT8qc.d.cts +149 -0
- package/dist/types-gGaoT8qc.d.cts.map +1 -0
- package/dist/types-gGaoT8qc.d.mts +149 -0
- package/dist/types-gGaoT8qc.d.mts.map +1 -0
- package/package.json +3 -3
- package/dist/PgBossConnection-C3qYNfm5.cjs +0 -80
- package/dist/PgBossConnection-C3qYNfm5.cjs.map +0 -1
- package/dist/PgBossConnection-Cvgv2l1G.mjs +0 -74
- package/dist/PgBossConnection-Cvgv2l1G.mjs.map +0 -1
- package/dist/PgBossConnection-DCUxQLZz.cjs +0 -4
- package/dist/PgBossConnection-DbO0ypJf.mjs +0 -4
- package/dist/RabbitMQConnection-8PbOD8hM.cjs +0 -4
- package/dist/RabbitMQConnection-Bpx3Lgij.mjs +0 -4
- package/dist/RabbitMQConnection-DGAk4aUb.cjs +0 -113
- package/dist/RabbitMQConnection-DGAk4aUb.cjs.map +0 -1
- package/dist/RabbitMQConnection-aE0dtb63.mjs +0 -107
- package/dist/RabbitMQConnection-aE0dtb63.mjs.map +0 -1
- package/dist/SNSConnection-B2K86hpz.cjs +0 -75
- package/dist/SNSConnection-B2K86hpz.cjs.map +0 -1
- package/dist/SNSConnection-BVo84FeW.mjs +0 -5
- package/dist/SNSConnection-DIHRjrQF.cjs +0 -5
- package/dist/SNSConnection-v9jjF99w.mjs +0 -69
- package/dist/SNSConnection-v9jjF99w.mjs.map +0 -1
- package/dist/SQSConnection-4tBMkVZE.mjs +0 -69
- package/dist/SQSConnection-4tBMkVZE.mjs.map +0 -1
- package/dist/SQSConnection-BBLAvF-1.cjs +0 -5
- package/dist/SQSConnection-BaC7rGhI.cjs +0 -75
- package/dist/SQSConnection-BaC7rGhI.cjs.map +0 -1
- package/dist/SQSConnection-DbavOq8_.mjs +0 -5
- package/dist/SQSSubscriber-BMWZ3s5q.mjs.map +0 -1
- package/dist/SQSSubscriber-YXPlg2rm.cjs.map +0 -1
- package/dist/chunk-B9dir_RE.mjs +0 -11
- package/dist/snsUrl-ASdc0D1v.d.cts +0 -61
- package/dist/snsUrl-ASdc0D1v.d.cts.map +0 -1
- package/dist/snsUrl-CifYhPAa.mjs +0 -79
- package/dist/snsUrl-CifYhPAa.mjs.map +0 -1
- package/dist/snsUrl-FRbRcwF0.d.mts +0 -61
- package/dist/snsUrl-FRbRcwF0.d.mts.map +0 -1
- package/dist/snsUrl-f2gU3CzK.cjs +0 -114
- package/dist/snsUrl-f2gU3CzK.cjs.map +0 -1
- package/dist/sqsUrl-BV6zS-mi.d.mts +0 -60
- package/dist/sqsUrl-BV6zS-mi.d.mts.map +0 -1
- package/dist/sqsUrl-CbAsBFHI.mjs +0 -80
- package/dist/sqsUrl-CbAsBFHI.mjs.map +0 -1
- package/dist/sqsUrl-CdisN3GU.cjs +0 -115
- package/dist/sqsUrl-CdisN3GU.cjs.map +0 -1
- package/dist/sqsUrl-DZDPHxIk.d.cts +0 -60
- package/dist/sqsUrl-DZDPHxIk.d.cts.map +0 -1
- package/dist/types-BmixGrOD.d.cts +0 -67
- package/dist/types-BmixGrOD.d.cts.map +0 -1
- package/dist/types-Byv8N9ma.cjs +0 -24
- package/dist/types-Byv8N9ma.cjs.map +0 -1
- package/dist/types-CYDt-2RQ.mjs +0 -18
- package/dist/types-CYDt-2RQ.mjs.map +0 -1
- package/dist/types-DycjsYms.d.mts +0 -67
- package/dist/types-DycjsYms.d.mts.map +0 -1
package/dist/sns/index.mjs
CHANGED
|
@@ -1,14 +1,325 @@
|
|
|
1
|
-
import "../types-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import "../sqsUrl-CbAsBFHI.mjs";
|
|
5
|
-
import { SQSConnection } from "../SQSConnection-4tBMkVZE.mjs";
|
|
6
|
-
import { SQSSubscriber } from "../SQSSubscriber-BMWZ3s5q.mjs";
|
|
7
|
-
import { PublishCommand, SubscribeCommand, UnsubscribeCommand } from "@aws-sdk/client-sns";
|
|
1
|
+
import "../types-Bk5JM6MP.mjs";
|
|
2
|
+
import { n as SQSConnection, t as SQSSubscriber } from "../SQSSubscriber-BXLE_1L3.mjs";
|
|
3
|
+
import { parse, t as snsUrl_exports } from "./snsUrl.mjs";
|
|
8
4
|
import { CreateQueueCommand, DeleteQueueCommand, GetQueueAttributesCommand, SQSClient, SetQueueAttributesCommand } from "@aws-sdk/client-sqs";
|
|
9
|
-
|
|
5
|
+
import { createVerify } from "node:crypto";
|
|
6
|
+
import { GetSubscriptionAttributesCommand, ListSubscriptionsByTopicCommand, PublishCommand, SNSClient, SetSubscriptionAttributesCommand, SubscribeCommand, UnsubscribeCommand } from "@aws-sdk/client-sns";
|
|
7
|
+
//#region src/sns/push.ts
|
|
8
|
+
/** The fields signed, in the order SNS signs them, per message type. */
|
|
9
|
+
const SIGNED_FIELDS = {
|
|
10
|
+
Notification: [
|
|
11
|
+
"Message",
|
|
12
|
+
"MessageId",
|
|
13
|
+
"Subject",
|
|
14
|
+
"Timestamp",
|
|
15
|
+
"TopicArn",
|
|
16
|
+
"Type"
|
|
17
|
+
],
|
|
18
|
+
SubscriptionConfirmation: [
|
|
19
|
+
"Message",
|
|
20
|
+
"MessageId",
|
|
21
|
+
"SubscribeURL",
|
|
22
|
+
"Timestamp",
|
|
23
|
+
"Token",
|
|
24
|
+
"TopicArn",
|
|
25
|
+
"Type"
|
|
26
|
+
],
|
|
27
|
+
UnsubscribeConfirmation: [
|
|
28
|
+
"Message",
|
|
29
|
+
"MessageId",
|
|
30
|
+
"SubscribeURL",
|
|
31
|
+
"Timestamp",
|
|
32
|
+
"Token",
|
|
33
|
+
"TopicArn",
|
|
34
|
+
"Type"
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Where SNS serves its signing certificates. Anywhere else is a certificate
|
|
39
|
+
* the sender chose, which would let the sender sign its own forgeries.
|
|
40
|
+
*/
|
|
41
|
+
const SNS_CERTIFICATE_HOST = /^sns\.[a-z0-9-]+\.amazonaws\.com(\.cn)?$/;
|
|
42
|
+
const certificates = /* @__PURE__ */ new Map();
|
|
43
|
+
async function defaultFetchCertificate(url) {
|
|
44
|
+
const response = await fetch(url);
|
|
45
|
+
if (!response.ok) throw new SnsCertificateUnavailable(url, response.status);
|
|
46
|
+
return response.text();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A URL, or undefined for a value that is not one at all — what an emulator
|
|
50
|
+
* sends — so it is untrusted rather than a crash. (`URL.parse` is Node 22.1+.)
|
|
51
|
+
*/
|
|
52
|
+
function parseUrl(value) {
|
|
53
|
+
try {
|
|
54
|
+
return new URL(String(value));
|
|
55
|
+
} catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** The string SNS signed for this message. */
|
|
60
|
+
function snsStringToSign(message) {
|
|
61
|
+
const fields = SIGNED_FIELDS[message.Type];
|
|
62
|
+
if (!fields) throw new SnsMessageTypeUnknown(String(message.Type));
|
|
63
|
+
return fields.filter((field) => message[field] !== void 0).map((field) => `${field}\n${message[field]}\n`).join("");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Check that SNS sent this message: the certificate is SNS's own, and the
|
|
67
|
+
* signature over the message's signed fields verifies against it.
|
|
68
|
+
*
|
|
69
|
+
* Throws rather than returning false, so a caller cannot forget to look.
|
|
70
|
+
*/
|
|
71
|
+
async function verifySnsMessage(message, options = {}) {
|
|
72
|
+
const url = parseUrl(message.SigningCertURL);
|
|
73
|
+
if (url?.protocol !== "https:" || !SNS_CERTIFICATE_HOST.test(url.hostname)) throw new SnsCertificateUntrusted(message.SigningCertURL);
|
|
74
|
+
const algorithm = message.SignatureVersion === "1" ? "RSA-SHA1" : message.SignatureVersion === "2" ? "RSA-SHA256" : void 0;
|
|
75
|
+
if (!algorithm) throw new SnsSignatureVersionUnknown(message.SignatureVersion);
|
|
76
|
+
const fetchCertificate = options.fetchCertificate ?? defaultFetchCertificate;
|
|
77
|
+
let certificate = certificates.get(message.SigningCertURL);
|
|
78
|
+
if (!certificate || options.fetchCertificate) {
|
|
79
|
+
certificate = fetchCertificate(message.SigningCertURL);
|
|
80
|
+
if (!options.fetchCertificate) {
|
|
81
|
+
certificates.set(message.SigningCertURL, certificate);
|
|
82
|
+
certificate.catch(() => certificates.delete(message.SigningCertURL));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const verifier = createVerify(algorithm);
|
|
86
|
+
verifier.update(snsStringToSign(message), "utf8");
|
|
87
|
+
if (!verifier.verify(await certificate, message.Signature, "base64")) throw new SnsSignatureInvalid(message.MessageId, message.TopicArn);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Confirm a subscription by visiting the URL SNS sent. Until this happens SNS
|
|
91
|
+
* delivers nothing to the endpoint.
|
|
92
|
+
*/
|
|
93
|
+
async function confirmSnsSubscription(message, request = fetch) {
|
|
94
|
+
if (message.Type !== "SubscriptionConfirmation" || !message.SubscribeURL) throw new SnsNotAConfirmation(message.Type);
|
|
95
|
+
const response = await request(message.SubscribeURL);
|
|
96
|
+
if (!response.ok) throw new SnsConfirmationFailed(message.TopicArn, response.status);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Subscribe an HTTP endpoint to the connection's topic, converging on one
|
|
100
|
+
* confirmed subscription with the filter asked for — so it is safe on every
|
|
101
|
+
* start:
|
|
102
|
+
*
|
|
103
|
+
* - **none yet** — subscribed; SNS sends a confirmation to the endpoint.
|
|
104
|
+
* - **confirmed** — its filter and dead-letter queue are brought up to date.
|
|
105
|
+
* Subscribing again with different attributes is an error on SNS, and the
|
|
106
|
+
* events a subscriber names change.
|
|
107
|
+
* - **stuck pending** — a confirmation that never landed (the endpoint was
|
|
108
|
+
* down, or answered wrong). Subscribing the same endpoint again is a no-op
|
|
109
|
+
* on an emulator, so it is dropped and subscribed afresh, which sends a new
|
|
110
|
+
* confirmation.
|
|
111
|
+
*/
|
|
112
|
+
async function subscribeHttpEndpoint(connection, options) {
|
|
113
|
+
const protocol = new URL(options.endpoint).protocol.replace(":", "");
|
|
114
|
+
if (protocol !== "http" && protocol !== "https") throw new SnsEndpointNotHttp(options.endpoint);
|
|
115
|
+
const attributes = {
|
|
116
|
+
FilterPolicyScope: "MessageAttributes",
|
|
117
|
+
FilterPolicy: JSON.stringify({ type: [...options.events] })
|
|
118
|
+
};
|
|
119
|
+
if (options.deadLetterQueueArn) attributes.RedrivePolicy = JSON.stringify({ deadLetterTargetArn: options.deadLetterQueueArn });
|
|
120
|
+
const client = connection.snsClient;
|
|
121
|
+
const existing = await existingSubscription(connection, options.endpoint);
|
|
122
|
+
if (existing) {
|
|
123
|
+
const { Attributes = {} } = await client.send(new GetSubscriptionAttributesCommand({ SubscriptionArn: existing }));
|
|
124
|
+
if (Attributes.PendingConfirmation === "true") await client.send(new UnsubscribeCommand({ SubscriptionArn: existing }));
|
|
125
|
+
else {
|
|
126
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
127
|
+
if (Attributes[name] === value) continue;
|
|
128
|
+
await client.send(new SetSubscriptionAttributesCommand({
|
|
129
|
+
SubscriptionArn: existing,
|
|
130
|
+
AttributeName: name,
|
|
131
|
+
AttributeValue: value
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
return existing;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const { SubscriptionArn } = await client.send(new SubscribeCommand({
|
|
138
|
+
TopicArn: connection.topicArn,
|
|
139
|
+
Protocol: protocol,
|
|
140
|
+
Endpoint: options.endpoint,
|
|
141
|
+
Attributes: attributes,
|
|
142
|
+
ReturnSubscriptionArn: true
|
|
143
|
+
}));
|
|
144
|
+
return SubscriptionArn;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The ARN of the endpoint's subscription to the topic, if it has one SNS will
|
|
148
|
+
* name. (Real SNS lists a pending one as `PendingConfirmation`, with no ARN to
|
|
149
|
+
* act on — and resends its confirmation on a new Subscribe anyway.)
|
|
150
|
+
*/
|
|
151
|
+
async function existingSubscription(connection, endpoint) {
|
|
152
|
+
let NextToken;
|
|
153
|
+
do {
|
|
154
|
+
const page = await connection.snsClient.send(new ListSubscriptionsByTopicCommand({
|
|
155
|
+
TopicArn: connection.topicArn,
|
|
156
|
+
NextToken
|
|
157
|
+
}));
|
|
158
|
+
const match = page.Subscriptions?.find((s) => s.Endpoint === endpoint && s.SubscriptionArn?.startsWith("arn:"));
|
|
159
|
+
if (match) return match.SubscriptionArn;
|
|
160
|
+
NextToken = page.NextToken;
|
|
161
|
+
} while (NextToken);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A pushed notification as the Lambda event SNS would have invoked a function
|
|
165
|
+
* with, so it can be handed to the same adaptor.
|
|
166
|
+
*/
|
|
167
|
+
function toSnsEvent(message, subscriptionArn = "") {
|
|
168
|
+
return { Records: [{
|
|
169
|
+
EventSource: "aws:sns",
|
|
170
|
+
EventVersion: "1.0",
|
|
171
|
+
EventSubscriptionArn: subscriptionArn,
|
|
172
|
+
Sns: {
|
|
173
|
+
Type: message.Type,
|
|
174
|
+
MessageId: message.MessageId,
|
|
175
|
+
TopicArn: message.TopicArn,
|
|
176
|
+
Subject: message.Subject ?? null,
|
|
177
|
+
Message: message.Message,
|
|
178
|
+
Timestamp: message.Timestamp,
|
|
179
|
+
SignatureVersion: message.SignatureVersion,
|
|
180
|
+
Signature: message.Signature,
|
|
181
|
+
SigningCertUrl: message.SigningCertURL,
|
|
182
|
+
UnsubscribeUrl: message.UnsubscribeURL ?? "",
|
|
183
|
+
MessageAttributes: message.MessageAttributes ?? {}
|
|
184
|
+
}
|
|
185
|
+
}] };
|
|
186
|
+
}
|
|
187
|
+
var SnsCertificateUntrusted = class extends Error {
|
|
188
|
+
url;
|
|
189
|
+
constructor(url) {
|
|
190
|
+
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.`);
|
|
191
|
+
this.url = url;
|
|
192
|
+
this.name = "SnsCertificateUntrusted";
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var SnsCertificateUnavailable = class extends Error {
|
|
196
|
+
url;
|
|
197
|
+
status;
|
|
198
|
+
constructor(url, status) {
|
|
199
|
+
super(`Fetching the SNS signing certificate at '${url}' answered ${status}. The message cannot be verified, so it is not handled; SNS will retry.`);
|
|
200
|
+
this.url = url;
|
|
201
|
+
this.status = status;
|
|
202
|
+
this.name = "SnsCertificateUnavailable";
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
var SnsSignatureVersionUnknown = class extends Error {
|
|
206
|
+
version;
|
|
207
|
+
constructor(version) {
|
|
208
|
+
super(`SNS signature version '${version}' is not one this endpoint verifies (it knows '1' and '2'). Reject the request.`);
|
|
209
|
+
this.version = version;
|
|
210
|
+
this.name = "SnsSignatureVersionUnknown";
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var SnsSignatureInvalid = class extends Error {
|
|
214
|
+
messageId;
|
|
215
|
+
topicArn;
|
|
216
|
+
constructor(messageId, topicArn) {
|
|
217
|
+
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.`);
|
|
218
|
+
this.messageId = messageId;
|
|
219
|
+
this.topicArn = topicArn;
|
|
220
|
+
this.name = "SnsSignatureInvalid";
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var SnsMessageTypeUnknown = class extends Error {
|
|
224
|
+
type;
|
|
225
|
+
constructor(type) {
|
|
226
|
+
super(`'${type}' is not an SNS message type (Notification, SubscriptionConfirmation, UnsubscribeConfirmation). Reject it.`);
|
|
227
|
+
this.type = type;
|
|
228
|
+
this.name = "SnsMessageTypeUnknown";
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var SnsNotAConfirmation = class extends Error {
|
|
232
|
+
type;
|
|
233
|
+
constructor(type) {
|
|
234
|
+
super(`Only a SubscriptionConfirmation carries a URL to confirm; this is a '${type}'.`);
|
|
235
|
+
this.type = type;
|
|
236
|
+
this.name = "SnsNotAConfirmation";
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
var SnsConfirmationFailed = class extends Error {
|
|
240
|
+
topicArn;
|
|
241
|
+
status;
|
|
242
|
+
constructor(topicArn, status) {
|
|
243
|
+
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.`);
|
|
244
|
+
this.topicArn = topicArn;
|
|
245
|
+
this.status = status;
|
|
246
|
+
this.name = "SnsConfirmationFailed";
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var SnsEndpointNotHttp = class extends Error {
|
|
250
|
+
endpoint;
|
|
251
|
+
constructor(endpoint) {
|
|
252
|
+
super(`'${endpoint}' is not an http(s) URL. SNS pushes to an HTTP endpoint; subscribe a queue for anything else.`);
|
|
253
|
+
this.endpoint = endpoint;
|
|
254
|
+
this.name = "SnsEndpointNotHttp";
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/sns/SNSConnection.ts
|
|
259
|
+
var SNSConnection = class SNSConnection {
|
|
260
|
+
type = "sns";
|
|
261
|
+
client;
|
|
262
|
+
connected = false;
|
|
263
|
+
config;
|
|
264
|
+
constructor(config) {
|
|
265
|
+
this.config = config;
|
|
266
|
+
const clientConfig = {
|
|
267
|
+
region: config.region,
|
|
268
|
+
endpoint: config.endpoint,
|
|
269
|
+
credentials: config.credentials
|
|
270
|
+
};
|
|
271
|
+
this.client = new SNSClient(clientConfig);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Create an SNSConnection from a connection string
|
|
275
|
+
* Format: sns://?topicArn=arn:aws:sns:region:account:topic®ion=us-east-1&endpoint=http://localhost:4566
|
|
276
|
+
*/
|
|
277
|
+
static async fromConnectionString(connectionString) {
|
|
278
|
+
const { topicArn, region, endpoint } = parse(connectionString);
|
|
279
|
+
const params = new URL(connectionString).searchParams;
|
|
280
|
+
const config = {
|
|
281
|
+
topicArn,
|
|
282
|
+
region,
|
|
283
|
+
endpoint
|
|
284
|
+
};
|
|
285
|
+
const accessKeyId = params.get("accessKeyId");
|
|
286
|
+
const secretAccessKey = params.get("secretAccessKey");
|
|
287
|
+
if (accessKeyId && secretAccessKey) config.credentials = {
|
|
288
|
+
accessKeyId,
|
|
289
|
+
secretAccessKey,
|
|
290
|
+
sessionToken: params.get("sessionToken") || void 0
|
|
291
|
+
};
|
|
292
|
+
const connection = new SNSConnection(config);
|
|
293
|
+
await connection.connect();
|
|
294
|
+
return connection;
|
|
295
|
+
}
|
|
296
|
+
async connect() {
|
|
297
|
+
this.connected = true;
|
|
298
|
+
}
|
|
299
|
+
async close() {
|
|
300
|
+
this.client.destroy();
|
|
301
|
+
this.connected = false;
|
|
302
|
+
}
|
|
303
|
+
isConnected() {
|
|
304
|
+
return this.connected;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Get the underlying SNS client
|
|
308
|
+
*/
|
|
309
|
+
get snsClient() {
|
|
310
|
+
return this.client;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get the topic ARN for this connection
|
|
314
|
+
*/
|
|
315
|
+
get topicArn() {
|
|
316
|
+
return this.config.topicArn;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
//#endregion
|
|
10
320
|
//#region src/sns/SNSPublisher.ts
|
|
11
321
|
var SNSPublisher = class SNSPublisher {
|
|
322
|
+
connection;
|
|
12
323
|
constructor(connection, _options = {}) {
|
|
13
324
|
this.connection = connection;
|
|
14
325
|
}
|
|
@@ -17,8 +328,8 @@ var SNSPublisher = class SNSPublisher {
|
|
|
17
328
|
* Format: sns://?topicArn=arn:aws:sns:region:account:topic®ion=us-east-1&endpoint=http://localhost:4566
|
|
18
329
|
*/
|
|
19
330
|
static async fromConnectionString(connectionString) {
|
|
20
|
-
const { SNSConnection
|
|
21
|
-
const connection = await SNSConnection
|
|
331
|
+
const { SNSConnection } = await import("../SNSConnection-ezQvVrDv.mjs");
|
|
332
|
+
const connection = await SNSConnection.fromConnectionString(connectionString);
|
|
22
333
|
return new SNSPublisher(connection);
|
|
23
334
|
}
|
|
24
335
|
async publish(messages) {
|
|
@@ -42,7 +353,6 @@ var SNSPublisher = class SNSPublisher {
|
|
|
42
353
|
}
|
|
43
354
|
async close() {}
|
|
44
355
|
};
|
|
45
|
-
|
|
46
356
|
//#endregion
|
|
47
357
|
//#region src/sns/SNSSubscriber.ts
|
|
48
358
|
/**
|
|
@@ -63,6 +373,7 @@ var SNSPublisher = class SNSPublisher {
|
|
|
63
373
|
* Messages are pushed from SNS to SQS, and we receive those pushed messages.
|
|
64
374
|
*/
|
|
65
375
|
var SNSSubscriber = class SNSSubscriber {
|
|
376
|
+
connection;
|
|
66
377
|
sqsClient;
|
|
67
378
|
queueUrl;
|
|
68
379
|
queueArn;
|
|
@@ -89,10 +400,9 @@ var SNSSubscriber = class SNSSubscriber {
|
|
|
89
400
|
* Format: sns://?topicArn=...&queueName=myqueue®ion=us-east-1
|
|
90
401
|
*/
|
|
91
402
|
static async fromConnectionString(connectionString) {
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const connection = await SNSConnection$1.fromConnectionString(connectionString);
|
|
403
|
+
const params = new URL(connectionString).searchParams;
|
|
404
|
+
const { SNSConnection } = await import("../SNSConnection-ezQvVrDv.mjs");
|
|
405
|
+
const connection = await SNSConnection.fromConnectionString(connectionString);
|
|
96
406
|
const options = {
|
|
97
407
|
queueName: params.get("queueName") || void 0,
|
|
98
408
|
createQueue: params.get("createQueue") !== "false",
|
|
@@ -138,7 +448,7 @@ var SNSSubscriber = class SNSSubscriber {
|
|
|
138
448
|
const createCommand = new CreateQueueCommand({ QueueName: this.options.queueName });
|
|
139
449
|
const response = await this.sqsClient.send(createCommand);
|
|
140
450
|
this.queueUrl = response.QueueUrl;
|
|
141
|
-
} else throw new
|
|
451
|
+
} else throw new SnsQueueMissing(this.options.queueName);
|
|
142
452
|
const attrsCommand = new GetQueueAttributesCommand({
|
|
143
453
|
QueueUrl: this.queueUrl,
|
|
144
454
|
AttributeNames: ["QueueArn"]
|
|
@@ -174,7 +484,19 @@ var SNSSubscriber = class SNSSubscriber {
|
|
|
174
484
|
this.subscriptionArn = response.SubscriptionArn;
|
|
175
485
|
}
|
|
176
486
|
};
|
|
177
|
-
|
|
487
|
+
/**
|
|
488
|
+
* A subscriber asked to use a queue it may not create. The SQS queue behind an
|
|
489
|
+
* SNS subscription has to exist first, or `createQueue` has to allow it.
|
|
490
|
+
*/
|
|
491
|
+
var SnsQueueMissing = class extends Error {
|
|
492
|
+
queueName;
|
|
493
|
+
constructor(queueName) {
|
|
494
|
+
super(`The SNS subscriber's queue${queueName ? ` "${queueName}"` : ""} is not created here: pass createQueue: true, or create the queue first.`);
|
|
495
|
+
this.queueName = queueName;
|
|
496
|
+
this.name = "SnsQueueMissing";
|
|
497
|
+
}
|
|
498
|
+
};
|
|
178
499
|
//#endregion
|
|
179
|
-
export { SNSConnection, SNSPublisher, SNSSubscriber, snsUrl_exports as snsUrl };
|
|
500
|
+
export { SNSConnection, SNSPublisher, SNSSubscriber, SnsCertificateUnavailable, SnsCertificateUntrusted, SnsConfirmationFailed, SnsEndpointNotHttp, SnsMessageTypeUnknown, SnsNotAConfirmation, SnsQueueMissing, SnsSignatureInvalid, SnsSignatureVersionUnknown, confirmSnsSubscription, snsStringToSign, snsUrl_exports as snsUrl, subscribeHttpEndpoint, toSnsEvent, verifySnsMessage };
|
|
501
|
+
|
|
180
502
|
//# sourceMappingURL=index.mjs.map
|
package/dist/sns/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["connection: SNSConnection","_options: SNSPublisherOptions","connectionString: string","messages: TMessage[]","message: TMessage","connection: SNSConnection","options: SNSSubscriberOptions","connectionString: string","messages: TMessage['type'][]","listener: (payload: TMessage) => Promise<void>"],"sources":["../../src/sns/SNSPublisher.ts","../../src/sns/SNSSubscriber.ts"],"sourcesContent":["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®ion=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®ion=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 Error('Queue must exist or createQueue option must be true');\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["snsUrl.parse"],"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®ion=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®ion=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®ion=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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"}
|
package/dist/sns/snsUrl.cjs
CHANGED
|
@@ -1,7 +1,92 @@
|
|
|
1
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/sns/snsUrl.ts
|
|
3
|
+
var snsUrl_exports = /* @__PURE__ */ require("../rolldown-runtime-C0BPl7ul.cjs").__exportAll({
|
|
4
|
+
MalformedTopicUrl: () => MalformedTopicUrl,
|
|
5
|
+
MissingTopicArn: () => MissingTopicArn,
|
|
6
|
+
UnexpectedTopicScheme: () => UnexpectedTopicScheme,
|
|
7
|
+
build: () => build,
|
|
8
|
+
parse: () => parse
|
|
9
|
+
});
|
|
10
|
+
const SCHEME = "sns:";
|
|
11
|
+
/**
|
|
12
|
+
* Compose an address into a connection string.
|
|
13
|
+
*
|
|
14
|
+
* Credentials never appear, for the same reason they never appear in an
|
|
15
|
+
* `s3://` URL: the SDK resolves them from its own chain.
|
|
16
|
+
*/
|
|
17
|
+
function build(address) {
|
|
18
|
+
const { topicArn, region, endpoint, queueName } = address;
|
|
19
|
+
if (!topicArn) throw new MissingTopicArn("");
|
|
20
|
+
const url = new URL(`${SCHEME}//`);
|
|
21
|
+
url.searchParams.set("topicArn", topicArn);
|
|
22
|
+
if (region) url.searchParams.set("region", region);
|
|
23
|
+
if (endpoint) url.searchParams.set("endpoint", endpoint);
|
|
24
|
+
if (queueName) url.searchParams.set("queueName", queueName);
|
|
25
|
+
return url.toString();
|
|
26
|
+
}
|
|
27
|
+
/** Parse a connection string back into an address. */
|
|
28
|
+
function parse(connectionString) {
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = new URL(connectionString);
|
|
32
|
+
} catch {
|
|
33
|
+
throw new MalformedTopicUrl(connectionString);
|
|
34
|
+
}
|
|
35
|
+
if (parsed.protocol !== SCHEME) throw new UnexpectedTopicScheme(connectionString, SCHEME, parsed.protocol);
|
|
36
|
+
const topicArn = parsed.searchParams.get("topicArn");
|
|
37
|
+
if (!topicArn) throw new MissingTopicArn(connectionString);
|
|
38
|
+
const region = parsed.searchParams.get("region") ?? void 0;
|
|
39
|
+
const endpoint = parsed.searchParams.get("endpoint") ?? void 0;
|
|
40
|
+
const queueName = parsed.searchParams.get("queueName") ?? void 0;
|
|
41
|
+
return {
|
|
42
|
+
topicArn,
|
|
43
|
+
...region ? { region } : {},
|
|
44
|
+
...endpoint ? { endpoint } : {},
|
|
45
|
+
...queueName ? { queueName } : {}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** The string is not a URL at all. */
|
|
49
|
+
var MalformedTopicUrl = class extends Error {
|
|
50
|
+
url;
|
|
51
|
+
constructor(url) {
|
|
52
|
+
super(`'${url}' is not a valid connection string.`);
|
|
53
|
+
this.url = url;
|
|
54
|
+
this.name = "MalformedTopicUrl";
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/** The string is a URL, but not an `sns://` one. */
|
|
58
|
+
var UnexpectedTopicScheme = class extends Error {
|
|
59
|
+
url;
|
|
60
|
+
expected;
|
|
61
|
+
actual;
|
|
62
|
+
constructor(url, expected, actual) {
|
|
63
|
+
super(`Expected a '${expected}//' connection string and got '${actual}//' in '${url}'.`);
|
|
64
|
+
this.url = url;
|
|
65
|
+
this.expected = expected;
|
|
66
|
+
this.actual = actual;
|
|
67
|
+
this.name = "UnexpectedTopicScheme";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/** An `sns://` string with no topic names nothing. */
|
|
71
|
+
var MissingTopicArn = class extends Error {
|
|
72
|
+
url;
|
|
73
|
+
constructor(url) {
|
|
74
|
+
super(url ? `'${url}' names no topic: an sns:// connection string needs ?topicArn=.` : "An sns:// connection string needs a topicArn.");
|
|
75
|
+
this.url = url;
|
|
76
|
+
this.name = "MissingTopicArn";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
exports.MalformedTopicUrl = MalformedTopicUrl;
|
|
81
|
+
exports.MissingTopicArn = MissingTopicArn;
|
|
82
|
+
exports.UnexpectedTopicScheme = UnexpectedTopicScheme;
|
|
83
|
+
exports.build = build;
|
|
84
|
+
exports.parse = parse;
|
|
85
|
+
Object.defineProperty(exports, "snsUrl_exports", {
|
|
86
|
+
enumerable: true,
|
|
87
|
+
get: function() {
|
|
88
|
+
return snsUrl_exports;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
2
91
|
|
|
3
|
-
|
|
4
|
-
exports.MissingTopicArn = require_snsUrl.MissingTopicArn;
|
|
5
|
-
exports.UnexpectedTopicScheme = require_snsUrl.UnexpectedTopicScheme;
|
|
6
|
-
exports.build = require_snsUrl.build;
|
|
7
|
-
exports.parse = require_snsUrl.parse;
|
|
92
|
+
//# sourceMappingURL=snsUrl.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snsUrl.cjs","names":[],"sources":["../../src/sns/snsUrl.ts"],"sourcesContent":["/**\n * The `sns://` connection-string codec — how a topic is addressed as one string.\n *\n * The twin of `sqsUrl`, and separate from it for the same reason `gs://` would\n * be separate from `s3://`: a topic ARN and a queue URL are different\n * vocabularies, and a shared codec would have to contain both. What they share\n * is only the rule that a construct provides one string.\n *\n * Both directions live here so they cannot drift: `parse(build(x))` is `x`.\n */\n\n/** What an SNS connection string addresses. */\nexport interface SnsAddress {\n\ttopicArn: string;\n\t/** Read off the topic, never inherited: a topic may live in another region. */\n\tregion?: string;\n\t/** Set for SNS-compatible backends such as LocalStack. */\n\tendpoint?: string;\n\t/**\n\t * The queue a subscriber drains.\n\t *\n\t * Present only on the subscriber's string: fan-out delivers to a queue per\n\t * subscriber, so the publisher's string names the topic and nothing else.\n\t */\n\tqueueName?: string;\n}\n\nconst SCHEME = 'sns:';\n\n/**\n * Compose an address into a connection string.\n *\n * Credentials never appear, for the same reason they never appear in an\n * `s3://` URL: the SDK resolves them from its own chain.\n */\nexport function build(address: SnsAddress): string {\n\tconst { topicArn, region, endpoint, queueName } = address;\n\tif (!topicArn) throw new MissingTopicArn('');\n\n\tconst url = new URL(`${SCHEME}//`);\n\turl.searchParams.set('topicArn', topicArn);\n\tif (region) url.searchParams.set('region', region);\n\tif (endpoint) url.searchParams.set('endpoint', endpoint);\n\tif (queueName) url.searchParams.set('queueName', queueName);\n\n\treturn url.toString();\n}\n\n/** Parse a connection string back into an address. */\nexport function parse(connectionString: string): SnsAddress {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(connectionString);\n\t} catch {\n\t\tthrow new MalformedTopicUrl(connectionString);\n\t}\n\n\tif (parsed.protocol !== SCHEME) {\n\t\tthrow new UnexpectedTopicScheme(connectionString, SCHEME, parsed.protocol);\n\t}\n\n\tconst topicArn = parsed.searchParams.get('topicArn');\n\tif (!topicArn) throw new MissingTopicArn(connectionString);\n\n\tconst region = parsed.searchParams.get('region') ?? undefined;\n\tconst endpoint = parsed.searchParams.get('endpoint') ?? undefined;\n\tconst queueName = parsed.searchParams.get('queueName') ?? undefined;\n\n\treturn {\n\t\ttopicArn,\n\t\t...(region ? { region } : {}),\n\t\t...(endpoint ? { endpoint } : {}),\n\t\t...(queueName ? { queueName } : {}),\n\t};\n}\n\n/** The string is not a URL at all. */\nexport class MalformedTopicUrl extends Error {\n\tconstructor(readonly url: string) {\n\t\tsuper(`'${url}' is not a valid connection string.`);\n\t\tthis.name = 'MalformedTopicUrl';\n\t}\n}\n\n/** The string is a URL, but not an `sns://` one. */\nexport class UnexpectedTopicScheme extends Error {\n\tconstructor(\n\t\treadonly url: string,\n\t\treadonly expected: string,\n\t\treadonly actual: string,\n\t) {\n\t\tsuper(\n\t\t\t`Expected a '${expected}//' connection string and got '${actual}//' in '${url}'.`,\n\t\t);\n\t\tthis.name = 'UnexpectedTopicScheme';\n\t}\n}\n\n/** An `sns://` string with no topic names nothing. */\nexport class MissingTopicArn extends Error {\n\tconstructor(readonly url: string) {\n\t\tsuper(\n\t\t\turl\n\t\t\t\t? `'${url}' names no topic: an sns:// connection string needs ?topicArn=.`\n\t\t\t\t: 'An sns:// connection string needs a topicArn.',\n\t\t);\n\t\tthis.name = 'MissingTopicArn';\n\t}\n}\n"],"mappings":";;;;;;;;;AA2BA,MAAM,SAAS;;;;;;;AAQf,SAAgB,MAAM,SAA6B;CAClD,MAAM,EAAE,UAAU,QAAQ,UAAU,cAAc;CAClD,IAAI,CAAC,UAAU,MAAM,IAAI,gBAAgB,EAAE;CAE3C,MAAM,MAAM,IAAI,IAAI,GAAG,OAAO,GAAG;CACjC,IAAI,aAAa,IAAI,YAAY,QAAQ;CACzC,IAAI,QAAQ,IAAI,aAAa,IAAI,UAAU,MAAM;CACjD,IAAI,UAAU,IAAI,aAAa,IAAI,YAAY,QAAQ;CACvD,IAAI,WAAW,IAAI,aAAa,IAAI,aAAa,SAAS;CAE1D,OAAO,IAAI,SAAS;AACrB;;AAGA,SAAgB,MAAM,kBAAsC;CAC3D,IAAI;CACJ,IAAI;EACH,SAAS,IAAI,IAAI,gBAAgB;CAClC,QAAQ;EACP,MAAM,IAAI,kBAAkB,gBAAgB;CAC7C;CAEA,IAAI,OAAO,aAAa,QACvB,MAAM,IAAI,sBAAsB,kBAAkB,QAAQ,OAAO,QAAQ;CAG1E,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU;CACnD,IAAI,CAAC,UAAU,MAAM,IAAI,gBAAgB,gBAAgB;CAEzD,MAAM,SAAS,OAAO,aAAa,IAAI,QAAQ,KAAK,KAAA;CACpD,MAAM,WAAW,OAAO,aAAa,IAAI,UAAU,KAAK,KAAA;CACxD,MAAM,YAAY,OAAO,aAAa,IAAI,WAAW,KAAK,KAAA;CAE1D,OAAO;EACN;EACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC3B,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAC/B,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;CAClC;AACD;;AAGA,IAAa,oBAAb,cAAuC,MAAM;CACvB;CAArB,YAAY,KAAsB;EACjC,MAAM,IAAI,IAAI,oCAAoC;EAD9B,KAAA,MAAA;EAEpB,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,wBAAb,cAA2C,MAAM;CAEtC;CACA;CACA;CAHV,YACC,KACA,UACA,QACC;EACD,MACC,eAAe,SAAS,iCAAiC,OAAO,UAAU,IAAI,GAC/E;EANS,KAAA,MAAA;EACA,KAAA,WAAA;EACA,KAAA,SAAA;EAKT,KAAK,OAAO;CACb;AACD;;AAGA,IAAa,kBAAb,cAAqC,MAAM;CACrB;CAArB,YAAY,KAAsB;EACjC,MACC,MACG,IAAI,IAAI,mEACR,+CACJ;EALoB,KAAA,MAAA;EAMpB,KAAK,OAAO;CACb;AACD"}
|