serverless-ircd 0.2.0 → 0.4.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 (221) hide show
  1. package/.github/workflows/ci.yml +2 -2
  2. package/.github/workflows/deploy-aws.yml +1 -3
  3. package/.github/workflows/deploy-cf-tcp.yml +87 -0
  4. package/.github/workflows/deploy-cf.yml +1 -1
  5. package/.node-version +1 -0
  6. package/.nvmrc +1 -0
  7. package/CHANGELOG.md +258 -18
  8. package/README.md +72 -30
  9. package/apps/aws-stack/README.md +2 -2
  10. package/apps/aws-stack/bin/aws.ts +7 -0
  11. package/apps/aws-stack/package.json +4 -4
  12. package/apps/aws-stack/src/aws-stack.ts +118 -6
  13. package/apps/aws-stack/tests/stack.test.ts +98 -3
  14. package/apps/cf-tcp-container/Dockerfile +69 -0
  15. package/apps/cf-tcp-container/package.json +34 -0
  16. package/apps/cf-tcp-container/src/config-loader.ts +145 -0
  17. package/apps/cf-tcp-container/src/container-do.ts +38 -0
  18. package/apps/cf-tcp-container/src/container-server.ts +363 -0
  19. package/apps/cf-tcp-container/src/main.ts +77 -0
  20. package/apps/cf-tcp-container/src/persistence.ts +144 -0
  21. package/apps/cf-tcp-container/src/worker.ts +41 -0
  22. package/apps/cf-tcp-container/terraform/provider.tf +24 -0
  23. package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
  24. package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
  25. package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
  26. package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
  27. package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
  28. package/apps/cf-tcp-container/tsconfig.build.json +17 -0
  29. package/apps/cf-tcp-container/tsconfig.test.json +15 -0
  30. package/apps/cf-tcp-container/vitest.config.ts +26 -0
  31. package/apps/cf-tcp-container/wrangler.toml +63 -0
  32. package/apps/cf-worker/package.json +1 -1
  33. package/apps/cf-worker/scripts/smoke.mjs +0 -0
  34. package/apps/cf-worker/src/worker.ts +8 -2
  35. package/apps/cf-worker/tests/smoke.test.ts +1 -0
  36. package/apps/cf-worker/wrangler.test.toml +11 -1
  37. package/apps/cf-worker/wrangler.toml +69 -19
  38. package/apps/local-cli/package.json +1 -1
  39. package/apps/local-cli/src/config-loader.ts +11 -0
  40. package/apps/local-cli/src/main.ts +1 -1
  41. package/apps/local-cli/src/server.ts +46 -3
  42. package/apps/local-cli/tests/e2e.test.ts +52 -0
  43. package/package.json +19 -16
  44. package/packages/aws-adapter/package.json +3 -3
  45. package/packages/aws-adapter/src/account-store.ts +121 -0
  46. package/packages/aws-adapter/src/admission.ts +74 -0
  47. package/packages/aws-adapter/src/aws-runtime.ts +124 -34
  48. package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
  49. package/packages/aws-adapter/src/config-loader.ts +62 -0
  50. package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
  51. package/packages/aws-adapter/src/handlers/connect.ts +64 -8
  52. package/packages/aws-adapter/src/handlers/default.ts +43 -2
  53. package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
  54. package/packages/aws-adapter/src/handlers/index.ts +120 -9
  55. package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
  56. package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
  57. package/packages/aws-adapter/src/index.ts +13 -0
  58. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
  59. package/packages/aws-adapter/tests/account-store.test.ts +279 -0
  60. package/packages/aws-adapter/tests/admission.test.ts +70 -0
  61. package/packages/aws-adapter/tests/aws-harness.ts +13 -1
  62. package/packages/aws-adapter/tests/config-loader.test.ts +75 -0
  63. package/packages/aws-adapter/tests/connect.test.ts +78 -0
  64. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
  65. package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
  66. package/packages/aws-adapter/tests/handlers.test.ts +194 -47
  67. package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
  68. package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
  69. package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
  70. package/packages/aws-adapter/tests/transactions.test.ts +25 -20
  71. package/packages/cf-adapter/package.json +1 -1
  72. package/packages/cf-adapter/src/cf-runtime.ts +40 -22
  73. package/packages/cf-adapter/src/channel-do.ts +40 -1
  74. package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
  75. package/packages/cf-adapter/src/config-loader.ts +62 -0
  76. package/packages/cf-adapter/src/connection-do.ts +181 -31
  77. package/packages/cf-adapter/src/d1-account-store.ts +198 -0
  78. package/packages/cf-adapter/src/env.ts +58 -8
  79. package/packages/cf-adapter/src/index.ts +11 -9
  80. package/packages/cf-adapter/tests/cf-harness.ts +11 -1
  81. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
  82. package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
  83. package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
  84. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
  85. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
  86. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
  87. package/packages/cf-adapter/tests/connection-do.test.ts +40 -0
  88. package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
  89. package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
  90. package/packages/cf-adapter/tests/worker/main.ts +9 -1
  91. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
  92. package/packages/cf-adapter/wrangler.test.toml +18 -1
  93. package/packages/in-memory-runtime/package.json +1 -1
  94. package/packages/in-memory-runtime/src/in-memory-runtime.ts +14 -28
  95. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
  96. package/packages/irc-core/package.json +8 -2
  97. package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
  98. package/packages/irc-core/src/caps/capabilities.ts +23 -2
  99. package/packages/irc-core/src/case-fold.ts +64 -0
  100. package/packages/irc-core/src/cloak.ts +1 -1
  101. package/packages/irc-core/src/commands/cap.ts +8 -1
  102. package/packages/irc-core/src/commands/index.ts +11 -0
  103. package/packages/irc-core/src/commands/invite.ts +6 -9
  104. package/packages/irc-core/src/commands/ison.ts +61 -0
  105. package/packages/irc-core/src/commands/isupport.ts +1 -1
  106. package/packages/irc-core/src/commands/join.ts +4 -3
  107. package/packages/irc-core/src/commands/kick.ts +4 -11
  108. package/packages/irc-core/src/commands/list.ts +5 -4
  109. package/packages/irc-core/src/commands/mode.ts +5 -9
  110. package/packages/irc-core/src/commands/motd-lines.ts +127 -0
  111. package/packages/irc-core/src/commands/motd.ts +6 -110
  112. package/packages/irc-core/src/commands/oper.ts +151 -0
  113. package/packages/irc-core/src/commands/quit.ts +12 -0
  114. package/packages/irc-core/src/commands/registration.ts +26 -19
  115. package/packages/irc-core/src/commands/sasl.ts +72 -9
  116. package/packages/irc-core/src/commands/server-info.ts +129 -0
  117. package/packages/irc-core/src/commands/tagmsg.ts +205 -0
  118. package/packages/irc-core/src/commands/userhost.ts +84 -0
  119. package/packages/irc-core/src/commands/whowas.ts +113 -0
  120. package/packages/irc-core/src/config.ts +84 -3
  121. package/packages/irc-core/src/credential-hashing.ts +124 -0
  122. package/packages/irc-core/src/effects.ts +6 -29
  123. package/packages/irc-core/src/index.ts +3 -0
  124. package/packages/irc-core/src/ports.ts +240 -7
  125. package/packages/irc-core/src/protocol/numerics.ts +14 -8
  126. package/packages/irc-core/src/types.ts +60 -1
  127. package/packages/irc-core/tests/account-store.test.ts +131 -0
  128. package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
  129. package/packages/irc-core/tests/case-fold.test.ts +80 -0
  130. package/packages/irc-core/tests/commands/cap.test.ts +33 -1
  131. package/packages/irc-core/tests/commands/ison.test.ts +166 -0
  132. package/packages/irc-core/tests/commands/kick.test.ts +15 -0
  133. package/packages/irc-core/tests/commands/oper.test.ts +257 -0
  134. package/packages/irc-core/tests/commands/quit.test.ts +69 -2
  135. package/packages/irc-core/tests/commands/registration.test.ts +256 -19
  136. package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
  137. package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
  138. package/packages/irc-core/tests/commands/tagmsg.test.ts +662 -0
  139. package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
  140. package/packages/irc-core/tests/commands/who.test.ts +22 -4
  141. package/packages/irc-core/tests/commands/whois.test.ts +8 -1
  142. package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
  143. package/packages/irc-core/tests/config.test.ts +170 -1
  144. package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
  145. package/packages/irc-core/tests/effects.test.ts +0 -27
  146. package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
  147. package/packages/irc-core/tests/numerics.test.ts +12 -0
  148. package/packages/irc-core/tests/types.test.ts +35 -1
  149. package/packages/irc-core/tsconfig.build.json +1 -1
  150. package/packages/irc-core/tsconfig.test.json +1 -1
  151. package/packages/irc-server/package.json +1 -1
  152. package/packages/irc-server/src/actor.ts +182 -14
  153. package/packages/irc-server/src/dispatch.ts +0 -3
  154. package/packages/irc-server/src/index.ts +10 -2
  155. package/packages/irc-server/src/routing.ts +21 -0
  156. package/packages/irc-server/src/transport.ts +101 -0
  157. package/packages/irc-server/tests/actor.test.ts +617 -1
  158. package/packages/irc-server/tests/dispatch.test.ts +0 -17
  159. package/packages/irc-server/tests/routing.test.ts +7 -0
  160. package/packages/irc-server/tests/transport.test.ts +230 -0
  161. package/packages/irc-test-support/package.json +1 -1
  162. package/packages/irc-test-support/src/harness.ts +44 -9
  163. package/packages/irc-test-support/src/in-memory-harness.ts +78 -13
  164. package/packages/irc-test-support/src/index.ts +3 -0
  165. package/packages/irc-test-support/src/scenarios.ts +132 -2
  166. package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
  167. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
  168. package/pnpm-workspace.yaml +9 -0
  169. package/tools/ci-hardening/package.json +1 -1
  170. package/tools/package.json +4 -0
  171. package/tools/seed-aws-accounts.ts +79 -0
  172. package/tools/seed-cf-accounts.ts +104 -0
  173. package/tools/tcp-ws-forwarder/package.json +1 -1
  174. package/AGENTS.md +0 -5
  175. package/MOTD.txt +0 -3
  176. package/PLAN-FIXES.md +0 -358
  177. package/PLAN.md +0 -420
  178. package/dashboards/cloudwatch-irc.json +0 -139
  179. package/docs/AWS-Adapter-Architecture.md +0 -494
  180. package/docs/AWS-Deployment.md +0 -1107
  181. package/docs/Cloudflare-Deployment-Guide.md +0 -660
  182. package/docs/Home.md +0 -1
  183. package/docs/Observability.md +0 -87
  184. package/docs/PlanIRCv3Websocket.md +0 -489
  185. package/docs/PlanWebClient.md +0 -451
  186. package/docs/Release-Process.md +0 -440
  187. package/progress.md +0 -107
  188. package/tickets.md +0 -2485
  189. package/webircgateway/LICENSE +0 -201
  190. package/webircgateway/Makefile +0 -44
  191. package/webircgateway/README.md +0 -134
  192. package/webircgateway/config.conf.example +0 -135
  193. package/webircgateway/go.mod +0 -16
  194. package/webircgateway/go.sum +0 -89
  195. package/webircgateway/main.go +0 -118
  196. package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
  197. package/webircgateway/pkg/identd/identd.go +0 -86
  198. package/webircgateway/pkg/identd/rpcclient.go +0 -59
  199. package/webircgateway/pkg/irc/isupport.go +0 -56
  200. package/webircgateway/pkg/irc/message.go +0 -217
  201. package/webircgateway/pkg/irc/state.go +0 -79
  202. package/webircgateway/pkg/proxy/proxy.go +0 -129
  203. package/webircgateway/pkg/proxy/server.go +0 -237
  204. package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
  205. package/webircgateway/pkg/webircgateway/client.go +0 -741
  206. package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
  207. package/webircgateway/pkg/webircgateway/config.go +0 -385
  208. package/webircgateway/pkg/webircgateway/gateway.go +0 -278
  209. package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
  210. package/webircgateway/pkg/webircgateway/hooks.go +0 -152
  211. package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
  212. package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
  213. package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
  214. package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
  215. package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
  216. package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
  217. package/webircgateway/pkg/webircgateway/utils.go +0 -147
  218. package/webircgateway/plugins/example/plugin.go +0 -11
  219. package/webircgateway/plugins/stats/plugin.go +0 -52
  220. package/webircgateway/staticcheck.conf +0 -1
  221. package/webircgateway/webircgateway.svg +0 -3
