serverless-ircd 0.8.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/ci.yml +4 -0
- 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 +550 -0
- package/README.md +275 -222
- 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 +33 -10
- package/apps/cf-tcp-container/tests/config-loader.test.ts +43 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +249 -1
- package/apps/cf-tcp-container/tests/persistence.test.ts +9 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +24 -5
- 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 +115 -48
- package/apps/local-cli/tests/config-resolution.test.ts +65 -0
- package/apps/local-cli/tests/motd-file-non-error.test.ts +29 -0
- package/apps/local-cli/tests/rehash.test.ts +147 -0
- package/apps/local-cli/tests/server-helpers.test.ts +63 -0
- package/apps/local-cli/tests/tcp.test.ts +89 -0
- package/apps/local-cli/tests/ws-subprotocol.test.ts +92 -0
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +227 -3
- package/apps/web/package.json +3 -2
- package/apps/web/scripts/build.mjs +91 -6
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/src/render-docs.ts +292 -0
- 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 +33 -4
- package/apps/web/tests/config-schema.test.ts +149 -25
- package/apps/web/tests/landing-content.test.ts +103 -0
- package/apps/web/tests/render-docs.test.ts +198 -0
- package/docs/AWS-Adapter-Architecture.md +3 -2
- 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 +102 -23
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/aws-runtime.ts +15 -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 +13 -8
- 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/aws-runtime.test.ts +23 -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/connection-counter.test.ts +17 -0
- 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/global-setup.ts +28 -1
- package/packages/aws-adapter/tests/gone-exception.test.ts +21 -2
- 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/nlb-stream.test.ts +29 -1
- 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 +20 -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 +284 -115
- 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 +222 -51
- 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 +60 -35
- package/packages/irc-core/src/commands/chanserv.ts +288 -4
- package/packages/irc-core/src/commands/hostserv.ts +38 -3
- package/packages/irc-core/src/commands/index.ts +1 -0
- package/packages/irc-core/src/commands/join.ts +41 -35
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/nickserv.ts +138 -15
- package/packages/irc-core/src/commands/registration.ts +28 -17
- package/packages/irc-core/src/commands/sasl.ts +22 -31
- package/packages/irc-core/src/commands/service-aliases.ts +52 -0
- package/packages/irc-core/src/commands/topic.ts +23 -10
- 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/state/channel.ts +17 -0
- 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/chanserv.test.ts +668 -1
- package/packages/irc-core/tests/commands/hostserv.test.ts +71 -0
- package/packages/irc-core/tests/commands/join.test.ts +179 -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 +422 -3
- package/packages/irc-core/tests/commands/oper.test.ts +15 -0
- package/packages/irc-core/tests/commands/registration.test.ts +336 -108
- package/packages/irc-core/tests/commands/sasl.test.ts +194 -169
- package/packages/irc-core/tests/commands/service-aliases.test.ts +52 -0
- 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 +80 -44
- package/packages/irc-server/tests/actor.test.ts +384 -50
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +8 -5
- package/packages/irc-test-support/src/scenarios.ts +21 -6
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +19 -0
- package/packages/irc-test-support/vitest.config.ts +6 -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/load-test/src/client.ts +13 -13
- package/tools/load-test/tests/client.test.ts +258 -2
- package/tools/load-test/tests/config.test.ts +39 -0
- package/tools/load-test/tests/harness.test.ts +21 -0
- package/tools/load-test/tests/metrics.test.ts +7 -0
- 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/tools/tcp-ws-forwarder/tests/close-error.test.ts +40 -0
- package/tools/tcp-ws-forwarder/tests/defensive-branches.test.ts +78 -0
- package/tools/tcp-ws-forwarder/tests/forwarder.test.ts +51 -0
- package/tools/tcp-ws-forwarder/tests/logger.test.ts +31 -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
|
@@ -5,22 +5,36 @@
|
|
|
5
5
|
* unit-testable without running the (slow) Kiwi upstream build. The build
|
|
6
6
|
* script delegates here and then validates the selected file against
|
|
7
7
|
* {@link parseKiwiConfig} before baking it into `dist/webclient/static/config.json`.
|
|
8
|
+
*
|
|
9
|
+
* There is a single Cloudflare deploy target (`prod`); the staging/prod
|
|
10
|
+
* split was collapsed. `--env prod` selects `config.prod.json`; omitting
|
|
11
|
+
* `--env` bakes the dev default (`config.json`). `--env staging` is rejected.
|
|
12
|
+
*
|
|
13
|
+
* AWS has its own per-env config because the WebSocket endpoint is a separate
|
|
14
|
+
* API Gateway URL (`wss://{api-id}.execute-api.{region}.amazonaws.com/{stage}`),
|
|
15
|
+
* not the CloudFront origin serving the SPA. `--env prod-aws` selects
|
|
16
|
+
* `config.prod-aws.json`, whose `server` carries `{{API_ID}}` / `{{REGION}}`
|
|
17
|
+
* / `{{STAGE}}` placeholders the deploy pipeline substitutes at bake time
|
|
18
|
+
* (no concrete URLs in the repo). The `prod-aws` env name is retained for
|
|
19
|
+
* fidelity with the historical staging/prod nomenclature even though the
|
|
20
|
+
* staging/prod split is gone — `aws` alone would read as a cleaner label but
|
|
21
|
+
* is left for a separate rename pass.
|
|
8
22
|
*/
|
|
9
23
|
|
|
10
24
|
/** argv values accepted by `--env`; mapped to a `static/config.<env>.json` file. */
|
|
11
25
|
export const ENV_TO_CONFIG_FILE = {
|
|
12
|
-
staging: 'config.staging.json',
|
|
13
26
|
prod: 'config.prod.json',
|
|
27
|
+
'prod-aws': 'config.prod-aws.json',
|
|
14
28
|
} as const;
|
|
15
29
|
|
|
16
30
|
export type BuildEnv = keyof typeof ENV_TO_CONFIG_FILE;
|
|
17
31
|
|
|
18
|
-
/** Default config file baked when `--env` is not supplied (
|
|
32
|
+
/** Default config file baked when `--env` is not supplied (dev default). */
|
|
19
33
|
export const DEFAULT_CONFIG_FILE = 'config.json';
|
|
20
34
|
|
|
21
35
|
/**
|
|
22
36
|
* Resolves which `static/config.*.json` the build should bake from the
|
|
23
|
-
* `--env
|
|
37
|
+
* `--env prod` CLI flag.
|
|
24
38
|
*
|
|
25
39
|
* @param argv the CLI args after the script name (typically `process.argv.slice(2)`)
|
|
26
40
|
* @returns `{ file, env }` where `file` is the basename under `static/` and
|
|
@@ -31,7 +45,7 @@ export function resolveConfigFile(argv: string[]): { file: string; env: string }
|
|
|
31
45
|
const envIndex = argv.indexOf('--env');
|
|
32
46
|
const env = envIndex !== -1 ? argv[envIndex + 1] : undefined;
|
|
33
47
|
if (env === undefined) {
|
|
34
|
-
return { file: DEFAULT_CONFIG_FILE, env: 'default
|
|
48
|
+
return { file: DEFAULT_CONFIG_FILE, env: 'default' };
|
|
35
49
|
}
|
|
36
50
|
const file = ENV_TO_CONFIG_FILE[env as BuildEnv];
|
|
37
51
|
if (file === undefined) {
|
|
@@ -41,3 +55,110 @@ export function resolveConfigFile(argv: string[]): { file: string; env: string }
|
|
|
41
55
|
}
|
|
42
56
|
return { file, env };
|
|
43
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resolves the optional `--api-url <value>` flag carried on the build argv.
|
|
61
|
+
*
|
|
62
|
+
* The AWS web deploy pipeline reads the deployed stack's `ConnectUrl`
|
|
63
|
+
* CloudFormation output (the API Gateway WebSocket URL) and forwards it
|
|
64
|
+
* to the SPA build via `--api-url`. The build script then overrides
|
|
65
|
+
* `startupOptions.server` in the parsed Kiwi config with this value before
|
|
66
|
+
* baking `dist/webclient/static/config.json`, so the baked SPA carries the
|
|
67
|
+
* real wss URL instead of the `{{API_ID}}`/`{{REGION}}`/`{{STAGE}}`
|
|
68
|
+
* template that lives in `static/config.prod-aws.json`. No concrete URLs
|
|
69
|
+
* are committed; the URL is read from stack outputs at deploy time.
|
|
70
|
+
*
|
|
71
|
+
* Returns `undefined` when the flag is absent OR when it is the final
|
|
72
|
+
* token on the argv (no following value). The caller treats both cases
|
|
73
|
+
* identically: leave the config's `server` field untouched. The Cloudflare
|
|
74
|
+
* deploys never pass this flag; only the AWS deploy pipeline does.
|
|
75
|
+
*
|
|
76
|
+
* @param argv the CLI args after the script name (typically `process.argv.slice(2)`)
|
|
77
|
+
* @returns the URL string following `--api-url`, or `undefined` when unset.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveApiUrl(argv: string[]): string | undefined {
|
|
80
|
+
const i = argv.indexOf('--api-url');
|
|
81
|
+
if (i === -1) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return argv[i + 1];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Splits a `wss://` API Gateway WebSocket URL into the two Kiwi fields it
|
|
89
|
+
* becomes at bake time: `server` (hostname only) and `direct_path` (the
|
|
90
|
+
* stage-name path segment).
|
|
91
|
+
*
|
|
92
|
+
* irc-framework builds the WebSocket URL as `wss://<server>:<port><direct_path>`,
|
|
93
|
+
* prepending the `wss://` scheme itself whenever `tls: true`. Setting
|
|
94
|
+
* `server` to a full `wss://...` URL would make irc-framework prepend a
|
|
95
|
+
* second `wss://`, producing a malformed URL that Firefox reports as
|
|
96
|
+
* `wss://wss//<host>...` (the inner `:` becomes a userinfo separator).
|
|
97
|
+
*
|
|
98
|
+
* AWS API Gateway WebSocket URLs include the stage name as a path
|
|
99
|
+
* segment (`wss://<id>.execute-api.<region>.amazonaws.com/<stage>`) —
|
|
100
|
+
* that segment must reach the browser as `direct_path`, not as part of
|
|
101
|
+
* `server`. The CF Worker's WS upgrade is at root, so CF configs leave
|
|
102
|
+
* `direct_path` unset (or `/`) and `server` is just the hostname.
|
|
103
|
+
*
|
|
104
|
+
* @param url a `wss://` or `ws://` URL (the stack's `ConnectUrl` output)
|
|
105
|
+
* @returns `{ server, directPath }` where `server` is the bare hostname
|
|
106
|
+
* and `directPath` is the leading-slash path (e.g. `/prod`) or
|
|
107
|
+
* `undefined` when the URL's path is `/` or empty.
|
|
108
|
+
* @throws {Error} when the URL is missing the `ws://`/`wss://` scheme or
|
|
109
|
+
* fails `new URL()` parsing.
|
|
110
|
+
*/
|
|
111
|
+
export function parseApiUrlForKiwi(url: string): {
|
|
112
|
+
server: string;
|
|
113
|
+
directPath: string | undefined;
|
|
114
|
+
} {
|
|
115
|
+
if (!/^wss?:\/\//iu.test(url)) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`--api-url must be a wss:// (or ws://) URL; got: ${url}. irc-framework prepends the scheme itself from tls:true, so the bake must split the URL into a hostname-only server + a direct_path.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const parsed = new URL(url);
|
|
121
|
+
const directPath =
|
|
122
|
+
parsed.pathname === '' || parsed.pathname === '/' ? undefined : parsed.pathname;
|
|
123
|
+
return { server: parsed.hostname, directPath };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Pattern matching any `{{key}}` placeholder, used by both the Kiwi
|
|
128
|
+
* runtime templates (`{{hostname}}`, `{{host}}`, `{{port}}`) and the
|
|
129
|
+
* AWS deploy-time placeholders (`{{API_ID}}`, `{{REGION}}`,
|
|
130
|
+
* `{{STAGE}}`).
|
|
131
|
+
*/
|
|
132
|
+
export const PLACEHOLDER_PATTERN = /\{\{[^}]+\}\}/u;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The Cloudflare deploy envs whose `server` field legitimately bakes
|
|
136
|
+
* Kiwi runtime templates. The placeholder guard must NOT fire on these
|
|
137
|
+
* — `{{hostname}}` is resolved by Kiwi in the browser at load time.
|
|
138
|
+
*/
|
|
139
|
+
const RUNTIME_TEMPLATE_ENVS = new Set<string>(['default', 'prod']);
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns `true` when a baked Kiwi config carries an unsubstituted
|
|
143
|
+
* placeholder that would produce a broken SPA. The guard is scoped to
|
|
144
|
+
* the `prod-aws` env: that is the only config whose `server` carries
|
|
145
|
+
* AWS deploy-time placeholders (`{{API_ID}}`/`{{REGION}}`/`{{STAGE}}`)
|
|
146
|
+
* that the deploy pipeline (`scripts/deploy-web-aws.mjs`) must
|
|
147
|
+
* substitute via `--api-url` before baking.
|
|
148
|
+
*
|
|
149
|
+
* The Cloudflare envs (`default`, `prod`) use Kiwi's `{{hostname}}`
|
|
150
|
+
* runtime template, which is resolved by Kiwi in the browser at load
|
|
151
|
+
* time and MUST survive the bake. Applying the guard to them would
|
|
152
|
+
* break every default `pnpm build` and every `build:prod`.
|
|
153
|
+
*
|
|
154
|
+
* @param bakedConfigJson the serialised config payload about to be
|
|
155
|
+
* written to `dist/webclient/static/config.json`.
|
|
156
|
+
* @param env the selected build env (the `env` field from
|
|
157
|
+
* {@link resolveConfigFile}).
|
|
158
|
+
*/
|
|
159
|
+
export function hasUnsubstitutedPlaceholder(bakedConfigJson: string, env: string): boolean {
|
|
160
|
+
if (RUNTIME_TEMPLATE_ENVS.has(env)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
return PLACEHOLDER_PATTERN.test(bakedConfigJson);
|
|
164
|
+
}
|
|
@@ -83,13 +83,27 @@ export const KiwiStartupOptionsSchema = z
|
|
|
83
83
|
/**
|
|
84
84
|
* WebSocket path on the server. Kiwi reads `direct_path` (NOT `path`) —
|
|
85
85
|
* upstream `helpers/Misc.js:139` copies `config.direct_path` into the
|
|
86
|
-
* connection info; a `path` field is silently ignored.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
86
|
+
* connection info; a `path` field is silently ignored.
|
|
87
|
+
*
|
|
88
|
+
* Two valid shapes:
|
|
89
|
+
* - CF Worker: WS upgrade is at root, so `direct_path` is `/`.
|
|
90
|
+
* - AWS API Gateway: the WebSocket endpoint URL includes the stage
|
|
91
|
+
* name as a path segment
|
|
92
|
+
* (`wss://<id>.execute-api.<region>.amazonaws.com/<stage>`), so
|
|
93
|
+
* `direct_path` MUST be `/<stage>` (e.g. `/prod`). irc-framework
|
|
94
|
+
* builds the WS URL as `wss://<server>:<port><direct_path>`.
|
|
95
|
+
*
|
|
96
|
+
* The constraint accepts a leading slash plus zero or one path segment
|
|
97
|
+
* (no nested paths), allowing alphanumeric stage names and the
|
|
98
|
+
* `{{STAGE}}` placeholder baked into `config.prod-aws.json` for the
|
|
99
|
+
* deploy pipeline to substitute. Optional — when omitted irc-framework
|
|
100
|
+
* builds `wss://host:port` with no path, which the browser normalises
|
|
101
|
+
* to `wss://host:port/` (CF root case).
|
|
91
102
|
*/
|
|
92
|
-
direct_path: z
|
|
103
|
+
direct_path: z
|
|
104
|
+
.string()
|
|
105
|
+
.regex(/^\/(?:[a-zA-Z0-9._-]+|\{\{[^}]+\}\})?$/u, 'direct_path must be / or /<stage>')
|
|
106
|
+
.optional(),
|
|
93
107
|
})
|
|
94
108
|
.passthrough();
|
|
95
109
|
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → static-HTML render pipeline for the `docs/` submodule.
|
|
3
|
+
*
|
|
4
|
+
* Renders every `docs/*.md` to a standalone HTML page under `dist/docs/` so
|
|
5
|
+
* the Worker `[assets]` binding serves them at `/docs/<name>.html` (and
|
|
6
|
+
* `/docs/` for the index — `Home.md` becomes `index.html`). Wired into the
|
|
7
|
+
* `apps/web` build by `scripts/build.mjs`.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from the build script so the rendering pipeline is unit-testable
|
|
10
|
+
* without running the (heavy) Kiwi upstream SPA build.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { marked } from 'marked';
|
|
17
|
+
|
|
18
|
+
// GFM is on by default in marked v18+, but pin it explicitly so a future
|
|
19
|
+
// upstream default flip can't silently change the rendered output. `async:
|
|
20
|
+
// false` makes `.parse()` return a plain string (its return type is the
|
|
21
|
+
// union `string | Promise<string>` — narrowed with a type assertion at the
|
|
22
|
+
// call site).
|
|
23
|
+
marked.use({ gfm: true, async: false });
|
|
24
|
+
|
|
25
|
+
/** Basename of the docs landing page (rendered to `index.html`). */
|
|
26
|
+
const HOME_SLUG = 'Home';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Render GitHub-flavored markdown to an HTML fragment (no surrounding
|
|
30
|
+
* `<html>`/`<body>` — {@link buildDocs} wraps the result in a full page).
|
|
31
|
+
*/
|
|
32
|
+
export function renderMarkdown(md: string): string {
|
|
33
|
+
return marked.parse(md, { async: false }) as string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Rewrite intra-docs links in already-rendered HTML so relative references
|
|
38
|
+
* resolve on the static site. Two source shapes are handled:
|
|
39
|
+
*
|
|
40
|
+
* 1. `[x](OtherDoc.md)` / `[x](./OtherDoc.md)` — Gitea-relative, emitted by
|
|
41
|
+
* marked as `<a href="./OtherDoc.md">`.
|
|
42
|
+
* 2. `[x](OtherDoc)` — Gitea-wiki "bare" link (no extension), used heavily
|
|
43
|
+
* in `Home.md` and `ADR-Index.md`.
|
|
44
|
+
*
|
|
45
|
+
* Only slugs present in `knownDocs` are rewritten, so external links, repo
|
|
46
|
+
* paths, and unknown targets are left untouched. Anchor fragments
|
|
47
|
+
* (`#section`) are preserved.
|
|
48
|
+
*/
|
|
49
|
+
export function rewriteDocLinks(html: string, knownDocs: Set<string>): string {
|
|
50
|
+
return html.replace(/href="([^"]*)"/g, (match, raw: string) => {
|
|
51
|
+
// Leave external URLs and pure-anchor links alone.
|
|
52
|
+
if (raw === '' || raw.startsWith('#') || /^(?:https?:|mailto:|tel:|data:)/i.test(raw)) {
|
|
53
|
+
return match;
|
|
54
|
+
}
|
|
55
|
+
const [pathPartRaw, ...fragParts] = raw.split('#');
|
|
56
|
+
const pathPart = pathPartRaw ?? '';
|
|
57
|
+
const fragment = fragParts.length > 0 ? `#${fragParts.join('#')}` : '';
|
|
58
|
+
const slug = pathPart.replace(/^\.\//, '').replace(/\.md$/i, '').trim();
|
|
59
|
+
if (slug === '' || !knownDocs.has(slug)) {
|
|
60
|
+
return match;
|
|
61
|
+
}
|
|
62
|
+
return `href="${slug}.html${fragment}"`;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BuildDocsOptions {
|
|
67
|
+
/** Source directory containing the `*.md` files (typically the `docs/` submodule). */
|
|
68
|
+
srcDir: string;
|
|
69
|
+
/** Output directory to write the rendered site (typically `dist/docs/`). */
|
|
70
|
+
outDir: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface BuildDocsResult {
|
|
74
|
+
/** Relative names of the files written under `outDir`. */
|
|
75
|
+
emitted: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render every `*.md` under `srcDir` into a standalone HTML page under
|
|
80
|
+
* `outDir`. `Home.md` becomes `index.html`; every other file `<Slug>.md`
|
|
81
|
+
* becomes `<Slug>.html`. Throws a clear error if `srcDir` is missing or
|
|
82
|
+
* empty — the build script surfaces this so a stale `docs/` submodule
|
|
83
|
+
* checkout fails loudly (mirroring the Kiwi `upstream/` guard).
|
|
84
|
+
*/
|
|
85
|
+
export async function buildDocs({ srcDir, outDir }: BuildDocsOptions): Promise<BuildDocsResult> {
|
|
86
|
+
if (!existsSync(srcDir)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Docs source missing at ${srcDir}. Run \`git submodule update --init docs\` first.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const entries = await readdir(srcDir);
|
|
92
|
+
const mdFiles = entries.filter((f) => f.toLowerCase().endsWith('.md'));
|
|
93
|
+
if (mdFiles.length === 0) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Docs source directory is empty at ${srcDir}. Run \`git submodule update --init docs\` first.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const knownSlugs = new Set(mdFiles.map((f) => f.replace(/\.md$/i, '')));
|
|
100
|
+
await mkdir(outDir, { recursive: true });
|
|
101
|
+
|
|
102
|
+
// Shared stylesheet — single source of truth for every page, mirroring the
|
|
103
|
+
// landing page's dark theme (CSS variables kept inline so the rendered
|
|
104
|
+
// docs work as static files served from any path).
|
|
105
|
+
await writeFile(path.join(outDir, 'docs.css'), DOCS_CSS, 'utf8');
|
|
106
|
+
|
|
107
|
+
const emitted: string[] = [];
|
|
108
|
+
for (const file of mdFiles) {
|
|
109
|
+
const slug = file.replace(/\.md$/i, '');
|
|
110
|
+
const md = await readFile(path.join(srcDir, file), 'utf8');
|
|
111
|
+
const body = rewriteDocLinks(renderMarkdown(md), knownSlugs);
|
|
112
|
+
const title = extractTitle(md) ?? slug;
|
|
113
|
+
const outName = slug === HOME_SLUG ? 'index.html' : `${slug}.html`;
|
|
114
|
+
const html = wrapPage(title, body, slug === HOME_SLUG);
|
|
115
|
+
await writeFile(path.join(outDir, outName), html, 'utf8');
|
|
116
|
+
emitted.push(outName);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { emitted };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Extract the first ATX `# Heading` as the page title; `undefined` if none. */
|
|
123
|
+
function extractTitle(md: string): string | undefined {
|
|
124
|
+
const match = md.match(/^#\s+(.+?)\s*$/m);
|
|
125
|
+
return match?.[1];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Escape the four significant HTML characters in a text string. */
|
|
129
|
+
function escapeHtml(s: string): string {
|
|
130
|
+
return s
|
|
131
|
+
.replace(/&/g, '&')
|
|
132
|
+
.replace(/</g, '<')
|
|
133
|
+
.replace(/>/g, '>')
|
|
134
|
+
.replace(/"/g, '"');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Wrap a rendered HTML fragment in a full page with the shared stylesheet. */
|
|
138
|
+
function wrapPage(title: string, body: string, isIndex: boolean): string {
|
|
139
|
+
const back = isIndex ? '' : '<p class="back"><a href="./index.html">Docs home</a></p>';
|
|
140
|
+
return `<!doctype html>
|
|
141
|
+
<html lang="en">
|
|
142
|
+
<head>
|
|
143
|
+
<meta charset="utf-8" />
|
|
144
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
145
|
+
<title>${escapeHtml(title)} — ServerlessIRCd Docs</title>
|
|
146
|
+
<link rel="stylesheet" href="./docs.css" />
|
|
147
|
+
</head>
|
|
148
|
+
<body>
|
|
149
|
+
<header>
|
|
150
|
+
<div class="container">
|
|
151
|
+
<h1><a href="./index.html">ServerlessIRCd Docs</a></h1>
|
|
152
|
+
</div>
|
|
153
|
+
</header>
|
|
154
|
+
<main class="container">
|
|
155
|
+
${back}
|
|
156
|
+
<article>
|
|
157
|
+
${body}
|
|
158
|
+
</article>
|
|
159
|
+
</main>
|
|
160
|
+
<footer>
|
|
161
|
+
<div class="container">
|
|
162
|
+
<p><a href="/">Back to landing</a></p>
|
|
163
|
+
</div>
|
|
164
|
+
</footer>
|
|
165
|
+
</body>
|
|
166
|
+
</html>
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const DOCS_CSS = `:root {
|
|
171
|
+
--bg: #0d1117;
|
|
172
|
+
--surface: #161b22;
|
|
173
|
+
--border: #30363d;
|
|
174
|
+
--text: #e6edf3;
|
|
175
|
+
--muted: #8b949e;
|
|
176
|
+
--accent: #58a6ff;
|
|
177
|
+
--accent-hover: #79c0ff;
|
|
178
|
+
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
|
|
179
|
+
"Liberation Mono", monospace;
|
|
180
|
+
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
|
|
181
|
+
sans-serif;
|
|
182
|
+
}
|
|
183
|
+
* { box-sizing: border-box; }
|
|
184
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
185
|
+
body {
|
|
186
|
+
margin: 0;
|
|
187
|
+
font-family: var(--sans);
|
|
188
|
+
line-height: 1.6;
|
|
189
|
+
color: var(--text);
|
|
190
|
+
background-color: var(--bg);
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
min-height: 100vh;
|
|
194
|
+
}
|
|
195
|
+
.container {
|
|
196
|
+
width: 100%;
|
|
197
|
+
max-width: 820px;
|
|
198
|
+
margin: 0 auto;
|
|
199
|
+
padding: 0 20px;
|
|
200
|
+
}
|
|
201
|
+
header {
|
|
202
|
+
padding: 24px 0 14px;
|
|
203
|
+
border-bottom: 1px solid var(--border);
|
|
204
|
+
}
|
|
205
|
+
header h1 {
|
|
206
|
+
margin: 0;
|
|
207
|
+
font-size: 1.4rem;
|
|
208
|
+
letter-spacing: -0.02em;
|
|
209
|
+
}
|
|
210
|
+
header h1 a {
|
|
211
|
+
color: var(--accent);
|
|
212
|
+
text-decoration: none;
|
|
213
|
+
}
|
|
214
|
+
header h1 a:hover { text-decoration: underline; }
|
|
215
|
+
main { flex: 1 0 auto; padding: 28px 0; }
|
|
216
|
+
article {
|
|
217
|
+
font-size: 1rem;
|
|
218
|
+
overflow-wrap: break-word;
|
|
219
|
+
}
|
|
220
|
+
article h1, article h2, article h3, article h4 {
|
|
221
|
+
letter-spacing: -0.01em;
|
|
222
|
+
line-height: 1.25;
|
|
223
|
+
margin-top: 1.6em;
|
|
224
|
+
margin-bottom: 0.6em;
|
|
225
|
+
}
|
|
226
|
+
article h1 { font-size: 1.7rem; margin-top: 0; }
|
|
227
|
+
article h2 { font-size: 1.35rem; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
|
|
228
|
+
article h3 { font-size: 1.12rem; }
|
|
229
|
+
article h4 { font-size: 1rem; color: var(--accent-hover); }
|
|
230
|
+
article p { margin: 0.8em 0; }
|
|
231
|
+
article a { color: var(--accent); text-decoration: none; }
|
|
232
|
+
article a:hover { text-decoration: underline; }
|
|
233
|
+
article ul, article ol { padding-left: 1.6em; }
|
|
234
|
+
article li { margin: 0.25em 0; }
|
|
235
|
+
article blockquote {
|
|
236
|
+
margin: 0.9em 0;
|
|
237
|
+
padding: 0.4em 1em;
|
|
238
|
+
border-left: 3px solid var(--border);
|
|
239
|
+
color: var(--muted);
|
|
240
|
+
}
|
|
241
|
+
article blockquote p { margin: 0.4em 0; }
|
|
242
|
+
article code {
|
|
243
|
+
font-family: var(--mono);
|
|
244
|
+
background-color: var(--surface);
|
|
245
|
+
padding: 2px 6px;
|
|
246
|
+
border-radius: 4px;
|
|
247
|
+
font-size: 0.9em;
|
|
248
|
+
}
|
|
249
|
+
article pre {
|
|
250
|
+
font-family: var(--mono);
|
|
251
|
+
background-color: var(--surface);
|
|
252
|
+
border: 1px solid var(--border);
|
|
253
|
+
border-radius: 8px;
|
|
254
|
+
padding: 14px 16px;
|
|
255
|
+
margin: 12px 0;
|
|
256
|
+
overflow-x: auto;
|
|
257
|
+
line-height: 1.5;
|
|
258
|
+
font-size: 0.88rem;
|
|
259
|
+
}
|
|
260
|
+
article pre code {
|
|
261
|
+
background: none; border: none; padding: 0; font-size: inherit;
|
|
262
|
+
}
|
|
263
|
+
article table {
|
|
264
|
+
border-collapse: collapse;
|
|
265
|
+
margin: 1em 0;
|
|
266
|
+
display: block;
|
|
267
|
+
overflow-x: auto;
|
|
268
|
+
}
|
|
269
|
+
article table th, article table td {
|
|
270
|
+
border: 1px solid var(--border);
|
|
271
|
+
padding: 6px 12px;
|
|
272
|
+
text-align: left;
|
|
273
|
+
}
|
|
274
|
+
article table th { background-color: var(--surface); }
|
|
275
|
+
article hr {
|
|
276
|
+
border: none;
|
|
277
|
+
border-top: 1px solid var(--border);
|
|
278
|
+
margin: 1.6em 0;
|
|
279
|
+
}
|
|
280
|
+
article img { max-width: 100%; }
|
|
281
|
+
.back { margin-top: 0; color: var(--muted); font-size: 0.92rem; }
|
|
282
|
+
.back a { color: var(--accent); }
|
|
283
|
+
footer {
|
|
284
|
+
flex-shrink: 0;
|
|
285
|
+
padding: 18px 0;
|
|
286
|
+
border-top: 1px solid var(--border);
|
|
287
|
+
color: var(--muted);
|
|
288
|
+
font-size: 0.9rem;
|
|
289
|
+
}
|
|
290
|
+
footer a { color: var(--muted); text-decoration: none; }
|
|
291
|
+
footer a:hover { color: var(--accent); }
|
|
292
|
+
`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"windowTitle": "ServerlessIRCd
|
|
2
|
+
"windowTitle": "ServerlessIRCd",
|
|
3
3
|
"startupScreen": "welcome",
|
|
4
4
|
"restricted": false,
|
|
5
5
|
"theme": "Dark",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
{ "name": "Elite", "url": "static/themes/elite" }
|
|
16
16
|
],
|
|
17
17
|
"startupOptions": {
|
|
18
|
-
"server": "{{
|
|
18
|
+
"server": "{{API_ID}}.execute-api.{{REGION}}.amazonaws.com",
|
|
19
|
+
"direct_path": "/{{STAGE}}",
|
|
19
20
|
"direct": true,
|
|
20
21
|
"tls": true,
|
|
21
22
|
"port": 443,
|