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
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Pure unit tests for `src/serialize.ts`.
3
+ *
4
+ * No DynamoDB calls — these tests assert the irc-core → marshalled-row
5
+ * transforms are correct and round-trip-safe. The DynamoDB-backed
6
+ * runtime tests live in `aws-runtime.test.ts`.
7
+ */
8
+
9
+ import {
10
+ type ChannelState,
11
+ type ConnectionState,
12
+ createChannel,
13
+ createConnection,
14
+ } from '@serverless-ircd/irc-core';
15
+ import { describe, expect, it } from 'vitest';
16
+ import {
17
+ marshalChannelMember,
18
+ marshalChannelMeta,
19
+ marshalConnection,
20
+ marshalNick,
21
+ unmarshalChannelMember,
22
+ unmarshalChannelMeta,
23
+ unmarshalConnection,
24
+ unmarshalNick,
25
+ } from '../src/serialize.js';
26
+
27
+ describe('marshalConnection / unmarshalConnection', () => {
28
+ it('round-trips a pre-registration connection with no optional fields', () => {
29
+ const state = createConnection({ id: 'c1', connectedSince: 100 });
30
+ const row = marshalConnection(state, 100);
31
+ const restored = unmarshalConnection(row);
32
+ expectRestoredConnection(restored, state);
33
+ expect(row.idleSince).toBe(100);
34
+ expect(row.version).toBe(1);
35
+ });
36
+
37
+ it('round-trips a fully-registered connection with every optional field set', () => {
38
+ const state = createConnection({ id: 'c1', connectedSince: 100 });
39
+ state.nick = 'Alice';
40
+ state.user = 'alice';
41
+ state.host = 'example.com';
42
+ state.realname = 'Alice In Chains';
43
+ state.passAttempt = 'hunter2';
44
+ state.account = 'alice-acct';
45
+ state.away = 'back later';
46
+ state.saslMech = 'PLAIN';
47
+ state.saslBuffer = 'partial';
48
+ state.caps.add('echo-message');
49
+ state.caps.add('batch');
50
+ state.joinedChannels.add('#foo');
51
+ state.joinedChannels.add('#bar');
52
+ state.userModes.invisible = true;
53
+
54
+ const row = marshalConnection(state, 999);
55
+ const restored = unmarshalConnection(row);
56
+ expectRestoredConnection(restored, state);
57
+ expect(row.idleSince).toBe(999);
58
+ });
59
+
60
+ it('stores caps as an array and joinedChannels as a Set (for atomic ADD/DELETE)', () => {
61
+ const state = createConnection({ id: 'c1', connectedSince: 0 });
62
+ state.caps.add('echo-message');
63
+ state.joinedChannels.add('#chan');
64
+ const row = marshalConnection(state, 0);
65
+ expect(Array.isArray(row.caps)).toBe(true);
66
+ expect(row.joinedChannels).toBeInstanceOf(Set);
67
+ expect((row.joinedChannels as Set<string>).has('#chan')).toBe(true);
68
+ });
69
+
70
+ it('omits joinedChannels from the marshalled row when the set is empty (DynamoDB rejects empty sets)', () => {
71
+ const state = createConnection({ id: 'c1', connectedSince: 0 });
72
+ const row = marshalConnection(state, 0);
73
+ expect(row.joinedChannels).toBeUndefined();
74
+ });
75
+
76
+ it('round-trips a connection whose joinedChannels started empty (attribute absent → empty Set)', () => {
77
+ const state = createConnection({ id: 'c1', connectedSince: 0 });
78
+ const row = marshalConnection(state, 0);
79
+ const restored = unmarshalConnection(row);
80
+ expect(restored.joinedChannels).toBeInstanceOf(Set);
81
+ expect(restored.joinedChannels.size).toBe(0);
82
+ });
83
+
84
+ it('rejects a row whose version is newer than the supported schema', () => {
85
+ const state = createConnection({ id: 'c1', connectedSince: 0 });
86
+ const row = marshalConnection(state, 0);
87
+ const future = { ...row, version: 9999 };
88
+ expect(() => unmarshalConnection(future)).toThrow(/newer than supported/u);
89
+ });
90
+
91
+ it('preserves the connection id as the row primary key', () => {
92
+ const state = createConnection({ id: 'c-fixed', connectedSince: 0 });
93
+ const row = marshalConnection(state, 0);
94
+ expect(row.connectionId).toBe('c-fixed');
95
+ });
96
+ });
97
+
98
+ describe('marshalNick / unmarshalNick', () => {
99
+ it('round-trips a nick reservation row', () => {
100
+ const row = marshalNick('Alice', 'c1', 123);
101
+ expect(row.nickLower).toBe('alice');
102
+ expect(row.nick).toBe('Alice');
103
+ expect(row.connectionId).toBe('c1');
104
+ expect(row.reservedAt).toBe(123);
105
+ expect(unmarshalNick(row)).toEqual({ nick: 'Alice', connectionId: 'c1' });
106
+ });
107
+
108
+ it('lowercases the PK but preserves display case', () => {
109
+ const row = marshalNick('BoB', 'c2', 0);
110
+ expect(row.nickLower).toBe('bob');
111
+ expect(row.nick).toBe('BoB');
112
+ });
113
+ });
114
+
115
+ describe('marshalChannelMeta / unmarshalChannelMeta', () => {
116
+ it('round-trips an empty channel with no topic', () => {
117
+ const chan = createChannel({
118
+ name: '#Foo',
119
+ nameLower: '#foo',
120
+ createdAt: 42,
121
+ });
122
+ const row = marshalChannelMeta(chan);
123
+ expect(row.channelName).toBe('#foo');
124
+ expect(row.displayName).toBe('#Foo');
125
+ expect(row.topicText).toBeUndefined();
126
+ const members = new Map();
127
+ const restored = unmarshalChannelMeta(row, members);
128
+ expectChannelEqual(restored, chan);
129
+ });
130
+
131
+ it('round-trips a channel with topic, modes, banMasks, and pendingInvites', () => {
132
+ const chan = createChannel({
133
+ name: '#Foo',
134
+ nameLower: '#foo',
135
+ createdAt: 1,
136
+ });
137
+ chan.topic = { text: 'welcome', setter: 'alice', setAt: 100 };
138
+ chan.modes.inviteOnly = true;
139
+ chan.modes.topicLock = true;
140
+ chan.modes.key = 'sekret';
141
+ chan.modes.limit = 25;
142
+ chan.banMasks.add('*!bad@*');
143
+ chan.pendingInvites.add('bob');
144
+
145
+ const row = marshalChannelMeta(chan);
146
+ expect(row.topicText).toBe('welcome');
147
+ expect(row.topicSetter).toBe('alice');
148
+ expect(row.topicSetAt).toBe(100);
149
+ expect(row.modes.inviteOnly).toBe(true);
150
+ expect(row.modes.key).toBe('sekret');
151
+ expect(row.modes.limit).toBe(25);
152
+ expect(row.banMasks).toEqual(['*!bad@*']);
153
+ expect(row.pendingInvites).toEqual(['bob']);
154
+
155
+ const restored = unmarshalChannelMeta(row, new Map());
156
+ expectChannelEqual(restored, chan);
157
+ });
158
+
159
+ it('serializes banMasks and pendingInvites as arrays', () => {
160
+ const chan = createChannel({ name: '#x', nameLower: '#x', createdAt: 0 });
161
+ chan.banMasks.add('a');
162
+ chan.banMasks.add('b');
163
+ chan.pendingInvites.add('x');
164
+ const row = marshalChannelMeta(chan);
165
+ expect(Array.isArray(row.banMasks)).toBe(true);
166
+ expect(Array.isArray(row.pendingInvites)).toBe(true);
167
+ });
168
+
169
+ it('omits topic fields when the channel has no topic', () => {
170
+ const chan = createChannel({ name: '#x', nameLower: '#x', createdAt: 0 });
171
+ const row = marshalChannelMeta(chan);
172
+ expect(row.topicText).toBeUndefined();
173
+ expect(row.topicSetter).toBeUndefined();
174
+ expect(row.topicSetAt).toBeUndefined();
175
+ });
176
+
177
+ it('rehydrates members from the supplied map', () => {
178
+ const chan = createChannel({ name: '#x', nameLower: '#x', createdAt: 0 });
179
+ const row = marshalChannelMeta(chan);
180
+ const members = new Map([['c1', { conn: 'c1', nick: 'alice', op: true, voice: false }]]);
181
+ const restored = unmarshalChannelMeta(row, members);
182
+ expect(restored.members.get('c1')).toEqual({
183
+ conn: 'c1',
184
+ nick: 'alice',
185
+ op: true,
186
+ voice: false,
187
+ });
188
+ });
189
+ });
190
+
191
+ describe('marshalChannelMember / unmarshalChannelMember', () => {
192
+ it('round-trips a roster entry', () => {
193
+ const entry = { conn: 'c1', nick: 'alice', op: true, voice: false };
194
+ const row = marshalChannelMember('#foo', entry);
195
+ expect(row.channelName).toBe('#foo');
196
+ expect(row.connectionId).toBe('c1');
197
+ expect(row.nick).toBe('alice');
198
+ expect(row.op).toBe(true);
199
+ expect(row.voice).toBe(false);
200
+ expect(unmarshalChannelMember(row)).toEqual(entry);
201
+ });
202
+
203
+ it('preserves op/voice=false without coercing to undefined', () => {
204
+ const entry = { conn: 'c1', nick: 'alice', op: false, voice: false };
205
+ const row = marshalChannelMember('#foo', entry);
206
+ expect(row.op).toBe(false);
207
+ expect(row.voice).toBe(false);
208
+ });
209
+ });
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Helpers
213
+ // ---------------------------------------------------------------------------
214
+
215
+ function expectRestoredConnection(restored: ConnectionState, original: ConnectionState): void {
216
+ expect(restored.id).toBe(original.id);
217
+ expect(restored.registration).toBe(original.registration);
218
+ expect(restored.capNegotiating).toBe(original.capNegotiating);
219
+ expect(restored.caps).toEqual(original.caps);
220
+ expect(restored.joinedChannels).toEqual(original.joinedChannels);
221
+ expect(restored.userModes).toEqual(original.userModes);
222
+ expect(restored.lastSeen).toBe(original.lastSeen);
223
+ expect(restored.connectedSince).toBe(original.connectedSince);
224
+ expect(restored.nick).toBe(original.nick);
225
+ expect(restored.user).toBe(original.user);
226
+ expect(restored.host).toBe(original.host);
227
+ expect(restored.realname).toBe(original.realname);
228
+ expect(restored.passAttempt).toBe(original.passAttempt);
229
+ expect(restored.account).toBe(original.account);
230
+ expect(restored.away).toBe(original.away);
231
+ expect(restored.saslMech).toBe(original.saslMech);
232
+ expect(restored.saslBuffer).toBe(original.saslBuffer);
233
+ // Sets must be re-hydrated as Sets, not arrays.
234
+ expect(restored.caps).toBeInstanceOf(Set);
235
+ expect(restored.joinedChannels).toBeInstanceOf(Set);
236
+ }
237
+
238
+ function expectChannelEqual(restored: ChannelState, original: ChannelState): void {
239
+ expect(restored.name).toBe(original.name);
240
+ expect(restored.nameLower).toBe(original.nameLower);
241
+ expect(restored.modes).toEqual(original.modes);
242
+ expect(restored.banMasks).toEqual(original.banMasks);
243
+ expect(restored.pendingInvites).toEqual(original.pendingInvites);
244
+ expect(restored.createdAt).toBe(original.createdAt);
245
+ expect(restored.topic).toEqual(original.topic);
246
+ expect(restored.banMasks).toBeInstanceOf(Set);
247
+ expect(restored.pendingInvites).toBeInstanceOf(Set);
248
+ expect(restored.members).toBeInstanceOf(Map);
249
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Smoke test for the global-setup: confirms the DynamoDB Local endpoint
3
+ * responds to a real SDK call when `DDB_AVAILABLE=1`, and that the test
4
+ * suite is skipped gracefully otherwise.
5
+ */
6
+
7
+ import {
8
+ CreateTableCommand,
9
+ DeleteTableCommand,
10
+ DescribeTableCommand,
11
+ } from '@aws-sdk/client-dynamodb';
12
+ import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
13
+ import { describe, expect, it } from 'vitest';
14
+ import { createDynamoDocumentClient } from '../src/index.js';
15
+
16
+ const available = process.env.DDB_AVAILABLE === '1';
17
+ const endpoint = process.env.DYNAMO_ENDPOINT;
18
+
19
+ describe.skipIf(!available)('DynamoDB Local endpoint', () => {
20
+ it('accepts CreateTable + DescribeTable round-trip', async () => {
21
+ if (endpoint === undefined) {
22
+ throw new Error('DDB_AVAILABLE=1 but DYNAMO_ENDPOINT is unset');
23
+ }
24
+ const client = createDynamoDocumentClient({ endpoint });
25
+ const tableName = `aws-adapter-smoke-${Date.now()}`;
26
+ await client.send(
27
+ new CreateTableCommand({
28
+ TableName: tableName,
29
+ AttributeDefinitions: [{ AttributeName: 'pk', AttributeType: 'S' }],
30
+ KeySchema: [{ AttributeName: 'pk', KeyType: 'HASH' }],
31
+ BillingMode: 'PAY_PER_REQUEST',
32
+ }),
33
+ );
34
+ const described = await client.send(new DescribeTableCommand({ TableName: tableName }));
35
+ expect(described.Table?.TableName).toBe(tableName);
36
+ await client.send(new DeleteTableCommand({ TableName: tableName }));
37
+ });
38
+ });
39
+
40
+ describe.skipIf(available)('DynamoDB Local unavailable', () => {
41
+ it('skips the integration suite', () => {
42
+ expect(process.env.DDB_AVAILABLE ?? '0').toBe('0');
43
+ });
44
+ });
45
+
46
+ // Reference the type so TS doesn't complain about unused import on skip paths.
47
+ export type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
48
+ void (null as unknown as DynamoDBDocumentClient);