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
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DynamoDB-backed {@link AccountStore} primitives for the AWS adapter.
|
|
3
|
-
*
|
|
4
|
-
* The `Accounts` DynamoDB table stores SASL PLAIN credentials as
|
|
5
|
-
* **scrypt hashes** (never plaintext). Each row is a flat
|
|
6
|
-
* {@link HashedAccountCredential} (`{ account, algorithm, salt, hash }`).
|
|
7
|
-
*
|
|
8
|
-
* Two-phase construction keeps the `AccountStore` port synchronous:
|
|
9
|
-
* 1. {@link loadDynamoAccountStore} (async) scans the table at Lambda
|
|
10
|
-
* cold start and returns a fully-populated `DynamoAccountStore`.
|
|
11
|
-
* 2. `DynamoAccountStore(entries)` (sync constructor) takes the
|
|
12
|
-
* pre-loaded entries. The class itself has zero DynamoDB
|
|
13
|
-
* coupling — it works equally well with entries from any source.
|
|
14
|
-
*
|
|
15
|
-
* This mirrors the pre-load pattern documented in
|
|
16
|
-
* `irc-core/src/ports.ts` and used by `MotdProvider` / `MessageStore`:
|
|
17
|
-
* adapters that need an async backend pre-load the data at boot into a
|
|
18
|
-
* synchronously-readable store.
|
|
19
|
-
*
|
|
20
|
-
* The scrypt hashing helpers (`hashAccountCredential` /
|
|
21
|
-
* `verifyHashedPassword` + the `HashedAccountCredential` type) live in
|
|
22
|
-
* `irc-core` and are shared with the CF adapter's `D1AccountStore`. They
|
|
23
|
-
* are re-exported below so existing call sites importing from this module
|
|
24
|
-
* keep working.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import type { ScanCommandOutput } from '@aws-sdk/client-dynamodb';
|
|
28
|
-
import { ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
29
|
-
import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
30
|
-
import {
|
|
31
|
-
type HashedAccountCredential,
|
|
32
|
-
HashedAccountStore,
|
|
33
|
-
hashAccountCredential,
|
|
34
|
-
verifyHashedPassword,
|
|
35
|
-
} from '@serverless-ircd/irc-core';
|
|
36
|
-
|
|
37
|
-
// Re-export so callers importing these from `@serverless-ircd/aws-adapter`
|
|
38
|
-
// (the historical home of the hashing helpers) keep compiling.
|
|
39
|
-
export { type HashedAccountCredential, hashAccountCredential, verifyHashedPassword };
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Synchronous {@link AccountStore} backed by pre-loaded hashed credentials.
|
|
43
|
-
*
|
|
44
|
-
* Now a re-export of the shared `HashedAccountStore` from `irc-core` —
|
|
45
|
-
* both adapters use one store implementation. The AWS-specific surface
|
|
46
|
-
* (`loadDynamoAccountStore`) lives below; it diverges from CF only at the
|
|
47
|
-
* scan boundary.
|
|
48
|
-
*/
|
|
49
|
-
export { HashedAccountStore as DynamoAccountStore };
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Scans the `Accounts` table and returns a fully-populated
|
|
53
|
-
* {@link DynamoAccountStore}, or `undefined` when the table has no rows.
|
|
54
|
-
*
|
|
55
|
-
* Handles pagination (`LastEvaluatedKey`) so tables larger than the 1 MB
|
|
56
|
-
* scan page are fully loaded. Rows missing required attributes are
|
|
57
|
-
* silently skipped.
|
|
58
|
-
*/
|
|
59
|
-
export async function loadDynamoAccountStore(
|
|
60
|
-
docClient: DynamoDBDocumentClient,
|
|
61
|
-
tableName: string,
|
|
62
|
-
): Promise<HashedAccountStore | undefined> {
|
|
63
|
-
const entries: HashedAccountCredential[] = [];
|
|
64
|
-
let exclusiveStartKey: Record<string, unknown> | undefined = undefined;
|
|
65
|
-
do {
|
|
66
|
-
const result: ScanCommandOutput = await docClient.send(
|
|
67
|
-
new ScanCommand({
|
|
68
|
-
TableName: tableName,
|
|
69
|
-
ExclusiveStartKey: exclusiveStartKey,
|
|
70
|
-
}),
|
|
71
|
-
);
|
|
72
|
-
for (const item of result.Items ?? []) {
|
|
73
|
-
const account = typeof item.account === 'string' ? item.account : undefined;
|
|
74
|
-
const algorithm = typeof item.algorithm === 'string' ? item.algorithm : undefined;
|
|
75
|
-
const salt = typeof item.salt === 'string' ? item.salt : undefined;
|
|
76
|
-
const hash = typeof item.hash === 'string' ? item.hash : undefined;
|
|
77
|
-
if (
|
|
78
|
-
account !== undefined &&
|
|
79
|
-
algorithm !== undefined &&
|
|
80
|
-
salt !== undefined &&
|
|
81
|
-
hash !== undefined
|
|
82
|
-
) {
|
|
83
|
-
entries.push({
|
|
84
|
-
account,
|
|
85
|
-
algorithm: algorithm as HashedAccountCredential['algorithm'],
|
|
86
|
-
salt,
|
|
87
|
-
hash,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
exclusiveStartKey = result.LastEvaluatedKey;
|
|
92
|
-
} while (exclusiveStartKey !== undefined);
|
|
93
|
-
if (entries.length === 0) return undefined;
|
|
94
|
-
return new HashedAccountStore(entries);
|
|
95
|
-
}
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DynamoDB-backed integration tests for the `Accounts` table path.
|
|
3
|
-
* Exercises the real DynamoDB round-trip that pure unit
|
|
4
|
-
* tests cannot: `putAccountCredential` writes a hashed row,
|
|
5
|
-
* `loadDynamoAccountStore` scans it back, and `resolveAccountStore`
|
|
6
|
-
* applies the table-then-config precedence.
|
|
7
|
-
*
|
|
8
|
-
* Gated on DynamoDB Local availability (same `DDB_AVAILABLE` flag as
|
|
9
|
-
* `handlers.test.ts`).
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
|
|
13
|
-
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
14
|
-
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
15
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
16
|
-
import {
|
|
17
|
-
loadDynamoAccountStore,
|
|
18
|
-
putAccountCredential,
|
|
19
|
-
resolveAccountStore,
|
|
20
|
-
} from '../src/account-store.js';
|
|
21
|
-
import { TABLE_DEFS } from '../src/cdk-table-defs.js';
|
|
22
|
-
import { createDynamoDocumentClient } from '../src/dynamo.js';
|
|
23
|
-
|
|
24
|
-
const available = process.env.DDB_AVAILABLE === '1';
|
|
25
|
-
const endpoint = process.env.DYNAMO_ENDPOINT;
|
|
26
|
-
|
|
27
|
-
interface Fx {
|
|
28
|
-
tableName: string;
|
|
29
|
-
client: ReturnType<typeof createDynamoDocumentClient>;
|
|
30
|
-
cleanup: () => Promise<void>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let fx: Fx | null = null;
|
|
34
|
-
|
|
35
|
-
function requireFx(): Fx {
|
|
36
|
-
if (!fx) throw new Error('test fixture not initialized');
|
|
37
|
-
return fx;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function makeFx(): Promise<Fx> {
|
|
41
|
-
if (endpoint === undefined) throw new Error('DYNAMO_ENDPOINT missing');
|
|
42
|
-
const tableName = `acct-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
43
|
-
const client = createDynamoDocumentClient({ endpoint });
|
|
44
|
-
const props = TABLE_DEFS.Accounts;
|
|
45
|
-
const pkName = props.partitionKey?.name;
|
|
46
|
-
if (pkName === undefined) throw new Error('Accounts table missing partitionKey');
|
|
47
|
-
await client.send(
|
|
48
|
-
new CreateTableCommand({
|
|
49
|
-
TableName: tableName,
|
|
50
|
-
AttributeDefinitions: [{ AttributeName: pkName, AttributeType: 'S' }],
|
|
51
|
-
KeySchema: [{ AttributeName: pkName, KeyType: 'HASH' }],
|
|
52
|
-
BillingMode: 'PAY_PER_REQUEST',
|
|
53
|
-
}),
|
|
54
|
-
);
|
|
55
|
-
return {
|
|
56
|
-
tableName,
|
|
57
|
-
client,
|
|
58
|
-
cleanup: async () => {
|
|
59
|
-
try {
|
|
60
|
-
await client.send(new DeleteTableCommand({ TableName: tableName }));
|
|
61
|
-
} catch {
|
|
62
|
-
// Idempotent.
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
describe.skipIf(!available)('Accounts table round-trip', () => {
|
|
69
|
-
beforeEach(async () => {
|
|
70
|
-
fx = await makeFx();
|
|
71
|
-
});
|
|
72
|
-
afterEach(async () => {
|
|
73
|
-
if (fx) await fx.cleanup();
|
|
74
|
-
fx = null;
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('putAccountCredential then loadDynamoAccountStore verifies the account', async () => {
|
|
78
|
-
await putAccountCredential(requireFx().client, requireFx().tableName, 'alice', 's3cret');
|
|
79
|
-
const store = await loadDynamoAccountStore(requireFx().client, requireFx().tableName);
|
|
80
|
-
expect(store).toBeDefined();
|
|
81
|
-
expect(
|
|
82
|
-
store?.verify('PLAIN', { kind: 'PLAIN', username: 'alice', password: 's3cret' }),
|
|
83
|
-
).toEqual({
|
|
84
|
-
ok: true,
|
|
85
|
-
account: 'alice',
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('loadDynamoAccountStore returns undefined when the table has no rows', async () => {
|
|
90
|
-
const store = await loadDynamoAccountStore(requireFx().client, requireFx().tableName);
|
|
91
|
-
expect(store).toBeUndefined();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('putAccountCredential writes multiple distinct accounts', async () => {
|
|
95
|
-
await putAccountCredential(requireFx().client, requireFx().tableName, 'alice', 'a1');
|
|
96
|
-
await putAccountCredential(requireFx().client, requireFx().tableName, 'bob', 'b2');
|
|
97
|
-
const store = await loadDynamoAccountStore(requireFx().client, requireFx().tableName);
|
|
98
|
-
expect(store).toBeDefined();
|
|
99
|
-
expect(store?.size).toBe(2);
|
|
100
|
-
expect(store?.verify('PLAIN', { kind: 'PLAIN', username: 'alice', password: 'a1' }).ok).toBe(
|
|
101
|
-
true,
|
|
102
|
-
);
|
|
103
|
-
expect(store?.verify('PLAIN', { kind: 'PLAIN', username: 'bob', password: 'b2' }).ok).toBe(
|
|
104
|
-
true,
|
|
105
|
-
);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('putAccountCredential overwrites an existing account (re-seed)', async () => {
|
|
109
|
-
await putAccountCredential(requireFx().client, requireFx().tableName, 'alice', 'old');
|
|
110
|
-
await putAccountCredential(requireFx().client, requireFx().tableName, 'alice', 'new');
|
|
111
|
-
const store = await loadDynamoAccountStore(requireFx().client, requireFx().tableName);
|
|
112
|
-
expect(store).toBeDefined();
|
|
113
|
-
expect(store?.size).toBe(1);
|
|
114
|
-
expect(store?.verify('PLAIN', { kind: 'PLAIN', username: 'alice', password: 'old' }).ok).toBe(
|
|
115
|
-
false,
|
|
116
|
-
);
|
|
117
|
-
expect(store?.verify('PLAIN', { kind: 'PLAIN', username: 'alice', password: 'new' }).ok).toBe(
|
|
118
|
-
true,
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('rejects a wrong password after round-trip', async () => {
|
|
123
|
-
await putAccountCredential(requireFx().client, requireFx().tableName, 'alice', 'right');
|
|
124
|
-
const store = await loadDynamoAccountStore(requireFx().client, requireFx().tableName);
|
|
125
|
-
expect(store).toBeDefined();
|
|
126
|
-
expect(store?.verify('PLAIN', { kind: 'PLAIN', username: 'alice', password: 'wrong' }).ok).toBe(
|
|
127
|
-
false,
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('silently skips rows with missing or wrong-typed credential fields', async () => {
|
|
132
|
-
// The scan path must tolerate partially-malformed rows (legacy seeds,
|
|
133
|
-
// schema drift) without throwing — it just skips them. Each branch of
|
|
134
|
-
// the per-field `typeof === 'string'` guard is exercised by writing a
|
|
135
|
-
// row where exactly one non-key field has the wrong type. (The
|
|
136
|
-
// `account` field is the partition key, so DynamoDB itself enforces
|
|
137
|
-
// its type at write time and the false branch is not exercisable
|
|
138
|
-
// through the SDK; that branch is a defensive guard against
|
|
139
|
-
// synthetically-shaped Scan results.)
|
|
140
|
-
const client = requireFx().client;
|
|
141
|
-
const table = requireFx().tableName;
|
|
142
|
-
// Well-formed row — must survive.
|
|
143
|
-
await putAccountCredential(client, table, 'good', 's3cret');
|
|
144
|
-
// Row with non-string `algorithm`.
|
|
145
|
-
await client.send(
|
|
146
|
-
new PutCommand({
|
|
147
|
-
TableName: table,
|
|
148
|
-
Item: { account: 'noalgo', algorithm: 7, salt: 's', hash: 'h' },
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
|
-
// Row with non-string `salt`.
|
|
152
|
-
await client.send(
|
|
153
|
-
new PutCommand({
|
|
154
|
-
TableName: table,
|
|
155
|
-
Item: { account: 'nosalt', algorithm: 'SCRYPT-SHA-256', salt: false, hash: 'h' },
|
|
156
|
-
}),
|
|
157
|
-
);
|
|
158
|
-
// Row with non-string `hash`.
|
|
159
|
-
await client.send(
|
|
160
|
-
new PutCommand({
|
|
161
|
-
TableName: table,
|
|
162
|
-
Item: { account: 'nohash', algorithm: 'SCRYPT-SHA-256', salt: 's', hash: null },
|
|
163
|
-
}),
|
|
164
|
-
);
|
|
165
|
-
const store = await loadDynamoAccountStore(client, table);
|
|
166
|
-
expect(store).toBeDefined();
|
|
167
|
-
expect(store?.size).toBe(1);
|
|
168
|
-
expect(store?.verify('PLAIN', { kind: 'PLAIN', username: 'good', password: 's3cret' }).ok).toBe(
|
|
169
|
-
true,
|
|
170
|
-
);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe.skipIf(!available)('resolveAccountStore precedence', () => {
|
|
175
|
-
beforeEach(async () => {
|
|
176
|
-
fx = await makeFx();
|
|
177
|
-
});
|
|
178
|
-
afterEach(async () => {
|
|
179
|
-
if (fx) await fx.cleanup();
|
|
180
|
-
fx = null;
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('returns a DynamoAccountStore when the table has rows (table wins)', async () => {
|
|
184
|
-
await putAccountCredential(requireFx().client, requireFx().tableName, 'alice', 'table-secret');
|
|
185
|
-
const cfgWithSasl = makeTestServerConfig({
|
|
186
|
-
saslAccounts: [{ username: 'envuser', password: 'env-secret' }],
|
|
187
|
-
});
|
|
188
|
-
const store = await resolveAccountStore(requireFx().client, requireFx().tableName, cfgWithSasl);
|
|
189
|
-
expect(store).toBeDefined();
|
|
190
|
-
// Table account verifies; config account does NOT (table is authoritative).
|
|
191
|
-
expect(
|
|
192
|
-
store?.verify('PLAIN', { kind: 'PLAIN', username: 'alice', password: 'table-secret' }).ok,
|
|
193
|
-
).toBe(true);
|
|
194
|
-
expect(
|
|
195
|
-
store?.verify('PLAIN', { kind: 'PLAIN', username: 'envuser', password: 'env-secret' }).ok,
|
|
196
|
-
).toBe(false);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('falls back to the config-seeded store when the table is empty', async () => {
|
|
200
|
-
const cfgWithSasl = makeTestServerConfig({
|
|
201
|
-
saslAccounts: [{ username: 'envuser', password: 'env-secret' }],
|
|
202
|
-
});
|
|
203
|
-
const store = await resolveAccountStore(requireFx().client, requireFx().tableName, cfgWithSasl);
|
|
204
|
-
expect(store).toBeDefined();
|
|
205
|
-
expect(
|
|
206
|
-
store?.verify('PLAIN', { kind: 'PLAIN', username: 'envuser', password: 'env-secret' }).ok,
|
|
207
|
-
).toBe(true);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('returns undefined when the table is empty and no config accounts are set', async () => {
|
|
211
|
-
const store = await resolveAccountStore(
|
|
212
|
-
requireFx().client,
|
|
213
|
-
requireFx().tableName,
|
|
214
|
-
makeTestServerConfig(),
|
|
215
|
-
);
|
|
216
|
-
expect(store).toBeUndefined();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('returns undefined when the table is empty and serverConfig is undefined', async () => {
|
|
220
|
-
const store = await resolveAccountStore(requireFx().client, requireFx().tableName, undefined);
|
|
221
|
-
expect(store).toBeUndefined();
|
|
222
|
-
});
|
|
223
|
-
});
|
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import { DynamoDBDocumentClient, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
2
|
-
import type { ParsedServerConfig, SaslPayload } from '@serverless-ircd/irc-core';
|
|
3
|
-
import { makeTestServerConfig } from '@serverless-ircd/irc-test-support';
|
|
4
|
-
import { mockClient } from 'aws-sdk-client-mock';
|
|
5
|
-
import { describe, expect, it } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
DynamoAccountStore,
|
|
8
|
-
type HashedAccountCredential,
|
|
9
|
-
bindAccountStore,
|
|
10
|
-
hashAccountCredential,
|
|
11
|
-
putAccountCredential,
|
|
12
|
-
resolveAccountStore,
|
|
13
|
-
} from '../src/account-store';
|
|
14
|
-
|
|
15
|
-
function plainPayload(username: string, password: string): SaslPayload {
|
|
16
|
-
return { kind: 'PLAIN', username, password };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Builds a minimal parsed config with the supplied saslAccounts. */
|
|
20
|
-
function configWith(accounts: Array<{ username: string; password: string }>): ParsedServerConfig {
|
|
21
|
-
return makeTestServerConfig({ saslAccounts: accounts });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// bindAccountStore — config-seeded fallback
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
describe('bindAccountStore', () => {
|
|
29
|
-
it('returns an AccountStore that accepts valid PLAIN creds', () => {
|
|
30
|
-
const store = bindAccountStore(configWith([{ username: 'alice', password: 'secret' }]));
|
|
31
|
-
expect(store).toBeDefined();
|
|
32
|
-
const out = store?.verify('PLAIN', { kind: 'PLAIN', username: 'alice', password: 'secret' });
|
|
33
|
-
expect(out).toEqual({ ok: true, account: 'alice' });
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('returns undefined when the account list is empty', () => {
|
|
37
|
-
expect(bindAccountStore(configWith([]))).toBeUndefined();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('returns undefined when serverConfig is undefined', () => {
|
|
41
|
-
expect(bindAccountStore(undefined)).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('rejects wrong credentials', () => {
|
|
45
|
-
const store = bindAccountStore(configWith([{ username: 'alice', password: 'secret' }]));
|
|
46
|
-
expect(store).toBeDefined();
|
|
47
|
-
expect(store?.verify('PLAIN', { kind: 'PLAIN', username: 'alice', password: 'wrong' }).ok).toBe(
|
|
48
|
-
false,
|
|
49
|
-
);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// hashAccountCredential — pure scrypt hashing helper
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
describe('hashAccountCredential', () => {
|
|
58
|
-
it('produces an entry keyed by the username with scrypt', () => {
|
|
59
|
-
const entry = hashAccountCredential('alice', 'secret');
|
|
60
|
-
expect(entry.account).toBe('alice');
|
|
61
|
-
expect(entry.algorithm).toBe('scrypt');
|
|
62
|
-
expect(entry.salt.length).toBeGreaterThan(0);
|
|
63
|
-
expect(entry.hash.length).toBeGreaterThan(0);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('never stores the plaintext password in any field', () => {
|
|
67
|
-
const entry = hashAccountCredential('alice', 'hunter2-secret');
|
|
68
|
-
expect(entry.account).not.toContain('hunter2');
|
|
69
|
-
expect(entry.salt).not.toContain('hunter2');
|
|
70
|
-
expect(entry.hash).not.toContain('hunter2');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('produces different hashes for the same password under different salts', () => {
|
|
74
|
-
const a = hashAccountCredential('alice', 'secret', { salt: Buffer.from('aaaaaaaaaaaaaaaa') });
|
|
75
|
-
const b = hashAccountCredential('alice', 'secret', { salt: Buffer.from('bbbbbbbbbbbbbbbb') });
|
|
76
|
-
expect(a.hash).not.toBe(b.hash);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('is deterministic for a fixed salt', () => {
|
|
80
|
-
const salt = Buffer.from('1234567890123456');
|
|
81
|
-
const a = hashAccountCredential('alice', 'secret', { salt });
|
|
82
|
-
const b = hashAccountCredential('alice', 'secret', { salt });
|
|
83
|
-
expect(a.hash).toBe(b.hash);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('honours a custom key length', () => {
|
|
87
|
-
const entry = hashAccountCredential('alice', 'secret', { keyLen: 32 });
|
|
88
|
-
// base64 of 32 bytes → ~44 chars; base64 of 64 bytes → ~88 chars
|
|
89
|
-
expect(Buffer.from(entry.hash, 'base64').length).toBe(32);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// DynamoAccountStore — synchronous verify against pre-loaded hashed entries
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
describe('DynamoAccountStore', () => {
|
|
98
|
-
it('accepts valid PLAIN credentials and returns the account name', () => {
|
|
99
|
-
const entry = hashAccountCredential('alice', 'secret');
|
|
100
|
-
const store = new DynamoAccountStore([entry]);
|
|
101
|
-
expect(store.verify('PLAIN', plainPayload('alice', 'secret'))).toEqual({
|
|
102
|
-
ok: true,
|
|
103
|
-
account: 'alice',
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('rejects a wrong password', () => {
|
|
108
|
-
const entry = hashAccountCredential('alice', 'secret');
|
|
109
|
-
const store = new DynamoAccountStore([entry]);
|
|
110
|
-
expect(store.verify('PLAIN', plainPayload('alice', 'wrong')).ok).toBe(false);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('rejects an unknown username', () => {
|
|
114
|
-
const store = new DynamoAccountStore([hashAccountCredential('alice', 'secret')]);
|
|
115
|
-
expect(store.verify('PLAIN', plainPayload('bob', 'secret')).ok).toBe(false);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('rejects a non-PLAIN mechanism', () => {
|
|
119
|
-
const store = new DynamoAccountStore([hashAccountCredential('alice', 'secret')]);
|
|
120
|
-
expect(store.verify('EXTERNAL', plainPayload('alice', 'secret')).ok).toBe(false);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('rejects a RAW payload', () => {
|
|
124
|
-
const store = new DynamoAccountStore([hashAccountCredential('alice', 'secret')]);
|
|
125
|
-
expect(store.verify('PLAIN', { kind: 'RAW', data: 'whatever' }).ok).toBe(false);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('handles multiple loaded accounts', () => {
|
|
129
|
-
const store = new DynamoAccountStore([
|
|
130
|
-
hashAccountCredential('alice', 'a-secret'),
|
|
131
|
-
hashAccountCredential('bob', 'b-secret'),
|
|
132
|
-
]);
|
|
133
|
-
expect(store.verify('PLAIN', plainPayload('alice', 'a-secret'))).toEqual({
|
|
134
|
-
ok: true,
|
|
135
|
-
account: 'alice',
|
|
136
|
-
});
|
|
137
|
-
expect(store.verify('PLAIN', plainPayload('bob', 'b-secret'))).toEqual({
|
|
138
|
-
ok: true,
|
|
139
|
-
account: 'bob',
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('fails every verify when constructed with no entries', () => {
|
|
144
|
-
const store = new DynamoAccountStore([]);
|
|
145
|
-
expect(store.verify('PLAIN', plainPayload('alice', 'secret')).ok).toBe(false);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('round-trips an externally-constructed HashedAccountCredential', () => {
|
|
149
|
-
const row: HashedAccountCredential = hashAccountCredential('carol', 'p@ss');
|
|
150
|
-
const store = new DynamoAccountStore([row]);
|
|
151
|
-
expect(store.verify('PLAIN', plainPayload('carol', 'p@ss'))).toEqual({
|
|
152
|
-
ok: true,
|
|
153
|
-
account: 'carol',
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('reports the number of loaded entries via size', () => {
|
|
158
|
-
const store = new DynamoAccountStore([
|
|
159
|
-
hashAccountCredential('alice', 'a'),
|
|
160
|
-
hashAccountCredential('bob', 'b'),
|
|
161
|
-
]);
|
|
162
|
-
expect(store.size).toBe(2);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
// resolveAccountStore — table-then-config precedence (mock-based)
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
describe('resolveAccountStore', () => {
|
|
171
|
-
it('returns a DynamoAccountStore when the table has rows (table wins)', async () => {
|
|
172
|
-
const entry = hashAccountCredential('alice', 's3cret');
|
|
173
|
-
const ddb = mockClient(DynamoDBDocumentClient);
|
|
174
|
-
ddb.on(ScanCommand).resolves({
|
|
175
|
-
Items: [entry],
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
const fallback = bindAccountStore(configWith([{ username: 'envuser', password: 'env-pw' }]));
|
|
179
|
-
const store = await resolveAccountStore(
|
|
180
|
-
ddb as unknown as DynamoDBDocumentClient,
|
|
181
|
-
'Accounts',
|
|
182
|
-
fallback === undefined
|
|
183
|
-
? undefined
|
|
184
|
-
: makeTestServerConfig({ saslAccounts: [{ username: 'envuser', password: 'env-pw' }] }),
|
|
185
|
-
);
|
|
186
|
-
expect(store).toBeDefined();
|
|
187
|
-
expect(store?.verify('PLAIN', plainPayload('alice', 's3cret')).ok).toBe(true);
|
|
188
|
-
// Config account does NOT verify (table is authoritative).
|
|
189
|
-
expect(store?.verify('PLAIN', plainPayload('envuser', 'env-pw')).ok).toBe(false);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('falls back to config-seeded store when the table is empty', async () => {
|
|
193
|
-
const ddb = mockClient(DynamoDBDocumentClient);
|
|
194
|
-
ddb.on(ScanCommand).resolves({ Items: [] });
|
|
195
|
-
|
|
196
|
-
const store = await resolveAccountStore(
|
|
197
|
-
ddb as unknown as DynamoDBDocumentClient,
|
|
198
|
-
'Accounts',
|
|
199
|
-
configWith([{ username: 'bob', password: 'pw' }]),
|
|
200
|
-
);
|
|
201
|
-
expect(store).toBeDefined();
|
|
202
|
-
expect(store?.verify('PLAIN', plainPayload('bob', 'pw')).ok).toBe(true);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('falls back to config-seeded store when the scan throws', async () => {
|
|
206
|
-
const ddb = mockClient(DynamoDBDocumentClient);
|
|
207
|
-
ddb.on(ScanCommand).rejects(new Error('ResourceNotFoundException'));
|
|
208
|
-
|
|
209
|
-
const store = await resolveAccountStore(
|
|
210
|
-
ddb as unknown as DynamoDBDocumentClient,
|
|
211
|
-
'Accounts',
|
|
212
|
-
configWith([{ username: 'bob', password: 'pw' }]),
|
|
213
|
-
);
|
|
214
|
-
expect(store).toBeDefined();
|
|
215
|
-
expect(store?.verify('PLAIN', plainPayload('bob', 'pw')).ok).toBe(true);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('returns undefined when table is empty and no config accounts', async () => {
|
|
219
|
-
const ddb = mockClient(DynamoDBDocumentClient);
|
|
220
|
-
ddb.on(ScanCommand).resolves({ Items: [] });
|
|
221
|
-
|
|
222
|
-
const store = await resolveAccountStore(
|
|
223
|
-
ddb as unknown as DynamoDBDocumentClient,
|
|
224
|
-
'Accounts',
|
|
225
|
-
undefined,
|
|
226
|
-
);
|
|
227
|
-
expect(store).toBeUndefined();
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
// ---------------------------------------------------------------------------
|
|
232
|
-
// putAccountCredential — CRUD/seed entry point (mock-based)
|
|
233
|
-
// ---------------------------------------------------------------------------
|
|
234
|
-
|
|
235
|
-
describe('putAccountCredential', () => {
|
|
236
|
-
it('writes a hashed row to DynamoDB and returns the credential', async () => {
|
|
237
|
-
const ddb = mockClient(DynamoDBDocumentClient);
|
|
238
|
-
ddb.on(PutCommand).resolves({});
|
|
239
|
-
|
|
240
|
-
const entry = await putAccountCredential(
|
|
241
|
-
ddb as unknown as DynamoDBDocumentClient,
|
|
242
|
-
'Accounts',
|
|
243
|
-
'alice',
|
|
244
|
-
's3cret',
|
|
245
|
-
);
|
|
246
|
-
expect(entry.account).toBe('alice');
|
|
247
|
-
expect(entry.algorithm).toBe('scrypt');
|
|
248
|
-
|
|
249
|
-
const calls = ddb.commandCalls(PutCommand);
|
|
250
|
-
expect(calls).toHaveLength(1);
|
|
251
|
-
const item = calls[0]?.args[0].input.Item as HashedAccountCredential;
|
|
252
|
-
expect(item.account).toBe('alice');
|
|
253
|
-
expect(item.algorithm).toBe('scrypt');
|
|
254
|
-
expect(item.salt).toBe(entry.salt);
|
|
255
|
-
expect(item.hash).toBe(entry.hash);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('the written row round-trips through DynamoAccountStore', async () => {
|
|
259
|
-
const ddb = mockClient(DynamoDBDocumentClient);
|
|
260
|
-
ddb.on(PutCommand).resolves({});
|
|
261
|
-
|
|
262
|
-
const entry = await putAccountCredential(
|
|
263
|
-
ddb as unknown as DynamoDBDocumentClient,
|
|
264
|
-
'Accounts',
|
|
265
|
-
'alice',
|
|
266
|
-
's3cret',
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
// Simulate reading the row back via DynamoAccountStore.
|
|
270
|
-
const store = new DynamoAccountStore([entry]);
|
|
271
|
-
expect(store.verify('PLAIN', plainPayload('alice', 's3cret'))).toEqual({
|
|
272
|
-
ok: true,
|
|
273
|
-
account: 'alice',
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
});
|