serverless-ircd 0.1.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.
Files changed (165) hide show
  1. package/.github/workflows/ci.yml +47 -0
  2. package/.github/workflows/deploy-cf.yml +97 -0
  3. package/LICENSE +29 -0
  4. package/README.md +323 -0
  5. package/apps/cf-worker/package.json +37 -0
  6. package/apps/cf-worker/scripts/smoke.mjs +175 -0
  7. package/apps/cf-worker/src/worker.ts +59 -0
  8. package/apps/cf-worker/tests/smoke.test.ts +150 -0
  9. package/apps/cf-worker/tsconfig.build.json +17 -0
  10. package/apps/cf-worker/tsconfig.test.json +18 -0
  11. package/apps/cf-worker/vitest.config.ts +31 -0
  12. package/apps/cf-worker/wrangler.test.toml +33 -0
  13. package/apps/cf-worker/wrangler.toml +98 -0
  14. package/apps/local-cli/package.json +35 -0
  15. package/apps/local-cli/src/line-scanner.ts +60 -0
  16. package/apps/local-cli/src/main.ts +145 -0
  17. package/apps/local-cli/src/motd-file.ts +45 -0
  18. package/apps/local-cli/src/server.ts +409 -0
  19. package/apps/local-cli/tests/e2e.test.ts +346 -0
  20. package/apps/local-cli/tests/id-factory.test.ts +25 -0
  21. package/apps/local-cli/tests/line-scanner.test.ts +81 -0
  22. package/apps/local-cli/tests/motd-file.test.ts +71 -0
  23. package/apps/local-cli/tests/tcp.test.ts +358 -0
  24. package/apps/local-cli/tsconfig.build.json +17 -0
  25. package/apps/local-cli/tsconfig.test.json +15 -0
  26. package/apps/local-cli/vitest.config.ts +24 -0
  27. package/biome.json +52 -0
  28. package/package.json +35 -0
  29. package/packages/cf-adapter/package.json +38 -0
  30. package/packages/cf-adapter/src/cf-runtime.ts +308 -0
  31. package/packages/cf-adapter/src/channel-do.ts +374 -0
  32. package/packages/cf-adapter/src/connection-do.ts +542 -0
  33. package/packages/cf-adapter/src/env.ts +67 -0
  34. package/packages/cf-adapter/src/index.ts +38 -0
  35. package/packages/cf-adapter/src/registry-do.ts +130 -0
  36. package/packages/cf-adapter/src/serialize.ts +142 -0
  37. package/packages/cf-adapter/src/sharding.ts +101 -0
  38. package/packages/cf-adapter/tests/cf-harness.ts +264 -0
  39. package/packages/cf-adapter/tests/cf-integration.test.ts +73 -0
  40. package/packages/cf-adapter/tests/cf-runtime.test.ts +527 -0
  41. package/packages/cf-adapter/tests/channel-do.test.ts +480 -0
  42. package/packages/cf-adapter/tests/connection-do.test.ts +329 -0
  43. package/packages/cf-adapter/tests/integration/harness.test.ts +229 -0
  44. package/packages/cf-adapter/tests/integration/harness.ts +102 -0
  45. package/packages/cf-adapter/tests/integration/in-memory-harness.ts +242 -0
  46. package/packages/cf-adapter/tests/integration/index.ts +17 -0
  47. package/packages/cf-adapter/tests/integration/scenarios.test.ts +20 -0
  48. package/packages/cf-adapter/tests/integration/scenarios.ts +495 -0
  49. package/packages/cf-adapter/tests/registry-do.test.ts +335 -0
  50. package/packages/cf-adapter/tests/sharding.test.ts +100 -0
  51. package/packages/cf-adapter/tests/worker/main.ts +33 -0
  52. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +50 -0
  53. package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +57 -0
  54. package/packages/cf-adapter/tsconfig.build.json +16 -0
  55. package/packages/cf-adapter/tsconfig.test.json +18 -0
  56. package/packages/cf-adapter/vitest.config.ts +32 -0
  57. package/packages/cf-adapter/wrangler.test.toml +50 -0
  58. package/packages/in-memory-runtime/package.json +29 -0
  59. package/packages/in-memory-runtime/src/in-memory-runtime.ts +245 -0
  60. package/packages/in-memory-runtime/src/index.ts +9 -0
  61. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +502 -0
  62. package/packages/in-memory-runtime/tsconfig.build.json +16 -0
  63. package/packages/in-memory-runtime/tsconfig.test.json +14 -0
  64. package/packages/in-memory-runtime/vitest.config.ts +21 -0
  65. package/packages/irc-core/package.json +29 -0
  66. package/packages/irc-core/src/caps/capabilities.ts +96 -0
  67. package/packages/irc-core/src/caps/index.ts +1 -0
  68. package/packages/irc-core/src/commands/away.ts +82 -0
  69. package/packages/irc-core/src/commands/cap.ts +257 -0
  70. package/packages/irc-core/src/commands/chghost.ts +30 -0
  71. package/packages/irc-core/src/commands/index.ts +40 -0
  72. package/packages/irc-core/src/commands/invite.ts +156 -0
  73. package/packages/irc-core/src/commands/isupport.ts +133 -0
  74. package/packages/irc-core/src/commands/join.ts +309 -0
  75. package/packages/irc-core/src/commands/kick.ts +162 -0
  76. package/packages/irc-core/src/commands/list.ts +126 -0
  77. package/packages/irc-core/src/commands/mode.ts +655 -0
  78. package/packages/irc-core/src/commands/motd.ts +146 -0
  79. package/packages/irc-core/src/commands/names.ts +164 -0
  80. package/packages/irc-core/src/commands/part.ts +118 -0
  81. package/packages/irc-core/src/commands/ping.ts +47 -0
  82. package/packages/irc-core/src/commands/privmsg.ts +256 -0
  83. package/packages/irc-core/src/commands/quit.ts +70 -0
  84. package/packages/irc-core/src/commands/registration.ts +251 -0
  85. package/packages/irc-core/src/commands/topic.ts +171 -0
  86. package/packages/irc-core/src/commands/who.ts +169 -0
  87. package/packages/irc-core/src/commands/whois.ts +165 -0
  88. package/packages/irc-core/src/effects.ts +184 -0
  89. package/packages/irc-core/src/index.ts +20 -0
  90. package/packages/irc-core/src/ports.ts +153 -0
  91. package/packages/irc-core/src/protocol/batch.ts +85 -0
  92. package/packages/irc-core/src/protocol/index.ts +18 -0
  93. package/packages/irc-core/src/protocol/messages.ts +36 -0
  94. package/packages/irc-core/src/protocol/numerics.ts +155 -0
  95. package/packages/irc-core/src/protocol/outbound.ts +145 -0
  96. package/packages/irc-core/src/protocol/parser.ts +175 -0
  97. package/packages/irc-core/src/protocol/serializer.ts +71 -0
  98. package/packages/irc-core/src/state/channel.ts +293 -0
  99. package/packages/irc-core/src/state/connection.ts +153 -0
  100. package/packages/irc-core/src/state/index.ts +25 -0
  101. package/packages/irc-core/src/state/registry.ts +22 -0
  102. package/packages/irc-core/src/types.ts +83 -0
  103. package/packages/irc-core/tests/batch.test.ts +79 -0
  104. package/packages/irc-core/tests/caps/capabilities.test.ts +148 -0
  105. package/packages/irc-core/tests/commands/away.test.ts +233 -0
  106. package/packages/irc-core/tests/commands/batch.test.ts +178 -0
  107. package/packages/irc-core/tests/commands/cap.test.ts +499 -0
  108. package/packages/irc-core/tests/commands/chghost.test.ts +186 -0
  109. package/packages/irc-core/tests/commands/echo-message.test.ts +212 -0
  110. package/packages/irc-core/tests/commands/extended-join.test.ts +147 -0
  111. package/packages/irc-core/tests/commands/invite-notify.test.ts +160 -0
  112. package/packages/irc-core/tests/commands/invite.test.ts +321 -0
  113. package/packages/irc-core/tests/commands/isupport.test.ts +209 -0
  114. package/packages/irc-core/tests/commands/join.test.ts +687 -0
  115. package/packages/irc-core/tests/commands/kick.test.ts +316 -0
  116. package/packages/irc-core/tests/commands/list.test.ts +452 -0
  117. package/packages/irc-core/tests/commands/mode.test.ts +1048 -0
  118. package/packages/irc-core/tests/commands/motd.test.ts +322 -0
  119. package/packages/irc-core/tests/commands/names.test.ts +342 -0
  120. package/packages/irc-core/tests/commands/part.test.ts +265 -0
  121. package/packages/irc-core/tests/commands/ping.test.ts +144 -0
  122. package/packages/irc-core/tests/commands/privmsg.test.ts +665 -0
  123. package/packages/irc-core/tests/commands/quit.test.ts +220 -0
  124. package/packages/irc-core/tests/commands/registration.test.ts +599 -0
  125. package/packages/irc-core/tests/commands/topic.test.ts +337 -0
  126. package/packages/irc-core/tests/commands/who.test.ts +441 -0
  127. package/packages/irc-core/tests/commands/whois.test.ts +422 -0
  128. package/packages/irc-core/tests/effects.test.ts +147 -0
  129. package/packages/irc-core/tests/message-tags.test.ts +182 -0
  130. package/packages/irc-core/tests/numerics.test.ts +98 -0
  131. package/packages/irc-core/tests/outbound.test.ts +232 -0
  132. package/packages/irc-core/tests/parser.test.ts +162 -0
  133. package/packages/irc-core/tests/ports.test.ts +101 -0
  134. package/packages/irc-core/tests/serializer.test.ts +164 -0
  135. package/packages/irc-core/tests/state/channel.test.ts +351 -0
  136. package/packages/irc-core/tests/state/connection.test.ts +122 -0
  137. package/packages/irc-core/tests/state/registry.test.ts +20 -0
  138. package/packages/irc-core/tests/types.test.ts +127 -0
  139. package/packages/irc-core/tsconfig.build.json +12 -0
  140. package/packages/irc-core/tsconfig.test.json +10 -0
  141. package/packages/irc-core/vitest.config.ts +21 -0
  142. package/packages/irc-server/package.json +28 -0
  143. package/packages/irc-server/src/actor.ts +449 -0
  144. package/packages/irc-server/src/dispatch.ts +120 -0
  145. package/packages/irc-server/src/index.ts +11 -0
  146. package/packages/irc-server/src/runtime.ts +61 -0
  147. package/packages/irc-server/tests/actor.test.ts +816 -0
  148. package/packages/irc-server/tests/dispatch.test.ts +512 -0
  149. package/packages/irc-server/tests/runtime.test.ts +52 -0
  150. package/packages/irc-server/tsconfig.build.json +13 -0
  151. package/packages/irc-server/tsconfig.test.json +11 -0
  152. package/packages/irc-server/vitest.config.ts +21 -0
  153. package/pnpm-workspace.yaml +4 -0
  154. package/tools/tcp-ws-forwarder/package.json +32 -0
  155. package/tools/tcp-ws-forwarder/src/forwarder.ts +244 -0
  156. package/tools/tcp-ws-forwarder/src/line-scanner.ts +55 -0
  157. package/tools/tcp-ws-forwarder/src/main.ts +114 -0
  158. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +618 -0
  159. package/tools/tcp-ws-forwarder/tests/framing.test.ts +51 -0
  160. package/tools/tcp-ws-forwarder/tests/line-scanner.test.ts +75 -0
  161. package/tools/tcp-ws-forwarder/tsconfig.build.json +12 -0
  162. package/tools/tcp-ws-forwarder/tsconfig.test.json +10 -0
  163. package/tools/tcp-ws-forwarder/vitest.config.ts +27 -0
  164. package/tsconfig.base.json +25 -0
  165. package/turbo.json +29 -0
