serverless-ircd 0.9.0 → 0.11.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 +28 -0
- package/.github/workflows/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +35 -9
- package/.github/workflows/deploy-cf.yml +40 -14
- package/CHANGELOG.md +594 -0
- package/README.md +286 -60
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +118 -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 +398 -67
- 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 +714 -105
- 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/Dockerfile +37 -5
- package/apps/cf-tcp-container/package.json +7 -3
- package/apps/cf-tcp-container/src/config-loader.ts +113 -2
- package/apps/cf-tcp-container/src/container-server.ts +267 -87
- package/apps/cf-tcp-container/src/main.ts +22 -7
- package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
- package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
- package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
- package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
- package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
- package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
- package/apps/cf-tcp-container/wrangler.toml +18 -14
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/src/worker.ts +77 -5
- package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
- package/apps/cf-worker/tests/smoke.test.ts +4 -0
- package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
- package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
- package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
- package/apps/cf-worker/wrangler.test.toml +15 -1
- package/apps/cf-worker/wrangler.toml +95 -77
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +14 -2
- package/apps/local-cli/src/line-scanner.ts +26 -0
- package/apps/local-cli/src/server.ts +44 -19
- package/apps/local-cli/tests/line-scanner.test.ts +64 -0
- package/apps/local-cli/tests/tcp.test.ts +29 -0
- 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 +793 -118
- package/docs/AWS-TCP-Deployment.md +57 -47
- package/docs/Chat-History.md +55 -0
- package/docs/Cloudflare-Deployment-Guide.md +95 -114
- package/docs/Cloudflare-TCP-Deployment.md +160 -101
- package/docs/Release-Process.md +27 -23
- package/docs/SASL-EXTERNAL.md +175 -0
- 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/admission.ts +28 -13
- package/packages/aws-adapter/src/aws-runtime.ts +30 -3
- package/packages/aws-adapter/src/cdk-table-defs.ts +39 -16
- package/packages/aws-adapter/src/config-loader.ts +153 -8
- package/packages/aws-adapter/src/dynamo-services-store.ts +19 -0
- package/packages/aws-adapter/src/handlers/connect.ts +73 -1
- package/packages/aws-adapter/src/handlers/default.ts +279 -123
- package/packages/aws-adapter/src/handlers/index.ts +98 -25
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +135 -14
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/ip-admission.ts +79 -0
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +23 -0
- package/packages/aws-adapter/src/tables.ts +11 -12
- package/packages/aws-adapter/tests/admission.test.ts +60 -2
- package/packages/aws-adapter/tests/aws-harness.ts +23 -2
- package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +217 -0
- package/packages/aws-adapter/tests/connect.test.ts +323 -3
- package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +226 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +134 -1
- package/packages/aws-adapter/tests/handlers.test.ts +174 -12
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
- 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 +100 -10
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/config-loader.ts +133 -8
- package/packages/cf-adapter/src/connection-do.ts +406 -116
- package/packages/cf-adapter/src/counter-do.ts +142 -0
- package/packages/cf-adapter/src/d1-services-store.ts +105 -26
- package/packages/cf-adapter/src/env.ts +99 -10
- package/packages/cf-adapter/src/index.ts +17 -7
- package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +205 -16
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
- package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +164 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
- package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
- package/packages/cf-adapter/tests/d1-services-store.test.ts +245 -3
- package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/cf-adapter/tests/worker/main.ts +4 -0
- package/packages/cf-adapter/wrangler.test.toml +18 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +25 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/caps/capabilities.ts +20 -10
- package/packages/irc-core/src/certfp.ts +178 -0
- package/packages/irc-core/src/commands/account-auth.ts +16 -19
- package/packages/irc-core/src/commands/cap.ts +10 -2
- package/packages/irc-core/src/commands/chanserv.ts +117 -14
- package/packages/irc-core/src/commands/chathistory.ts +13 -5
- package/packages/irc-core/src/commands/hostserv.ts +84 -8
- package/packages/irc-core/src/commands/index.ts +2 -1
- package/packages/irc-core/src/commands/invite.ts +1 -7
- package/packages/irc-core/src/commands/join.ts +1 -16
- package/packages/irc-core/src/commands/kick.ts +1 -8
- package/packages/irc-core/src/commands/list.ts +1 -8
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/mode.ts +1 -8
- package/packages/irc-core/src/commands/multiline.ts +4 -10
- package/packages/irc-core/src/commands/names.ts +53 -13
- package/packages/irc-core/src/commands/nickserv.ts +161 -11
- package/packages/irc-core/src/commands/oper.ts +361 -8
- package/packages/irc-core/src/commands/part.ts +4 -10
- package/packages/irc-core/src/commands/privmsg.ts +8 -4
- package/packages/irc-core/src/commands/registration.ts +148 -4
- package/packages/irc-core/src/commands/sasl.ts +154 -46
- package/packages/irc-core/src/commands/topic.ts +10 -12
- package/packages/irc-core/src/commands/who.ts +1 -8
- package/packages/irc-core/src/config.ts +424 -25
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/effects.ts +24 -0
- package/packages/irc-core/src/flood-control.ts +10 -10
- package/packages/irc-core/src/frame-rate-limit.ts +82 -0
- package/packages/irc-core/src/index.ts +9 -0
- package/packages/irc-core/src/oper-hashing.ts +43 -0
- package/packages/irc-core/src/oper-lockout.ts +87 -0
- package/packages/irc-core/src/ports.ts +529 -190
- package/packages/irc-core/src/protocol/bytes.ts +65 -0
- package/packages/irc-core/src/protocol/channel-name.ts +37 -0
- package/packages/irc-core/src/protocol/index.ts +12 -1
- package/packages/irc-core/src/protocol/outbound.ts +43 -10
- package/packages/irc-core/src/protocol/parser.ts +79 -10
- package/packages/irc-core/src/state/connection.ts +13 -0
- package/packages/irc-core/src/types.ts +266 -23
- package/packages/irc-core/src/ws-framing.ts +5 -4
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/bytes.test.ts +89 -0
- package/packages/irc-core/tests/certfp.test.ts +117 -0
- package/packages/irc-core/tests/commands/cap.test.ts +76 -2
- package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
- package/packages/irc-core/tests/commands/join.test.ts +78 -1
- 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/names.test.ts +193 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +419 -3
- package/packages/irc-core/tests/commands/oper.test.ts +574 -1
- package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
- package/packages/irc-core/tests/commands/registration.test.ts +602 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +742 -172
- package/packages/irc-core/tests/commands/topic.test.ts +137 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +104 -84
- package/packages/irc-core/tests/config.test.ts +534 -2
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/flood-control.test.ts +29 -1
- package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
- package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
- package/packages/irc-core/tests/outbound.test.ts +148 -0
- package/packages/irc-core/tests/parser.test.ts +287 -5
- package/packages/irc-core/tests/persistent-services-store.test.ts +212 -12
- package/packages/irc-core/tests/ports.test.ts +170 -7
- package/packages/irc-core/tests/services-store.test.ts +567 -1
- package/packages/irc-core/tests/ws-framing.test.ts +45 -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 +123 -22
- package/packages/irc-server/src/dispatch.ts +1 -0
- package/packages/irc-server/src/index.ts +7 -0
- package/packages/irc-server/src/redact.ts +159 -0
- package/packages/irc-server/src/runtime.ts +14 -0
- package/packages/irc-server/src/transport.ts +28 -1
- package/packages/irc-server/tests/actor.test.ts +563 -54
- package/packages/irc-server/tests/dispatch.test.ts +31 -0
- package/packages/irc-server/tests/redact.test.ts +198 -0
- package/packages/irc-server/tests/runtime.test.ts +2 -0
- package/packages/irc-server/tests/transport.test.ts +66 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
- 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 +2 -2
- package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
- package/tools/ci-hardening/src/cf-deploy.ts +118 -0
- package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
- package/tools/ci-hardening/src/env-var-drift.ts +192 -0
- package/tools/ci-hardening/src/hostname-guard.ts +11 -0
- package/tools/ci-hardening/src/index.ts +19 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
- package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
- package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
- package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
- package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- package/tools/ci-hardening/vitest.config.ts +5 -1
- package/tools/hash-oper-cred.ts +85 -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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-connection inbound frame-rate window for the adapter boundary.
|
|
3
|
+
*
|
|
4
|
+
* Client-side flood control (the token bucket in `flood-control.ts`)
|
|
5
|
+
* lives in the actor and only fires AFTER the frame has been parsed and
|
|
6
|
+
* dispatched — which on the serverless adapters means the frame has
|
|
7
|
+
* already cost a Durable Object storage write (Cloudflare) or a DynamoDB
|
|
8
|
+
* write (AWS). This module is the cheaper, earlier gate: a pure
|
|
9
|
+
* sliding-window admission decision the adapters consult BEFORE invoking
|
|
10
|
+
* the actor, so a hostile client spamming low-cost commands is cut off at
|
|
11
|
+
* the connection boundary instead of driving outsized storage cost.
|
|
12
|
+
*
|
|
13
|
+
* Semantics: a connection may deliver at most `maxFrames` frames inside
|
|
14
|
+
* any sliding `windowMs` window. Timestamps older than the window decay
|
|
15
|
+
* out lazily (they stop counting as soon as `now - t >= windowMs`), so a
|
|
16
|
+
* connection that pauses longer than the window starts fresh. The frame
|
|
17
|
+
* that would exceed the budget is rejected — the caller closes the
|
|
18
|
+
* connection with RFC 6455 close code 1008 (Policy Violation) after
|
|
19
|
+
* delivering the canonical `ERROR :Closing link (flooding)` line.
|
|
20
|
+
*
|
|
21
|
+
* Determinism invariant: the decision is a pure function of
|
|
22
|
+
* `(times, now, maxFrames, windowMs)`; the caller owns the timestamp
|
|
23
|
+
* list (in-memory on the CF Durable Object, persisted on the AWS
|
|
24
|
+
* Connections row) and the clock.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* RFC 6455 close code used when the inbound frame window is exceeded
|
|
29
|
+
* (Policy Violation).
|
|
30
|
+
*/
|
|
31
|
+
export const FRAME_FLOOD_CLOSE_CODE = 1008;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The `ERROR` line delivered to the client just before the flood close,
|
|
35
|
+
* matching the canonical ircd convention for connection-level flood
|
|
36
|
+
* kills.
|
|
37
|
+
*/
|
|
38
|
+
export const FRAME_FLOOD_ERROR_LINE = 'ERROR :Closing link (flooding)';
|
|
39
|
+
|
|
40
|
+
/** Result of {@link admitFrameWindow}. */
|
|
41
|
+
export interface FrameWindowDecision {
|
|
42
|
+
/**
|
|
43
|
+
* `true` when the frame fits the window (the caller processes it);
|
|
44
|
+
* `false` when the window is full (the caller closes the connection).
|
|
45
|
+
*/
|
|
46
|
+
readonly admitted: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The post-decision window state the caller should retain: the
|
|
49
|
+
* previous entries pruned of decayed timestamps, plus — when
|
|
50
|
+
* admitted — the current frame's `now` appended. Never mutated in
|
|
51
|
+
* place; a fresh array is returned on every call.
|
|
52
|
+
*/
|
|
53
|
+
readonly times: number[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Decides whether the frame arriving at `now` fits inside the
|
|
58
|
+
* `maxFrames`-per-`windowMs` sliding window.
|
|
59
|
+
*
|
|
60
|
+
* Entries with `t < now - windowMs` decay out first (an entry at exactly
|
|
61
|
+
* `now - windowMs` still counts — inclusive boundary, mirroring
|
|
62
|
+
* `AdmissionStats.recentAdmissions`). The decision then reduces to:
|
|
63
|
+
* if the pruned window already holds `maxFrames` entries, reject;
|
|
64
|
+
* otherwise admit and record `now`.
|
|
65
|
+
*/
|
|
66
|
+
export function admitFrameWindow(
|
|
67
|
+
times: readonly number[],
|
|
68
|
+
now: number,
|
|
69
|
+
maxFrames: number,
|
|
70
|
+
windowMs: number,
|
|
71
|
+
): FrameWindowDecision {
|
|
72
|
+
const cutoff = now - windowMs;
|
|
73
|
+
const kept: number[] = [];
|
|
74
|
+
for (const t of times) {
|
|
75
|
+
if (t >= cutoff) kept.push(t);
|
|
76
|
+
}
|
|
77
|
+
if (kept.length >= maxFrames) {
|
|
78
|
+
return { admitted: false, times: kept };
|
|
79
|
+
}
|
|
80
|
+
kept.push(now);
|
|
81
|
+
return { admitted: true, times: kept };
|
|
82
|
+
}
|
|
@@ -10,19 +10,28 @@ export * from './caps/index.js';
|
|
|
10
10
|
export * from './commands/index.js';
|
|
11
11
|
export * from './effects.js';
|
|
12
12
|
export * from './ports.js';
|
|
13
|
+
export * from './certfp.js';
|
|
13
14
|
export * from './credential-hashing.js';
|
|
15
|
+
export * from './account-migration.js';
|
|
16
|
+
export * from './oper-hashing.js';
|
|
14
17
|
export * from './flood-control.js';
|
|
18
|
+
export * from './frame-rate-limit.js';
|
|
15
19
|
export * from './cloak.js';
|
|
16
20
|
export * from './case-fold.js';
|
|
17
21
|
export * from './ws-subprotocol.js';
|
|
18
22
|
export * from './ws-framing.js';
|
|
19
23
|
export * from './admission.js';
|
|
24
|
+
export * from './oper-lockout.js';
|
|
20
25
|
export * from './config.js';
|
|
21
26
|
export type {
|
|
22
27
|
BuildCtxOptions,
|
|
28
|
+
ChathistoryConfig,
|
|
23
29
|
CloakingConfig,
|
|
24
30
|
Ctx,
|
|
25
31
|
OperCred,
|
|
32
|
+
OperCredHashed,
|
|
33
|
+
OperCredPlaintext,
|
|
34
|
+
OperThrottleConfig,
|
|
26
35
|
Reducer,
|
|
27
36
|
ReducerResult,
|
|
28
37
|
ServerConfig,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper for `tools/hash-oper-cred.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Wraps {@link hashAccountCredential}'s scrypt derivation in the
|
|
5
|
+
* `{user, salt, hash}` shape the oper config consumes (see
|
|
6
|
+
* {@link OperCredSchema}). The CLI entry point (arg parsing, console
|
|
7
|
+
* output) lives in the tools script; this helper is side-effect-free
|
|
8
|
+
* and fully unit-testable, mirroring the
|
|
9
|
+
* `account-migration.ts` / `tools/migrate-accounts-to-services.ts`
|
|
10
|
+
* split.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { hashAccountCredential } from './credential-hashing.js';
|
|
14
|
+
import type { OperCredHashed } from './types.js';
|
|
15
|
+
|
|
16
|
+
export type { OperCredHashed };
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Hashes a plaintext oper `{user, password}` pair into the at-rest
|
|
20
|
+
* `{user, salt, hash}` shape operators paste into secrets.
|
|
21
|
+
*
|
|
22
|
+
* The `salt` is cryptographically random by default; pass `{ salt }`
|
|
23
|
+
* for deterministic test scenarios. The `hash` is scrypt-derived via
|
|
24
|
+
* {@link hashAccountCredential} (memory-hard, GPU-resistant) and both
|
|
25
|
+
* `salt` and `hash` are base64 strings safe to embed in JSON / env
|
|
26
|
+
* vars / wrangler secrets.
|
|
27
|
+
*
|
|
28
|
+
* Round-trips via {@link matchOperCred} (the OPER reducer's verify
|
|
29
|
+
* path) — the resulting {@link OperCredHashed} is directly assignable
|
|
30
|
+
* to the {@link OperCredSchema} array on `ServerConfig`.
|
|
31
|
+
*/
|
|
32
|
+
export function hashOperCredential(
|
|
33
|
+
user: string,
|
|
34
|
+
password: string,
|
|
35
|
+
opts?: { salt?: Uint8Array },
|
|
36
|
+
): OperCredHashed {
|
|
37
|
+
const entry = hashAccountCredential(user, password, opts);
|
|
38
|
+
return {
|
|
39
|
+
user: entry.account,
|
|
40
|
+
salt: entry.salt,
|
|
41
|
+
hash: entry.hash,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-IP failed-`OPER` counter — the failed-authentication counterpart of
|
|
3
|
+
* {@link AdmissionStats}.
|
|
4
|
+
*
|
|
5
|
+
* Online brute force against `OPER` is bounded by this sliding-window
|
|
6
|
+
* counter: once a single source host accumulates `oper.maxFailures`
|
|
7
|
+
* credential failures within `oper.failureWindowSeconds`, every further
|
|
8
|
+
* `OPER` from that host is rejected with `491 ERR_NOOPERHOST` before the
|
|
9
|
+
* credential store is consulted (see `commands/oper.ts`).
|
|
10
|
+
*
|
|
11
|
+
* Ownership + injection follows the {@link AdmissionStats} precedent: the
|
|
12
|
+
* runtime (in-memory reference runtime, local CLI) constructs exactly one
|
|
13
|
+
* instance per process and shares it across every connection via the
|
|
14
|
+
* `ctx.operFailures` port, so the counter is per-IP across the whole
|
|
15
|
+
* deployment rather than per-connection. The instance MUST be constructed
|
|
16
|
+
* with the same {@link Clock} the actors use so window arithmetic is
|
|
17
|
+
* coherent.
|
|
18
|
+
*
|
|
19
|
+
* Determinism: same Clock + same call stream → same answers. There are no
|
|
20
|
+
* ambient inputs (never `Date.now`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Clock } from './ports.js';
|
|
24
|
+
|
|
25
|
+
/** Per-IP failed-`OPER` counter owned by the runtime (sliding window). */
|
|
26
|
+
export class OperFailureStats {
|
|
27
|
+
private readonly clock: Clock;
|
|
28
|
+
/**
|
|
29
|
+
* Per-IP timestamps of recent OPER credential failures, used by the
|
|
30
|
+
* lockout gate. Stored ascending; entries older than the queried window
|
|
31
|
+
* are pruned lazily (identical bookkeeping to `AdmissionStats`'
|
|
32
|
+
* `perIpAdmitTimes`).
|
|
33
|
+
*/
|
|
34
|
+
private readonly failureTimes = new Map<string, number[]>();
|
|
35
|
+
|
|
36
|
+
constructor(clock: Clock) {
|
|
37
|
+
this.clock = clock;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns the number of `OPER` credential failures recorded for `ip`
|
|
42
|
+
* within the last `windowMs` milliseconds (inclusive of the moment
|
|
43
|
+
* returned by `clock.now()`). Stale entries are pruned as a side effect,
|
|
44
|
+
* so an IP whose every failure has expired drops out of the map
|
|
45
|
+
* entirely.
|
|
46
|
+
*/
|
|
47
|
+
recentFailures(ip: string, windowMs: number): number {
|
|
48
|
+
const times = this.failureTimes.get(ip);
|
|
49
|
+
if (times === undefined) return 0;
|
|
50
|
+
const cutoff = this.clock.now() - windowMs;
|
|
51
|
+
let writeIdx = 0;
|
|
52
|
+
// `times` is dense (only ever appended to via push), so iterating
|
|
53
|
+
// values via for-of avoids the noUncheckedIndexedAccess trap while
|
|
54
|
+
// still allowing in-place compaction of stale entries.
|
|
55
|
+
for (const t of times) {
|
|
56
|
+
if (t >= cutoff) {
|
|
57
|
+
times[writeIdx++] = t;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (writeIdx === 0) {
|
|
61
|
+
this.failureTimes.delete(ip);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
times.length = writeIdx;
|
|
65
|
+
return writeIdx;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Records one `OPER` credential failure for `ip`, stamped with the
|
|
70
|
+
* injected clock's current time. Called by the OPER reducer on the
|
|
71
|
+
* `464 ERR_PASSWDMISMATCH` path.
|
|
72
|
+
*/
|
|
73
|
+
recordFailure(ip: string): void {
|
|
74
|
+
const times = this.failureTimes.get(ip) ?? [];
|
|
75
|
+
times.push(this.clock.now());
|
|
76
|
+
this.failureTimes.set(ip, times);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Clears every recorded failure for `ip`. Called by the OPER reducer on
|
|
81
|
+
* successful authentication so a legitimate oper's success never
|
|
82
|
+
* contributes to a later lockout.
|
|
83
|
+
*/
|
|
84
|
+
reset(ip: string): void {
|
|
85
|
+
this.failureTimes.delete(ip);
|
|
86
|
+
}
|
|
87
|
+
}
|