serverless-ircd 0.3.0 → 0.5.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 (223) 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 +349 -18
  8. package/README.md +132 -30
  9. package/apps/aws-stack/README.md +6 -5
  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 +150 -10
  13. package/apps/aws-stack/tests/stack.test.ts +145 -4
  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/wrangler.test.toml +6 -0
  34. package/apps/cf-worker/wrangler.toml +28 -2
  35. package/apps/local-cli/package.json +1 -1
  36. package/apps/local-cli/src/config-loader.ts +10 -0
  37. package/apps/local-cli/src/server.ts +149 -24
  38. package/apps/local-cli/tests/e2e.test.ts +1 -1
  39. package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -0
  40. package/package.json +14 -10
  41. package/packages/aws-adapter/package.json +3 -3
  42. package/packages/aws-adapter/src/account-store.ts +1 -1
  43. package/packages/aws-adapter/src/admission.ts +74 -0
  44. package/packages/aws-adapter/src/aws-runtime.ts +75 -4
  45. package/packages/aws-adapter/src/config-loader.ts +32 -0
  46. package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
  47. package/packages/aws-adapter/src/handlers/connect.ts +96 -9
  48. package/packages/aws-adapter/src/handlers/default.ts +98 -7
  49. package/packages/aws-adapter/src/handlers/index.ts +109 -4
  50. package/packages/aws-adapter/src/handlers/nlb-stream.ts +490 -0
  51. package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
  52. package/packages/aws-adapter/src/index.ts +8 -0
  53. package/packages/aws-adapter/src/serialize.ts +11 -1
  54. package/packages/aws-adapter/src/stats.ts +80 -0
  55. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
  56. package/packages/aws-adapter/tests/account-store.test.ts +19 -20
  57. package/packages/aws-adapter/tests/admission.test.ts +70 -0
  58. package/packages/aws-adapter/tests/aws-harness.ts +13 -1
  59. package/packages/aws-adapter/tests/aws-integration.test.ts +1 -1
  60. package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
  61. package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
  62. package/packages/aws-adapter/tests/connect.test.ts +174 -0
  63. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
  64. package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
  65. package/packages/aws-adapter/tests/handlers.test.ts +302 -53
  66. package/packages/aws-adapter/tests/nlb-stream.test.ts +480 -0
  67. package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
  68. package/packages/aws-adapter/tests/stats.test.ts +317 -0
  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 +5 -1
  72. package/packages/cf-adapter/src/cf-runtime.ts +68 -5
  73. package/packages/cf-adapter/src/channel-do.ts +2 -2
  74. package/packages/cf-adapter/src/config-loader.ts +33 -0
  75. package/packages/cf-adapter/src/connection-do.ts +278 -85
  76. package/packages/cf-adapter/src/d1-account-store.ts +198 -0
  77. package/packages/cf-adapter/src/env.ts +54 -11
  78. package/packages/cf-adapter/src/index.ts +11 -8
  79. package/packages/cf-adapter/src/registry-do.ts +22 -3
  80. package/packages/cf-adapter/src/sharding.ts +1 -2
  81. package/packages/cf-adapter/src/stats.ts +65 -0
  82. package/packages/cf-adapter/tests/cf-harness.ts +12 -2
  83. package/packages/cf-adapter/tests/cf-integration.test.ts +6 -5
  84. package/packages/cf-adapter/tests/cf-runtime.test.ts +38 -2
  85. package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
  86. package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
  87. package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
  88. package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
  89. package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
  90. package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
  91. package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
  92. package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
  93. package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
  94. package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
  95. package/packages/cf-adapter/tests/registry-do.test.ts +4 -4
  96. package/packages/cf-adapter/tests/sharding.test.ts +1 -1
  97. package/packages/cf-adapter/tests/stats.test.ts +120 -0
  98. package/packages/cf-adapter/tests/worker/main.ts +15 -8
  99. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
  100. package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
  101. package/packages/cf-adapter/wrangler.test.toml +15 -0
  102. package/packages/in-memory-runtime/package.json +1 -1
  103. package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
  104. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
  105. package/packages/irc-core/package.json +6 -1
  106. package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
  107. package/packages/irc-core/src/admission.ts +16 -15
  108. package/packages/irc-core/src/caps/capabilities.ts +24 -3
  109. package/packages/irc-core/src/cloak.ts +1 -1
  110. package/packages/irc-core/src/commands/cap.ts +8 -1
  111. package/packages/irc-core/src/commands/index.ts +17 -0
  112. package/packages/irc-core/src/commands/invite.ts +2 -4
  113. package/packages/irc-core/src/commands/ison.ts +61 -0
  114. package/packages/irc-core/src/commands/isupport.ts +6 -2
  115. package/packages/irc-core/src/commands/kick.ts +2 -4
  116. package/packages/irc-core/src/commands/kill.ts +127 -0
  117. package/packages/irc-core/src/commands/list.ts +1 -1
  118. package/packages/irc-core/src/commands/lusers.ts +204 -0
  119. package/packages/irc-core/src/commands/mode.ts +4 -8
  120. package/packages/irc-core/src/commands/names.ts +3 -5
  121. package/packages/irc-core/src/commands/part.ts +2 -4
  122. package/packages/irc-core/src/commands/quit.ts +12 -0
  123. package/packages/irc-core/src/commands/registration.ts +18 -12
  124. package/packages/irc-core/src/commands/rehash.ts +119 -0
  125. package/packages/irc-core/src/commands/sasl.ts +72 -9
  126. package/packages/irc-core/src/commands/server-info.ts +129 -0
  127. package/packages/irc-core/src/commands/setname.ts +109 -0
  128. package/packages/irc-core/src/commands/stats.ts +152 -0
  129. package/packages/irc-core/src/commands/topic.ts +2 -4
  130. package/packages/irc-core/src/commands/trace.ts +137 -0
  131. package/packages/irc-core/src/commands/userhost.ts +84 -0
  132. package/packages/irc-core/src/commands/wallops.ts +118 -0
  133. package/packages/irc-core/src/commands/whowas.ts +113 -0
  134. package/packages/irc-core/src/config.ts +65 -0
  135. package/packages/irc-core/src/credential-hashing.ts +124 -0
  136. package/packages/irc-core/src/effects.ts +33 -30
  137. package/packages/irc-core/src/index.ts +3 -0
  138. package/packages/irc-core/src/ports.ts +360 -12
  139. package/packages/irc-core/src/protocol/numerics.ts +48 -11
  140. package/packages/irc-core/src/protocol/outbound.ts +20 -3
  141. package/packages/irc-core/src/types.ts +46 -2
  142. package/packages/irc-core/src/ws-framing.ts +132 -0
  143. package/packages/irc-core/src/ws-subprotocol.ts +66 -0
  144. package/packages/irc-core/tests/account-store.test.ts +45 -2
  145. package/packages/irc-core/tests/admission.test.ts +18 -0
  146. package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
  147. package/packages/irc-core/tests/commands/cap.test.ts +33 -1
  148. package/packages/irc-core/tests/commands/ison.test.ts +166 -0
  149. package/packages/irc-core/tests/commands/kill.test.ts +243 -0
  150. package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
  151. package/packages/irc-core/tests/commands/mode.test.ts +57 -0
  152. package/packages/irc-core/tests/commands/quit.test.ts +69 -2
  153. package/packages/irc-core/tests/commands/registration.test.ts +151 -6
  154. package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
  155. package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
  156. package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
  157. package/packages/irc-core/tests/commands/setname.test.ts +225 -0
  158. package/packages/irc-core/tests/commands/stats.test.ts +294 -0
  159. package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
  160. package/packages/irc-core/tests/commands/trace.test.ts +282 -0
  161. package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
  162. package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
  163. package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
  164. package/packages/irc-core/tests/config.test.ts +95 -1
  165. package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
  166. package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
  167. package/packages/irc-core/tests/effects.test.ts +14 -27
  168. package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
  169. package/packages/irc-core/tests/numerics.test.ts +102 -0
  170. package/packages/irc-core/tests/outbound.test.ts +51 -0
  171. package/packages/irc-core/tests/ports.test.ts +22 -0
  172. package/packages/irc-core/tests/raw-modules.d.ts +11 -0
  173. package/packages/irc-core/tests/stats-store.test.ts +222 -0
  174. package/packages/irc-core/tests/types.test.ts +35 -1
  175. package/packages/irc-core/tests/ws-framing.test.ts +213 -0
  176. package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
  177. package/packages/irc-core/tsconfig.build.json +1 -1
  178. package/packages/irc-core/tsconfig.test.json +1 -1
  179. package/packages/irc-server/package.json +1 -1
  180. package/packages/irc-server/src/actor.ts +393 -16
  181. package/packages/irc-server/src/dispatch.ts +1 -3
  182. package/packages/irc-server/src/index.ts +10 -2
  183. package/packages/irc-server/src/routing.ts +15 -0
  184. package/packages/irc-server/src/runtime.ts +31 -0
  185. package/packages/irc-server/src/transport.ts +104 -0
  186. package/packages/irc-server/tests/actor.test.ts +1489 -4
  187. package/packages/irc-server/tests/dispatch.test.ts +37 -17
  188. package/packages/irc-server/tests/raw-modules.d.ts +11 -0
  189. package/packages/irc-server/tests/routing.test.ts +5 -0
  190. package/packages/irc-server/tests/runtime.test.ts +7 -0
  191. package/packages/irc-server/tests/transport.test.ts +230 -0
  192. package/packages/irc-test-support/package.json +1 -1
  193. package/packages/irc-test-support/src/harness.ts +44 -9
  194. package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
  195. package/packages/irc-test-support/src/index.ts +3 -0
  196. package/packages/irc-test-support/src/scenarios.ts +141 -3
  197. package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
  198. package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
  199. package/pnpm-workspace.yaml +10 -1
  200. package/tools/ci-hardening/package.json +1 -1
  201. package/tools/package.json +9 -0
  202. package/tools/seed-aws-accounts.ts +5 -6
  203. package/tools/seed-cf-accounts.ts +107 -0
  204. package/tools/tcp-ws-forwarder/package.json +1 -1
  205. package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
  206. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
  207. package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
  208. package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
  209. package/docs/ADR-002-location-of-authority.md +0 -82
  210. package/docs/ADR-003-durable-object-sharding.md +0 -93
  211. package/docs/ADR-004-dynamodb-schema.md +0 -96
  212. package/docs/ADR-005-wss-only-transport-v1.md +0 -83
  213. package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
  214. package/docs/ADR-007-deterministic-ports.md +0 -82
  215. package/docs/ADR-008-monorepo-tooling.md +0 -60
  216. package/docs/AWS-Adapter-Architecture.md +0 -496
  217. package/docs/AWS-Deployment.md +0 -1186
  218. package/docs/Cloudflare-Deployment-Guide.md +0 -660
  219. package/docs/Home.md +0 -11
  220. package/docs/Observability.md +0 -87
  221. package/docs/PlanIRCv3Websocket.md +0 -489
  222. package/docs/PlanWebClient.md +0 -451
  223. package/docs/Release-Process.md +0 -443
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Synth-time assertions for the `apps/aws-stack` CDK stack.
3
3
  *
