@remit/sqs-client 0.0.1 → 0.0.3
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/package.json +38 -38
- package/src/producer.test.ts +135 -13
- package/src/producer.ts +42 -5
package/package.json
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
2
|
+
"name": "@remit/sqs-client",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"default": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"./poller": {
|
|
13
|
+
"types": "./src/poller.ts",
|
|
14
|
+
"default": "./src/poller.ts"
|
|
15
|
+
},
|
|
16
|
+
"./producer": {
|
|
17
|
+
"types": "./src/producer.ts",
|
|
18
|
+
"default": "./src/producer.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test:typecheck": "tsgo --noEmit",
|
|
23
|
+
"test:run": "node --env-file=../../localhost-test-unit.env --test 'src/**/*.test.ts'",
|
|
24
|
+
"test": "npm run test:typecheck && npm run test:run"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@aws-sdk/client-sqs": "*",
|
|
28
|
+
"@aws-sdk/core": "*",
|
|
29
|
+
"@types/aws-lambda": "*"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/remit-mail/reader.git",
|
|
38
|
+
"directory": "packages/sqs-client"
|
|
39
|
+
}
|
|
40
40
|
}
|
package/src/producer.test.ts
CHANGED
|
@@ -1,28 +1,150 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
|
-
import {
|
|
3
|
+
import type { SQSClient } from "@aws-sdk/client-sqs";
|
|
4
|
+
import { AwsQueryProtocol } from "@aws-sdk/core/protocols";
|
|
5
|
+
import { createQueueProducer, isLocalEndpoint } from "./producer.js";
|
|
6
|
+
|
|
7
|
+
// The queue URL a self-hosted compose stack hands its workers: a plain
|
|
8
|
+
// http:// URL addressing the queue container by name, never "localhost".
|
|
9
|
+
const composeQueueUrl = "http://queue:9324/000000000000/mailboxes";
|
|
10
|
+
const awsQueueUrl =
|
|
11
|
+
"https://sqs.eu-west-1.amazonaws.com/123456789012/remit-dev-mailboxes.fifo";
|
|
12
|
+
|
|
13
|
+
const usesQueryProtocol = (client: SQSClient): boolean =>
|
|
14
|
+
client.config.protocol instanceof AwsQueryProtocol;
|
|
15
|
+
|
|
16
|
+
const resolvedEndpoint = async (
|
|
17
|
+
client: SQSClient,
|
|
18
|
+
): Promise<string | undefined> => {
|
|
19
|
+
const { endpoint } = client.config;
|
|
20
|
+
if (!endpoint) return undefined;
|
|
21
|
+
const resolved = await endpoint();
|
|
22
|
+
return `${resolved.protocol}//${resolved.hostname}:${resolved.port}`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("isLocalEndpoint", () => {
|
|
26
|
+
it("treats a non-localhost http:// queue URL as local", () => {
|
|
27
|
+
assert.equal(isLocalEndpoint(composeQueueUrl), true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("treats http://localhost as local", () => {
|
|
31
|
+
assert.equal(
|
|
32
|
+
isLocalEndpoint("http://localhost:9324/000000000000/mailboxes"),
|
|
33
|
+
true,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("treats https://localhost as local", () => {
|
|
38
|
+
assert.equal(
|
|
39
|
+
isLocalEndpoint("https://localhost:9324/000000000000/mailboxes"),
|
|
40
|
+
true,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("treats a real SQS queue URL as remote", () => {
|
|
45
|
+
assert.equal(isLocalEndpoint(awsQueueUrl), false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// A prefix match on "https://localhost" reads all of these as local. The
|
|
49
|
+
// host is localhost in none of them.
|
|
50
|
+
it("treats an https host that merely starts with localhost as remote", () => {
|
|
51
|
+
assert.equal(isLocalEndpoint("https://localhost.example.com/0/q"), false);
|
|
52
|
+
assert.equal(
|
|
53
|
+
isLocalEndpoint("https://localhost-queue.example.com/0/q"),
|
|
54
|
+
false,
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("treats an https URL with localhost in the userinfo as remote", () => {
|
|
59
|
+
assert.equal(isLocalEndpoint("https://localhost@example.com/0/q"), false);
|
|
60
|
+
assert.equal(
|
|
61
|
+
isLocalEndpoint("https://localhost:pass@example.com/0/q"),
|
|
62
|
+
false,
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// The mirror image: a prefix match misses these, and they are local.
|
|
67
|
+
it("treats a loopback https URL carrying credentials as local", () => {
|
|
68
|
+
assert.equal(isLocalEndpoint("https://user:pass@localhost/0/q"), true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("ignores scheme case", () => {
|
|
72
|
+
assert.equal(isLocalEndpoint("HTTP://queue:9324/0/q"), true);
|
|
73
|
+
assert.equal(isLocalEndpoint("HtTpS://localhost:9324/0/q"), true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("treats loopback addresses over https as local", () => {
|
|
77
|
+
assert.equal(isLocalEndpoint("https://127.0.0.1:9324/0/q"), true);
|
|
78
|
+
assert.equal(isLocalEndpoint("https://[::1]:9324/0/q"), true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("treats any http URL as local regardless of host, port, or credentials", () => {
|
|
82
|
+
assert.equal(isLocalEndpoint("http://queue/0/q"), true);
|
|
83
|
+
assert.equal(isLocalEndpoint("http://user:pass@queue:9324/0/q"), true);
|
|
84
|
+
assert.equal(isLocalEndpoint("http://example.com/0/q"), true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Must not throw: several callers construct their client at module load, so
|
|
88
|
+
// a throw here would take the worker down at import.
|
|
89
|
+
it("treats an unparseable queue URL as remote without throwing", () => {
|
|
90
|
+
assert.equal(isLocalEndpoint(""), false);
|
|
91
|
+
assert.equal(isLocalEndpoint("not a url"), false);
|
|
92
|
+
assert.equal(isLocalEndpoint("queue:9324/0/q"), false);
|
|
93
|
+
assert.equal(isLocalEndpoint("//queue:9324/0/q"), false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
4
96
|
|
|
5
97
|
describe("createQueueProducer", () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
98
|
+
// Regression (self-host stack): a queue URL of http://queue:9324/... used to
|
|
99
|
+
// fall through to the AWS JSON protocol, so every send hit the local queue
|
|
100
|
+
// server with a body it cannot parse and mail never synced.
|
|
101
|
+
it("selects the query protocol for a non-localhost http:// queue URL", async () => {
|
|
102
|
+
const client = createQueueProducer({ queueUrl: composeQueueUrl });
|
|
103
|
+
assert.equal(usesQueryProtocol(client), true);
|
|
104
|
+
assert.equal(await resolvedEndpoint(client), "http://queue:9324");
|
|
11
105
|
});
|
|
12
106
|
|
|
13
|
-
it("
|
|
107
|
+
it("selects the query protocol for a localhost queue URL", () => {
|
|
14
108
|
const client = createQueueProducer({
|
|
15
|
-
queueUrl:
|
|
16
|
-
"https://sqs.eu-west-1.amazonaws.com/123456789012/remit-dev-mailboxes.fifo",
|
|
109
|
+
queueUrl: "http://localhost:9324/000000000000/mailboxes",
|
|
17
110
|
});
|
|
18
|
-
assert.equal(
|
|
111
|
+
assert.equal(usesQueryProtocol(client), true);
|
|
19
112
|
});
|
|
20
113
|
|
|
21
|
-
it("
|
|
114
|
+
it("leaves the default protocol and endpoint for a real AWS queue URL", async () => {
|
|
115
|
+
const client = createQueueProducer({ queueUrl: awsQueueUrl });
|
|
116
|
+
assert.equal(usesQueryProtocol(client), false);
|
|
117
|
+
assert.equal(await resolvedEndpoint(client), undefined);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("does not throw when the queue URL is unparseable", async () => {
|
|
121
|
+
const client = createQueueProducer({ queueUrl: "not a url" });
|
|
122
|
+
assert.equal(usesQueryProtocol(client), false);
|
|
123
|
+
assert.equal(await resolvedEndpoint(client), undefined);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("accepts an explicit endpoint override", async () => {
|
|
22
127
|
const client = createQueueProducer({
|
|
23
|
-
queueUrl:
|
|
128
|
+
queueUrl: composeQueueUrl,
|
|
24
129
|
endpoint: "http://custom-endpoint:1234",
|
|
25
130
|
});
|
|
26
|
-
assert.equal(
|
|
131
|
+
assert.equal(usesQueryProtocol(client), true);
|
|
132
|
+
assert.equal(await resolvedEndpoint(client), "http://custom-endpoint:1234");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("uses localCredentials only for a local endpoint", async () => {
|
|
136
|
+
const local = createQueueProducer({
|
|
137
|
+
queueUrl: composeQueueUrl,
|
|
138
|
+
localCredentials: { accessKeyId: "local", secretAccessKey: "local" },
|
|
139
|
+
env: {},
|
|
140
|
+
});
|
|
141
|
+
assert.equal((await local.config.credentials()).accessKeyId, "local");
|
|
142
|
+
|
|
143
|
+
const remote = createQueueProducer({
|
|
144
|
+
queueUrl: awsQueueUrl,
|
|
145
|
+
localCredentials: { accessKeyId: "local", secretAccessKey: "local" },
|
|
146
|
+
env: { SQS_ACCESS_KEY_ID: "real", SQS_SECRET_ACCESS_KEY: "real" },
|
|
147
|
+
});
|
|
148
|
+
assert.equal((await remote.config.credentials()).accessKeyId, "real");
|
|
27
149
|
});
|
|
28
150
|
});
|
package/src/producer.ts
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
1
|
import { SQSClient } from "@aws-sdk/client-sqs";
|
|
2
2
|
import { AwsQueryProtocol } from "@aws-sdk/core/protocols";
|
|
3
|
-
import { resolveSqsCredentials } from "./index.js";
|
|
3
|
+
import { resolveSqsCredentials, type SqsStaticCredentials } from "./index.js";
|
|
4
4
|
|
|
5
5
|
export interface CreateQueueProducerOptions {
|
|
6
6
|
readonly queueUrl: string;
|
|
7
7
|
readonly endpoint?: string;
|
|
8
8
|
readonly env?: NodeJS.ProcessEnv;
|
|
9
|
+
/**
|
|
10
|
+
* Credentials to use when the queue URL resolves to a local endpoint. A
|
|
11
|
+
* local queue server ignores them, but the SDK still needs something to
|
|
12
|
+
* sign with and a self-hosted stack has no credential chain to fall back on.
|
|
13
|
+
*/
|
|
14
|
+
readonly localCredentials?: SqsStaticCredentials;
|
|
9
15
|
}
|
|
10
16
|
|
|
11
|
-
const
|
|
12
|
-
|
|
17
|
+
const LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether a queue URL addresses a local, SQS-compatible queue server
|
|
21
|
+
* (ElasticMQ, the queue sidecar) rather than real SQS.
|
|
22
|
+
*
|
|
23
|
+
* Any plain `http://` URL is local: real SQS and Scaleway M&Q are always
|
|
24
|
+
* HTTPS, and a self-hosted stack addresses its queue server by container name
|
|
25
|
+
* (`http://queue:9324/...`), not by `localhost`. Over HTTPS only a loopback
|
|
26
|
+
* host is local, which covers a local server behind TLS.
|
|
27
|
+
*
|
|
28
|
+
* The URL is parsed rather than prefix-matched. Prefix-matching the string
|
|
29
|
+
* gets the interesting cases wrong in both directions: it reads
|
|
30
|
+
* `https://localhost.example.com` and `https://localhost@example.com` as
|
|
31
|
+
* local (the host is neither), and misses `https://user:pass@localhost` and
|
|
32
|
+
* an uppercase `HTTP://` scheme (both of which are).
|
|
33
|
+
*
|
|
34
|
+
* A queue URL that does not parse is treated as remote. It cannot be sent to
|
|
35
|
+
* either way, and several callers build their client at module load — throwing
|
|
36
|
+
* here would take a worker down at import instead of surfacing the bad
|
|
37
|
+
* configuration at the send that actually uses it.
|
|
38
|
+
*/
|
|
39
|
+
export const isLocalEndpoint = (queueUrl: string): boolean => {
|
|
40
|
+
let url: URL;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(queueUrl);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (url.protocol === "http:") return true;
|
|
47
|
+
return url.protocol === "https:" && LOOPBACK_HOSTNAMES.has(url.hostname);
|
|
48
|
+
};
|
|
13
49
|
|
|
14
50
|
/**
|
|
15
51
|
* Producer-side counterpart of `runQueuePoller`: the one place an
|
|
@@ -22,11 +58,12 @@ const isLocalEndpoint = (queueUrl: string): boolean =>
|
|
|
22
58
|
export const createQueueProducer = (
|
|
23
59
|
options: CreateQueueProducerOptions,
|
|
24
60
|
): SQSClient => {
|
|
25
|
-
const { queueUrl, endpoint, env } = options;
|
|
61
|
+
const { queueUrl, endpoint, env, localCredentials } = options;
|
|
26
62
|
const local = isLocalEndpoint(queueUrl);
|
|
27
63
|
return new SQSClient({
|
|
28
64
|
endpoint: endpoint ?? (local ? new URL(queueUrl).origin : undefined),
|
|
29
65
|
...(local && { protocol: AwsQueryProtocol }),
|
|
30
|
-
credentials:
|
|
66
|
+
credentials:
|
|
67
|
+
(local ? localCredentials : undefined) ?? resolveSqsCredentials(env),
|
|
31
68
|
});
|
|
32
69
|
};
|