serverless-ircd 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/.github/workflows/ci.yml +96 -2
  2. package/.github/workflows/deploy-aws.yml +129 -0
  3. package/.github/workflows/deploy-cf.yml +0 -2
  4. package/.gitmodules +3 -0
  5. package/CHANGELOG.md +436 -0
  6. package/README.md +127 -84
  7. package/apps/aws-stack/README.md +73 -0
  8. package/apps/aws-stack/bin/aws.ts +49 -0
  9. package/apps/aws-stack/cdk.json +10 -0
  10. package/apps/aws-stack/package.json +41 -0
  11. package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
  12. package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
  13. package/apps/aws-stack/scripts/smoke.mjs +142 -0
  14. package/apps/aws-stack/src/aws-stack.ts +263 -0
  15. package/apps/aws-stack/src/tables.ts +10 -0
  16. package/apps/aws-stack/tests/localstack.test.ts +46 -0
  17. package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
  18. package/apps/aws-stack/tests/stack.test.ts +464 -0
  19. package/apps/aws-stack/tsconfig.build.json +11 -0
  20. package/apps/aws-stack/tsconfig.test.json +8 -0
  21. package/apps/aws-stack/vitest.config.ts +20 -0
  22. package/apps/cf-worker/package.json +1 -1
  23. package/apps/cf-worker/scripts/smoke.mjs +0 -0
  24. package/apps/cf-worker/src/worker.ts +8 -2
  25. package/apps/cf-worker/tests/smoke.test.ts +1 -0
  26. package/apps/cf-worker/wrangler.test.toml +5 -1
  27. package/apps/cf-worker/wrangler.toml +66 -17
  28. package/apps/local-cli/package.json +1 -1
  29. package/apps/local-cli/src/config-loader.ts +57 -0
  30. package/apps/local-cli/src/main.ts +1 -1
  31. package/apps/local-cli/src/server.ts +267 -32
  32. package/apps/local-cli/tests/config-loader.test.ts +107 -0
  33. package/apps/local-cli/tests/e2e.test.ts +126 -0
  34. package/apps/local-cli/tests/security-e2e.test.ts +239 -0
  35. package/biome.json +3 -1
  36. package/docs/ADR-001-pure-reducers-and-effect-system.md +74 -0
  37. package/docs/ADR-002-location-of-authority.md +82 -0
  38. package/docs/ADR-003-durable-object-sharding.md +93 -0
  39. package/docs/ADR-004-dynamodb-schema.md +96 -0
  40. package/docs/ADR-005-wss-only-transport-v1.md +83 -0
  41. package/docs/ADR-006-sasl-mechanism-scope.md +86 -0
  42. package/docs/ADR-007-deterministic-ports.md +82 -0
  43. package/docs/ADR-008-monorepo-tooling.md +60 -0
  44. package/docs/AWS-Adapter-Architecture.md +496 -0
  45. package/docs/AWS-Deployment.md +1186 -0
  46. package/docs/Cloudflare-Deployment-Guide.md +660 -0
  47. package/docs/Home.md +11 -0
  48. package/docs/Observability.md +87 -0
  49. package/docs/PlanIRCv3Websocket.md +489 -0
  50. package/docs/PlanWebClient.md +451 -0
  51. package/docs/Release-Process.md +443 -0
  52. package/package.json +19 -14
  53. package/packages/aws-adapter/README.md +35 -0
  54. package/packages/aws-adapter/package.json +52 -0
  55. package/packages/aws-adapter/src/account-store.ts +121 -0
  56. package/packages/aws-adapter/src/aws-runtime.ts +871 -0
  57. package/packages/aws-adapter/src/cdk-table-defs.ts +73 -0
  58. package/packages/aws-adapter/src/config-loader.ts +126 -0
  59. package/packages/aws-adapter/src/dynamo-account-store.ts +156 -0
  60. package/packages/aws-adapter/src/dynamo.ts +61 -0
  61. package/packages/aws-adapter/src/handlers/connect.ts +44 -0
  62. package/packages/aws-adapter/src/handlers/default.ts +330 -0
  63. package/packages/aws-adapter/src/handlers/disconnect.ts +48 -0
  64. package/packages/aws-adapter/src/handlers/index.ts +293 -0
  65. package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
  66. package/packages/aws-adapter/src/handlers/state.ts +13 -0
  67. package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
  68. package/packages/aws-adapter/src/index.ts +74 -0
  69. package/packages/aws-adapter/src/message-store.ts +34 -0
  70. package/packages/aws-adapter/src/serialize.ts +283 -0
  71. package/packages/aws-adapter/src/tables.ts +53 -0
  72. package/packages/aws-adapter/tests/account-store-dynamo.test.ts +171 -0
  73. package/packages/aws-adapter/tests/account-store.test.ts +280 -0
  74. package/packages/aws-adapter/tests/aws-harness.ts +408 -0
  75. package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
  76. package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
  77. package/packages/aws-adapter/tests/config-loader.test.ts +213 -0
  78. package/packages/aws-adapter/tests/disconnect-fanout.test.ts +336 -0
  79. package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
  80. package/packages/aws-adapter/tests/global-setup.ts +301 -0
  81. package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
  82. package/packages/aws-adapter/tests/handlers.test.ts +473 -0
  83. package/packages/aws-adapter/tests/message-store.test.ts +55 -0
  84. package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
  85. package/packages/aws-adapter/tests/serialize.test.ts +249 -0
  86. package/packages/aws-adapter/tests/smoke.test.ts +48 -0
  87. package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
  88. package/packages/aws-adapter/tests/tables.test.ts +74 -0
  89. package/packages/aws-adapter/tests/transactions.test.ts +446 -0
  90. package/packages/aws-adapter/tsconfig.build.json +16 -0
  91. package/packages/aws-adapter/tsconfig.test.json +14 -0
  92. package/packages/aws-adapter/vitest.config.ts +23 -0
  93. package/packages/cf-adapter/package.json +2 -1
  94. package/packages/cf-adapter/src/cf-runtime.ts +42 -22
  95. package/packages/cf-adapter/src/channel-do.ts +40 -1
  96. package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
  97. package/packages/cf-adapter/src/config-loader.ts +135 -0
  98. package/packages/cf-adapter/src/connection-do.ts +119 -0
  99. package/packages/cf-adapter/src/env.ts +27 -1
  100. package/packages/cf-adapter/src/index.ts +2 -1
  101. package/packages/cf-adapter/tests/cf-harness.ts +2 -2
  102. package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
  103. package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
  104. package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
  105. package/packages/cf-adapter/tests/connection-do.test.ts +82 -0
  106. package/packages/cf-adapter/tests/worker/main.ts +2 -1
  107. package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
  108. package/packages/cf-adapter/wrangler.test.toml +10 -1
  109. package/packages/in-memory-runtime/package.json +1 -1
  110. package/packages/in-memory-runtime/src/in-memory-runtime.ts +107 -16
  111. package/packages/in-memory-runtime/src/index.ts +1 -1
  112. package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +134 -0
  113. package/packages/irc-core/package.json +13 -2
  114. package/packages/irc-core/src/admission.ts +216 -0
  115. package/packages/irc-core/src/caps/capabilities.ts +1 -0
  116. package/packages/irc-core/src/case-fold.ts +64 -0
  117. package/packages/irc-core/src/cloak.ts +81 -0
  118. package/packages/irc-core/src/commands/cap.ts +1 -2
  119. package/packages/irc-core/src/commands/chathistory.ts +305 -0
  120. package/packages/irc-core/src/commands/index.ts +9 -0
  121. package/packages/irc-core/src/commands/invite.ts +6 -9
  122. package/packages/irc-core/src/commands/isupport.ts +1 -1
  123. package/packages/irc-core/src/commands/join.ts +63 -10
  124. package/packages/irc-core/src/commands/kick.ts +4 -11
  125. package/packages/irc-core/src/commands/list.ts +5 -4
  126. package/packages/irc-core/src/commands/mode.ts +5 -9
  127. package/packages/irc-core/src/commands/motd-lines.ts +127 -0
  128. package/packages/irc-core/src/commands/motd.ts +6 -110
  129. package/packages/irc-core/src/commands/oper.ts +151 -0
  130. package/packages/irc-core/src/commands/privmsg.ts +24 -0
  131. package/packages/irc-core/src/commands/registration.ts +79 -21
  132. package/packages/irc-core/src/commands/sasl.ts +251 -0
  133. package/packages/irc-core/src/commands/tagmsg.ts +205 -0
  134. package/packages/irc-core/src/config.ts +270 -0
  135. package/packages/irc-core/src/flood-control.ts +175 -0
  136. package/packages/irc-core/src/index.ts +7 -0
  137. package/packages/irc-core/src/ports.ts +719 -0
  138. package/packages/irc-core/src/protocol/base64.ts +16 -0
  139. package/packages/irc-core/src/protocol/index.ts +2 -1
  140. package/packages/irc-core/src/protocol/numerics.ts +11 -0
  141. package/packages/irc-core/src/protocol/parser.ts +27 -2
  142. package/packages/irc-core/src/state/connection.ts +41 -0
  143. package/packages/irc-core/src/types.ts +88 -2
  144. package/packages/irc-core/stryker.commands.conf.json +41 -0
  145. package/packages/irc-core/stryker.protocol.conf.json +26 -0
  146. package/packages/irc-core/tests/account-store.test.ts +88 -0
  147. package/packages/irc-core/tests/admission.test.ts +229 -0
  148. package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
  149. package/packages/irc-core/tests/case-fold.test.ts +80 -0
  150. package/packages/irc-core/tests/cloak.test.ts +78 -0
  151. package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
  152. package/packages/irc-core/tests/commands/join.test.ts +246 -2
  153. package/packages/irc-core/tests/commands/kick.test.ts +15 -0
  154. package/packages/irc-core/tests/commands/oper.test.ts +257 -0
  155. package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
  156. package/packages/irc-core/tests/commands/registration.test.ts +380 -20
  157. package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
  158. package/packages/irc-core/tests/commands/tagmsg.test.ts +688 -0
  159. package/packages/irc-core/tests/commands/who.test.ts +22 -4
  160. package/packages/irc-core/tests/commands/whois.test.ts +8 -1
  161. package/packages/irc-core/tests/config.test.ts +721 -0
  162. package/packages/irc-core/tests/flood-control.test.ts +394 -0
  163. package/packages/irc-core/tests/message-store.test.ts +530 -0
  164. package/packages/irc-core/tests/parser.test.ts +44 -1
  165. package/packages/irc-core/tests/ports.test.ts +263 -0
  166. package/packages/irc-server/package.json +1 -1
  167. package/packages/irc-server/src/actor.ts +186 -44
  168. package/packages/irc-server/src/dispatch.ts +89 -5
  169. package/packages/irc-server/src/index.ts +2 -0
  170. package/packages/irc-server/src/routing.ts +160 -0
  171. package/packages/irc-server/tests/actor.test.ts +690 -15
  172. package/packages/irc-server/tests/dispatch.test.ts +84 -0
  173. package/packages/irc-server/tests/routing.test.ts +204 -0
  174. package/packages/irc-test-support/README.md +32 -0
  175. package/packages/irc-test-support/package.json +37 -0
  176. package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +6 -5
  177. package/packages/irc-test-support/src/index.ts +24 -0
  178. package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
  179. package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
  180. package/packages/irc-test-support/tests/smoke.test.ts +11 -0
  181. package/packages/irc-test-support/tsconfig.build.json +16 -0
  182. package/packages/irc-test-support/tsconfig.test.json +14 -0
  183. package/packages/irc-test-support/vitest.config.ts +20 -0
  184. package/pnpm-workspace.yaml +1 -0
  185. package/tools/ci-hardening/package.json +30 -0
  186. package/tools/ci-hardening/src/index.ts +6 -0
  187. package/tools/ci-hardening/src/validate.ts +103 -0
  188. package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
  189. package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
  190. package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
  191. package/tools/ci-hardening/tests/validate.test.ts +177 -0
  192. package/tools/ci-hardening/tsconfig.build.json +12 -0
  193. package/tools/ci-hardening/tsconfig.test.json +10 -0
  194. package/tools/ci-hardening/vitest.config.ts +21 -0
  195. package/tools/seed-aws-accounts.ts +80 -0
  196. package/tools/tcp-ws-forwarder/package.json +1 -1
  197. package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
  198. package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
  199. package/tools/tcp-ws-forwarder/src/main.ts +33 -14
  200. package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
  201. package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
  202. package/packages/cf-adapter/tests/integration/index.ts +0 -17
  203. /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
  204. /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
