serverless-ircd 0.2.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/CHANGELOG.md +84 -0
- package/README.md +28 -19
- 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 +41 -17
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +1 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +28 -2
- package/apps/local-cli/tests/e2e.test.ts +52 -0
- 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 +6 -4
- package/docs/AWS-Deployment.md +86 -7
- package/docs/Cloudflare-Deployment-Guide.md +5 -5
- package/docs/Home.md +11 -1
- package/docs/Release-Process.md +9 -6
- package/package.json +14 -15
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/aws-runtime.ts +118 -30
- package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
- package/packages/aws-adapter/src/config-loader.ts +30 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +156 -0
- package/packages/aws-adapter/src/handlers/default.ts +8 -0
- package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
- package/packages/aws-adapter/src/handlers/index.ts +51 -6
- package/packages/aws-adapter/src/index.ts +7 -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/config-loader.test.ts +55 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +336 -0
- package/packages/aws-adapter/tests/handlers.test.ts +46 -0
- package/packages/cf-adapter/package.json +1 -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 +29 -0
- package/packages/cf-adapter/src/connection-do.ts +85 -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-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +40 -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 +14 -28
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
- package/packages/irc-core/package.json +3 -2
- package/packages/irc-core/src/case-fold.ts +64 -0
- package/packages/irc-core/src/commands/index.ts +2 -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 +4 -3
- 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/registration.ts +8 -7
- package/packages/irc-core/src/commands/tagmsg.ts +205 -0
- package/packages/irc-core/src/config.ts +26 -3
- package/packages/irc-core/src/index.ts +2 -0
- package/packages/irc-core/src/ports.ts +68 -4
- package/packages/irc-core/src/types.ts +22 -0
- package/packages/irc-core/tests/account-store.test.ts +88 -0
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- 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/registration.test.ts +106 -14
- 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 +75 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +24 -0
- package/packages/irc-server/src/routing.ts +9 -0
- package/packages/irc-server/tests/actor.test.ts +220 -1
- package/packages/irc-server/tests/routing.test.ts +3 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +5 -4
- package/pnpm-workspace.yaml +1 -0
- package/tools/seed-aws-accounts.ts +80 -0
- package/AGENTS.md +0 -5
- package/MOTD.txt +0 -3
- package/PLAN-FIXES.md +0 -358
- package/PLAN.md +0 -420
- package/dashboards/cloudwatch-irc.json +0 -139
- package/progress.md +0 -107
- package/tickets.md +0 -2485
- package/webircgateway/LICENSE +0 -201
- package/webircgateway/Makefile +0 -44
- package/webircgateway/README.md +0 -134
- package/webircgateway/config.conf.example +0 -135
- package/webircgateway/go.mod +0 -16
- package/webircgateway/go.sum +0 -89
- package/webircgateway/main.go +0 -118
- package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
- package/webircgateway/pkg/identd/identd.go +0 -86
- package/webircgateway/pkg/identd/rpcclient.go +0 -59
- package/webircgateway/pkg/irc/isupport.go +0 -56
- package/webircgateway/pkg/irc/message.go +0 -217
- package/webircgateway/pkg/irc/state.go +0 -79
- package/webircgateway/pkg/proxy/proxy.go +0 -129
- package/webircgateway/pkg/proxy/server.go +0 -237
- package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
- package/webircgateway/pkg/webircgateway/client.go +0 -741
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
- package/webircgateway/pkg/webircgateway/config.go +0 -385
- package/webircgateway/pkg/webircgateway/gateway.go +0 -278
- package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
- package/webircgateway/pkg/webircgateway/hooks.go +0 -152
- package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
- package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
- package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
- package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
- package/webircgateway/pkg/webircgateway/utils.go +0 -147
- package/webircgateway/plugins/example/plugin.go +0 -11
- package/webircgateway/plugins/stats/plugin.go +0 -52
- package/webircgateway/staticcheck.conf +0 -1
- package/webircgateway/webircgateway.svg +0 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# ADR-003: Cloudflare Durable Object sharding strategy
|
|
2
|
+
|
|
3
|
+
**Date:** 2025-02-01 (Phase 3)
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Supersedes:** —
|
|
6
|
+
**Superseded by:** —
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Cloudflare Durable Objects (DOs) are single-threaded per-instance: all
|
|
11
|
+
requests to a given DO instance are serialized. This is ideal for enforcing
|
|
12
|
+
uniqueness invariants (two racing requests land on the same DO, one after the
|
|
13
|
+
other), but a single DO instance is a throughput ceiling. ServerlessIRCd needs
|
|
14
|
+
three DO classes:
|
|
15
|
+
|
|
16
|
+
1. **ConnectionDO** — one per WebSocket connection. Keyed by `connectionId`.
|
|
17
|
+
No sharding needed: each connection is its own instance.
|
|
18
|
+
2. **ChannelDO** — one per channel. Keyed by lowercased channel name.
|
|
19
|
+
No sharding needed: one DO per channel is the natural unit; a hot channel
|
|
20
|
+
(1k+ members) is bounded by DO fanout latency, not by DO count.
|
|
21
|
+
3. **RegistryDO** — holds the global nick → connectionId map. If this were a
|
|
22
|
+
single DO, every nick registration on the entire server would serialize
|
|
23
|
+
through one instance. That is an unacceptable bottleneck for any non-trivial
|
|
24
|
+
deployment.
|
|
25
|
+
|
|
26
|
+
The open question (PLAN §9): *"Channel DO sharding granularity (one DO per
|
|
27
|
+
channel vs. bucketed)?"* and *"Registry DO: how to shard?"*
|
|
28
|
+
|
|
29
|
+
## Decision
|
|
30
|
+
|
|
31
|
+
**ConnectionDO:** one instance per connection (`idFromName(connectionId)`).
|
|
32
|
+
No sharding.
|
|
33
|
+
|
|
34
|
+
**ChannelDO:** one instance per channel
|
|
35
|
+
(`idFromName(lowercasedChannelName)`). No bucketing. If a hot channel exceeds
|
|
36
|
+
DO throughput limits, the mitigation is fanout batching (one `stub()` call
|
|
37
|
+
carrying multiple lines) rather than splitting the channel across DOs. A
|
|
38
|
+
single channel's roster and modes live in one DO so that all mutations are
|
|
39
|
+
serialized correctly.
|
|
40
|
+
|
|
41
|
+
**RegistryDO:** sharded by `hash(lowercased(nick)) % N`, where `N` defaults
|
|
42
|
+
to **32** (`DEFAULT_REGISTRY_SHARDS`). The hash function is FNV-1a 32-bit,
|
|
43
|
+
chosen for its uniform distribution, deterministic output (no randomness),
|
|
44
|
+
and trivial implementation.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
shard = fnv1a32(nick.toLowerCase()) % N
|
|
48
|
+
key = `s${shard}` // e.g. "s7", "s31"
|
|
49
|
+
stub = env.REGISTRY_DO.get(env.REGISTRY_DO.idFromName(key))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Two clients racing on the same nick always land on the same shard (because
|
|
53
|
+
lowercasing collapses case variants), so the single-shard uniqueness check
|
|
54
|
+
covers all case variants. Different nicks spread across shards, so
|
|
55
|
+
registration throughput scales with the fleet size.
|
|
56
|
+
|
|
57
|
+
Cross-shard nick changes (old nick and new nick hash to different shards)
|
|
58
|
+
use a two-phase reserve-new-then-release-old. The new-nick shard wins the
|
|
59
|
+
race; the old-nick shard is released only after the new reservation succeeds.
|
|
60
|
+
|
|
61
|
+
`N` is configurable via the `REGISTRY_SHARDS` environment variable. Changing
|
|
62
|
+
`N` in production requires a migration (re-sharding every existing nick entry
|
|
63
|
+
to its new shard).
|
|
64
|
+
|
|
65
|
+
## Consequences
|
|
66
|
+
|
|
67
|
+
**Positive:**
|
|
68
|
+
- Nick uniqueness is race-free within the DO model. No conditional writes,
|
|
69
|
+
no optimistic concurrency — the DO's serial execution is the lock.
|
|
70
|
+
- Registration throughput scales horizontally: 32 shards handle 32× the
|
|
71
|
+
single-DO throughput for distinct nicks.
|
|
72
|
+
- One DO per channel keeps channel semantics simple: JOIN/PART/MODE/KICK all
|
|
73
|
+
serialize through the same instance.
|
|
74
|
+
|
|
75
|
+
**Negative:**
|
|
76
|
+
- Changing `N` (the shard count) requires migrating every existing nick to
|
|
77
|
+
its new shard. This is an offline migration, not a live reshard. The shard
|
|
78
|
+
count should be chosen conservatively at deployment time.
|
|
79
|
+
- The channel registry problem: there is no DO that "knows" all channels
|
|
80
|
+
(LIST, TICKET-073). A dedicated registry DO or KV index is needed — this is
|
|
81
|
+
a known gap tracked in TICKET-073.
|
|
82
|
+
- Hot-nick collisions (many nicks hashing to the same shard) are possible but
|
|
83
|
+
statistically rare with FNV-1a and `N = 32` for typical populations.
|
|
84
|
+
|
|
85
|
+
## References
|
|
86
|
+
|
|
87
|
+
- PLAN §9 — open question: "Channel DO sharding granularity"
|
|
88
|
+
- PLAN §6.1 — `RegistryDO` sharded by `hash(nick) % N`
|
|
89
|
+
- `packages/cf-adapter/src/sharding.ts` — `shardNick`, `registryKeyForNick`,
|
|
90
|
+
`DEFAULT_REGISTRY_SHARDS = 32`
|
|
91
|
+
- `packages/cf-adapter/src/registry-do.ts` — per-shard registry authority
|
|
92
|
+
- TICKET-034 — RegistryDO implementation
|
|
93
|
+
- TICKET-073 — CF runtime stubbed lookups (channel registry gap)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# ADR-004: AWS DynamoDB schema
|
|
2
|
+
|
|
3
|
+
**Date:** 2025-02-15 (Phase 4)
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Supersedes:** —
|
|
6
|
+
**Superseded by:** —
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
The AWS adapter runs IRC state on DynamoDB (single table family, not
|
|
11
|
+
single-table design). Unlike Cloudflare Durable Objects, DynamoDB does not
|
|
12
|
+
serialize access to a record by default — concurrent writes can interleave.
|
|
13
|
+
Nick uniqueness, channel membership atomicity, and gone-connection cleanup
|
|
14
|
+
all require explicit concurrency control.
|
|
15
|
+
|
|
16
|
+
The schema must support: connection records, channel metadata, channel
|
|
17
|
+
membership (roster), nick registry, and SASL account credentials. It must
|
|
18
|
+
also handle the Lambda invocation model: each `$default` invocation is a
|
|
19
|
+
fresh process that reads state, runs a reducer, and writes back — there is no
|
|
20
|
+
in-memory state between invocations.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
Five tables, each with a clear partition-key boundary:
|
|
25
|
+
|
|
26
|
+
| Table | Partition Key | Sort Key | TTL | Purpose |
|
|
27
|
+
|---|---|---|---|---|
|
|
28
|
+
| `Connections` | `connectionId` | — | `idleSince` | Per-connection state: nick, user, host, caps, joined channels, away |
|
|
29
|
+
| `ChannelMeta` | `channelName` | — | — | Topic, modes, key, limit |
|
|
30
|
+
| `ChannelMembers` | `channelName` | `connectionId` | — | Roster: membership + prefix flags (op/voice) |
|
|
31
|
+
| `Nicks` | `nickLower` | — | — | Nick → connectionId registry |
|
|
32
|
+
| `Accounts` | `account` | — | — | SASL credentials (scrypt-hashed, never plaintext) |
|
|
33
|
+
|
|
34
|
+
**Concurrency invariants:**
|
|
35
|
+
- **Nick uniqueness:** enforced via conditional `PutItem`
|
|
36
|
+
(`ConditionExpression: attribute_not_exists(nickLower)`). Two racing
|
|
37
|
+
Puts for the same nick — exactly one succeeds; the other gets a
|
|
38
|
+
`ConditionalCheckFailedException` → `433 ERR_NICKNAMEINUSE`.
|
|
39
|
+
- **Membership atomicity:** `TransactWriteItems` spans `Connections`
|
|
40
|
+
(update `joinedChannels`) and `ChannelMembers` (add/remove member) in a
|
|
41
|
+
single atomic transaction. Concurrent JOIN/PART by the same connection
|
|
42
|
+
cannot leave inconsistent state.
|
|
43
|
+
- **Gone connections:** `ApiGatewayManagementApi.postToConnection` throws
|
|
44
|
+
`GoneException` for a stale connection. The handler catches this, triggers
|
|
45
|
+
lazy cleanup (removes the `Connections` row + `ChannelMembers` rows), and
|
|
46
|
+
broadcasts QUIT to peers (TICKET-074). A sweeper Lambda reaps rows whose
|
|
47
|
+
`idleSince` TTL has expired.
|
|
48
|
+
- **Environment isolation:** physical table names are environment-prefixed
|
|
49
|
+
(`<Env>Connections`, e.g. `StagingConnections`) so staging and production
|
|
50
|
+
can coexist in the same account + region (TICKET-066).
|
|
51
|
+
|
|
52
|
+
**SASL credentials** (`Accounts`): stored as scrypt `HashedAccountCredential`
|
|
53
|
+
rows (`{ account, algorithm, salt, hash }`, base64-encoded). The Lambda
|
|
54
|
+
pre-loads all rows at cold start into an in-memory `DynamoAccountStore`; the
|
|
55
|
+
reducer's `AccountStore.verify()` stays synchronous (reducers are pure — no
|
|
56
|
+
async). Seed tooling (`tools/seed-aws-accounts.ts`) writes hashed rows; never
|
|
57
|
+
plaintext.
|
|
58
|
+
|
|
59
|
+
## Consequences
|
|
60
|
+
|
|
61
|
+
**Positive:**
|
|
62
|
+
- Clear separation of concerns: each table is one authority (matches
|
|
63
|
+
ADR-002). A channel's metadata, membership, and each member's connection
|
|
64
|
+
record are independently addressable.
|
|
65
|
+
- Conditional writes + `TransactWriteItems` give race-free semantics without
|
|
66
|
+
locks — the natural fit for DynamoDB.
|
|
67
|
+
- `idleSince` TTL auto-reaps idle connections even if the sweeper misses a
|
|
68
|
+
tick; the sweeper is a belt-and-suspenders cleanup, not the only path.
|
|
69
|
+
- Pay-per-request billing means idle tables cost nothing; the schema does not
|
|
70
|
+
over-provision.
|
|
71
|
+
|
|
72
|
+
**Negative:**
|
|
73
|
+
- Five tables (vs. a single-table design) means more `TransactWriteItems`
|
|
74
|
+
spanning multiple partitions. `TransactWriteItems` consumes more RCUs than
|
|
75
|
+
individual writes and has a 100-item-per-transaction cap. Membership
|
|
76
|
+
transactions are 2 items (well within limits), but a future feature that
|
|
77
|
+
touches many channels in one command could approach the cap.
|
|
78
|
+
- No cross-table GSI for "all channels a connection is in" — the
|
|
79
|
+
`Connections` row stores `joinedChannels` as a set, which must be updated
|
|
80
|
+
on every JOIN/PART. This duplicates membership data between `Connections`
|
|
81
|
+
and `ChannelMembers`, kept consistent by the transaction.
|
|
82
|
+
- `Accounts` pre-loading at cold start means every Lambda cold start scans
|
|
83
|
+
the table. For deployments with thousands of accounts, this adds latency.
|
|
84
|
+
Mitigated by Lambda reuse (warm starts skip the scan); a cache layer or
|
|
85
|
+
on-demand lookup is a documented follow-up.
|
|
86
|
+
|
|
87
|
+
## References
|
|
88
|
+
|
|
89
|
+
- PLAN §4 — state model & authoritative ownership
|
|
90
|
+
- PLAN §6.2 — DynamoDB tables
|
|
91
|
+
- `packages/aws-adapter/src/cdk-table-defs.ts` — `TABLE_DEFS` (source of truth)
|
|
92
|
+
- `packages/aws-adapter/src/aws-runtime.ts` — `AwsRuntime` implementation
|
|
93
|
+
- TICKET-039 — CDK stack
|
|
94
|
+
- TICKET-041 — DynamoDB transactions for membership
|
|
95
|
+
- TICKET-066 — environment-name prefixing
|
|
96
|
+
- TICKET-072 — `Accounts` table scrypt-hashed `AccountStore`
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# ADR-005: wss-only transport in v1 (no raw TCP)
|
|
2
|
+
|
|
3
|
+
**Date:** 2025-01-15 (Phase 1)
|
|
4
|
+
**Status:** Superseded by ADR-009 (Phase 7 — TICKET-052)
|
|
5
|
+
**Supersedes:** —
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
IRC is traditionally spoken over raw TCP (port 6667 plaintext, 6697 TLS).
|
|
10
|
+
WebSocket Secure (wss) is a framing layer over TLS that adds an HTTP upgrade
|
|
11
|
+
handshake. Serverless compute platforms have fundamentally different support
|
|
12
|
+
for these two transports:
|
|
13
|
+
|
|
14
|
+
- **Cloudflare Workers** can accept WebSocket upgrades but **cannot accept raw
|
|
15
|
+
TCP**. Spectrum (Enterprise tier) can terminate TLS and proxy to an origin,
|
|
16
|
+
but Workers itself is request-response.
|
|
17
|
+
- **AWS API Gateway WebSocket API** provides persistent WebSocket connections
|
|
18
|
+
with per-message Lambda invocation. Raw TCP requires a Network Load Balancer
|
|
19
|
+
+ Lambda streaming (invoke mode) — a fundamentally different integration.
|
|
20
|
+
|
|
21
|
+
The v1 goal is a clean serverless story: one codebase, two adapters, zero
|
|
22
|
+
stateful origin servers. Raw TCP on either platform requires additional,
|
|
23
|
+
non-serverless infrastructure (a long-running container on CF; NLB + Lambda
|
|
24
|
+
streaming on AWS).
|
|
25
|
+
|
|
26
|
+
Additionally, wss carries one IRC message per WebSocket text frame, which
|
|
27
|
+
simplifies framing: the server does not need to implement a stateful
|
|
28
|
+
byte-stream line buffer. The parser still tolerates `\r\n`-joined frames
|
|
29
|
+
(multiple messages in one text frame) for robustness, but the common case is
|
|
30
|
+
one-message-per-frame.
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
v1 ships **wss-only**. No plaintext TCP listener, no `irc://` scheme, no
|
|
35
|
+
port 6667/6697. Clients connect via `wss://server.tld/` and speak IRC inside
|
|
36
|
+
WebSocket text frames.
|
|
37
|
+
|
|
38
|
+
Rationale:
|
|
39
|
+
1. Both target platforms support wss natively without additional
|
|
40
|
+
infrastructure.
|
|
41
|
+
2. wss is TLS-terminated at the edge (CF) or at API Gateway (AWS) — the
|
|
42
|
+
Lambda/Worker never sees plaintext credentials.
|
|
43
|
+
3. One-message-per-frame eliminates the need for a TCP-style line-framing
|
|
44
|
+
buffer in the core (though the parser tolerates joined frames).
|
|
45
|
+
4. The serverless cost/ops model is clean: no long-running origin, no TCP
|
|
46
|
+
keepalive management.
|
|
47
|
+
|
|
48
|
+
The `ConnectionActor` transport seam accepts WebSocket text frames directly.
|
|
49
|
+
A `Transport` interface is not abstracted in v1 — the actor's
|
|
50
|
+
`receiveTextFrame(text)` is the only entry point.
|
|
51
|
+
|
|
52
|
+
## Consequences
|
|
53
|
+
|
|
54
|
+
**Positive:**
|
|
55
|
+
- Zero infrastructure beyond the serverless platform. No TCP listener, no
|
|
56
|
+
origin server, no port management.
|
|
57
|
+
- TLS everywhere — no accidental plaintext exposure.
|
|
58
|
+
- Simple framing: WebSocket text frames are message-delimited by definition.
|
|
59
|
+
- Lower operational burden for v1.
|
|
60
|
+
|
|
61
|
+
**Negative:**
|
|
62
|
+
- RFC 1459/2812 clients that only speak raw TCP cannot connect. WeeChat and
|
|
63
|
+
HexChat support WebSocket; some older clients and bots do not.
|
|
64
|
+
- The WebSocket overhead (HTTP upgrade, per-frame masking) adds ~2–10 bytes
|
|
65
|
+
per message vs. raw TCP. Negligible for IRC's small message sizes.
|
|
66
|
+
- Some IRCv3 extensions assume a TCP transport (e.g. SASL EXTERNAL via client
|
|
67
|
+
certs over TLS passthrough). Without raw TCP, mTLS must be done at the edge
|
|
68
|
+
(CF API Shield, API Gateway custom domain), which changes the cert
|
|
69
|
+
verification surface.
|
|
70
|
+
|
|
71
|
+
**Superseded:** Phase 7 (TICKET-052) reverses this decision for v1.x: a
|
|
72
|
+
dual-transport model (wss stays the default; `irc+tls` on :6697 is added via
|
|
73
|
+
Cloudflare Spectrum + Container origin, and AWS NLB + Lambda streaming). This
|
|
74
|
+
ADR documents the original v1 rationale; the superseding ADR covers the
|
|
75
|
+
reversal and per-platform TCP+TLS mapping.
|
|
76
|
+
|
|
77
|
+
## References
|
|
78
|
+
|
|
79
|
+
- PLAN §3 — "No plaintext TCP in v1 (wss only)"
|
|
80
|
+
- PLAN §9 — "Framing: wss-only"
|
|
81
|
+
- TICKET-052 — superseding ADR: dual wss + irc+tls
|
|
82
|
+
- `packages/irc-server/src/actor.ts` — `receiveTextFrame(text)` (single
|
|
83
|
+
transport entry point in v1)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# ADR-006: SASL mechanism scope (PLAIN only, EXTERNAL deferred)
|
|
2
|
+
|
|
3
|
+
**Date:** 2025-01-20 (Phase 1)
|
|
4
|
+
**Status:** Accepted (EXTERNAL re-evaluation tracked in TICKET-079)
|
|
5
|
+
**Supersedes:** —
|
|
6
|
+
**Superseded by:** —
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
IRCv3 SASL (`CAP sasl`) lets a client authenticate during registration,
|
|
11
|
+
before the welcome sequence. The server advertises supported mechanisms in
|
|
12
|
+
the `sasl` cap value (e.g. `sasl=PLAIN,EXTERNAL`). Each mechanism has
|
|
13
|
+
different infrastructure requirements:
|
|
14
|
+
|
|
15
|
+
- **PLAIN:** the client sends `AUTHENTICATE PLAIN`, then a base64 payload of
|
|
16
|
+
`\0username\0password`. The server verifies the credentials against an
|
|
17
|
+
`AccountStore`. No client certificates, no TLS-passthrough — works over
|
|
18
|
+
any transport, including wss with edge-terminated TLS.
|
|
19
|
+
- **EXTERNAL:** the server authenticates the client based on an external
|
|
20
|
+
identity, typically a client certificate verified via mutual TLS (mTLS).
|
|
21
|
+
This requires the TLS handshake to present the client cert to the
|
|
22
|
+
application layer. On serverless platforms with edge-terminated TLS
|
|
23
|
+
(Cloudflare, API Gateway), the application does not see the raw client
|
|
24
|
+
cert — it must be configured at the edge (API Shield mTLS, custom-domain
|
|
25
|
+
mTLS trust store). That infrastructure does not exist in v1 (ADR-005:
|
|
26
|
+
wss-only, no raw TCP; mTLS passthrough is Phase 7, TICKET-054).
|
|
27
|
+
|
|
28
|
+
## Decision
|
|
29
|
+
|
|
30
|
+
v1 ships **SASL PLAIN only** as a functional mechanism. EXTERNAL is
|
|
31
|
+
**recognized but stubbed**: the reducer accepts `AUTHENTICATE EXTERNAL`,
|
|
32
|
+
acknowledges the mechanism, and immediately returns `904 ERR_SASLFAIL` with a
|
|
33
|
+
documented code comment cross-referencing TICKET-054 (mTLS) and TICKET-079
|
|
34
|
+
(decision: implement or stop advertising).
|
|
35
|
+
|
|
36
|
+
The `sasl` cap advertises `PLAIN,EXTERNAL` (both names listed) so clients
|
|
37
|
+
that probe mechanism availability see EXTERNAL as nominally supported. The
|
|
38
|
+
actual `904` on EXTERNAL attempt is the functional gate. TICKET-079 is the
|
|
39
|
+
follow-up that decides whether to implement EXTERNAL against a pluggable
|
|
40
|
+
`MtlsIdentityProvider` or stop advertising it until mTLS lands.
|
|
41
|
+
|
|
42
|
+
`AccountStore` is a port in `irc-core` consumed by the SASL reducer. The
|
|
43
|
+
`verify(mechanism, payload)` method is **synchronous** — adapters pre-load
|
|
44
|
+
credentials at boot so the reducer stays pure (ADR-001). PLAIN verification
|
|
45
|
+
decodes the base64 payload, extracts username + password, and matches against
|
|
46
|
+
the store.
|
|
47
|
+
|
|
48
|
+
## Consequences
|
|
49
|
+
|
|
50
|
+
**Positive:**
|
|
51
|
+
- PLAIN works end-to-end in v1 with no special infrastructure. The
|
|
52
|
+
`AccountStore` port abstracts the credential store; adapters seed it from
|
|
53
|
+
config (in-memory), DynamoDB (AWS), or a persistent backend (D1/DO for CF,
|
|
54
|
+
TICKET-093).
|
|
55
|
+
- EXTERNAL is forward-compatible: advertising it now means clients that
|
|
56
|
+
prefer EXTERNAL will attempt it; once mTLS lands (Phase 7), the same
|
|
57
|
+
`AUTHENTICATE EXTERNAL` flow succeeds without a client-side change.
|
|
58
|
+
- The synchronous `AccountStore.verify()` keeps the reducer pure and
|
|
59
|
+
deterministic. Async credential lookups (DynamoDB) are resolved at cold
|
|
60
|
+
start or via pre-loading, not inside the reducer.
|
|
61
|
+
|
|
62
|
+
**Negative:**
|
|
63
|
+
- EXTERNAL is advertised but non-functional in v1 — a client that negotiates
|
|
64
|
+
`sasl` and sends `AUTHENTICATE EXTERNAL` gets `904` with no actionable
|
|
65
|
+
error message. This is a poor UX until TICKET-079 resolves (stop-advertising
|
|
66
|
+
is the recommended interim per the ticket).
|
|
67
|
+
- PLAIN sends the password base64-encoded (not encrypted) inside the wss
|
|
68
|
+
frame. wss provides the TLS layer, so the password is encrypted in transit
|
|
69
|
+
— but PLAIN is still weaker than EXTERNAL (no mutual auth).
|
|
70
|
+
- The synchronous `AccountStore` contract means the adapter must pre-load all
|
|
71
|
+
credentials. For deployments with many accounts, cold-start latency grows
|
|
72
|
+
linearly with account count. A lazy/async lookup variant is a documented
|
|
73
|
+
follow-up but would require an async-reducer or effect-based credential
|
|
74
|
+
query — a significant architecture change.
|
|
75
|
+
|
|
76
|
+
## References
|
|
77
|
+
|
|
78
|
+
- PLAN §3 — "sasl (mechanisms: PLAIN, EXTERNAL reserved for later with mTLS)"
|
|
79
|
+
- PLAN §9 — SASL mechanism scope
|
|
80
|
+
- TICKET-026 — SASL reducer implementation
|
|
81
|
+
- TICKET-071 — AccountStore wiring into ConnectionActor
|
|
82
|
+
- TICKET-072 — AWS DynamoDB AccountStore (scrypt hashing)
|
|
83
|
+
- TICKET-079 — SASL EXTERNAL: implement or stop advertising (interim decision)
|
|
84
|
+
- TICKET-054 — mTLS support (Phase 7, enables functional EXTERNAL)
|
|
85
|
+
- `packages/irc-core/src/commands/sasl.ts` — `authenticateReducer`
|
|
86
|
+
- `packages/irc-core/src/ports.ts` — `AccountStore` port
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# ADR-007: Deterministic ports for time and ID generation
|
|
2
|
+
|
|
3
|
+
**Date:** 2025-01-15 (Phase 1)
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Supersedes:** —
|
|
6
|
+
**Superseded by:** —
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
IRC protocol behaviour is time-sensitive: idle/PING timers, WHOIS signon
|
|
11
|
+
time, `server-time` tags (ISO-8601 timestamps on every message), SASL nonce
|
|
12
|
+
generation, chathistory message IDs. A traditional ircd calls `Date.now()`
|
|
13
|
+
and `crypto.randomUUID()` directly in handlers. This makes tests
|
|
14
|
+
non-deterministic: the same input produces different output on every run,
|
|
15
|
+
depending on when the test executed.
|
|
16
|
+
|
|
17
|
+
Non-determinism is fatal for the parametrized contract suite (TICKET-032),
|
|
18
|
+
which runs identical scenarios across in-memory, CF, and AWS runtimes. If a
|
|
19
|
+
reducer embeds `Date.now()`, the expected `@time=` tag changes every
|
|
20
|
+
millisecond and the assertion cannot be written.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
Time and ID generation are **injected ports**, not global calls:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
interface Clock {
|
|
28
|
+
now(): number; // epoch milliseconds
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface IdFactory {
|
|
32
|
+
nonce(): string; // unique message ID for message-tags/chathistory
|
|
33
|
+
traceId(): string; // per-request trace ID for observability
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Both are passed through `Ctx` to every reducer. Reducers read `ctx.clock.now()`
|
|
38
|
+
and `ctx.ids.nonce()` — never `Date.now()` or `Math.random()`.
|
|
39
|
+
|
|
40
|
+
Concrete implementations:
|
|
41
|
+
- **`FakeClock`** (tests): returns a fixed value set at construction.
|
|
42
|
+
Deterministic: `new FakeClock(10_000)` always reports `now() === 10_000`.
|
|
43
|
+
- **`SystemClock`** (production): wraps `Date.now()`.
|
|
44
|
+
- **`SequentialIdFactory`** (tests): returns `id-0`, `id-1`, `id-2`, …
|
|
45
|
+
Deterministic: the same sequence of calls produces the same IDs.
|
|
46
|
+
- **`RandomIdFactory`** (production): wraps `crypto.randomUUID()`.
|
|
47
|
+
|
|
48
|
+
## Consequences
|
|
49
|
+
|
|
50
|
+
**Positive:**
|
|
51
|
+
- Reducer tests are fully deterministic. The same `(state, message, ctx)`
|
|
52
|
+
triple always produces the same `{ state, effects }`. No flaky tests.
|
|
53
|
+
- The contract suite can assert exact wire output including `@time=` tags
|
|
54
|
+
and `msgid=` tags, because the fake clock and ID factory are pinned.
|
|
55
|
+
- Time travel is trivial in tests: advance the `FakeClock` between messages
|
|
56
|
+
to test idle/PING logic, flood-control refill, and chathistory bounds.
|
|
57
|
+
- The reducer purity contract (ADR-001) is enforceable: a reducer that calls
|
|
58
|
+
`Date.now()` is simply not using the injected port, and the test will fail
|
|
59
|
+
because the output is wrong.
|
|
60
|
+
|
|
61
|
+
**Negative:**
|
|
62
|
+
- Every call site that needs time or an ID must thread `ctx` — there is no
|
|
63
|
+
shortcut. This is a small constant cost per reducer.
|
|
64
|
+
- The `Clock` and `IdFactory` must be consistent across the actor's lifetime.
|
|
65
|
+
The actor pins them at construction; switching mid-connection would
|
|
66
|
+
produce inconsistent timestamps. This is enforced by the actor's readonly
|
|
67
|
+
fields.
|
|
68
|
+
- `Date.now()` resolution is milliseconds; sub-millisecond ordering is not
|
|
69
|
+
possible with this port. This has not been a problem for IRC (message
|
|
70
|
+
ordering is preserved by connection/channel authority serialization, not
|
|
71
|
+
by timestamp).
|
|
72
|
+
|
|
73
|
+
## References
|
|
74
|
+
|
|
75
|
+
- PLAN §9 — "Time/IDs: injected ports (Clock, IdFactory) for deterministic
|
|
76
|
+
tests"
|
|
77
|
+
- PLAN §8 — "Determinism rules: inject a Clock and deterministic ID/nonce
|
|
78
|
+
ports. No real timers/random in reducers."
|
|
79
|
+
- ADR-001 — pure reducers (why determinism matters)
|
|
80
|
+
- `packages/irc-core/src/ports.ts` — `Clock`, `IdFactory`, `FakeClock`,
|
|
81
|
+
`SequentialIdFactory`, `SystemClock`, `RandomIdFactory`
|
|
82
|
+
- `packages/irc-core/src/types.ts` — `Ctx.clock`, `Ctx.ids`
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# ADR-008: Monorepo and tooling stack
|
|
2
|
+
|
|
3
|
+
**Date:** 2025-01-10 (Phase 0)
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Supersedes:** —
|
|
6
|
+
**Superseded by:** —
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
ServerlessIRCd is a multi-package TypeScript project: a platform-agnostic
|
|
11
|
+
core (`irc-core`), an orchestration layer (`irc-server`), a reference runtime
|
|
12
|
+
(`in-memory-runtime`), two cloud adapters (`cf-adapter`, `aws-adapter`), and
|
|
13
|
+
several apps (`local-cli`, `cf-worker`, `aws-stack`). The toolchain must
|
|
14
|
+
support cross-package type checking, incremental builds, a unified test
|
|
15
|
+
runner, and CI that enforces coverage gates per package.
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
|
|
19
|
+
| Concern | Choice | Rationale |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Package manager | **pnpm** (workspaces) | Fast, disk-efficient, first-class monorepo support |
|
|
22
|
+
| Build orchestration | **turbo** (caching) | Incremental builds, remote cache, task dependency graph |
|
|
23
|
+
| TypeScript | Project references + `tsconfig.base.json` | Strict mode, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess` |
|
|
24
|
+
| Test runner | **vitest** | ESM-native, fast, integrates with Workers (`vitest-pool-workers`), built-in coverage |
|
|
25
|
+
| Coverage | v8 provider, gate ≥90% (100% on `irc-core`) | Enforced in CI (`.github/workflows/ci.yml`) |
|
|
26
|
+
| Lint/format | **Biome** | Single tool (replaces ESLint + Prettier), fast, zero config drift |
|
|
27
|
+
| CF bundler | **wrangler** (esbuild internally) | Native Workers toolchain |
|
|
28
|
+
| AWS bundler | **esbuild** (Lambda bundles) + **CDK v2** (infra) | Tree-shaking, fast cold starts |
|
|
29
|
+
| Runtime target | Node 20 (Lambda) + latest Workers runtime; ES2022+ | Aligns with platform-supported runtimes |
|
|
30
|
+
|
|
31
|
+
## Consequences
|
|
32
|
+
|
|
33
|
+
**Positive:**
|
|
34
|
+
- pnpm workspaces + turbo give fast local dev: only changed packages rebuild;
|
|
35
|
+
tests run incrementally.
|
|
36
|
+
- vitest's ESM-native design avoids the CJS/ESM interop pain that plagues
|
|
37
|
+
Jest-based monorepos. The same test files run under `vitest-pool-workers`
|
|
38
|
+
(real `workerd`) for CF adapter tests.
|
|
39
|
+
- Biome eliminates the ESLint/Prettier config drift problem — one tool, one
|
|
40
|
+
config, one command (`biome check`).
|
|
41
|
+
- TypeScript project references enforce package boundaries at compile time:
|
|
42
|
+
`irc-core` cannot accidentally import from `cf-adapter`.
|
|
43
|
+
|
|
44
|
+
**Negative:**
|
|
45
|
+
- pnpm's strict node-linker can surprise contributors used to npm/yarn's
|
|
46
|
+
hoisting. `node_modules` is per-package; global scripts need
|
|
47
|
+
`pnpm --filter`.
|
|
48
|
+
- turbo's caching requires consistent environment hashing; a CI cache miss
|
|
49
|
+
(e.g. different Node minor version) degrades to a full rebuild silently.
|
|
50
|
+
- Biome is newer than ESLint and lacks some ecosystem plugins. Custom rules
|
|
51
|
+
(e.g. "no ticket IDs in source files") are enforced via convention
|
|
52
|
+
(`AGENTS.md`) rather than a Biome plugin.
|
|
53
|
+
|
|
54
|
+
## References
|
|
55
|
+
|
|
56
|
+
- PLAN §9 — "Monorepo: pnpm + TypeScript project references; turbo for
|
|
57
|
+
caching"
|
|
58
|
+
- `pnpm-workspace.yaml`, `turbo.json`, `tsconfig.base.json`, `biome.json`
|
|
59
|
+
- `.github/workflows/ci.yml` — coverage gates and CI pipeline
|
|
60
|
+
- `AGENTS.md` — code style conventions
|
|
@@ -235,8 +235,9 @@ splitting yields clearer conditional writes and transactions.
|
|
|
235
235
|
┌──────────────┬────────────┐
|
|
236
236
|
│ PK │ account │
|
|
237
237
|
├──────────────┼────────────┤
|
|
238
|
-
│
|
|
239
|
-
│ salt │
|
|
238
|
+
│ algorithm │ string │
|
|
239
|
+
│ salt │ string │
|
|
240
|
+
│ hash │ string │
|
|
240
241
|
└──────────────┴────────────┘
|
|
241
242
|
```
|
|
242
243
|
|
|
@@ -247,8 +248,9 @@ logic:
|
|
|
247
248
|
nick reservation atomic across concurrent Lambdas.
|
|
248
249
|
- `ChannelMembers` PK+SK composite — `TransactWriteItems` for JOIN/PART
|
|
249
250
|
updates `Connections.joined` and `ChannelMembers` in one atomic write.
|
|
250
|
-
- `Accounts
|
|
251
|
-
|
|
251
|
+
- `Accounts` row schema — `{ account (PK), algorithm, salt, hash }`,
|
|
252
|
+
all strings. Never plaintext; PLAIN SASL verifies the scrypt hash
|
|
253
|
+
server-side.
|
|
252
254
|
|
|
253
255
|
---
|
|
254
256
|
|
package/docs/AWS-Deployment.md
CHANGED
|
@@ -481,6 +481,11 @@ any of them** — to enable one, add an `addEnvironment(...)` line in
|
|
|
481
481
|
| `TOPIC_LEN` | int | `topicLen` |
|
|
482
482
|
| `MAX_LIST_ENTRIES` | int | `maxListEntries` |
|
|
483
483
|
| `QUIT_MESSAGE` | string | `quitMessage` |
|
|
484
|
+
| `SASL_ACCOUNTS` | string | `saslAccounts` |
|
|
485
|
+
|
|
486
|
+
`SASL_ACCOUNTS` is the legacy/config fallback (newline-delimited
|
|
487
|
+
`username:password` pairs); it is used only when the `Accounts` table is
|
|
488
|
+
empty. When the table has rows it is authoritative — see §8.1.
|
|
484
489
|
|
|
485
490
|
Invalid values cause a readable boot-time error — the Lambda will
|
|
486
491
|
throw on the first invocation (cold start). Test changes locally with
|
|
@@ -523,11 +528,12 @@ namespaces).
|
|
|
523
528
|
|
|
524
529
|
## 8. Secrets
|
|
525
530
|
|
|
526
|
-
**As of v1 the Lambda consumes no
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
+
**As of v1 the Lambda consumes no Secrets-Manager/SSM bindings.** Server
|
|
532
|
+
password and oper creds are handled by later hardening work; when they
|
|
533
|
+
ship, the natural homes are AWS Secrets Manager or SSM Parameter Store
|
|
534
|
+
(the AWS adapter architecture doc calls both out as supporting
|
|
535
|
+
services). SASL account credentials, however, **are** persisted — in the
|
|
536
|
+
`Accounts` DynamoDB table (see §8.1).
|
|
531
537
|
|
|
532
538
|
When secrets land, the typical pattern will be:
|
|
533
539
|
|
|
@@ -544,6 +550,77 @@ aws secretsmanager create-secret \
|
|
|
544
550
|
--secret-string "$(openssl rand -base64 32)"
|
|
545
551
|
```
|
|
546
552
|
|
|
553
|
+
### 8.1 SASL accounts (`Accounts` table + seed tooling)
|
|
554
|
+
|
|
555
|
+
SASL PLAIN credentials live in the `Accounts` DynamoDB table as
|
|
556
|
+
`HashedAccountCredential` rows — `{ account, algorithm, salt, hash }`,
|
|
557
|
+
all strings, with scrypt hashes encoded as base64 (never plaintext).
|
|
558
|
+
At Lambda cold start, `buildDepsFromEnv` → `resolveAccountStore`
|
|
559
|
+
scans the table once and pre-loads every row into a
|
|
560
|
+
`DynamoAccountStore`; the synchronous `verify()` called from the SASL
|
|
561
|
+
reducer works against that in-memory snapshot. The snapshot is rebuilt
|
|
562
|
+
on every cold start, so account changes take effect within one cold
|
|
563
|
+
start (seconds to minutes).
|
|
564
|
+
|
|
565
|
+
**Precedence** (`resolveAccountStore`, in `account-store.ts`):
|
|
566
|
+
|
|
567
|
+
1. The `Accounts` table is **authoritative when it has ≥1 row** — the
|
|
568
|
+
table wins and the config seed is ignored.
|
|
569
|
+
2. When the table is empty, the adapter falls back to the
|
|
570
|
+
`SASL_ACCOUNTS` env var (newline-delimited `username:password` pairs,
|
|
571
|
+
see §7.3) so existing deployments keep working without migrating.
|
|
572
|
+
3. When neither source has accounts, no `AccountStore` is bound and
|
|
573
|
+
`AUTHENTICATE PLAIN` returns `904 ERR_SASLFAIL`.
|
|
574
|
+
|
|
575
|
+
**Seed / CRUD tooling.** Provision accounts by writing
|
|
576
|
+
`HashedAccountCredential` rows produced by `hashAccountCredential`
|
|
577
|
+
(scrypt, random salt, base64-encoded). The adapter exports
|
|
578
|
+
`putAccountCredential(docClient, tableName, username, password)`
|
|
579
|
+
for this — a repeat call for the same `username` overwrites the row
|
|
580
|
+
(the PK is `account`). A minimal seeding snippet (Node 20, from the repo
|
|
581
|
+
root):
|
|
582
|
+
|
|
583
|
+
```bash
|
|
584
|
+
node --input-type=module -e '
|
|
585
|
+
import { createDynamoDocumentClient, putAccountCredential } from "@serverless-ircd/aws-adapter";
|
|
586
|
+
const dynamo = createDynamoDocumentClient({ region: "us-east-1" });
|
|
587
|
+
await putAccountCredential(dynamo, "StagingAccounts", "alice", "change-me");
|
|
588
|
+
console.log("seeded alice");
|
|
589
|
+
'
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
A repo-local seed script (`tools/seed-aws-accounts.ts`) wraps the same
|
|
593
|
+
calls for batch provisioning:
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
# Seed against DynamoDB Local:
|
|
597
|
+
npx tsx tools/seed-aws-accounts.ts \
|
|
598
|
+
--table StagingAccounts \
|
|
599
|
+
--endpoint http://localhost:8000 \
|
|
600
|
+
--accounts alice:s3cret bob:password2
|
|
601
|
+
|
|
602
|
+
# Or from a file (newline-delimited username:password):
|
|
603
|
+
npx tsx tools/seed-aws-accounts.ts \
|
|
604
|
+
--table StagingAccounts \
|
|
605
|
+
--region us-east-1 \
|
|
606
|
+
--file accounts.txt
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
For a one-off `aws-cli` write, hash the password out-of-band and put the
|
|
610
|
+
row directly:
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
# Hash with the adapter, then write the row yourself:
|
|
614
|
+
aws dynamodb put-item \
|
|
615
|
+
--table-name StagingAccounts \
|
|
616
|
+
--item '{"account":{"S":"alice"},"algorithm":{"S":"scrypt"},"salt":{"S":"<base64-salt>"},"hash":{"S":"<base64-hash>"}}'
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
> The `Accounts` table is granted to all three Lambdas (`handler`,
|
|
620
|
+
> `sweeper`, `pingChecker`) via `grantReadWriteData`; only the handler
|
|
621
|
+
> reads it at cold start. The sweeper/ping-checker never touch SASL
|
|
622
|
+
> state, but sharing the grant keeps the IAM story uniform.
|
|
623
|
+
|
|
547
624
|
**Never** put credentials, password hashes, or API tokens in
|
|
548
625
|
`aws-stack.ts`, in Lambda env vars, or in committed files. Lambda env
|
|
549
626
|
vars are visible in the console and in CloudFormation describe output;
|
|
@@ -611,8 +688,10 @@ code):
|
|
|
611
688
|
- `ChannelMembers` PK+SK composite — `TransactWriteItems` for JOIN/PART
|
|
612
689
|
updates `Connections.joined` and `ChannelMembers` in one atomic
|
|
613
690
|
write (up to 100 items per transaction, the AWS limit).
|
|
614
|
-
- `Accounts
|
|
615
|
-
|
|
691
|
+
- `Accounts` row schema — `{ account (PK), algorithm, salt, hash }`,
|
|
692
|
+
all strings. `algorithm` is `'scrypt'`; `salt` and `hash` are
|
|
693
|
+
base64-encoded. Never plaintext. SASL PLAIN verifies against this
|
|
694
|
+
hash server-side. Seed rows with `putAccountCredential` (see §8.1).
|
|
616
695
|
|
|
617
696
|
### 9.3 The hottest pattern: channel fanout
|
|
618
697
|
|
|
@@ -31,14 +31,14 @@ namespaces that hold all IRC state:
|
|
|
31
31
|
▼
|
|
32
32
|
┌─────────────────────────────────────────────────────────────┐
|
|
33
33
|
│ ConnectionDO (one per WebSocket connection) │
|
|
34
|
-
│ • hibernatable WS via HibernatingWebSocketHandler
|
|
35
|
-
│ • owns ConnectionState, drives ConnectionActor
|
|
36
|
-
│ • DO storage persists state across hibernation
|
|
37
|
-
│ • alarm() drives the PING / no-PONG disconnect sweep
|
|
34
|
+
│ • hibernatable WS via HibernatingWebSocketHandler │
|
|
35
|
+
│ • owns ConnectionState, drives ConnectionActor │
|
|
36
|
+
│ • DO storage persists state across hibernation │
|
|
37
|
+
│ • alarm() drives the PING / no-PONG disconnect sweep │
|
|
38
38
|
└────┬───────────────────────────────┬────────────────────────┘
|
|
39
39
|
│ stub() RPC │ stub() RPC
|
|
40
40
|
▼ ▼
|
|
41
|
-
┌──────────────────────┐
|
|
41
|
+
┌──────────────────────┐ ┌──────────────────────────────────┐
|
|
42
42
|
│ RegistryDO │ │ ChannelDO │
|
|
43
43
|
│ (sharded by nick) │ │ (one per lowercased chan name) │
|
|
44
44
|
│ • nick uniqueness │ │ • roster + modes + topic │
|