@@ -1,13 +1,22 @@
1
1
  /**
2
2
  * Reusable IRC scenario runner — exported so adapter packages (cf-adapter,
3
3
  * aws-stack) can register their own {@link IrcHarnessFactory} and re-run
4
- * the exact same scenario suite under their native runtime.
4
+ * the exact same scenario suite under their native runtime AND transport.
5
5
  *
6
6
  * Each scenario is written once against the {@link IrcHarness} seam. The
7
7
  * runner takes a list of factories and produces one `describe` block per
8
8
  * factory, mirroring the parametrized matrix called for in PLAN §8
9
9
  * ("Runtime contract: vitest parametrized over IrcRuntime. Same scenarios
10
- * pass in-memory + CF + AWS.").
10
+ * pass in-memory + CF + AWS.") extended to cover the transport dimension
11
+ * ("Same scenarios pass in-memory+WS, in-memory+TCP, CF+WS, CF+TCP,
12
+ * AWS+WS, AWS+TCP.").
13
+ *
14
+ * The general scenario block runs against every factory regardless of
15
+ * transport. The TCP-framing block (chunk boundaries, slow-loris partial
16
+ * sends, mixed line endings) runs ONLY against factories declaring
17
+ * `transport: 'tcp'` — those cases are meaningless (and behave
18
+ * differently) under the WebSocket text-frame transport where every
19
+ * `feed()` is a complete unit.
11
20
  */