@@ -18,6 +18,7 @@ describe('SUPPORTED_CAPABILITIES', () => {
18
18
  'away-notify',
19
19
  'batch',
20
20
  'chghost',
21
+ 'draft/chathistory',
21
22
  'echo-message',
22
23
  'extended-join',
23
24
  'invite-notify',
@@ -73,6 +74,10 @@ describe('isSupportedCap', () => {
73
74
  expect(isSupportedCap('safelist')).toBe(true);
74
75
  });
75
76
 
77
+ it('returns true for draft/chathistory (bare name, no value)', () => {
78
+ expect(isSupportedCap('draft/chathistory')).toBe(true);
79
+ });
80
+
76
81
  it('returns false for an unknown cap name', () => {
77
82
  expect(isSupportedCap('does-not-exist')).toBe(false);
78
83
  });
@@ -111,6 +116,23 @@ describe('getLsString', () => {
111
116
  });
112
117
  });
113
118
 
119
+ describe('draft/chathistory advertisement', () => {
120
+ it('is advertised as a bare name (no value parameter)', () => {
121
+ const cap = SUPPORTED_CAPABILITIES.find((c) => c.name === 'draft/chathistory');
122
+ expect(cap).toBeDefined();
123
+ expect(cap?.value).toBeUndefined();
124
+ });
125
+
126
+ it('appears in CAP LS output as the bare token', () => {
127
+ expect(getLsString()).toContain('draft/chathistory');
128
+ expect(getLsString()).not.toContain('draft/chathistory=');
129
+ });
130
+
131
+ it('is ACKable via SUPPORTED_CAP_NAMES membership (CAP REQ round-trip)', () => {
132
+ expect(SUPPORTED_CAP_NAMES.has('draft/chathistory')).toBe(true);
133
+ });
134
+ });
135
+
114
136
  describe('splitCapsForWire', () => {
115
137
  it('returns a single chunk when everything fits', () => {
116
138
  const tokens = ['a', 'b', 'c'];
@@ -0,0 +1,80 @@
1
+ import fc from 'fast-check';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { type CaseMapping, caseFold } from '../src/case-fold';
4
+
5
+ // ============================================================================
6
+ // caseFold — rfc1459 special-character folding
7
+ // ============================================================================
8
+
9
+ describe('caseFold — rfc1459', () => {
10
+ it('folds [Foo] to {foo}', () => {
11
+ expect(caseFold('rfc1459', '[Foo]')).toBe('{foo}');
12
+ });
13
+
14
+ it('folds uppercase A–Z to a–z', () => {
15
+ expect(caseFold('rfc1459', 'ABCXYZ')).toBe('abcxyz');
16
+ });
17
+
18
+ it('folds the four rfc1459 specials [ \\ ] ^ to { | } ~', () => {
19
+ expect(caseFold('rfc1459', '[\\]^')).toBe('{|}~');
20
+ });
21
+
22
+ it('does not fold { | } ~ any further', () => {
23
+ expect(caseFold('rfc1459', '{|}~')).toBe('{|}~');
24
+ });
25
+
26
+ it('leaves non-folding characters unchanged', () => {
27
+ expect(caseFold('rfc1459', 'a1_-.#@')).toBe('a1_-.#@');
28
+ });
29
+
30
+ it('returns the empty string unchanged', () => {
31
+ expect(caseFold('rfc1459', '')).toBe('');
32
+ });
33
+ });
34
+
35
+ // ============================================================================
36
+ // caseFold — ascii mapping (strict subset of rfc1459)
37
+ // ============================================================================
38
+
39
+ describe('caseFold — ascii', () => {
40
+ it('folds uppercase A–Z to a–z', () => {
41
+ expect(caseFold('ascii', 'ABCXYZ')).toBe('abcxyz');
42
+ });
43
+
44
+ it('does NOT fold the rfc1459 specials [ \\ ] ^', () => {
45
+ expect(caseFold('ascii', '[Foo]')).toBe('[foo]');
46
+ });
47
+
48
+ it('leaves non-folding characters unchanged', () => {
49
+ expect(caseFold('ascii', 'a1_-.#[]')).toBe('a1_-.#[]');
50
+ });
51
+ });
52
+
53
+ // ============================================================================
54
+ // caseFold — idempotency (property test)
55
+ // ============================================================================
56
+
57
+ describe('caseFold — idempotency', () => {
58
+ const mappings: CaseMapping[] = ['rfc1459', 'ascii'];
59
+
60
+ for (const mapping of mappings) {
61
+ it(`fold(fold(s)) === fold(s) under ${mapping}`, () => {
62
+ const nickish = fc.stringMatching(/[\x21-\x7e]{0,16}/u);
63
+ fc.assert(
64
+ fc.property(nickish, (s) => {
65
+ const once = caseFold(mapping, s);
66
+ expect(caseFold(mapping, once)).toBe(once);
67
+ }),
68
+ );
69
+ });
70
+ }
71
+
72
+ it('rfc1459 and ascii agree on pure A–Z / a–z input', () => {
73
+ const alpha = fc.stringMatching(/^[A-Za-z]{1,16}$/u);
74
+ fc.assert(
75
+ fc.property(alpha, (s) => {
76
+ expect(caseFold('rfc1459', s)).toBe(caseFold('ascii', s));
77
+ }),
78
+ );
79
+ });
80
+ });
@@ -0,0 +1,78 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { cloakHost } from '../src/cloak';
3
+
4
+ // ============================================================================
5
+ // cloakHost — basic deterministic cloaking
6
+ // ============================================================================
7
+
8
+ describe('cloakHost — determinism', () => {
9
+ it('returns the same cloak for the same IP + secret', () => {
10
+ const a = cloakHost('203.0.113.5', { enabled: true, secret: 's3cret' }, 'irc.example.com');
11
+ const b = cloakHost('203.0.113.5', { enabled: true, secret: 's3cret' }, 'irc.example.com');
12
+ expect(a).toBe(b);
13
+ });
14
+
15
+ it('returns different cloaks for different IPs with the same secret', () => {
16
+ const a = cloakHost('203.0.113.5', { enabled: true, secret: 's3cret' }, 'irc.example.com');
17
+ const b = cloakHost('198.51.100.7', { enabled: true, secret: 's3cret' }, 'irc.example.com');
18
+ expect(a).not.toBe(b);
19
+ });
20
+
21
+ it('returns different cloaks for the same IP with different secrets', () => {
22
+ const a = cloakHost('203.0.113.5', { enabled: true, secret: 's3cret' }, 'irc.example.com');
23
+ const b = cloakHost('203.0.113.5', { enabled: true, secret: 'other' }, 'irc.example.com');
24
+ expect(a).not.toBe(b);
25
+ });
26
+ });
27
+
28
+ // ============================================================================
29
+ // cloakHost — format
30
+ // ============================================================================
31
+
32
+ describe('cloakHost — format', () => {
33
+ it('produces a cloak ending in the configured suffix', () => {
34
+ const cloak = cloakHost('203.0.113.5', { enabled: true, secret: 's3cret' }, 'irc.example.com');
35
+ expect(cloak.endsWith('.irc.example.com')).toBe(true);
36
+ });
37
+
38
+ it('produces a cloak whose leading segment is a stable hex-encoded hash', () => {
39
+ const cloak = cloakHost('203.0.113.5', { enabled: true, secret: 's3cret' }, 'irc.example.com');
40
+ // Strip the suffix, leaving just the hash segment.
41
+ const segment = cloak.slice(0, cloak.indexOf('.'));
42
+ expect(segment).toMatch(/^[a-z0-9]+$/u);
43
+ expect(segment.length).toBeGreaterThanOrEqual(8);
44
+ });
45
+
46
+ it('produces a cloak no longer than 63 bytes per label (DNS limit)', () => {
47
+ const cloak = cloakHost('203.0.113.5', { enabled: true, secret: 's3cret' }, 'irc.example.com');
48
+ for (const label of cloak.split('.')) {
49
+ expect(label.length).toBeLessThanOrEqual(63);
50
+ }
51
+ });
52
+ });
53
+
54
+ // ============================================================================
55
+ // cloakHost — disabled
56
+ // ============================================================================
57
+
58
+ describe('cloakHost — disabled', () => {
59
+ it('returns the original host when cloaking is disabled', () => {
60
+ const cloak = cloakHost('203.0.113.5', { enabled: false, secret: 's3cret' }, 'irc.example.com');
61
+ expect(cloak).toBe('203.0.113.5');
62
+ });
63
+ });
64
+
65
+ // ============================================================================
66
+ // cloakHost — IPv6
67
+ // ============================================================================
68
+
69
+ describe('cloakHost — IPv6', () => {
70
+ it('cloaks an IPv6 address the same way as an IPv4 address', () => {
71
+ const cloak = cloakHost('2001:db8::1', { enabled: true, secret: 's3cret' }, 'irc.example.com');
72
+ expect(cloak.endsWith('.irc.example.com')).toBe(true);
73
+ // Re-cloaking deterministically yields the same value.
74
+ expect(cloakHost('2001:db8::1', { enabled: true, secret: 's3cret' }, 'irc.example.com')).toBe(
75
+ cloak,
76
+ );
77
+ });
78
+ });