serverless-ircd 0.7.0 → 0.9.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 +7 -3
- package/.gitmodules +1 -1
- package/CHANGELOG.md +518 -29
- package/README.md +221 -161
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +186 -18
- package/apps/aws-stack/tests/stack.test.ts +400 -56
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-tcp-container/src/container-server.ts +21 -1
- package/apps/cf-tcp-container/tests/config-loader.test.ts +43 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +249 -1
- package/apps/cf-tcp-container/tests/persistence.test.ts +9 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +24 -5
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/src/worker.ts +4 -4
- package/apps/cf-worker/tests/fixtures/web-dist/webclient/index.html +18 -0
- package/apps/cf-worker/tests/smoke.test.ts +5 -5
- package/apps/cf-worker/wrangler.test.toml +6 -6
- package/apps/cf-worker/wrangler.toml +7 -5
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +8 -0
- package/apps/local-cli/src/main.ts +16 -0
- package/apps/local-cli/src/server.ts +95 -31
- package/apps/local-cli/tests/config-loader.test.ts +14 -0
- package/apps/local-cli/tests/config-resolution.test.ts +65 -0
- package/apps/local-cli/tests/motd-file-non-error.test.ts +29 -0
- package/apps/local-cli/tests/rehash.test.ts +147 -0
- package/apps/local-cli/tests/server-helpers.test.ts +63 -0
- package/apps/local-cli/tests/tcp.test.ts +89 -0
- package/apps/local-cli/tests/ws-subprotocol.test.ts +92 -0
- package/apps/web/landing/index.html +237 -16
- package/apps/web/package.json +3 -2
- package/apps/web/scripts/build.mjs +46 -16
- package/apps/web/src/build-env.ts +1 -1
- package/apps/web/src/config-schema.ts +6 -6
- package/apps/web/src/render-docs.ts +292 -0
- package/apps/web/tests/build-smoke.test.ts +43 -14
- package/apps/web/tests/config-schema.test.ts +1 -1
- package/apps/web/tests/landing-content.test.ts +103 -0
- package/apps/web/tests/render-docs.test.ts +198 -0
- package/docs/AWS-Adapter-Architecture.md +3 -2
- package/docs/AWS-Deployment.md +21 -8
- package/docs/Cloudflare-Deployment-Guide.md +22 -6
- package/docs/PlanExtensions.md +113 -3
- package/docs/Release-Process.md +23 -13
- package/docs/Services.md +578 -0
- package/docs/WebClientGuide.md +32 -31
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +20 -1
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -0
- package/packages/aws-adapter/src/config-loader.ts +11 -0
- package/packages/aws-adapter/src/connection-counter.ts +89 -0
- package/packages/aws-adapter/src/dynamo-services-store.ts +649 -0
- package/packages/aws-adapter/src/handlers/connect.ts +55 -51
- package/packages/aws-adapter/src/handlers/default.ts +36 -4
- package/packages/aws-adapter/src/handlers/index.ts +15 -0
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +25 -2
- package/packages/aws-adapter/src/handlers/sweeper.ts +5 -1
- package/packages/aws-adapter/src/index.ts +4 -0
- package/packages/aws-adapter/src/stats.ts +6 -1
- package/packages/aws-adapter/src/tables.ts +34 -4
- package/packages/aws-adapter/tests/aws-harness.ts +3 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +23 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +8 -0
- package/packages/aws-adapter/tests/connect.test.ts +158 -32
- package/packages/aws-adapter/tests/connection-counter.test.ts +144 -0
- package/packages/aws-adapter/tests/dynamo-services-store-dynamo.test.ts +183 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +568 -0
- package/packages/aws-adapter/tests/global-setup.ts +28 -1
- package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
- package/packages/aws-adapter/tests/handlers.test.ts +105 -3
- package/packages/aws-adapter/tests/nlb-stream.test.ts +29 -1
- package/packages/aws-adapter/tests/sweeper.test.ts +20 -0
- package/packages/aws-adapter/tests/tables.test.ts +6 -1
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/config-loader.ts +11 -0
- package/packages/cf-adapter/src/connection-do.ts +130 -8
- package/packages/cf-adapter/src/d1-services-store.ts +703 -0
- package/packages/cf-adapter/src/env.ts +8 -0
- package/packages/cf-adapter/src/index.ts +5 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +19 -0
- package/packages/cf-adapter/tests/connection-do-nickserv-d1.test.ts +128 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +130 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +150 -2
- package/packages/cf-adapter/tests/d1-services-store.test.ts +582 -0
- package/packages/cf-adapter/tests/serialize.test.ts +1 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/scripts/generate-build-info.mjs +26 -5
- package/packages/irc-core/src/commands/account-auth.ts +200 -0
- package/packages/irc-core/src/commands/chanserv.ts +1166 -0
- package/packages/irc-core/src/commands/hostserv.ts +522 -0
- package/packages/irc-core/src/commands/index.ts +13 -0
- package/packages/irc-core/src/commands/join.ts +171 -9
- package/packages/irc-core/src/commands/markread.ts +202 -0
- package/packages/irc-core/src/commands/memoserv.ts +319 -0
- package/packages/irc-core/src/commands/mode.ts +96 -4
- package/packages/irc-core/src/commands/nickserv.ts +402 -0
- package/packages/irc-core/src/commands/oper.ts +18 -1
- package/packages/irc-core/src/commands/operserv.ts +346 -0
- package/packages/irc-core/src/commands/pre-away.ts +3 -1
- package/packages/irc-core/src/commands/privmsg.ts +42 -0
- package/packages/irc-core/src/commands/read-marker.ts +8 -8
- package/packages/irc-core/src/commands/registration.ts +78 -12
- package/packages/irc-core/src/commands/sasl.ts +18 -49
- package/packages/irc-core/src/commands/service-aliases.ts +52 -0
- package/packages/irc-core/src/commands/tagmsg.ts +41 -6
- package/packages/irc-core/src/commands/topic.ts +60 -10
- package/packages/irc-core/src/config.ts +36 -5
- package/packages/irc-core/src/effects.ts +56 -1
- package/packages/irc-core/src/ports.ts +1653 -84
- package/packages/irc-core/src/protocol/numerics.ts +8 -0
- package/packages/irc-core/src/state/channel.ts +38 -1
- package/packages/irc-core/src/state/connection.ts +25 -1
- package/packages/irc-core/src/types.ts +48 -12
- package/packages/irc-core/tests/commands/chanserv.test.ts +2335 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +6 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +1006 -0
- package/packages/irc-core/tests/commands/join.test.ts +572 -1
- package/packages/irc-core/tests/commands/markread.test.ts +361 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +654 -0
- package/packages/irc-core/tests/commands/mode.test.ts +381 -2
- package/packages/irc-core/tests/commands/nickserv.test.ts +990 -0
- package/packages/irc-core/tests/commands/oper.test.ts +13 -0
- package/packages/irc-core/tests/commands/operserv.test.ts +656 -0
- package/packages/irc-core/tests/commands/privmsg.test.ts +147 -0
- package/packages/irc-core/tests/commands/read-marker.test.ts +28 -28
- package/packages/irc-core/tests/commands/registration.test.ts +1015 -20
- package/packages/irc-core/tests/commands/sasl.test.ts +229 -12
- package/packages/irc-core/tests/commands/server-info.test.ts +9 -5
- package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +73 -33
- package/packages/irc-core/tests/commands/topic.test.ts +94 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +416 -0
- package/packages/irc-core/tests/config.test.ts +49 -5
- package/packages/irc-core/tests/effects.test.ts +19 -0
- package/packages/irc-core/tests/message-store.test.ts +63 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +582 -0
- package/packages/irc-core/tests/services-store.test.ts +1289 -0
- package/packages/irc-core/tests/state/channel.test.ts +3 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +121 -16
- package/packages/irc-server/src/dispatch.ts +94 -7
- package/packages/irc-server/src/routing.ts +19 -0
- package/packages/irc-server/tests/actor.test.ts +988 -15
- package/packages/irc-server/tests/dispatch.test.ts +270 -2
- package/packages/irc-server/tests/routing.test.ts +6 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +37 -8
- package/packages/irc-test-support/src/index.ts +1 -0
- package/packages/irc-test-support/src/scenarios.ts +21 -6
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +51 -0
- package/packages/irc-test-support/vitest.config.ts +6 -1
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/load-test/src/client.ts +13 -13
- package/tools/load-test/tests/client.test.ts +258 -2
- package/tools/load-test/tests/config.test.ts +39 -0
- package/tools/load-test/tests/harness.test.ts +21 -0
- package/tools/load-test/tests/metrics.test.ts +7 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/tests/close-error.test.ts +40 -0
- package/tools/tcp-ws-forwarder/tests/defensive-branches.test.ts +78 -0
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +53 -2
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -1
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +0 -18
- package/packages/irc-core/tests/read-marker-store.test.ts +0 -108
|
@@ -171,6 +171,20 @@ describe('local-cli IRCv3 WebSocket subprotocol', () => {
|
|
|
171
171
|
await client.close();
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
+
it('falls back to legacy framing when only unsupported protocols are offered', async () => {
|
|
175
|
+
// `selectSubprotocol` returns undefined for an all-unsupported offer, so
|
|
176
|
+
// `handleProtocols` returns false and the ws library completes the
|
|
177
|
+
// handshake with no negotiated subprotocol. Exercises the
|
|
178
|
+
// `?? false` fallback in handleProtocols. The offering client then
|
|
179
|
+
// aborts ("Server sent no subprotocol"), which is expected — the
|
|
180
|
+
// server-side branch is what this covers.
|
|
181
|
+
const client = new SpecClient(`ws://127.0.0.1:${server.port}/`, ['chat']);
|
|
182
|
+
client.ws.on('error', () => {
|
|
183
|
+
// Expected: server returned no subprotocol for an all-unsupported offer.
|
|
184
|
+
});
|
|
185
|
+
await expect(client.opened()).rejects.toThrow(/no subprotocol/iu);
|
|
186
|
+
});
|
|
187
|
+
|
|
174
188
|
it('completes registration with one IRC message per WebSocket message (text mode)', async () => {
|
|
175
189
|
const client = new SpecClient(`ws://127.0.0.1:${server.port}/`, 'text.ircv3.net');
|
|
176
190
|
await client.opened();
|
|
@@ -260,3 +274,81 @@ describe('local-cli IRCv3 WebSocket subprotocol', () => {
|
|
|
260
274
|
});
|
|
261
275
|
}
|
|
262
276
|
});
|
|
277
|
+
|
|
278
|
+
describe('local-cli WebSocket — transport seams', () => {
|
|
279
|
+
let server: LocalServer;
|
|
280
|
+
|
|
281
|
+
beforeAll(async () => {
|
|
282
|
+
server = await startLocalServer({
|
|
283
|
+
port: 0,
|
|
284
|
+
hostname: '127.0.0.1',
|
|
285
|
+
serverName: 'seam.example.com',
|
|
286
|
+
networkName: 'SeamNet',
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
afterAll(async () => {
|
|
291
|
+
await server.close();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('logs a spec-mode actor failure without crashing the server', async () => {
|
|
295
|
+
// Register normally first so the connection has a nick, then swap the
|
|
296
|
+
// runtime send to throw — the next inbound message's dispatch rejects
|
|
297
|
+
// inside receiveTextFrame and the spec-mode `.catch` (server.ts:453)
|
|
298
|
+
// routes it to logError instead of crashing the server.
|
|
299
|
+
const client = new SpecClient(`ws://127.0.0.1:${server.port}/`, 'text.ircv3.net');
|
|
300
|
+
await client.opened();
|
|
301
|
+
await client.send('NICK specErr');
|
|
302
|
+
await client.send('USER specErr 0 * :Spec Err');
|
|
303
|
+
await client.waitFor((l) => l.startsWith(':seam.example.com 001 '));
|
|
304
|
+
|
|
305
|
+
const original = server.runtime.send.bind(server.runtime);
|
|
306
|
+
server.runtime.send = async () => {
|
|
307
|
+
throw new Error('synthetic spec send failure');
|
|
308
|
+
};
|
|
309
|
+
try {
|
|
310
|
+
// PING dispatches a Send effect → synthetic throw → actor catch.
|
|
311
|
+
await client.send('PING :will-throw');
|
|
312
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
313
|
+
} finally {
|
|
314
|
+
server.runtime.send = original;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// The server must still serve fresh spec-mode connections.
|
|
318
|
+
const next = new SpecClient(`ws://127.0.0.1:${server.port}/`, 'text.ircv3.net');
|
|
319
|
+
await next.opened();
|
|
320
|
+
await next.send('NICK specRecover');
|
|
321
|
+
await next.send('USER specRecover 0 * :Spec Recover');
|
|
322
|
+
await next.waitFor((l) => l.startsWith(':seam.example.com 001 '), 2_000);
|
|
323
|
+
await next.close();
|
|
324
|
+
await client.close();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('drops an outbound send to a WebSocket that is no longer OPEN', async () => {
|
|
328
|
+
// Register, then transition the server-side ws to CLOSING. The runtime
|
|
329
|
+
// still tracks the connection until the 'close' event fires on a later
|
|
330
|
+
// tick, so a synchronous dispatch hits the `ws.readyState !== OPEN`
|
|
331
|
+
// guard and returns before calling ws.send (server.ts:423).
|
|
332
|
+
const client = new SpecClient(`ws://127.0.0.1:${server.port}/`, 'text.ircv3.net');
|
|
333
|
+
await client.opened();
|
|
334
|
+
await client.send('NICK dropSend');
|
|
335
|
+
await client.send('USER dropSend 0 * :Drop');
|
|
336
|
+
await client.waitFor((l) => l.startsWith(':seam.example.com 001 '));
|
|
337
|
+
|
|
338
|
+
const connId = await server.runtime.lookupNick('dropSend');
|
|
339
|
+
expect(connId).not.toBeNull();
|
|
340
|
+
const serverWs = [...server.testSockets].find((ws) => ws.readyState === WebSocket.OPEN);
|
|
341
|
+
expect(serverWs).toBeDefined();
|
|
342
|
+
serverWs?.close();
|
|
343
|
+
// Synchronous dispatch before the 'close' cleanup runs: the send handler
|
|
344
|
+
// must observe readyState !== OPEN and no-op without throwing.
|
|
345
|
+
await expect(
|
|
346
|
+
server.runtime.send(connId as string, [{ text: ':seam NOTICE dropSend :dropped' }]),
|
|
347
|
+
).resolves.toBeUndefined();
|
|
348
|
+
|
|
349
|
+
await new Promise<void>((resolve) => {
|
|
350
|
+
client.ws.once('close', () => resolve());
|
|
351
|
+
client.ws.close();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
Served at `/` by the Worker `[assets]` binding (apps/cf-worker/wrangler.toml).
|
|
6
6
|
The build (`scripts/build.mjs`) copies this file verbatim to `dist/index.html`.
|
|
7
|
-
The Kiwi IRC SPA lives at `/
|
|
8
|
-
|
|
7
|
+
The Kiwi IRC SPA lives at `/webclient/`; the rendered docs at `/docs/`
|
|
8
|
+
(built from the `docs/` submodule); this page links through to both plus
|
|
9
|
+
the source repo. Pure static HTML + inline CSS, no JavaScript.
|
|
9
10
|
-->
|
|
10
11
|
<html lang="en">
|
|
11
12
|
<head>
|
|
@@ -135,6 +136,113 @@
|
|
|
135
136
|
border-radius: 4px;
|
|
136
137
|
font-size: 0.9em;
|
|
137
138
|
}
|
|
139
|
+
pre {
|
|
140
|
+
font-family: var(--mono);
|
|
141
|
+
background-color: var(--surface);
|
|
142
|
+
border: 1px solid var(--border);
|
|
143
|
+
border-radius: 8px;
|
|
144
|
+
padding: 14px 16px;
|
|
145
|
+
margin: 12px 0;
|
|
146
|
+
overflow-x: auto;
|
|
147
|
+
line-height: 1.5;
|
|
148
|
+
font-size: 0.88rem;
|
|
149
|
+
}
|
|
150
|
+
pre code {
|
|
151
|
+
background: none;
|
|
152
|
+
border: none;
|
|
153
|
+
padding: 0;
|
|
154
|
+
font-size: inherit;
|
|
155
|
+
}
|
|
156
|
+
section.connect {
|
|
157
|
+
margin: 36px 0 8px;
|
|
158
|
+
padding: 24px 0;
|
|
159
|
+
border-top: 1px solid var(--border);
|
|
160
|
+
}
|
|
161
|
+
section.connect h2 {
|
|
162
|
+
margin: 0 0 12px;
|
|
163
|
+
font-size: 1.4rem;
|
|
164
|
+
letter-spacing: -0.01em;
|
|
165
|
+
}
|
|
166
|
+
section.connect h3 {
|
|
167
|
+
margin: 22px 0 6px;
|
|
168
|
+
font-size: 1.08rem;
|
|
169
|
+
color: var(--accent-hover);
|
|
170
|
+
}
|
|
171
|
+
section.connect p {
|
|
172
|
+
margin: 8px 0;
|
|
173
|
+
color: var(--text);
|
|
174
|
+
}
|
|
175
|
+
section.connect p.lead {
|
|
176
|
+
color: var(--muted);
|
|
177
|
+
}
|
|
178
|
+
section.connect .note {
|
|
179
|
+
color: var(--muted);
|
|
180
|
+
font-size: 0.92rem;
|
|
181
|
+
}
|
|
182
|
+
section.connect .cta {
|
|
183
|
+
margin: 14px 0;
|
|
184
|
+
}
|
|
185
|
+
/* Client picker — pure CSS, no JavaScript. Radio inputs hold the
|
|
186
|
+
selected tab; the labels are the visible buttons arranged in a
|
|
187
|
+
horizontal row; `:checked ~` reveals the matching panel and
|
|
188
|
+
highlights its label. See build-smoke "works without JavaScript". */
|
|
189
|
+
.client-tabs {
|
|
190
|
+
margin: 8px 0 4px;
|
|
191
|
+
}
|
|
192
|
+
.client-tabs > input[type="radio"] {
|
|
193
|
+
position: absolute;
|
|
194
|
+
opacity: 0;
|
|
195
|
+
pointer-events: none;
|
|
196
|
+
}
|
|
197
|
+
.client-tabs .tablist {
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-wrap: wrap;
|
|
200
|
+
gap: 8px;
|
|
201
|
+
margin-bottom: 16px;
|
|
202
|
+
}
|
|
203
|
+
.client-tabs .tablist label {
|
|
204
|
+
padding: 10px 16px;
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
border: 1px solid var(--border);
|
|
207
|
+
border-radius: 8px;
|
|
208
|
+
background-color: var(--surface);
|
|
209
|
+
color: var(--muted);
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
font-size: 0.95rem;
|
|
212
|
+
-webkit-tap-highlight-color: transparent;
|
|
213
|
+
transition: border-color 0.15s, color 0.15s, background-color 0.15s;
|
|
214
|
+
}
|
|
215
|
+
.client-tabs .tablist label:hover {
|
|
216
|
+
border-color: var(--accent);
|
|
217
|
+
color: var(--text);
|
|
218
|
+
}
|
|
219
|
+
.client-tabs .panel {
|
|
220
|
+
display: none;
|
|
221
|
+
}
|
|
222
|
+
.client-tabs .panel h3 {
|
|
223
|
+
margin-top: 0;
|
|
224
|
+
}
|
|
225
|
+
#tab-web:checked ~ .tablist label[for="tab-web"],
|
|
226
|
+
#tab-weechat:checked ~ .tablist label[for="tab-weechat"],
|
|
227
|
+
#tab-hexchat:checked ~ .tablist label[for="tab-hexchat"],
|
|
228
|
+
#tab-irccloud:checked ~ .tablist label[for="tab-irccloud"] {
|
|
229
|
+
background-color: var(--bg);
|
|
230
|
+
color: var(--accent-hover);
|
|
231
|
+
border-color: var(--accent);
|
|
232
|
+
}
|
|
233
|
+
#tab-web:focus-visible ~ .tablist label[for="tab-web"],
|
|
234
|
+
#tab-weechat:focus-visible ~ .tablist label[for="tab-weechat"],
|
|
235
|
+
#tab-hexchat:focus-visible ~ .tablist label[for="tab-hexchat"],
|
|
236
|
+
#tab-irccloud:focus-visible ~ .tablist label[for="tab-irccloud"] {
|
|
237
|
+
outline: 2px solid var(--accent);
|
|
238
|
+
outline-offset: 2px;
|
|
239
|
+
}
|
|
240
|
+
#tab-web:checked ~ #panel-web,
|
|
241
|
+
#tab-weechat:checked ~ #panel-weechat,
|
|
242
|
+
#tab-hexchat:checked ~ #panel-hexchat,
|
|
243
|
+
#tab-irccloud:checked ~ #panel-irccloud {
|
|
244
|
+
display: block;
|
|
245
|
+
}
|
|
138
246
|
.links {
|
|
139
247
|
margin-top: 28px;
|
|
140
248
|
padding: 20px 0;
|
|
@@ -170,7 +278,7 @@
|
|
|
170
278
|
<header>
|
|
171
279
|
<div class="container">
|
|
172
280
|
<h1>Serverless<span class="irc">IRCd</span></h1>
|
|
173
|
-
<p>
|
|
281
|
+
<p>IRC daemon with a serverless deployment model.</p>
|
|
174
282
|
</div>
|
|
175
283
|
</header>
|
|
176
284
|
|
|
@@ -178,28 +286,141 @@
|
|
|
178
286
|
<div class="container">
|
|
179
287
|
<p>
|
|
180
288
|
ServerlessIRCd is an IRC server where the protocol logic lives in a
|
|
181
|
-
|
|
182
|
-
<strong>Cloudflare Workers</strong> (Durable Objects)
|
|
289
|
+
platform-agnostic TypeScript core. Two adapters run that core on
|
|
290
|
+
<strong>Cloudflare Workers</strong> (Durable Objects) and
|
|
183
291
|
<strong>AWS</strong> (API Gateway WebSockets + Lambda + DynamoDB).
|
|
184
|
-
The same code, unchanged, on both.
|
|
185
292
|
</p>
|
|
186
293
|
|
|
187
294
|
<ul class="features">
|
|
188
|
-
<li>
|
|
189
|
-
<li>
|
|
190
|
-
<li>
|
|
191
|
-
<li>
|
|
295
|
+
<li>Reducer-based protocol core: each command is a pure function</li>
|
|
296
|
+
<li>WebSocket and <code>irc+tls</code> (port 6697) transports</li>
|
|
297
|
+
<li>IRCv3 extensions: message-tags, chathistory, SASL, labeled-response</li>
|
|
298
|
+
<li>Per-datum authoritative state held in a single location</li>
|
|
192
299
|
</ul>
|
|
193
300
|
|
|
194
301
|
<div class="cta">
|
|
195
|
-
<a class="primary" href="/
|
|
196
|
-
<a class="secondary" href="docs/">Read the Docs</a>
|
|
302
|
+
<a class="primary" href="/webclient/">Launch Web Client</a>
|
|
197
303
|
</div>
|
|
198
304
|
|
|
305
|
+
<section class="connect">
|
|
306
|
+
<h2>Connect</h2>
|
|
307
|
+
<p class="lead">
|
|
308
|
+
ServerlessIRCd speaks IRC over secure WebSocket
|
|
309
|
+
(<code>wss://</code>, port <code>443</code>) as its primary
|
|
310
|
+
transport, and optionally raw <code>irc+tls</code> on port
|
|
311
|
+
<code>6697</code>. Replace <code>irc.example.com</code> with your
|
|
312
|
+
deployed Worker hostname in the snippets below. This page is
|
|
313
|
+
static and identical across staging and prod.
|
|
314
|
+
</p>
|
|
315
|
+
|
|
316
|
+
<div class="client-tabs">
|
|
317
|
+
<input type="radio" name="client-tab" id="tab-web" checked />
|
|
318
|
+
<input type="radio" name="client-tab" id="tab-weechat" />
|
|
319
|
+
<input type="radio" name="client-tab" id="tab-hexchat" />
|
|
320
|
+
<input type="radio" name="client-tab" id="tab-irccloud" />
|
|
321
|
+
|
|
322
|
+
<div
|
|
323
|
+
class="tablist"
|
|
324
|
+
role="tablist"
|
|
325
|
+
aria-label="IRC client connect instructions"
|
|
326
|
+
>
|
|
327
|
+
<label for="tab-web" class="tab" role="tab">Web client</label>
|
|
328
|
+
<label for="tab-weechat" class="tab" role="tab">WeeChat</label>
|
|
329
|
+
<label for="tab-hexchat" class="tab" role="tab"
|
|
330
|
+
>HexChat / irssi</label
|
|
331
|
+
>
|
|
332
|
+
<label for="tab-irccloud" class="tab" role="tab"
|
|
333
|
+
>IRCCloud / TheLounge</label
|
|
334
|
+
>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<div
|
|
338
|
+
class="panel"
|
|
339
|
+
id="panel-web"
|
|
340
|
+
role="tabpanel"
|
|
341
|
+
aria-labelledby="tab-web"
|
|
342
|
+
>
|
|
343
|
+
<p>
|
|
344
|
+
<code>wss</code> on port <code>443</code>, same origin as this
|
|
345
|
+
page. No client install or bridge required.
|
|
346
|
+
</p>
|
|
347
|
+
<div class="cta">
|
|
348
|
+
<a class="primary" href="/webclient/">Launch Web Client</a>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div
|
|
353
|
+
class="panel"
|
|
354
|
+
id="panel-weechat"
|
|
355
|
+
role="tabpanel"
|
|
356
|
+
aria-labelledby="tab-weechat"
|
|
357
|
+
>
|
|
358
|
+
<p>
|
|
359
|
+
Connect over <code>wss</code> (port <code>443</code>) using the
|
|
360
|
+
snippet below, or directly over <code>irc+tls</code> on port
|
|
361
|
+
<code>6697</code> when the TCP+TLS surface is deployed.
|
|
362
|
+
</p>
|
|
363
|
+
<pre><code>/server add ircd irc.example.com/443
|
|
364
|
+
/set irc.server.ircd.ssl on
|
|
365
|
+
/connect ircd
|
|
366
|
+
/join #test</code></pre>
|
|
367
|
+
<p class="note">
|
|
368
|
+
For the <code>irc+tls :6697</code> path, swap the port above for
|
|
369
|
+
<code>6697</code>. The TCP surface is a separate deployment
|
|
370
|
+
(Cloudflare Spectrum + Container); see the
|
|
371
|
+
<a
|
|
372
|
+
href="https://gitea.com/ServerlessIRCd/ServerlessIRCd/wiki/Cloudflare-TCP-Deployment"
|
|
373
|
+
>Cloudflare TCP Deployment Guide</a
|
|
374
|
+
>.
|
|
375
|
+
</p>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<div
|
|
379
|
+
class="panel"
|
|
380
|
+
id="panel-hexchat"
|
|
381
|
+
role="tabpanel"
|
|
382
|
+
aria-labelledby="tab-hexchat"
|
|
383
|
+
>
|
|
384
|
+
<p>
|
|
385
|
+
These TCP-only IRC clients do not support WebSocket natively.
|
|
386
|
+
Use <code>tools/tcp-ws-forwarder</code> to bridge them: it
|
|
387
|
+
listens on a local TCP port and forwards each IRC line over
|
|
388
|
+
<code>wss</code> to the Worker, negotiating the
|
|
389
|
+
<code>binary.ircv3.net</code> subprotocol upstream.
|
|
390
|
+
</p>
|
|
391
|
+
<pre><code>pnpm --filter tcp-ws-forwarder start \
|
|
392
|
+
--target wss://irc.example.com/ --listen-port 16667</code></pre>
|
|
393
|
+
<p class="note">
|
|
394
|
+
Then point HexChat / irssi at <code>127.0.0.1:16667</code>
|
|
395
|
+
(TLS off — the bridge is local). Bridging instructions
|
|
396
|
+
are in the
|
|
397
|
+
<a
|
|
398
|
+
href="https://gitea.com/ServerlessIRCd/ServerlessIRCd/wiki/WebClientGuide"
|
|
399
|
+
>Web Client Guide</a
|
|
400
|
+
>.
|
|
401
|
+
</p>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<div
|
|
405
|
+
class="panel"
|
|
406
|
+
id="panel-irccloud"
|
|
407
|
+
role="tabpanel"
|
|
408
|
+
aria-labelledby="tab-irccloud"
|
|
409
|
+
>
|
|
410
|
+
<p>
|
|
411
|
+
Both support WebSocket natively. Add a network pointing at
|
|
412
|
+
<code>wss://irc.example.com/</code> (port <code>443</code>, TLS
|
|
413
|
+
on); no bridge required.
|
|
414
|
+
</p>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</section>
|
|
418
|
+
|
|
199
419
|
<div class="links">
|
|
200
|
-
<a href="
|
|
201
|
-
<a href="https://
|
|
202
|
-
<a href="/
|
|
420
|
+
<a href="/docs/">Docs</a>
|
|
421
|
+
<a href="https://gitea.com/ServerlessIRCd/ServerlessIRCd">Source Code (Gitea)</a>
|
|
422
|
+
<a href="https://gitea.com/ServerlessIRCd/ServerlessIRCd/issues">Issue Tracker</a>
|
|
423
|
+
<a href="/webclient/">Web IRC Client</a>
|
|
203
424
|
</div>
|
|
204
425
|
</div>
|
|
205
426
|
</main>
|
|
@@ -208,7 +429,7 @@
|
|
|
208
429
|
<div class="container">
|
|
209
430
|
<p>
|
|
210
431
|
ServerlessIRCd ·
|
|
211
|
-
<a href="https://
|
|
432
|
+
<a href="https://gitea.com/ServerlessIRCd/ServerlessIRCd">Gitea</a> ·
|
|
212
433
|
BSD-3-Clause License
|
|
213
434
|
</p>
|
|
214
435
|
</div>
|
package/apps/web/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "Web client workspace: Kiwi IRC SPA served at /
|
|
5
|
+
"description": "Web client workspace: Kiwi IRC SPA served at /webclient/ and project landing page at /",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"scripts": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^26.1.1",
|
|
23
23
|
"@vitest/coverage-v8": "^4.1.0",
|
|
24
|
+
"marked": "^18.0.9",
|
|
24
25
|
"rimraf": "^6.0.0",
|
|
25
26
|
"typescript": "^5.6.0",
|
|
26
27
|
"vitest": "^4.1.0"
|
|
@@ -5,17 +5,22 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { resolveConfigFile } from '../src/build-env.ts';
|
|
7
7
|
import { parseKiwiConfig } from '../src/config-schema.ts';
|
|
8
|
+
import { buildDocs } from '../src/render-docs.ts';
|
|
8
9
|
|
|
9
10
|
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
11
|
const upstream = path.resolve(pkgRoot, 'upstream');
|
|
11
12
|
const dist = path.resolve(pkgRoot, 'dist');
|
|
12
|
-
const appDist = path.join(dist, '
|
|
13
|
+
const appDist = path.join(dist, 'webclient');
|
|
13
14
|
const staticDir = path.join(pkgRoot, 'static');
|
|
15
|
+
// The `docs/` submodule lives at the monorepo root (two levels up from
|
|
16
|
+
// apps/web). Rendered to dist/docs/ by the docs build step (see step 6).
|
|
17
|
+
const docsSrc = path.resolve(pkgRoot, '..', '..', 'docs');
|
|
18
|
+
const docsDist = path.join(dist, 'docs');
|
|
14
19
|
|
|
15
|
-
// The SPA is served at /
|
|
16
|
-
//
|
|
17
|
-
// from /
|
|
18
|
-
const PUBLIC_PATH = '/
|
|
20
|
+
// The SPA is served at /webclient/ on the Worker (the [assets] binding serves
|
|
21
|
+
// `dist/`). All hashed asset URLs must be /webclient/-prefixed so they load
|
|
22
|
+
// from /webclient/static/... rather than /static/... (which would 404 at root).
|
|
23
|
+
const PUBLIC_PATH = '/webclient/';
|
|
19
24
|
|
|
20
25
|
// Yarn runs from a pnpm lifecycle script. Kiwi v1.7.1's package.json has no
|
|
21
26
|
// `packageManager` field, so yarn classic walks up and finds the workspace
|
|
@@ -44,16 +49,32 @@ if (!existsSync(upstream)) {
|
|
|
44
49
|
);
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
// The `docs/` submodule must be checked out — step 6 renders every docs/*.md
|
|
53
|
+
// to dist/docs/. Mirrors the Kiwi upstream guard above so a stale/partial
|
|
54
|
+
// submodule checkout fails loudly with the recovery command instead of
|
|
55
|
+
// producing an empty docs/ section on the deployed site.
|
|
56
|
+
if (!existsSync(docsSrc)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Docs submodule missing at ${docsSrc}. Run \`git submodule update --init docs\` first.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
// 1. Install upstream deps. Kiwi ships a yarn.lock; --frozen-lockfile gives a
|
|
48
|
-
// reproducible tree. Contributors get yarn via `corepack enable`.
|
|
49
|
-
|
|
63
|
+
// reproducible tree. Contributors get yarn via `corepack enable`. The
|
|
64
|
+
// guard checks for the actual binary the build invokes, not just the
|
|
65
|
+
// `node_modules` directory — a stale/partial install (interrupted, or
|
|
66
|
+
// populated by a foreign package manager that skipped `.bin/` linking)
|
|
67
|
+
// leaves the directory present but `vue-cli-service` unresolvable, which
|
|
68
|
+
// would otherwise crash the build at step 2 with a confusing exit 127.
|
|
69
|
+
const vueCliBin = path.join(upstream, 'node_modules', '.bin', 'vue-cli-service');
|
|
70
|
+
if (!existsSync(vueCliBin)) {
|
|
50
71
|
runYarn(['install', '--frozen-lockfile', '--ignore-engines']);
|
|
51
72
|
}
|
|
52
73
|
|
|
53
|
-
// 2. Build with publicPath=/
|
|
54
|
-
// temporarily patch vue.config.js so webpack emits /
|
|
55
|
-
// URLs and the runtime public_path matches the mount point. Restored
|
|
56
|
-
// `finally` so the submodule stays clean.
|
|
74
|
+
// 2. Build with publicPath=/webclient/. Upstream pins publicPath:'' (relative);
|
|
75
|
+
// we temporarily patch vue.config.js so webpack emits /webclient/-prefixed
|
|
76
|
+
// asset URLs and the runtime public_path matches the mount point. Restored
|
|
77
|
+
// in `finally` so the submodule stays clean.
|
|
57
78
|
const vueConfigPath = path.join(upstream, 'vue.config.js');
|
|
58
79
|
const originalVueConfig = await readFile(vueConfigPath, 'utf8');
|
|
59
80
|
try {
|
|
@@ -67,14 +88,15 @@ try {
|
|
|
67
88
|
await writeFile(vueConfigPath, originalVueConfig);
|
|
68
89
|
}
|
|
69
90
|
|
|
70
|
-
// 3. Copy the built SPA into dist/
|
|
91
|
+
// 3. Copy the built SPA into dist/webclient/.
|
|
71
92
|
await rm(appDist, { recursive: true, force: true });
|
|
72
93
|
await mkdir(appDist, { recursive: true });
|
|
73
94
|
await cp(path.join(upstream, 'dist'), appDist, { recursive: true });
|
|
74
95
|
|
|
75
96
|
// 3b. Webpack applies publicPath to the bundles it emits, but template-hardcoded
|
|
76
97
|
// refs (e.g. the favicon) stay relative. Prefix every bare `static/...`
|
|
77
|
-
// reference so all asset URLs resolve to /
|
|
98
|
+
// reference so all asset URLs resolve to /webclient/static/... when served
|
|
99
|
+
// at /webclient/.
|
|
78
100
|
const indexDest = path.join(appDist, 'index.html');
|
|
79
101
|
let indexHtml = await readFile(indexDest, 'utf8');
|
|
80
102
|
indexHtml = indexHtml.replace(/((?:src|href)=["'])(static\/)/g, `$1${PUBLIC_PATH}$2`);
|
|
@@ -109,8 +131,8 @@ await writeFile(
|
|
|
109
131
|
|
|
110
132
|
// 5. Landing page: copy the static landing page (landing/index.html) to
|
|
111
133
|
// dist/index.html so the Worker `[assets]` binding serves it at `/`. Pure
|
|
112
|
-
// static HTML + inline CSS (no JS); links to /
|
|
113
|
-
// the source repo.
|
|
134
|
+
// static HTML + inline CSS (no JS); links to /webclient/ (the SPA),
|
|
135
|
+
// /docs/ (the rendered docs built in step 6), and the source repo.
|
|
114
136
|
await mkdir(dist, { recursive: true });
|
|
115
137
|
const landingSrc = path.join(pkgRoot, 'landing', 'index.html');
|
|
116
138
|
if (!existsSync(landingSrc)) {
|
|
@@ -120,6 +142,14 @@ if (!existsSync(landingSrc)) {
|
|
|
120
142
|
}
|
|
121
143
|
await cp(landingSrc, path.join(dist, 'index.html'));
|
|
122
144
|
|
|
145
|
+
// 6. Docs: render every docs/*.md to standalone HTML under dist/docs/ so the
|
|
146
|
+
// Worker `[assets]` binding serves them at /docs/<name>.html (and /docs/
|
|
147
|
+
// for the index — Home.md becomes index.html). Requires the `docs/`
|
|
148
|
+
// submodule (guarded at step 0). Zero Worker changes — the existing
|
|
149
|
+
// [assets] binding already serves dist/ with not_found_handling = "none".
|
|
150
|
+
const docsResult = await buildDocs({ srcDir: docsSrc, outDir: docsDist });
|
|
151
|
+
|
|
123
152
|
console.log(
|
|
124
|
-
`Kiwi SPA built -> dist/
|
|
153
|
+
`Kiwi SPA built -> dist/webclient/ (publicPath=${PUBLIC_PATH}, config=${configFileName}, env=${configEnv})`,
|
|
125
154
|
);
|
|
155
|
+
console.log(`Docs rendered -> dist/docs/ (${docsResult.emitted.length} pages, index=index.html)`);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Extracted from the build script so the `--env` → config-file mapping is
|
|
5
5
|
* unit-testable without running the (slow) Kiwi upstream build. The build
|
|
6
6
|
* script delegates here and then validates the selected file against
|
|
7
|
-
* {@link parseKiwiConfig} before baking it into `dist/
|
|
7
|
+
* {@link parseKiwiConfig} before baking it into `dist/webclient/static/config.json`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/** argv values accepted by `--env`; mapped to a `static/config.<env>.json` file. */
|
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
* supply to drive the embedded Kiwi SPA.
|
|
5
5
|
*
|
|
6
6
|
* `build.mjs` runs {@link parseKiwiConfig} against the selected env config
|
|
7
|
-
* before baking it into `dist/
|
|
8
|
-
* fails the build with a readable aggregated error rather than
|
|
9
|
-
* broken SPA. The schema only models the fields ServerlessIRCd
|
|
10
|
-
* (server, port, channel, WS path); the rest of Kiwi's surface
|
|
11
|
-
* through untouched.
|
|
7
|
+
* before baking it into `dist/webclient/static/config.json`, so a malformed
|
|
8
|
+
* config fails the build with a readable aggregated error rather than
|
|
9
|
+
* shipping a broken SPA. The schema only models the fields ServerlessIRCd
|
|
10
|
+
* cares about (server, port, channel, WS path); the rest of Kiwi's surface
|
|
11
|
+
* is passed through untouched.
|
|
12
12
|
*
|
|
13
13
|
* Hosting layout reminder:
|
|
14
14
|
* - The Worker's WebSocket endpoint is at `/` (root), so Kiwi's
|
|
15
15
|
* `startupOptions.path` — when supplied — MUST be `/`.
|
|
16
|
-
* - The SPA itself is served from `/
|
|
16
|
+
* - The SPA itself is served from `/webclient/` by the Worker `[assets]`
|
|
17
17
|
* binding (a separate concern handled by the public-path patch in
|
|
18
18
|
* `build.mjs`); that is independent of the WS path modelled here.
|
|
19
19
|
*/
|