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.
- package/.github/workflows/ci.yml +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +349 -18
- package/README.md +132 -30
- package/apps/aws-stack/README.md +6 -5
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +150 -10
- package/apps/aws-stack/tests/stack.test.ts +145 -4
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.test.toml +6 -0
- package/apps/cf-worker/wrangler.toml +28 -2
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +10 -0
- package/apps/local-cli/src/server.ts +149 -24
- package/apps/local-cli/tests/e2e.test.ts +1 -1
- package/apps/local-cli/tests/ws-subprotocol.test.ts +257 -0
- package/package.json +14 -10
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +1 -1
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +75 -4
- package/packages/aws-adapter/src/config-loader.ts +32 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +35 -96
- package/packages/aws-adapter/src/handlers/connect.ts +96 -9
- package/packages/aws-adapter/src/handlers/default.ts +98 -7
- package/packages/aws-adapter/src/handlers/index.ts +109 -4
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +490 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +8 -0
- package/packages/aws-adapter/src/serialize.ts +11 -1
- package/packages/aws-adapter/src/stats.ts +80 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +45 -34
- package/packages/aws-adapter/tests/account-store.test.ts +19 -20
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/aws-integration.test.ts +1 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +61 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +20 -0
- package/packages/aws-adapter/tests/connect.test.ts +174 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +47 -40
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +302 -53
- package/packages/aws-adapter/tests/nlb-stream.test.ts +480 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/stats.test.ts +317 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +5 -1
- package/packages/cf-adapter/src/cf-runtime.ts +68 -5
- package/packages/cf-adapter/src/channel-do.ts +2 -2
- package/packages/cf-adapter/src/config-loader.ts +33 -0
- package/packages/cf-adapter/src/connection-do.ts +278 -85
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +54 -11
- package/packages/cf-adapter/src/index.ts +11 -8
- package/packages/cf-adapter/src/registry-do.ts +22 -3
- package/packages/cf-adapter/src/sharding.ts +1 -2
- package/packages/cf-adapter/src/stats.ts +65 -0
- package/packages/cf-adapter/tests/cf-harness.ts +12 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +6 -5
- package/packages/cf-adapter/tests/cf-runtime.test.ts +38 -2
- package/packages/cf-adapter/tests/channel-do.test.ts +2 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +289 -0
- package/packages/cf-adapter/tests/connection-do-ws-subprotocol.test.ts +184 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +27 -2
- package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/registry-do.test.ts +4 -4
- package/packages/cf-adapter/tests/sharding.test.ts +1 -1
- package/packages/cf-adapter/tests/stats.test.ts +120 -0
- package/packages/cf-adapter/tests/worker/main.ts +15 -8
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +2 -2
- package/packages/cf-adapter/tests/worker/stubs/registry-stub.ts +8 -2
- package/packages/cf-adapter/wrangler.test.toml +15 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +39 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +259 -0
- package/packages/irc-core/package.json +6 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/admission.ts +16 -15
- package/packages/irc-core/src/caps/capabilities.ts +24 -3
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +17 -0
- package/packages/irc-core/src/commands/invite.ts +2 -4
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +6 -2
- package/packages/irc-core/src/commands/kick.ts +2 -4
- package/packages/irc-core/src/commands/kill.ts +127 -0
- package/packages/irc-core/src/commands/list.ts +1 -1
- package/packages/irc-core/src/commands/lusers.ts +204 -0
- package/packages/irc-core/src/commands/mode.ts +4 -8
- package/packages/irc-core/src/commands/names.ts +3 -5
- package/packages/irc-core/src/commands/part.ts +2 -4
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +18 -12
- package/packages/irc-core/src/commands/rehash.ts +119 -0
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/setname.ts +109 -0
- package/packages/irc-core/src/commands/stats.ts +152 -0
- package/packages/irc-core/src/commands/topic.ts +2 -4
- package/packages/irc-core/src/commands/trace.ts +137 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/wallops.ts +118 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +65 -0
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +33 -30
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +360 -12
- package/packages/irc-core/src/protocol/numerics.ts +48 -11
- package/packages/irc-core/src/protocol/outbound.ts +20 -3
- package/packages/irc-core/src/types.ts +46 -2
- package/packages/irc-core/src/ws-framing.ts +132 -0
- package/packages/irc-core/src/ws-subprotocol.ts +66 -0
- package/packages/irc-core/tests/account-store.test.ts +45 -2
- package/packages/irc-core/tests/admission.test.ts +18 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -0
- package/packages/irc-core/tests/commands/kill.test.ts +243 -0
- package/packages/irc-core/tests/commands/lusers.test.ts +368 -0
- package/packages/irc-core/tests/commands/mode.test.ts +57 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +151 -6
- package/packages/irc-core/tests/commands/rehash.test.ts +171 -0
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- package/packages/irc-core/tests/commands/setname.test.ts +225 -0
- package/packages/irc-core/tests/commands/stats.test.ts +294 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +9 -35
- package/packages/irc-core/tests/commands/trace.test.ts +282 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/wallops.test.ts +231 -0
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +95 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/dropped-s2s-and-obsolete-verbs.test.ts +90 -0
- package/packages/irc-core/tests/effects.test.ts +14 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +102 -0
- package/packages/irc-core/tests/outbound.test.ts +51 -0
- package/packages/irc-core/tests/ports.test.ts +22 -0
- package/packages/irc-core/tests/raw-modules.d.ts +11 -0
- package/packages/irc-core/tests/stats-store.test.ts +222 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tests/ws-framing.test.ts +213 -0
- package/packages/irc-core/tests/ws-subprotocol.test.ts +111 -0
- package/packages/irc-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +393 -16
- package/packages/irc-server/src/dispatch.ts +1 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +15 -0
- package/packages/irc-server/src/runtime.ts +31 -0
- package/packages/irc-server/src/transport.ts +104 -0
- package/packages/irc-server/tests/actor.test.ts +1489 -4
- package/packages/irc-server/tests/dispatch.test.ts +37 -17
- package/packages/irc-server/tests/raw-modules.d.ts +11 -0
- package/packages/irc-server/tests/routing.test.ts +5 -0
- package/packages/irc-server/tests/runtime.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +73 -9
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +141 -3
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +10 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +9 -0
- package/tools/seed-aws-accounts.ts +5 -6
- package/tools/seed-cf-accounts.ts +107 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +57 -9
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +34 -1
- package/tools/tcp-ws-forwarder/tests/framing.test.ts +65 -1
- package/docs/ADR-001-pure-reducers-and-effect-system.md +0 -74
- package/docs/ADR-002-location-of-authority.md +0 -82
- package/docs/ADR-003-durable-object-sharding.md +0 -93
- package/docs/ADR-004-dynamodb-schema.md +0 -96
- package/docs/ADR-005-wss-only-transport-v1.md +0 -83
- package/docs/ADR-006-sasl-mechanism-scope.md +0 -86
- package/docs/ADR-007-deterministic-ports.md +0 -82
- package/docs/ADR-008-monorepo-tooling.md +0 -60
- package/docs/AWS-Adapter-Architecture.md +0 -496
- package/docs/AWS-Deployment.md +0 -1186
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -11
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- 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
|
|
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
|
|
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
|
|
206
|
+
it('targets the Node 24 runtime for every function', () => {
|
|
161
207
|
const { template } = makeTemplate();
|
|
162
208
|
template.allResourcesProperties('AWS::Lambda::Function', {
|
|
163
|
-
Runtime: '
|
|
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
|
+
}
|