serverless-ircd 0.10.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-cf-tcp.yml +26 -2
- package/.github/workflows/deploy-cf.yml +26 -0
- package/CHANGELOG.md +289 -0
- package/README.md +153 -20
- package/apps/aws-stack/bin/aws.ts +36 -0
- package/apps/aws-stack/package.json +1 -1
- package/apps/aws-stack/src/aws-stack.ts +221 -15
- package/apps/aws-stack/tests/stack.test.ts +450 -16
- package/apps/cf-tcp-container/Dockerfile +37 -5
- package/apps/cf-tcp-container/package.json +7 -2
- package/apps/cf-tcp-container/src/config-loader.ts +113 -2
- package/apps/cf-tcp-container/src/container-server.ts +256 -79
- 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 +17 -4
- package/apps/cf-worker/package.json +2 -2
- 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 +86 -9
- 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 +23 -2
- package/apps/local-cli/tests/line-scanner.test.ts +64 -0
- package/apps/local-cli/tests/tcp.test.ts +29 -0
- package/apps/web/package.json +1 -1
- package/docs/AWS-Deployment.md +123 -22
- package/docs/AWS-TCP-Deployment.md +37 -2
- package/docs/Chat-History.md +55 -0
- package/docs/Cloudflare-Deployment-Guide.md +9 -2
- package/docs/Cloudflare-TCP-Deployment.md +135 -52
- package/docs/SASL-EXTERNAL.md +175 -0
- package/package.json +3 -3
- 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 +34 -6
- package/packages/aws-adapter/src/config-loader.ts +134 -6
- package/packages/aws-adapter/src/dynamo-services-store.ts +12 -0
- package/packages/aws-adapter/src/handlers/connect.ts +47 -1
- package/packages/aws-adapter/src/handlers/default.ts +95 -6
- package/packages/aws-adapter/src/handlers/index.ts +31 -2
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +132 -8
- package/packages/aws-adapter/src/ip-admission.ts +79 -0
- package/packages/aws-adapter/src/serialize.ts +8 -0
- package/packages/aws-adapter/src/tables.ts +9 -0
- package/packages/aws-adapter/tests/admission.test.ts +60 -2
- package/packages/aws-adapter/tests/aws-harness.ts +23 -1
- package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +151 -0
- package/packages/aws-adapter/tests/connect.test.ts +199 -2
- package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +10 -3
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +123 -1
- package/packages/aws-adapter/tests/handlers.test.ts +57 -1
- package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +48 -9
- package/packages/cf-adapter/src/config-loader.ts +133 -8
- package/packages/cf-adapter/src/connection-do.ts +154 -21
- package/packages/cf-adapter/src/counter-do.ts +142 -0
- package/packages/cf-adapter/src/d1-services-store.ts +47 -5
- package/packages/cf-adapter/src/env.ts +88 -0
- package/packages/cf-adapter/src/index.ts +17 -1
- package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +104 -15
- 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-frame-limit.test.ts +177 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +74 -5
- 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 +192 -1
- package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -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/caps/capabilities.ts +20 -10
- package/packages/irc-core/src/certfp.ts +178 -0
- 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/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 +40 -1
- 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 +146 -2
- package/packages/irc-core/src/commands/sasl.ts +136 -19
- 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 +393 -20
- 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 +8 -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 +395 -36
- 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 +228 -13
- package/packages/irc-core/src/ws-framing.ts +5 -4
- 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/names.test.ts +193 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +182 -2
- package/packages/irc-core/tests/commands/oper.test.ts +560 -2
- package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
- package/packages/irc-core/tests/commands/registration.test.ts +463 -1
- package/packages/irc-core/tests/commands/sasl.test.ts +596 -7
- package/packages/irc-core/tests/commands/topic.test.ts +137 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +2 -0
- package/packages/irc-core/tests/config.test.ts +534 -2
- 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/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 +141 -0
- package/packages/irc-core/tests/ports.test.ts +99 -7
- package/packages/irc-core/tests/services-store.test.ts +376 -14
- package/packages/irc-core/tests/ws-framing.test.ts +45 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +123 -8
- 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 +544 -7
- 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/scripts/package.json +1 -1
- 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 +17 -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-hostname.test.ts +348 -0
- package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -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/tcp-ws-forwarder/package.json +1 -1
|
@@ -21,6 +21,9 @@ function fakeRuntime(calls: RecordedCall[]): IrcRuntime {
|
|
|
21
21
|
async broadcastWallops(lines, except) {
|
|
22
22
|
calls.push({ method: 'broadcastWallops', args: [lines, except] });
|
|
23
23
|
},
|
|
24
|
+
async broadcastOperNotice(lines, except) {
|
|
25
|
+
calls.push({ method: 'broadcastOperNotice', args: [lines, except] });
|
|
26
|
+
},
|
|
24
27
|
async disconnect(conn, reason) {
|
|
25
28
|
calls.push({ method: 'disconnect', args: [conn, reason] });
|
|
26
29
|
},
|
|
@@ -151,6 +154,30 @@ describe('dispatch', () => {
|
|
|
151
154
|
});
|
|
152
155
|
});
|
|
153
156
|
|
|
157
|
+
it('routes BroadcastOperNotice with lines and except', async () => {
|
|
158
|
+
const calls: RecordedCall[] = [];
|
|
159
|
+
await dispatch(
|
|
160
|
+
[Effect.broadcastOperNotice([L(':s NOTICE * :OPER lockout triggered for h')], 'c1')],
|
|
161
|
+
fakeRuntime(calls),
|
|
162
|
+
);
|
|
163
|
+
expect(calls[0]).toEqual({
|
|
164
|
+
method: 'broadcastOperNotice',
|
|
165
|
+
args: [[{ text: ':s NOTICE * :OPER lockout triggered for h' }], 'c1'],
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('routes BroadcastOperNotice preserving undefined except', async () => {
|
|
170
|
+
const calls: RecordedCall[] = [];
|
|
171
|
+
await dispatch(
|
|
172
|
+
[Effect.broadcastOperNotice([L(':s NOTICE * :OPER lockout triggered for h')])],
|
|
173
|
+
fakeRuntime(calls),
|
|
174
|
+
);
|
|
175
|
+
expect(calls[0]).toEqual({
|
|
176
|
+
method: 'broadcastOperNotice',
|
|
177
|
+
args: [[{ text: ':s NOTICE * :OPER lockout triggered for h' }], undefined],
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
154
181
|
it('calls reserveNick, changeNick, releaseNick in order', async () => {
|
|
155
182
|
const calls: RecordedCall[] = [];
|
|
156
183
|
await dispatch(
|
|
@@ -279,6 +306,7 @@ function capAwareRuntime(members: ReadonlyMap<ConnId, MemberInfo>): {
|
|
|
279
306
|
broadcasts.push({ chan, lines, except });
|
|
280
307
|
},
|
|
281
308
|
async broadcastWallops() {},
|
|
309
|
+
async broadcastOperNotice() {},
|
|
282
310
|
async disconnect() {},
|
|
283
311
|
async sendToNick() {},
|
|
284
312
|
async reserveNick() {
|
|
@@ -429,6 +457,7 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
429
457
|
},
|
|
430
458
|
async broadcast() {},
|
|
431
459
|
async broadcastWallops() {},
|
|
460
|
+
async broadcastOperNotice() {},
|
|
432
461
|
async disconnect() {},
|
|
433
462
|
async sendToNick() {},
|
|
434
463
|
async reserveNick() {
|
|
@@ -476,6 +505,7 @@ describe('dispatch — Broadcast cap / capLines', () => {
|
|
|
476
505
|
},
|
|
477
506
|
async broadcast() {},
|
|
478
507
|
async broadcastWallops() {},
|
|
508
|
+
async broadcastOperNotice() {},
|
|
479
509
|
async disconnect() {},
|
|
480
510
|
async sendToNick() {},
|
|
481
511
|
async reserveNick() {
|
|
@@ -662,6 +692,7 @@ describe('dispatch — Broadcast caps (OR semantics)', () => {
|
|
|
662
692
|
},
|
|
663
693
|
async broadcast() {},
|
|
664
694
|
async broadcastWallops() {},
|
|
695
|
+
async broadcastOperNotice() {},
|
|
665
696
|
async disconnect() {},
|
|
666
697
|
async sendToNick() {},
|
|
667
698
|
async reserveNick() {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log-scrubbing helpers: `redactLine`, `truncateToken`, `parseErrorReason`.
|
|
3
|
+
*
|
|
4
|
+
* These are the credential-exfiltration guards for log payloads. A hostile
|
|
5
|
+
* client can smuggle secret material into server logs by sending a
|
|
6
|
+
* malformed or sensitive line (e.g. `PASS hunter2` inside a frame that
|
|
7
|
+
* trips the parser). Anything that could end up in a warn/error record
|
|
8
|
+
* must pass through these helpers first: raw lines go through
|
|
9
|
+
* `redactLine`, parse diagnostics go through `truncateToken` +
|
|
10
|
+
* `parseErrorReason`.
|
|
11
|
+
*
|
|
12
|
+
* Coverage here is exhaustive by design (100% on this module is a
|
|
13
|
+
* release requirement): every sensitive command prefix and every branch
|
|
14
|
+
* of the scrubbers is pinned by a named case.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { MAX_TAGS, parse, parseTags } from '@serverless-ircd/irc-core';
|
|
18
|
+
import { describe, expect, it } from 'vitest';
|
|
19
|
+
import { parseErrorReason, redactLine, truncateToken } from '../src/redact';
|
|
20
|
+
|
|
21
|
+
describe('redactLine — sensitive command prefixes', () => {
|
|
22
|
+
it('redacts the parameter of PASS while keeping the verb', () => {
|
|
23
|
+
expect(redactLine('PASS hunter2')).toBe('PASS [REDACTED]');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('matches the PASS verb case-insensitively but preserves its spelling', () => {
|
|
27
|
+
expect(redactLine('pass hunter2')).toBe('pass [REDACTED]');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('redacts the parameter of AUTHENTICATE (SASL payloads carry credentials)', () => {
|
|
31
|
+
expect(redactLine('AUTHENTICATE dXNlcjpodW50ZXIy')).toBe('AUTHENTICATE [REDACTED]');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('redacts the parameters of OPER (name + password)', () => {
|
|
35
|
+
expect(redactLine('OPER admin secretpw')).toBe('OPER [REDACTED]');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('redacts the parameter of a bare IDENTIFY (service shortcut form)', () => {
|
|
39
|
+
expect(redactLine('IDENTIFY hunter2')).toBe('IDENTIFY [REDACTED]');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('redacts a NickServ-targeted PRIVMSG carrying IDENTIFY, keeping target + subcommand', () => {
|
|
43
|
+
expect(redactLine('PRIVMSG NickServ :IDENTIFY hunter2')).toBe(
|
|
44
|
+
'PRIVMSG NickServ :IDENTIFY [REDACTED]',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('redacts a case-folded NickServ-targeted NOTICE carrying the ID alias', () => {
|
|
49
|
+
expect(redactLine('NOTICE nickserv :id hunter2')).toBe('NOTICE nickserv :id [REDACTED]');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('redacts a NickServ-targeted PRIVMSG whose body lacks the trailing colon', () => {
|
|
53
|
+
expect(redactLine('PRIVMSG NickServ IDENTIFY hunter2')).toBe(
|
|
54
|
+
'PRIVMSG NickServ IDENTIFY [REDACTED]',
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('redactLine — non-sensitive input passes through', () => {
|
|
60
|
+
it('leaves ordinary commands untouched', () => {
|
|
61
|
+
expect(redactLine('PRIVMSG #chan :hello there')).toBe('PRIVMSG #chan :hello there');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('leaves a NickServ message that carries no credential subcommand untouched', () => {
|
|
65
|
+
expect(redactLine('PRIVMSG NickServ :INFO')).toBe('PRIVMSG NickServ :INFO');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('leaves an IDENTIFY with no arguments untouched (nothing to redact)', () => {
|
|
69
|
+
expect(redactLine('PRIVMSG NickServ :IDENTIFY')).toBe('PRIVMSG NickServ :IDENTIFY');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('leaves an IDENTIFY sent to a non-service nick untouched', () => {
|
|
73
|
+
expect(redactLine('PRIVMSG Mallory :IDENTIFY hunter2')).toBe(
|
|
74
|
+
'PRIVMSG Mallory :IDENTIFY hunter2',
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('leaves a bare sensitive verb with no parameters untouched', () => {
|
|
79
|
+
expect(redactLine('PASS')).toBe('PASS');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('leaves a bare PRIVMSG with no target or body untouched', () => {
|
|
83
|
+
expect(redactLine('PRIVMSG')).toBe('PRIVMSG');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('leaves an empty line untouched', () => {
|
|
87
|
+
expect(redactLine('')).toBe('');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('redactLine — source prefixes', () => {
|
|
92
|
+
it('redacts the parameters of a prefixed sensitive command, keeping the prefix', () => {
|
|
93
|
+
expect(redactLine(':srv.example.com PASS secret')).toBe(':srv.example.com PASS [REDACTED]');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('redacts a prefixed NickServ-targeted PRIVMSG', () => {
|
|
97
|
+
expect(redactLine(':alice!u@h PRIVMSG NickServ :IDENTIFY hunter2')).toBe(
|
|
98
|
+
':alice!u@h PRIVMSG NickServ :IDENTIFY [REDACTED]',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('leaves a prefix-only line untouched', () => {
|
|
103
|
+
expect(redactLine(':prefix-only')).toBe(':prefix-only');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('truncateToken', () => {
|
|
108
|
+
it('returns a token at the cap unchanged', () => {
|
|
109
|
+
expect(truncateToken('a'.repeat(32))).toBe('a'.repeat(32));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('truncates a token beyond the cap with an ellipsis marker', () => {
|
|
113
|
+
expect(truncateToken('a'.repeat(33))).toBe(`${'a'.repeat(32)}...`);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('parseErrorReason — stable codes for every parser diagnostic', () => {
|
|
118
|
+
it('maps the empty-message diagnostic', () => {
|
|
119
|
+
expect(() => parse('')).toThrow();
|
|
120
|
+
try {
|
|
121
|
+
parse('');
|
|
122
|
+
} catch (err) {
|
|
123
|
+
expect(parseErrorReason(err)).toBe('empty-message');
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('maps the input-cap diagnostic', () => {
|
|
128
|
+
try {
|
|
129
|
+
parse('A'.repeat(600));
|
|
130
|
+
expect.unreachable('600 bytes must fail the input cap');
|
|
131
|
+
} catch (err) {
|
|
132
|
+
expect(parseErrorReason(err)).toBe('input-cap-exceeded');
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('maps the tags-without-body diagnostic', () => {
|
|
137
|
+
try {
|
|
138
|
+
parse('@account=alice');
|
|
139
|
+
expect.unreachable('a tag section with no message body must fail');
|
|
140
|
+
} catch (err) {
|
|
141
|
+
expect(parseErrorReason(err)).toBe('tags-without-body');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('maps the prefix-without-command diagnostic', () => {
|
|
146
|
+
try {
|
|
147
|
+
parse(':src.example.com');
|
|
148
|
+
expect.unreachable('a source prefix with no command must fail');
|
|
149
|
+
} catch (err) {
|
|
150
|
+
expect(parseErrorReason(err)).toBe('prefix-without-command');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('maps the missing-command diagnostic', () => {
|
|
155
|
+
try {
|
|
156
|
+
parse('@a :src ');
|
|
157
|
+
expect.unreachable('tags + prefix with no command must fail');
|
|
158
|
+
} catch (err) {
|
|
159
|
+
expect(parseErrorReason(err)).toBe('missing-command');
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('maps the invalid-command-token diagnostic', () => {
|
|
164
|
+
try {
|
|
165
|
+
parse('12345');
|
|
166
|
+
expect.unreachable('a 5-digit command token must fail');
|
|
167
|
+
} catch (err) {
|
|
168
|
+
expect(parseErrorReason(err)).toBe('invalid-command-token');
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('maps the too-many-tags diagnostic', () => {
|
|
173
|
+
try {
|
|
174
|
+
const tags = Array.from({ length: MAX_TAGS + 1 }, (_, i) => `t${i}`);
|
|
175
|
+
parse(`@${tags.join(';')} PRIVMSG #foo :hi`);
|
|
176
|
+
expect.unreachable('a 16-tag line must fail the tag count cap');
|
|
177
|
+
} catch (err) {
|
|
178
|
+
expect(parseErrorReason(err)).toBe('too-many-tags');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('maps the tag-section-byte-cap diagnostic', () => {
|
|
183
|
+
try {
|
|
184
|
+
parseTags(`a=${'x'.repeat(8193 - 2)}`, `@a=${'x'.repeat(8193 - 2)} PRIVMSG #foo :hi`);
|
|
185
|
+
expect.unreachable('an over-budget tag section must fail');
|
|
186
|
+
} catch (err) {
|
|
187
|
+
expect(parseErrorReason(err)).toBe('tag-section-too-large');
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('falls back to a stable code for an unrecognized Error message', () => {
|
|
192
|
+
expect(parseErrorReason(new Error('boom'))).toBe('parse-failed');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('falls back to a stable code for a non-Error throw', () => {
|
|
196
|
+
expect(parseErrorReason('boom')).toBe('parse-failed');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -38,6 +38,7 @@ describe('IrcRuntime contract', () => {
|
|
|
38
38
|
throw new Error('reloadConfig not configured for this fake');
|
|
39
39
|
},
|
|
40
40
|
async broadcastWallops() {},
|
|
41
|
+
async broadcastOperNotice() {},
|
|
41
42
|
};
|
|
42
43
|
expect(runtime).toBeDefined();
|
|
43
44
|
expect(typeof runtime.send).toBe('function');
|
|
@@ -55,5 +56,6 @@ describe('IrcRuntime contract', () => {
|
|
|
55
56
|
expect(typeof runtime.listChannels).toBe('function');
|
|
56
57
|
expect(typeof runtime.reloadConfig).toBe('function');
|
|
57
58
|
expect(typeof runtime.broadcastWallops).toBe('function');
|
|
59
|
+
expect(typeof runtime.broadcastOperNotice).toBe('function');
|
|
58
60
|
});
|
|
59
61
|
});
|
|
@@ -199,6 +199,72 @@ describe('TcpByteStreamTransport — buffer save/restore', () => {
|
|
|
199
199
|
});
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Input buffer cap: a peer streaming megabytes with no line terminator must
|
|
204
|
+
// not grow the partial-line buffer unboundedly (edge-level memory DoS). The
|
|
205
|
+
// cap is measured in UTF-8 bytes on the retained (unterminated) tail after
|
|
206
|
+
// each append — complete lines flushed out by the same chunk never count.
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
describe('TcpByteStreamTransport — input buffer cap', () => {
|
|
210
|
+
it('signals onOverflow and drops the buffer when an appended tail exceeds the 8 KiB cap', () => {
|
|
211
|
+
const overflows: number[] = [];
|
|
212
|
+
const t = new TcpByteStreamTransport({ onOverflow: () => overflows.push(1) });
|
|
213
|
+
expect(t.feed('x'.repeat(9 * 1024))).toEqual([]);
|
|
214
|
+
expect(overflows.length).toBe(1);
|
|
215
|
+
// The oversized tail is dropped, not retained.
|
|
216
|
+
expect(t.getBuffer()).toBe('');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('checks the cap once per append (chunk), not per byte', () => {
|
|
220
|
+
// A single 9 KiB chunk that overflows fires the callback exactly once —
|
|
221
|
+
// the guard runs on the appended buffer, not per input byte.
|
|
222
|
+
const overflows: number[] = [];
|
|
223
|
+
const t = new TcpByteStreamTransport({ onOverflow: () => overflows.push(1) });
|
|
224
|
+
t.feed('y'.repeat(9 * 1024));
|
|
225
|
+
expect(overflows.length).toBe(1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('signals overflow only on the append that crosses the cap (accumulated chunks)', () => {
|
|
229
|
+
const overflows: number[] = [];
|
|
230
|
+
const t = new TcpByteStreamTransport({ onOverflow: () => overflows.push(1) });
|
|
231
|
+
expect(t.feed('a'.repeat(4 * 1024))).toEqual([]);
|
|
232
|
+
expect(overflows.length).toBe(0);
|
|
233
|
+
expect(t.feed('b'.repeat(5 * 1024))).toEqual([]);
|
|
234
|
+
expect(overflows.length).toBe(1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('does NOT overflow a buffered partial line at (just under) the cap', () => {
|
|
238
|
+
const overflows: number[] = [];
|
|
239
|
+
const t = new TcpByteStreamTransport({ onOverflow: () => overflows.push(1) });
|
|
240
|
+
// Exactly 8192 bytes: at the cap, still allowed.
|
|
241
|
+
t.feed('z'.repeat(8192));
|
|
242
|
+
expect(overflows.length).toBe(0);
|
|
243
|
+
expect(t.getBuffer()).toBe('z'.repeat(8192));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('does not overflow a chunk of complete lines totalling over the cap (batching)', () => {
|
|
247
|
+
const overflows: number[] = [];
|
|
248
|
+
const t = new TcpByteStreamTransport({ onOverflow: () => overflows.push(1) });
|
|
249
|
+
// 20 terminated ~500-byte lines ≈ 10 KiB in one chunk: every line is
|
|
250
|
+
// complete, so the retained tail is empty and nothing overflows.
|
|
251
|
+
const chunk = `${Array.from({ length: 20 }, (_, i) => `PING :${`${i}`.padStart(495, '-')}`).join('\r\n')}\r\n`;
|
|
252
|
+
expect(chunk.length).toBeGreaterThan(8192);
|
|
253
|
+
const out = t.feed(chunk);
|
|
254
|
+
expect(overflows.length).toBe(0);
|
|
255
|
+
expect(out.length).toBe(20);
|
|
256
|
+
expect(t.getBuffer()).toBe('');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('measures the tail in UTF-8 bytes, not UTF-16 code units', () => {
|
|
260
|
+
// 'é' is one UTF-16 unit but two UTF-8 bytes: 5000 × é = 10000 bytes.
|
|
261
|
+
const overflows: number[] = [];
|
|
262
|
+
const t = new TcpByteStreamTransport({ onOverflow: () => overflows.push(1) });
|
|
263
|
+
t.feed('é'.repeat(5000));
|
|
264
|
+
expect(overflows.length).toBe(1);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
202
268
|
// ---------------------------------------------------------------------------
|
|
203
269
|
// Equivalence: a fully-terminated message stream produces the same complete
|
|
204
270
|
// lines regardless of how it is chunked across feed() calls.
|
|
@@ -192,6 +192,10 @@ export class InMemoryHarness implements IrcHarness {
|
|
|
192
192
|
clock: this.clock,
|
|
193
193
|
ids: this.ids,
|
|
194
194
|
motd: this.motd,
|
|
195
|
+
// Shared per-IP failed-OPER counter (runtime-owned) so harness
|
|
196
|
+
// scenarios exercise the OPER brute-force lockout exactly like the
|
|
197
|
+
// local CLI does.
|
|
198
|
+
operFailures: this.runtime.operFailures,
|
|
195
199
|
transport: this.newTransport(),
|
|
196
200
|
});
|
|
197
201
|
client.bindActor(actor);
|
package/scripts/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serverless-ircd/ci-hardening",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "CI hardening helpers: coverage-gate config validator + mutation-testing driver for irc-core",
|
|
5
|
+
"description": "CI hardening helpers: coverage-gate config validator + mutation-testing driver for irc-core + deploy hostname placeholder guard",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./dist/index.js",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { checkDeployHostname, readServerNameFromWranglerToml } from './deploy-hostname.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Guarded `wrangler deploy` wrapper for the Cloudflare apps.
|
|
7
|
+
*
|
|
8
|
+
* Runs the deploy-time hostname guard and injects the resolved
|
|
9
|
+
* SERVER_NAME into the wrangler invocation itself, so an operator
|
|
10
|
+
* setting ONE override (`SERVER_NAME=irc.example.org pnpm deploy:cf`)
|
|
11
|
+
* satisfies both the guard and the deployed var. Without the wrapper
|
|
12
|
+
* the guard reads the env var but wrangler ignores it (vars come from
|
|
13
|
+
* wrangler.toml / --var flags), forcing the value to be specified
|
|
14
|
+
* twice.
|
|
15
|
+
*
|
|
16
|
+
* Usage (from an app's npm script, cwd = the app dir):
|
|
17
|
+
*
|
|
18
|
+
* node ../../tools/ci-hardening/dist/cf-deploy-cli.js wrangler.toml -- <wrangler args...>
|
|
19
|
+
*
|
|
20
|
+
* Resolution order: a non-blank `SERVER_NAME` env override wins; else
|
|
21
|
+
* the `[vars]` SERVER_NAME in the config. The resolved value is always
|
|
22
|
+
* passed to wrangler as `--var SERVER_NAME:<hostname>` (identical to
|
|
23
|
+
* the config value in the fallback case, an override otherwise) and
|
|
24
|
+
* validated by the same placeholder guard as the deploy workflows.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface WrapperArgv {
|
|
28
|
+
readonly configPath: string;
|
|
29
|
+
readonly passthrough: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parses the wrapper argv: first positional is the wrangler.toml path;
|
|
34
|
+
* everything after it (with leading `--` separators stripped, which
|
|
35
|
+
* pnpm appends for `pnpm deploy:cf -- <args>`) is passed through to
|
|
36
|
+
* `wrangler deploy`.
|
|
37
|
+
*/
|
|
38
|
+
export function parseWrapperArgv(argv: readonly string[]): WrapperArgv {
|
|
39
|
+
const configPath = argv[0] ?? '';
|
|
40
|
+
const rest = argv.slice(1);
|
|
41
|
+
// Strip every leading `--` separator — pnpm forwarding can produce
|
|
42
|
+
// more than one (`pnpm deploy:cf -- --flag` on a script that itself
|
|
43
|
+
// ends in `--`); wrangler would treat them as positionals.
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < rest.length && rest[i] === '--') i++;
|
|
46
|
+
return { configPath, passthrough: rest.slice(i) };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type GuardedHostname = { ok: true; hostname: string } | { ok: false; message: string };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolves the deploy-time hostname (env override → config `[vars]`
|
|
53
|
+
* SERVER_NAME) and runs it through the placeholder guard.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveGuardedHostname(
|
|
56
|
+
configPath: string,
|
|
57
|
+
env: Record<string, string | undefined>,
|
|
58
|
+
): GuardedHostname {
|
|
59
|
+
const override = env.SERVER_NAME;
|
|
60
|
+
const hostname =
|
|
61
|
+
typeof override === 'string' && override.trim() !== ''
|
|
62
|
+
? override
|
|
63
|
+
: readServerNameFromWranglerToml(readFileSync(configPath, 'utf8'));
|
|
64
|
+
if (hostname === undefined) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
message: `::error::SERVER_NAME is not set: no SERVER_NAME var found in ${configPath} and no SERVER_NAME environment override is set`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const check = checkDeployHostname(hostname);
|
|
71
|
+
return check.ok
|
|
72
|
+
? { ok: true, hostname: check.hostname }
|
|
73
|
+
: { ok: false, message: `::error::${check.error}` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Builds the wrangler argument vector: `deploy` with the resolved
|
|
78
|
+
* hostname injected as a `--var` override, followed by any passthrough
|
|
79
|
+
* flags (`--dry-run`, `--keep-vars`, …). Uses the documented
|
|
80
|
+
* `KEY:value` pair syntax.
|
|
81
|
+
*/
|
|
82
|
+
export function buildWranglerDeployArgs(
|
|
83
|
+
hostname: string,
|
|
84
|
+
passthrough: readonly string[],
|
|
85
|
+
): string[] {
|
|
86
|
+
return ['deploy', '--var', `SERVER_NAME:${hostname}`, ...passthrough];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const USAGE =
|
|
90
|
+
'usage: cf-deploy <wrangler.toml> [-- <wrangler deploy args...>] (SERVER_NAME env var optionally overrides the config value)';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolves + guards, then execs `wrangler` (resolved from PATH — the
|
|
94
|
+
* npm script context puts node_modules/.bin there) with the resolved
|
|
95
|
+
* hostname injected. Returns the process exit code to propagate.
|
|
96
|
+
*/
|
|
97
|
+
export function runDeploy(
|
|
98
|
+
argv: readonly string[],
|
|
99
|
+
env: Record<string, string | undefined>,
|
|
100
|
+
): number {
|
|
101
|
+
const { configPath, passthrough } = parseWrapperArgv(argv);
|
|
102
|
+
if (configPath === '') {
|
|
103
|
+
console.error(USAGE);
|
|
104
|
+
return 2;
|
|
105
|
+
}
|
|
106
|
+
const resolved = resolveGuardedHostname(configPath, env);
|
|
107
|
+
if (!resolved.ok) {
|
|
108
|
+
console.error(resolved.message);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
const args = buildWranglerDeployArgs(resolved.hostname, passthrough);
|
|
112
|
+
const result = spawnSync('wrangler', args, { stdio: 'inherit' });
|
|
113
|
+
if (result.error !== undefined) {
|
|
114
|
+
console.error(`::error::failed to run wrangler: ${result.error.message}`);
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
return result.status ?? 1;
|
|
118
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hostnames the repo ships as defaults in its deploy configs
|
|
5
|
+
* (`SERVER_NAME = "irc.your-domain.invalid"` in both wrangler.toml
|
|
6
|
+
* files, `irc.example.com` in the Spectrum apply docs/defaults).
|
|
7
|
+
*
|
|
8
|
+
* A deploy whose public hostname is still one of these values is a
|
|
9
|
+
* misconfiguration, not a deploy: the wss Worker would advertise the
|
|
10
|
+
* placeholder in its numerics and the Spectrum cert/handshake would
|
|
11
|
+
* target a domain the operator does not own. Deploy paths refuse to
|
|
12
|
+
* ship while the deploy-time hostname is on this list.
|
|
13
|
+
*/
|
|
14
|
+
export const PLACEHOLDER_HOSTNAMES: ReadonlySet<string> = new Set([
|
|
15
|
+
'irc.example.com',
|
|
16
|
+
'irc.your-domain.invalid',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether a hostname is one of the repo's shipping placeholders.
|
|
21
|
+
* Hostnames are case-insensitive and tolerant of surrounding
|
|
22
|
+
* whitespace (workflow inputs / env vars).
|
|
23
|
+
*/
|
|
24
|
+
export function isPlaceholderHostname(hostname: string): boolean {
|
|
25
|
+
return PLACEHOLDER_HOSTNAMES.has(hostname.trim().toLowerCase());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type HostnameCheck = { ok: true; hostname: string } | { ok: false; error: string };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate a deploy-time hostname. Returns the trimmed, lowercased
|
|
32
|
+
* hostname on success; on failure returns an `error` explaining how
|
|
33
|
+
* to set a real one (workflow `hostname` input, or `SERVER_NAME` in
|
|
34
|
+
* the app's wrangler.toml `[vars]` block).
|
|
35
|
+
*/
|
|
36
|
+
export function checkDeployHostname(raw: string | undefined | null): HostnameCheck {
|
|
37
|
+
const hostname = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
|
|
38
|
+
if (hostname === '') {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error:
|
|
42
|
+
'deploy hostname is required: pass the public IRC hostname via the workflow `hostname` input (deploy-cf / deploy-cf-tcp) or set SERVER_NAME in the app wrangler.toml [vars] block',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (isPlaceholderHostname(hostname)) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
error: `${hostname} is a placeholder hostname shipped with the repo defaults, not a real deploy target: set the public IRC hostname (workflow \`hostname\` input or SERVER_NAME in wrangler.toml [vars]) and redeploy`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, hostname };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const SERVER_NAME_RE = /^\s*SERVER_NAME\s*=\s*["']([^"'\r\n]+)["']\s*$/m;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract the `SERVER_NAME` var from wrangler.toml text. Anchored at
|
|
58
|
+
* line starts so commented-out (`# SERVER_NAME = ...`) assignments
|
|
59
|
+
* do not match; accepts both TOML quote styles.
|
|
60
|
+
*/
|
|
61
|
+
export function readServerNameFromWranglerToml(tomlText: string): string | undefined {
|
|
62
|
+
return tomlText.match(SERVER_NAME_RE)?.[1];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface HostnameGuardResult {
|
|
66
|
+
/** 0 = deployable, 1 = rejected (unset or placeholder), 2 = usage error. */
|
|
67
|
+
exitCode: number;
|
|
68
|
+
message: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const USAGE = 'usage: hostname-guard <hostname> | hostname-guard --from-config <wrangler.toml>';
|
|
72
|
+
|
|
73
|
+
function report(check: HostnameCheck): HostnameGuardResult {
|
|
74
|
+
return check.ok
|
|
75
|
+
? { exitCode: 0, message: `deploy hostname OK: ${check.hostname}` }
|
|
76
|
+
: { exitCode: 1, message: `::error::${check.error}` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The deploy-time hostname guard. Two modes:
|
|
81
|
+
*
|
|
82
|
+
* - `hostname-guard <hostname>` — validate an explicit value (the
|
|
83
|
+
* deploy workflows pass their `hostname` input through this).
|
|
84
|
+
* - `hostname-guard --from-config <wrangler.toml>` — validate the
|
|
85
|
+
* deploy-time SERVER_NAME: the `SERVER_NAME` env override if set
|
|
86
|
+
* and non-blank, else the `[vars]` SERVER_NAME in the config (the
|
|
87
|
+
* guarded `wrangler deploy` npm scripts use this).
|
|
88
|
+
*
|
|
89
|
+
* Pure with respect to the process: `main` wires `process.argv` /
|
|
90
|
+
* `process.env` / stdio / `process.exit`.
|
|
91
|
+
*/
|
|
92
|
+
export function runHostnameGuard(
|
|
93
|
+
argv: readonly string[],
|
|
94
|
+
env: Record<string, string | undefined>,
|
|
95
|
+
): HostnameGuardResult {
|
|
96
|
+
if (argv[0] === '--from-config') {
|
|
97
|
+
const configPath = argv[1];
|
|
98
|
+
if (configPath === undefined || configPath === '') {
|
|
99
|
+
return { exitCode: 2, message: USAGE };
|
|
100
|
+
}
|
|
101
|
+
const override = env.SERVER_NAME;
|
|
102
|
+
const hostname =
|
|
103
|
+
typeof override === 'string' && override.trim() !== ''
|
|
104
|
+
? override
|
|
105
|
+
: readServerNameFromWranglerToml(readFileSync(configPath, 'utf8'));
|
|
106
|
+
if (hostname === undefined) {
|
|
107
|
+
return {
|
|
108
|
+
exitCode: 1,
|
|
109
|
+
message: `::error::SERVER_NAME is not set: no SERVER_NAME var found in ${configPath} and no SERVER_NAME environment override is set`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return report(checkDeployHostname(hostname));
|
|
113
|
+
}
|
|
114
|
+
if (argv.length === 1) {
|
|
115
|
+
return report(checkDeployHostname(argv[0]));
|
|
116
|
+
}
|
|
117
|
+
return { exitCode: 2, message: USAGE };
|
|
118
|
+
}
|