serverless-ircd 0.9.0 → 0.10.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/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +11 -9
- package/.github/workflows/deploy-cf.yml +14 -14
- package/CHANGELOG.md +305 -0
- package/README.md +134 -41
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +82 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +177 -52
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +267 -92
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/package.json +2 -3
- package/apps/cf-tcp-container/src/container-server.ts +12 -9
- package/apps/cf-tcp-container/wrangler.toml +1 -10
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/wrangler.toml +12 -71
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/server.ts +21 -17
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +1 -0
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +66 -4
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +2 -2
- package/apps/web/tests/config-schema.test.ts +149 -25
- package/docs/AWS-Deployment.md +670 -96
- package/docs/AWS-TCP-Deployment.md +20 -45
- package/docs/Cloudflare-Deployment-Guide.md +87 -113
- package/docs/Cloudflare-TCP-Deployment.md +25 -49
- package/docs/Release-Process.md +27 -23
- package/docs/Services.md +69 -22
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/cdk-table-defs.ts +6 -11
- package/packages/aws-adapter/src/config-loader.ts +19 -2
- package/packages/aws-adapter/src/dynamo-services-store.ts +7 -0
- package/packages/aws-adapter/src/handlers/connect.ts +26 -0
- package/packages/aws-adapter/src/handlers/default.ts +190 -123
- package/packages/aws-adapter/src/handlers/index.ts +67 -23
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +3 -6
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +15 -0
- package/packages/aws-adapter/src/tables.ts +2 -12
- package/packages/aws-adapter/tests/aws-harness.ts +0 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +66 -0
- package/packages/aws-adapter/tests/connect.test.ts +124 -1
- package/packages/aws-adapter/tests/default-occ.test.ts +219 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +11 -0
- package/packages/aws-adapter/tests/handlers.test.ts +117 -11
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +59 -8
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/connection-do.ts +266 -109
- package/packages/cf-adapter/src/d1-services-store.ts +63 -26
- package/packages/cf-adapter/src/env.ts +11 -10
- package/packages/cf-adapter/src/index.ts +0 -6
- package/packages/cf-adapter/tests/cf-runtime.test.ts +101 -1
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +95 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/d1-services-store.test.ts +53 -2
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/commands/account-auth.ts +16 -19
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/nickserv.ts +122 -11
- package/packages/irc-core/src/commands/registration.ts +2 -2
- package/packages/irc-core/src/commands/sasl.ts +22 -31
- package/packages/irc-core/src/config.ts +35 -9
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/index.ts +1 -0
- package/packages/irc-core/src/ports.ts +159 -179
- package/packages/irc-core/src/types.ts +38 -10
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/commands/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +237 -1
- package/packages/irc-core/tests/commands/oper.test.ts +15 -0
- package/packages/irc-core/tests/commands/registration.test.ts +140 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +159 -178
- package/packages/irc-core/tests/commands/unified-account.test.ts +102 -84
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/persistent-services-store.test.ts +71 -12
- package/packages/irc-core/tests/ports.test.ts +71 -0
- package/packages/irc-core/tests/services-store.test.ts +204 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +0 -14
- package/packages/irc-server/tests/actor.test.ts +19 -47
- package/packages/irc-test-support/package.json +1 -1
- package/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/ci-hardening/src/index.ts +2 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- package/tools/load-test/package.json +1 -1
- package/tools/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_CONFIG_FILE,
|
|
4
|
+
ENV_TO_CONFIG_FILE,
|
|
5
|
+
hasUnsubstitutedPlaceholder,
|
|
6
|
+
parseApiUrlForKiwi,
|
|
7
|
+
resolveApiUrl,
|
|
8
|
+
resolveConfigFile,
|
|
9
|
+
} from '../src/build-env';
|
|
3
10
|
|
|
4
11
|
// ============================================================================
|
|
5
12
|
// resolveConfigFile — per-env config selection
|
|
6
13
|
// ============================================================================
|
|
7
14
|
|
|
8
15
|
describe('resolveConfigFile', () => {
|
|
9
|
-
it('selects config.staging.json for --env staging', () => {
|
|
10
|
-
const result = resolveConfigFile(['--env', 'staging']);
|
|
11
|
-
expect(result.file).toBe('config.staging.json');
|
|
12
|
-
expect(result.env).toBe('staging');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
16
|
it('selects config.prod.json for --env prod', () => {
|
|
16
17
|
const result = resolveConfigFile(['--env', 'prod']);
|
|
17
18
|
expect(result.file).toBe('config.prod.json');
|
|
18
19
|
expect(result.env).toBe('prod');
|
|
19
20
|
});
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
// AWS deploys the WebSocket endpoint as a separate API Gateway URL, so the
|
|
23
|
+
// AWS SPA build needs its own per-env config (`config.prod-aws.json`) baked
|
|
24
|
+
// into dist/webclient/static/config.json. The `prod-aws` env name matches
|
|
25
|
+
// the historical staging/prod split nomenclature (now collapsed); see the
|
|
26
|
+
// build-env.ts docstring.
|
|
27
|
+
it('selects config.prod-aws.json for --env prod-aws', () => {
|
|
28
|
+
const result = resolveConfigFile(['--env', 'prod-aws']);
|
|
29
|
+
expect(result.file).toBe('config.prod-aws.json');
|
|
30
|
+
expect(result.env).toBe('prod-aws');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('falls back to the default config when --env is absent', () => {
|
|
22
34
|
const result = resolveConfigFile([]);
|
|
23
35
|
expect(result.file).toBe(DEFAULT_CONFIG_FILE);
|
|
24
36
|
expect(result.file).toBe('config.json');
|
|
25
|
-
expect(result.env).toBe('default
|
|
37
|
+
expect(result.env).toBe('default');
|
|
26
38
|
});
|
|
27
39
|
|
|
28
40
|
it('throws on an unknown --env value', () => {
|
|
@@ -36,6 +48,15 @@ describe('resolveConfigFile', () => {
|
|
|
36
48
|
expect(() => resolveConfigFile(['--env', 'stg'])).toThrowError(/Unknown --env value: stg/u);
|
|
37
49
|
});
|
|
38
50
|
|
|
51
|
+
it('rejects staging (no longer a recognised env)', () => {
|
|
52
|
+
// staging was collapsed into the single prod target; the config file
|
|
53
|
+
// and the ENV_TO_CONFIG_FILE entry are gone, so --env staging must
|
|
54
|
+
// throw like any other unknown value.
|
|
55
|
+
expect(() => resolveConfigFile(['--env', 'staging'])).toThrowError(
|
|
56
|
+
/Unknown --env value: staging/u,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
39
60
|
it('names the accepted env values in the error message', () => {
|
|
40
61
|
try {
|
|
41
62
|
resolveConfigFile(['--env', 'nope']);
|
|
@@ -61,3 +82,183 @@ describe('resolveConfigFile', () => {
|
|
|
61
82
|
expect(result.file).toBe(DEFAULT_CONFIG_FILE);
|
|
62
83
|
});
|
|
63
84
|
});
|
|
85
|
+
|
|
86
|
+
// ============================================================================
|
|
87
|
+
// resolveApiUrl — stack-output URL override (`--api-url` flag)
|
|
88
|
+
// ============================================================================
|
|
89
|
+
//
|
|
90
|
+
// The AWS web deploy pipeline reads the deployed stack's `ConnectUrl` output
|
|
91
|
+
// (the API Gateway WebSocket URL) and forwards it to the SPA build via
|
|
92
|
+
// `--api-url`, so the baked `config.json` `startupOptions.server` carries
|
|
93
|
+
// the real wss URL instead of the `{{API_ID}}`/`{{REGION}}`/`{{STAGE}}`
|
|
94
|
+
// template baked into `static/config.prod-aws.json`. No hardcoded URLs
|
|
95
|
+
// live in the repo: the URL is read from CloudFormation outputs at deploy
|
|
96
|
+
// time and threaded through this flag.
|
|
97
|
+
|
|
98
|
+
describe('resolveApiUrl', () => {
|
|
99
|
+
it('returns the value following --api-url', () => {
|
|
100
|
+
expect(resolveApiUrl(['--api-url', 'wss://abc.execute-api.us-east-1.amazonaws.com/prod'])).toBe(
|
|
101
|
+
'wss://abc.execute-api.us-east-1.amazonaws.com/prod',
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns undefined when --api-url is absent', () => {
|
|
106
|
+
expect(resolveApiUrl([])).toBeUndefined();
|
|
107
|
+
expect(resolveApiUrl(['--env', 'prod-aws'])).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns undefined when --api-url has no following value', () => {
|
|
111
|
+
// Trailing `--api-url` with no value: indexOf + 1 yields `undefined`,
|
|
112
|
+
// which the caller treats as "not supplied".
|
|
113
|
+
expect(resolveApiUrl(['--api-url'])).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('coexists with --env on the same argv', () => {
|
|
117
|
+
// The deploy pipeline invokes the build with both flags at once:
|
|
118
|
+
// pnpm --filter web run build:prod-aws -- --api-url <url>
|
|
119
|
+
// resolves to `node scripts/build.mjs --env prod-aws --api-url <url>`.
|
|
120
|
+
const argv = ['--env', 'prod-aws', '--api-url', 'wss://example'];
|
|
121
|
+
expect(resolveApiUrl(argv)).toBe('wss://example');
|
|
122
|
+
expect(resolveConfigFile(argv).file).toBe('config.prod-aws.json');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// hasUnsubstitutedPlaceholder — guard against shipping a broken baked config
|
|
128
|
+
// ============================================================================
|
|
129
|
+
//
|
|
130
|
+
// The placeholder guard prevents the AWS deploy pipeline from shipping a
|
|
131
|
+
// prod-aws config that still carries `{{API_ID}}`/`{{REGION}}`/`{{STAGE}}`
|
|
132
|
+
// (which would produce a broken SPA dialling a literal `{{API_ID}}...` URL).
|
|
133
|
+
// It is scoped to the prod-aws env only — the CF envs (`default`, `prod`)
|
|
134
|
+
// intentionally use Kiwi's `{{hostname}}` runtime template, which is
|
|
135
|
+
// resolved by Kiwi in the browser at load time and must survive the bake.
|
|
136
|
+
// A regression that applied the guard to the CF envs would break every
|
|
137
|
+
// default `pnpm build` and every `build:prod`.
|
|
138
|
+
|
|
139
|
+
describe('hasUnsubstitutedPlaceholder', () => {
|
|
140
|
+
it('returns false for the default env with a {{hostname}} runtime template (CF dev config)', () => {
|
|
141
|
+
// This is the regression that broke `pnpm build` after the initial
|
|
142
|
+
// T167 placeholder guard: the check fired on {{hostname}}, treating
|
|
143
|
+
// it as an unsubstituted AWS placeholder. {{hostname}} is a Kiwi
|
|
144
|
+
// runtime template resolved at browser load time and must survive.
|
|
145
|
+
const baked = JSON.stringify({
|
|
146
|
+
startupOptions: { server: '{{hostname}}', tls: true, port: 443 },
|
|
147
|
+
});
|
|
148
|
+
expect(hasUnsubstitutedPlaceholder(baked, 'default')).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns false for the prod env with a {{hostname}} runtime template (CF prod config)', () => {
|
|
152
|
+
const baked = JSON.stringify({
|
|
153
|
+
startupOptions: { server: '{{hostname}}', tls: true, port: 443 },
|
|
154
|
+
});
|
|
155
|
+
expect(hasUnsubstitutedPlaceholder(baked, 'prod')).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns true for the prod-aws env with unsubstituted {{API_ID}}/{{REGION}}/{{STAGE}}', () => {
|
|
159
|
+
const baked = JSON.stringify({
|
|
160
|
+
startupOptions: {
|
|
161
|
+
server: 'wss://{{API_ID}}.execute-api.{{REGION}}.amazonaws.com/{{STAGE}}',
|
|
162
|
+
tls: true,
|
|
163
|
+
port: 443,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
expect(hasUnsubstitutedPlaceholder(baked, 'prod-aws')).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('returns false for the prod-aws env after --api-url substitution', () => {
|
|
170
|
+
const baked = JSON.stringify({
|
|
171
|
+
startupOptions: {
|
|
172
|
+
server: 'wss://abc123.execute-api.us-east-1.amazonaws.com/prod',
|
|
173
|
+
tls: true,
|
|
174
|
+
port: 443,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
expect(hasUnsubstitutedPlaceholder(baked, 'prod-aws')).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('still rejects a {{hostname}}-only payload in the prod-aws env (defensive — AWS does not use the runtime template)', () => {
|
|
181
|
+
// prod-aws config carries literal wss:// + AWS placeholders, never
|
|
182
|
+
// {{hostname}}. If somehow {{hostname}} ended up in a prod-aws bake,
|
|
183
|
+
// the guard would catch it — better to fail loudly than ship a
|
|
184
|
+
// browser-resolved hostname on the AWS path where the wss endpoint
|
|
185
|
+
// is on a different origin.
|
|
186
|
+
const baked = JSON.stringify({ startupOptions: { server: '{{hostname}}' } });
|
|
187
|
+
expect(hasUnsubstitutedPlaceholder(baked, 'prod-aws')).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('returns false for any env when the baked payload has no placeholder at all', () => {
|
|
191
|
+
const baked = JSON.stringify({ startupOptions: { server: 'wss://concrete.example.com' } });
|
|
192
|
+
expect(hasUnsubstitutedPlaceholder(baked, 'default')).toBe(false);
|
|
193
|
+
expect(hasUnsubstitutedPlaceholder(baked, 'prod')).toBe(false);
|
|
194
|
+
expect(hasUnsubstitutedPlaceholder(baked, 'prod-aws')).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ============================================================================
|
|
199
|
+
// parseApiUrlForKiwi — split a wss:// URL into Kiwi's server + direct_path
|
|
200
|
+
// ============================================================================
|
|
201
|
+
//
|
|
202
|
+
// irc-framework builds the WS URL as `wss://<server>:<port><direct_path>`,
|
|
203
|
+
// prepending the `wss://` scheme itself whenever `tls: true`. The AWS
|
|
204
|
+
// deploy pipeline's `--api-url` therefore cannot pass the full URL through
|
|
205
|
+
// to `server` — that would produce a doubled-scheme URL like
|
|
206
|
+
// `wss://wss//<host>...`. The helper splits the stack-output URL into the
|
|
207
|
+
// two fields Kiwi actually wants: a hostname-only `server` and a
|
|
208
|
+
// stage-name `direct_path`.
|
|
209
|
+
|
|
210
|
+
describe('parseApiUrlForKiwi', () => {
|
|
211
|
+
it('splits a wss:// URL with a stage path into server + direct_path', () => {
|
|
212
|
+
const r = parseApiUrlForKiwi('wss://abc.execute-api.us-east-1.amazonaws.com/prod');
|
|
213
|
+
expect(r.server).toBe('abc.execute-api.us-east-1.amazonaws.com');
|
|
214
|
+
expect(r.directPath).toBe('/prod');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('returns direct_path undefined when the URL path is / (CF Worker root case)', () => {
|
|
218
|
+
const r = parseApiUrlForKiwi('wss://worker.example.com/');
|
|
219
|
+
expect(r.server).toBe('worker.example.com');
|
|
220
|
+
expect(r.directPath).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('returns direct_path undefined when the URL has no path', () => {
|
|
224
|
+
const r = parseApiUrlForKiwi('wss://worker.example.com');
|
|
225
|
+
expect(r.server).toBe('worker.example.com');
|
|
226
|
+
expect(r.directPath).toBeUndefined();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('accepts a ws:// (insecure) URL', () => {
|
|
230
|
+
// Local dev / testing only; the prod build always carries wss://. The
|
|
231
|
+
// helper still splits it so the build script does not need to know the
|
|
232
|
+
// scheme in advance.
|
|
233
|
+
const r = parseApiUrlForKiwi('ws://localhost:8787/dev');
|
|
234
|
+
expect(r.server).toBe('localhost');
|
|
235
|
+
expect(r.directPath).toBe('/dev');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('strips the port into neither server nor direct_path (Kiwi reads `port` separately)', () => {
|
|
239
|
+
// The deploy pipeline sets `port` to 443 via the template; the URL's
|
|
240
|
+
// explicit `:443` is dropped during the split. irc-framework re-adds
|
|
241
|
+
// it from the `port` field at WS construction time.
|
|
242
|
+
const r = parseApiUrlForKiwi('wss://host.example.com:8443/stage');
|
|
243
|
+
expect(r.server).toBe('host.example.com');
|
|
244
|
+
expect(r.directPath).toBe('/stage');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('throws when the URL has no wss:// or ws:// scheme', () => {
|
|
248
|
+
expect(() => parseApiUrlForKiwi('abc.execute-api.us-east-1.amazonaws.com/prod')).toThrowError(
|
|
249
|
+
/must be a wss:\/\/ \(or ws:\/\/\) URL/u,
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('throws on a https:// URL (wrong scheme)', () => {
|
|
254
|
+
expect(() => parseApiUrlForKiwi('https://example.com/prod')).toThrowError(
|
|
255
|
+
/must be a wss:\/\/ \(or ws:\/\/\) URL/u,
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('throws on an unparseable URL', () => {
|
|
260
|
+
// `new URL('wss://')` does not throw, but a hostname with invalid
|
|
261
|
+
// chars does. Pick a value the URL parser rejects outright.
|
|
262
|
+
expect(() => parseApiUrlForKiwi('wss://<<>>/prod')).toThrowError();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -10,8 +10,8 @@ const indexPath = path.join(appDist, 'index.html');
|
|
|
10
10
|
const landingSrc = path.join(pkgRoot, 'landing', 'index.html');
|
|
11
11
|
const landingDist = path.join(pkgRoot, 'dist', 'index.html');
|
|
12
12
|
|
|
13
|
-
// Validates the output of `pnpm --filter web build` (or `build:
|
|
14
|
-
//
|
|
13
|
+
// Validates the output of `pnpm --filter web build` (or `build:prod`).
|
|
14
|
+
// Skipped until the SPA has been built (the Kiwi upstream
|
|
15
15
|
// build is too heavy to run inside the test suite); run the build first to
|
|
16
16
|
// exercise these assertions.
|
|
17
17
|
const built = existsSync(indexPath);
|
|
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { resolveConfigFile } from '../src/build-env';
|
|
5
6
|
import { KiwiConfigSchema, parseKiwiConfig } from '../src/config-schema';
|
|
6
7
|
|
|
7
8
|
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -240,23 +241,21 @@ describe('KiwiConfigSchema — direct WebSocket mode', () => {
|
|
|
240
241
|
//
|
|
241
242
|
// NOTE: Kiwi reads `startupOptions.direct_path`, NOT `startupOptions.path`
|
|
242
243
|
// (upstream `src/helpers/Misc.js:139`: `connection.direct_path = config.direct_path || ''`).
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
244
|
+
//
|
|
245
|
+
// Two valid shapes:
|
|
246
|
+
// - CF Worker: WS upgrade is at root, so `direct_path` is `/` (or absent).
|
|
247
|
+
// - AWS API Gateway: the WebSocket endpoint URL includes the stage name as
|
|
248
|
+
// a path segment (`wss://<id>.execute-api.<region>.amazonaws.com/prod`),
|
|
249
|
+
// so `direct_path` MUST be `/<stage>` (e.g. `/prod`). irc-framework
|
|
250
|
+
// builds the WS URL as `wss://<server>:<port><direct_path>`.
|
|
251
|
+
//
|
|
252
|
+
// The constraint accepts a leading-slash plus zero or one path segment
|
|
253
|
+
// (no nested paths), allowing alphanumeric stage names and the
|
|
254
|
+
// `{{STAGE}}` placeholder baked into `config.prod-aws.json` for the
|
|
255
|
+
// deploy pipeline to substitute.
|
|
247
256
|
|
|
248
257
|
describe('KiwiConfigSchema — WS path (direct_path)', () => {
|
|
249
|
-
it('
|
|
250
|
-
expect(() =>
|
|
251
|
-
parseKiwiConfig(
|
|
252
|
-
validConfig({
|
|
253
|
-
startupOptions: validStartupOptions({ direct_path: '/webclient/' }),
|
|
254
|
-
}),
|
|
255
|
-
),
|
|
256
|
-
).toThrowError(/direct_path/u);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('accepts the / direct_path', () => {
|
|
258
|
+
it('accepts the / direct_path (CF Worker — WS upgrade at root)', () => {
|
|
260
259
|
const cfg = parseKiwiConfig(
|
|
261
260
|
validConfig({
|
|
262
261
|
startupOptions: validStartupOptions({ direct_path: '/' }),
|
|
@@ -269,6 +268,44 @@ describe('KiwiConfigSchema — WS path (direct_path)', () => {
|
|
|
269
268
|
const cfg = parseKiwiConfig(validConfig());
|
|
270
269
|
expect(cfg.startupOptions.direct_path).toBeUndefined();
|
|
271
270
|
});
|
|
271
|
+
|
|
272
|
+
it('accepts a single-segment stage path (AWS API Gateway — /prod)', () => {
|
|
273
|
+
const cfg = parseKiwiConfig(
|
|
274
|
+
validConfig({
|
|
275
|
+
startupOptions: validStartupOptions({ direct_path: '/prod' }),
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
expect(cfg.startupOptions.direct_path).toBe('/prod');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('accepts the /{{STAGE}} template placeholder (config.prod-aws.json before substitution)', () => {
|
|
282
|
+
const cfg = parseKiwiConfig(
|
|
283
|
+
validConfig({
|
|
284
|
+
startupOptions: validStartupOptions({ direct_path: '/{{STAGE}}' }),
|
|
285
|
+
}),
|
|
286
|
+
);
|
|
287
|
+
expect(cfg.startupOptions.direct_path).toBe('/{{STAGE}}');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('rejects a multi-segment direct_path (path nesting is not a stage shape)', () => {
|
|
291
|
+
expect(() =>
|
|
292
|
+
parseKiwiConfig(
|
|
293
|
+
validConfig({
|
|
294
|
+
startupOptions: validStartupOptions({ direct_path: '/webclient/' }),
|
|
295
|
+
}),
|
|
296
|
+
),
|
|
297
|
+
).toThrowError(/direct_path/u);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('rejects a direct_path with no leading slash', () => {
|
|
301
|
+
expect(() =>
|
|
302
|
+
parseKiwiConfig(
|
|
303
|
+
validConfig({
|
|
304
|
+
startupOptions: validStartupOptions({ direct_path: 'prod' }),
|
|
305
|
+
}),
|
|
306
|
+
),
|
|
307
|
+
).toThrowError(/direct_path/u);
|
|
308
|
+
});
|
|
272
309
|
});
|
|
273
310
|
|
|
274
311
|
// ============================================================================
|
|
@@ -368,30 +405,35 @@ describe('KiwiConfigSchema — server auto-detect ({{hostname}} template)', () =
|
|
|
368
405
|
});
|
|
369
406
|
|
|
370
407
|
// ============================================================================
|
|
371
|
-
// Static config files —
|
|
408
|
+
// Static config files — build matrix (single prod target + dev default)
|
|
372
409
|
// ============================================================================
|
|
373
410
|
|
|
374
411
|
describe('static config files', () => {
|
|
375
|
-
it('config.json parses (default
|
|
412
|
+
it('config.json parses (dev default)', () => {
|
|
376
413
|
const cfg = parseKiwiConfig(readStaticConfig('config.json'));
|
|
377
414
|
expect(cfg.startupOptions.server.length).toBeGreaterThan(0);
|
|
378
415
|
expect(cfg.startupOptions.channel.startsWith('#')).toBe(true);
|
|
379
416
|
});
|
|
380
417
|
|
|
381
|
-
it('config.
|
|
382
|
-
const cfg = parseKiwiConfig(readStaticConfig('config.
|
|
418
|
+
it('config.prod.json parses', () => {
|
|
419
|
+
const cfg = parseKiwiConfig(readStaticConfig('config.prod.json'));
|
|
383
420
|
expect(cfg.startupOptions.channel.startsWith('#')).toBe(true);
|
|
384
421
|
});
|
|
385
422
|
|
|
386
|
-
|
|
387
|
-
|
|
423
|
+
// AWS deploys the WebSocket endpoint as a separate API Gateway URL
|
|
424
|
+
// (wss://{api-id}.execute-api.{region}.amazonaws.com/{stage}), not the
|
|
425
|
+
// CloudFront origin the SPA is served from. The prod-aws config carries
|
|
426
|
+
// a templated API-GW URL whose {{...}} placeholders the deploy pipeline
|
|
427
|
+
// substitutes at bake time, so no concrete URL is hardcoded in the repo.
|
|
428
|
+
it('config.prod-aws.json parses', () => {
|
|
429
|
+
const cfg = parseKiwiConfig(readStaticConfig('config.prod-aws.json'));
|
|
388
430
|
expect(cfg.startupOptions.channel.startsWith('#')).toBe(true);
|
|
389
431
|
});
|
|
390
432
|
|
|
391
433
|
it('the raw schema also accepts every static config', () => {
|
|
392
434
|
expect(KiwiConfigSchema.safeParse(readStaticConfig('config.json')).success).toBe(true);
|
|
393
|
-
expect(KiwiConfigSchema.safeParse(readStaticConfig('config.staging.json')).success).toBe(true);
|
|
394
435
|
expect(KiwiConfigSchema.safeParse(readStaticConfig('config.prod.json')).success).toBe(true);
|
|
436
|
+
expect(KiwiConfigSchema.safeParse(readStaticConfig('config.prod-aws.json')).success).toBe(true);
|
|
395
437
|
});
|
|
396
438
|
|
|
397
439
|
// Deploy-time resolution: every shipped config uses the {{hostname}} template
|
|
@@ -399,7 +441,7 @@ describe('static config files', () => {
|
|
|
399
441
|
// time. This is the whole point of NOT baking a per-env hostname; if any
|
|
400
442
|
// config regresses to a literal host, this assertion fires.
|
|
401
443
|
it('every static config bakes {{hostname}} for server (deploy-time auto-detect)', () => {
|
|
402
|
-
for (const file of ['config.json', 'config.
|
|
444
|
+
for (const file of ['config.json', 'config.prod.json']) {
|
|
403
445
|
const cfg = parseKiwiConfig(readStaticConfig(file));
|
|
404
446
|
expect(cfg.startupOptions.server, `${file} server`).toBe('{{hostname}}');
|
|
405
447
|
}
|
|
@@ -410,7 +452,7 @@ describe('static config files', () => {
|
|
|
410
452
|
// opens a wss:// WebSocket straight to the Worker instead of probing the
|
|
411
453
|
// (non-existent) /webirc/kiwiirc/info gateway endpoint and 404-ing.
|
|
412
454
|
it('every static config sets direct:true + tls:true (no kiwiServer gateway)', () => {
|
|
413
|
-
for (const file of ['config.json', 'config.
|
|
455
|
+
for (const file of ['config.json', 'config.prod.json', 'config.prod-aws.json']) {
|
|
414
456
|
const cfg = parseKiwiConfig(readStaticConfig(file));
|
|
415
457
|
expect(cfg.startupOptions.direct, `${file} direct`).toBe(true);
|
|
416
458
|
expect(cfg.startupOptions.tls, `${file} tls`).toBe(true);
|
|
@@ -424,9 +466,91 @@ describe('static config files', () => {
|
|
|
424
466
|
// `plugins` block. If plugin support is ever needed, the build must copy
|
|
425
467
|
// `.example` → `.html` (or ship a real plugin file) first.
|
|
426
468
|
it('no static config ships a plugins block (avoids the customise.html 404)', () => {
|
|
427
|
-
for (const file of ['config.json', 'config.
|
|
469
|
+
for (const file of ['config.json', 'config.prod.json', 'config.prod-aws.json']) {
|
|
428
470
|
const raw = readStaticConfig(file) as Record<string, unknown>;
|
|
429
471
|
expect(raw.plugins, `${file} plugins`).toBeUndefined();
|
|
430
472
|
}
|
|
431
473
|
});
|
|
432
474
|
});
|
|
475
|
+
|
|
476
|
+
// ============================================================================
|
|
477
|
+
// AWS prod-aws config — API Gateway WebSocket URL + bake pipeline shape
|
|
478
|
+
// ============================================================================
|
|
479
|
+
//
|
|
480
|
+
// On AWS the WebSocket endpoint is a separate API Gateway URL, not the
|
|
481
|
+
// CloudFront origin that serves the SPA. The prod-aws config therefore
|
|
482
|
+
// bakes a templated host (`{{API_ID}}.execute-api.{{REGION}}.amazonaws.com`)
|
|
483
|
+
// for `server` plus a templated `direct_path` of `/{{STAGE}}` for the API
|
|
484
|
+
// Gateway stage path. irc-framework builds the WS URL as
|
|
485
|
+
// `wss://<server>:<port><direct_path>` (it prepends `wss://` itself when
|
|
486
|
+
// `tls: true`), so a full `wss://...` URL in `server` would produce a
|
|
487
|
+
// doubled-scheme URL that Firefox rejects. No concrete URL is committed;
|
|
488
|
+
// the `{{...}}` placeholders are substituted by the deploy pipeline (see
|
|
489
|
+
// `parseApiUrlForKiwi` and the AWS web deploy ticket) after `cdk deploy`
|
|
490
|
+
// emits the real API id, region, and stage. This block covers the
|
|
491
|
+
// accept-criteria: schema-conformant file, API-GW-shaped server + path,
|
|
492
|
+
// invariants shared with the CF configs, and a malformed prod-aws payload
|
|
493
|
+
// failing the build with a readable aggregated Zod error.
|
|
494
|
+
|
|
495
|
+
describe('static config files — prod-aws (AWS API Gateway)', () => {
|
|
496
|
+
it('config.prod-aws.json server is the templated API Gateway host (no wss:// scheme)', () => {
|
|
497
|
+
const cfg = parseKiwiConfig(readStaticConfig('config.prod-aws.json'));
|
|
498
|
+
expect(cfg.startupOptions.server).toBe('{{API_ID}}.execute-api.{{REGION}}.amazonaws.com');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('config.prod-aws.json direct_path is the templated stage path', () => {
|
|
502
|
+
const cfg = parseKiwiConfig(readStaticConfig('config.prod-aws.json'));
|
|
503
|
+
expect(cfg.startupOptions.direct_path).toBe('/{{STAGE}}');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('config.prod-aws.json keeps direct:true + tls:true (CloudFront serves HTTPS; WS must be wss://)', () => {
|
|
507
|
+
const cfg = parseKiwiConfig(readStaticConfig('config.prod-aws.json'));
|
|
508
|
+
expect(cfg.startupOptions.direct).toBe(true);
|
|
509
|
+
expect(cfg.startupOptions.tls).toBe(true);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// Mirrors the bake step in scripts/build.mjs (resolveConfigFile → read →
|
|
513
|
+
// parseKiwiConfig → re-serialise) without invoking the heavy Kiwi upstream
|
|
514
|
+
// build. The point of the assertion is that the prod-aws server + path
|
|
515
|
+
// survive the parse + re-stringify round-trip verbatim, i.e. the baked
|
|
516
|
+
// dist/webclient/static/config.json would carry the API-GW-shaped fields.
|
|
517
|
+
it('the baked prod-aws config carries the API-GW-shaped server + direct_path (bake round-trip)', () => {
|
|
518
|
+
const resolved = resolveConfigFile(['--env', 'prod-aws']);
|
|
519
|
+
expect(resolved.file).toBe('config.prod-aws.json');
|
|
520
|
+
const raw = readStaticConfig(resolved.file);
|
|
521
|
+
const parsed = parseKiwiConfig(raw);
|
|
522
|
+
const baked = `${JSON.stringify(parsed, null, 2)}\n`;
|
|
523
|
+
expect(baked).toContain('{{API_ID}}.execute-api.{{REGION}}.amazonaws.com');
|
|
524
|
+
expect(baked).toContain('/{{STAGE}}');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('a malformed prod-aws server (non-empty string required) fails the build with a readable Zod error', () => {
|
|
528
|
+
const valid = readStaticConfig('config.prod-aws.json') as Record<string, unknown>;
|
|
529
|
+
const startupOptions = (valid.startupOptions as Record<string, unknown>) ?? {};
|
|
530
|
+
const malformed = {
|
|
531
|
+
...valid,
|
|
532
|
+
startupOptions: { ...startupOptions, server: '' },
|
|
533
|
+
};
|
|
534
|
+
expect(() => parseKiwiConfig(malformed)).toThrowError(/server/u);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('an out-of-range port in a prod-aws payload fails the build', () => {
|
|
538
|
+
const valid = readStaticConfig('config.prod-aws.json') as Record<string, unknown>;
|
|
539
|
+
const startupOptions = (valid.startupOptions as Record<string, unknown>) ?? {};
|
|
540
|
+
const malformed = {
|
|
541
|
+
...valid,
|
|
542
|
+
startupOptions: { ...startupOptions, port: 99999 },
|
|
543
|
+
};
|
|
544
|
+
expect(() => parseKiwiConfig(malformed)).toThrowError(/port/u);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('a non-# channel in a prod-aws payload fails the build', () => {
|
|
548
|
+
const valid = readStaticConfig('config.prod-aws.json') as Record<string, unknown>;
|
|
549
|
+
const startupOptions = (valid.startupOptions as Record<string, unknown>) ?? {};
|
|
550
|
+
const malformed = {
|
|
551
|
+
...valid,
|
|
552
|
+
startupOptions: { ...startupOptions, channel: 'welcome' },
|
|
553
|
+
};
|
|
554
|
+
expect(() => parseKiwiConfig(malformed)).toThrowError(/channel/u);
|
|
555
|
+
});
|
|
556
|
+
});
|