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.
- package/.github/workflows/ci.yml +47 -0
- package/.github/workflows/deploy-cf.yml +97 -0
- package/LICENSE +29 -0
- package/README.md +323 -0
- package/apps/cf-worker/package.json +37 -0
- package/apps/cf-worker/scripts/smoke.mjs +175 -0
- package/apps/cf-worker/src/worker.ts +59 -0
- package/apps/cf-worker/tests/smoke.test.ts +150 -0
- package/apps/cf-worker/tsconfig.build.json +17 -0
- package/apps/cf-worker/tsconfig.test.json +18 -0
- package/apps/cf-worker/vitest.config.ts +31 -0
- package/apps/cf-worker/wrangler.test.toml +33 -0
- package/apps/cf-worker/wrangler.toml +98 -0
- package/apps/local-cli/package.json +35 -0
- package/apps/local-cli/src/line-scanner.ts +60 -0
- package/apps/local-cli/src/main.ts +145 -0
- package/apps/local-cli/src/motd-file.ts +45 -0
- package/apps/local-cli/src/server.ts +409 -0
- package/apps/local-cli/tests/e2e.test.ts +346 -0
- package/apps/local-cli/tests/id-factory.test.ts +25 -0
- package/apps/local-cli/tests/line-scanner.test.ts +81 -0
- package/apps/local-cli/tests/motd-file.test.ts +71 -0
- package/apps/local-cli/tests/tcp.test.ts +358 -0
- package/apps/local-cli/tsconfig.build.json +17 -0
- package/apps/local-cli/tsconfig.test.json +15 -0
- package/apps/local-cli/vitest.config.ts +24 -0
- package/biome.json +52 -0
- package/package.json +35 -0
- package/packages/cf-adapter/package.json +38 -0
- package/packages/cf-adapter/src/cf-runtime.ts +308 -0
- package/packages/cf-adapter/src/channel-do.ts +374 -0
- package/packages/cf-adapter/src/connection-do.ts +542 -0
- package/packages/cf-adapter/src/env.ts +67 -0
- package/packages/cf-adapter/src/index.ts +38 -0
- package/packages/cf-adapter/src/registry-do.ts +130 -0
- package/packages/cf-adapter/src/serialize.ts +142 -0
- package/packages/cf-adapter/src/sharding.ts +101 -0
- package/packages/cf-adapter/tests/cf-harness.ts +264 -0
- package/packages/cf-adapter/tests/cf-integration.test.ts +73 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +527 -0
- package/packages/cf-adapter/tests/channel-do.test.ts +480 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +329 -0
- package/packages/cf-adapter/tests/integration/harness.test.ts +229 -0
- package/packages/cf-adapter/tests/integration/harness.ts +102 -0
- package/packages/cf-adapter/tests/integration/in-memory-harness.ts +242 -0
- package/packages/cf-adapter/tests/integration/index.ts +17 -0
- package/packages/cf-adapter/tests/integration/scenarios.test.ts +20 -0
- package/packages/cf-adapter/tests/integration/scenarios.ts +495 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +335 -0
- package/packages/cf-adapter/tests/sharding.test.ts +100 -0
- package/packages/cf-adapter/tests/worker/main.ts +33 -0
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +50 -0
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +57 -0
- package/packages/cf-adapter/tsconfig.build.json +16 -0
- package/packages/cf-adapter/tsconfig.test.json +18 -0
- package/packages/cf-adapter/vitest.config.ts +32 -0
- package/packages/cf-adapter/wrangler.test.toml +50 -0
- package/packages/in-memory-runtime/package.json +29 -0
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +245 -0
- package/packages/in-memory-runtime/src/index.ts +9 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +502 -0
- package/packages/in-memory-runtime/tsconfig.build.json +16 -0
- package/packages/in-memory-runtime/tsconfig.test.json +14 -0
- package/packages/in-memory-runtime/vitest.config.ts +21 -0
- package/packages/irc-core/package.json +29 -0
- package/packages/irc-core/src/caps/capabilities.ts +96 -0
- package/packages/irc-core/src/caps/index.ts +1 -0
- package/packages/irc-core/src/commands/away.ts +82 -0
- package/packages/irc-core/src/commands/cap.ts +257 -0
- package/packages/irc-core/src/commands/chghost.ts +30 -0
- package/packages/irc-core/src/commands/index.ts +40 -0
- package/packages/irc-core/src/commands/invite.ts +156 -0
- package/packages/irc-core/src/commands/isupport.ts +133 -0
- package/packages/irc-core/src/commands/join.ts +309 -0
- package/packages/irc-core/src/commands/kick.ts +162 -0
- package/packages/irc-core/src/commands/list.ts +126 -0
- package/packages/irc-core/src/commands/mode.ts +655 -0
- package/packages/irc-core/src/commands/motd.ts +146 -0
- package/packages/irc-core/src/commands/names.ts +164 -0
- package/packages/irc-core/src/commands/part.ts +118 -0
- package/packages/irc-core/src/commands/ping.ts +47 -0
- package/packages/irc-core/src/commands/privmsg.ts +256 -0
- package/packages/irc-core/src/commands/quit.ts +70 -0
- package/packages/irc-core/src/commands/registration.ts +251 -0
- package/packages/irc-core/src/commands/topic.ts +171 -0
- package/packages/irc-core/src/commands/who.ts +169 -0
- package/packages/irc-core/src/commands/whois.ts +165 -0
- package/packages/irc-core/src/effects.ts +184 -0
- package/packages/irc-core/src/index.ts +20 -0
- package/packages/irc-core/src/ports.ts +153 -0
- package/packages/irc-core/src/protocol/batch.ts +85 -0
- package/packages/irc-core/src/protocol/index.ts +18 -0
- package/packages/irc-core/src/protocol/messages.ts +36 -0
- package/packages/irc-core/src/protocol/numerics.ts +155 -0
- package/packages/irc-core/src/protocol/outbound.ts +145 -0
- package/packages/irc-core/src/protocol/parser.ts +175 -0
- package/packages/irc-core/src/protocol/serializer.ts +71 -0
- package/packages/irc-core/src/state/channel.ts +293 -0
- package/packages/irc-core/src/state/connection.ts +153 -0
- package/packages/irc-core/src/state/index.ts +25 -0
- package/packages/irc-core/src/state/registry.ts +22 -0
- package/packages/irc-core/src/types.ts +83 -0
- package/packages/irc-core/tests/batch.test.ts +79 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +148 -0
- package/packages/irc-core/tests/commands/away.test.ts +233 -0
- package/packages/irc-core/tests/commands/batch.test.ts +178 -0
- package/packages/irc-core/tests/commands/cap.test.ts +499 -0
- package/packages/irc-core/tests/commands/chghost.test.ts +186 -0
- package/packages/irc-core/tests/commands/echo-message.test.ts +212 -0
- package/packages/irc-core/tests/commands/extended-join.test.ts +147 -0
- package/packages/irc-core/tests/commands/invite-notify.test.ts +160 -0
- package/packages/irc-core/tests/commands/invite.test.ts +321 -0
- package/packages/irc-core/tests/commands/isupport.test.ts +209 -0
- package/packages/irc-core/tests/commands/join.test.ts +687 -0
- package/packages/irc-core/tests/commands/kick.test.ts +316 -0
- package/packages/irc-core/tests/commands/list.test.ts +452 -0
- package/packages/irc-core/tests/commands/mode.test.ts +1048 -0
- package/packages/irc-core/tests/commands/motd.test.ts +322 -0
- package/packages/irc-core/tests/commands/names.test.ts +342 -0
- package/packages/irc-core/tests/commands/part.test.ts +265 -0
- package/packages/irc-core/tests/commands/ping.test.ts +144 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +665 -0
- package/packages/irc-core/tests/commands/quit.test.ts +220 -0
- package/packages/irc-core/tests/commands/registration.test.ts +599 -0
- package/packages/irc-core/tests/commands/topic.test.ts +337 -0
- package/packages/irc-core/tests/commands/who.test.ts +441 -0
- package/packages/irc-core/tests/commands/whois.test.ts +422 -0
- package/packages/irc-core/tests/effects.test.ts +147 -0
- package/packages/irc-core/tests/message-tags.test.ts +182 -0
- package/packages/irc-core/tests/numerics.test.ts +98 -0
- package/packages/irc-core/tests/outbound.test.ts +232 -0
- package/packages/irc-core/tests/parser.test.ts +162 -0
- package/packages/irc-core/tests/ports.test.ts +101 -0
- package/packages/irc-core/tests/serializer.test.ts +164 -0
- package/packages/irc-core/tests/state/channel.test.ts +351 -0
- package/packages/irc-core/tests/state/connection.test.ts +122 -0
- package/packages/irc-core/tests/state/registry.test.ts +20 -0
- package/packages/irc-core/tests/types.test.ts +127 -0
- package/packages/irc-core/tsconfig.build.json +12 -0
- package/packages/irc-core/tsconfig.test.json +10 -0
- package/packages/irc-core/vitest.config.ts +21 -0
- package/packages/irc-server/package.json +28 -0
- package/packages/irc-server/src/actor.ts +449 -0
- package/packages/irc-server/src/dispatch.ts +120 -0
- package/packages/irc-server/src/index.ts +11 -0
- package/packages/irc-server/src/runtime.ts +61 -0
- package/packages/irc-server/tests/actor.test.ts +816 -0
- package/packages/irc-server/tests/dispatch.test.ts +512 -0
- package/packages/irc-server/tests/runtime.test.ts +52 -0
- package/packages/irc-server/tsconfig.build.json +13 -0
- package/packages/irc-server/tsconfig.test.json +11 -0
- package/packages/irc-server/vitest.config.ts +21 -0
- package/pnpm-workspace.yaml +4 -0
- package/tools/tcp-ws-forwarder/package.json +32 -0
- package/tools/tcp-ws-forwarder/src/forwarder.ts +244 -0
- package/tools/tcp-ws-forwarder/src/line-scanner.ts +55 -0
- package/tools/tcp-ws-forwarder/src/main.ts +114 -0
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +618 -0
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +51 -0
- package/tools/tcp-ws-forwarder/tests/line-scanner.test.ts +75 -0
- package/tools/tcp-ws-forwarder/tsconfig.build.json +12 -0
- package/tools/tcp-ws-forwarder/tsconfig.test.json +10 -0
- package/tools/tcp-ws-forwarder/vitest.config.ts +27 -0
- package/tsconfig.base.json +25 -0
- 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
|
+
}
|