serverless-ircd 0.8.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/ci.yml +4 -0
- 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 +550 -0
- package/README.md +275 -222
- 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 +33 -10
- 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-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 +115 -48
- 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/favicon.ico +0 -0
- package/apps/web/landing/index.html +227 -3
- package/apps/web/package.json +3 -2
- package/apps/web/scripts/build.mjs +91 -6
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/src/render-docs.ts +292 -0
- 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 +33 -4
- package/apps/web/tests/config-schema.test.ts +149 -25
- 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 +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 +102 -23
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +15 -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 +13 -8
- 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/aws-runtime.test.ts +23 -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/connection-counter.test.ts +17 -0
- 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/global-setup.ts +28 -1
- package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
- 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/nlb-stream.test.ts +29 -1
- 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 +20 -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 +284 -115
- 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 +222 -51
- 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 +60 -35
- package/packages/irc-core/src/commands/chanserv.ts +288 -4
- package/packages/irc-core/src/commands/hostserv.ts +38 -3
- package/packages/irc-core/src/commands/index.ts +1 -0
- package/packages/irc-core/src/commands/join.ts +41 -35
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/nickserv.ts +138 -15
- package/packages/irc-core/src/commands/registration.ts +28 -17
- package/packages/irc-core/src/commands/sasl.ts +22 -31
- package/packages/irc-core/src/commands/service-aliases.ts +52 -0
- package/packages/irc-core/src/commands/topic.ts +23 -10
- 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/state/channel.ts +17 -0
- 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/chanserv.test.ts +668 -1
- package/packages/irc-core/tests/commands/hostserv.test.ts +71 -0
- package/packages/irc-core/tests/commands/join.test.ts +179 -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 +422 -3
- package/packages/irc-core/tests/commands/oper.test.ts +15 -0
- package/packages/irc-core/tests/commands/registration.test.ts +336 -108
- package/packages/irc-core/tests/commands/sasl.test.ts +194 -169
- package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
- 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 +80 -44
- package/packages/irc-server/tests/actor.test.ts +384 -50
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +8 -5
- package/packages/irc-test-support/src/scenarios.ts +21 -6
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +19 -0
- package/packages/irc-test-support/vitest.config.ts +6 -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/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/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/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 +51 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service shortcut command aliases (Atheme/Anope convention).
|
|
3
|
+
*
|
|
4
|
+
* Clients and user expectations support shortcut verbs like
|
|
5
|
+
* `NICKSERV REGISTER pw` as an alias for `PRIVMSG NickServ :REGISTER pw`.
|
|
6
|
+
* These are not RFC-standardized but are a de facto convention from
|
|
7
|
+
* services packages (Atheme/Anope) and IRCd alias modules (InspIRCd
|
|
8
|
+
* `m_alias`, UnrealIRCd `alias{}`).
|
|
9
|
+
*
|
|
10
|
+
* The actor's dispatcher rewrites any of these verbs to a
|
|
11
|
+
* `PRIVMSG <ServiceNick> :<joined args>` and delegates to the existing
|
|
12
|
+
* `routeMessageTarget` path, so the service reducers see the canonical
|
|
13
|
+
* `params=[serviceNick, body]` shape unchanged.
|
|
14
|
+
*
|
|
15
|
+
* This module is the **single source of truth** for the alias → service
|
|
16
|
+
* nick mapping. Both the dispatcher rewrite (in `irc-server`'s actor) and
|
|
17
|
+
* the service-nick recognition in `serviceReducerFor` consult the derived
|
|
18
|
+
* {@link SERVICE_NICKS} list rather than maintaining their own copies.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { CHANSERV_NICK } from './chanserv.js';
|
|
22
|
+
import { HOSTSERV_NICK } from './hostserv.js';
|
|
23
|
+
import { MEMOSERV_NICK } from './memoserv.js';
|
|
24
|
+
import { NICKSERV_NICK } from './nickserv.js';
|
|
25
|
+
import { OPERSERV_NICK } from './operserv.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Shortcut command verb (uppercased) → canonical service nick. Keyed by
|
|
29
|
+
* uppercased command so the dispatcher's `msg.command.toUpperCase()` lookup
|
|
30
|
+
* is case-insensitive. The full set of Atheme/Anope-convention aliases.
|
|
31
|
+
*/
|
|
32
|
+
export const SHORTCUT_TO_SERVICE: ReadonlyMap<string, string> = new Map<string, string>([
|
|
33
|
+
['NICKSERV', NICKSERV_NICK],
|
|
34
|
+
['NS', NICKSERV_NICK],
|
|
35
|
+
['CHANSERV', CHANSERV_NICK],
|
|
36
|
+
['CS', CHANSERV_NICK],
|
|
37
|
+
['HOSTSERV', HOSTSERV_NICK],
|
|
38
|
+
['HS', HOSTSERV_NICK],
|
|
39
|
+
['MEMOSERV', MEMOSERV_NICK],
|
|
40
|
+
['MS', MEMOSERV_NICK],
|
|
41
|
+
['OPERSERV', OPERSERV_NICK],
|
|
42
|
+
['OS', OPERSERV_NICK],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The canonical, de-duplicated list of service nicks derived from
|
|
47
|
+
* {@link SHORTCUT_TO_SERVICE}'s values. Used by `serviceReducerFor` for the
|
|
48
|
+
* "is this target a service nick?" membership check so the service-nick list
|
|
49
|
+
* lives in exactly one place (this module), not duplicated across an
|
|
50
|
+
* `if`-chain.
|
|
51
|
+
*/
|
|
52
|
+
export const SERVICE_NICKS: readonly string[] = Array.from(new Set(SHORTCUT_TO_SERVICE.values()));
|
|
@@ -153,7 +153,8 @@ function readTopic(
|
|
|
153
153
|
ctx: Ctx,
|
|
154
154
|
effects: EffectType[],
|
|
155
155
|
): { state: ChannelState; effects: EffectType[] } {
|
|
156
|
-
|
|
156
|
+
const topicReply = topicNumerics(state, ctx);
|
|
157
|
+
if (topicReply === undefined) {
|
|
157
158
|
effects.push(
|
|
158
159
|
Effect.send(ctx.connId, [
|
|
159
160
|
numericErr(ctx, Numerics.RPL_NOTOPIC, 'No topic is set', state.name),
|
|
@@ -161,19 +162,31 @@ function readTopic(
|
|
|
161
162
|
);
|
|
162
163
|
return { state, effects };
|
|
163
164
|
}
|
|
165
|
+
effects.push(topicReply);
|
|
166
|
+
return { state, effects };
|
|
167
|
+
}
|
|
164
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Builds the `332 RPL_TOPIC` + `333 RPL_TOPICWHOTIME` {@link Effect.send}
|
|
171
|
+
* for the connection's current nick, or `undefined` when no topic is set
|
|
172
|
+
* on `state.topic`.
|
|
173
|
+
*
|
|
174
|
+
* Shared by the TOPIC read-form ({@link readTopic}) and the JOIN success
|
|
175
|
+
* path so both emit an identical numeric block whenever a topic is
|
|
176
|
+
* present, regardless of whether the topic came from a ChanServ KEEPTOPIC
|
|
177
|
+
* restore, a deploy snapshot, or an earlier joiner.
|
|
178
|
+
*/
|
|
179
|
+
export function topicNumerics(state: ChannelState, ctx: Ctx): EffectType | undefined {
|
|
180
|
+
if (state.topic === undefined) return undefined;
|
|
165
181
|
const nick = ctx.connection.nick ?? '*';
|
|
166
182
|
const codeStr332 = Numerics.RPL_TOPIC.toString().padStart(3, '0');
|
|
167
183
|
const codeStr333 = Numerics.RPL_TOPICWHOTIME.toString().padStart(3, '0');
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
]),
|
|
175
|
-
);
|
|
176
|
-
return { state, effects };
|
|
184
|
+
return Effect.send(ctx.connId, [
|
|
185
|
+
{ text: `:${ctx.serverName} ${codeStr332} ${nick} ${state.name} :${state.topic.text}` },
|
|
186
|
+
{
|
|
187
|
+
text: `:${ctx.serverName} ${codeStr333} ${nick} ${state.name} ${state.topic.setter} ${state.topic.setAt}`,
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
177
190
|
}
|
|
178
191
|
|
|
179
192
|
// ============================================================================
|
|
@@ -34,8 +34,8 @@ export const OperCredSchema = z.object({
|
|
|
34
34
|
/**
|
|
35
35
|
* Schema for a single SASL PLAIN account credential. The `username` is also
|
|
36
36
|
* the canonical account name recorded on the connection (for `extended-join`
|
|
37
|
-
* / `account-tag`). Both fields are non-empty strings; adapters seed
|
|
38
|
-
*
|
|
37
|
+
* / `account-tag`). Both fields are non-empty strings; adapters seed the
|
|
38
|
+
* `InMemoryServicesStore` from this list at boot.
|
|
39
39
|
*/
|
|
40
40
|
export const SaslAccountSchema = z.object({
|
|
41
41
|
username: z.string().min(1),
|
|
@@ -173,6 +173,15 @@ export const DEFAULT_CREATED_TEXT: string = BUILD_DATE;
|
|
|
173
173
|
*/
|
|
174
174
|
export const DEFAULT_NICK_ENFORCE_GRACE_MS = 30_000;
|
|
175
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Default minimum password length enforced by NickServ `SET PASSWORD`.
|
|
178
|
+
* Mirrors common IRC services (Atheme/Anope) and modern guidance (NIST
|
|
179
|
+
* SP 800-63B). Overridable per deployment via
|
|
180
|
+
* `ServerConfig.nickServ.minPasswordLength`. A new password shorter than
|
|
181
|
+
* the floor is rejected before hashing.
|
|
182
|
+
*/
|
|
183
|
+
export const DEFAULT_MIN_PASSWORD_LENGTH = 8;
|
|
184
|
+
|
|
176
185
|
/**
|
|
177
186
|
* The full server-config schema. Optional fields default to deployment-
|
|
178
187
|
* sensible values so a minimal `{ serverName, networkName }` config is
|
|
@@ -271,13 +280,12 @@ export const ServerConfigSchema = z.object({
|
|
|
271
280
|
operCreds: z.array(OperCredSchema).default([]),
|
|
272
281
|
|
|
273
282
|
/**
|
|
274
|
-
* SASL PLAIN
|
|
275
|
-
* `
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* follow-up; swapping it in means replacing the construction call.
|
|
283
|
+
* SASL PLAIN seed credentials. The adapter ingests these into the
|
|
284
|
+
* `ServicesStore` at boot via `registerNick` (idempotent — existing
|
|
285
|
+
* registrations win). `AUTHENTICATE PLAIN`, `PASS <nick>:<password>`,
|
|
286
|
+
* and NickServ `IDENTIFY` all verify against the resulting
|
|
287
|
+
* `nickserv_accounts` rows. Empty array (the default) leaves the seed
|
|
288
|
+
* empty; accounts can still be created at runtime via NickServ REGISTER.
|
|
281
289
|
*/
|
|
282
290
|
saslAccounts: z.array(SaslAccountSchema).default([]),
|
|
283
291
|
|
|
@@ -357,6 +365,24 @@ export const ServerConfigSchema = z.object({
|
|
|
357
365
|
*/
|
|
358
366
|
nickEnforceGraceMs: z.number().int().nonnegative().optional(),
|
|
359
367
|
|
|
368
|
+
/**
|
|
369
|
+
* NickServ-specific tunables. Currently only the minimum length enforced
|
|
370
|
+
* by `SET PASSWORD` is configurable; the maximum (256) is fixed to bound
|
|
371
|
+
* the scrypt input budget. Omit entirely to inherit the documented
|
|
372
|
+
* defaults.
|
|
373
|
+
*/
|
|
374
|
+
nickServ: z
|
|
375
|
+
.object({
|
|
376
|
+
/**
|
|
377
|
+
* Minimum length a new password must meet to be accepted by
|
|
378
|
+
* `SET PASSWORD`. Defaults to {@link DEFAULT_MIN_PASSWORD_LENGTH}
|
|
379
|
+
* (8) when unset. A shorter new password is rejected before hashing
|
|
380
|
+
* with `Password is too short (minimum <n> characters).`.
|
|
381
|
+
*/
|
|
382
|
+
minPasswordLength: z.number().int().positive().optional(),
|
|
383
|
+
})
|
|
384
|
+
.optional(),
|
|
385
|
+
|
|
360
386
|
/**
|
|
361
387
|
* HostServ approval mode for `REQUEST <vhost>`. `true` (the default)
|
|
362
388
|
* preserves the legacy auto-approve behaviour — the requested vhost is
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared SASL credential hashing primitives.
|
|
3
3
|
*
|
|
4
|
-
* Both the AWS adapter (`
|
|
5
|
-
* (`
|
|
6
|
-
* These helpers live in `irc-core` so both
|
|
7
|
-
*
|
|
4
|
+
* Both the AWS adapter (`DynamoServicesStore`) and the CF adapter
|
|
5
|
+
* (`D1ServicesStore`) store NickServ/SASL credentials as **scrypt hashes**
|
|
6
|
+
* via the unified `ServicesStore`. These helpers live in `irc-core` so both
|
|
7
|
+
* adapters share one hashing implementation.
|
|
8
8
|
*
|
|
9
9
|
* Uses `node:crypto` (`scryptSync`), which is available on Node (AWS Lambda)
|
|
10
10
|
* natively and on Cloudflare Workers via the `nodejs_compat` flag. The public
|
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
|
|
17
|
-
import type { AccountStore, SaslPayload, SaslResult } from './ports.js';
|
|
18
17
|
|
|
19
18
|
const SCRYPT_KEY_LEN = 64;
|
|
20
19
|
const SALT_LEN = 16;
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
|
-
* A single
|
|
22
|
+
* A single credential stored as a scrypt hash.
|
|
24
23
|
*
|
|
25
|
-
* Every field is a persistence-backend attribute — this object IS the
|
|
26
|
-
*
|
|
24
|
+
* Every field is a persistence-backend attribute — this object IS the
|
|
25
|
+
* credential portion of a `nickserv_accounts` row (DynamoDB `Services`
|
|
26
|
+
* item / D1 `nickserv_accounts` row, keyed by the folded nick).
|
|
27
27
|
* `algorithm` is retained so future migrations to Argon2/bcrypt can be
|
|
28
28
|
* detected and handled gracefully by {@link verifyHashedPassword}.
|
|
29
29
|
*/
|
|
@@ -36,8 +36,9 @@ export interface HashedAccountCredential {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Hashes a plaintext password into a {@link HashedAccountCredential}
|
|
39
|
-
* suitable for writing to the
|
|
40
|
-
* `
|
|
39
|
+
* suitable for writing to the services backend
|
|
40
|
+
* (`ServicesStore.registerNick` stores the result on the registered-nick
|
|
41
|
+
* row).
|
|
41
42
|
*
|
|
42
43
|
* Uses `scryptSync` (memory-hard, GPU-resistant) with a random salt.
|
|
43
44
|
* Pass `{ salt }` for deterministic test scenarios; pass `{ keyLen }` to
|
|
@@ -78,47 +79,3 @@ export function verifyHashedPassword(password: string, entry: HashedAccountCrede
|
|
|
78
79
|
const computed = scryptSync(password, salt, expected.length);
|
|
79
80
|
return computed.length === expected.length && timingSafeEqual(computed, expected);
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Synchronous {@link AccountStore} backed by pre-loaded hashed credentials.
|
|
84
|
-
*
|
|
85
|
-
* Construct with entries produced by {@link hashAccountCredential} (for unit
|
|
86
|
-
* tests / seeding) or pre-loaded from a persistence backend at boot. Both
|
|
87
|
-
* adapters use this class:
|
|
88
|
-
*
|
|
89
|
-
* - AWS: `loadDynamoAccountStore` scans the `Accounts` table at Lambda
|
|
90
|
-
* cold start and constructs this store; `DynamoAccountStore` is a
|
|
91
|
-
* re-export alias for back-compat.
|
|
92
|
-
* - CF: `loadD1AccountStore` queries the D1 `accounts` table at
|
|
93
|
-
* `ConnectionDO` construction (mirroring the two-phase load pattern).
|
|
94
|
-
*
|
|
95
|
-
* The class has zero backend coupling — it works equally well with entries
|
|
96
|
-
* from any source. The `AccountStore.verify` port stays synchronous, so
|
|
97
|
-
* the async pre-load MUST complete before the first frame dispatches.
|
|
98
|
-
*/
|
|
99
|
-
export class HashedAccountStore implements AccountStore {
|
|
100
|
-
private readonly entries: ReadonlyMap<string, HashedAccountCredential>;
|
|
101
|
-
|
|
102
|
-
constructor(entries: ReadonlyArray<HashedAccountCredential>) {
|
|
103
|
-
this.entries = new Map(entries.map((e) => [e.account, e]));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
verify(mech: string, payload: SaslPayload): SaslResult {
|
|
107
|
-
if (mech.toUpperCase() !== 'PLAIN' || payload.kind !== 'PLAIN') {
|
|
108
|
-
return { ok: false, reason: `unsupported mechanism: ${mech}` };
|
|
109
|
-
}
|
|
110
|
-
const entry = this.entries.get(payload.username);
|
|
111
|
-
if (entry === undefined) {
|
|
112
|
-
return { ok: false, reason: 'invalid credentials' };
|
|
113
|
-
}
|
|
114
|
-
if (!verifyHashedPassword(payload.password, entry)) {
|
|
115
|
-
return { ok: false, reason: 'invalid credentials' };
|
|
116
|
-
}
|
|
117
|
-
return { ok: true, account: payload.username };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Number of loaded accounts (diagnostics / logging). */
|
|
121
|
-
get size(): number {
|
|
122
|
-
return this.entries.size;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
@@ -11,6 +11,7 @@ export * from './commands/index.js';
|
|
|
11
11
|
export * from './effects.js';
|
|
12
12
|
export * from './ports.js';
|
|
13
13
|
export * from './credential-hashing.js';
|
|
14
|
+
export * from './account-migration.js';
|
|
14
15
|
export * from './flood-control.js';
|
|
15
16
|
export * from './cloak.js';
|
|
16
17
|
export * from './case-fold.js';
|