12
21
 
13
22
  import { describe, expect, it } from 'vitest';
@@ -491,5 +500,126 @@ export function runIrcScenarios(factories: readonly IrcHarnessFactory[]): void {
491
500
  await harness.close();
492
501
  });
493
502
  });
503
+
504
+ // -------------------------------------------------------------------------
505
+ // TCP-framing scenarios.
506
+ //
507
+ // These cases exercise the stateful `\r\n` byte-stream transport
508
+ // (PLAN §9, ADR-009): chunk boundaries, partial-line buffering across
509
+ // chunks, slow-loris-style one-byte sends, bare `\n` tolerance, and
510
+ // multi-message chunks. They are meaningless under the WebSocket
511
+ // text-frame transport (where every feed() is a complete unit), so
512
+ // they only run against factories declaring `transport: 'tcp'`.
513
+ // -------------------------------------------------------------------------
514
+ if (factory.transport === 'tcp') {
515
+ describe(`TCP framing — ${factory.name}`, () => {
516
+ let harness: IrcHarness;
517
+
518
+ it('tcp 01: message split across two chunks produces one command', async () => {
519
+ harness = await factory.create();
520
+ const c = await harness.spawnClient({ nick: 'alice' });
521
+ await c.waitForNumeric('001');
522
+ c.received.length = 0;
523
+ // Feed the first half — incomplete, must buffer without dispatching.
524
+ await c.feed('PI');
525
+ // Nothing has terminated yet: no PONG, no 421.
526
+ expect(c.received.length).toBe(0);
527
+ await c.feed('NG :split\r\n');
528
+ await c.waitForLine((l) => l === 'PONG :split');
529
+ await harness.close();
530
+ });
531
+
532
+ it('tcp 02: partial line buffered across many one-byte chunks (slow-loris style)', async () => {
533
+ harness = await factory.create();
534
+ const c = await harness.spawnClient({ nick: 'alice' });
535
+ await c.waitForNumeric('001');
536
+ c.received.length = 0;
537
+ const line = 'PING :slow';
538
+ for (const ch of line) {
539
+ await c.feed(ch);
540
+ // No terminator yet — nothing dispatches.
541
+ expect(c.received.length).toBe(0);
542
+ }
543
+ await c.feed('\r\n');
544
+ await c.waitForLine((l) => l === 'PONG :slow');
545
+ await harness.close();
546
+ });
547
+
548
+ it('tcp 03: bare LF line ending is tolerated (legacy/mixed clients)', async () => {
549
+ harness = await factory.create();
550
+ const c = await harness.spawnClient({ nick: 'alice' });
551
+ await c.waitForNumeric('001');
552
+ c.received.length = 0;
553
+ await c.feed('PING :bare\n');
554
+ await c.waitForLine((l) => l === 'PONG :bare');
555
+ await harness.close();
556
+ });
557
+
558
+ it('tcp 04: multiple CRLF-terminated messages in one chunk dispatch in order', async () => {
559
+ harness = await factory.create();
560
+ const c = await harness.spawnClient({ nick: 'alice' });
561
+ await c.waitForNumeric('001');
562
+ c.received.length = 0;
563
+ await c.feed('PING :one\r\nPING :two\r\nPING :three\r\n');
564
+ await c.waitForLine((l) => l === 'PONG :three');
565
+ const pongs = c.received.filter((l) => l.startsWith('PONG :'));
566
+ expect(pongs).toEqual(['PONG :one', 'PONG :two', 'PONG :three']);
567
+ await harness.close();
568
+ });
569
+
570
+ it('tcp 05: mixed CRLF and LF terminators in one chunk both frame', async () => {
571
+ harness = await factory.create();
572
+ const c = await harness.spawnClient({ nick: 'alice' });
573
+ await c.waitForNumeric('001');
574
+ c.received.length = 0;
575
+ await c.feed('PING :crlf\r\nPING :lf\n');
576
+ await c.waitForLine((l) => l === 'PONG :lf');
577
+ const toks = c.received
578
+ .filter((l) => l.startsWith('PONG :'))
579
+ .map((l) => l.slice('PONG :'.length));
580
+ expect(toks).toEqual(['crlf', 'lf']);
581
+ await harness.close();
582
+ });
583
+
584
+ it('tcp 06: CRLF terminator split across chunks (CR in one, LF in next)', async () => {
585
+ harness = await factory.create();
586
+ const c = await harness.spawnClient({ nick: 'alice' });
587
+ await c.waitForNumeric('001');
588
+ c.received.length = 0;
589
+ await c.feed('PING :cr\r');
590
+ expect(c.received.length).toBe(0);
591
+ await c.feed('\n');
592
+ await c.waitForLine((l) => l === 'PONG :cr');
593
+ await harness.close();
594
+ });
595
+
596
+ it('tcp 07: an empty chunk is a no-op (no dispatch, no crash)', async () => {
597
+ harness = await factory.create();
598
+ const c = await harness.spawnClient({ nick: 'alice' });
599
+ await c.waitForNumeric('001');
600
+ c.received.length = 0;
601
+ await c.feed('');
602
+ expect(c.received.length).toBe(0);
603
+ // The connection is still usable after the empty chunk.
604
+ await c.feed('PING :after-empty\r\n');
605
+ await c.waitForLine((l) => l === 'PONG :after-empty');
606
+ await harness.close();
607
+ });
608
+
609
+ it('tcp 08: a complete line followed by a partial line buffers the tail', async () => {
610
+ harness = await factory.create();
611
+ const c = await harness.spawnClient({ nick: 'alice' });
612
+ await c.waitForNumeric('001');
613
+ c.received.length = 0;
614
+ await c.feed('PING :first\r\nPI');
615
+ await c.waitForLine((l) => l === 'PONG :first');
616
+ // The trailing partial 'PI' must not have dispatched yet.
617
+ expect(c.received.some((l) => l === 'PONG :')).toBe(false);
618
+ await c.feed('NG :second\r\n');
619
+ await c.waitForLine((l) => l === 'PONG :second');
620
+ await harness.close();
621
+ });
622
+ });
623
+ }
494
624
  }
