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
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
statSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
14
|
+
import { buildDocs, renderMarkdown, rewriteDocLinks } from '../src/render-docs';
|
|
15
|
+
|
|
16
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const docsSrc = path.resolve(pkgRoot, '..', '..', 'docs');
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// renderMarkdown — GFM rendering of markdown source
|
|
21
|
+
// ============================================================================
|
|
22
|
+
describe('renderMarkdown', () => {
|
|
23
|
+
it('renders an ATX H1 heading as <h1>', () => {
|
|
24
|
+
const html = renderMarkdown('# Title');
|
|
25
|
+
expect(html).toMatch(/<h1[^>]*>\s*Title\s*<\/h1>/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('renders a fenced code block as <pre><code>', () => {
|
|
29
|
+
const md = ['```ts', 'const x = 1;', '```'].join('\n');
|
|
30
|
+
const html = renderMarkdown(md);
|
|
31
|
+
expect(html).toMatch(/<pre[^>]*><code[^>]*>/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders GFM tables', () => {
|
|
35
|
+
const md = ['| a | b |', '| --- | --- |', '| 1 | 2 |'].join('\n');
|
|
36
|
+
const html = renderMarkdown(md);
|
|
37
|
+
expect(html).toContain('<table>');
|
|
38
|
+
expect(html).toContain('<th');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('renders inline code', () => {
|
|
42
|
+
const html = renderMarkdown('Use `npm` to install.');
|
|
43
|
+
expect(html).toContain('<code>npm</code>');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// rewriteDocLinks — intra-docs link rewriting
|
|
49
|
+
// ============================================================================
|
|
50
|
+
describe('rewriteDocLinks', () => {
|
|
51
|
+
const known = new Set([
|
|
52
|
+
'Services',
|
|
53
|
+
'Home',
|
|
54
|
+
'ADR-Index',
|
|
55
|
+
'ADR-001-pure-reducers-and-effect-system',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
it('rewrites a relative OtherDoc.md href to OtherDoc.html', () => {
|
|
59
|
+
const input = '<a href="./Services.md">Services</a>';
|
|
60
|
+
const out = rewriteDocLinks(input, known);
|
|
61
|
+
expect(out).toContain('href="Services.html"');
|
|
62
|
+
expect(out).not.toContain('Services.md');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('rewrites a bare wikilink-style href (no extension) when the slug is a known doc', () => {
|
|
66
|
+
const input = '<a href="Services">Services</a>';
|
|
67
|
+
const out = rewriteDocLinks(input, known);
|
|
68
|
+
expect(out).toContain('href="Services.html"');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('leaves unknown bare hrefs alone (could be an external path)', () => {
|
|
72
|
+
const input = '<a href="SomeUnknownPage">x</a>';
|
|
73
|
+
const out = rewriteDocLinks(input, new Set(['Services']));
|
|
74
|
+
expect(out).toContain('href="SomeUnknownPage"');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('preserves an anchor fragment when rewriting', () => {
|
|
78
|
+
const input = '<a href="./Services.md#section">link</a>';
|
|
79
|
+
const out = rewriteDocLinks(input, known);
|
|
80
|
+
expect(out).toContain('href="Services.html#section"');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not touch external (http/https) links', () => {
|
|
84
|
+
const input = '<a href="https://example.com/foo.md">external</a>';
|
|
85
|
+
const out = rewriteDocLinks(input, known);
|
|
86
|
+
expect(out).toContain('href="https://example.com/foo.md"');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('does not touch pure-anchor (#section) links', () => {
|
|
90
|
+
const input = '<a href="#section">anchor</a>';
|
|
91
|
+
const out = rewriteDocLinks(input, known);
|
|
92
|
+
expect(out).toContain('href="#section"');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('handles a trailing anchor on a bare wikilink-style href', () => {
|
|
96
|
+
const input = '<a href="Home#pages">Home</a>';
|
|
97
|
+
const out = rewriteDocLinks(input, known);
|
|
98
|
+
expect(out).toContain('href="Home.html#pages"');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ============================================================================
|
|
103
|
+
// buildDocs — end-to-end render pipeline (against the real docs/ submodule)
|
|
104
|
+
// ============================================================================
|
|
105
|
+
describe('buildDocs against docs/ submodule', () => {
|
|
106
|
+
let outDir: string;
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
outDir = mkdtempSync(path.join(os.tmpdir(), 'render-docs-'));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('throws a clear error when the docs source directory is missing', async () => {
|
|
117
|
+
await expect(buildDocs({ srcDir: path.join(outDir, 'nope'), outDir })).rejects.toThrow(
|
|
118
|
+
/docs.*missing|missing.*docs|submodule/iu,
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('throws a clear error when the docs source directory is present but empty', async () => {
|
|
123
|
+
// Mirrors the build-script guard: an empty `docs/` checkout (submodule
|
|
124
|
+
// init'd but not populated) must fail loudly with the recovery command,
|
|
125
|
+
// not silently produce a docs section with zero pages.
|
|
126
|
+
const empty = mkdirSync(path.join(outDir, 'empty-docs'), { recursive: true });
|
|
127
|
+
if (empty === undefined) {
|
|
128
|
+
throw new Error('test setup failed: could not create empty-docs dir');
|
|
129
|
+
}
|
|
130
|
+
await expect(buildDocs({ srcDir: empty, outDir })).rejects.toThrow(/empty|submodule/iu);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('emits an index.html under the output directory', async () => {
|
|
134
|
+
await buildDocs({ srcDir: docsSrc, outDir });
|
|
135
|
+
const idx = path.join(outDir, 'index.html');
|
|
136
|
+
expect(existsSync(idx)).toBe(true);
|
|
137
|
+
expect(statSync(idx).size).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('emits one HTML file per markdown source (non-empty output directory)', async () => {
|
|
141
|
+
const result = await buildDocs({ srcDir: docsSrc, outDir });
|
|
142
|
+
expect(result.emitted.length).toBeGreaterThanOrEqual(20);
|
|
143
|
+
for (const f of result.emitted) {
|
|
144
|
+
expect(f.endsWith('.html')).toBe(true);
|
|
145
|
+
expect(existsSync(path.join(outDir, f))).toBe(true);
|
|
146
|
+
}
|
|
147
|
+
expect(readdirSync(outDir).filter((f) => f.endsWith('.html')).length).toBe(
|
|
148
|
+
result.emitted.length,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('renders Services.html with an <h1> and at least one <code> block', async () => {
|
|
153
|
+
await buildDocs({ srcDir: docsSrc, outDir });
|
|
154
|
+
const html = readFileSync(path.join(outDir, 'Services.html'), 'utf8');
|
|
155
|
+
expect(html).toMatch(/<h1[^>]*>/);
|
|
156
|
+
expect(html).toMatch(/<pre[^>]*><code/);
|
|
157
|
+
// The raw markdown marker should not survive rendering.
|
|
158
|
+
expect(html).not.toMatch(/^# IRC Services Reference/m);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('emits Home as the index (index.html)', async () => {
|
|
162
|
+
await buildDocs({ srcDir: docsSrc, outDir });
|
|
163
|
+
const html = readFileSync(path.join(outDir, 'index.html'), 'utf8');
|
|
164
|
+
// Home.md's "Welcome to the Wiki." lead-in should be present.
|
|
165
|
+
expect(html).toMatch(/Welcome to the Wiki/i);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('rewrites intra-docs links to .html in the rendered output (no .md hrefs remain)', async () => {
|
|
169
|
+
await buildDocs({ srcDir: docsSrc, outDir });
|
|
170
|
+
const files = readdirSync(outDir).filter((f) => f.endsWith('.html'));
|
|
171
|
+
expect(files.length).toBeGreaterThan(0);
|
|
172
|
+
for (const f of files) {
|
|
173
|
+
const html = readFileSync(path.join(outDir, f), 'utf8');
|
|
174
|
+
// No intra-docs .md href may remain. (External https://…/foo.md is fine.)
|
|
175
|
+
const intraMd = html.match(/href="(?!https?:|mailto:|#)[^"]*\.md[^"]*"/g) ?? [];
|
|
176
|
+
expect(intraMd).toEqual([]);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('emits a shared stylesheet (referenced by every page)', async () => {
|
|
181
|
+
await buildDocs({ srcDir: docsSrc, outDir });
|
|
182
|
+
const files = readdirSync(outDir).filter((f) => f.endsWith('.html'));
|
|
183
|
+
// Every page links the same stylesheet.
|
|
184
|
+
for (const f of files) {
|
|
185
|
+
const html = readFileSync(path.join(outDir, f), 'utf8');
|
|
186
|
+
expect(html).toMatch(/<link[^>]+rel=["']stylesheet["']/);
|
|
187
|
+
}
|
|
188
|
+
// And the stylesheet actually exists in the output.
|
|
189
|
+
const css = readdirSync(outDir).filter((f) => f.endsWith('.css'));
|
|
190
|
+
expect(css.length).toBeGreaterThanOrEqual(1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('emits an ADR-Index page that links each ADR via a rewritten .html href', async () => {
|
|
194
|
+
await buildDocs({ srcDir: docsSrc, outDir });
|
|
195
|
+
const html = readFileSync(path.join(outDir, 'ADR-Index.html'), 'utf8');
|
|
196
|
+
expect(html).toMatch(/ADR-001-pure-reducers-and-effect-system\.html/);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -373,8 +373,9 @@ scheme (post-v1).
|
|
|
373
373
|
│
|
|
374
374
|
│ all-or-nothing → races structurally impossible
|
|
375
375
|
│
|
|
376
|
-
|
|
377
|
-
dispatch Broadcast(JOIN), 353 NAMES, 366 END-NAMES
|
|
376
|
+
▼
|
|
377
|
+
dispatch Broadcast(JOIN), 353 NAMES, 366 END-NAMES,
|
|
378
|
+
and when a topic is set: 332 RPL_TOPIC, 333 RPL_TOPICWHOTIME
|
|
378
379
|
```
|
|
379
380
|
|
|
380
381
|
A `CancellationException` from `TransactWriteItems` maps to the
|
package/docs/AWS-Deployment.md
CHANGED
|
@@ -530,21 +530,24 @@ namespaces).
|
|
|
530
530
|
|
|
531
531
|
## 8. Secrets
|
|
532
532
|
|
|
533
|
-
**
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
`
|
|
533
|
+
**The Lambda consumes one optional secret: `SERVER_PASSWORD`.** When
|
|
534
|
+
set (as a Lambda env var, conventionally sourced from AWS Secrets
|
|
535
|
+
Manager or SSM Parameter Store), every connection must supply the
|
|
536
|
+
matching `PASS <value>` (or SASL-identify) before `001 RPL_WELCOME`;
|
|
537
|
+
when unset/empty the gate is disabled (the default). The adapter's
|
|
538
|
+
config loader reads `process.env.SERVER_PASSWORD` at cold start via
|
|
539
|
+
`loadServerConfigFromLambdaEnv` and threads it through to
|
|
540
|
+
`ServerConfig.serverPassword`.
|
|
539
541
|
|
|
540
|
-
|
|
542
|
+
The typical provisioning pattern:
|
|
541
543
|
|
|
542
544
|
1. Create the secret in Secrets Manager (or a `SecureString`
|
|
543
545
|
parameter in SSM).
|
|
544
546
|
2. Grant the Lambda role `secretsmanager:GetSecretValue` (or
|
|
545
547
|
`ssm:GetParameter`) scoped to that one secret ARN.
|
|
546
548
|
3. Read it inside `buildDepsFromEnv` (cold start) — never inside the
|
|
547
|
-
per-frame hot path
|
|
549
|
+
per-frame hot path — and expose the value to the Lambda env as
|
|
550
|
+
`SERVER_PASSWORD`.
|
|
548
551
|
|
|
549
552
|
```bash
|
|
550
553
|
aws secretsmanager create-secret \
|
|
@@ -552,6 +555,16 @@ aws secretsmanager create-secret \
|
|
|
552
555
|
--secret-string "$(openssl rand -base64 32)"
|
|
553
556
|
```
|
|
554
557
|
|
|
558
|
+
**SASL short-circuit:** a connection that has authenticated an account
|
|
559
|
+
via SASL (`AUTHENTICATE PLAIN` / `EXTERNAL`) is exempt from the server
|
|
560
|
+
password — the reducer's gate treats `state.account !== undefined` as
|
|
561
|
+
already authorised. Oper creds (`OPER_USER` / `OPER_PASSWORD`) follow
|
|
562
|
+
the same secret pattern; SASL account credentials are **not** secrets —
|
|
563
|
+
they are persisted in the `Accounts` DynamoDB table (see §8.1). **Never
|
|
564
|
+
log `SERVER_PASSWORD`** — treat it as a shared deployment secret,
|
|
565
|
+
distinct from per-account SASL credentials (which go through the hashed
|
|
566
|
+
`Accounts` table).
|
|
567
|
+
|
|
555
568
|
### 8.1 SASL accounts (`Accounts` table + seed tooling)
|
|
556
569
|
|
|
557
570
|
SASL PLAIN credentials live in the `Accounts` DynamoDB table as
|
|
@@ -365,11 +365,11 @@ managed separately in the dashboard.
|
|
|
365
365
|
|
|
366
366
|
## 8. Secrets
|
|
367
367
|
|
|
368
|
-
**
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
368
|
+
**The Worker consumes one optional secret binding: `SERVER_PASSWORD`.**
|
|
369
|
+
When set, every connection must supply the matching `PASS <value>` (or
|
|
370
|
+
SASL-identify) before `001 RPL_WELCOME`; when unset/empty the gate is
|
|
371
|
+
disabled (the default). Configure it as a wrangler secret, never a
|
|
372
|
+
plaintext `[vars]` entry:
|
|
373
373
|
|
|
374
374
|
```bash
|
|
375
375
|
wrangler secret put SERVER_PASSWORD --env staging
|
|
@@ -377,10 +377,26 @@ wrangler secret put SERVER_PASSWORD --env staging
|
|
|
377
377
|
# NOT in wrangler.toml or the repo.
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
+
Unset (`wrangler secret delete SERVER_PASSWORD`) disables the gate.
|
|
381
|
+
**SASL short-circuit:** a connection that has authenticated an account
|
|
382
|
+
via SASL (`AUTHENTICATE PLAIN` / `EXTERNAL`) is exempt from the
|
|
383
|
+
server password — the reducer's gate treats `state.account !== undefined`
|
|
384
|
+
as already authorised, so a deployment with both `SERVER_PASSWORD` and
|
|
385
|
+
configured SASL accounts does not need to hand the shared password to
|
|
386
|
+
identified users.
|
|
387
|
+
|
|
388
|
+
Oper creds (`OPER_USER` / `OPER_PASSWORD`) are still on the roadmap;
|
|
389
|
+
when they ship they will follow the same wrangler-secret pattern. SASL
|
|
390
|
+
account credentials are **not** secrets — they are persisted as scrypt
|
|
391
|
+
hashes in D1 (see §8.1 below).
|
|
392
|
+
|
|
380
393
|
Secrets are visible to the Worker as plain `env.SECRET_NAME` bindings,
|
|
381
394
|
but their values are never written to disk, the bundle, or the
|
|
382
395
|
dashboard's variable listing. To rotate, run `wrangler secret put`
|
|
383
|
-
again; to remove, `wrangler secret delete <NAME>`.
|
|
396
|
+
again; to remove, `wrangler secret delete <NAME>`. **Never log
|
|
397
|
+
`SERVER_PASSWORD`** — treat it as a shared deployment secret, distinct
|
|
398
|
+
from per-account SASL credentials (which go through the hashed D1
|
|
399
|
+
store).
|
|
384
400
|
|
|
385
401
|
**Never** put credentials, API tokens, or password hashes in
|
|
386
402
|
`wrangler.toml`, in `[vars]`, or in committed files. The
|
package/docs/PlanExtensions.md
CHANGED
|
@@ -253,9 +253,30 @@ connection is not identified as. The dispatch layer resolves it against
|
|
|
253
253
|
Already-present seams the services reuse:
|
|
254
254
|
|
|
255
255
|
- `AccountStore.verify('PLAIN', ...)` (TICKET-026 ✅) → a successful
|
|
256
|
-
NickServ `IDENTIFY
|
|
257
|
-
connection is now account X"
|
|
258
|
-
|
|
256
|
+
NickServ `IDENTIFY`, a SASL PLAIN login, and a PASS-based account login
|
|
257
|
+
(TICKET-152 ✅) all reduce to "this connection is now account X". The
|
|
258
|
+
shared `applyAccountSuccess(state, ctx, account)` helper stamps the
|
|
259
|
+
account, sets the `+r` user mode, restores per-account state (read
|
|
260
|
+
markers, persisted away, queued memos), and emits `900 RPL_LOGGEDIN` +
|
|
261
|
+
`903 RPL_SASLSUCCESS` plus the `ACCOUNT` broadcast to
|
|
262
|
+
`account-notify`-capable peers.
|
|
263
|
+
- **PASS-based account auth (TICKET-152 ✅)** — a third account-login
|
|
264
|
+
entry point alongside SASL and NickServ `IDENTIFY`. A client that sends
|
|
265
|
+
`NICK <nick>` then `PASS <nick>:<password>` (then `USER`) is identified
|
|
266
|
+
to `ctx.accounts` at registration completion without speaking SASL. The
|
|
267
|
+
`<nick>:<password>` literal matches the `SASL_ACCOUNTS` seed format. The
|
|
268
|
+
payload nick (left of the first `:`) is the account name and MUST equal
|
|
269
|
+
`state.nick` at registration completion. Order of precedence at
|
|
270
|
+
registration is: (1) already identified (SASL/PASS-auth) → gate
|
|
271
|
+
satisfied; (2) `<nick>:<password>` verifies against `ctx.accounts` →
|
|
272
|
+
account set; (3) bare `PASS` matches `serverPassword` → gate satisfied;
|
|
273
|
+
(4) else `464 ERR_PASSWDMISMATCH` + disconnect. A bare `PASS <value>`
|
|
274
|
+
(no colon) is always a server-password candidate, never an account
|
|
275
|
+
credential; when `ctx.accounts` is unbound the `<nick>:<password>` form
|
|
276
|
+
is ignored and behaviour collapses to the server-password gate. Like
|
|
277
|
+
SASL, PASS-auth runs through `applyAccountSuccess` and therefore stamps
|
|
278
|
+
`+r`; a failed verify reveals nothing — unknown nick and wrong password
|
|
279
|
+
are indistinguishable (a fixed dummy verify equalises the failure path).
|
|
259
280
|
- `MessageStore` (TICKET-063 ✅) → MemoServ's `SEND` is a record into
|
|
260
281
|
a per-account memo queue; delivery is a fanout at identify time.
|
|
261
282
|
- `clock` / `IdFactory` ports → memo ids, enforcement grace timers.
|
|
@@ -464,6 +485,52 @@ follows strict TDD (Red → Green → Refactor) per `CLAUDE.md` /
|
|
|
464
485
|
`seedReadMarkers`; `CHATHISTORY BEFORE` with no pivot falls back to the
|
|
465
486
|
connection's marker. TICKET-122 should fold this store into the unified
|
|
466
487
|
services `UserStore` when it lands.
|
|
488
|
+
- **Divergence vs. spec compliance (TICKET-150 follow-up, shipped):**
|
|
489
|
+
TICKET-118 shipped **channel-wide** mark TAGMSG fanout for pragmatic parity
|
|
490
|
+
with the `draft/typing` precedent — every `message-tags` OR
|
|
491
|
+
`draft/read-marker` peer in the channel received the mark. The ircv3
|
|
492
|
+
`draft/read-marker` spec, however, mandates that a read marker **MUST NOT be
|
|
493
|
+
disclosed to other users**: a client's own mark is private to its account.
|
|
494
|
+
The Phase 14 follow-up (account-scoped `draft/read-marker` fanout) restored
|
|
495
|
+
spec compliance with two behaviour changes:
|
|
496
|
+
|
|
497
|
+
1. **Identified originator → account-scoped fanout.** The
|
|
498
|
+
`tagmsgChannelReducer` now annotates the mark `TAGMSG` Broadcast effect
|
|
499
|
+
with the originator's `account`. The dispatch layer's cap-aware
|
|
500
|
+
`broadcast()` restricts the recipient set to channel members
|
|
501
|
+
authenticated as that same account (RFC 1459 case-folded comparison),
|
|
502
|
+
still honouring `except` (originator skip) and the OR-semantics `caps`
|
|
503
|
+
filter (`message-tags` OR `draft/read-marker`). Other accounts in the
|
|
504
|
+
channel never receive the mark; the originator's other live sessions on
|
|
505
|
+
the same account (multi-device sync, when they share the channel)
|
|
506
|
+
receive it. `filterClientTags` and its `+draft/read-marker` whitelist
|
|
507
|
+
are unchanged — only the *recipient set* changes.
|
|
508
|
+
2. **Unidentified originator → no fanout (session-local).** An
|
|
509
|
+
unidentified connection (no account) emits **no** Broadcast for a read-
|
|
510
|
+
marker mark. Its mark stays session-local (the in-memory
|
|
511
|
+
`lastReadMarkers` still advances; persistence is a no-op without an
|
|
512
|
+
account, unchanged from TICKET-118). The originator's own client still
|
|
513
|
+
observes its mark via the `echo-message` self-echo. This matches the
|
|
514
|
+
spec's privacy default: a read marker never reaches *other* users, and
|
|
515
|
+
an unidentified connection has no account to share a mark with.
|
|
516
|
+
|
|
517
|
+
**Recipient-resolution primitive ("or equivalent" index).** The ticket's
|
|
518
|
+
description floated an `AccountConnections` index on the runtime. The
|
|
519
|
+
shipped implementation takes the spec-permitted "(or equivalent)" path:
|
|
520
|
+
there is **no separate index structure**. The recipient set is resolved at
|
|
521
|
+
dispatch time by walking the channel roster and consulting each member's
|
|
522
|
+
live `ConnectionState.account` via the existing `getConnectionInfo` port
|
|
523
|
+
method (the same per-recipient resolution already used for cap gating).
|
|
524
|
+
This is always fresh by construction — disconnected connections leave the
|
|
525
|
+
roster, and account changes are live references — so the staleness
|
|
526
|
+
concerns an explicit index would need (re-sync on `ACCOUNT`/SASL/
|
|
527
|
+
disconnect) do not apply. Global cross-channel account fanout (reaching an
|
|
528
|
+
account's sessions that are *not* in the marked channel) is a documented
|
|
529
|
+
follow-up, not needed for spec privacy: the mark is channel-specific, and
|
|
530
|
+
the persistence layer (`ServicesStore.setLastReadMarker`) already syncs a
|
|
531
|
+
session's marker on reconnect/rejoin via `seedReadMarkers`. 100% coverage
|
|
532
|
+
on the account-scoped fanout + dispatch filter (irc-core and irc-server
|
|
533
|
+
both enforce a 100% gate).
|
|
467
534
|
|
|
468
535
|
#### TICKET-119 — IRCv3 `draft/pre-away` (persist away status)
|
|
469
536
|
- **Phase:** 12
|
|
@@ -613,6 +680,49 @@ follows strict TDD (Red → Green → Refactor) per `CLAUDE.md` /
|
|
|
613
680
|
- **TDD Outline:** Red on REGISTER→`+r`; green; then `+R`/`+M`;
|
|
614
681
|
then KEEPTOPIC; then MLOCK.
|
|
615
682
|
|
|
683
|
+
Implementation notes (post-land):
|
|
684
|
+
- Three new boolean channel modes added to `ChannelModes`:
|
|
685
|
+
`registered` (`+r`), `blockUnidentified` (`+R`), `moderatedIdent`
|
|
686
|
+
(`+M`). They appear in the `324 RPL_CHANNELMODEIS` reply when set,
|
|
687
|
+
but `channelModeReducer` rejects `+r`/`+R`/`+M` via `MODE` with `472
|
|
688
|
+
ERR_UNKNOWNMODE` — only ChanServ may toggle them via
|
|
689
|
+
`ApplyChannelDelta` effects.
|
|
690
|
+
- `519 ERR_ALLMUSTREGISTER` added to `Numerics` for the `+R` JOIN gate.
|
|
691
|
+
- `ServicesStore` extended: `getChannel`, `setChannelFounder`,
|
|
692
|
+
`setChannelMlock`/`getChannelMlock`, `setChannelRestricted`,
|
|
693
|
+
`setChannelKeepTopic`, `setChannelTopic`/`getChannelTopic`. The
|
|
694
|
+
`RegisteredChannel` record now carries `mlock` / `restricted` /
|
|
695
|
+
`keepTopic` / `topic?`. The in-memory store returns defensive
|
|
696
|
+
copies (callers cannot mutate the store by editing returned
|
|
697
|
+
records).
|
|
698
|
+
- `chanservReducer` (new, `commands/chanserv.ts`) handles
|
|
699
|
+
`PRIVMSG ChanServ :<command>`. Subcommands: `REGISTER`, `DROP`,
|
|
700
|
+
`INFO`, `SET FOUNDER/MLOCK/RESTRICTED/KEEPTOPIC`. Channel
|
|
701
|
+
mutations are emitted as `ApplyChannelDelta` effects so the actor
|
|
702
|
+
applies them to the authoritative `ChannelState`.
|
|
703
|
+
- `SET RESTRICTED ON|OFF` toggles the `+R` channel mode (and stores
|
|
704
|
+
the preference). `SET KEEPTOPIC ON|OFF` toggles topic persistence
|
|
705
|
+
and drops any previously-stored snapshot when turned off.
|
|
706
|
+
- JOIN reducer consults services on the first join after empty-recreate
|
|
707
|
+
to restore a KEEPTOPIC snapshot (emits `332`/`333` to the joiner).
|
|
708
|
+
`+R` JOIN gate rejects unidentified clients with `519`.
|
|
709
|
+
- TOPIC reducer persists the snapshot on every change (set or clear)
|
|
710
|
+
when KEEPTOPIC is on.
|
|
711
|
+
- PRIVMSG/NOTICE channel reducers enforce `+M` (op/voice/identified
|
|
712
|
+
pass) and `+R` (identified pass) with `404`; NOTICE is silent.
|
|
713
|
+
- `channelModeReducer` reapplies MLOCK after every applied MODE: any
|
|
714
|
+
positively-locked boolean mode that ended up unset (whether by this
|
|
715
|
+
MODE or by prior state) is re-asserted via a ChanServ-emitted
|
|
716
|
+
`ApplyChannelDelta` + `Broadcast` (`:ChanServ!ChanServ@services
|
|
717
|
+
MODE #chan +…`). Defence in depth — services-only modes (`+r`) are
|
|
718
|
+
also defended, so the registered mark survives even if a future
|
|
719
|
+
code path applies an `ApplyChannelDelta` clearing it.
|
|
720
|
+
- Actor routes `PRIVMSG ChanServ` (case-insensitive) to
|
|
721
|
+
`reducers.chanserv`; NOTICE and unbound-services cases fall through
|
|
722
|
+
to the normal user-target reducer (RFC: NOTICE is silent).
|
|
723
|
+
- 100% coverage on `chanserv.ts`, `mode.ts` (reapply branch), and the
|
|
724
|
+
new join/privmsg/topic branches.
|
|
725
|
+
|
|
616
726
|
#### TICKET-126 — ChanServ: ACCESS / LEVELS (SOP/AOP/VOP/HOP)
|
|
617
727
|
- **Phase:** 13
|
|
618
728
|
- **Points:** 5
|
package/docs/Release-Process.md
CHANGED
|
@@ -15,8 +15,8 @@ opening a PR that bumps versions and updates `CHANGELOG.md`; the
|
|
|
15
15
|
maintainer team cuts the tag.
|
|
16
16
|
|
|
17
17
|
Cross-reference: `README.md` (Roadmap), `progress.md` / `tickets.md`
|
|
18
|
-
(release content), `docs/
|
|
19
|
-
`.github/workflows/ci.yml`, `.github/workflows/deploy-cf.yml`.
|
|
18
|
+
(release content), `docs/Cloudflare-Deployment-Guide.md` (prod deploy
|
|
19
|
+
step §6), `.github/workflows/ci.yml`, `.github/workflows/deploy-cf.yml`.
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
@@ -37,7 +37,7 @@ this repo itself) should pin exact versions.
|
|
|
37
37
|
|
|
38
38
|
### 1.1 Workspace version lock
|
|
39
39
|
|
|
40
|
-
All
|
|
40
|
+
All fifteen `package.json` files (root + 14 workspace packages) carry the
|
|
41
41
|
**same** version string. They are bumped together in the release
|
|
42
42
|
commit. The pre-release checklist in §4 enforces this.
|
|
43
43
|
|
|
@@ -132,17 +132,20 @@ node apps/cf-worker/scripts/smoke.mjs \
|
|
|
132
132
|
If any box fails, **stop**. Fix forward on `main` and restart the
|
|
133
133
|
checklist — do not tag over a broken tip.
|
|
134
134
|
|
|
135
|
-
### 4.1 What is explicitly out of scope for
|
|
135
|
+
### 4.1 What is explicitly out of scope for the 0.x line
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
- Server password / rate limits / cloaking (TICKET-046).
|
|
141
|
-
- Config loader (TICKET-047).
|
|
137
|
+
Items still pending in `progress.md` that are **not** blockers for a
|
|
138
|
+
given release — they ship when ready and are called out under "Known
|
|
139
|
+
limitations" in the GitHub Release body:
|
|
142
140
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
- Client compatibility sweep (WeeChat / HexChat / IRCCloud / TheLounge /
|
|
142
|
+
matrix-IRC bridge) — `progress.md` TICKET-050.
|
|
143
|
+
- Playwright headless-browser e2e for the web client — TICKET-138.
|
|
144
|
+
- Driving the sub-100% packages to full coverage — TICKET-143 through
|
|
145
|
+
TICKET-148.
|
|
146
|
+
|
|
147
|
+
Their absence is **not** a blocker. If a release intentionally defers an
|
|
148
|
+
in-progress ticket, name it under "Known limitations".
|
|
146
149
|
|
|
147
150
|
---
|
|
148
151
|
|
|
@@ -184,7 +187,7 @@ understand what changed and what they need to do.
|
|
|
184
187
|
|
|
185
188
|
## 6. Bumping versions
|
|
186
189
|
|
|
187
|
-
There are
|
|
190
|
+
There are fifteen `package.json` files. They must end up at the same
|
|
188
191
|
version:
|
|
189
192
|
|
|
190
193
|
```
|
|
@@ -192,10 +195,17 @@ version:
|
|
|
192
195
|
packages/irc-core/package.json
|
|
193
196
|
packages/irc-server/package.json
|
|
194
197
|
packages/in-memory-runtime/package.json
|
|
198
|
+
packages/irc-test-support/package.json
|
|
195
199
|
packages/cf-adapter/package.json
|
|
200
|
+
packages/aws-adapter/package.json
|
|
196
201
|
apps/local-cli/package.json
|
|
197
202
|
apps/cf-worker/package.json
|
|
203
|
+
apps/cf-tcp-container/package.json
|
|
204
|
+
apps/aws-stack/package.json
|
|
205
|
+
apps/web/package.json
|
|
198
206
|
tools/tcp-ws-forwarder/package.json
|
|
207
|
+
tools/load-test/package.json
|
|
208
|
+
tools/ci-hardening/package.json
|
|
199
209
|
```
|
|
200
210
|
|
|
201
211
|
To cut `v0.X.Y`:
|