serverless-ircd 0.1.0 → 0.2.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 +96 -2
- package/.github/workflows/deploy-aws.yml +129 -0
- package/.github/workflows/deploy-cf.yml +0 -2
- package/.gitmodules +3 -0
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +352 -0
- package/MOTD.txt +3 -0
- package/PLAN-FIXES.md +358 -0
- package/PLAN.md +420 -0
- package/README.md +115 -81
- package/apps/aws-stack/README.md +73 -0
- package/apps/aws-stack/bin/aws.ts +49 -0
- package/apps/aws-stack/cdk.json +10 -0
- package/apps/aws-stack/package.json +41 -0
- package/apps/aws-stack/scripts/smoke-helpers.d.mts +22 -0
- package/apps/aws-stack/scripts/smoke-helpers.mjs +89 -0
- package/apps/aws-stack/scripts/smoke.mjs +142 -0
- package/apps/aws-stack/src/aws-stack.ts +263 -0
- package/apps/aws-stack/src/tables.ts +10 -0
- package/apps/aws-stack/tests/localstack.test.ts +46 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +98 -0
- package/apps/aws-stack/tests/stack.test.ts +464 -0
- package/apps/aws-stack/tsconfig.build.json +11 -0
- package/apps/aws-stack/tsconfig.test.json +8 -0
- package/apps/aws-stack/vitest.config.ts +20 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/wrangler.toml +25 -0
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +56 -0
- package/apps/local-cli/src/server.ts +241 -32
- package/apps/local-cli/tests/config-loader.test.ts +107 -0
- package/apps/local-cli/tests/e2e.test.ts +74 -0
- package/apps/local-cli/tests/security-e2e.test.ts +239 -0
- package/biome.json +3 -1
- package/dashboards/cloudwatch-irc.json +139 -0
- package/docs/AWS-Adapter-Architecture.md +494 -0
- package/docs/AWS-Deployment.md +1107 -0
- package/docs/Cloudflare-Deployment-Guide.md +660 -0
- package/docs/Home.md +1 -0
- package/docs/Observability.md +87 -0
- package/docs/PlanIRCv3Websocket.md +489 -0
- package/docs/PlanWebClient.md +451 -0
- package/docs/Release-Process.md +440 -0
- package/package.json +8 -2
- package/packages/aws-adapter/README.md +35 -0
- package/packages/aws-adapter/package.json +52 -0
- package/packages/aws-adapter/src/aws-runtime.ts +783 -0
- package/packages/aws-adapter/src/cdk-table-defs.ts +69 -0
- package/packages/aws-adapter/src/config-loader.ts +96 -0
- package/packages/aws-adapter/src/dynamo.ts +61 -0
- package/packages/aws-adapter/src/handlers/connect.ts +44 -0
- package/packages/aws-adapter/src/handlers/default.ts +322 -0
- package/packages/aws-adapter/src/handlers/disconnect.ts +29 -0
- package/packages/aws-adapter/src/handlers/index.ts +248 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +217 -0
- package/packages/aws-adapter/src/handlers/state.ts +13 -0
- package/packages/aws-adapter/src/handlers/sweeper.ts +84 -0
- package/packages/aws-adapter/src/index.ts +67 -0
- package/packages/aws-adapter/src/message-store.ts +34 -0
- package/packages/aws-adapter/src/serialize.ts +283 -0
- package/packages/aws-adapter/src/tables.ts +53 -0
- package/packages/aws-adapter/tests/aws-harness.ts +408 -0
- package/packages/aws-adapter/tests/aws-integration.test.ts +17 -0
- package/packages/aws-adapter/tests/aws-runtime.test.ts +654 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +158 -0
- package/packages/aws-adapter/tests/dynamo.test.ts +57 -0
- package/packages/aws-adapter/tests/global-setup.ts +301 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +271 -0
- package/packages/aws-adapter/tests/handlers.test.ts +427 -0
- package/packages/aws-adapter/tests/message-store.test.ts +55 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +571 -0
- package/packages/aws-adapter/tests/serialize.test.ts +249 -0
- package/packages/aws-adapter/tests/smoke.test.ts +48 -0
- package/packages/aws-adapter/tests/sweeper.test.ts +420 -0
- package/packages/aws-adapter/tests/tables.test.ts +74 -0
- package/packages/aws-adapter/tests/transactions.test.ts +446 -0
- package/packages/aws-adapter/tsconfig.build.json +16 -0
- package/packages/aws-adapter/tsconfig.test.json +14 -0
- package/packages/aws-adapter/vitest.config.ts +23 -0
- package/packages/cf-adapter/package.json +2 -1
- package/packages/cf-adapter/src/config-loader.ts +106 -0
- package/packages/cf-adapter/src/connection-do.ts +34 -0
- package/packages/cf-adapter/tests/cf-harness.ts +2 -2
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -2
- package/packages/cf-adapter/tests/config-loader.test.ts +149 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +42 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +108 -3
- package/packages/in-memory-runtime/src/index.ts +1 -1
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +115 -0
- package/packages/irc-core/package.json +12 -2
- package/packages/irc-core/src/admission.ts +216 -0
- package/packages/irc-core/src/caps/capabilities.ts +1 -0
- package/packages/irc-core/src/cloak.ts +81 -0
- package/packages/irc-core/src/commands/cap.ts +1 -2
- package/packages/irc-core/src/commands/chathistory.ts +305 -0
- package/packages/irc-core/src/commands/index.ts +7 -0
- package/packages/irc-core/src/commands/join.ts +59 -7
- package/packages/irc-core/src/commands/privmsg.ts +24 -0
- package/packages/irc-core/src/commands/registration.ts +71 -14
- package/packages/irc-core/src/commands/sasl.ts +251 -0
- package/packages/irc-core/src/config.ts +247 -0
- package/packages/irc-core/src/flood-control.ts +175 -0
- package/packages/irc-core/src/index.ts +5 -0
- package/packages/irc-core/src/ports.ts +655 -0
- package/packages/irc-core/src/protocol/base64.ts +16 -0
- package/packages/irc-core/src/protocol/index.ts +2 -1
- package/packages/irc-core/src/protocol/numerics.ts +11 -0
- package/packages/irc-core/src/protocol/parser.ts +27 -2
- package/packages/irc-core/src/state/connection.ts +41 -0
- package/packages/irc-core/src/types.ts +66 -2
- package/packages/irc-core/stryker.commands.conf.json +41 -0
- package/packages/irc-core/stryker.protocol.conf.json +26 -0
- package/packages/irc-core/tests/admission.test.ts +229 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +22 -0
- package/packages/irc-core/tests/cloak.test.ts +78 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +996 -0
- package/packages/irc-core/tests/commands/join.test.ts +246 -2
- package/packages/irc-core/tests/commands/privmsg.test.ts +146 -2
- package/packages/irc-core/tests/commands/registration.test.ts +277 -9
- package/packages/irc-core/tests/commands/sasl.test.ts +536 -0
- package/packages/irc-core/tests/config.test.ts +646 -0
- package/packages/irc-core/tests/flood-control.test.ts +394 -0
- package/packages/irc-core/tests/message-store.test.ts +530 -0
- package/packages/irc-core/tests/parser.test.ts +44 -1
- package/packages/irc-core/tests/ports.test.ts +263 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +162 -44
- package/packages/irc-server/src/dispatch.ts +89 -5
- package/packages/irc-server/src/index.ts +2 -0
- package/packages/irc-server/src/routing.ts +151 -0
- package/packages/irc-server/tests/actor.test.ts +470 -14
- package/packages/irc-server/tests/dispatch.test.ts +84 -0
- package/packages/irc-server/tests/routing.test.ts +201 -0
- package/packages/irc-test-support/README.md +32 -0
- package/packages/irc-test-support/package.json +37 -0
- package/packages/{cf-adapter/tests/integration → irc-test-support/src}/in-memory-harness.ts +1 -1
- package/packages/irc-test-support/src/index.ts +24 -0
- package/packages/{cf-adapter/tests/integration/harness.test.ts → irc-test-support/tests/in-memory-harness.test.ts} +1 -1
- package/packages/{cf-adapter/tests/integration/scenarios.test.ts → irc-test-support/tests/in-memory-scenarios.test.ts} +1 -2
- package/packages/irc-test-support/tests/smoke.test.ts +11 -0
- package/packages/irc-test-support/tsconfig.build.json +16 -0
- package/packages/irc-test-support/tsconfig.test.json +14 -0
- package/packages/irc-test-support/vitest.config.ts +20 -0
- package/progress.md +107 -0
- package/tickets.md +2485 -0
- package/tools/ci-hardening/package.json +30 -0
- package/tools/ci-hardening/src/index.ts +6 -0
- package/tools/ci-hardening/src/validate.ts +103 -0
- package/tools/ci-hardening/tests/__no_thresholds__.txt +11 -0
- package/tools/ci-hardening/tests/__partial_thresholds__.txt +16 -0
- package/tools/ci-hardening/tests/__stryker_bad__.json +4 -0
- package/tools/ci-hardening/tests/validate.test.ts +177 -0
- package/tools/ci-hardening/tsconfig.build.json +12 -0
- package/tools/ci-hardening/tsconfig.test.json +10 -0
- package/tools/ci-hardening/vitest.config.ts +21 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/tools/tcp-ws-forwarder/src/forwarder.ts +34 -23
- package/tools/tcp-ws-forwarder/src/logger.ts +155 -0
- package/tools/tcp-ws-forwarder/src/main.ts +33 -14
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +86 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +136 -0
- package/webircgateway/LICENSE +201 -0
- package/webircgateway/Makefile +44 -0
- package/webircgateway/README.md +134 -0
- package/webircgateway/config.conf.example +135 -0
- package/webircgateway/go.mod +16 -0
- package/webircgateway/go.sum +89 -0
- package/webircgateway/main.go +118 -0
- package/webircgateway/pkg/dnsbl/dnsbl.go +121 -0
- package/webircgateway/pkg/identd/identd.go +86 -0
- package/webircgateway/pkg/identd/rpcclient.go +59 -0
- package/webircgateway/pkg/irc/isupport.go +56 -0
- package/webircgateway/pkg/irc/message.go +217 -0
- package/webircgateway/pkg/irc/state.go +79 -0
- package/webircgateway/pkg/proxy/proxy.go +129 -0
- package/webircgateway/pkg/proxy/server.go +237 -0
- package/webircgateway/pkg/recaptcha/recaptcha.go +59 -0
- package/webircgateway/pkg/webircgateway/client.go +741 -0
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +495 -0
- package/webircgateway/pkg/webircgateway/config.go +385 -0
- package/webircgateway/pkg/webircgateway/gateway.go +278 -0
- package/webircgateway/pkg/webircgateway/gateway_utils.go +133 -0
- package/webircgateway/pkg/webircgateway/hooks.go +152 -0
- package/webircgateway/pkg/webircgateway/letsencrypt.go +41 -0
- package/webircgateway/pkg/webircgateway/messagetags.go +103 -0
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +206 -0
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +107 -0
- package/webircgateway/pkg/webircgateway/transport_tcp.go +113 -0
- package/webircgateway/pkg/webircgateway/transport_websocket.go +126 -0
- package/webircgateway/pkg/webircgateway/utils.go +147 -0
- package/webircgateway/plugins/example/plugin.go +11 -0
- package/webircgateway/plugins/stats/plugin.go +52 -0
- package/webircgateway/staticcheck.conf +1 -0
- package/webircgateway/webircgateway.svg +3 -0
- package/packages/cf-adapter/tests/integration/index.ts +0 -17
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/harness.ts +0 -0
- /package/packages/{cf-adapter/tests/integration → irc-test-support/src}/scenarios.ts +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marshal / unmarshal helpers between irc-core state objects and the
|
|
3
|
+
* DynamoDB NativeAttributeMap shapes (`@aws-sdk/util-dynamodb`).
|
|
4
|
+
*
|
|
5
|
+
* Why custom (not `marshall`/`unmarshall` alone): irc-core uses `Set`
|
|
6
|
+
* and `Map` for `caps`, `joinedChannels`, channel `members`, `banMasks`,
|
|
7
|
+
* etc. The AWS SDK's default marshallers turn `Set` into a DynamoDB
|
|
8
|
+
* `SS`/`NS`/`BS` typed value and `Map` into an `M`-typed nested attribute
|
|
9
|
+
* — both of which are awkward to query. We expand Sets to arrays and
|
|
10
|
+
* Maps to arrays of `[key, value]` pairs so every column is a flat
|
|
11
|
+
* scalar/list whose shape matches the PLAN §4 access patterns.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle:
|
|
14
|
+
* - `marshal*` — irc-core value → DynamoDB item (write path).
|
|
15
|
+
* - `unmarshal*` — DynamoDB item → irc-core value (read path).
|
|
16
|
+
*
|
|
17
|
+
* Round-trip invariant: `unmarshal*(marshal*(x))` is structurally equal
|
|
18
|
+
* to `x` for every value `x`. Tested exhaustively against the
|
|
19
|
+
* irc-core state generators.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
ChanName,
|
|
24
|
+
ChannelModes,
|
|
25
|
+
ChannelState,
|
|
26
|
+
ChannelTopic,
|
|
27
|
+
ConnId,
|
|
28
|
+
ConnectionState,
|
|
29
|
+
Nick,
|
|
30
|
+
RegistrationState,
|
|
31
|
+
RosterEntry,
|
|
32
|
+
UserModes,
|
|
33
|
+
} from '@serverless-ircd/irc-core';
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Connections
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/** Current Connections schema version. Bump when the shape changes. */
|
|
40
|
+
export const CONNECTION_VERSION = 1;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Persists a {@link ConnectionState} row. The `version` field supports
|
|
44
|
+
* forward migrations on read (mirrors the CF adapter's `PersistedConnectionState`).
|
|
45
|
+
*
|
|
46
|
+
* Storage-shape notes:
|
|
47
|
+
* - `joinedChannels` is stored as a DynamoDB **String Set** (`SS`) so
|
|
48
|
+
* the membership transaction can atomically `ADD`/`DELETE` a channel
|
|
49
|
+
* (DynamoDB set ops). The attribute is OMITTED when empty because
|
|
50
|
+
* DynamoDB rejects empty sets — the transaction path creates it on
|
|
51
|
+
* the first JOIN. `caps` stays an array: nothing needs to mutate a
|
|
52
|
+
* single cap atomically.
|
|
53
|
+
*/
|
|
54
|
+
export interface MarshalledConnection {
|
|
55
|
+
connectionId: ConnId;
|
|
56
|
+
registration: RegistrationState;
|
|
57
|
+
capNegotiating: boolean;
|
|
58
|
+
caps: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Present (as a JS `Set`, auto-marshalled to `SS`) only when the
|
|
61
|
+
* connection has joined at least one channel. Absent otherwise —
|
|
62
|
+
* DynamoDB forbids empty sets, so we omit rather than store `SS: []`.
|
|
63
|
+
*/
|
|
64
|
+
joinedChannels?: Set<ChanName>;
|
|
65
|
+
userModes: UserModes;
|
|
66
|
+
lastSeen: number;
|
|
67
|
+
connectedSince: number;
|
|
68
|
+
idleSince: number;
|
|
69
|
+
version: number;
|
|
70
|
+
nick?: Nick;
|
|
71
|
+
user?: string;
|
|
72
|
+
host?: string;
|
|
73
|
+
realname?: string;
|
|
74
|
+
passAttempt?: string;
|
|
75
|
+
account?: string;
|
|
76
|
+
away?: string;
|
|
77
|
+
saslMech?: string;
|
|
78
|
+
saslBuffer?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Marshals a {@link ConnectionState} into a plain JS object ready for
|
|
83
|
+
* `PutCommand` / `UpdateCommand`. `idleSince` is the TTL attribute and
|
|
84
|
+
* is set to the supplied epoch ms (callers pass `Date.now()`).
|
|
85
|
+
*
|
|
86
|
+
* `joinedChannels` is emitted as a native `Set` only when non-empty;
|
|
87
|
+
* the AWS SDK marshalls it to a DynamoDB `SS`, enabling atomic
|
|
88
|
+
* `ADD`/`DELETE` from the membership transaction without read-modify-write.
|
|
89
|
+
*/
|
|
90
|
+
export function marshalConnection(state: ConnectionState, idleSince: number): MarshalledConnection {
|
|
91
|
+
const out: MarshalledConnection = {
|
|
92
|
+
connectionId: state.id,
|
|
93
|
+
registration: state.registration,
|
|
94
|
+
capNegotiating: state.capNegotiating,
|
|
95
|
+
caps: [...state.caps],
|
|
96
|
+
userModes: { ...state.userModes },
|
|
97
|
+
lastSeen: state.lastSeen,
|
|
98
|
+
connectedSince: state.connectedSince,
|
|
99
|
+
idleSince,
|
|
100
|
+
version: CONNECTION_VERSION,
|
|
101
|
+
};
|
|
102
|
+
if (state.joinedChannels.size > 0) {
|
|
103
|
+
// Copy so later mutations to `state` don't alias the stored value.
|
|
104
|
+
out.joinedChannels = new Set<ChanName>(state.joinedChannels);
|
|
105
|
+
}
|
|
106
|
+
if (state.nick !== undefined) out.nick = state.nick;
|
|
107
|
+
if (state.user !== undefined) out.user = state.user;
|
|
108
|
+
if (state.host !== undefined) out.host = state.host;
|
|
109
|
+
if (state.realname !== undefined) out.realname = state.realname;
|
|
110
|
+
if (state.passAttempt !== undefined) out.passAttempt = state.passAttempt;
|
|
111
|
+
if (state.account !== undefined) out.account = state.account;
|
|
112
|
+
if (state.away !== undefined) out.away = state.away;
|
|
113
|
+
if (state.saslMech !== undefined) out.saslMech = state.saslMech;
|
|
114
|
+
if (state.saslBuffer !== undefined) out.saslBuffer = state.saslBuffer;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Materialises a {@link ConnectionState} from a stored row. Rehydrates
|
|
120
|
+
* `caps` and `joinedChannels` into Sets. Throws on unrecognised
|
|
121
|
+
* `version` values (forward migrations live in a future ticket).
|
|
122
|
+
*/
|
|
123
|
+
export function unmarshalConnection(row: MarshalledConnection): ConnectionState {
|
|
124
|
+
if (row.version > CONNECTION_VERSION) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Connections row version ${row.version} is newer than supported ${CONNECTION_VERSION}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const state: ConnectionState = {
|
|
130
|
+
id: row.connectionId,
|
|
131
|
+
registration: row.registration,
|
|
132
|
+
capNegotiating: row.capNegotiating,
|
|
133
|
+
caps: new Set<string>(row.caps),
|
|
134
|
+
// `joinedChannels` is absent on rows that never joined a channel
|
|
135
|
+
// (DynamoDB rejects empty sets); default to an empty Set. The SDK
|
|
136
|
+
// unmarshalls `SS` back to a native Set, which the Set constructor
|
|
137
|
+
// copies; arrays (older rows) also iterate fine.
|
|
138
|
+
joinedChannels: new Set<ChanName>(row.joinedChannels ?? []),
|
|
139
|
+
userModes: { ...row.userModes },
|
|
140
|
+
lastSeen: row.lastSeen,
|
|
141
|
+
connectedSince: row.connectedSince,
|
|
142
|
+
};
|
|
143
|
+
if (row.nick !== undefined) state.nick = row.nick;
|
|
144
|
+
if (row.user !== undefined) state.user = row.user;
|
|
145
|
+
if (row.host !== undefined) state.host = row.host;
|
|
146
|
+
if (row.realname !== undefined) state.realname = row.realname;
|
|
147
|
+
if (row.passAttempt !== undefined) state.passAttempt = row.passAttempt;
|
|
148
|
+
if (row.account !== undefined) state.account = row.account;
|
|
149
|
+
if (row.away !== undefined) state.away = row.away;
|
|
150
|
+
if (row.saslMech !== undefined) state.saslMech = row.saslMech;
|
|
151
|
+
if (row.saslBuffer !== undefined) state.saslBuffer = row.saslBuffer;
|
|
152
|
+
return state;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Nicks
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
/** Stored shape for the `Nicks` table. */
|
|
160
|
+
export interface MarshalledNick {
|
|
161
|
+
nickLower: string;
|
|
162
|
+
nick: Nick;
|
|
163
|
+
connectionId: ConnId;
|
|
164
|
+
reservedAt: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Marshals a nick registration into the stored row shape. */
|
|
168
|
+
export function marshalNick(nick: Nick, connectionId: ConnId, reservedAt: number): MarshalledNick {
|
|
169
|
+
return {
|
|
170
|
+
nickLower: nick.toLowerCase(),
|
|
171
|
+
nick,
|
|
172
|
+
connectionId,
|
|
173
|
+
reservedAt,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Materialises the canonical (nick → connectionId) lookup from a row. */
|
|
178
|
+
export function unmarshalNick(row: MarshalledNick): { nick: Nick; connectionId: ConnId } {
|
|
179
|
+
return { nick: row.nick, connectionId: row.connectionId };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// ChannelMeta
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/** Stored shape for the `ChannelMeta` table (modes + topic, no members). */
|
|
187
|
+
export interface MarshalledChannelMeta {
|
|
188
|
+
channelName: string;
|
|
189
|
+
displayName: ChanName;
|
|
190
|
+
modes: ChannelModes;
|
|
191
|
+
banMasks: string[];
|
|
192
|
+
pendingInvites: string[];
|
|
193
|
+
createdAt: number;
|
|
194
|
+
topicText?: string;
|
|
195
|
+
topicSetter?: string;
|
|
196
|
+
topicSetAt?: number;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Marshals a channel's metadata into the stored row shape. */
|
|
200
|
+
export function marshalChannelMeta(chan: ChannelState): MarshalledChannelMeta {
|
|
201
|
+
const out: MarshalledChannelMeta = {
|
|
202
|
+
channelName: chan.nameLower,
|
|
203
|
+
displayName: chan.name,
|
|
204
|
+
modes: { ...chan.modes },
|
|
205
|
+
banMasks: [...chan.banMasks],
|
|
206
|
+
pendingInvites: [...chan.pendingInvites],
|
|
207
|
+
createdAt: chan.createdAt,
|
|
208
|
+
};
|
|
209
|
+
if (chan.topic !== undefined) {
|
|
210
|
+
out.topicText = chan.topic.text;
|
|
211
|
+
out.topicSetter = chan.topic.setter;
|
|
212
|
+
out.topicSetAt = chan.topic.setAt;
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Materialises a {@link ChannelState} from the meta row + the (separately
|
|
219
|
+
* fetched) member list. The caller is expected to populate `members`
|
|
220
|
+
* afterwards — meta and members live in different tables so they're fetched
|
|
221
|
+
* in parallel by the runtime.
|
|
222
|
+
*/
|
|
223
|
+
export function unmarshalChannelMeta(
|
|
224
|
+
row: MarshalledChannelMeta,
|
|
225
|
+
members: ReadonlyMap<ConnId, RosterEntry>,
|
|
226
|
+
): ChannelState {
|
|
227
|
+
let topic: ChannelTopic | undefined;
|
|
228
|
+
if (
|
|
229
|
+
row.topicText !== undefined &&
|
|
230
|
+
row.topicSetter !== undefined &&
|
|
231
|
+
row.topicSetAt !== undefined
|
|
232
|
+
) {
|
|
233
|
+
topic = { text: row.topicText, setter: row.topicSetter, setAt: row.topicSetAt };
|
|
234
|
+
}
|
|
235
|
+
const chan: ChannelState = {
|
|
236
|
+
name: row.displayName,
|
|
237
|
+
nameLower: row.channelName,
|
|
238
|
+
modes: { ...row.modes },
|
|
239
|
+
banMasks: new Set<string>(row.banMasks),
|
|
240
|
+
members: new Map<ConnId, RosterEntry>(members),
|
|
241
|
+
pendingInvites: new Set<Nick>(row.pendingInvites),
|
|
242
|
+
createdAt: row.createdAt,
|
|
243
|
+
};
|
|
244
|
+
if (topic !== undefined) chan.topic = topic;
|
|
245
|
+
return chan;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// ChannelMembers
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/** Stored shape for the `ChannelMembers` table — one row per (chan, conn). */
|
|
253
|
+
export interface MarshalledChannelMember {
|
|
254
|
+
channelName: string;
|
|
255
|
+
connectionId: ConnId;
|
|
256
|
+
nick: Nick;
|
|
257
|
+
op: boolean;
|
|
258
|
+
voice: boolean;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Marshals a single roster entry into the stored row shape. */
|
|
262
|
+
export function marshalChannelMember(
|
|
263
|
+
channelName: string,
|
|
264
|
+
entry: RosterEntry,
|
|
265
|
+
): MarshalledChannelMember {
|
|
266
|
+
return {
|
|
267
|
+
channelName,
|
|
268
|
+
connectionId: entry.conn,
|
|
269
|
+
nick: entry.nick,
|
|
270
|
+
op: entry.op,
|
|
271
|
+
voice: entry.voice,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Materialises a {@link RosterEntry} from a stored member row. */
|
|
276
|
+
export function unmarshalChannelMember(row: MarshalledChannelMember): RosterEntry {
|
|
277
|
+
return {
|
|
278
|
+
conn: row.connectionId,
|
|
279
|
+
nick: row.nick,
|
|
280
|
+
op: row.op,
|
|
281
|
+
voice: row.voice,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-safe DynamoDB table metadata (PLAN §4).
|
|
3
|
+
*
|
|
4
|
+
* The CDK-coupled `TABLE_DEFS` (TableProps with `AttributeType`/`BillingMode`
|
|
5
|
+
* enums) lives in `./cdk-table-defs.ts` so the Lambda handler bundle does
|
|
6
|
+
* not have to ship `aws-cdk-lib`. The runtime only needs:
|
|
7
|
+
*
|
|
8
|
+
* - the *names* of the logical tables (so env-var→TablesConfig lookup works),
|
|
9
|
+
* - the *names* of the key columns (so DynamoDB DocumentClient commands
|
|
10
|
+
* reference them without stringly-typed typos).
|
|
11
|
+
*
|
|
12
|
+
* Both files share {@link TableName} as the single source of truth for the
|
|
13
|
+
* canonical logical names (`Connections`, `ChannelMeta`, …).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Logical table names — also used as CDK construct ids. */
|
|
17
|
+
export type TableName = 'Connections' | 'ChannelMeta' | 'ChannelMembers' | 'Nicks' | 'Accounts';
|
|
18
|
+
|
|
19
|
+
/** Logical table names in stable order. */
|
|
20
|
+
export const TABLE_NAMES: readonly TableName[] = [
|
|
21
|
+
'Connections',
|
|
22
|
+
'ChannelMeta',
|
|
23
|
+
'ChannelMembers',
|
|
24
|
+
'Nicks',
|
|
25
|
+
'Accounts',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** Key column names used throughout the runtime (avoids stringly-typed typos). */
|
|
29
|
+
export const TABLE_KEYS = {
|
|
30
|
+
Connections: { pk: 'connectionId' },
|
|
31
|
+
ChannelMeta: { pk: 'channelName' },
|
|
32
|
+
ChannelMembers: { pk: 'channelName', sk: 'connectionId' },
|
|
33
|
+
Nicks: { pk: 'nickLower' },
|
|
34
|
+
Accounts: { pk: 'account' },
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maps logical names to physical table names. Production uses the bare
|
|
39
|
+
* names; tests override with a unique prefix per harness to isolate
|
|
40
|
+
* parallel runs against a shared DynamoDB Local instance.
|
|
41
|
+
*/
|
|
42
|
+
export type TablesConfig = Record<TableName, string>;
|
|
43
|
+
|
|
44
|
+
/** Builds a {@link TablesConfig} from the supplied physical table names. */
|
|
45
|
+
export function tablesConfigFromNames(names: {
|
|
46
|
+
Connections: string;
|
|
47
|
+
ChannelMeta: string;
|
|
48
|
+
ChannelMembers: string;
|
|
49
|
+
Nicks: string;
|
|
50
|
+
Accounts: string;
|
|
51
|
+
}): TablesConfig {
|
|
52
|
+
return { ...names };
|
|
53
|
+
}
|