495
625
  }
@@ -12,7 +12,8 @@ import { InMemoryHarness, inMemoryHarnessFactory } from '../src/index.js';
12
12
 
13
13
  describe('inMemoryHarnessFactory', () => {
14
14
  it('exposes a stable name for the describe.each matrix', () => {
15
- expect(inMemoryHarnessFactory.name).toBe('in-memory');
15
+ expect(inMemoryHarnessFactory.name).toBe('in-memory+ws');
16
+ expect(inMemoryHarnessFactory.transport).toBe('ws');
16
17
  });
17
18
 
18
19
  it('create() returns a fresh, independent InMemoryHarness each call', async () => {
@@ -1,19 +1,33 @@
1
1
  /**
2
- * Parametrized IRC scenarios — 032.
2
+ * Parametrized IRC scenarios — 032 + 057.
3
3
  *
4
4
  * The scenario bodies live in `src/scenarios.ts` (now re-exported as
5
5
  * `runIrcScenarios`) so adapter packages can register their own
6
6
  * {@link IrcHarnessFactory} against the exact same suite under their
7
7
  * native vitest configuration (CF runs in `workerd`, AWS runs in node
8
- * with dynamodb-local, etc.). This file is the in-memory entry point
9
- * it registers only {@link inMemoryHarnessFactory}.
8
+ * with dynamodb-local, etc.). This file is the in-memory entry point.
10
9
  *
11
- * Adding a runtime to the in-memory suite = appending one entry to the
12
- * `FACTORIES` array (acceptance criterion: "adding a new runtime
13
- * requires only registering a factory").
10
+ * The transport dimension extends the matrix: every scenario runs
11
+ * against (runtime × transport). The in-memory runtime
12
+ * registers both transports `inMemoryWsHarnessFactory` (WebSocket
13
+ * text-frame framing) and `inMemoryTcpHarnessFactory` (raw TCP+TLS
14
+ * byte-stream framing) — proving the same scenario suite is green across
15
+ * both transports on the reference runtime. CF and AWS append their own
16
+ * factories (per transport) to their package suites.
17
+ *
18
+ * Adding a runtime OR a transport to the in-memory suite = appending one
19
+ * entry to the `FACTORIES` array (acceptance criterion: "adding a new
20
+ * transport requires only registering a factory").
14
21
  */
15
22
 
16
- import { inMemoryHarnessFactory, runIrcScenarios } from '../src/index.js';
23
+ import {
24
+ inMemoryTcpHarnessFactory,
25
+ inMemoryWsHarnessFactory,
26
+ runIrcScenarios,
27
+ } from '../src/index.js';
17
28
 
18
- /** The runtime matrix. Adding a runtime = appending one entry here. */
19
- runIrcScenarios([inMemoryHarnessFactory]);
29
+ /**
30
+ * The (runtime × transport) matrix. Adding a runtime or transport =
31
+ * appending one entry here.
32
+ */
33
+ runIrcScenarios([inMemoryWsHarnessFactory, inMemoryTcpHarnessFactory]);
@@ -2,3 +2,12 @@ packages:
2
2
  - "packages/*"
3
3
  - "apps/*"
4
4
  - "tools/*"
5
+ linkWorkspacePackages: true
6
+ allowBuilds:
7
+ '@biomejs/biome': true
8
+ cpu-features: true
9
+ esbuild: true
10
+ protobufjs: true
11
+ sharp: true
12
+ ssh2: true
13
+ workerd: true
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/ci-hardening",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "private": true,
5
5
  "description": "CI hardening helpers: coverage-gate config validator + mutation-testing driver for irc-core",
6
6
  "license": "BSD-3-Clause",
@@ -0,0 +1,4 @@
1
+ {
2
+ "private": true,
3
+ "type": "module"
4
+ }
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Seed SASL PLAIN accounts into the DynamoDB `Accounts` table.
4
+ *
5
+ * Usage:
6
+ * node --import tsx tools/seed-aws-accounts.ts \
7
+ * --table StagingAccounts \
8
+ * --endpoint http://localhost:8000 \
9
+ * --accounts alice:s3cret bob:password2
10
+ *
11
+ * Or read from a file (newline-delimited `username:password`):
12
+ * node --import tsx tools/seed-aws-accounts.ts \
13
+ * --table StagingAccounts \
14
+ * --file accounts.txt
15
+ *
16
+ * The script hashes each password with scrypt (never writes plaintext).
17
+ * Re-running with the same username overwrites the prior row.
18
+ */
19
+
20
+ import { readFileSync } from 'node:fs';
21
+ import { parseArgs } from 'node:util';
22
+ import { createDynamoDocumentClient, putAccountCredential } from '@serverless-ircd/aws-adapter';
23
+
24
+ const { values } = parseArgs({
25
+ options: {
26
+ table: { type: 'string' },
27
+ endpoint: { type: 'string', default: undefined },
28
+ region: { type: 'string', default: undefined },
29
+ accounts: { type: 'string', multiple: true, default: [] },
30
+ file: { type: 'string', default: undefined },
31
+ },
32
+ });
33
+
34
+ if (values.table === undefined) {
35
+ console.error(
36
+ 'Usage: seed-aws-accounts --table <TableName> [--endpoint <url>] [--accounts user:pass ...] [--file <path>]',
37
+ );
38
+ process.exit(1);
39
+ }
40
+
41
+ const clientOptions: Record<string, string> = {};
42
+ if (values.endpoint !== undefined) clientOptions.endpoint = values.endpoint;
43
+ if (values.region !== undefined) clientOptions.region = values.region;
44
+
45
+ const docClient = createDynamoDocumentClient(clientOptions);
46
+
47
+ const pairs: Array<{ username: string; password: string }> = [];
48
+
49
+ for (const pair of values.accounts) {
50
+ const sep = pair.indexOf(':');
51
+ if (sep <= 0) {
52
+ console.error(`Skipping malformed entry (no colon): ${pair}`);
53
+ continue;
54
+ }
55
+ pairs.push({ username: pair.slice(0, sep), password: pair.slice(sep + 1) });
56
+ }
57
+
58
+ if (values.file !== undefined) {
59
+ const content = readFileSync(values.file, 'utf-8');
60
+ for (const line of content.split('\n')) {
61
+ const trimmed = line.trim();
62
+ if (trimmed.length === 0) continue;
63
+ const sep = trimmed.indexOf(':');
64
+ if (sep <= 0) continue;
65
+ pairs.push({ username: trimmed.slice(0, sep), password: trimmed.slice(sep + 1) });
66
+ }
67
+ }
68
+
69
+ if (pairs.length === 0) {
70
+ console.error('No accounts to seed. Use --accounts or --file.');
71
+ process.exit(1);
72
+ }
73
+
74
+ for (const { username, password } of pairs) {
75
+ const entry = await putAccountCredential(docClient, values.table, username, password);
76
+ console.log(`Seeded ${username} (algorithm=${entry.algorithm})`);
77
+ }
78
+
79
+ console.log(`Done: ${pairs.length} account(s) written to ${values.table}.`);
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Seed SASL PLAIN accounts into a Cloudflare D1 `accounts` table.
4
+ *
5
+ * Usage:
6
+ * node --import tsx tools/seed-cf-accounts.ts \
7
+ * --database serverless-ircd-accounts-staging \
8
+ * --env staging \
9
+ * --remote \
10
+ * --accounts alice:s3cret bob:password2
11
+ *
12
+ * Or read from a file (newline-delimited `username:password`):
13
+ * node --import tsx tools/seed-cf-accounts.ts \
14
+ * --database serverless-ircd-accounts-staging \
15
+ * --env staging \
16
+ * --remote \
17
+ * --file accounts.txt
18
+ *
19
+ * The script hashes each password with scrypt (never writes plaintext) and
20
+ * executes the resulting `INSERT OR REPLACE` via `wrangler d1 execute`.
21
+ * Re-running with the same username overwrites the prior row. The `accounts`
22
+ * table is created (IF NOT EXISTS) on the first run.
23
+ *
24
+ * Flags:
25
+ * --database D1 database name (as declared in wrangler.toml). Required.
26
+ * --env Wrangler environment (e.g. `staging`). Optional.
27
+ * --remote Target the remote D1 (default: local preview).
28
+ * --local Target local D1 (explicit; default).
29
+ * --accounts One or more `username:password` pairs.
30
+ * --file Path to a newline-delimited `username:password` file.
31
+ */
32
+
33
+ import { execFileSync } from 'node:child_process';
34
+ import { readFileSync } from 'node:fs';
35
+ import { parseArgs } from 'node:util';
36
+ import { CREATE_ACCOUNTS_TABLE_SQL, buildAccountPutStatement } from '@serverless-ircd/cf-adapter';
37
+
38
+ const { values } = parseArgs({
39
+ options: {
40
+ database: { type: 'string' },
41
+ env: { type: 'string', default: undefined },
42
+ remote: { type: 'boolean', default: false },
43
+ local: { type: 'boolean', default: false },
44
+ accounts: { type: 'string', multiple: true, default: [] },
45
+ file: { type: 'string', default: undefined },
46
+ },
47
+ });
48
+
49
+ if (values.database === undefined) {
50
+ console.error(
51
+ 'Usage: seed-cf-accounts --database <d1-name> [--env staging] [--remote] [--accounts user:pass ...] [--file <path>]',
52
+ );
53
+ process.exit(1);
54
+ }
55
+
56
+ const pairs: Array<{ username: string; password: string }> = [];
57
+
58
+ for (const pair of values.accounts) {
59
+ const sep = pair.indexOf(':');
60
+ if (sep <= 0) {
61
+ console.error(`Skipping malformed entry (no colon): ${pair}`);
62
+ continue;
63
+ }
64
+ pairs.push({ username: pair.slice(0, sep), password: pair.slice(sep + 1) });
65
+ }
66
+
67
+ if (values.file !== undefined) {
68
+ const content = readFileSync(values.file, 'utf-8');
69
+ for (const line of content.split('\n')) {
70
+ const trimmed = line.trim();
71
+ if (trimmed.length === 0) continue;
72
+ const sep = trimmed.indexOf(':');
73
+ if (sep <= 0) continue;
74
+ pairs.push({ username: trimmed.slice(0, sep), password: trimmed.slice(sep + 1) });
75
+ }
76
+ }
77
+
78
+ if (pairs.length === 0) {
79
+ console.error('No accounts to seed. Use --accounts or --file.');
80
+ process.exit(1);
81
+ }
82
+
83
+ /** Runs `wrangler d1 execute` with the supplied SQL command. */
84
+ function runD1Execute(sql: string): void {
85
+ const args = ['d1', 'execute', values.database as string, '--command', sql];
86
+ if (values.env !== undefined) {
87
+ args.push('--env', values.env);
88
+ }
89
+ if (values.remote) {
90
+ args.push('--remote');
91
+ }
92
+ execFileSync('npx', ['wrangler', ...args], { stdio: 'inherit' });
93
+ }
94
+
95
+ // Ensure the table exists (idempotent).
96
+ runD1Execute(CREATE_ACCOUNTS_TABLE_SQL);
97
+
98
+ for (const { username, password } of pairs) {
99
+ const { entry, sql } = buildAccountPutStatement(username, password);
100
+ runD1Execute(sql);
101
+ console.log(`Seeded ${username} (algorithm=${entry.algorithm})`);
102
+ }
103
+
104
+ console.log(`Done: ${pairs.length} account(s) written to D1 database ${values.database}.`);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverless-ircd/tcp-ws-forwarder",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "private": true,
5
5
  "description": "Local TCP to ws/wss forwarder so standard IRC clients can reach the WebSocket-only ServerlessIRCd transport",
6
6
  "license": "BSD-3-Clause",
package/AGENTS.md DELETED
@@ -1,5 +0,0 @@
1
- # AGENTS.md
2
-
3
- ## Code Style
4
-
5
- - **Do not reference ticket IDs in source files.** Never put `TICKET-###` or similar ticket-tracker tokens in `.ts` or `.js` files (code or comments). Describe the intent or cross-reference by feature/command name instead. Ticket tracking lives in `progress.md` / `tickets.md`, not in source.
package/MOTD.txt DELETED
@@ -1,3 +0,0 @@
1
- Welcome to the test network for Serverless IRCd project.
2
-
3
- Join #general for chat.