4
- * These are the primary red→green tests for the infrastructure ticket:
4
+ * These are the primary red→green tests for the infrastructure stack:
5
5
  * they assert the exact CloudFormation shape that `cdk synth` must
6
6
  * produce — five DynamoDB tables with the PLAN §4 schema, a single
7
- * Node 20 Lambda wired to all three WebSocket routes, the API Gateway
7
+ * Node 24 Lambda wired to all three WebSocket routes, the API Gateway
8
8
  * v2 WebSocket API, the two stack outputs, and a least-privilege guard
9
9
  * that no IAM policy grants `Resource: '*'` or `Action: '*'`.
10
10
  *
@@ -149,6 +149,52 @@ describe('IrcAwsStack — API Gateway v2 WebSocket API', () => {
149
149
  const { template } = makeTemplate();
150
150
  template.resourceCountIs('AWS::ApiGatewayV2::Route', 3);
151
151
  });
152
+
153
+ it('enables the $connect route to return a response (route response selection)', () => {
154
+ const { template } = makeTemplate();
155
+ template.hasResourceProperties('AWS::ApiGatewayV2::Route', {
156
+ RouteKey: '$connect',
157
+ RouteResponseSelectionExpression: '$default',
158
+ });
159
+ });
160
+
161
+ it('creates a $connect route response so the upgrade can carry a response', () => {
162
+ const { template } = makeTemplate();
163
+ template.hasResourceProperties('AWS::ApiGatewayV2::RouteResponse', {
164
+ RouteResponseKey: '$default',
165
+ });
166
+ });
167
+ });
168
+
169
+ describe('IrcAwsStack — IRCv3 WebSocket subprotocol echo', () => {
170
+ it('attaches an integration response on the $connect route mapping Sec-WebSocket-Protocol', () => {
171
+ const { template } = makeTemplate();
172
+ template.hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', {
173
+ ResponseParameters: {
174
+ 'method.response.header.Sec-WebSocket-Protocol':
175
+ 'integration.response.header.Sec-WebSocket-Protocol',
176
+ },
177
+ });
178
+ });
179
+
180
+ it('binds the subprotocol integration response to the $connect integration', () => {
181
+ const { template, stack } = makeTemplate();
182
+ const responses = template.findResources('AWS::ApiGatewayV2::IntegrationResponse');
183
+ // Exactly one integration response is configured (the $connect subprotocol echo).
184
+ expect(Object.keys(responses)).toHaveLength(1);
185
+ // The IntegrationId Ref must resolve to a CfnIntegration under the $connect route.
186
+ const stackName = stack.stackName;
187
+ void stackName;
188
+ const cfnIntegrations = template.findResources('AWS::ApiGatewayV2::Integration');
189
+ expect(Object.keys(cfnIntegrations).length).toBeGreaterThanOrEqual(3);
190
+ });
191
+
192
+ it('uses the $default integration response key', () => {
193
+ const { template } = makeTemplate();
194
+ template.hasResourceProperties('AWS::ApiGatewayV2::IntegrationResponse', {
195
+ IntegrationResponseKey: '$default',
196
+ });
197
+ });
152
198
  });
