serverless-ircd 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +28 -0
- package/.github/workflows/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +35 -9
- package/.github/workflows/deploy-cf.yml +40 -14
- package/CHANGELOG.md +594 -0
- package/README.md +286 -60
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +118 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +398 -67
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +714 -105
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/Dockerfile +37 -5
- package/apps/cf-tcp-container/package.json +7 -3
- package/apps/cf-tcp-container/src/config-loader.ts +113 -2
- package/apps/cf-tcp-container/src/container-server.ts +267 -87
- package/apps/cf-tcp-container/src/main.ts +22 -7
- package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
- package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
- package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
- package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
- package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
- package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
- package/apps/cf-tcp-container/wrangler.toml +18 -14
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/src/worker.ts +77 -5
- package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
- package/apps/cf-worker/tests/smoke.test.ts +4 -0
- package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
- package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
- package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
- package/apps/cf-worker/wrangler.test.toml +15 -1
- package/apps/cf-worker/wrangler.toml +95 -77
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +14 -2
- package/apps/local-cli/src/line-scanner.ts +26 -0
- package/apps/local-cli/src/server.ts +44 -19
- package/apps/local-cli/tests/line-scanner.test.ts +64 -0
- package/apps/local-cli/tests/tcp.test.ts +29 -0
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +1 -0
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +66 -4
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +2 -2
- package/apps/web/tests/config-schema.test.ts +149 -25
- package/docs/AWS-Deployment.md +793 -118
- package/docs/AWS-TCP-Deployment.md +57 -47
- package/docs/Chat-History.md +55 -0
- package/docs/Cloudflare-Deployment-Guide.md +95 -114
- package/docs/Cloudflare-TCP-Deployment.md +160 -101
- package/docs/Release-Process.md +27 -23
- package/docs/SASL-EXTERNAL.md +175 -0
- package/docs/Services.md +69 -22
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/admission.ts +28 -13
- package/packages/aws-adapter/src/aws-runtime.ts +30 -3
- package/packages/aws-adapter/src/cdk-table-defs.ts +39 -16
- package/packages/aws-adapter/src/config-loader.ts +153 -8
- package/packages/aws-adapter/src/dynamo-services-store.ts +19 -0
- package/packages/aws-adapter/src/handlers/connect.ts +73 -1
- package/packages/aws-adapter/src/handlers/default.ts +279 -123
- package/packages/aws-adapter/src/handlers/index.ts +98 -25
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +135 -14
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/ip-admission.ts +79 -0
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +23 -0
- package/packages/aws-adapter/src/tables.ts +11 -12
- package/packages/aws-adapter/tests/admission.test.ts +60 -2
- package/packages/aws-adapter/tests/aws-harness.ts +23 -2
- package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +217 -0
- package/packages/aws-adapter/tests/connect.test.ts +323 -3
- package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +226 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +134 -1
- package/packages/aws-adapter/tests/handlers.test.ts +174 -12
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +100 -10
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/config-loader.ts +133 -8
- package/packages/cf-adapter/src/connection-do.ts +406 -116
- package/packages/cf-adapter/src/counter-do.ts +142 -0
- package/packages/cf-adapter/src/d1-services-store.ts +105 -26
- package/packages/cf-adapter/src/env.ts +99 -10
- package/packages/cf-adapter/src/index.ts +17 -7
- package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +205 -16
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
- package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +164 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
- package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
- package/packages/cf-adapter/tests/d1-services-store.test.ts +245 -3
- package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/cf-adapter/tests/worker/main.ts +4 -0
- package/packages/cf-adapter/wrangler.test.toml +18 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +25 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/caps/capabilities.ts +20 -10
- package/packages/irc-core/src/certfp.ts +178 -0
- package/packages/irc-core/src/commands/account-auth.ts +16 -19
- package/packages/irc-core/src/commands/cap.ts +10 -2
- package/packages/irc-core/src/commands/chanserv.ts +117 -14
- package/packages/irc-core/src/commands/chathistory.ts +13 -5
- package/packages/irc-core/src/commands/hostserv.ts +84 -8
- package/packages/irc-core/src/commands/index.ts +2 -1
- package/packages/irc-core/src/commands/invite.ts +1 -7
- package/packages/irc-core/src/commands/join.ts +1 -16
- package/packages/irc-core/src/commands/kick.ts +1 -8
- package/packages/irc-core/src/commands/list.ts +1 -8
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/mode.ts +1 -8
- package/packages/irc-core/src/commands/multiline.ts +4 -10
- package/packages/irc-core/src/commands/names.ts +53 -13
- package/packages/irc-core/src/commands/nickserv.ts +161 -11
- package/packages/irc-core/src/commands/oper.ts +361 -8
- package/packages/irc-core/src/commands/part.ts +4 -10
- package/packages/irc-core/src/commands/privmsg.ts +8 -4
- package/packages/irc-core/src/commands/registration.ts +148 -4
- package/packages/irc-core/src/commands/sasl.ts +154 -46
- package/packages/irc-core/src/commands/topic.ts +10 -12
- package/packages/irc-core/src/commands/who.ts +1 -8
- package/packages/irc-core/src/config.ts +424 -25
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/effects.ts +24 -0
- package/packages/irc-core/src/flood-control.ts +10 -10
- package/packages/irc-core/src/frame-rate-limit.ts +82 -0
- package/packages/irc-core/src/index.ts +9 -0
- package/packages/irc-core/src/oper-hashing.ts +43 -0
- package/packages/irc-core/src/oper-lockout.ts +87 -0
- package/packages/irc-core/src/ports.ts +529 -190
- package/packages/irc-core/src/protocol/bytes.ts +65 -0
- package/packages/irc-core/src/protocol/channel-name.ts +37 -0
- package/packages/irc-core/src/protocol/index.ts +12 -1
- package/packages/irc-core/src/protocol/outbound.ts +43 -10
- package/packages/irc-core/src/protocol/parser.ts +79 -10
- package/packages/irc-core/src/state/connection.ts +13 -0
- package/packages/irc-core/src/types.ts +266 -23
- package/packages/irc-core/src/ws-framing.ts +5 -4
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/bytes.test.ts +89 -0
- package/packages/irc-core/tests/certfp.test.ts +117 -0
- package/packages/irc-core/tests/commands/cap.test.ts +76 -2
- package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
- package/packages/irc-core/tests/commands/join.test.ts +78 -1
- package/packages/irc-core/tests/commands/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/names.test.ts +193 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +419 -3
- package/packages/irc-core/tests/commands/oper.test.ts +574 -1
- package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
- package/packages/irc-core/tests/commands/registration.test.ts +602 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +742 -172
- package/packages/irc-core/tests/commands/topic.test.ts +137 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +104 -84
- package/packages/irc-core/tests/config.test.ts +534 -2
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/flood-control.test.ts +29 -1
- package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
- package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
- package/packages/irc-core/tests/outbound.test.ts +148 -0
- package/packages/irc-core/tests/parser.test.ts +287 -5
- package/packages/irc-core/tests/persistent-services-store.test.ts +212 -12
- package/packages/irc-core/tests/ports.test.ts +170 -7
- package/packages/irc-core/tests/services-store.test.ts +567 -1
- package/packages/irc-core/tests/ws-framing.test.ts +45 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +123 -22
- package/packages/irc-server/src/dispatch.ts +1 -0
- package/packages/irc-server/src/index.ts +7 -0
- package/packages/irc-server/src/redact.ts +159 -0
- package/packages/irc-server/src/runtime.ts +14 -0
- package/packages/irc-server/src/transport.ts +28 -1
- package/packages/irc-server/tests/actor.test.ts +563 -54
- package/packages/irc-server/tests/dispatch.test.ts +31 -0
- package/packages/irc-server/tests/redact.test.ts +198 -0
- package/packages/irc-server/tests/runtime.test.ts +2 -0
- package/packages/irc-server/tests/transport.test.ts +66 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
- package/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- package/tools/ci-hardening/package.json +2 -2
- package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
- package/tools/ci-hardening/src/cf-deploy.ts +118 -0
- package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
- package/tools/ci-hardening/src/env-var-drift.ts +192 -0
- package/tools/ci-hardening/src/hostname-guard.ts +11 -0
- package/tools/ci-hardening/src/index.ts +19 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
- package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
- package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
- package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
- package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- package/tools/ci-hardening/vitest.config.ts +5 -1
- package/tools/hash-oper-cred.ts +85 -0
- package/tools/load-test/package.json +1 -1
- package/tools/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `scripts/deploy-web-aws.mjs` — the AWS web-client deploy pipeline helper.
|
|
4
|
+
*
|
|
5
|
+
* Two-phase deploy that runs AFTER `cdk deploy` applies the `IrcAwsStack`
|
|
6
|
+
* CloudFormation stack:
|
|
7
|
+
*
|
|
8
|
+
* 1. `aws cloudformation describe-stacks --stack-name IrcAwsStack` reads
|
|
9
|
+
* the stack's outputs (`ConnectUrl`, `WebsiteBucketName`,
|
|
10
|
+
* `WebsiteDistributionId`).
|
|
11
|
+
* 2. `pnpm --filter @serverless-ircd/web run build:prod-aws -- --api-url <url>`
|
|
12
|
+
* bakes the SPA with the real API Gateway WebSocket URL (read from
|
|
13
|
+
* `ConnectUrl` in step 1). The build script overrides the templated
|
|
14
|
+
* `{{API_ID}}`/`{{REGION}}`/`{{STAGE}}` `server` value in
|
|
15
|
+
* `apps/web/static/config.prod-aws.json` and rejects any leftover
|
|
16
|
+
* `{{...}}` placeholders, so no hardcoded URLs ship.
|
|
17
|
+
* 3. `aws s3 sync apps/web/dist s3://<bucket> --delete` uploads the
|
|
18
|
+
* build. The CLI's `sync` compares etags and SKIPS unchanged objects,
|
|
19
|
+
* so a re-deploy with no SPA changes uploads nothing (the
|
|
20
|
+
* invalidation in step 4 still fires — CloudFront has no
|
|
21
|
+
* unchanged-skip equivalent).
|
|
22
|
+
* 4. `aws cloudfront create-invalidation --distribution-id <id> --paths /*`
|
|
23
|
+
* busts the edge cache so the new build is served immediately.
|
|
24
|
+
*
|
|
25
|
+
* Why a script instead of CDK `BucketDeployment`: `BucketDeployment`
|
|
26
|
+
* re-synths on every deploy (slower), cannot bake the post-deploy API URL
|
|
27
|
+
* into the baked config (it runs before the stack outputs exist), and
|
|
28
|
+
* pulls in a custom-resource Lambda for a one-shot copy. A stand-alone
|
|
29
|
+
* script reads the deployed outputs directly.
|
|
30
|
+
*
|
|
31
|
+
* Why the AWS CLI instead of `@aws-sdk/client-*`: `aws s3 sync`'s etag
|
|
32
|
+
* comparison and unchanged-skip are non-trivial to reimplement on top of
|
|
33
|
+
* the SDK; the CLI is already a deploy-time dependency (the workflow uses
|
|
34
|
+
* it for `describe-stacks`). One-off SDK calls would multiply surface
|
|
35
|
+
* area without buying anything.
|
|
36
|
+
*
|
|
37
|
+
* @see docs/AWS-Deployment.md for the full deploy runbook.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { spawnSync } from 'node:child_process';
|
|
41
|
+
import path from 'node:path';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fixed CloudFormation stack name (post-staging/prod-collapse: a single
|
|
45
|
+
* target per platform, no env suffix). Mirrors the value the deploy
|
|
46
|
+
* workflow's `describe-stacks` query already uses.
|
|
47
|
+
*/
|
|
48
|
+
export const STACK_NAME = 'IrcAwsStack';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Outputs the script consumes. Each MUST be present on the deployed
|
|
52
|
+
* stack — a missing output aborts the deploy with a clear message so
|
|
53
|
+
* the operator knows to `cdk deploy` (or check the `webSite` prop) before
|
|
54
|
+
* re-running.
|
|
55
|
+
*/
|
|
56
|
+
export const REQUIRED_OUTPUTS = ['ConnectUrl', 'WebsiteBucketName', 'WebsiteDistributionId'];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Pattern matching the `{{...}}` placeholders baked into
|
|
60
|
+
* `apps/web/static/config.prod-aws.json`. The stack output's `ConnectUrl`
|
|
61
|
+
* should be a fully-resolved wss URL; if it isn't, the build script would
|
|
62
|
+
* bake a broken SPA. This guard fails the deploy at the script layer
|
|
63
|
+
* (defence in depth) instead of relying on `build.mjs` alone.
|
|
64
|
+
*/
|
|
65
|
+
const PLACEHOLDER_PATTERN = /\{\{[^}]+\}\}/u;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Builds the default runner that shells out to the `aws` CLI and `pnpm`
|
|
69
|
+
* via `spawnSync`. IO is captured (pipe) on the AWS path so the script can
|
|
70
|
+
* surface stderr in its own error messages; stdio for the `pnpm build`
|
|
71
|
+
* call is inherited so the (verbose) Kiwi build log streams live to the
|
|
72
|
+
* operator.
|
|
73
|
+
*
|
|
74
|
+
* @param {{ cwd?: string }} [opts]
|
|
75
|
+
* @returns {{ aws: (args: string[]) => CliResult; pnpm: (args: string[]) => CliResult }}
|
|
76
|
+
*/
|
|
77
|
+
export function makeDefaultRunner({ cwd = process.cwd() } = {}) {
|
|
78
|
+
return {
|
|
79
|
+
/* v8 ignore next -- 1-line wrapper over the tested runCli(); the literal binary name is the only thing that differs and it cannot be exercised without `aws` on the test PATH. */
|
|
80
|
+
aws(args) {
|
|
81
|
+
return runCli('aws', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
82
|
+
},
|
|
83
|
+
/* v8 ignore next -- 1-line wrapper over the tested runCli(); same reason as aws() above. */
|
|
84
|
+
pnpm(args) {
|
|
85
|
+
// Inherit stdio for pnpm so the build log streams live.
|
|
86
|
+
return runCli('pnpm', args, { cwd, stdio: 'inherit' });
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Spawns a child process and returns the captured IO + exit status. Pure
|
|
93
|
+
* plumbing — no throwing on non-zero exits (the caller decides whether a
|
|
94
|
+
* non-zero status is an error). Exported so the test-suite can exercise
|
|
95
|
+
* the spawnSync shape with `node` as the binary instead of `aws`/`pnpm`
|
|
96
|
+
* (neither of which is guaranteed to be on PATH in the test environment).
|
|
97
|
+
*
|
|
98
|
+
* @param {string} command
|
|
99
|
+
* @param {string[]} args
|
|
100
|
+
* @param {{ cwd: string; stdio: 'inherit' | Array<'ignore' | 'pipe' | 'inherit'> }} opts
|
|
101
|
+
* @returns {CliResult}
|
|
102
|
+
*/
|
|
103
|
+
export function runCli(command, args, opts) {
|
|
104
|
+
const result = spawnSync(command, args, {
|
|
105
|
+
cwd: opts.cwd,
|
|
106
|
+
stdio: opts.stdio,
|
|
107
|
+
encoding: 'utf8',
|
|
108
|
+
});
|
|
109
|
+
const status = result.status ?? -1;
|
|
110
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout : '';
|
|
111
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr : '';
|
|
112
|
+
return { status, stdout, stderr };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parses the JSON body returned by
|
|
117
|
+
* `aws cloudformation describe-stacks --query 'Stacks[0].Outputs' --output json`
|
|
118
|
+
* into a plain `OutputKey → OutputValue` lookup. Empty / malformed bodies
|
|
119
|
+
* surface as thrown Errors so the caller (the deploy function) can fail
|
|
120
|
+
* loudly with context.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} jsonBody
|
|
123
|
+
* @returns {Record<string, string>}
|
|
124
|
+
*/
|
|
125
|
+
export function parseStackOutputs(jsonBody) {
|
|
126
|
+
const parsed = JSON.parse(jsonBody);
|
|
127
|
+
if (!Array.isArray(parsed)) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
const map = {};
|
|
131
|
+
for (const entry of parsed) {
|
|
132
|
+
if (
|
|
133
|
+
typeof entry === 'object' &&
|
|
134
|
+
entry !== null &&
|
|
135
|
+
typeof entry.OutputKey === 'string' &&
|
|
136
|
+
typeof entry.OutputValue === 'string'
|
|
137
|
+
) {
|
|
138
|
+
map[entry.OutputKey] = entry.OutputValue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return map;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Runs the two-phase AWS web deploy. Throws on any failure (missing
|
|
146
|
+
* output, CLI non-zero exit, leftover placeholder) — the CLI entrypoint
|
|
147
|
+
* catches and exits non-zero.
|
|
148
|
+
*
|
|
149
|
+
* Implemented as an `async` function so the entrypoint and tests can
|
|
150
|
+
* consume it uniformly via `.catch()` / `rejects.toThrowError`; the
|
|
151
|
+
* underlying `spawnSync` calls are themselves synchronous, but wrapping
|
|
152
|
+
* the orchestration in a Promise keeps the contract stable if a future
|
|
153
|
+
* revision swaps in the AWS SDK or a streaming upload.
|
|
154
|
+
*
|
|
155
|
+
* @param {Object} [opts]
|
|
156
|
+
* @param {{ aws: (args: string[]) => CliResult; pnpm: (args: string[]) => CliResult }} [opts.runner]
|
|
157
|
+
* @param {string} [opts.stackName]
|
|
158
|
+
* @param {string} [opts.distDir]
|
|
159
|
+
* @param {(message: string) => void} [opts.log]
|
|
160
|
+
* @returns {Promise<{ connectUrl: string; bucket: string; distributionId: string }>}
|
|
161
|
+
*/
|
|
162
|
+
export async function deployWebAws({
|
|
163
|
+
runner = makeDefaultRunner(),
|
|
164
|
+
stackName = STACK_NAME,
|
|
165
|
+
distDir = path.join('apps', 'web', 'dist'),
|
|
166
|
+
log = (m) => console.log(m),
|
|
167
|
+
} = {}) {
|
|
168
|
+
// 1. Read the deployed stack's outputs.
|
|
169
|
+
const describeResult = runner.aws([
|
|
170
|
+
'cloudformation',
|
|
171
|
+
'describe-stacks',
|
|
172
|
+
'--stack-name',
|
|
173
|
+
stackName,
|
|
174
|
+
'--query',
|
|
175
|
+
'Stacks[0].Outputs',
|
|
176
|
+
'--output',
|
|
177
|
+
'json',
|
|
178
|
+
]);
|
|
179
|
+
if (describeResult.status !== 0) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`aws cloudformation describe-stacks --stack-name ${stackName} exited with ${describeResult.status}: ${describeResult.stderr}`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const outputs = parseStackOutputs(describeResult.stdout);
|
|
185
|
+
|
|
186
|
+
// 2. Guard: every required output MUST be present. A missing output means
|
|
187
|
+
// the stack hasn't been deployed (or the `webSite` prop was omitted on
|
|
188
|
+
// the last synth), and proceeding would either bake a broken config
|
|
189
|
+
// or sync to the wrong bucket.
|
|
190
|
+
const connectUrl = outputs.ConnectUrl;
|
|
191
|
+
const bucket = outputs.WebsiteBucketName;
|
|
192
|
+
const distributionId = outputs.WebsiteDistributionId;
|
|
193
|
+
for (const key of REQUIRED_OUTPUTS) {
|
|
194
|
+
const value = outputs[key];
|
|
195
|
+
if (value === undefined || value.length === 0) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Missing required CloudFormation output '${key}' on stack '${stackName}'. ` +
|
|
198
|
+
`Has the stack been deployed (cdk deploy --all)? The web deploy pipeline requires: ${REQUIRED_OUTPUTS.join(', ')}.`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Defence in depth: the stack output itself should never carry an
|
|
204
|
+
// unsubstituted placeholder, but if it did the build would bake a
|
|
205
|
+
// broken SPA. Fail loudly here so the operator sees the script's
|
|
206
|
+
// diagnostic before the build's.
|
|
207
|
+
if (PLACEHOLDER_PATTERN.test(connectUrl)) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Stack output ConnectUrl carries an unsubstituted placeholder: '${connectUrl}'. The deployed stage URL must be a fully-resolved wss:// URL, not a template.`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 3. Bake the SPA with the real API URL. The build script overrides the
|
|
214
|
+
// templated `server` field in config.prod-aws.json with this value
|
|
215
|
+
// and rejects any remaining `{{...}}` placeholders.
|
|
216
|
+
log(`Baking SPA with API URL ${connectUrl}`);
|
|
217
|
+
const buildResult = runner.pnpm([
|
|
218
|
+
'--filter',
|
|
219
|
+
'@serverless-ircd/web',
|
|
220
|
+
'run',
|
|
221
|
+
'build:prod-aws',
|
|
222
|
+
'--',
|
|
223
|
+
'--env',
|
|
224
|
+
'prod-aws',
|
|
225
|
+
'--api-url',
|
|
226
|
+
connectUrl,
|
|
227
|
+
]);
|
|
228
|
+
if (buildResult.status !== 0) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`pnpm build:prod-aws exited with ${buildResult.status}. The web SPA build failed; see the build log above.`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 4. s3 sync. The CLI's `sync` skips unchanged objects via etag
|
|
235
|
+
// comparison, so a re-deploy with no SPA changes uploads nothing.
|
|
236
|
+
// `--delete` removes objects that no longer exist in the dist tree
|
|
237
|
+
// (e.g. hashed assets from the previous build) so the bucket does
|
|
238
|
+
// not accumulate stale entries.
|
|
239
|
+
log(`Syncing ${distDir} → s3://${bucket}`);
|
|
240
|
+
const syncResult = runner.aws(['s3', 'sync', distDir, `s3://${bucket}`, '--delete']);
|
|
241
|
+
if (syncResult.status !== 0) {
|
|
242
|
+
throw new Error(`aws s3 sync exited with ${syncResult.status}: ${syncResult.stderr}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 5. CloudFront invalidation. Always fires — CloudFront has no
|
|
246
|
+
// unchanged-skip equivalent, so even a no-op sync requires this to
|
|
247
|
+
// guarantee the edge serves the latest origin state.
|
|
248
|
+
log(`Invalidating CloudFront distribution ${distributionId}`);
|
|
249
|
+
const invalidationResult = runner.aws([
|
|
250
|
+
'cloudfront',
|
|
251
|
+
'create-invalidation',
|
|
252
|
+
'--distribution-id',
|
|
253
|
+
distributionId,
|
|
254
|
+
'--paths',
|
|
255
|
+
'/*',
|
|
256
|
+
]);
|
|
257
|
+
if (invalidationResult.status !== 0) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`aws cloudfront create-invalidation exited with ${invalidationResult.status}: ${invalidationResult.stderr}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
log(`Deployed web client → s3://${bucket} (CloudFront ${distributionId}, invalidated /*)`);
|
|
264
|
+
return { connectUrl, bucket, distributionId };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// --- CLI entrypoint --------------------------------------------------------
|
|
268
|
+
//
|
|
269
|
+
// Excluded from coverage: the entrypoint is exercised by the deploy
|
|
270
|
+
// pipeline / a human operator, not by the unit tests (which call
|
|
271
|
+
// `deployWebAws` directly with an injected runner). The branch under test
|
|
272
|
+
// is the `if (invokedDirectly)` body, which only fires when this file is
|
|
273
|
+
// the process entry point — exactly the case vitest's import does NOT
|
|
274
|
+
// produce. v8's ignore markers keep the coverage threshold meaningful
|
|
275
|
+
// for the testable surface.
|
|
276
|
+
|
|
277
|
+
/* v8 ignore start */
|
|
278
|
+
const invokedDirectly =
|
|
279
|
+
process.argv[1] !== undefined &&
|
|
280
|
+
path.resolve(process.argv[1]) ===
|
|
281
|
+
path.resolve(new URL('./deploy-web-aws.mjs', import.meta.url).pathname);
|
|
282
|
+
|
|
283
|
+
if (invokedDirectly) {
|
|
284
|
+
deployWebAws().catch((err) => {
|
|
285
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
286
|
+
console.error(`deploy-web-aws: ${message}`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/* v8 ignore stop */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@serverless-ircd/deploy-scripts",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Repo-level deploy helpers (AWS web client s3 sync + CloudFront invalidation pipeline).",
|
|
6
|
+
"license": "BSD-3-Clause",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"deploy:web-aws": "node ./deploy-web-aws.mjs",
|
|
10
|
+
"typecheck": "tsc -p tsconfig.test.json --noEmit",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"coverage": "vitest run --coverage",
|
|
14
|
+
"clean": "rimraf coverage .turbo"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^26.1.1",
|
|
18
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
19
|
+
"rimraf": "^6.0.0",
|
|
20
|
+
"typescript": "^5.6.0",
|
|
21
|
+
"vitest": "^4.1.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"composite": false,
|
|
6
|
+
"types": ["node"],
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"checkJs": false
|
|
9
|
+
},
|
|
10
|
+
"include": ["**/*.ts", "**/*.mjs", "**/*.d.mts"],
|
|
11
|
+
"exclude": ["node_modules", "coverage"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
environment: 'node',
|
|
6
|
+
include: ['__tests__/**/*.test.ts'],
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: 'v8',
|
|
9
|
+
include: ['deploy-web-aws.mjs'],
|
|
10
|
+
reporter: ['text', 'html', 'json-summary'],
|
|
11
|
+
thresholds: {
|
|
12
|
+
lines: 90,
|
|
13
|
+
functions: 90,
|
|
14
|
+
branches: 90,
|
|
15
|
+
statements: 90,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -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
|
+
}
|