serverless-ircd 0.1.0 → 0.3.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/CHANGELOG.md +436 -0
- package/README.md +127 -84
- 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/scripts/smoke.mjs +0 -0
- package/apps/cf-worker/src/worker.ts +8 -2
- package/apps/cf-worker/tests/smoke.test.ts +1 -0
- package/apps/cf-worker/wrangler.test.toml +5 -1
- package/apps/cf-worker/wrangler.toml +66 -17
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +57 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +267 -32
- package/apps/local-cli/tests/config-loader.test.ts +107 -0
- package/apps/local-cli/tests/e2e.test.ts +126 -0
- package/apps/local-cli/tests/security-e2e.test.ts +239 -0
- package/biome.json +3 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
- package/docs/ADR-002-location-of-authority.md +82 -0
- package/docs/ADR-003-durable-object-sharding.md +93 -0
- package/docs/ADR-004-dynamodb-schema.md +96 -0
- package/docs/ADR-005-wss-only-transport-v1.md +83 -0
- package/docs/ADR-006-sasl-mechanism-scope.md +86 -0
- package/docs/ADR-007-deterministic-ports.md +82 -0
- package/docs/ADR-008-monorepo-tooling.md +60 -0
- package/docs/AWS-Adapter-Architecture.md +496 -0
- package/docs/AWS-Deployment.md +1186 -0
- package/docs/Cloudflare-Deployment-Guide.md +660 -0
- package/docs/Home.md +11 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanIRCv3Websocket.md +489 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +443 -0
- package/package.json +19 -14
- package/packages/aws-adapter/README.md +35 -0
- package/packages/aws-adapter/package.json +52 -0
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/aws-runtime.ts +871 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +73 -0
- package/packages/aws-adapter/src/config-loader.ts +126 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +156 -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 +330 -0
- package/packages/aws-adapter/src/handlers/disconnect.ts +48 -0
- package/packages/aws-adapter/src/handlers/index.ts +293 -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 +74 -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/account-store-dynamo.test.ts +171 -0
- package/packages/aws-adapter/tests/account-store.test.ts +280 -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 +213 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +336 -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 +473 -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/cf-runtime.ts +42 -22
- package/packages/cf-adapter/src/channel-do.ts +40 -1
- package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
- package/packages/cf-adapter/src/config-loader.ts +135 -0
- package/packages/cf-adapter/src/connection-do.ts +119 -0
- package/packages/cf-adapter/src/env.ts +27 -1
- package/packages/cf-adapter/src/index.ts +2 -1
- 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/cf-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +82 -0
- package/packages/cf-adapter/tests/worker/main.ts +2 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- package/packages/cf-adapter/wrangler.test.toml +10 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +107 -16
- package/packages/in-memory-runtime/src/index.ts +1 -1
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +134 -0
- package/packages/irc-core/package.json +13 -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/case-fold.ts +64 -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 +9 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +63 -10
- package/packages/irc-core/src/commands/kick.ts +4 -11
- package/packages/irc-core/src/commands/list.ts +5 -4
- package/packages/irc-core/src/commands/mode.ts +5 -9
- package/packages/irc-core/src/commands/motd-lines.ts +127 -0
- package/packages/irc-core/src/commands/motd.ts +6 -110
- package/packages/irc-core/src/commands/oper.ts +151 -0
- package/packages/irc-core/src/commands/privmsg.ts +24 -0
- package/packages/irc-core/src/commands/registration.ts +79 -21
- package/packages/irc-core/src/commands/sasl.ts +251 -0
- package/packages/irc-core/src/commands/tagmsg.ts +205 -0
- package/packages/irc-core/src/config.ts +270 -0
- package/packages/irc-core/src/flood-control.ts +175 -0
- package/packages/irc-core/src/index.ts +7 -0
- package/packages/irc-core/src/ports.ts +719 -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 +88 -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/account-store.test.ts +88 -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/case-fold.test.ts +80 -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/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
- package/packages/irc-core/tests/commands/registration.test.ts +380 -20
- package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +688 -0
- package/packages/irc-core/tests/commands/who.test.ts +22 -4
- package/packages/irc-core/tests/commands/whois.test.ts +8 -1
- package/packages/irc-core/tests/config.test.ts +721 -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 +186 -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 +160 -0
- package/packages/irc-server/tests/actor.test.ts +690 -15
- package/packages/irc-server/tests/dispatch.test.ts +84 -0
- package/packages/irc-server/tests/routing.test.ts +204 -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 +6 -5
- 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/pnpm-workspace.yaml +1 -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/seed-aws-accounts.ts +80 -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/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,20 @@ 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.3.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. v0.2.0 delivered SASL (`PLAIN`), token-bucket
|
|
14
|
+
> flood control, server-password enforcement, hostmask cloaking,
|
|
15
|
+
> connection admission, and IRCv3 chat history; v0.3.0 adds **OPER**
|
|
16
|
+
> credential auth, the IRCv3 **TAGMSG** command, **RFC 1459
|
|
17
|
+
> case-mapping**, a real **MOTD** in the registration welcome, a
|
|
18
|
+
> **ChannelRegistryDO** (4th Cloudflare Durable Object) plus a
|
|
19
|
+
> Cloudflare config loader, and **SASL account persistence** in
|
|
20
|
+
> DynamoDB (scrypt hashes) with a `seed-aws-accounts` provisioning
|
|
21
|
+
> tool. Remaining 0.x work: SASL `EXTERNAL` (mTLS) and production TLS
|
|
22
|
+
> transport. See `CHANGELOG.md` for the per-release manifests.
|
|
17
23
|
|
|
18
24
|
---
|
|
19
25
|
|
|
@@ -52,16 +58,17 @@ Hexagonal / ports-and-adapters. The core reasons about IRC; adapters move bytes.
|
|
|
52
58
|
└─────────────────────────────────────────────────────────────────┘
|
|
53
59
|
▲ ▲
|
|
54
60
|
┌────────────────────────────┐ ┌──────────────────────────────┐
|
|
55
|
-
│
|
|
61
|
+
│ packages/cf-adapter │ │ packages/aws-adapter │
|
|
56
62
|
│ ConnectionDO │ │ $connect/$disconnect/ │
|
|
57
63
|
│ ChannelDO │ │ $default Lambda handlers │
|
|
58
|
-
│
|
|
59
|
-
│
|
|
64
|
+
│ ChannelRegistryDO │ │ DynamoDB tables │
|
|
65
|
+
│ RegistryDO │ │ AwsRuntime │
|
|
66
|
+
│ CfRuntime │ │ │
|
|
60
67
|
└────────────────────────────┘ └──────────────────────────────┘
|
|
61
68
|
```
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
`IrcRuntime
|
|
70
|
+
`packages/in-memory-runtime` is the reference implementation of
|
|
71
|
+
`IrcRuntime`, used by integration tests and the local CLI server.
|
|
65
72
|
|
|
66
73
|
### The key idea: pure reducers + location-of-authority
|
|
67
74
|
|
|
@@ -98,34 +105,37 @@ ServerlessIRCd/
|
|
|
98
105
|
├── packages/
|
|
99
106
|
│ ├── irc-core/ pure protocol + reducers (the brain)
|
|
100
107
|
│ ├── irc-server/ orchestration, IrcRuntime port, dispatch
|
|
101
|
-
│ ├── in-memory-runtime/ reference runtime + local CLI
|
|
102
|
-
│ ├──
|
|
103
|
-
│
|
|
108
|
+
│ ├── in-memory-runtime/ reference runtime (used by tests + local CLI)
|
|
109
|
+
│ ├── irc-test-support/ parametrized IRC scenario suite + harness seam
|
|
110
|
+
│ ├── cf-adapter/ Durable Objects + CfRuntime
|
|
111
|
+
│ └── aws-adapter/ Lambda + DynamoDB + AwsRuntime
|
|
104
112
|
├── apps/
|
|
105
|
-
│ ├── cf-worker/ worker entry, DO migrations, bindings
|
|
106
|
-
│ ├── aws-stack/ CDK
|
|
107
|
-
│ └── local-cli/ runnable WS server using in-memory-runtime
|
|
113
|
+
│ ├── cf-worker/ worker entry, DO migrations, bindings
|
|
114
|
+
│ ├── aws-stack/ CDK stack (APIGW WS + Lambda + DynamoDB)
|
|
115
|
+
│ └── local-cli/ runnable WS + TCP server using in-memory-runtime
|
|
108
116
|
├── tools/
|
|
109
|
-
│
|
|
110
|
-
├──
|
|
111
|
-
│ └──
|
|
117
|
+
│ ├── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
|
|
118
|
+
│ ├── ci-hardening/ coverage-gate + mutation-config validators
|
|
119
|
+
│ └── seed-aws-accounts.ts scrypt-hash SASL PLAIN accounts into DynamoDB
|
|
112
120
|
├── pnpm-workspace.yaml turbo.json tsconfig.base.json
|
|
113
|
-
└──
|
|
121
|
+
└── README.md CHANGELOG.md
|
|
114
122
|
```
|
|
115
123
|
|
|
116
124
|
---
|
|
117
125
|
|
|
118
126
|
## Toolchain
|
|
119
127
|
|
|
120
|
-
| Concern
|
|
121
|
-
|
|
122
|
-
| Runtime
|
|
123
|
-
| Package mgr
|
|
124
|
-
| Build cache
|
|
125
|
-
| Language
|
|
126
|
-
| Test runner
|
|
127
|
-
| Property tests
|
|
128
|
-
|
|
|
128
|
+
| Concern | Choice |
|
|
129
|
+
|------------------|---------------------------------------------------|
|
|
130
|
+
| Runtime | Node ≥ 20, ES2022+ |
|
|
131
|
+
| Package mgr | pnpm workspaces |
|
|
132
|
+
| Build cache | turbo |
|
|
133
|
+
| Language | TypeScript (strict, `exactOptionalPropertyTypes`) |
|
|
134
|
+
| Test runner | vitest + `@vitest/coverage-v8` |
|
|
135
|
+
| Property tests | fast-check |
|
|
136
|
+
| Mutation tests | Stryker (≥80% score gate on irc-core) |
|
|
137
|
+
| AWS IaC | AWS CDK v2 |
|
|
138
|
+
| Lint / format | Biome |
|
|
129
139
|
|
|
130
140
|
---
|
|
131
141
|
|
|
@@ -140,26 +150,37 @@ pnpm build # build all packages (turbo)
|
|
|
140
150
|
pnpm typecheck # tsc --noEmit across the workspace
|
|
141
151
|
pnpm test # run all tests once
|
|
142
152
|
pnpm test:watch # vitest watch mode
|
|
143
|
-
pnpm coverage # tests + v8 coverage report
|
|
153
|
+
pnpm coverage # tests + v8 coverage report (enforces thresholds)
|
|
144
154
|
|
|
145
155
|
pnpm lint # biome check .
|
|
146
156
|
pnpm lint:fix # biome check --write .
|
|
147
157
|
pnpm format # biome format --write .
|
|
148
158
|
|
|
159
|
+
pnpm mutation # Stryker spot-check on irc-core (protocol + commands)
|
|
160
|
+
|
|
149
161
|
pnpm clean # remove dist/coverage/.turbo + node_modules
|
|
150
162
|
```
|
|
151
163
|
|
|
152
164
|
Coverage reports are written to `packages/*/coverage/`. CI (`.github/workflows/ci.yml`)
|
|
153
|
-
runs lint, typecheck,
|
|
165
|
+
runs lint, typecheck, the coverage gate, the parametrized contract suite, and a
|
|
166
|
+
Stryker mutation spot-check on every push and pull request. Coverage thresholds
|
|
167
|
+
enforce 100% on `irc-core` / `irc-server` / `in-memory-runtime` and ≥90% on every
|
|
168
|
+
other package.
|
|
154
169
|
|
|
155
170
|
---
|
|
156
171
|
|
|
157
172
|
## Running the local server
|
|
158
173
|
|
|
159
|
-
`apps/local-cli` is a runnable
|
|
160
|
-
|
|
161
|
-
`irc-core` reducers and `ConnectionActor` the cloud adapters
|
|
162
|
-
wired to
|
|
174
|
+
`apps/local-cli` is a runnable IRC server built on the in-memory runtime.
|
|
175
|
+
It's the manual-test harness and the e2e fixture target — the same
|
|
176
|
+
`irc-core` reducers and `ConnectionActor` the cloud adapters use, just
|
|
177
|
+
wired to plain localhost listeners. By default it binds **two** listeners
|
|
178
|
+
that share one runtime, so a TCP client and a WS client can see each other
|
|
179
|
+
(cross-transport channel broadcast):
|
|
180
|
+
|
|
181
|
+
- a **WebSocket** listener (the serverless transport) on `--port`, and
|
|
182
|
+
- an **RFC-style TCP** listener on `--tcp-port` (default `<port> + 1`) so
|
|
183
|
+
real IRC clients (WeeChat, HexChat, irssi, …) can dial in directly.
|
|
163
184
|
|
|
164
185
|
First build the workspace (the CLI runs from compiled `dist/`):
|
|
165
186
|
|
|
@@ -178,37 +199,50 @@ pnpm --filter local-cli start --port 6667 --host 127.0.0.1
|
|
|
178
199
|
On startup it logs a JSON line like:
|
|
179
200
|
|
|
180
201
|
```json
|
|
181
|
-
{"ts":"...","level":"info","msg":"local-cli listening","
|
|
202
|
+
{"ts":"...","level":"info","msg":"local-cli listening","wsUrl":"ws://127.0.0.1:6667/","port":6667,"tcpPort":6668,"hostname":"127.0.0.1"}
|
|
182
203
|
```
|
|
183
204
|
|
|
184
205
|
### CLI flags
|
|
185
206
|
|
|
186
|
-
| Flag
|
|
187
|
-
|
|
188
|
-
| `--port <n>`
|
|
189
|
-
| `--host <h>`
|
|
190
|
-
|
|
|
207
|
+
| Flag | Default | Description |
|
|
208
|
+
|--------------------|---------------|--------------------------------------------------------|
|
|
209
|
+
| `--port <n>` | `6667` | WebSocket port to bind. Use `0` for an ephemeral. |
|
|
210
|
+
| `--host <h>` | `127.0.0.1` | Hostname / interface to bind. |
|
|
211
|
+
| `--tcp-port <n>` | `<port> + 1` | TCP port for RFC-style IRC clients. |
|
|
212
|
+
| `--no-tcp` | | Disable the TCP listener (WebSocket only). |
|
|
213
|
+
| `--motd-file <p>` | built-in | Read MOTD lines from this file (one per line). |
|
|
214
|
+
| `-h, --help` | | Show help and exit. |
|
|
191
215
|
|
|
192
216
|
Pass `--host 0.0.0.0` to expose the server on all interfaces. `SIGINT` /
|
|
193
217
|
`SIGTERM` perform a graceful shutdown (closes active sockets, then exits).
|
|
194
218
|
|
|
195
219
|
### Connecting
|
|
196
220
|
|
|
197
|
-
The
|
|
198
|
-
WebSocket-capable IRC client
|
|
199
|
-
|
|
200
|
-
|
|
221
|
+
The WebSocket listener speaks WebSocket text frames (one IRC message per
|
|
222
|
+
frame) — point any WebSocket-capable IRC client or script at
|
|
223
|
+
`ws://127.0.0.1:6667/`. The TCP listener speaks plain RFC IRC over the
|
|
224
|
+
wire, so stock clients connect directly, e.g. in WeeChat:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
/connect 127.0.0.1/6668
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
No `tcp-ws-forwarder` is needed for local development — both transports
|
|
231
|
+
are built in. The MOTD is the built-in default unless `--motd-file` is
|
|
232
|
+
given.
|
|
201
233
|
|
|
202
234
|
---
|
|
203
235
|
|
|
204
|
-
## Connecting a TCP IRC client (TCP→WS forwarder)
|
|
236
|
+
## Connecting a TCP IRC client to a deployed stack (TCP→WS forwarder)
|
|
205
237
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
238
|
+
The local CLI ships a built-in TCP listener, so no bridge is needed for local
|
|
239
|
+
development. Deployed stacks (Cloudflare Workers and AWS API Gateway) are
|
|
240
|
+
WebSocket-only, so a stock TCP IRC client (WeeChat, HexChat, irssi, …) cannot
|
|
241
|
+
dial them directly. `tools/tcp-ws-forwarder` is a local bridge for that case:
|
|
242
|
+
it listens on a TCP port and, for each connection, opens one WebSocket to a
|
|
243
|
+
`ws://` / `wss://` target and pumps IRC lines both ways — reassembling the TCP
|
|
244
|
+
byte stream into one message per WS frame outbound, and normalizing inbound
|
|
245
|
+
frames back to canonical CRLF.
|
|
212
246
|
|
|
213
247
|
Build the workspace, then start the forwarder (from the repo root):
|
|
214
248
|
|
|
@@ -255,14 +289,15 @@ This project follows strict TDD (Red → Green → Refactor) — every reducer i
|
|
|
255
289
|
landed test-first. See the project-level `CLAUDE.md` / `AGENTS.md` for the
|
|
256
290
|
full rules.
|
|
257
291
|
|
|
258
|
-
| Layer
|
|
259
|
-
|
|
260
|
-
| Parser/serializer| vitest + fast-check
|
|
261
|
-
| Reducers (core)
|
|
262
|
-
|
|
|
263
|
-
|
|
|
264
|
-
|
|
|
265
|
-
|
|
|
292
|
+
| Layer | Tooling | What it asserts |
|
|
293
|
+
|--------------------|---------------------------------------------|---------------------------------------|
|
|
294
|
+
| Parser/serializer | vitest + fast-check | Grammar correctness, round-trip |
|
|
295
|
+
| Reducers (core) | vitest pure unit tests | `(state,msg) → {state,effects}` exact |
|
|
296
|
+
| Mutation (core) | Stryker spot-check (≥80% score gate) | Reducer test quality, surviving mutants |
|
|
297
|
+
| Runtime contract | vitest parametrized via `irc-test-support` | Same scenarios pass in-memory+CF+AWS |
|
|
298
|
+
| CF adapter | `vitest-pool-workers` (real workerd) | DO behavior, alarms, stub fanout |
|
|
299
|
+
| AWS adapter | vitest + testcontainers (DynamoDB Local) | DynamoDB schema, transactions, fanout |
|
|
300
|
+
| E2E | `scripts/smoke.mjs` against deployed stack | CONNECT→REGISTER→JOIN→PRIVMSG→QUIT |
|
|
266
301
|
|
|
267
302
|
**Determinism:** reducers never touch real timers or randomness. A `Clock`
|
|
268
303
|
port and an `IdFactory` port are injected so tests are fully deterministic.
|
|
@@ -274,20 +309,22 @@ port and an `IdFactory` port are injected so tests are fully deterministic.
|
|
|
274
309
|
A v1 ships when `irc-core` is at 100% coverage, the parametrized contract
|
|
275
310
|
suite passes against every runtime, both adapters are deployed to staging,
|
|
276
311
|
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`.
|
|
312
|
+
cleanly. Per-release manifests live in `CHANGELOG.md`.
|
|
280
313
|
|
|
281
314
|
- **Phase 0** — Foundation (monorepo, turbo, vitest, biome, CI). ✅
|
|
282
|
-
- **Phase 1** — Pure protocol engine: reducers, IRCv3 caps, isupport
|
|
283
|
-
|
|
284
|
-
|
|
315
|
+
- **Phase 1** — Pure protocol engine: reducers, IRCv3 caps, isupport,
|
|
316
|
+
SASL (`PLAIN`), token-bucket flood control, chat history. ✅ (most in
|
|
317
|
+
**v0.1.0**; SASL/flood/chathistory landed in **v0.2.0**).
|
|
285
318
|
- **Phase 2** — `IrcRuntime` port, in-memory runtime, `ConnectionActor`,
|
|
286
319
|
local CLI, parametrized contract suite. ✅ landed in **v0.1.0**.
|
|
287
320
|
- **Phase 3** — Cloudflare adapter (ConnectionDO / ChannelDO / RegistryDO).
|
|
288
|
-
✅ landed in **v0.1.0** (staging
|
|
321
|
+
✅ landed in **v0.1.0** (staging auto-deploy via CI; prod deploy is manual).
|
|
289
322
|
- **Phase 4** — AWS adapter (APIGW WS + Lambda + DynamoDB + CDK).
|
|
323
|
+
✅ landed in **v0.2.0** (staging auto-deploy via CI; prod deploy is manual).
|
|
290
324
|
- **Phase 5** — Observability, security hardening, config, CI gates.
|
|
325
|
+
✅ landed (logger port, server-password, cloaking, admission limits,
|
|
326
|
+
coverage + mutation gates, OPER credential auth, and the Cloudflare
|
|
327
|
+
config loader all shipped across v0.2.0–v0.3.0).
|
|
291
328
|
- **Phase 6** — Load testing, client compatibility sweep, ADRs.
|
|
292
329
|
|
|
293
330
|
---
|
|
@@ -296,28 +333,34 @@ manifests live in `CHANGELOG.md`; the release process is documented in
|
|
|
296
333
|
|
|
297
334
|
**Core (RFC 1459/2812 subset):** registration (`NICK`/`USER`/`CAP`/`PASS`),
|
|
298
335
|
`PING`/`PONG`, `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`, `MODE` (user +
|
|
299
|
-
channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`, `MOTD
|
|
336
|
+
channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`, `MOTD`,
|
|
337
|
+
`AWAY`, `OPER` (credential auth → `o` user mode).
|
|
300
338
|
|
|
301
339
|
**Channel modes:** `o v b i k l t n m s p`.
|
|
302
340
|
**User modes:** `i`, `o` (local only), `w`, `s`.
|
|
303
341
|
|
|
304
|
-
**IRCv3 extensions (negotiated via `CAP`):** `message-tags
|
|
305
|
-
`account-tag`, `echo-message`, `batch`,
|
|
306
|
-
`
|
|
342
|
+
**IRCv3 extensions (negotiated via `CAP`):** `message-tags` (incl. the
|
|
343
|
+
`TAGMSG` command), `server-time`, `account-tag`, `echo-message`, `batch`,
|
|
344
|
+
`sasl` (`PLAIN` implemented, `EXTERNAL` reserved pending mTLS),
|
|
345
|
+
`multi-prefix`, `away-notify`, `chghost`, `invite-notify`, `extended-join`,
|
|
346
|
+
`draft/chathistory`, `safelist`. Case-insensitive nick/channel comparison
|
|
347
|
+
uses **RFC 1459** case-mapping (advertised via `005 CASEMAPPING=rfc1459`).
|
|
307
348
|
|
|
308
|
-
**Transport:**
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
349
|
+
**Transport:** deployed stacks (Cloudflare Workers, AWS API Gateway) speak
|
|
350
|
+
WebSocket text frames only (one IRC message per frame, with tolerance for
|
|
351
|
+
`\r\n`-joined frames). The local CLI additionally binds a plain RFC TCP
|
|
352
|
+
listener for direct IRC-client access. Stock TCP IRC clients reach a deployed
|
|
353
|
+
WebSocket endpoint through the local
|
|
354
|
+
[`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-to-a-deployed-stack-tcpws-forwarder).
|
|
312
355
|
|
|
313
356
|
---
|
|
314
357
|
|
|
315
358
|
## Further reading
|
|
316
359
|
|
|
317
|
-
- `
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
- `
|
|
322
|
-
|
|
323
|
-
|
|
360
|
+
- `CHANGELOG.md` — per-release manifests (Keep a Changelog format), including
|
|
361
|
+
the v0.2.0 work (AWS adapter, SASL, flood control, security hardening, CI
|
|
362
|
+
gates) and v0.3.0 (OPER, TAGMSG, RFC 1459 case-mapping, real MOTD,
|
|
363
|
+
ChannelRegistryDO, SASL account persistence).
|
|
364
|
+
- `README.md` in each `packages/*` and `apps/*` — per-package notes (e.g.
|
|
365
|
+
`packages/aws-adapter/README.md` for the DynamoDB-Local test setup,
|
|
366
|
+
`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
|
+
}
|