serverless-ircd 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +96 -2
- package/.github/workflows/deploy-aws.yml +129 -0
- package/.github/workflows/deploy-cf.yml +0 -2
- package/.gitmodules +3 -0
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +352 -0
- package/MOTD.txt +3 -0
- package/PLAN-FIXES.md +358 -0
- package/PLAN.md +420 -0
- package/README.md +115 -81
- package/apps/aws-stack/README.md +73 -0
- package/apps/aws-stack/bin/aws.ts +49 -0
- package/apps/aws-stack/cdk.json +10 -0
- package/apps/aws-stack/package.json +41 -0
- package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
- package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
- package/apps/aws-stack/scripts/smoke.mjs +142 -0
- package/apps/aws-stack/src/aws-stack.ts +263 -0
- package/apps/aws-stack/src/tables.ts +10 -0
- package/apps/aws-stack/tests/localstack.test.ts +46 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
- package/apps/aws-stack/tests/stack.test.ts +464 -0
- package/apps/aws-stack/tsconfig.build.json +11 -0
- package/apps/aws-stack/tsconfig.test.json +8 -0
- package/apps/aws-stack/vitest.config.ts +20 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.toml +25 -0
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +56 -0
- package/apps/local-cli/src/server.ts +241 -32
- package/apps/local-cli/tests/config-loader.test.ts +107 -0
- package/apps/local-cli/tests/e2e.test.ts +74 -0
- package/apps/local-cli/tests/security-e2e.test.ts +239 -0
- package/biome.json +3 -1
- package/dashboards/cloudwatch-irc.json +139 -0
- package/docs/AWS-Adapter-Architecture.md +494 -0
- package/docs/AWS-Deployment.md +1107 -0
- package/docs/Cloudflare-Deployment-Guide.md +660 -0
- package/docs/Home.md +1 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanIRCv3Websocket.md +489 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +440 -0
- package/package.json +8 -2
- package/packages/aws-adapter/README.md +35 -0
- package/packages/aws-adapter/package.json +52 -0
- package/packages/aws-adapter/src/aws-runtime.ts +783 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +69 -0
- package/packages/aws-adapter/src/config-loader.ts +96 -0
- package/packages/aws-adapter/src/dynamo.ts +61 -0
- package/packages/aws-adapter/src/handlers/connect.ts +44 -0
- package/packages/aws-adapter/src/handlers/default.ts +322 -0
- package/packages/aws-adapter/src/handlers/disconnect.ts +29 -0
- package/packages/aws-adapter/src/handlers/index.ts +248 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
- package/packages/aws-adapter/src/handlers/state.ts +13 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
- package/packages/aws-adapter/src/index.ts +67 -0
- package/packages/aws-adapter/src/message-store.ts +34 -0
- package/packages/aws-adapter/src/serialize.ts +283 -0
- package/packages/aws-adapter/src/tables.ts +53 -0
- package/packages/aws-adapter/tests/aws-harness.ts +408 -0
- package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +158 -0
- package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
- package/packages/aws-adapter/tests/global-setup.ts +301 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
- package/packages/aws-adapter/tests/handlers.test.ts +427 -0
- package/packages/aws-adapter/tests/message-store.test.ts +55 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
- package/packages/aws-adapter/tests/serialize.test.ts +249 -0
- package/packages/aws-adapter/tests/smoke.test.ts +48 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
- package/packages/aws-adapter/tests/tables.test.ts +74 -0
- package/packages/aws-adapter/tests/transactions.test.ts +446 -0
- package/packages/aws-adapter/tsconfig.build.json +16 -0
- package/packages/aws-adapter/tsconfig.test.json +14 -0
- package/packages/aws-adapter/vitest.config.ts +23 -0
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/config-loader.ts +106 -0
- package/packages/cf-adapter/src/connection-do.ts +34 -0
- package/packages/cf-adapter/tests/cf-harness.ts +2 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +42 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +108 -3
- package/packages/in-memory-runtime/src/index.ts +1 -1
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +115 -0
- package/packages/irc-core/package.json +12 -2
- package/packages/irc-core/src/admission.ts +216 -0
- package/packages/irc-core/src/caps/capabilities.ts +1 -0
- package/packages/irc-core/src/cloak.ts +81 -0
- package/packages/irc-core/src/commands/cap.ts +1 -2
- package/packages/irc-core/src/commands/chathistory.ts +305 -0
- package/packages/irc-core/src/commands/index.ts +7 -0
- package/packages/irc-core/src/commands/join.ts +59 -7
- package/packages/irc-core/src/commands/privmsg.ts +24 -0
- package/packages/irc-core/src/commands/registration.ts +71 -14
- package/packages/irc-core/src/commands/sasl.ts +251 -0
- package/packages/irc-core/src/config.ts +247 -0
- package/packages/irc-core/src/flood-control.ts +175 -0
- package/packages/irc-core/src/index.ts +5 -0
- package/packages/irc-core/src/ports.ts +655 -0
- package/packages/irc-core/src/protocol/base64.ts +16 -0
- package/packages/irc-core/src/protocol/index.ts +2 -1
- package/packages/irc-core/src/protocol/numerics.ts +11 -0
- package/packages/irc-core/src/protocol/parser.ts +27 -2
- package/packages/irc-core/src/state/connection.ts +41 -0
- package/packages/irc-core/src/types.ts +66 -2
- package/packages/irc-core/stryker.commands.conf.json +41 -0
- package/packages/irc-core/stryker.protocol.conf.json +26 -0
- package/packages/irc-core/tests/admission.test.ts +229 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
- package/packages/irc-core/tests/cloak.test.ts +78 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
- package/packages/irc-core/tests/commands/join.test.ts +246 -2
- package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
- package/packages/irc-core/tests/commands/registration.test.ts +277 -9
- package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
- package/packages/irc-core/tests/config.test.ts +646 -0
- package/packages/irc-core/tests/flood-control.test.ts +394 -0
- package/packages/irc-core/tests/message-store.test.ts +530 -0
- package/packages/irc-core/tests/parser.test.ts +44 -1
- package/packages/irc-core/tests/ports.test.ts +263 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +162 -44
- package/packages/irc-server/src/dispatch.ts +89 -5
- package/packages/irc-server/src/index.ts +2 -0
- package/packages/irc-server/src/routing.ts +151 -0
- package/packages/irc-server/tests/actor.test.ts +470 -14
- package/packages/irc-server/tests/dispatch.test.ts +84 -0
- package/packages/irc-server/tests/routing.test.ts +201 -0
- package/packages/irc-test-support/README.md +32 -0
- package/packages/irc-test-support/package.json +37 -0
- package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +1 -1
- package/packages/irc-test-support/src/index.ts +24 -0
- package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
- package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
- package/packages/irc-test-support/tests/smoke.test.ts +11 -0
- package/packages/irc-test-support/tsconfig.build.json +16 -0
- package/packages/irc-test-support/tsconfig.test.json +14 -0
- package/packages/irc-test-support/vitest.config.ts +20 -0
- package/progress.md +107 -0
- package/tickets.md +2485 -0
- package/tools/ci-hardening/package.json +30 -0
- package/tools/ci-hardening/src/index.ts +6 -0
- package/tools/ci-hardening/src/validate.ts +103 -0
- package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
- package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
- package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
- package/tools/ci-hardening/tests/validate.test.ts +177 -0
- package/tools/ci-hardening/tsconfig.build.json +12 -0
- package/tools/ci-hardening/tsconfig.test.json +10 -0
- package/tools/ci-hardening/vitest.config.ts +21 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
- package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
- package/tools/tcp-ws-forwarder/src/main.ts +33 -14
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
- package/webircgateway/LICENSE +201 -0
- package/webircgateway/Makefile +44 -0
- package/webircgateway/README.md +134 -0
- package/webircgateway/config.conf.example +135 -0
- package/webircgateway/go.mod +16 -0
- package/webircgateway/go.sum +89 -0
- package/webircgateway/main.go +118 -0
- package/webircgateway/pkg/dnsbl/dnsbl.go +121 -0
- package/webircgateway/pkg/identd/identd.go +86 -0
- package/webircgateway/pkg/identd/rpcclient.go +59 -0
- package/webircgateway/pkg/irc/isupport.go +56 -0
- package/webircgateway/pkg/irc/message.go +217 -0
- package/webircgateway/pkg/irc/state.go +79 -0
- package/webircgateway/pkg/proxy/proxy.go +129 -0
- package/webircgateway/pkg/proxy/server.go +237 -0
- package/webircgateway/pkg/recaptcha/recaptcha.go +59 -0
- package/webircgateway/pkg/webircgateway/client.go +741 -0
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +495 -0
- package/webircgateway/pkg/webircgateway/config.go +385 -0
- package/webircgateway/pkg/webircgateway/gateway.go +278 -0
- package/webircgateway/pkg/webircgateway/gateway_utils.go +133 -0
- package/webircgateway/pkg/webircgateway/hooks.go +152 -0
- package/webircgateway/pkg/webircgateway/letsencrypt.go +41 -0
- package/webircgateway/pkg/webircgateway/messagetags.go +103 -0
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +206 -0
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +107 -0
- package/webircgateway/pkg/webircgateway/transport_tcp.go +113 -0
- package/webircgateway/pkg/webircgateway/transport_websocket.go +126 -0
- package/webircgateway/pkg/webircgateway/utils.go +147 -0
- package/webircgateway/plugins/example/plugin.go +11 -0
- package/webircgateway/plugins/stats/plugin.go +52 -0
- package/webircgateway/staticcheck.conf +1 -0
- package/webircgateway/webircgateway.svg +3 -0
- package/packages/cf-adapter/tests/integration/index.ts +0 -17
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
package/README.md
CHANGED
|
@@ -6,14 +6,16 @@ platform-agnostic core, and two thin adapters run it on **Cloudflare Workers**
|
|
|
6
6
|
|
|
7
7
|
One TypeScript codebase. Two serverless substrates. No S2S linking in v1.
|
|
8
8
|
|
|
9
|
-
> **Status:** **v0.
|
|
9
|
+
> **Status:** **v0.2.0 (preview).** The pure protocol core, the
|
|
10
10
|
> `IrcRuntime` port + in-memory runtime, a runnable local CLI server,
|
|
11
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
11
|
+
> the **Cloudflare Workers** adapter, and the **AWS** (API Gateway
|
|
12
|
+
> WebSocket + Lambda + DynamoDB + CDK) adapter are all functional and
|
|
13
|
+
> deployed to staging. SASL (`PLAIN`), token-bucket flood control,
|
|
14
|
+
> server-password enforcement, hostmask cloaking, connection admission,
|
|
15
|
+
> IRCv3 chat history, and a CI coverage + mutation-testing gate landed
|
|
16
|
+
> in v0.2.0. Remaining 0.x work: oper credentials /
|
|
17
|
+
> config loader, SASL `EXTERNAL` (mTLS), and production TLS transport.
|
|
18
|
+
> See `CHANGELOG.md` for the per-release manifests.
|
|
17
19
|
|
|
18
20
|
---
|
|
19
21
|
|
|
@@ -52,7 +54,7 @@ Hexagonal / ports-and-adapters. The core reasons about IRC; adapters move bytes.
|
|
|
52
54
|
└─────────────────────────────────────────────────────────────────┘
|
|
53
55
|
▲ ▲
|
|
54
56
|
┌────────────────────────────┐ ┌──────────────────────────────┐
|
|
55
|
-
│
|
|
57
|
+
│ packages/cf-adapter │ │ packages/aws-adapter │
|
|
56
58
|
│ ConnectionDO │ │ $connect/$disconnect/ │
|
|
57
59
|
│ ChannelDO │ │ $default Lambda handlers │
|
|
58
60
|
│ RegistryDO │ │ DynamoDB tables │
|
|
@@ -60,8 +62,8 @@ Hexagonal / ports-and-adapters. The core reasons about IRC; adapters move bytes.
|
|
|
60
62
|
└────────────────────────────┘ └──────────────────────────────┘
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
`IrcRuntime
|
|
65
|
+
`packages/in-memory-runtime` is the reference implementation of
|
|
66
|
+
`IrcRuntime`, used by integration tests and the local CLI server.
|
|
65
67
|
|
|
66
68
|
### The key idea: pure reducers + location-of-authority
|
|
67
69
|
|
|
@@ -98,34 +100,36 @@ ServerlessIRCd/
|
|
|
98
100
|
├── packages/
|
|
99
101
|
│ ├── irc-core/ pure protocol + reducers (the brain)
|
|
100
102
|
│ ├── irc-server/ orchestration, IrcRuntime port, dispatch
|
|
101
|
-
│ ├── in-memory-runtime/ reference runtime + local CLI
|
|
102
|
-
│ ├──
|
|
103
|
-
│
|
|
103
|
+
│ ├── in-memory-runtime/ reference runtime (used by tests + local CLI)
|
|
104
|
+
│ ├── irc-test-support/ parametrized IRC scenario suite + harness seam
|
|
105
|
+
│ ├── cf-adapter/ Durable Objects + CfRuntime
|
|
106
|
+
│ └── aws-adapter/ Lambda + DynamoDB + AwsRuntime
|
|
104
107
|
├── apps/
|
|
105
|
-
│ ├── cf-worker/ worker entry, DO migrations, bindings
|
|
106
|
-
│ ├── aws-stack/ CDK
|
|
107
|
-
│ └── local-cli/ runnable WS server using in-memory-runtime
|
|
108
|
+
│ ├── cf-worker/ worker entry, DO migrations, bindings
|
|
109
|
+
│ ├── aws-stack/ CDK stack (APIGW WS + Lambda + DynamoDB)
|
|
110
|
+
│ └── local-cli/ runnable WS + TCP server using in-memory-runtime
|
|
108
111
|
├── tools/
|
|
109
|
-
│
|
|
110
|
-
|
|
111
|
-
│ └── architecture-aws.md
|
|
112
|
+
│ ├── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
|
|
113
|
+
│ └── ci-hardening/ coverage-gate + mutation-config validators
|
|
112
114
|
├── pnpm-workspace.yaml turbo.json tsconfig.base.json
|
|
113
|
-
└──
|
|
115
|
+
└── README.md CHANGELOG.md
|
|
114
116
|
```
|
|
115
117
|
|
|
116
118
|
---
|
|
117
119
|
|
|
118
120
|
## Toolchain
|
|
119
121
|
|
|
120
|
-
| Concern
|
|
121
|
-
|
|
122
|
-
| Runtime
|
|
123
|
-
| Package mgr
|
|
124
|
-
| Build cache
|
|
125
|
-
| Language
|
|
126
|
-
| Test runner
|
|
127
|
-
| Property tests
|
|
128
|
-
|
|
|
122
|
+
| Concern | Choice |
|
|
123
|
+
|------------------|---------------------------------------------------|
|
|
124
|
+
| Runtime | Node ≥ 20, ES2022+ |
|
|
125
|
+
| Package mgr | pnpm workspaces |
|
|
126
|
+
| Build cache | turbo |
|
|
127
|
+
| Language | TypeScript (strict, `exactOptionalPropertyTypes`) |
|
|
128
|
+
| Test runner | vitest + `@vitest/coverage-v8` |
|
|
129
|
+
| Property tests | fast-check |
|
|
130
|
+
| Mutation tests | Stryker (≥80% score gate on irc-core) |
|
|
131
|
+
| AWS IaC | AWS CDK v2 |
|
|
132
|
+
| Lint / format | Biome |
|
|
129
133
|
|
|
130
134
|
---
|
|
131
135
|
|
|
@@ -140,26 +144,37 @@ pnpm build # build all packages (turbo)
|
|
|
140
144
|
pnpm typecheck # tsc --noEmit across the workspace
|
|
141
145
|
pnpm test # run all tests once
|
|
142
146
|
pnpm test:watch # vitest watch mode
|
|
143
|
-
pnpm coverage # tests + v8 coverage report
|
|
147
|
+
pnpm coverage # tests + v8 coverage report (enforces thresholds)
|
|
144
148
|
|
|
145
149
|
pnpm lint # biome check .
|
|
146
150
|
pnpm lint:fix # biome check --write .
|
|
147
151
|
pnpm format # biome format --write .
|
|
148
152
|
|
|
153
|
+
pnpm mutation # Stryker spot-check on irc-core (protocol + commands)
|
|
154
|
+
|
|
149
155
|
pnpm clean # remove dist/coverage/.turbo + node_modules
|
|
150
156
|
```
|
|
151
157
|
|
|
152
158
|
Coverage reports are written to `packages/*/coverage/`. CI (`.github/workflows/ci.yml`)
|
|
153
|
-
runs lint, typecheck,
|
|
159
|
+
runs lint, typecheck, the coverage gate, the parametrized contract suite, and a
|
|
160
|
+
Stryker mutation spot-check on every push and pull request. Coverage thresholds
|
|
161
|
+
enforce 100% on `irc-core` / `irc-server` / `in-memory-runtime` and ≥90% on every
|
|
162
|
+
other package.
|
|
154
163
|
|
|
155
164
|
---
|
|
156
165
|
|
|
157
166
|
## Running the local server
|
|
158
167
|
|
|
159
|
-
`apps/local-cli` is a runnable
|
|
160
|
-
|
|
161
|
-
`irc-core` reducers and `ConnectionActor` the cloud adapters
|
|
162
|
-
wired to
|
|
168
|
+
`apps/local-cli` is a runnable IRC server built on the in-memory runtime.
|
|
169
|
+
It's the manual-test harness and the e2e fixture target — the same
|
|
170
|
+
`irc-core` reducers and `ConnectionActor` the cloud adapters use, just
|
|
171
|
+
wired to plain localhost listeners. By default it binds **two** listeners
|
|
172
|
+
that share one runtime, so a TCP client and a WS client can see each other
|
|
173
|
+
(cross-transport channel broadcast):
|
|
174
|
+
|
|
175
|
+
- a **WebSocket** listener (the serverless transport) on `--port`, and
|
|
176
|
+
- an **RFC-style TCP** listener on `--tcp-port` (default `<port> + 1`) so
|
|
177
|
+
real IRC clients (WeeChat, HexChat, irssi, …) can dial in directly.
|
|
163
178
|
|
|
164
179
|
First build the workspace (the CLI runs from compiled `dist/`):
|
|
165
180
|
|
|
@@ -178,37 +193,50 @@ pnpm --filter local-cli start --port 6667 --host 127.0.0.1
|
|
|
178
193
|
On startup it logs a JSON line like:
|
|
179
194
|
|
|
180
195
|
```json
|
|
181
|
-
{"ts":"...","level":"info","msg":"local-cli listening","
|
|
196
|
+
{"ts":"...","level":"info","msg":"local-cli listening","wsUrl":"ws://127.0.0.1:6667/","port":6667,"tcpPort":6668,"hostname":"127.0.0.1"}
|
|
182
197
|
```
|
|
183
198
|
|
|
184
199
|
### CLI flags
|
|
185
200
|
|
|
186
|
-
| Flag
|
|
187
|
-
|
|
188
|
-
| `--port <n>`
|
|
189
|
-
| `--host <h>`
|
|
190
|
-
|
|
|
201
|
+
| Flag | Default | Description |
|
|
202
|
+
|--------------------|---------------|--------------------------------------------------------|
|
|
203
|
+
| `--port <n>` | `6667` | WebSocket port to bind. Use `0` for an ephemeral. |
|
|
204
|
+
| `--host <h>` | `127.0.0.1` | Hostname / interface to bind. |
|
|
205
|
+
| `--tcp-port <n>` | `<port> + 1` | TCP port for RFC-style IRC clients. |
|
|
206
|
+
| `--no-tcp` | | Disable the TCP listener (WebSocket only). |
|
|
207
|
+
| `--motd-file <p>` | built-in | Read MOTD lines from this file (one per line). |
|
|
208
|
+
| `-h, --help` | | Show help and exit. |
|
|
191
209
|
|
|
192
210
|
Pass `--host 0.0.0.0` to expose the server on all interfaces. `SIGINT` /
|
|
193
211
|
`SIGTERM` perform a graceful shutdown (closes active sockets, then exits).
|
|
194
212
|
|
|
195
213
|
### Connecting
|
|
196
214
|
|
|
197
|
-
The
|
|
198
|
-
WebSocket-capable IRC client
|
|
199
|
-
|
|
200
|
-
|
|
215
|
+
The WebSocket listener speaks WebSocket text frames (one IRC message per
|
|
216
|
+
frame) — point any WebSocket-capable IRC client or script at
|
|
217
|
+
`ws://127.0.0.1:6667/`. The TCP listener speaks plain RFC IRC over the
|
|
218
|
+
wire, so stock clients connect directly, e.g. in WeeChat:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
/connect 127.0.0.1/6668
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
No `tcp-ws-forwarder` is needed for local development — both transports
|
|
225
|
+
are built in. The MOTD is the built-in default unless `--motd-file` is
|
|
226
|
+
given.
|
|
201
227
|
|
|
202
228
|
---
|
|
203
229
|
|
|
204
|
-
## Connecting a TCP IRC client (TCP→WS forwarder)
|
|
230
|
+
## Connecting a TCP IRC client to a deployed stack (TCP→WS forwarder)
|
|
205
231
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
232
|
+
The local CLI ships a built-in TCP listener, so no bridge is needed for local
|
|
233
|
+
development. Deployed stacks (Cloudflare Workers and AWS API Gateway) are
|
|
234
|
+
WebSocket-only, so a stock TCP IRC client (WeeChat, HexChat, irssi, …) cannot
|
|
235
|
+
dial them directly. `tools/tcp-ws-forwarder` is a local bridge for that case:
|
|
236
|
+
it listens on a TCP port and, for each connection, opens one WebSocket to a
|
|
237
|
+
`ws://` / `wss://` target and pumps IRC lines both ways — reassembling the TCP
|
|
238
|
+
byte stream into one message per WS frame outbound, and normalizing inbound
|
|
239
|
+
frames back to canonical CRLF.
|
|
212
240
|
|
|
213
241
|
Build the workspace, then start the forwarder (from the repo root):
|
|
214
242
|
|
|
@@ -255,14 +283,15 @@ This project follows strict TDD (Red → Green → Refactor) — every reducer i
|
|
|
255
283
|
landed test-first. See the project-level `CLAUDE.md` / `AGENTS.md` for the
|
|
256
284
|
full rules.
|
|
257
285
|
|
|
258
|
-
| Layer
|
|
259
|
-
|
|
260
|
-
| Parser/serializer| vitest + fast-check
|
|
261
|
-
| Reducers (core)
|
|
262
|
-
|
|
|
263
|
-
|
|
|
264
|
-
|
|
|
265
|
-
|
|
|
286
|
+
| Layer | Tooling | What it asserts |
|
|
287
|
+
|--------------------|---------------------------------------------|---------------------------------------|
|
|
288
|
+
| Parser/serializer | vitest + fast-check | Grammar correctness, round-trip |
|
|
289
|
+
| Reducers (core) | vitest pure unit tests | `(state,msg) → {state,effects}` exact |
|
|
290
|
+
| Mutation (core) | Stryker spot-check (≥80% score gate) | Reducer test quality, surviving mutants |
|
|
291
|
+
| Runtime contract | vitest parametrized via `irc-test-support` | Same scenarios pass in-memory+CF+AWS |
|
|
292
|
+
| CF adapter | `vitest-pool-workers` (real workerd) | DO behavior, alarms, stub fanout |
|
|
293
|
+
| AWS adapter | vitest + testcontainers (DynamoDB Local) | DynamoDB schema, transactions, fanout |
|
|
294
|
+
| E2E | `scripts/smoke.mjs` against deployed stack | CONNECT→REGISTER→JOIN→PRIVMSG→QUIT |
|
|
266
295
|
|
|
267
296
|
**Determinism:** reducers never touch real timers or randomness. A `Clock`
|
|
268
297
|
port and an `IdFactory` port are injected so tests are fully deterministic.
|
|
@@ -274,20 +303,22 @@ port and an `IdFactory` port are injected so tests are fully deterministic.
|
|
|
274
303
|
A v1 ships when `irc-core` is at 100% coverage, the parametrized contract
|
|
275
304
|
suite passes against every runtime, both adapters are deployed to staging,
|
|
276
305
|
and ≥3 reference clients (WeeChat, HexChat, IRCCloud, TheLounge) connect
|
|
277
|
-
cleanly.
|
|
278
|
-
manifests live in `CHANGELOG.md`; the release process is documented in
|
|
279
|
-
`docs/release.md`.
|
|
306
|
+
cleanly. Per-release manifests live in `CHANGELOG.md`.
|
|
280
307
|
|
|
281
308
|
- **Phase 0** — Foundation (monorepo, turbo, vitest, biome, CI). ✅
|
|
282
|
-
- **Phase 1** — Pure protocol engine: reducers, IRCv3 caps, isupport
|
|
283
|
-
|
|
284
|
-
|
|
309
|
+
- **Phase 1** — Pure protocol engine: reducers, IRCv3 caps, isupport,
|
|
310
|
+
SASL (`PLAIN`), token-bucket flood control, chat history. ✅ (most in
|
|
311
|
+
**v0.1.0**; SASL/flood/chathistory landed in **v0.2.0**).
|
|
285
312
|
- **Phase 2** — `IrcRuntime` port, in-memory runtime, `ConnectionActor`,
|
|
286
313
|
local CLI, parametrized contract suite. ✅ landed in **v0.1.0**.
|
|
287
314
|
- **Phase 3** — Cloudflare adapter (ConnectionDO / ChannelDO / RegistryDO).
|
|
288
|
-
✅ landed in **v0.1.0** (staging
|
|
315
|
+
✅ landed in **v0.1.0** (staging auto-deploy via CI; prod deploy is manual).
|
|
289
316
|
- **Phase 4** — AWS adapter (APIGW WS + Lambda + DynamoDB + CDK).
|
|
317
|
+
✅ landed in **v0.2.0** (staging auto-deploy via CI; prod deploy is manual).
|
|
290
318
|
- **Phase 5** — Observability, security hardening, config, CI gates.
|
|
319
|
+
✅ mostly landed (logger port, server-password, cloaking, admission
|
|
320
|
+
limits, coverage + mutation gates). Remaining: oper credentials / config
|
|
321
|
+
loader.
|
|
291
322
|
- **Phase 6** — Load testing, client compatibility sweep, ADRs.
|
|
292
323
|
|
|
293
324
|
---
|
|
@@ -296,28 +327,31 @@ manifests live in `CHANGELOG.md`; the release process is documented in
|
|
|
296
327
|
|
|
297
328
|
**Core (RFC 1459/2812 subset):** registration (`NICK`/`USER`/`CAP`/`PASS`),
|
|
298
329
|
`PING`/`PONG`, `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`, `MODE` (user +
|
|
299
|
-
channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`, `MOTD
|
|
330
|
+
channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`, `MOTD`,
|
|
331
|
+
`AWAY`.
|
|
300
332
|
|
|
301
333
|
**Channel modes:** `o v b i k l t n m s p`.
|
|
302
334
|
**User modes:** `i`, `o` (local only), `w`, `s`.
|
|
303
335
|
|
|
304
336
|
**IRCv3 extensions (negotiated via `CAP`):** `message-tags`, `server-time`,
|
|
305
|
-
`account-tag`, `echo-message`, `batch`, `sasl` (`PLAIN
|
|
306
|
-
`multi-prefix`, `away-notify`, `chghost`,
|
|
337
|
+
`account-tag`, `echo-message`, `batch`, `sasl` (`PLAIN` implemented,
|
|
338
|
+
`EXTERNAL` reserved pending mTLS), `multi-prefix`, `away-notify`, `chghost`,
|
|
339
|
+
`invite-notify`, `extended-join`, `draft/chathistory`, `safelist`.
|
|
307
340
|
|
|
308
|
-
**Transport:**
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
341
|
+
**Transport:** deployed stacks (Cloudflare Workers, AWS API Gateway) speak
|
|
342
|
+
WebSocket text frames only (one IRC message per frame, with tolerance for
|
|
343
|
+
`\r\n`-joined frames). The local CLI additionally binds a plain RFC TCP
|
|
344
|
+
listener for direct IRC-client access. Stock TCP IRC clients reach a deployed
|
|
345
|
+
WebSocket endpoint through the local
|
|
346
|
+
[`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-to-a-deployed-stack-tcpws-forwarder).
|
|
312
347
|
|
|
313
348
|
---
|
|
314
349
|
|
|
315
350
|
## Further reading
|
|
316
351
|
|
|
317
|
-
- `
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
-
|
|
322
|
-
|
|
323
|
-
- `docs/architecture-aws.md` — AWS adapter design notes.
|
|
352
|
+
- `CHANGELOG.md` — per-release manifests (Keep a Changelog format), including
|
|
353
|
+
the v0.2.0 work (AWS adapter, SASL, flood control, security hardening, CI
|
|
354
|
+
gates).
|
|
355
|
+
- `README.md` in each `packages/*` and `apps/*` — per-package notes (e.g.
|
|
356
|
+
`packages/aws-adapter/README.md` for the DynamoDB-Local test setup,
|
|
357
|
+
`apps/aws-stack/README.md` for CDK commands and localstack validation).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# `@serverless-ircd/aws-stack`
|
|
2
|
+
|
|
3
|
+
AWS CDK v2 stack that provisions the AWS substrate for ServerlessIRCd
|
|
4
|
+
(PLAN §6.2): an API Gateway v2 WebSocket API, a single Node 20 Lambda wired
|
|
5
|
+
to the `$connect` / `$disconnect` / `$default` routes, five DynamoDB tables
|
|
6
|
+
(`Connections`, `ChannelMeta`, `ChannelMembers`, `Nicks`, `Accounts`), and the
|
|
7
|
+
least-privilege IAM glue to let the Lambda read/write the tables and post back
|
|
8
|
+
to connected clients via the management API.
|
|
9
|
+
|
|
10
|
+
The Lambda handler is currently a stub (`{ statusCode: 200 }`); the real
|
|
11
|
+
`AwsRuntime` dispatch lands in TICKET-040.
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- Node.js 20+
|
|
16
|
+
- pnpm (workspace root installs this package)
|
|
17
|
+
- An AWS account with credentials configured
|
|
18
|
+
(`aws configure`, `AWS_PROFILE`, or `SSO`)
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pnpm cdk:synth # synthesize the CloudFormation template (cdk.out/)
|
|
24
|
+
pnpm deploy:staging # cdk deploy --all
|
|
25
|
+
pnpm deploy:prod # cdk deploy --all --require-approval never
|
|
26
|
+
pnpm test # synth-time assertions (no AWS, no Docker)
|
|
27
|
+
pnpm typecheck
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Repo-level shortcuts (from the monorepo root):
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pnpm deploy:aws:staging
|
|
34
|
+
pnpm deploy:aws:prod
|
|
35
|
+
pnpm smoke:aws:staging # deferred to TICKET-043
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Optional localstack validation
|
|
39
|
+
|
|
40
|
+
The default `pnpm test` runs only the in-process synth assertions
|
|
41
|
+
(`tests/stack.test.ts`). A deploy-time check against localstack lives in
|
|
42
|
+
`tests/localstack.test.ts` and is **skipped unless `LOCALSTACK=1`** is set,
|
|
43
|
+
so CI never depends on a Docker daemon.
|
|
44
|
+
|
|
45
|
+
To run it locally:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
# 1. Start localstack (requires Docker)
|
|
49
|
+
docker run --rm -d -p 4566:4566 localstack/localstack
|
|
50
|
+
|
|
51
|
+
# 2. Install the localstack-aware CDK CLI globally
|
|
52
|
+
npm install -g aws-cdk-local
|
|
53
|
+
|
|
54
|
+
# 3. Point the AWS SDK at localstack (cdklocal reads these)
|
|
55
|
+
export AWS_ACCESS_KEY_ID=test
|
|
56
|
+
export AWS_SECRET_ACCESS_KEY=test
|
|
57
|
+
export AWS_DEFAULT_REGION=us-east-1
|
|
58
|
+
|
|
59
|
+
# 4. Run the gated test
|
|
60
|
+
LOCALSTACK=1 pnpm test
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Stack outputs
|
|
64
|
+
|
|
65
|
+
| Output | Description |
|
|
66
|
+
|----------------|--------------------------------------------------------------------|
|
|
67
|
+
| `ConnectUrl` | `wss://` endpoint IRC clients dial. |
|
|
68
|
+
| `ManagementUrl`| `https://` endpoint for `ApiGatewayManagementApi.postToConnection`.|
|
|
69
|
+
|
|
70
|
+
## Handoff
|
|
71
|
+
|
|
72
|
+
Lambda runtime logic, DynamoDB access patterns, and the `AwsRuntime`
|
|
73
|
+
implementation are TICKET-040. The scheduler/sweeper is TICKET-041/042.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDK app entry point for `apps/aws-stack`.
|
|
3
|
+
*
|
|
4
|
+
* Run via `tsx bin/aws.ts` (see `cdk.json`). Environment resolves to the
|
|
5
|
+
* CDK CLI's default account/region when those env vars are set; otherwise
|
|
6
|
+
* the stack is environment-agnostic (suitable for `cdk synth` and localstack).
|
|
7
|
+
*
|
|
8
|
+
* Per-environment isolation is driven by the `environmentName` CDK context
|
|
9
|
+
* variable (`-c environmentName=staging`). It defaults to `staging` so the
|
|
10
|
+
* existing deploy pipeline stays green without changes, and it is folded
|
|
11
|
+
* into the CloudFormation stack id (`IrcAwsStack-<environmentName>`) so
|
|
12
|
+
* parallel envs coexist in one account+region.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { App } from 'aws-cdk-lib';
|
|
16
|
+
import type { IrcStackProps } from '../src/aws-stack.js';
|
|
17
|
+
import { IrcAwsStack } from '../src/aws-stack.js';
|
|
18
|
+
|
|
19
|
+
const app = new App();
|
|
20
|
+
|
|
21
|
+
const account = process.env.CDK_DEFAULT_ACCOUNT;
|
|
22
|
+
const region = process.env.CDK_DEFAULT_REGION;
|
|
23
|
+
const environmentName =
|
|
24
|
+
(app.node.tryGetContext('environmentName') as string | undefined) ?? 'staging';
|
|
25
|
+
|
|
26
|
+
// Server identity is read from CDK context so deploys can override it
|
|
27
|
+
// without editing source (`-c serverName=…`, `-c networkName=…`,
|
|
28
|
+
// `-c motd=…`). `motd` may be passed as a `\n`-embedded string from the
|
|
29
|
+
// CLI; split it back into lines so the stack re-joins with `\n` and the
|
|
30
|
+
// Lambda config loader round-trips it correctly. Defaults (omitted here)
|
|
31
|
+
// live on the stack itself.
|
|
32
|
+
const serverName = app.node.tryGetContext('serverName') as string | undefined;
|
|
33
|
+
const networkName = app.node.tryGetContext('networkName') as string | undefined;
|
|
34
|
+
const rawMotd = app.node.tryGetContext('motd');
|
|
35
|
+
const motdLines = Array.isArray(rawMotd)
|
|
36
|
+
? (rawMotd as string[])
|
|
37
|
+
: typeof rawMotd === 'string'
|
|
38
|
+
? rawMotd.split('\n')
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
const stackProps: IrcStackProps = {
|
|
42
|
+
environmentName,
|
|
43
|
+
...(serverName !== undefined ? { serverName } : {}),
|
|
44
|
+
...(networkName !== undefined ? { networkName } : {}),
|
|
45
|
+
...(motdLines !== undefined ? { motdLines } : {}),
|
|
46
|
+
...(account && region ? { env: { account, region } } : {}),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
new IrcAwsStack(app, `IrcAwsStack-${environmentName}`, stackProps);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@serverless-ircd/aws-stack",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "AWS CDK v2 stack: API Gateway v2 WebSocket API, Lambda, DynamoDB tables, least-privilege IAM",
|
|
6
|
+
"license": "BSD-3-Clause",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.build.json",
|
|
10
|
+
"typecheck": "tsc -p tsconfig.test.json --noEmit",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"coverage": "vitest run --coverage",
|
|
14
|
+
"clean": "rimraf dist coverage .tsbuildinfo .turbo cdk.out",
|
|
15
|
+
"cdk": "cdk",
|
|
16
|
+
"cdk:synth": "cdk synth",
|
|
17
|
+
"deploy:staging": "cdk deploy --all --require-approval never",
|
|
18
|
+
"deploy:prod": "cdk deploy --all --require-approval never",
|
|
19
|
+
"smoke:staging": "node scripts/smoke.mjs"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"aws-cdk": "^2.160.0",
|
|
23
|
+
"aws-cdk-lib": "^2.160.0",
|
|
24
|
+
"constructs": "^10.4.0",
|
|
25
|
+
"@types/node": "^26.1.1",
|
|
26
|
+
"@types/ws": "^8.5.13",
|
|
27
|
+
"esbuild": "^0.28.1",
|
|
28
|
+
"rimraf": "^6.0.0",
|
|
29
|
+
"tsx": "^4.19.0",
|
|
30
|
+
"typescript": "^5.6.0",
|
|
31
|
+
"vitest": "^4.1.0",
|
|
32
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
33
|
+
"ws": "^8.18.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@serverless-ircd/aws-adapter": "workspace:*"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for `scripts/smoke-helpers.mjs` (plain ESM consumed by the
|
|
3
|
+
* AWS staging smoke e2e). The runtime file ships as `.mjs` so CI can run it
|
|
4
|
+
* without a build step; this declaration lets the workspace typecheck resolve
|
|
5
|
+
* the imports from `tests/smoke-helpers.test.ts`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type ParseResult = { url: string; help: boolean };
|
|
9
|
+
|
|
10
|
+
export declare const DEFAULT_SMOKE_URL: string;
|
|
11
|
+
|
|
12
|
+
export declare function numericEquals(line: string, code: string): boolean;
|
|
13
|
+
|
|
14
|
+
export declare function parseArgs(argv: string[]): ParseResult;
|
|
15
|
+
|
|
16
|
+
export declare function waitForPredicate(
|
|
17
|
+
predicate: (line: string) => boolean,
|
|
18
|
+
label: string,
|
|
19
|
+
received: string[],
|
|
20
|
+
timeoutMs: number,
|
|
21
|
+
pollMs: number,
|
|
22
|
+
): Promise<string>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers backing the AWS staging smoke e2e (`scripts/smoke.mjs`).
|
|
3
|
+
*
|
|
4
|
+
* Kept dependency-free and side-effect-free (apart from the unavoidable
|
|
5
|
+
* `setTimeout` polling inside `waitForPredicate`) so they can be unit-tested
|
|
6
|
+
* in isolation. The smoke script itself owns the WebSocket I/O and process
|
|
7
|
+
* exit codes; these helpers own the parsing and timing logic.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Default WebSocket target when no `--url` is passed (matches the local dev convention). */
|
|
11
|
+
export const DEFAULT_SMOKE_URL = 'ws://localhost:8787';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Numeric command position in an IRC line (after an optional `:server`
|
|
15
|
+
* prefix). Recognises both `001 :Welcome` and `:irc.example.com 001 ...`.
|
|
16
|
+
*/
|
|
17
|
+
export function numericEquals(line, code) {
|
|
18
|
+
const parts = line.split(' ');
|
|
19
|
+
const start = parts[0]?.startsWith(':') ? 1 : 0;
|
|
20
|
+
return parts[start] === code;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse the smoke script's CLI args. Returns `{ url, help }` — `url` always
|
|
25
|
+
* carries a sensible default, `help` is set when `-h`/`--help` was requested
|
|
26
|
+
* so the caller can print usage and exit 0. Throws on unknown flags or a
|
|
27
|
+
* dangling `--url` with no value.
|
|
28
|
+
*
|
|
29
|
+
* @param {string[]} argv
|
|
30
|
+
* @returns {{ url: string; help: boolean }}
|
|
31
|
+
*/
|
|
32
|
+
export function parseArgs(argv) {
|
|
33
|
+
let url = DEFAULT_SMOKE_URL;
|
|
34
|
+
let help = false;
|
|
35
|
+
for (let i = 0; i < argv.length; i++) {
|
|
36
|
+
const a = argv[i];
|
|
37
|
+
const next = argv[i + 1];
|
|
38
|
+
if (a === '--') {
|
|
39
|
+
// POSIX options terminator — `pnpm <script> -- --url x` forwards it.
|
|
40
|
+
// Nothing positional follows for this script; just consume and continue.
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (a === '--url') {
|
|
44
|
+
if (next === undefined) {
|
|
45
|
+
throw new Error('--url requires a value');
|
|
46
|
+
}
|
|
47
|
+
url = next;
|
|
48
|
+
i++;
|
|
49
|
+
} else if (a === '-h' || a === '--help') {
|
|
50
|
+
help = true;
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error(`unknown argument: ${a}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { url, help };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve with the first entry in `received` that satisfies `predicate`, or
|
|
60
|
+
* reject with a descriptive message once `timeoutMs` elapses. Polls every
|
|
61
|
+
* `pollMs` so a live socket can keep appending to `received` (by reference)
|
|
62
|
+
* between checks.
|
|
63
|
+
*
|
|
64
|
+
* @param {(line: string) => boolean} predicate
|
|
65
|
+
* @param {string} label
|
|
66
|
+
* @param {string[]} received
|
|
67
|
+
* @param {number} timeoutMs
|
|
68
|
+
* @param {number} pollMs
|
|
69
|
+
* @returns {Promise<string>}
|
|
70
|
+
*/
|
|
71
|
+
export function waitForPredicate(predicate, label, received, timeoutMs, pollMs) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const deadline = Date.now() + timeoutMs;
|
|
74
|
+
const check = () => {
|
|
75
|
+
for (const line of received) {
|
|
76
|
+
if (predicate(line)) {
|
|
77
|
+
resolve(line);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (Date.now() >= deadline) {
|
|
82
|
+
reject(new Error(`timed out waiting for ${label}; received=${JSON.stringify(received)}`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
setTimeout(check, pollMs);
|
|
86
|
+
};
|
|
87
|
+
check();
|
|
88
|
+
});
|
|
89
|
+
}
|