@@ -0,0 +1,47 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ verify:
11
+ name: typecheck / lint / test / coverage
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Setup pnpm
18
+ uses: pnpm/action-setup@v4
19
+ with:
20
+ version: 9
21
+
22
+ - name: Setup Node
23
+ uses: actions/setup-node@v4
24
+ with:
25
+ node-version: 20
26
+ cache: pnpm
27
+
28
+ - name: Install dependencies
29
+ run: pnpm install --frozen-lockfile
30
+
31
+ - name: Lint
32
+ run: pnpm lint
33
+
34
+ - name: Typecheck
35
+ run: pnpm typecheck
36
+
37
+ - name: Test with coverage
38
+ run: pnpm coverage
39
+
40
+ - name: Upload coverage
41
+ if: always()
42
+ uses: actions/upload-artifact@v4
43
+ with:
44
+ name: coverage
45
+ path: |
46
+ packages/*/coverage
47
+ apps/*/coverage
@@ -0,0 +1,97 @@
1
+ name: deploy-cf
2
+
3
+ # Cloudflare Workers deploy pipeline.
4
+ #
5
+ # On every push to `main`:
6
+ # 1. Install deps (frozen lockfile).
7
+ # 2. Build the cf-adapter + irc-core/irc-server so the Worker entry
8
+ # typechecks against real workspace packages.
9
+ # 3. `wrangler deploy --env staging` the `apps/cf-worker` worker to the
10
+ # `serverless-ircd-staging` Worker + DO namespace.
11
+ # 4. Replay the smoke e2e (`scripts/smoke.mjs`) against the deployed
12
+ # URL — CONNECT/REGISTER/JOIN/PRIVMSG/QUIT. Failure blocks the
13
+ # merge (the deploy job is required on `main`).
14
+ #
15
+ # Secrets (configure under repo Settings → Secrets and variables → Actions):
16
+ # CLOUDFLARE_API_TOKEN Workers API token with `Workers Scripts:Edit`
17
+ # and `Durable Objects:Edit` on the target
18
+ # account. Never committed.
19
+ # CLOUDFLARE_ACCOUNT_ID Optional; required if the token's account
20
+ # context is ambiguous.
21
+ #
22
+ # The smoke URL is derived from the staging Worker name
23
+ # (`serverless-ircd-staging`) plus the deployer's account subdomain.
24
+ # Override by setting `CF_SMOKE_URL` in the repo Variables store when
25
+ # custom routes are wired.
26
+
27
+ on:
28
+ push:
29
+ branches: [main]
30
+ workflow_dispatch:
31
+
32
+ # Prevent two deploys racing the same DO namespace.
33
+ concurrency:
34
+ group: cf-staging
35
+ cancel-in-progress: false
36
+
37
+ jobs:
38
+ deploy-staging:
39
+ name: wrangler deploy --env staging
40
+ runs-on: ubuntu-latest
41
+ timeout-minutes: 10
42
+ steps:
43
+ - name: Checkout
44
+ uses: actions/checkout@v4
45
+
46
+ - name: Setup pnpm
47
+ uses: pnpm/action-setup@v4
48
+ with:
49
+ version: 9
50
+
51
+ - name: Setup Node
52
+ uses: actions/setup-node@v4
53
+ with:
54
+ node-version: 20
55
+ cache: pnpm
56
+
57
+ - name: Install dependencies
58
+ run: pnpm install --frozen-lockfile
59
+
60
+ - name: Build workspace
61
+ run: pnpm build
62
+
63
+ - name: Typecheck
64
+ run: pnpm typecheck
65
+
66
+ - name: Test
67
+ run: pnpm test
68
+
69
+ - name: Deploy to staging
70
+ working-directory: apps/cf-worker
71
+ env:
72
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
73
+ CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
74
+ run: pnpm deploy:staging
75
+
76
+ - name: Resolve smoke URL
77
+ id: smoke-url
78
+ env:
79
+ CF_SMOKE_URL: ${{ vars.CF_SMOKE_URL }}
80
+ run: |
81
+ if [ -n "$CF_SMOKE_URL" ]; then
82
+ echo "url=$CF_SMOKE_URL" >> "$GITHUB_OUTPUT"
83
+ else
84
+ # Default convention: <worker-name>.<account-subdomain>.workers.dev
85
+ # The subdomain is per-account; if CF_SMOKE_URL isn't set we
86
+ # fall back to a workers.dev route that the deployer must
87
+ # configure once via the Cloudflare dashboard.
88
+ echo "::warning::CF_SMOKE_URL repo variable is not set; smoke step will be skipped. Set it to the staging Worker's wss:// URL."
89
+ echo "url=" >> "$GITHUB_OUTPUT"
90
+ fi
91
+
92
+ - name: Smoke e2e
93
+ if: steps.smoke-url.outputs.url != ''
94
+ working-directory: apps/cf-worker
95
+ env:
96
+ SMOKE_URL: ${{ steps.smoke-url.outputs.url }}
97
+ run: node scripts/smoke.mjs --url "$SMOKE_URL"
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, fowlmouth
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF NOT ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # ServerlessIRCd
2
+
3
+ A serverless IRC daemon where the IRC protocol logic lives in a pure,
4
+ platform-agnostic core, and two thin adapters run it on **Cloudflare Workers**
5
+ (Durable Objects) or **AWS** (API Gateway WebSockets + Lambda + DynamoDB).
6
+
7
+ One TypeScript codebase. Two serverless substrates. No S2S linking in v1.
8
+
9
+ > **Status:** **v0.1.0 (preview).** The pure protocol core, the
10
+ > `IrcRuntime` port + in-memory runtime, a runnable local CLI server,
11
+ > and the Cloudflare Workers adapter (deployed to staging) are all
12
+ > functional. SASL, flood control, the AWS adapter, server-password
13
+ > enforcement, and the CI coverage gate are still ahead. See
14
+ > `CHANGELOG.md` for the v0.1.0 manifest, `docs/release.md` for the
15
+ > release process, and `progress.md` / `tickets.md` for per-ticket
16
+ > status.
17
+
18
+ ---
19
+
20
+ ## Why
21
+
22
+ IRC servers have historically been long-running stateful processes. This
23
+ project explores what happens when the protocol brain is a set of **pure
24
+ reducers** and the only platform-specific code is the side-effect plumbing
25
+ (transports, registries, fanout). The payoff:
26
+
27
+ - Every command handler is a trivial unit test: arrange state, apply a message,
28
+ assert the new state and emitted effects.
29
+ - The same core runs unchanged on Cloudflare and AWS.
30
+ - Authoritative state lives in exactly one place per piece of data, so races
31
+ are structurally impossible rather than defended against.
32
+
33
+ ---
34
+
35
+ ## Architecture
36
+
37
+ Hexagonal / ports-and-adapters. The core reasons about IRC; adapters move bytes.
38
+
39
+ ```
40
+ ┌─────────────────────────────────────────────────────────────────┐
41
+ │ packages/irc-core (pure TS, no cloud deps) │
42
+ │ protocol/ parse · serialize · numerics · messages │
43
+ │ commands/ pure reducers: (state, msg) → { state, effects } │
44
+ │ state/ channel · connection · mode · registry shapes │
45
+ └─────────────────────────────────────────────────────────────────┘
46
+ ▲ implements IrcRuntime (port)
47
+
48
+ ┌─────────────────────────────────────────────────────────────────┐
49
+ │ packages/irc-server (orchestration, defines the port) │
50
+ │ IrcRuntime side-effect interface (transport + state IO) │
51
+ │ dispatch interprets Effect[] against a bound runtime │
52
+ └─────────────────────────────────────────────────────────────────┘
53
+ ▲ ▲
54
+ ┌────────────────────────────┐ ┌──────────────────────────────┐
55
+ │ adapters/cf │ │ adapters/aws │
56
+ │ ConnectionDO │ │ $connect/$disconnect/ │
57
+ │ ChannelDO │ │ $default Lambda handlers │
58
+ │ RegistryDO │ │ DynamoDB tables │
59
+ │ CfRuntime │ │ AwsRuntime │
60
+ └────────────────────────────┘ └──────────────────────────────┘
61
+ ```
62
+
63
+ A planned `packages/in-memory-runtime` provides a reference implementation of
64
+ `IrcRuntime` used by integration tests and a local CLI server.
65
+
66
+ ### The key idea: pure reducers + location-of-authority
67
+
68
+ Each command handler is a pure function:
69
+
70
+ ```ts
71
+ type Reducer<S> = (state: S, msg: IrcMessage, ctx: Ctx) => {
72
+ state: S;
73
+ effects: Effect[];
74
+ };
75
+ ```
76
+
77
+ Side effects are **values** (`Effect[]`), not performed in the handler. The
78
+ actor layer's `dispatch(effects, runtime)` interprets each effect against the
79
+ bound runtime.
80
+
81
+ Each reducer runs in whichever authority **owns** the state it mutates:
82
+
83
+ | Command family | Runs in authority | Primary state mutated |
84
+ |-----------------------------|-------------------------|------------------------------|
85
+ | Registration (NICK/USER) | Connection entity | Connection state |
86
+ | PING/PONG, QUIT, AWAY | Connection entity | Connection state |
87
+ | JOIN / PART / channel MODE | Channel entity | Roster, channel modes |
88
+ | PRIVMSG/NOTICE to channel | Channel entity | Fanout (reads roster) |
89
+ | Nick collision check | Registry entity | Nick → Connection map |
90
+ | NAMES / WHO / WHOIS | Reads snapshots | (no mutation) |
91
+
92
+ ---
93
+
94
+ ## Repository layout
95
+
96
+ ```
97
+ ServerlessIRCd/
98
+ ├── packages/
99
+ │ ├── irc-core/ pure protocol + reducers (the brain)
100
+ │ ├── irc-server/ orchestration, IrcRuntime port, dispatch
101
+ │ ├── in-memory-runtime/ reference runtime + local CLI server (planned)
102
+ │ ├── cf-adapter/ Durable Objects + CfRuntime (planned)
103
+ │ └── aws-adapter/ Lambda + DynamoDB + AwsRuntime (planned)
104
+ ├── apps/
105
+ │ ├── cf-worker/ worker entry, DO migrations, bindings (planned)
106
+ │ ├── aws-stack/ CDK app entry (planned)
107
+ │ └── local-cli/ runnable WS server using in-memory-runtime
108
+ ├── tools/
109
+ │ └── tcp-ws-forwarder/ local TCP↔ws/wss bridge for stock IRC clients
110
+ ├── docs/
111
+ │ └── architecture-aws.md
112
+ ├── pnpm-workspace.yaml turbo.json tsconfig.base.json
113
+ └── PLAN.md progress.md tickets.md
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Toolchain
119
+
120
+ | Concern | Choice |
121
+ |----------------|---------------------------------------------------|
122
+ | Runtime | Node ≥ 20, ES2022+ |
123
+ | Package mgr | pnpm workspaces |
124
+ | Build cache | turbo |
125
+ | Language | TypeScript (strict, `exactOptionalPropertyTypes`) |
126
+ | Test runner | vitest + `@vitest/coverage-v8` |
127
+ | Property tests | fast-check |
128
+ | Lint / format | Biome |
129
+
130
+ ---
131
+
132
+ ## Getting started
133
+
134
+ Requires Node ≥ 20 and pnpm 9.
135
+
136
+ ```bash
137
+ pnpm install # install workspace deps
138
+
139
+ pnpm build # build all packages (turbo)
140
+ pnpm typecheck # tsc --noEmit across the workspace
141
+ pnpm test # run all tests once
142
+ pnpm test:watch # vitest watch mode
143
+ pnpm coverage # tests + v8 coverage report
144
+
145
+ pnpm lint # biome check .
146
+ pnpm lint:fix # biome check --write .
147
+ pnpm format # biome format --write .
148
+
149
+ pnpm clean # remove dist/coverage/.turbo + node_modules
150
+ ```
151
+
152
+ Coverage reports are written to `packages/*/coverage/`. CI (`.github/workflows/ci.yml`)
153
+ runs lint, typecheck, and coverage on every push and pull request.
154
+
155
+ ---
156
+
157
+ ## Running the local server
158
+
159
+ `apps/local-cli` is a runnable WebSocket IRC server built on the in-memory
160
+ runtime. It's the manual-test harness and the e2e fixture target — the same
161
+ `irc-core` reducers and `ConnectionActor` the cloud adapters will use, just
162
+ wired to a plain `ws.WebSocketServer` on localhost.
163
+
164
+ First build the workspace (the CLI runs from compiled `dist/`):
165
+
166
+ ```bash
167
+ pnpm build
168
+ ```
169
+
170
+ Then start the server (from the repo root):
171
+
172
+ ```bash
173
+ pnpm --filter local-cli start
174
+ # or with explicit args:
175
+ pnpm --filter local-cli start --port 6667 --host 127.0.0.1
176
+ ```
177
+
178
+ On startup it logs a JSON line like:
179
+
180
+ ```json
181
+ {"ts":"...","level":"info","msg":"local-cli listening","url":"ws://127.0.0.1:6667/","port":6667,"hostname":"127.0.0.1"}
182
+ ```
183
+
184
+ ### CLI flags
185
+
186
+ | Flag | Default | Description |
187
+ |--------------|-------------|----------------------------------------------|
188
+ | `--port <n>` | `6667` | TCP port to bind. Use `0` for an ephemeral. |
189
+ | `--host <h>` | `127.0.0.1` | Hostname / interface to bind. |
190
+ | `-h, --help` | | Show help and exit. |
191
+
192
+ Pass `--host 0.0.0.0` to expose the server on all interfaces. `SIGINT` /
193
+ `SIGTERM` perform a graceful shutdown (closes active sockets, then exits).
194
+
195
+ ### Connecting
196
+
197
+ The server speaks WebSocket text frames (one IRC message per frame). Point any
198
+ WebSocket-capable IRC client (WeeChat with the `irc` protocol's WS relay, or a
199
+ small script) at `ws://127.0.0.1:6667/`. Server name and MOTD are fixed
200
+ defaults (`irc.example.com`) until the config-loader ticket lands.
201
+
202
+ ---
203
+
204
+ ## Connecting a TCP IRC client (TCP→WS forwarder)
205
+
206
+ v1 is WebSocket-only, so a stock TCP IRC client (WeeChat, HexChat, irssi, …)
207
+ cannot dial a deployed worker or the local WS server directly.
208
+ `tools/tcp-ws-forwarder` is a local bridge: it listens on a TCP port and, for
209
+ each connection, opens one WebSocket to a `ws://` / `wss://` target and pumps
210
+ IRC lines both ways — reassembling the TCP byte stream into one message per WS
211
+ frame outbound, and normalizing inbound frames back to canonical CRLF.
212
+
213
+ Build the workspace, then start the forwarder (from the repo root):
214
+
215
+ ```bash
216
+ pnpm build
217
+
218
+ # Bridge localhost:16667 → the local WS server started above:
219
+ pnpm --filter tcp-ws-forwarder start \
220
+ --target ws://127.0.0.1:6667/ --listen-port 16667
221
+
222
+ # Or bridge straight to a deployed stack:
223
+ pnpm --filter tcp-ws-forwarder start \
224
+ --target wss://irc.example.com/ --listen-port 16667
225
+ ```
226
+
227
+ On startup it logs a JSON line like:
228
+
229
+ ```json
230
+ {"ts":"...","level":"info","msg":"tcp-ws-forwarder listening","listen":"127.0.0.1:16667","target":"ws://127.0.0.1:6667/"}
231
+ ```
232
+
233
+ ### CLI flags
234
+
235
+ | Flag | Default | Description |
236
+ |---------------------|-------------|--------------------------------------------------|
237
+ | `--target <url>` | (required) | Upstream `ws://` or `wss://` URL to bridge to. |
238
+ | `--listen-port <n>` | `6667` | TCP port to listen on. Use `0` for an ephemeral. |
239
+ | `--listen-host <h>` | `127.0.0.1` | Interface to bind. |
240
+ | `-h, --help` | | Show help and exit. |
241
+
242
+ ### Connecting
243
+
244
+ Point any TCP IRC client at `127.0.0.1:<listen-port>` and it will appear as a
245
+ direct connection to the WebSocket endpoint. `SIGINT` / `SIGTERM` perform a
246
+ graceful shutdown (closes the listener and every live bridge). The forwarder
247
+ is transport-agnostic — it ships no `@serverless-ircd/*` dependency and works
248
+ against any line-oriented WebSocket endpoint.
249
+
250
+ ---
251
+
252
+ ## Testing strategy
253
+
254
+ This project follows strict TDD (Red → Green → Refactor) — every reducer is
255
+ landed test-first. See the project-level `CLAUDE.md` / `AGENTS.md` for the
256
+ full rules.
257
+
258
+ | Layer | Tooling | What it asserts |
259
+ |------------------|-----------------------------------------|---------------------------------------|
260
+ | Parser/serializer| vitest + fast-check | Grammar correctness, round-trip |
261
+ | Reducers (core) | vitest pure unit tests | `(state,msg) → {state,effects}` exact |
262
+ | Runtime contract | vitest parametrized over `IrcRuntime` | Same scenarios pass in-memory+CF+AWS |
263
+ | CF adapter | `vitest-pool-workers` (real workerd) | DO behavior, alarms, stub fanout |
264
+ | AWS adapter | vitest + dynamodb-local / localstack | DynamoDB schema, transactions, fanout |
265
+ | E2E | real WS clients in `tools/irc-client` | RFC-shaped flows vs deployed stack |
266
+
267
+ **Determinism:** reducers never touch real timers or randomness. A `Clock`
268
+ port and an `IdFactory` port are injected so tests are fully deterministic.
269
+
270
+ ---
271
+
272
+ ## Roadmap
273
+
274
+ A v1 ships when `irc-core` is at 100% coverage, the parametrized contract
275
+ suite passes against every runtime, both adapters are deployed to staging,
276
+ and ≥3 reference clients (WeeChat, HexChat, IRCCloud, TheLounge) connect
277
+ cleanly. Full per-ticket status lives in `progress.md`. Per-release
278
+ manifests live in `CHANGELOG.md`; the release process is documented in
279
+ `docs/release.md`.
280
+
281
+ - **Phase 0** — Foundation (monorepo, turbo, vitest, biome, CI). ✅
282
+ - **Phase 1** — Pure protocol engine: reducers, IRCv3 caps, isupport.
283
+ Most of this landed in **v0.1.0**; SASL and flood control are still
284
+ pending.
285
+ - **Phase 2** — `IrcRuntime` port, in-memory runtime, `ConnectionActor`,
286
+ local CLI, parametrized contract suite. ✅ landed in **v0.1.0**.
287
+ - **Phase 3** — Cloudflare adapter (ConnectionDO / ChannelDO / RegistryDO).
288
+ ✅ landed in **v0.1.0** (staging only; prod deploy is manual).
289
+ - **Phase 4** — AWS adapter (APIGW WS + Lambda + DynamoDB + CDK).
290
+ - **Phase 5** — Observability, security hardening, config, CI gates.
291
+ - **Phase 6** — Load testing, client compatibility sweep, ADRs.
292
+
293
+ ---
294
+
295
+ ## Protocol scope (v1)
296
+
297
+ **Core (RFC 1459/2812 subset):** registration (`NICK`/`USER`/`CAP`/`PASS`),
298
+ `PING`/`PONG`, `QUIT`, `JOIN`, `PART`, `PRIVMSG`, `NOTICE`, `MODE` (user +
299
+ channel), `TOPIC`, `KICK`, `INVITE`, `NAMES`, `LIST`, `WHO`, `WHOIS`, `MOTD`.
300
+
301
+ **Channel modes:** `o v b i k l t n m s p`.
302
+ **User modes:** `i`, `o` (local only), `w`, `s`.
303
+
304
+ **IRCv3 extensions (negotiated via `CAP`):** `message-tags`, `server-time`,
305
+ `account-tag`, `echo-message`, `batch`, `sasl` (`PLAIN`, `EXTERNAL` reserved),
306
+ `multi-prefix`, `away-notify`, `chghost`, `invite-notify`, `extended-join`.
307
+
308
+ **Transport:** WebSocket text frames only (one IRC message per frame, with
309
+ tolerance for `\r\n`-joined frames). No plaintext TCP in v1 — keeps the
310
+ serverless story clean. Stock TCP IRC clients reach the server through the
311
+ local [`tcp-ws-forwarder`](#connecting-a-tcp-irc-client-tcpws-forwarder).
312
+
313
+ ---
314
+
315
+ ## Further reading
316
+
317
+ - `PLAN.md` — full project plan, platform mappings, risks, decisions.
318
+ - `CHANGELOG.md` — per-release manifests (Keep a Changelog format).
319
+ - `docs/release.md` — release process runbook (versioning, tagging,
320
+ rollback).
321
+ - `progress.md` / `tickets.md` — what's done, what's next.
322
+ - `docs/deployment-cf.md` — Cloudflare deployment guide.
323
+ - `docs/architecture-aws.md` — AWS adapter design notes.
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@serverless-ircd/cf-worker",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Cloudflare Worker deploy glue: WebSocket edge entry point + DO bindings + wrangler pipeline",
6
+ "license": "BSD-3-Clause",
7
+ "type": "module",
8
+ "main": "./dist/worker.js",
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.build.json",
11
+ "typecheck": "tsc -p tsconfig.test.json --noEmit",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "coverage": "vitest run --coverage",
15
+ "dev": "wrangler dev",
16
+ "deploy:staging": "wrangler deploy --env staging",
17
+ "deploy:prod": "wrangler deploy",
18
+ "smoke:staging": "node scripts/smoke.mjs",
19
+ "clean": "rimraf dist coverage .tsbuildinfo .turbo"
20
+ },
21
+ "dependencies": {
22
+ "@serverless-ircd/cf-adapter": "workspace:*",
23
+ "@serverless-ircd/irc-core": "workspace:*",
24
+ "@serverless-ircd/irc-server": "workspace:*"
25
+ },
26
+ "devDependencies": {
27
+ "@cloudflare/vitest-pool-workers": "0.18.6",
28
+ "@cloudflare/workers-types": "^5.20260714.1",
29
+ "@types/ws": "^8.5.13",
30
+ "@vitest/coverage-v8": "^4.1.0",
31
+ "rimraf": "^6.0.0",
32
+ "typescript": "^5.6.0",
33
+ "vitest": "^4.1.0",
34
+ "wrangler": "4.112.0",
35
+ "ws": "^8.18.0"
36
+ }
37
+ }