153
199
 
154
200
  describe('IrcAwsStack — Lambda', () => {
@@ -157,10 +203,10 @@ describe('IrcAwsStack — Lambda', () => {
157
203
  template.resourceCountIs('AWS::Lambda::Function', 3);
158
204
  });
159
205
 
160
- it('targets the Node 20 runtime for every function', () => {
206
+ it('targets the Node 24 runtime for every function', () => {
161
207
  const { template } = makeTemplate();
162
208
  template.allResourcesProperties('AWS::Lambda::Function', {
163
- Runtime: 'nodejs20.x',
209
+ Runtime: 'nodejs24.x',
164
210
  });
165
211
  });
166
212
  });
@@ -462,3 +508,98 @@ describe('IrcAwsStack — server identity props (configurable deploy identity)',
462
508
  ).toBeUndefined();
463
509
  });
464
510
  });
511
+
512
+ describe('IrcAwsStack — TCP+TLS NLB adapter', () => {
513
+ /** Makes a template WITH the tcpTlsDomainName prop (enables the NLB path). */
514
+ function makeTcpTemplate(domainName = 'irc.example.com'): {
515
+ app: App;
516
+ stack: Stack;
517
+ template: Template;
518
+ } {
519
+ const app = new App();
520
+ const stack = new IrcAwsStack(app, 'TestStack', {
521
+ environmentName: 'staging',
522
+ tcpTlsDomainName: domainName,
523
+ });
524
+ const template = Template.fromStack(stack);
525
+ return { app, stack, template };
526
+ }
527
+
528
+ describe('when tcpTlsDomainName is omitted (default)', () => {
529
+ it('creates no Network Load Balancer', () => {
530
+ const { template } = makeTemplate();
531
+ template.resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 0);
532
+ });
533
+
534
+ it('creates no ACM certificate', () => {
535
+ const { template } = makeTemplate();
536
+ template.resourceCountIs('AWS::CertificateManager::Certificate', 0);
537
+ });
538
+
539
+ it('still creates exactly three Lambda functions', () => {
540
+ const { template } = makeTemplate();
541
+ template.resourceCountIs('AWS::Lambda::Function', 3);
542
+ });
543
+ });
544
+
545
+ describe('when tcpTlsDomainName is provided', () => {
546
+ it('creates a Network Load Balancer', () => {
547
+ const { template } = makeTcpTemplate();
548
+ template.resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1);
549
+ });
550
+
551
+ it('creates an ACM certificate for the provided domain', () => {
552
+ const { template } = makeTcpTemplate('irc.my.net');
553
+ template.hasResourceProperties('AWS::CertificateManager::Certificate', {
554
+ DomainName: 'irc.my.net',
555
+ });
556
+ });
557
+
558
+ it('creates a TLS listener on port 6697', () => {
559
+ const { template } = makeTcpTemplate();
560
+ template.hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', {
561
+ Port: 6697,
562
+ Protocol: 'TLS',
563
+ });
564
+ });
565
+
566
+ it('creates a target group with Lambda as the target type', () => {
567
+ const { template } = makeTcpTemplate();
568
+ template.hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', {
569
+ TargetType: 'lambda',
570
+ });
571
+ });
572
+
573
+ it('creates a fourth Lambda function for the NLB streaming handler', () => {
574
+ const { template } = makeTcpTemplate();
575
+ template.resourceCountIs('AWS::Lambda::Function', 4);
576
+ });
577
+
578
+ it('targets the Node 24 runtime for the NLB handler Lambda', () => {
579
+ const { template } = makeTcpTemplate();
580
+ template.allResourcesProperties('AWS::Lambda::Function', {
581
+ Runtime: 'nodejs24.x',
582
+ });
583
+ });
584
+
585
+ it('grants the NLB permission to invoke the handler Lambda', () => {
586
+ const { template } = makeTcpTemplate();
587
+ template.hasResourceProperties('AWS::Lambda::Permission', {
588
+ Principal: 'elasticloadbalancing.amazonaws.com',
589
+ });
590
+ });
591
+
592
+ it('still creates the wss WebSocket API (both transports coexist)', () => {
593
+ const { template } = makeTcpTemplate();
594
+ template.hasResourceProperties('AWS::ApiGatewayV2::Api', {
595
+ ProtocolType: 'WEBSOCKET',
596
+ });
597
+ });
598
+
599
+ it('emits a TcpConnectUrl output pointing at the NLB DNS', () => {
600
+ const { template } = makeTcpTemplate();
601
+ const outputs = template.toJSON().Outputs ?? {};
602
+ expect(outputs).toHaveProperty('TcpConnectUrl');
603
+ });
604
+ });
605
+ });
@@ -0,0 +1,69 @@
1
+ # Cloudflare Container / Docker image for the ServerlessIRCd TCP origin.
2
+ #
3
+ # Spectrum terminates TLS at the edge and forwards plaintext TCP to this
4
+ # container. The container runs the IRC core (ConnectionActor +
5
+ # InMemoryRuntime) over the TcpByteStreamTransport seam on port 6667.
6
+ #
7
+ # Build context: the repository root (monorepo). The container needs the
8
+ # irc-core, irc-server, and in-memory-runtime packages.
9
+ #
10
+ # docker build -f apps/cf-tcp-container/Dockerfile -t sirc-tcp-origin .
11
+ # docker run -p 6667:6667 -e SERVER_NAME=irc.example.com sirc-tcp-origin
12
+ #
13
+ # For Cloudflare Containers deployment, the same image is referenced from
14
+ # wrangler.toml.
15
+
16
+ FROM node:24-slim AS builder
17
+
18
+ WORKDIR /app
19
+
20
+ # Install pnpm.
21
+ RUN corepack enable && corepack prepare pnpm@11.17.0 --activate
22
+
23
+ # Copy workspace config + lockfile first for layer caching.
24
+ COPY package.json pnpm-workspace.yaml pnpm-lock.yaml ./
25
+ COPY tsconfig.base.json ./
26
+
27
+ # Copy all workspace packages needed for the build.
28
+ COPY packages/irc-core/ packages/irc-core/
29
+ COPY packages/irc-server/ packages/irc-server/
30
+ COPY packages/in-memory-runtime/ packages/in-memory-runtime/
31
+ COPY apps/cf-tcp-container/ apps/cf-tcp-container/
32
+
33
+ # Install dependencies and build.
34
+ RUN pnpm install --frozen-lockfile
35
+ RUN pnpm --filter '@serverless-ircd/irc-core' build && \
36
+ pnpm --filter '@serverless-ircd/irc-server' build && \
37
+ pnpm --filter '@serverless-ircd/in-memory-runtime' build && \
38
+ pnpm --filter '@serverless-ircd/cf-tcp-container' build
39
+
40
+ # --- Runtime stage ---
41
+ FROM node:24-slim AS runtime
42
+
43
+ WORKDIR /app
44
+
45
+ # Copy built artifacts + package manifests (for workspace resolution).
46
+ COPY --from=builder /app/package.json /app/pnpm-workspace.yaml ./
47
+ COPY --from=builder /app/packages/irc-core/package.json packages/irc-core/
48
+ COPY --from=builder /app/packages/irc-core/dist/ packages/irc-core/dist/
49
+ COPY --from=builder /app/packages/irc-server/package.json packages/irc-server/
50
+ COPY --from=builder /app/packages/irc-server/dist/ packages/irc-server/dist/
51
+ COPY --from=builder /app/packages/in-memory-runtime/package.json packages/in-memory-runtime/
52
+ COPY --from=builder /app/packages/in-memory-runtime/dist/ packages/in-memory-runtime/dist/
53
+ COPY --from=builder /app/apps/cf-tcp-container/package.json apps/cf-tcp-container/
54
+ COPY --from=builder /app/apps/cf-tcp-container/dist/ apps/cf-tcp-container/dist/
55
+
56
+ # Production install (only the workspace deps, no devDependencies).
57
+ RUN corepack enable && corepack prepare pnpm@11.17.0 --activate && \
58
+ pnpm install --frozen-lockfile --prod
59
+
60
+ # Persistence volume: Spectrum-origin channel state survives restarts.
61
+ VOLUME ["/data"]
62
+
63
+ EXPOSE 6667
64
+
65
+ ENV TCP_PORT=6667
66
+ ENV TCP_HOST=0.0.0.0
67
+ ENV PERSISTENCE_PATH=/data/state.json
68
+
69
+ CMD ["node", "--enable-source-maps", "apps/cf-tcp-container/dist/main.js"]
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@serverless-ircd/cf-tcp-container",
3
+ "version": "0.5.0",
4
+ "private": true,
5
+ "description": "Cloudflare Container TCP origin for the irc+tls :6697 transport — Spectrum terminates TLS at the edge, this container runs the IRC core over plaintext TCP",
6
+ "license": "BSD-3-Clause",
7
+ "type": "module",
8
+ "main": "./dist/main.js",
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.build.json",
11
+ "typecheck": "tsc -p tsconfig.test.json --noEmit",
12
+ "start": "node --enable-source-maps ./dist/main.js",
13
+ "deploy:staging": "wrangler deploy --env staging",
14
+ "deploy:prod": "wrangler deploy",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "coverage": "vitest run --coverage",
18
+ "clean": "rimraf dist coverage .tsbuildinfo .turbo"
19
+ },
20
+ "dependencies": {
21
+ "@serverless-ircd/in-memory-runtime": "workspace:*",
22
+ "@serverless-ircd/irc-core": "workspace:*",
23
+ "@serverless-ircd/irc-server": "workspace:*"
24
+ },
25
+ "devDependencies": {
26
+ "@cloudflare/containers": "^0.1.0",
27
+ "@types/node": "^26.1.1",
28
+ "@vitest/coverage-v8": "^4.1.0",
29
+ "rimraf": "^6.0.0",
30
+ "typescript": "^5.6.0",
31
+ "vitest": "^4.1.0",
32
+ "wrangler": "4.112.0"
33
+ }
34
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * CF TCP container config loader.
3
+ *
4
+ * Adapts platform-native env vars (Cloudflare Container runtime env or
5
+ * Docker/Kubernetes env) into the shared `ServerConfigSchema` from
6
+ * `irc-core`, plus a handful of container-specific knobs (TCP port/host,
7
+ * persistence path, snapshot interval).
8
+ *
9
+ * Mirrors `packages/cf-adapter/src/config-loader.ts` in shape — both
10
+ * adapters consume the same `ServerConfigSchema`; only the env-mapping
11
+ * and the extra container knobs differ.
12
+ */
13
+
14
+ import { type ParsedServerConfig, parseServerConfig } from '@serverless-ircd/irc-core';
15
+
16
+ /**
17
+ * The env vars this loader reads. Every field is a string (the platform
18
+ * env shape); numeric fields are coerced inside {@link loadContainerConfig}.
19
+ */
20
+ export interface ContainerConfigEnv {
21
+ SERVER_NAME?: string;
22
+ NETWORK_NAME?: string;
23
+ SERVER_VERSION?: string;
24
+ CREATED_AT?: string;
25
+ MOTD_LINES?: string;
26
+ MAX_CLIENTS?: string;
27
+ CHANNEL_PREFIXES?: string;
28
+ OPER_USER?: string;
29
+ OPER_PASSWORD?: string;
30
+ MAX_CHANNELS_PER_USER?: string;
31
+ MAX_TARGETS_PER_COMMAND?: string;
32
+ NICK_LEN?: string;
33
+ CHANNEL_LEN?: string;
34
+ TOPIC_LEN?: string;
35
+ MAX_LIST_ENTRIES?: string;
36
+ QUIT_MESSAGE?: string;
37
+ SASL_ACCOUNTS?: string;
38
+ /** Container-specific: plaintext TCP port Spectrum forwards to. */
39
+ TCP_PORT?: string;
40
+ /** Container-specific: bind address (default 0.0.0.0 for Spectrum origin). */
41
+ TCP_HOST?: string;
42
+ /** Container-specific: path to the persistence snapshot file. */
43
+ PERSISTENCE_PATH?: string;
44
+ /** Container-specific: auto-snapshot interval in milliseconds. */
45
+ SNAPSHOT_INTERVAL_MS?: string;
46
+ }
47
+
48
+ /** Fully-resolved container config: IRC server config + container knobs. */
49
+ export interface ContainerConfig {
50
+ server: ParsedServerConfig;
51
+ tcpPort: number;
52
+ tcpHost: string;
53
+ persistencePath: string | undefined;
54
+ snapshotIntervalMs: number;
55
+ }
56
+
57
+ const DEFAULT_TCP_PORT = 6667;
58
+ const DEFAULT_TCP_HOST = '0.0.0.0';
59
+ const DEFAULT_SNAPSHOT_INTERVAL_MS = 60_000;
60
+
61
+ /** Default server name / network when env vars are missing. */
62
+ const DEFAULT_SERVER_NAME = 'irc.example.com';
63
+ const DEFAULT_NETWORK_NAME = 'ExampleNet';
64
+
65
+ /**
66
+ * Loads and validates the IRC server config plus container-specific knobs
67
+ * from a platform env. Throws on an invalid IRC config (bad port number,
68
+ * missing required fields, etc.) so boot fails fast.
69
+ */
70
+ export function loadContainerConfig(env: ContainerConfigEnv): ContainerConfig {
71
+ const server = parseServerConfig(buildServerInput(env));
72
+ return {
73
+ server,
74
+ tcpPort: parseIntOrDefault(env.TCP_PORT, DEFAULT_TCP_PORT),
75
+ tcpHost: env.TCP_HOST?.trim() || DEFAULT_TCP_HOST,
76
+ persistencePath:
77
+ env.PERSISTENCE_PATH !== undefined && env.PERSISTENCE_PATH.length > 0
78
+ ? env.PERSISTENCE_PATH
79
+ : undefined,
80
+ snapshotIntervalMs: parseIntOrDefault(env.SNAPSHOT_INTERVAL_MS, DEFAULT_SNAPSHOT_INTERVAL_MS),
81
+ };
82
+ }
83
+
84
+ function parseIntOrDefault(raw: string | undefined, def: number): number {
85
+ if (raw === undefined || raw.trim().length === 0) return def;
86
+ const n = Number.parseInt(raw, 10);
87
+ return Number.isFinite(n) ? n : def;
88
+ }
89
+
90
+ function buildServerInput(env: ContainerConfigEnv): Record<string, unknown> {
91
+ const input: Record<string, unknown> = {
92
+ serverName: env.SERVER_NAME ?? DEFAULT_SERVER_NAME,
93
+ networkName: env.NETWORK_NAME ?? DEFAULT_NETWORK_NAME,
94
+ };
95
+ if (env.SERVER_VERSION !== undefined) input.serverVersion = env.SERVER_VERSION;
96
+ if (env.CREATED_AT !== undefined) input.createdAt = parseCreatedAt(env.CREATED_AT);
97
+ if (env.MOTD_LINES !== undefined && env.MOTD_LINES.length > 0) {
98
+ input.motdLines = env.MOTD_LINES.split('\n');
99
+ }
100
+ if (env.MAX_CLIENTS !== undefined) input.maxClients = Number.parseInt(env.MAX_CLIENTS, 10);
101
+ if (env.CHANNEL_PREFIXES !== undefined) input.channelPrefixes = env.CHANNEL_PREFIXES;
102
+ if (env.OPER_USER !== undefined || env.OPER_PASSWORD !== undefined) {
103
+ input.operCreds = [{ user: env.OPER_USER ?? '', password: env.OPER_PASSWORD ?? '' }];
104
+ }
105
+ if (env.MAX_CHANNELS_PER_USER !== undefined) {
106
+ input.maxChannelsPerUser = Number.parseInt(env.MAX_CHANNELS_PER_USER, 10);
107
+ }
108
+ if (env.MAX_TARGETS_PER_COMMAND !== undefined) {
109
+ input.maxTargetsPerCommand = Number.parseInt(env.MAX_TARGETS_PER_COMMAND, 10);
110
+ }
111
+ if (env.NICK_LEN !== undefined) input.nickLen = Number.parseInt(env.NICK_LEN, 10);
112
+ if (env.CHANNEL_LEN !== undefined) input.channelLen = Number.parseInt(env.CHANNEL_LEN, 10);
113
+ if (env.TOPIC_LEN !== undefined) input.topicLen = Number.parseInt(env.TOPIC_LEN, 10);
114
+ if (env.MAX_LIST_ENTRIES !== undefined) {
115
+ input.maxListEntries = Number.parseInt(env.MAX_LIST_ENTRIES, 10);
116
+ }
117
+ if (env.QUIT_MESSAGE !== undefined) input.quitMessage = env.QUIT_MESSAGE;
118
+ if (env.SASL_ACCOUNTS !== undefined && env.SASL_ACCOUNTS.length > 0) {
119
+ input.saslAccounts = parseSaslAccountsLines(env.SASL_ACCOUNTS);
120
+ }
121
+ return input;
122
+ }
123
+
124
+ function parseCreatedAt(raw: string): string | number {
125
+ if (/^\d+$/.test(raw.trim())) {
126
+ const n = Number(raw.trim());
127
+ if (Number.isFinite(n)) return n;
128
+ }
129
+ return raw;
130
+ }
131
+
132
+ function parseSaslAccountsLines(raw: string): Array<{ username: string; password: string }> {
133
+ const out: Array<{ username: string; password: string }> = [];
134
+ for (const line of raw.split('\n')) {
135
+ const trimmed = line.trim();
136
+ if (trimmed.length === 0) continue;
137
+ const sep = trimmed.indexOf(':');
138
+ if (sep <= 0) continue;
139
+ const username = trimmed.slice(0, sep);
140
+ const password = trimmed.slice(sep + 1);
141
+ if (username.length === 0 || password.length === 0) continue;
142
+ out.push({ username, password });
143
+ }
144
+ return out;
145
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Cloudflare Container DO for the IRC TCP origin.
3
+ *
4
+ * The container runs the IRC core as a plaintext TCP server (port 6667).
5
+ * Spectrum terminates TLS on :6697 and forwards plaintext to this
6
+ * container. The DO wrapper provides persistent identity and lifecycle
7
+ * management.
8
+ */
9
+
10
+ import { Container } from '@cloudflare/containers';
11
+
12
+ export class IrcTcpOrigin extends Container {
13
+ // The IRC TCP server port inside the container.
14
+ defaultPort = 6667;
15
+ requiredPorts = [6667];
16
+
17
+ // Keep the container alive for long-lived IRC connections.
18
+ sleepAfter = '2h';
19
+
20
+ enableInternet = true;
21
+
22
+ envVars = {
23
+ TCP_PORT: '6667',
24
+ TCP_HOST: '0.0.0.0',
25
+ };
26
+
27
+ onStart(): void {
28
+ console.log('IrcTcpOrigin container started');
29
+ }
30
+
31
+ onStop(): void {
32
+ console.log('IrcTcpOrigin container stopping');
33
+ }
34
+
35
+ onError(error: Error): void {
36
+ console.error('IrcTcpOrigin container error:', error);
37
+ }
38
+ }