serverless-ircd 0.9.0 → 0.10.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/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +11 -9
- package/.github/workflows/deploy-cf.yml +14 -14
- package/CHANGELOG.md +305 -0
- package/README.md +134 -41
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +82 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +177 -52
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +267 -92
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/package.json +2 -3
- package/apps/cf-tcp-container/src/container-server.ts +12 -9
- package/apps/cf-tcp-container/wrangler.toml +1 -10
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/wrangler.toml +12 -71
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +21 -17
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +1 -0
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +66 -4
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +2 -2
- package/apps/web/tests/config-schema.test.ts +149 -25
- package/docs/AWS-Deployment.md +670 -96
- package/docs/AWS-TCP-Deployment.md +20 -45
- package/docs/Cloudflare-Deployment-Guide.md +87 -113
- package/docs/Cloudflare-TCP-Deployment.md +25 -49
- package/docs/Release-Process.md +27 -23
- package/docs/Services.md +69 -22
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -11
- package/packages/aws-adapter/src/config-loader.ts +19 -2
- package/packages/aws-adapter/src/dynamo-services-store.ts +7 -0
- package/packages/aws-adapter/src/handlers/connect.ts +26 -0
- package/packages/aws-adapter/src/handlers/default.ts +190 -123
- package/packages/aws-adapter/src/handlers/index.ts +67 -23
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +3 -6
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +15 -0
- package/packages/aws-adapter/src/tables.ts +2 -12
- package/packages/aws-adapter/tests/aws-harness.ts +0 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +66 -0
- package/packages/aws-adapter/tests/connect.test.ts +124 -1
- package/packages/aws-adapter/tests/default-occ.test.ts +219 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +11 -0
- package/packages/aws-adapter/tests/handlers.test.ts +117 -11
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +59 -8
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/connection-do.ts +266 -109
- package/packages/cf-adapter/src/d1-services-store.ts +63 -26
- package/packages/cf-adapter/src/env.ts +11 -10
- package/packages/cf-adapter/src/index.ts +0 -6
- package/packages/cf-adapter/tests/cf-runtime.test.ts +101 -1
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +95 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/d1-services-store.test.ts +53 -2
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/commands/account-auth.ts +16 -19
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/nickserv.ts +122 -11
- package/packages/irc-core/src/commands/registration.ts +2 -2
- package/packages/irc-core/src/commands/sasl.ts +22 -31
- package/packages/irc-core/src/config.ts +35 -9
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/index.ts +1 -0
- package/packages/irc-core/src/ports.ts +159 -179
- package/packages/irc-core/src/types.ts +38 -10
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/commands/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +237 -1
- package/packages/irc-core/tests/commands/oper.test.ts +15 -0
- package/packages/irc-core/tests/commands/registration.test.ts +140 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +159 -178
- package/packages/irc-core/tests/commands/unified-account.test.ts +102 -84
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +71 -12
- package/packages/irc-core/tests/ports.test.ts +71 -0
- package/packages/irc-core/tests/services-store.test.ts +204 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +0 -14
- package/packages/irc-server/tests/actor.test.ts +19 -47
- package/packages/irc-test-support/package.json +1 -1
- package/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/src/index.ts +2 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- package/tools/load-test/package.json +1 -1
- package/tools/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ReadMarkerEntry,
|
|
12
12
|
type RegisteredNick,
|
|
13
13
|
type ServicesAuthResult,
|
|
14
|
+
type ServicesSnapshot,
|
|
14
15
|
type ServicesStore,
|
|
15
16
|
} from '../src/ports';
|
|
16
17
|
|
|
@@ -116,6 +117,185 @@ describe('InMemoryServicesStore — NickServ IDENTIFY (verifyNick)', () => {
|
|
|
116
117
|
});
|
|
117
118
|
});
|
|
118
119
|
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// NickServ CertFP — hydrate round-trip (legacy snapshot without certSubjects)
|
|
122
|
+
// ============================================================================
|
|
123
|
+
|
|
124
|
+
describe('InMemoryServicesStore — CertFP snapshot hydration', () => {
|
|
125
|
+
it('defaults certSubjects to [] when a legacy snapshot row omits the field', () => {
|
|
126
|
+
// Simulate a pre-certSubjects snapshot: the row is missing the field
|
|
127
|
+
// entirely (as if loaded from JSON written before the column existed).
|
|
128
|
+
const legacySnap: ServicesSnapshot = {
|
|
129
|
+
nicks: [
|
|
130
|
+
{
|
|
131
|
+
nick: 'alice',
|
|
132
|
+
account: 'alice',
|
|
133
|
+
email: 'a@x',
|
|
134
|
+
createdAt: NOW,
|
|
135
|
+
enforce: 'none',
|
|
136
|
+
certSubjects: undefined as unknown as string[],
|
|
137
|
+
credential: { account: 'alice', algorithm: 'scrypt', salt: 's', hash: 'aGVsbG8=' },
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
channels: [],
|
|
141
|
+
access: [],
|
|
142
|
+
levels: [],
|
|
143
|
+
vhosts: [],
|
|
144
|
+
pendingVhosts: [],
|
|
145
|
+
memos: [],
|
|
146
|
+
readMarkers: [],
|
|
147
|
+
akills: [],
|
|
148
|
+
jupes: [],
|
|
149
|
+
};
|
|
150
|
+
const s = new InMemoryServicesStore({ clock: new FakeClock(NOW), snapshot: legacySnap });
|
|
151
|
+
const rec = s.getNick('alice');
|
|
152
|
+
expect(rec).toBeDefined();
|
|
153
|
+
if (rec !== undefined) {
|
|
154
|
+
expect(rec.certSubjects).toEqual([]);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// NickServ CertFP — SASL EXTERNAL surface (verifyCertFP / addCertFP / removeCertFP)
|
|
161
|
+
// ============================================================================
|
|
162
|
+
|
|
163
|
+
describe('InMemoryServicesStore — NickServ CertFP (verifyCertFP)', () => {
|
|
164
|
+
it('returns ok with the account name for a known subject', () => {
|
|
165
|
+
const s = store();
|
|
166
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
167
|
+
s.addCertFP('alice', 'CN=alice');
|
|
168
|
+
expect(s.verifyCertFP('CN=alice')).toEqual<ServicesAuthResult>({
|
|
169
|
+
ok: true,
|
|
170
|
+
account: 'alice',
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns ok:false for an unknown subject', () => {
|
|
175
|
+
const s = store();
|
|
176
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
177
|
+
s.addCertFP('alice', 'CN=alice');
|
|
178
|
+
const out = s.verifyCertFP('CN=evil');
|
|
179
|
+
expect(out.ok).toBe(false);
|
|
180
|
+
if (out.ok) return;
|
|
181
|
+
expect(out.reason.length).toBeGreaterThan(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('returns ok:false when no nick has any cert subject registered', () => {
|
|
185
|
+
const s = store();
|
|
186
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
187
|
+
expect(s.verifyCertFP('CN=alice').ok).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('matches the subject case-sensitively (byte-exact)', () => {
|
|
191
|
+
const s = store();
|
|
192
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
193
|
+
s.addCertFP('alice', 'CN=Alice');
|
|
194
|
+
expect(s.verifyCertFP('CN=alice').ok).toBe(false);
|
|
195
|
+
expect(s.verifyCertFP('CN=Alice').ok).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('resolves the account that owns the subject (multiple accounts)', () => {
|
|
199
|
+
const s = store();
|
|
200
|
+
s.registerNick('alice', 'pw', 'a@e');
|
|
201
|
+
s.registerNick('bob', 'pw', 'b@e');
|
|
202
|
+
s.addCertFP('bob', 'CN=bob');
|
|
203
|
+
expect(s.verifyCertFP('CN=bob')).toEqual<ServicesAuthResult>({
|
|
204
|
+
ok: true,
|
|
205
|
+
account: 'bob',
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('exposes the registered subjects on the RegisteredNick record', () => {
|
|
210
|
+
const s = store();
|
|
211
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
212
|
+
s.addCertFP('alice', 'CN=alice');
|
|
213
|
+
s.addCertFP('alice', 'CN=alice-2');
|
|
214
|
+
const rec = s.getNick('alice');
|
|
215
|
+
expect(rec).toBeDefined();
|
|
216
|
+
if (rec === undefined) return;
|
|
217
|
+
expect(rec.certSubjects).toEqual(['CN=alice', 'CN=alice-2']);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('defaults certSubjects to an empty array on a fresh registration', () => {
|
|
221
|
+
const s = store();
|
|
222
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
223
|
+
const rec = s.getNick('alice');
|
|
224
|
+
expect(rec).toBeDefined();
|
|
225
|
+
if (rec === undefined) return;
|
|
226
|
+
expect(rec.certSubjects).toEqual([]);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('InMemoryServicesStore — NickServ CertFP mutators (addCertFP / removeCertFP)', () => {
|
|
231
|
+
it('addCertFP attaches a subject to a registered nick', () => {
|
|
232
|
+
const s = store();
|
|
233
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
234
|
+
s.addCertFP('alice', 'CN=alice');
|
|
235
|
+
expect(s.getNick('alice')?.certSubjects).toEqual(['CN=alice']);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('addCertFP is a no-op on an unregistered nick', () => {
|
|
239
|
+
const s = store();
|
|
240
|
+
s.addCertFP('ghost', 'CN=ghost');
|
|
241
|
+
expect(s.verifyCertFP('CN=ghost').ok).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('addCertFP does not duplicate an already-attached subject', () => {
|
|
245
|
+
const s = store();
|
|
246
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
247
|
+
s.addCertFP('alice', 'CN=alice');
|
|
248
|
+
s.addCertFP('alice', 'CN=alice');
|
|
249
|
+
expect(s.getNick('alice')?.certSubjects).toEqual(['CN=alice']);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('addCertFP folds the nick case-insensitively', () => {
|
|
253
|
+
const s = store();
|
|
254
|
+
s.registerNick('Alice', 'hunter2', 'alice@example.com');
|
|
255
|
+
s.addCertFP('alice', 'CN=alice');
|
|
256
|
+
expect(s.verifyCertFP('CN=alice')).toEqual<ServicesAuthResult>({
|
|
257
|
+
ok: true,
|
|
258
|
+
account: 'Alice',
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('removeCertFP detaches a subject from a registered nick', () => {
|
|
263
|
+
const s = store();
|
|
264
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
265
|
+
s.addCertFP('alice', 'CN=alice');
|
|
266
|
+
expect(s.removeCertFP('alice', 'CN=alice')).toBe(true);
|
|
267
|
+
expect(s.getNick('alice')?.certSubjects).toEqual([]);
|
|
268
|
+
expect(s.verifyCertFP('CN=alice').ok).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('removeCertFP returns false when the subject is not attached', () => {
|
|
272
|
+
const s = store();
|
|
273
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
274
|
+
expect(s.removeCertFP('alice', 'CN=alice')).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('removeCertFP returns false on an unregistered nick', () => {
|
|
278
|
+
const s = store();
|
|
279
|
+
expect(s.removeCertFP('ghost', 'CN=ghost')).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('removeCertFP folds the nick case-insensitively but matches the subject byte-exactly', () => {
|
|
283
|
+
const s = store();
|
|
284
|
+
s.registerNick('Alice', 'hunter2', 'alice@example.com');
|
|
285
|
+
s.addCertFP('alice', 'CN=Alice');
|
|
286
|
+
expect(s.removeCertFP('ALICE', 'CN=alice')).toBe(false);
|
|
287
|
+
expect(s.removeCertFP('ALICE', 'CN=Alice')).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('dropNick removes the attached cert subjects along with the registration', () => {
|
|
291
|
+
const s = store();
|
|
292
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
293
|
+
s.addCertFP('alice', 'CN=alice');
|
|
294
|
+
s.dropNick('alice');
|
|
295
|
+
expect(s.verifyCertFP('CN=alice').ok).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
119
299
|
describe('InMemoryServicesStore — NickServ DROP (dropNick)', () => {
|
|
120
300
|
it('returns true and removes the registration for a known nick', () => {
|
|
121
301
|
const s = store();
|
|
@@ -175,6 +355,29 @@ describe('InMemoryServicesStore — NickServ SET ENFORCE (setNickEnforce)', () =
|
|
|
175
355
|
});
|
|
176
356
|
});
|
|
177
357
|
|
|
358
|
+
describe('InMemoryServicesStore — NickServ SET PASSWORD (setNickPassword)', () => {
|
|
359
|
+
it('overwrites the credential so the new password verifies and the old does not', () => {
|
|
360
|
+
const s = store();
|
|
361
|
+
s.registerNick('alice', 'hunter2', 'alice@example.com');
|
|
362
|
+
s.setNickPassword('alice', 'newpassword');
|
|
363
|
+
expect(s.verifyNick('alice', 'newpassword').ok).toBe(true);
|
|
364
|
+
expect(s.verifyNick('alice', 'hunter2').ok).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('folds the nick case-insensitively', () => {
|
|
368
|
+
const s = store();
|
|
369
|
+
s.registerNick('Alice', 'hunter2', 'alice@example.com');
|
|
370
|
+
s.setNickPassword('alice', 'newpassword');
|
|
371
|
+
expect(s.verifyNick('ALICE', 'newpassword').ok).toBe(true);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('is a no-op on an unregistered nick (the original credential, if any, is untouched)', () => {
|
|
375
|
+
const s = store();
|
|
376
|
+
s.setNickPassword('ghost', 'newpassword');
|
|
377
|
+
expect(s.isRegisteredNick('ghost')).toBe(false);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
178
381
|
describe('InMemoryServicesStore — NickServ INFO (getNick)', () => {
|
|
179
382
|
it('returns the full record for a registered nick', () => {
|
|
180
383
|
const s = store();
|
|
@@ -186,6 +389,7 @@ describe('InMemoryServicesStore — NickServ INFO (getNick)', () => {
|
|
|
186
389
|
email: 'alice@example.com',
|
|
187
390
|
createdAt: NOW,
|
|
188
391
|
enforce: 'none',
|
|
392
|
+
certSubjects: [],
|
|
189
393
|
});
|
|
190
394
|
});
|
|
191
395
|
|
|
@@ -8,7 +8,12 @@ export default defineConfig({
|
|
|
8
8
|
provider: 'v8',
|
|
9
9
|
all: false,
|
|
10
10
|
include: ['src/**/*.ts'],
|
|
11
|
-
|
|
11
|
+
// `index.ts` files are barrel re-exports (no runtime logic);
|
|
12
|
+
// `messages.ts` is type-only (`interface` declarations that
|
|
13
|
+
// TypeScript erases to an empty module). v8 scores both 0%
|
|
14
|
+
// because they contain no executable statements — excluding
|
|
15
|
+
// them keeps the 100% threshold honest about real coverage.
|
|
16
|
+
exclude: ['src/**/index.ts', 'src/**/messages.ts'],
|
|
12
17
|
reporter: ['text', 'html', 'json-summary'],
|
|
13
18
|
thresholds: {
|
|
14
19
|
lines: 100,
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
|
-
type AccountStore,
|
|
30
29
|
type AwayStore,
|
|
31
30
|
CHANSERV_NICK,
|
|
32
31
|
type ChanName,
|
|
@@ -150,16 +149,6 @@ export interface ConnectionActorOptions {
|
|
|
150
149
|
* opt in.
|
|
151
150
|
*/
|
|
152
151
|
readonly messages?: MessageStore;
|
|
153
|
-
/**
|
|
154
|
-
* SASL account verification source (absent when no accounts are
|
|
155
|
-
* configured). When bound, `ctx.accounts` is defined so the
|
|
156
|
-
* `AUTHENTICATE` reducer can verify PLAIN credentials end-to-end.
|
|
157
|
-
* Omitted (→ `ctx.accounts` undefined) preserves the existing
|
|
158
|
-
* behaviour for callers/tests that do not opt in: a client completing
|
|
159
|
-
* `AUTHENTICATE PLAIN` receives `904 ERR_SASLFAIL` because no store
|
|
160
|
-
* can vouch for the credentials.
|
|
161
|
-
*/
|
|
162
|
-
readonly accounts?: AccountStore;
|
|
163
152
|
/**
|
|
164
153
|
* mTLS client-cert identity source for SASL EXTERNAL (absent when mTLS
|
|
165
154
|
* is not configured). When bound, `ctx.mtlsIdentity` is defined so the
|
|
@@ -271,7 +260,6 @@ export class ConnectionActor {
|
|
|
271
260
|
private readonly ids: IdFactory;
|
|
272
261
|
private readonly motd: MotdProvider;
|
|
273
262
|
private readonly messages: MessageStore | undefined;
|
|
274
|
-
private readonly accounts: AccountStore | undefined;
|
|
275
263
|
private readonly mtlsIdentity: MtlsIdentityProvider | undefined;
|
|
276
264
|
private readonly history: NickHistoryStore | undefined;
|
|
277
265
|
private readonly away: AwayStore | undefined;
|
|
@@ -325,7 +313,6 @@ export class ConnectionActor {
|
|
|
325
313
|
this.ids = opts.ids;
|
|
326
314
|
this.motd = opts.motd ?? EmptyMotdProvider;
|
|
327
315
|
this.messages = opts.messages;
|
|
328
|
-
this.accounts = opts.accounts;
|
|
329
316
|
this.mtlsIdentity = opts.mtlsIdentity;
|
|
330
317
|
this.history = opts.history;
|
|
331
318
|
this.away = opts.away;
|
|
@@ -511,7 +498,6 @@ export class ConnectionActor {
|
|
|
511
498
|
motd: this.motd,
|
|
512
499
|
connection: this.state,
|
|
513
500
|
...(this.messages !== undefined ? { messages: this.messages } : {}),
|
|
514
|
-
...(this.accounts !== undefined ? { accounts: this.accounts } : {}),
|
|
515
501
|
...(this.mtlsIdentity !== undefined ? { mtlsIdentity: this.mtlsIdentity } : {}),
|
|
516
502
|
...(this.history !== undefined ? { history: this.history } : {}),
|
|
517
503
|
...(this.away !== undefined ? { away: this.away } : {}),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
type AccountStore,
|
|
3
2
|
type AwayStore,
|
|
4
3
|
CapturingLogger,
|
|
5
4
|
type ChanName,
|
|
@@ -26,8 +25,6 @@ import {
|
|
|
26
25
|
type NickHistoryStore,
|
|
27
26
|
Numerics,
|
|
28
27
|
type RawLine,
|
|
29
|
-
type SaslPayload,
|
|
30
|
-
type SaslResult,
|
|
31
28
|
SequentialIdFactory,
|
|
32
29
|
type ServerConfig,
|
|
33
30
|
type ServerStats,
|
|
@@ -219,7 +216,6 @@ function makeActor(opts: {
|
|
|
219
216
|
logger?: Logger;
|
|
220
217
|
traceId?: string;
|
|
221
218
|
messages?: MessageStore;
|
|
222
|
-
accounts?: AccountStore;
|
|
223
219
|
mtlsIdentity?: MtlsIdentityProvider;
|
|
224
220
|
history?: NickHistoryStore;
|
|
225
221
|
stats?: ServerStats;
|
|
@@ -280,9 +276,6 @@ function makeActor(opts: {
|
|
|
280
276
|
if (opts.messages !== undefined) {
|
|
281
277
|
finalOpts.messages = opts.messages;
|
|
282
278
|
}
|
|
283
|
-
if (opts.accounts !== undefined) {
|
|
284
|
-
finalOpts.accounts = opts.accounts;
|
|
285
|
-
}
|
|
286
279
|
if (opts.mtlsIdentity !== undefined) {
|
|
287
280
|
finalOpts.mtlsIdentity = opts.mtlsIdentity;
|
|
288
281
|
}
|
|
@@ -2129,17 +2122,7 @@ describe('ConnectionActor — CHATHISTORY', () => {
|
|
|
2129
2122
|
// SASL / AccountStore — actor plumbing (ctx.accounts threaded through buildCtx)
|
|
2130
2123
|
// ===========================================================================
|
|
2131
2124
|
|
|
2132
|
-
describe('ConnectionActor — SASL
|
|
2133
|
-
/** Capture-and-respond fake AccountStore (mirrors the irc-core sasl tests). */
|
|
2134
|
-
class FakeAccountStore implements AccountStore {
|
|
2135
|
-
readonly calls: Array<{ mech: string; payload: SaslPayload }> = [];
|
|
2136
|
-
constructor(private readonly result: SaslResult) {}
|
|
2137
|
-
verify(mech: string, payload: SaslPayload): SaslResult {
|
|
2138
|
-
this.calls.push({ mech, payload });
|
|
2139
|
-
return this.result;
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2125
|
+
describe('ConnectionActor — SASL ServicesStore plumbing', () => {
|
|
2143
2126
|
/** Builds the PLAIN base64 payload `authzid\0authcid\0password`. */
|
|
2144
2127
|
function plainPayload(authcid: string, password: string): string {
|
|
2145
2128
|
return encodeBase64(`\0${authcid}\0${password}`);
|
|
@@ -2154,11 +2137,12 @@ describe('ConnectionActor — SASL AccountStore plumbing', () => {
|
|
|
2154
2137
|
};
|
|
2155
2138
|
}
|
|
2156
2139
|
|
|
2157
|
-
it('completes SASL PLAIN with 900/903 when a bound
|
|
2158
|
-
const
|
|
2140
|
+
it('completes SASL PLAIN with 900/903 when a bound ServicesStore accepts creds', async () => {
|
|
2141
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(0) });
|
|
2142
|
+
services.registerNick('alice', 'secret', 'alice@example.com');
|
|
2159
2143
|
const { actor, runtime, conn } = makeActor({
|
|
2160
2144
|
conn: saslReadyConn(),
|
|
2161
|
-
|
|
2145
|
+
services,
|
|
2162
2146
|
});
|
|
2163
2147
|
conn.nick = 'alice';
|
|
2164
2148
|
runtime.calls.length = 0;
|
|
@@ -2173,18 +2157,14 @@ describe('ConnectionActor — SASL AccountStore plumbing', () => {
|
|
|
2173
2157
|
expect(lines.some((l) => l.text.includes(' 903 '))).toBe(true);
|
|
2174
2158
|
// Critical: never 904 for valid creds.
|
|
2175
2159
|
expect(lines.some((l) => l.text.includes(' 904 '))).toBe(false);
|
|
2176
|
-
// The AccountStore was consulted with the parsed PLAIN credentials.
|
|
2177
|
-
expect(accounts.calls).toEqual([
|
|
2178
|
-
{ mech: 'PLAIN', payload: { kind: 'PLAIN', username: 'alice', password: 'secret' } },
|
|
2179
|
-
]);
|
|
2180
2160
|
// Account name recorded on the connection for extended-join/account-tag.
|
|
2181
2161
|
expect(conn.account).toBe('alice');
|
|
2182
2162
|
});
|
|
2183
2163
|
|
|
2184
|
-
it('emits 904 ERR_SASLFAIL when no
|
|
2164
|
+
it('emits 904 ERR_SASLFAIL when no ServicesStore is bound (regression guard)', async () => {
|
|
2185
2165
|
const { actor, runtime, conn } = makeActor({
|
|
2186
2166
|
conn: saslReadyConn(),
|
|
2187
|
-
// No `
|
|
2167
|
+
// No `services` option → ctx.services is undefined.
|
|
2188
2168
|
});
|
|
2189
2169
|
conn.nick = 'alice';
|
|
2190
2170
|
runtime.calls.length = 0;
|
|
@@ -2202,11 +2182,12 @@ describe('ConnectionActor — SASL AccountStore plumbing', () => {
|
|
|
2202
2182
|
expect(conn.account).toBeUndefined();
|
|
2203
2183
|
});
|
|
2204
2184
|
|
|
2205
|
-
it('emits 904 when a bound
|
|
2206
|
-
const
|
|
2185
|
+
it('emits 904 when a bound ServicesStore rejects the credentials', async () => {
|
|
2186
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(0) });
|
|
2187
|
+
services.registerNick('alice', 'secret', 'alice@example.com');
|
|
2207
2188
|
const { actor, runtime, conn } = makeActor({
|
|
2208
2189
|
conn: saslReadyConn(),
|
|
2209
|
-
|
|
2190
|
+
services,
|
|
2210
2191
|
});
|
|
2211
2192
|
conn.nick = 'alice';
|
|
2212
2193
|
runtime.calls.length = 0;
|
|
@@ -2223,16 +2204,6 @@ describe('ConnectionActor — SASL AccountStore plumbing', () => {
|
|
|
2223
2204
|
});
|
|
2224
2205
|
|
|
2225
2206
|
describe('ConnectionActor — SASL EXTERNAL + mTLS plumbing', () => {
|
|
2226
|
-
/** Fake AccountStore that accepts EXTERNAL with a known cert subject. */
|
|
2227
|
-
class FakeAccountStore implements AccountStore {
|
|
2228
|
-
readonly calls: Array<{ mech: string; payload: SaslPayload }> = [];
|
|
2229
|
-
constructor(private readonly result: SaslResult) {}
|
|
2230
|
-
verify(mech: string, payload: SaslPayload): SaslResult {
|
|
2231
|
-
this.calls.push({ mech, payload });
|
|
2232
|
-
return this.result;
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
2207
|
/** Fake MtlsIdentityProvider returning a canned subject for 'c1'. */
|
|
2237
2208
|
class FakeMtlsProvider implements MtlsIdentityProvider {
|
|
2238
2209
|
constructor(private readonly identity: string | undefined) {}
|
|
@@ -2250,11 +2221,13 @@ describe('ConnectionActor — SASL EXTERNAL + mTLS plumbing', () => {
|
|
|
2250
2221
|
}
|
|
2251
2222
|
|
|
2252
2223
|
it('completes SASL EXTERNAL with 900/903 when mTLS provider and store accept', async () => {
|
|
2253
|
-
const
|
|
2224
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(0) });
|
|
2225
|
+
services.registerNick('alice', 'secret', 'alice@example.com');
|
|
2226
|
+
services.addCertFP('alice', 'CN=alice');
|
|
2254
2227
|
const mtlsIdentity = new FakeMtlsProvider('CN=alice');
|
|
2255
2228
|
const { actor, runtime, conn } = makeActor({
|
|
2256
2229
|
conn: saslReadyConn(),
|
|
2257
|
-
|
|
2230
|
+
services,
|
|
2258
2231
|
mtlsIdentity,
|
|
2259
2232
|
});
|
|
2260
2233
|
conn.nick = 'alice';
|
|
@@ -2268,17 +2241,16 @@ describe('ConnectionActor — SASL EXTERNAL + mTLS plumbing', () => {
|
|
|
2268
2241
|
expect(lines.some((l) => l.text.includes(' 900 '))).toBe(true);
|
|
2269
2242
|
expect(lines.some((l) => l.text.includes(' 903 '))).toBe(true);
|
|
2270
2243
|
expect(lines.some((l) => l.text.includes(' 904 '))).toBe(false);
|
|
2271
|
-
expect(accounts.calls).toEqual([
|
|
2272
|
-
{ mech: 'EXTERNAL', payload: { kind: 'EXTERNAL', identity: 'CN=alice' } },
|
|
2273
|
-
]);
|
|
2274
2244
|
expect(conn.account).toBe('alice');
|
|
2275
2245
|
});
|
|
2276
2246
|
|
|
2277
2247
|
it('returns 908 for EXTERNAL when no mTLS provider is bound', async () => {
|
|
2278
|
-
const
|
|
2248
|
+
const services = new InMemoryServicesStore({ clock: new FakeClock(0) });
|
|
2249
|
+
services.registerNick('alice', 'secret', 'alice@example.com');
|
|
2250
|
+
services.addCertFP('alice', 'CN=alice');
|
|
2279
2251
|
const { actor, runtime, conn } = makeActor({
|
|
2280
2252
|
conn: saslReadyConn(),
|
|
2281
|
-
|
|
2253
|
+
services,
|
|
2282
2254
|
// No mtlsIdentity → EXTERNAL should 908 at mechanism phase.
|
|
2283
2255
|
});
|
|
2284
2256
|
conn.nick = 'alice';
|