serverless-ircd 0.2.0 → 0.4.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 +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +258 -18
- package/README.md +72 -30
- package/apps/aws-stack/README.md +2 -2
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +118 -6
- package/apps/aws-stack/tests/stack.test.ts +98 -3
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/scripts/smoke.mjs +0 -0
- package/apps/cf-worker/src/worker.ts +8 -2
- package/apps/cf-worker/tests/smoke.test.ts +1 -0
- package/apps/cf-worker/wrangler.test.toml +11 -1
- package/apps/cf-worker/wrangler.toml +69 -19
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +11 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +46 -3
- package/apps/local-cli/tests/e2e.test.ts +52 -0
- package/package.json +19 -16
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +124 -34
- package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
- package/packages/aws-adapter/src/config-loader.ts +62 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
- package/packages/aws-adapter/src/handlers/connect.ts +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +43 -2
- package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
- package/packages/aws-adapter/src/handlers/index.ts +120 -9
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +13 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
- package/packages/aws-adapter/tests/account-store.test.ts +279 -0
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +75 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +194 -47
- package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +40 -22
- package/packages/cf-adapter/src/channel-do.ts +40 -1
- package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
- package/packages/cf-adapter/src/config-loader.ts +62 -0
- package/packages/cf-adapter/src/connection-do.ts +181 -31
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +58 -8
- package/packages/cf-adapter/src/index.ts +11 -9
- package/packages/cf-adapter/tests/cf-harness.ts +11 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +40 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/worker/main.ts +9 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- 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 +14 -28
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
- package/packages/irc-core/package.json +8 -2
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/caps/capabilities.ts +23 -2
- package/packages/irc-core/src/case-fold.ts +64 -0
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +11 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +4 -3
- package/packages/irc-core/src/commands/kick.ts +4 -11
- package/packages/irc-core/src/commands/list.ts +5 -4
- package/packages/irc-core/src/commands/mode.ts +5 -9
- package/packages/irc-core/src/commands/motd-lines.ts +127 -0
- package/packages/irc-core/src/commands/motd.ts +6 -110
- package/packages/irc-core/src/commands/oper.ts +151 -0
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +26 -19
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/tagmsg.ts +205 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +84 -3
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +6 -29
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +240 -7
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +60 -1
- package/packages/irc-core/tests/account-store.test.ts +131 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -0
- package/packages/irc-core/tests/commands/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +256 -19
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +662 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/who.test.ts +22 -4
- package/packages/irc-core/tests/commands/whois.test.ts +8 -1
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +170 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/effects.test.ts +0 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +12 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +182 -14
- package/packages/irc-server/src/dispatch.ts +0 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +21 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +617 -1
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +78 -13
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +132 -2
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +9 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +79 -0
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/AGENTS.md +0 -5
- package/MOTD.txt +0 -3
- package/PLAN-FIXES.md +0 -358
- package/PLAN.md +0 -420
- package/dashboards/cloudwatch-irc.json +0 -139
- package/docs/AWS-Adapter-Architecture.md +0 -494
- package/docs/AWS-Deployment.md +0 -1107
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -1
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -440
- package/progress.md +0 -107
- package/tickets.md +0 -2485
- package/webircgateway/LICENSE +0 -201
- package/webircgateway/Makefile +0 -44
- package/webircgateway/README.md +0 -134
- package/webircgateway/config.conf.example +0 -135
- package/webircgateway/go.mod +0 -16
- package/webircgateway/go.sum +0 -89
- package/webircgateway/main.go +0 -118
- package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
- package/webircgateway/pkg/identd/identd.go +0 -86
- package/webircgateway/pkg/identd/rpcclient.go +0 -59
- package/webircgateway/pkg/irc/isupport.go +0 -56
- package/webircgateway/pkg/irc/message.go +0 -217
- package/webircgateway/pkg/irc/state.go +0 -79
- package/webircgateway/pkg/proxy/proxy.go +0 -129
- package/webircgateway/pkg/proxy/server.go +0 -237
- package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
- package/webircgateway/pkg/webircgateway/client.go +0 -741
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
- package/webircgateway/pkg/webircgateway/config.go +0 -385
- package/webircgateway/pkg/webircgateway/gateway.go +0 -278
- package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
- package/webircgateway/pkg/webircgateway/hooks.go +0 -152
- package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
- package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
- package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
- package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
- package/webircgateway/pkg/webircgateway/utils.go +0 -147
- package/webircgateway/plugins/example/plugin.go +0 -11
- package/webircgateway/plugins/stats/plugin.go +0 -52
- package/webircgateway/staticcheck.conf +0 -1
- package/webircgateway/webircgateway.svg +0 -3
|
@@ -33,9 +33,11 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import {
|
|
36
|
+
SASL_CAP_NAME,
|
|
36
37
|
SUPPORTED_CAPABILITIES,
|
|
37
38
|
capToLsString,
|
|
38
39
|
isSupportedCap,
|
|
40
|
+
saslCapValue,
|
|
39
41
|
splitCapsForWire,
|
|
40
42
|
} from '../caps/capabilities.js';
|
|
41
43
|
import { Effect } from '../effects.js';
|
|
@@ -88,7 +90,12 @@ function handleLs(state: ConnectionState, ctx: Ctx): ReducerResult<ConnectionSta
|
|
|
88
90
|
state.capNegotiating = true;
|
|
89
91
|
|
|
90
92
|
const target = state.nick ?? '*';
|
|
91
|
-
const
|
|
93
|
+
const hasMtls = ctx.mtlsIdentity !== undefined;
|
|
94
|
+
const tokens = SUPPORTED_CAPABILITIES.map((cap) =>
|
|
95
|
+
cap.name === SASL_CAP_NAME
|
|
96
|
+
? capToLsString({ ...cap, value: saslCapValue(hasMtls) })
|
|
97
|
+
: capToLsString(cap),
|
|
98
|
+
);
|
|
92
99
|
const maxLen = maxCapsContentLen(ctx, target);
|
|
93
100
|
const chunks = splitCapsForWire(tokens, maxLen);
|
|
94
101
|
|
|
@@ -18,6 +18,7 @@ export { quitReducer } from './quit.js';
|
|
|
18
18
|
export { handleJoinZero, isValidChannelName, joinReducer } from './join.js';
|
|
19
19
|
export { partReducer } from './part.js';
|
|
20
20
|
export { motdReducer } from './motd.js';
|
|
21
|
+
export { operReducer, matchOperCred } from './oper.js';
|
|
21
22
|
export {
|
|
22
23
|
isChannelTarget,
|
|
23
24
|
noticeChannelReducer,
|
|
@@ -33,6 +34,7 @@ export { channelModeReducer, userModeReducer } from './mode.js';
|
|
|
33
34
|
export { inviteReducer } from './invite.js';
|
|
34
35
|
export { whoReducer } from './who.js';
|
|
35
36
|
export { whoisReducer } from './whois.js';
|
|
37
|
+
export { whowasReducer } from './whowas.js';
|
|
36
38
|
export { generateIsupport } from './isupport.js';
|
|
37
39
|
export { capReducer } from './cap.js';
|
|
38
40
|
export { emitChghost } from './chghost.js';
|
|
@@ -45,3 +47,12 @@ export {
|
|
|
45
47
|
buildChathistoryBatch,
|
|
46
48
|
CHATHISTORY_CAP,
|
|
47
49
|
} from './chathistory.js';
|
|
50
|
+
export { tagmsgChannelReducer, tagmsgUserReducer } from './tagmsg.js';
|
|
51
|
+
export {
|
|
52
|
+
versionReducer,
|
|
53
|
+
timeReducer,
|
|
54
|
+
adminReducer,
|
|
55
|
+
infoReducer,
|
|
56
|
+
} from './server-info.js';
|
|
57
|
+
export { userhostReducer, formatUserhostReply } from './userhost.js';
|
|
58
|
+
export { isonReducer } from './ison.js';
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* case-insensitively against the roster (443 if already a member).
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { caseFold } from '../case-fold.js';
|
|
23
24
|
import { Effect } from '../effects.js';
|
|
24
25
|
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
25
26
|
import { Numerics } from '../protocol/numerics.js';
|
|
@@ -50,15 +51,11 @@ function numericErr(ctx: Ctx, code: number, trailing: string, middle?: string):
|
|
|
50
51
|
return { text: parts.join(' ') };
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
return nick.toLowerCase();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Finds a roster entry's connId by nick (case-insensitive). */
|
|
54
|
+
/** Finds a roster entry's connId by nick (case-insensitively, per `CASEMAPPING=rfc1459`). */
|
|
58
55
|
function findConnIdByNick(state: ChannelState, nick: string): string | null {
|
|
59
|
-
const target =
|
|
56
|
+
const target = caseFold('rfc1459', nick);
|
|
60
57
|
for (const entry of state.members.values()) {
|
|
61
|
-
if (
|
|
58
|
+
if (caseFold('rfc1459', entry.nick) === target) return entry.conn;
|
|
62
59
|
}
|
|
63
60
|
return null;
|
|
64
61
|
}
|
|
@@ -132,9 +129,9 @@ export const inviteReducer: Reducer<ChannelState> = (state, msg, ctx) => {
|
|
|
132
129
|
return { state, effects };
|
|
133
130
|
}
|
|
134
131
|
|
|
135
|
-
// Success: record the pending invite (by
|
|
132
|
+
// Success: record the pending invite (by case-folded nick) so a later
|
|
136
133
|
// JOIN bypasses +i, emit 341, and route the INVITE notice to the invitee.
|
|
137
|
-
state.pendingInvites.add(targetNick
|
|
134
|
+
state.pendingInvites.add(caseFold('rfc1459', targetNick));
|
|
138
135
|
|
|
139
136
|
const inviterNick = ctx.connection.nick ?? '*';
|
|
140
137
|
effects.push(
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer for the IRC `ISON` command.
|
|
3
|
+
*
|
|
4
|
+
* PLAN §2.1 location-of-authority: ISON is read-only — it checks which of the
|
|
5
|
+
* requested nicks are currently online and emits a single `303 RPL_ISON`
|
|
6
|
+
* line. The reducer itself does not perform lookups (those are async and
|
|
7
|
+
* belong to the actor / runtime layer); it receives the set of online nicks
|
|
8
|
+
* and the requested list, then reports the intersection.
|
|
9
|
+
*
|
|
10
|
+
* Wire format (RFC 2812 §5.8):
|
|
11
|
+
* - `ISON <nick>{ <nick>}`
|
|
12
|
+
* - `303 RPL_ISON`:
|
|
13
|
+
* `:<server> 303 <nick> :<online_nick1> <online_nick2> …`
|
|
14
|
+
*
|
|
15
|
+
* Matching is case-insensitive per rfc1459 case-mapping. The reply carries
|
|
16
|
+
* the registered (display) spelling from the online map, not the requester's
|
|
17
|
+
* spelling, so a request for `BOB` reports `Bob` when that is the registered
|
|
18
|
+
* form.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Effect } from '../effects.js';
|
|
22
|
+
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
23
|
+
import { Numerics } from '../protocol/numerics.js';
|
|
24
|
+
import type { ConnectionState } from '../state/connection.js';
|
|
25
|
+
import type { Ctx, ReducerResult } from '../types.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handles `ISON <nick>{ <nick>}`.
|
|
29
|
+
*
|
|
30
|
+
* `online` maps each online nick's case-folded (lowercased) form to its
|
|
31
|
+
* display spelling. Each requested nick is folded and looked up; matches are
|
|
32
|
+
* reported using the display spelling from the map.
|
|
33
|
+
*
|
|
34
|
+
* Signature is bespoke (not {@link Reducer}) because the actor must resolve
|
|
35
|
+
* the nicks asynchronously before formatting — mirrors `whoReducer` /
|
|
36
|
+
* `whoisReducer`.
|
|
37
|
+
*/
|
|
38
|
+
export function isonReducer(
|
|
39
|
+
online: ReadonlyMap<string, string>,
|
|
40
|
+
msg: { params: readonly string[] },
|
|
41
|
+
ctx: Ctx,
|
|
42
|
+
): ReducerResult<ConnectionState> {
|
|
43
|
+
ctx.connection.lastSeen = ctx.clock.now();
|
|
44
|
+
|
|
45
|
+
const found: string[] = [];
|
|
46
|
+
for (const requested of msg.params) {
|
|
47
|
+
if (requested.length === 0) continue;
|
|
48
|
+
const display = online.get(requested.toLowerCase());
|
|
49
|
+
if (display !== undefined) {
|
|
50
|
+
found.push(display);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const nick = ctx.connection.nick ?? '*';
|
|
55
|
+
const codeStr = Numerics.RPL_ISON.toString().padStart(3, '0');
|
|
56
|
+
const trailing = found.join(' ');
|
|
57
|
+
const line: RawLine = { text: `:${ctx.serverName} ${codeStr} ${nick} :${trailing}` };
|
|
58
|
+
|
|
59
|
+
const effects: EffectType[] = [Effect.send(ctx.connId, [line])];
|
|
60
|
+
return { state: ctx.connection, effects };
|
|
61
|
+
}
|
|
@@ -39,7 +39,7 @@ const PREFIX = '(ov)@+';
|
|
|
39
39
|
/** Channel name sigils this server recognises. */
|
|
40
40
|
const CHANTYPES = '#';
|
|
41
41
|
|
|
42
|
-
/** Case-mapping algorithm — v1 uses RFC 1459 (matches `
|
|
42
|
+
/** Case-mapping algorithm — v1 uses RFC 1459 (matches `case-fold.ts`). */
|
|
43
43
|
const CASEMAPPING = 'rfc1459';
|
|
44
44
|
|
|
45
45
|
/** Default max mode changes per single MODE command (PLAN §3). */
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* channel.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { caseFold } from '../case-fold.js';
|
|
18
19
|
import { Effect } from '../effects.js';
|
|
19
20
|
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
20
21
|
import type { StoredMessage } from '../ports.js';
|
|
@@ -213,9 +214,9 @@ export const joinReducer: Reducer<ChannelState> = (state, msg, ctx) => {
|
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
// Invite-only check. pendingInvites stores
|
|
217
|
+
// Invite-only check. pendingInvites stores case-folded nicks.
|
|
217
218
|
const myNick = ctx.connection.nick;
|
|
218
|
-
const invited = myNick !== undefined && state.pendingInvites.has(myNick
|
|
219
|
+
const invited = myNick !== undefined && state.pendingInvites.has(caseFold('rfc1459', myNick));
|
|
219
220
|
if (state.modes.inviteOnly && !invited) {
|
|
220
221
|
effects.push(
|
|
221
222
|
Effect.send(ctx.connId, [
|
|
@@ -258,7 +259,7 @@ export const joinReducer: Reducer<ChannelState> = (state, msg, ctx) => {
|
|
|
258
259
|
});
|
|
259
260
|
|
|
260
261
|
// Pending invite is consumed on a successful join.
|
|
261
|
-
if (myNick !== undefined) state.pendingInvites.delete(myNick
|
|
262
|
+
if (myNick !== undefined) state.pendingInvites.delete(caseFold('rfc1459', myNick));
|
|
262
263
|
|
|
263
264
|
// Track the channel on the connection's joined set (cross-authority).
|
|
264
265
|
ctx.connection.joinedChannels.add(state.nameLower);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* one channel per invocation.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { caseFold } from '../case-fold.js';
|
|
16
17
|
import { Effect } from '../effects.js';
|
|
17
18
|
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
18
19
|
import { Numerics } from '../protocol/numerics.js';
|
|
@@ -52,14 +53,6 @@ function kickerHostmask(conn: ConnectionState): string {
|
|
|
52
53
|
return hostmaskOf(conn) ?? conn.nick ?? '?';
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
/**
|
|
56
|
-
* Lowercases a nick using ASCII case folding (good enough for v1; rfc1459
|
|
57
|
-
* case mapping lands with the isupport `CASEMAPPING` token).
|
|
58
|
-
*/
|
|
59
|
-
function lowerNick(nick: string): string {
|
|
60
|
-
return nick.toLowerCase();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
56
|
/**
|
|
64
57
|
* Handles `KICK <chan> <user> [:reason]`.
|
|
65
58
|
*
|
|
@@ -117,11 +110,11 @@ export const kickReducer: Reducer<ChannelState> = (state, msg, ctx) => {
|
|
|
117
110
|
return { state, effects };
|
|
118
111
|
}
|
|
119
112
|
|
|
120
|
-
// Find the target by nick (case-
|
|
121
|
-
const targetLower =
|
|
113
|
+
// Find the target by nick (case-insensitively, per `CASEMAPPING=rfc1459`).
|
|
114
|
+
const targetLower = caseFold('rfc1459', targetNick);
|
|
122
115
|
let targetConnId: string | undefined;
|
|
123
116
|
for (const entry of state.members.values()) {
|
|
124
|
-
if (
|
|
117
|
+
if (caseFold('rfc1459', entry.nick) === targetLower) {
|
|
125
118
|
targetConnId = entry.conn;
|
|
126
119
|
break;
|
|
127
120
|
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* the cap).
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { caseFold } from '../case-fold.js';
|
|
29
30
|
import { Effect } from '../effects.js';
|
|
30
31
|
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
31
32
|
import { Numerics } from '../protocol/numerics.js';
|
|
@@ -104,10 +105,10 @@ export const listReducer: Reducer<ChanSnapshot[]> = (state, msg, ctx) => {
|
|
|
104
105
|
for (const rawName of requested) {
|
|
105
106
|
if (lines.length >= max) break;
|
|
106
107
|
if (!isValidChannelName(rawName, ctx.serverConfig.channelLen)) continue;
|
|
107
|
-
const
|
|
108
|
-
if (seen.has(
|
|
109
|
-
seen.add(
|
|
110
|
-
const chan = state.find((c) => c.nameLower ===
|
|
108
|
+
const key = caseFold('rfc1459', rawName);
|
|
109
|
+
if (seen.has(key)) continue;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
const chan = state.find((c) => c.nameLower === key);
|
|
111
112
|
if (chan === undefined) continue;
|
|
112
113
|
if (!isVisible(chan, ctx)) continue;
|
|
113
114
|
lines.push(listLine(chan, ctx, nick));
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* for its own user modes, so the reducer's `state` argument is the
|
|
13
13
|
* caller's {@link ConnectionState}. A user may only change their own
|
|
14
14
|
* modes (502 otherwise). The oper flag (`+o`) is granted via the
|
|
15
|
-
*
|
|
15
|
+
* OPER command, not via MODE — `+o` here is rejected with 481.
|
|
16
16
|
*
|
|
17
17
|
* Mode classes (per modern IRC de-facto convention):
|
|
18
18
|
* - Type A (list mode, always takes arg): `b` (ban mask).
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* channel.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { caseFold } from '../case-fold.js';
|
|
31
32
|
import { Effect } from '../effects.js';
|
|
32
33
|
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
33
34
|
import { Numerics } from '../protocol/numerics.js';
|
|
@@ -66,11 +67,6 @@ function numericErr(ctx: Ctx, code: number, trailing: string, middle?: string):
|
|
|
66
67
|
return { text: parts.join(' ') };
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
/** Lowercases a nick using ASCII case folding (matches kick.ts). */
|
|
70
|
-
function lowerNick(nick: string): string {
|
|
71
|
-
return nick.toLowerCase();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
70
|
/** Boolean channel mode letters → mode-field name. */
|
|
75
71
|
const BOOLEAN_LETTERS: Readonly<Record<string, BooleanChannelMode>> = {
|
|
76
72
|
i: 'inviteOnly',
|
|
@@ -417,9 +413,9 @@ function findMemberByNick(
|
|
|
417
413
|
state: ChannelState,
|
|
418
414
|
nick: string,
|
|
419
415
|
): { conn: string; nick: string; op: boolean; voice: boolean } | null {
|
|
420
|
-
const targetLower =
|
|
416
|
+
const targetLower = caseFold('rfc1459', nick);
|
|
421
417
|
for (const entry of state.members.values()) {
|
|
422
|
-
if (
|
|
418
|
+
if (caseFold('rfc1459', entry.nick) === targetLower) {
|
|
423
419
|
return entry;
|
|
424
420
|
}
|
|
425
421
|
}
|
|
@@ -566,7 +562,7 @@ export const userModeReducer: Reducer<ConnectionState> = (state, msg, ctx) => {
|
|
|
566
562
|
|
|
567
563
|
// Target must be the caller's own nick (case-insensitive).
|
|
568
564
|
const myNick = state.nick;
|
|
569
|
-
if (myNick === undefined ||
|
|
565
|
+
if (myNick === undefined || caseFold('rfc1459', target) !== caseFold('rfc1459', myNick)) {
|
|
570
566
|
effects.push(
|
|
571
567
|
Effect.send(ctx.connId, [
|
|
572
568
|
numericErr(ctx, Numerics.ERR_USERSDONTMATCH, 'Cannot change mode for other users'),
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared MOTD-lines → IRC numerics builder.
|
|
3
|
+
*
|
|
4
|
+
* Both the registration welcome block ({@link buildWelcomeLines}) and the
|
|
5
|
+
* standalone `MOTD` command ({@link motdReducer}) render the same
|
|
6
|
+
* `375`/`372`×N/`376` sequence (or `422 ERR_NOMOTD` when no MOTD is
|
|
7
|
+
* configured) from the lines supplied by a {@link MotdProvider}. This module
|
|
8
|
+
* owns that rendering so the two call sites cannot drift.
|
|
9
|
+
*
|
|
10
|
+
* Wire format (RFC 2812 §5):
|
|
11
|
+
* - `375 RPL_MOTDSTART` — `:<server> 375 <nick> :- <server> Message of the day -`
|
|
12
|
+
* - `372 RPL_MOTD` — `:<server> 372 <nick> :- <line>` (one per MOTD line)
|
|
13
|
+
* - `376 RPL_ENDOFMOTD` — `:<server> 376 <nick> :End of MOTD command`
|
|
14
|
+
* - `422 ERR_NOMOTD` — `:<server> 422 <nick> :MOTD File is missing`
|
|
15
|
+
*
|
|
16
|
+
* Per RFC 1459 §2.3 the maximum wire line length is 512 bytes including the
|
|
17
|
+
* trailing CR-LF. Over-long MOTD content lines are hard-wrapped at the
|
|
18
|
+
* 372-line content budget so every emitted line fits within the 512-byte
|
|
19
|
+
* limit (also leaving headroom for message-tags).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { RawLine } from '../effects.js';
|
|
23
|
+
import { Numerics } from '../protocol/numerics.js';
|
|
24
|
+
|
|
25
|
+
/** RFC 1459 §2.3: maximum IRC line length in bytes, including the trailing CR-LF. */
|
|
26
|
+
const MAX_LINE_BYTES = 512;
|
|
27
|
+
|
|
28
|
+
/** Length of the trailing CR-LF that the wire layer appends to each line. */
|
|
29
|
+
const TRAILING_CRLF_LEN = 2;
|
|
30
|
+
|
|
31
|
+
/** Fixed text emitted for `375 RPL_MOTDSTART` (after `:- `). */
|
|
32
|
+
const MOTDSTART_TRAILING_TEXT = '- $server Message of the day -';
|
|
33
|
+
|
|
34
|
+
/** Fixed text emitted for `376 RPL_ENDOFMOTD`. */
|
|
35
|
+
const ENDOFMOTD_TRAILING_TEXT = 'End of MOTD command';
|
|
36
|
+
|
|
37
|
+
/** Fixed text emitted for `422 ERR_NOMOTD`. */
|
|
38
|
+
const NOMOTD_TRAILING_TEXT = 'MOTD File is missing';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Renders the MOTD numerics addressed to `nick` from the supplied `lines`.
|
|
42
|
+
*
|
|
43
|
+
* Returns `[422 ERR_NOMOTD]` when `lines` is empty (mirroring the standalone
|
|
44
|
+
* `MOTD` command), otherwise the `375`/`372`×N/`376` block with over-long
|
|
45
|
+
* lines hard-wrapped to fit the 512-byte wire limit.
|
|
46
|
+
*
|
|
47
|
+
* `nick` is taken verbatim from the caller — the welcome path passes the
|
|
48
|
+
* (guaranteed-defined) registering nick, the standalone command passes
|
|
49
|
+
* `connection.nick ?? '*'`.
|
|
50
|
+
*/
|
|
51
|
+
export function buildMotdNumerics(serverName: string, nick: string, lines: string[]): RawLine[] {
|
|
52
|
+
if (lines.length === 0) {
|
|
53
|
+
return [numericLine(serverName, nick, Numerics.ERR_NOMOTD, NOMOTD_TRAILING_TEXT)];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const out: RawLine[] = [
|
|
57
|
+
numericLine(
|
|
58
|
+
serverName,
|
|
59
|
+
nick,
|
|
60
|
+
Numerics.RPL_MOTDSTART,
|
|
61
|
+
MOTDSTART_TRAILING_TEXT.replace('$server', serverName),
|
|
62
|
+
),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const maxContent = maxMotdContentLen(serverName, nick);
|
|
66
|
+
for (const raw of lines) {
|
|
67
|
+
for (const chunk of splitForMotd(raw, maxContent)) {
|
|
68
|
+
out.push(numericLine(serverName, nick, Numerics.RPL_MOTD, `- ${chunk}`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
out.push(numericLine(serverName, nick, Numerics.RPL_ENDOFMOTD, ENDOFMOTD_TRAILING_TEXT));
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Builds a `:<server> <code> <nick> :<trailing>` line.
|
|
78
|
+
*/
|
|
79
|
+
function numericLine(serverName: string, nick: string, code: number, trailing: string): RawLine {
|
|
80
|
+
const codeStr = code.toString().padStart(3, '0');
|
|
81
|
+
return { text: `:${serverName} ${codeStr} ${nick} :${trailing}` };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns the maximum content-byte length that fits in a single `372 RPL_MOTD`
|
|
86
|
+
* line for this connection, given the server name and nick.
|
|
87
|
+
*
|
|
88
|
+
* The 372 wire format is `:<server> 372 <nick> :- <chunk>\r\n`, so the
|
|
89
|
+
* per-chunk budget is:
|
|
90
|
+
*
|
|
91
|
+
* 512
|
|
92
|
+
* - 2 (trailing CR-LF)
|
|
93
|
+
* - 1 (leading `:`)
|
|
94
|
+
* - <server>.length
|
|
95
|
+
* - 5 (` 372 `)
|
|
96
|
+
* - <nick>.length
|
|
97
|
+
* - 4 (` :- `)
|
|
98
|
+
*
|
|
99
|
+
* The result is clamped at 0 so a pathologically long server/nick combo
|
|
100
|
+
* cannot drive the budget negative.
|
|
101
|
+
*/
|
|
102
|
+
function maxMotdContentLen(serverName: string, nick: string): number {
|
|
103
|
+
const overhead = TRAILING_CRLF_LEN + 1 + serverName.length + 5 + nick.length + 4;
|
|
104
|
+
return Math.max(0, MAX_LINE_BYTES - overhead);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Splits a single MOTD content line into chunks that each fit the per-372
|
|
109
|
+
* content budget.
|
|
110
|
+
*
|
|
111
|
+
* Hard-wraps at the byte budget (no word-boundary awareness) — matching the
|
|
112
|
+
* behavior of mainstream IRC daemons and keeping the rendering deterministic.
|
|
113
|
+
*
|
|
114
|
+
* Edge cases:
|
|
115
|
+
* - Empty string → `['']` (one empty 372 line, still RFC-conformant).
|
|
116
|
+
* - Zero/negative budget (impossible in practice but defensive) → `['']`
|
|
117
|
+
* so the builder always emits at least one 372 per input line.
|
|
118
|
+
*/
|
|
119
|
+
function splitForMotd(text: string, maxLen: number): string[] {
|
|
120
|
+
if (maxLen <= 0) return [''];
|
|
121
|
+
if (text.length === 0) return [''];
|
|
122
|
+
const chunks: string[] = [];
|
|
123
|
+
for (let i = 0; i < text.length; i += maxLen) {
|
|
124
|
+
chunks.push(text.slice(i, i + maxLen));
|
|
125
|
+
}
|
|
126
|
+
return chunks;
|
|
127
|
+
}
|
|
@@ -11,38 +11,16 @@
|
|
|
11
11
|
* D1, or Secrets Manager) so the reducer itself stays pure and
|
|
12
12
|
* deterministic — no IO in the handler.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - `376 RPL_ENDOFMOTD` — `:<server> 376 <nick> :End of MOTD command`
|
|
18
|
-
* - `422 ERR_NOMOTD` — `:<server> 422 <nick> :MOTD File is missing`
|
|
19
|
-
*
|
|
20
|
-
* Per RFC 1459 §2.3 the maximum wire line length is 512 bytes including the
|
|
21
|
-
* trailing CR-LF. Over-long MOTD content lines are hard-wrapped at the
|
|
22
|
-
* 372-line content budget so every emitted line fits within the 512-byte
|
|
23
|
-
* limit (also leaving headroom for message-tags).
|
|
14
|
+
* The numeric rendering (`375`/`372`×N/`376`, or `422 ERR_NOMOTD`) is shared
|
|
15
|
+
* with the registration welcome block via {@link buildMotdNumerics} so the
|
|
16
|
+
* two paths cannot drift.
|
|
24
17
|
*/
|
|
25
18
|
|
|
26
19
|
import { Effect } from '../effects.js';
|
|
27
20
|
import type { Effect as EffectType, RawLine } from '../effects.js';
|
|
28
|
-
import { Numerics } from '../protocol/numerics.js';
|
|
29
21
|
import type { ConnectionState } from '../state/connection.js';
|
|
30
22
|
import type { Ctx, Reducer } from '../types.js';
|
|
31
|
-
|
|
32
|
-
/** RFC 1459 §2.3: maximum IRC line length in bytes, including the trailing CR-LF. */
|
|
33
|
-
const MAX_LINE_BYTES = 512;
|
|
34
|
-
|
|
35
|
-
/** Length of the trailing CR-LF that the wire layer appends to each line. */
|
|
36
|
-
const TRAILING_CRLF_LEN = 2;
|
|
37
|
-
|
|
38
|
-
/** Fixed text emitted for `375 RPL_MOTDSTART` (after `:- `). */
|
|
39
|
-
const MOTDSTART_TRAILING_TEXT = '- $server Message of the day -';
|
|
40
|
-
|
|
41
|
-
/** Fixed text emitted for `376 RPL_ENDOFMOTD`. */
|
|
42
|
-
const ENDOFMOTD_TRAILING_TEXT = 'End of MOTD command';
|
|
43
|
-
|
|
44
|
-
/** Fixed text emitted for `422 ERR_NOMOTD`. */
|
|
45
|
-
const NOMOTD_TRAILING_TEXT = 'MOTD File is missing';
|
|
23
|
+
import { buildMotdNumerics } from './motd-lines.js';
|
|
46
24
|
|
|
47
25
|
/**
|
|
48
26
|
* Handles `MOTD [server]`.
|
|
@@ -56,91 +34,9 @@ const NOMOTD_TRAILING_TEXT = 'MOTD File is missing';
|
|
|
56
34
|
export const motdReducer: Reducer<ConnectionState> = (state, _msg, ctx) => {
|
|
57
35
|
state.lastSeen = ctx.clock.now();
|
|
58
36
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
if (lines.length === 0) {
|
|
62
|
-
return {
|
|
63
|
-
state,
|
|
64
|
-
effects: [
|
|
65
|
-
Effect.send(ctx.connId, [numericLine(ctx, Numerics.ERR_NOMOTD, NOMOTD_TRAILING_TEXT)]),
|
|
66
|
-
],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const out: RawLine[] = [
|
|
71
|
-
numericLine(
|
|
72
|
-
ctx,
|
|
73
|
-
Numerics.RPL_MOTDSTART,
|
|
74
|
-
MOTDSTART_TRAILING_TEXT.replace('$server', ctx.serverName),
|
|
75
|
-
),
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
const maxContent = maxMotdContentLen(ctx);
|
|
79
|
-
for (const raw of lines) {
|
|
80
|
-
for (const chunk of splitForMotd(raw, maxContent)) {
|
|
81
|
-
out.push(numericLine(ctx, Numerics.RPL_MOTD, `- ${chunk}`));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
out.push(numericLine(ctx, Numerics.RPL_ENDOFMOTD, ENDOFMOTD_TRAILING_TEXT));
|
|
37
|
+
const nick = ctx.connection.nick ?? '*';
|
|
38
|
+
const out: RawLine[] = buildMotdNumerics(ctx.serverName, nick, ctx.motd.lines());
|
|
86
39
|
|
|
87
40
|
const effects: EffectType[] = [Effect.send(ctx.connId, out)];
|
|
88
41
|
return { state, effects };
|
|
89
42
|
};
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Builds a `:<server> <code> <nick> :<trailing>` line. The nick falls back
|
|
93
|
-
* to `*` when the connection has not yet registered one, matching the rest
|
|
94
|
-
* of the codebase's numeric-error formatting.
|
|
95
|
-
*/
|
|
96
|
-
function numericLine(ctx: Ctx, code: number, trailing: string): RawLine {
|
|
97
|
-
const nick = ctx.connection.nick ?? '*';
|
|
98
|
-
const codeStr = code.toString().padStart(3, '0');
|
|
99
|
-
return { text: `:${ctx.serverName} ${codeStr} ${nick} :${trailing}` };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Returns the maximum content-byte length that fits in a single `372 RPL_MOTD`
|
|
104
|
-
* line for this connection, given the server name and the connection's nick.
|
|
105
|
-
*
|
|
106
|
-
* The 372 wire format is `:<server> 372 <nick> :- <chunk>\r\n`, so the
|
|
107
|
-
* per-chunk budget is:
|
|
108
|
-
*
|
|
109
|
-
* 512
|
|
110
|
-
* - 2 (trailing CR-LF)
|
|
111
|
-
* - 1 (leading `:`)
|
|
112
|
-
* - <server>.length
|
|
113
|
-
* - 5 (` 372 `)
|
|
114
|
-
* - <nick>.length
|
|
115
|
-
* - 4 (` :- `)
|
|
116
|
-
*
|
|
117
|
-
* The result is clamped at 0 so a pathologically long server/nick combo
|
|
118
|
-
* cannot drive the budget negative.
|
|
119
|
-
*/
|
|
120
|
-
function maxMotdContentLen(ctx: Ctx): number {
|
|
121
|
-
const nick = ctx.connection.nick ?? '*';
|
|
122
|
-
const overhead = TRAILING_CRLF_LEN + 1 + ctx.serverName.length + 5 + nick.length + 4;
|
|
123
|
-
return Math.max(0, MAX_LINE_BYTES - overhead);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Splits a single MOTD content line into chunks that each fit the per-372
|
|
128
|
-
* content budget.
|
|
129
|
-
*
|
|
130
|
-
* Hard-wraps at the byte budget (no word-boundary awareness) — matching the
|
|
131
|
-
* behavior of mainstream IRC daemons and keeping the reducer deterministic.
|
|
132
|
-
*
|
|
133
|
-
* Edge cases:
|
|
134
|
-
* - Empty string → `['']` (one empty 372 line, still RFC-conformant).
|
|
135
|
-
* - Zero/negative budget (impossible in practice but defensive) → `['']`
|
|
136
|
-
* so the reducer always emits at least one 372 per input line.
|
|
137
|
-
*/
|
|
138
|
-
function splitForMotd(text: string, maxLen: number): string[] {
|
|
139
|
-
if (maxLen <= 0) return [''];
|
|
140
|
-
if (text.length === 0) return [''];
|
|
141
|
-
const chunks: string[] = [];
|
|
142
|
-
for (let i = 0; i < text.length; i += maxLen) {
|
|
143
|
-
chunks.push(text.slice(i, i + maxLen));
|
|
144
|
-
}
|
|
145
|
-
return chunks;
|
|
146
|
-
}
|