serverless-ircd 0.6.0 → 0.7.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 +6 -1
- package/.gitmodules +3 -0
- package/CHANGELOG.md +214 -0
- package/README.md +110 -23
- package/apps/aws-stack/package.json +1 -1
- package/apps/cf-tcp-container/package.json +1 -1
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/src/origin-allowlist.ts +99 -0
- package/apps/cf-worker/src/worker.ts +89 -9
- package/apps/cf-worker/tests/fixtures/web-dist/app/index.html +18 -0
- package/apps/cf-worker/tests/fixtures/web-dist/index.html +18 -0
- package/apps/cf-worker/tests/origin-allowlist.test.ts +101 -0
- package/apps/cf-worker/tests/smoke.test.ts +28 -2
- package/apps/cf-worker/tests/ws-origin.test.ts +89 -0
- package/apps/cf-worker/wrangler.test.toml +23 -0
- package/apps/cf-worker/wrangler.toml +54 -0
- package/apps/local-cli/package.json +1 -1
- package/apps/web/landing/index.html +217 -0
- package/apps/web/package.json +28 -0
- package/apps/web/scripts/build.mjs +125 -0
- package/apps/web/src/build-env.ts +43 -0
- package/apps/web/src/config-schema.ts +138 -0
- package/apps/web/static/config.json +28 -0
- package/apps/web/static/config.prod.json +28 -0
- package/apps/web/static/config.staging.json +28 -0
- package/apps/web/tests/build-env.test.ts +63 -0
- package/apps/web/tests/build-smoke.test.ts +103 -0
- package/apps/web/tests/config-schema.test.ts +432 -0
- package/apps/web/tests/workspace.test.ts +12 -0
- package/apps/web/tsconfig.json +10 -0
- package/apps/web/upstream/.browserslistrc +3 -0
- package/apps/web/upstream/.editorconfig +9 -0
- package/apps/web/upstream/.eslintignore +3 -0
- package/apps/web/upstream/.eslintrc.js +90 -0
- package/apps/web/upstream/.github/FUNDING.yml +12 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- package/apps/web/upstream/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/apps/web/upstream/.github/workflows/push.yml +26 -0
- package/apps/web/upstream/.github/workflows/release.yml +33 -0
- package/apps/web/upstream/.github/workflows/release_manual.yml +55 -0
- package/apps/web/upstream/.prettierrc.js +9 -0
- package/apps/web/upstream/.stylelintignore +5 -0
- package/apps/web/upstream/.stylelintrc.js +21 -0
- package/apps/web/upstream/LICENSE +201 -0
- package/apps/web/upstream/README.md +76 -0
- package/apps/web/upstream/babel.config.js +25 -0
- package/apps/web/upstream/crowdin.yml +3 -0
- package/apps/web/upstream/index.html +35 -0
- package/apps/web/upstream/jest.config.js +7 -0
- package/apps/web/upstream/jsdoc.json +22 -0
- package/apps/web/upstream/package.json +80 -0
- package/apps/web/upstream/scripts/pre-commit +43 -0
- package/apps/web/upstream/src/components/App.vue +516 -0
- package/apps/web/upstream/src/components/AppSettings.vue +587 -0
- package/apps/web/upstream/src/components/AutoComplete.vue +284 -0
- package/apps/web/upstream/src/components/Avatar.vue +105 -0
- package/apps/web/upstream/src/components/AwayStatusIndicator.vue +77 -0
- package/apps/web/upstream/src/components/BufferKey.vue +121 -0
- package/apps/web/upstream/src/components/BufferSettings.vue +97 -0
- package/apps/web/upstream/src/components/Captcha.vue +85 -0
- package/apps/web/upstream/src/components/ChannelBanlist.vue +96 -0
- package/apps/web/upstream/src/components/ChannelInfo.vue +159 -0
- package/apps/web/upstream/src/components/ChannelInvitelist.vue +276 -0
- package/apps/web/upstream/src/components/ChannelList.vue +345 -0
- package/apps/web/upstream/src/components/Container.vue +401 -0
- package/apps/web/upstream/src/components/ContainerHeader.vue +522 -0
- package/apps/web/upstream/src/components/ControlInput.vue +1016 -0
- package/apps/web/upstream/src/components/LoadingAnimation.vue +104 -0
- package/apps/web/upstream/src/components/MediaViewer.vue +199 -0
- package/apps/web/upstream/src/components/MessageInfo.vue +192 -0
- package/apps/web/upstream/src/components/MessageList.vue +1028 -0
- package/apps/web/upstream/src/components/MessageListAvatar.vue +20 -0
- package/apps/web/upstream/src/components/MessageListMessageCompact.vue +361 -0
- package/apps/web/upstream/src/components/MessageListMessageInline.vue +257 -0
- package/apps/web/upstream/src/components/MessageListMessageModern.vue +430 -0
- package/apps/web/upstream/src/components/NetworkSettings.vue +528 -0
- package/apps/web/upstream/src/components/Nicklist.vue +309 -0
- package/apps/web/upstream/src/components/NicklistUser.vue +181 -0
- package/apps/web/upstream/src/components/NotConnected.vue +271 -0
- package/apps/web/upstream/src/components/SelfUser.vue +283 -0
- package/apps/web/upstream/src/components/ServerSelector.vue +162 -0
- package/apps/web/upstream/src/components/ServerView.vue +126 -0
- package/apps/web/upstream/src/components/SettingsAdvanced.vue +232 -0
- package/apps/web/upstream/src/components/SettingsAliases.vue +121 -0
- package/apps/web/upstream/src/components/Sidebar.vue +429 -0
- package/apps/web/upstream/src/components/SidebarAboutBuffer.vue +288 -0
- package/apps/web/upstream/src/components/SidebarState.vue +145 -0
- package/apps/web/upstream/src/components/StartupError.vue +35 -0
- package/apps/web/upstream/src/components/StateBrowser.vue +420 -0
- package/apps/web/upstream/src/components/StateBrowserBuffer.vue +124 -0
- package/apps/web/upstream/src/components/StateBrowserNetwork.vue +823 -0
- package/apps/web/upstream/src/components/StateBrowserUsermenu.vue +162 -0
- package/apps/web/upstream/src/components/TypingStatusIndicator.vue +34 -0
- package/apps/web/upstream/src/components/TypingUsersList.vue +56 -0
- package/apps/web/upstream/src/components/UrlEmbed.vue +149 -0
- package/apps/web/upstream/src/components/UserBox.vue +728 -0
- package/apps/web/upstream/src/components/inputtools/Emoji.vue +67 -0
- package/apps/web/upstream/src/components/inputtools/TextStyle.vue +142 -0
- package/apps/web/upstream/src/components/startups/CommonLayout.vue +208 -0
- package/apps/web/upstream/src/components/startups/CustomServer.vue +495 -0
- package/apps/web/upstream/src/components/startups/KiwiBnc.vue +361 -0
- package/apps/web/upstream/src/components/startups/Personal.vue +317 -0
- package/apps/web/upstream/src/components/startups/Welcome.vue +544 -0
- package/apps/web/upstream/src/components/startups/ZncLogin.vue +229 -0
- package/apps/web/upstream/src/components/utils/InputConfirm.vue +68 -0
- package/apps/web/upstream/src/components/utils/InputPrompt.vue +118 -0
- package/apps/web/upstream/src/components/utils/InputText.vue +172 -0
- package/apps/web/upstream/src/components/utils/IrcInput.vue +579 -0
- package/apps/web/upstream/src/components/utils/PluginWrapper.vue +26 -0
- package/apps/web/upstream/src/components/utils/TabbedView.vue +149 -0
- package/apps/web/upstream/src/components/utils/TransitionExpand.vue +97 -0
- package/apps/web/upstream/src/helpers/Colours.js +128 -0
- package/apps/web/upstream/src/helpers/IrcdDiffs.js +26 -0
- package/apps/web/upstream/src/helpers/Md5.js +193 -0
- package/apps/web/upstream/src/helpers/Misc.js +401 -0
- package/apps/web/upstream/src/helpers/TextFormatting.js +237 -0
- package/apps/web/upstream/src/libs/AliasRewriter.js +157 -0
- package/apps/web/upstream/src/libs/AudioManager.js +60 -0
- package/apps/web/upstream/src/libs/BouncerMiddleware.js +247 -0
- package/apps/web/upstream/src/libs/BouncerProvider.js +551 -0
- package/apps/web/upstream/src/libs/ChathistoryMiddleware.js +153 -0
- package/apps/web/upstream/src/libs/ConfigLoader.js +94 -0
- package/apps/web/upstream/src/libs/EmojiProvider.js +46 -0
- package/apps/web/upstream/src/libs/GlobalApi.js +346 -0
- package/apps/web/upstream/src/libs/IPC.js +51 -0
- package/apps/web/upstream/src/libs/InputHandler.js +921 -0
- package/apps/web/upstream/src/libs/IrcClient.js +1506 -0
- package/apps/web/upstream/src/libs/Logger.js +71 -0
- package/apps/web/upstream/src/libs/Message.js +169 -0
- package/apps/web/upstream/src/libs/MessageFormatter.js +364 -0
- package/apps/web/upstream/src/libs/MessageParser.js +249 -0
- package/apps/web/upstream/src/libs/Notifications.js +78 -0
- package/apps/web/upstream/src/libs/ServerConnection.js +270 -0
- package/apps/web/upstream/src/libs/ServerSession.js +102 -0
- package/apps/web/upstream/src/libs/SoundBleep.js +20 -0
- package/apps/web/upstream/src/libs/StatePersistence.js +90 -0
- package/apps/web/upstream/src/libs/ThemeManager.js +128 -0
- package/apps/web/upstream/src/libs/TypingMiddleware.js +105 -0
- package/apps/web/upstream/src/libs/WindowTitle.js +63 -0
- package/apps/web/upstream/src/libs/batchedAdd.js +74 -0
- package/apps/web/upstream/src/libs/bufferTools.js +177 -0
- package/apps/web/upstream/src/libs/polyfill/Element.closest.js +18 -0
- package/apps/web/upstream/src/libs/renderers/Html.js +113 -0
- package/apps/web/upstream/src/libs/settingTools.js +31 -0
- package/apps/web/upstream/src/libs/state/BufferState.js +703 -0
- package/apps/web/upstream/src/libs/state/NetworkState.js +158 -0
- package/apps/web/upstream/src/libs/state/UserState.js +89 -0
- package/apps/web/upstream/src/libs/state/common.js +26 -0
- package/apps/web/upstream/src/libs/state.js +961 -0
- package/apps/web/upstream/src/libs/storage/Local.js +51 -0
- package/apps/web/upstream/src/main.js +566 -0
- package/apps/web/upstream/src/res/autocompleteCommands.js +31 -0
- package/apps/web/upstream/src/res/configTemplates.js +373 -0
- package/apps/web/upstream/src/res/globalStyle.css +277 -0
- package/apps/web/upstream/src/res/kiwiLoadingLogo.png +0 -0
- package/apps/web/upstream/src/res/locales/app.af-ZA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ar-SA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bg-BG.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.bs-BA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ca-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.cs-CZ.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.da-DK.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.de-DE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.dev.po +1246 -0
- package/apps/web/upstream/src/res/locales/app.el-GR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.en-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-419.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-AR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-EM.po +536 -0
- package/apps/web/upstream/src/res/locales/app.es-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.es-US.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.eu-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fi-FI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.fr-FR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.gl-ES.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.he-IL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hi-IN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.hu-HU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.id-ID.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.it-IT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ja-JP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ko-KR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.nl-NL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.no-NO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pl-PL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-BR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.pt-PT.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ro-RO.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.ru-RU.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sl-SI.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sq-AL.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sr-SP.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.sv-SE.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.tr-TR.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.uk-UA.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.vi-VN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-CN.po +1145 -0
- package/apps/web/upstream/src/res/locales/app.zh-TW.po +1145 -0
- package/apps/web/upstream/src/res/localesList.json +27 -0
- package/apps/web/upstream/src/res/logo.png +0 -0
- package/apps/web/upstream/src/thirdparty/about.html +43 -0
- package/apps/web/upstream/src/thirdparty/index.js +5 -0
- package/apps/web/upstream/src/thirdparty/kiwiirccom.vue +185 -0
- package/apps/web/upstream/static/config.json +28 -0
- package/apps/web/upstream/static/emoticons/smile.png +0 -0
- package/apps/web/upstream/static/favicon.png +0 -0
- package/apps/web/upstream/static/highlight.mp3 +0 -0
- package/apps/web/upstream/static/highlight.ogg +0 -0
- package/apps/web/upstream/static/locales/.gitignore +2 -0
- package/apps/web/upstream/static/plugins/customise.html.example +10 -0
- package/apps/web/upstream/static/themes/coffee/theme.css +145 -0
- package/apps/web/upstream/static/themes/common/base.css +1064 -0
- package/apps/web/upstream/static/themes/dark/theme.css +196 -0
- package/apps/web/upstream/static/themes/default/theme.css +92 -0
- package/apps/web/upstream/static/themes/elite/theme.css +248 -0
- package/apps/web/upstream/static/themes/grayfox/theme.css +143 -0
- package/apps/web/upstream/static/themes/nightswatch/theme.css +367 -0
- package/apps/web/upstream/static/themes/osprey/theme.css +115 -0
- package/apps/web/upstream/static/themes/radioactive/theme.css +1211 -0
- package/apps/web/upstream/static/themes/sky/theme.css +107 -0
- package/apps/web/upstream/tests/unit/BatchAdd.spec.js +174 -0
- package/apps/web/upstream/tests/unit/MessageParser.spec.js +125 -0
- package/apps/web/upstream/tests/unit/Misc.spec.js +24 -0
- package/apps/web/upstream/tests/unit/NetworkState.spec.js +58 -0
- package/apps/web/upstream/tests/unit/StartupError.spec.js +20 -0
- package/apps/web/upstream/vue.config.js +150 -0
- package/apps/web/upstream/yarn.lock +9767 -0
- package/apps/web/vitest.config.ts +13 -0
- package/biome.json +1 -0
- package/docs/Home.md +1 -0
- package/docs/WebClientGuide.md +535 -0
- package/package.json +2 -2
- package/packages/aws-adapter/package.json +1 -1
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/tests/cf-harness.ts +12 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/reports/mutation/mutation.html +342 -0
- package/packages/irc-core/tests/batch.test.ts +15 -0
- package/packages/irc-core/tests/numerics.test.ts +13 -0
- package/packages/irc-core/tests/parser.test.ts +109 -0
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/scenarios.ts +24 -2
- package/tools/ci-hardening/package.json +1 -1
- package/tools/load-test/package.json +1 -1
- package/tools/tcp-ws-forwarder/package.json +1 -1
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
* • Cross-DO coordination (registry + channel) is owned by
|
|
14
14
|
* `ConnectionDO` and `CfRuntime`; the worker never touches it.
|
|
15
15
|
*
|
|
16
|
+
* Request routing (non-WS):
|
|
17
|
+
* • `/health` — plaintext human-readable liveness string (the previous
|
|
18
|
+
* catch-all response, now scoped to an explicit path so the root URL
|
|
19
|
+
* is free for the landing page).
|
|
20
|
+
* • everything else — falls through to the `[assets]` binding, which
|
|
21
|
+
* serves the Kiwi SPA at `/app/` and the landing page at `/` from
|
|
22
|
+
* `apps/web/dist`. The asset layer is configured in `wrangler.toml`.
|
|
23
|
+
*
|
|
16
24
|
* The DO classes are re-exported so `wrangler.toml`'s
|
|
17
25
|
* `durable_objects.bindings.class_name` can resolve them in this
|
|
18
26
|
* module's scope (wrangler requires the class to be exported from the
|
|
@@ -29,6 +37,7 @@ import {
|
|
|
29
37
|
type Env,
|
|
30
38
|
RegistryDO,
|
|
31
39
|
} from '@serverless-ircd/cf-adapter';
|
|
40
|
+
import { decideOrigin, parseWebOrigins } from './origin-allowlist';
|
|
32
41
|
|
|
33
42
|
// Re-exported for wrangler DO binding resolution. The class names below
|
|
34
43
|
// MUST match `class_name` in `wrangler.toml`.
|
|
@@ -36,9 +45,68 @@ export { ChannelDO, ChannelRegistryDO, ConnectionDO, RegistryDO };
|
|
|
36
45
|
export type { Env };
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
48
|
+
* Production {@link Env} for the worker entry. Extends the cf-adapter
|
|
49
|
+
* {@link Env} with the `ASSETS` binding (the static-asset `Fetcher` for
|
|
50
|
+
* `/app/` SPA + `/` landing page). The cf-adapter's internal types stay
|
|
51
|
+
* asset-agnostic; only the edge entry knows about the asset layer.
|
|
52
|
+
*/
|
|
53
|
+
export interface WorkerEnv extends Env {
|
|
54
|
+
/**
|
|
55
|
+
* Static-asset `Fetcher` backed by `apps/web/dist` (production) or
|
|
56
|
+
* `tests/fixtures/web-dist` (test). Non-WS, non-`/health` requests are
|
|
57
|
+
* forwarded here so the asset layer serves the SPA and landing page
|
|
58
|
+
* without the worker inspecting asset paths.
|
|
59
|
+
*/
|
|
60
|
+
ASSETS: Fetcher;
|
|
61
|
+
/**
|
|
62
|
+
* Optional comma-separated allowlist of web origins permitted to open
|
|
63
|
+
* WebSocket upgrades (CSWSH defense — browsers send `Origin` on every
|
|
64
|
+
* cross-origin upgrade; WebSocket upgrades do not otherwise enforce
|
|
65
|
+
* same-origin).
|
|
66
|
+
*
|
|
67
|
+
* When set, only listed origins proceed. When unset/empty (the default),
|
|
68
|
+
* the Worker auto-derives the expected origin from the request's own
|
|
69
|
+
* `Host` header (same-origin enforcement) — no per-env configuration
|
|
70
|
+
* needed. Set this only for cross-origin deployments where the SPA is
|
|
71
|
+
* served from a different domain than the Worker.
|
|
72
|
+
*
|
|
73
|
+
* Non-browser clients (curl, WeeChat, the `tcp-ws-forwarder`) omit
|
|
74
|
+
* `Origin` and pass through unchanged regardless of mode.
|
|
75
|
+
*/
|
|
76
|
+
WEB_ORIGINS: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Human-readable liveness string surfaced at `GET /health`. Previously the
|
|
81
|
+
* catch-all response for every non-WS request; now scoped to an explicit
|
|
82
|
+
* path so `/` can serve the landing page from the `[assets]` binding.
|
|
83
|
+
*/
|
|
84
|
+
const HEALTH_BODY =
|
|
85
|
+
'ServerlessIRCd (Cloudflare Worker). Connect via WebSocket to begin an IRC session.';
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Response body sent when a browser-sent `Origin` is not allowed.
|
|
89
|
+
* Mentioned in `docs/web-client.md` as the canonical CSWSH rejection.
|
|
90
|
+
*/
|
|
91
|
+
const ORIGIN_DENIED_BODY = 'WebSocket upgrade refused: the request Origin is not allowed.';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Response body sent when neither the allowlist nor the request origin could
|
|
95
|
+
* be determined. In practice this never fires on Cloudflare (the platform
|
|
96
|
+
* always sets `Host`), but the pure policy function keeps the case for
|
|
97
|
+
* defensive completeness.
|
|
98
|
+
*/
|
|
99
|
+
const ORIGIN_UNCONFIGURED_BODY =
|
|
100
|
+
'WebSocket upgrades refused: could not determine the request origin and WEB_ORIGINS is not configured.';
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Default edge handler.
|
|
104
|
+
*
|
|
105
|
+
* Routing order:
|
|
106
|
+
* 1. WebSocket upgrade → CSWSH `Origin` check, then per-connection
|
|
107
|
+
* `ConnectionDO`.
|
|
108
|
+
* 2. `GET /health` → plaintext liveness response.
|
|
109
|
+
* 3. everything else → `env.ASSETS` (Kiwi SPA at `/app/`, landing at `/`).
|
|
42
110
|
*
|
|
43
111
|
* The connection id is allocated here via `crypto.randomUUID()` (the
|
|
44
112
|
* Web Crypto API is sync-available in Workers for RFC-4122 v4 UUIDs).
|
|
@@ -47,19 +115,31 @@ export type { Env };
|
|
|
47
115
|
* it before falling back to a fresh uuid.
|
|
48
116
|
*/
|
|
49
117
|
export default {
|
|
50
|
-
async fetch(request: Request, env:
|
|
118
|
+
async fetch(request: Request, env: WorkerEnv): Promise<Response> {
|
|
51
119
|
if (request.headers.get('Upgrade') === 'websocket') {
|
|
120
|
+
const allowlist = parseWebOrigins(env.WEB_ORIGINS);
|
|
121
|
+
const url = new URL(request.url);
|
|
122
|
+
const requestOrigin = `${url.protocol}//${url.host}`;
|
|
123
|
+
const decision = decideOrigin(request.headers.get('Origin'), allowlist, requestOrigin);
|
|
124
|
+
if (decision !== 'allow') {
|
|
125
|
+
const body = decision === 'fail-closed' ? ORIGIN_UNCONFIGURED_BODY : ORIGIN_DENIED_BODY;
|
|
126
|
+
return new Response(body, {
|
|
127
|
+
status: 403,
|
|
128
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
52
131
|
const connId = crypto.randomUUID();
|
|
53
132
|
const id = env.CONNECTION_DO.idFromName(connId);
|
|
54
133
|
const stub = env.CONNECTION_DO.get(id);
|
|
55
134
|
return stub.fetch(request);
|
|
56
135
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{
|
|
136
|
+
const url = new URL(request.url);
|
|
137
|
+
if (url.pathname === '/health') {
|
|
138
|
+
return new Response(HEALTH_BODY, {
|
|
60
139
|
status: 200,
|
|
61
140
|
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
62
|
-
}
|
|
63
|
-
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return env.ASSETS.fetch(request);
|
|
64
144
|
},
|
|
65
145
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!--
|
|
3
|
+
Test-only fixture mirroring the real `apps/web/dist/app/index.html` produced
|
|
4
|
+
by the Kiwi SPA build (`pnpm --filter web build`). The cf-worker smoke suite
|
|
5
|
+
points its `[assets]` binding here so the worker's fall-through routing
|
|
6
|
+
(WS → ConnectionDO, /health → plaintext, else → ASSETS) is exercised without
|
|
7
|
+
depending on the (slow, submodule-heavy) Kiwi build. Production points at
|
|
8
|
+
`apps/web/dist`; this fixture only stands in for the test run.
|
|
9
|
+
-->
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="utf-8" />
|
|
13
|
+
<title>KiwiIRC</title>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div id="app"></div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!--
|
|
3
|
+
Test-only fixture standing in for the landing page shipped by the
|
|
4
|
+
`apps/web` build (`apps/web/dist/index.html`). Production will serve a
|
|
5
|
+
real landing page at `/`; the cf-worker smoke suite asserts that `GET /`
|
|
6
|
+
is served by the `[assets]` binding rather than the legacy plaintext
|
|
7
|
+
health response (now exclusive to `/health`).
|
|
8
|
+
-->
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8" />
|
|
12
|
+
<title>ServerlessIRCd</title>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<h1>ServerlessIRCd</h1>
|
|
16
|
+
<p>A serverless IRC daemon.</p>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the WebSocket `Origin` allowlist helpers.
|
|
3
|
+
*
|
|
4
|
+
* The worker's CSWSH defense is split into two pure functions
|
|
5
|
+
* (`parseWebOrigins`, `decideOrigin`) so the policy logic is exhaustively
|
|
6
|
+
* coverable without driving a Durable Object through `worker.fetch`. The
|
|
7
|
+
* integration-side assertions (status codes against the real handler) live
|
|
8
|
+
* in `ws-origin.test.ts`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
import { decideOrigin, parseWebOrigins } from '../src/origin-allowlist';
|
|
13
|
+
|
|
14
|
+
describe('parseWebOrigins', () => {
|
|
15
|
+
it('splits a comma-separated list into a set of normalized origins', () => {
|
|
16
|
+
expect(parseWebOrigins('https://example.com, https://app.example.com')).toEqual(
|
|
17
|
+
new Set(['https://example.com', 'https://app.example.com']),
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('lowercases origins for case-insensitive comparison', () => {
|
|
22
|
+
expect(parseWebOrigins('https://Example.COM/Path').has('https://example.com/path')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('drops whitespace-only and empty entries', () => {
|
|
26
|
+
expect(parseWebOrigins('https://example.com, , ,https://b.example.com')).toEqual(
|
|
27
|
+
new Set(['https://example.com', 'https://b.example.com']),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns an empty set for an empty/undefined var (fail-closed sentinel)', () => {
|
|
32
|
+
expect(parseWebOrigins('').size).toBe(0);
|
|
33
|
+
expect(parseWebOrigins(' ').size).toBe(0);
|
|
34
|
+
expect(parseWebOrigins(undefined).size).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('decideOrigin', () => {
|
|
39
|
+
const allowlist = parseWebOrigins('https://example.com, https://app.example.com');
|
|
40
|
+
|
|
41
|
+
// ---- missing Origin = non-browser client (curl, WeeChat, tcp-ws-forwarder)
|
|
42
|
+
|
|
43
|
+
it('allows a missing Origin header (non-browser client)', () => {
|
|
44
|
+
expect(decideOrigin(null, allowlist, 'https://example.com')).toBe('allow');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('allows an empty Origin header (treated like a missing header)', () => {
|
|
48
|
+
expect(decideOrigin('', allowlist, 'https://example.com')).toBe('allow');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ---- explicit allowlist (WEB_ORIGINS is set)
|
|
52
|
+
|
|
53
|
+
it('allows an Origin that is in the allowlist', () => {
|
|
54
|
+
expect(decideOrigin('https://example.com', allowlist, 'https://example.com')).toBe('allow');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('allows an Origin that differs only by case', () => {
|
|
58
|
+
expect(decideOrigin('https://EXAMPLE.com', allowlist, 'https://example.com')).toBe('allow');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('denies an Origin that is not in the allowlist', () => {
|
|
62
|
+
expect(decideOrigin('https://evil.example.net', allowlist, 'https://example.com')).toBe('deny');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('denies an Origin that matches requestOrigin but is NOT in the allowlist (allowlist wins)', () => {
|
|
66
|
+
// WEB_ORIGINS takes precedence: if the operator pinned an explicit list,
|
|
67
|
+
// same-origin is NOT automatically allowed unless the origin is listed.
|
|
68
|
+
const strict = parseWebOrigins('https://only-this.com');
|
|
69
|
+
expect(decideOrigin('https://example.com', strict, 'https://example.com')).toBe('deny');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ---- auto-derive (WEB_ORIGINS is empty — same-origin enforcement)
|
|
73
|
+
|
|
74
|
+
it('auto-allows a same-origin Origin when the allowlist is empty', () => {
|
|
75
|
+
expect(decideOrigin('https://example.com', new Set(), 'https://example.com')).toBe('allow');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('auto-denies a cross-origin Origin when the allowlist is empty', () => {
|
|
79
|
+
expect(decideOrigin('https://evil.example.net', new Set(), 'https://example.com')).toBe('deny');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('auto-derive is case-insensitive', () => {
|
|
83
|
+
expect(decideOrigin('https://EXAMPLE.com', new Set(), 'https://example.com')).toBe('allow');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('auto-allows a missing Origin even when the allowlist is empty (non-browser client)', () => {
|
|
87
|
+
expect(decideOrigin(null, new Set(), 'https://example.com')).toBe('allow');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ---- fail-closed (cannot determine same-origin AND no allowlist)
|
|
91
|
+
|
|
92
|
+
it('fail-closes when a browser sends an Origin but neither allowlist nor requestOrigin exist', () => {
|
|
93
|
+
expect(decideOrigin('https://example.com', new Set(), null)).toBe('fail-closed');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('still allows a missing Origin even when neither allowlist nor requestOrigin exist (non-browser client)', () => {
|
|
97
|
+
// Non-browser clients never send Origin; they pass through regardless of
|
|
98
|
+
// configuration state. Only browser-sent Origins can be fail-closed.
|
|
99
|
+
expect(decideOrigin(null, new Set(), null)).toBe('allow');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -30,6 +30,12 @@ declare global {
|
|
|
30
30
|
SERVER_NAME: string;
|
|
31
31
|
NETWORK_NAME: string;
|
|
32
32
|
MOTD_LINES: string;
|
|
33
|
+
// Static-assets binding (`[assets]` in `wrangler.test.toml`, pointed at
|
|
34
|
+
// `tests/fixtures/web-dist`). Production points at `apps/web/dist`; the
|
|
35
|
+
// fixture mirrors the `app/index.html` + `index.html` layout so the
|
|
36
|
+
// worker's fall-through routing is exercised without depending on the
|
|
37
|
+
// Kiwi SPA build.
|
|
38
|
+
ASSETS: Fetcher;
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
}
|
|
@@ -47,13 +53,33 @@ function numericEquals(line: string, code: string): boolean {
|
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
describe('cf-worker deploy glue', () => {
|
|
50
|
-
it('
|
|
51
|
-
const response = await worker.fetch(new Request('https://irc.example.com/'), env);
|
|
56
|
+
it('GET /health returns a human-readable plaintext health response', async () => {
|
|
57
|
+
const response = await worker.fetch(new Request('https://irc.example.com/health'), env);
|
|
52
58
|
expect(response.status).toBe(200);
|
|
59
|
+
expect(response.headers.get('content-type')).toBe('text/plain; charset=utf-8');
|
|
53
60
|
const body = await response.text();
|
|
54
61
|
expect(body.toLowerCase()).toContain('serverlessircd');
|
|
55
62
|
});
|
|
56
63
|
|
|
64
|
+
it('GET /app/ serves the SPA index from the [assets] binding', async () => {
|
|
65
|
+
const response = await worker.fetch(new Request('https://irc.example.com/app/'), env);
|
|
66
|
+
expect(response.status).toBe(200);
|
|
67
|
+
const body = await response.text();
|
|
68
|
+
// The fixture SPA HTML carries this marker so the test asserts the
|
|
69
|
+
// assets binding (not the /health plaintext branch) produced the body.
|
|
70
|
+
expect(body).toContain('<title>KiwiIRC</title>');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('GET / (landing page) is served by the [assets] binding, not the health plaintext', async () => {
|
|
74
|
+
const response = await worker.fetch(new Request('https://irc.example.com/'), env);
|
|
75
|
+
expect(response.status).toBe(200);
|
|
76
|
+
const body = await response.text();
|
|
77
|
+
// The fixture landing page carries this marker; the old health
|
|
78
|
+
// plaintext response is now exclusively at /health.
|
|
79
|
+
expect(body).toContain('<title>ServerlessIRCd</title>');
|
|
80
|
+
expect(body).not.toContain('Connect via WebSocket');
|
|
81
|
+
});
|
|
82
|
+
|
|
57
83
|
it('completes CONNECT → REGISTER → JOIN → PRIVMSG → QUIT', async () => {
|
|
58
84
|
// ----- CONNECT: WebSocket upgrade through the worker's fetch handler.
|
|
59
85
|
const upgrade = new Request('https://irc.example.com/', {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the WebSocket `Origin` policy (CSWSH defense).
|
|
3
|
+
*
|
|
4
|
+
* Drives the production worker entry (`src/worker.ts`) end-to-end through
|
|
5
|
+
* `worker.fetch` to assert the `Origin` header policy is enforced at the
|
|
6
|
+
* edge, before any upgrade reaches `ConnectionDO`. The pure policy logic
|
|
7
|
+
* is covered separately in `origin-allowlist.test.ts`; these tests assert
|
|
8
|
+
* the wiring (status codes + that the DO is never reached on a rejection).
|
|
9
|
+
*
|
|
10
|
+
* Two modes are exercised:
|
|
11
|
+
*
|
|
12
|
+
* • Explicit allowlist — `WEB_ORIGINS` is set in `wrangler.test.toml`
|
|
13
|
+
* `[vars]` as `https://example.com`. Only listed origins proceed.
|
|
14
|
+
*
|
|
15
|
+
* • Auto-derive (same-origin) — per-test override blanks `WEB_ORIGINS`;
|
|
16
|
+
* the Worker compares the browser's `Origin` against the request's own
|
|
17
|
+
* host. Same-origin upgrades proceed; cross-origin are rejected.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { env } from 'cloudflare:test';
|
|
21
|
+
import { describe, expect, it } from 'vitest';
|
|
22
|
+
import worker from '../src/worker';
|
|
23
|
+
|
|
24
|
+
declare global {
|
|
25
|
+
namespace Cloudflare {
|
|
26
|
+
interface Env {
|
|
27
|
+
WEB_ORIGINS: string;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const UPGRADE_URL = 'https://irc.example.com/';
|
|
33
|
+
/** The request's own origin — what auto-derive compares Origin against. */
|
|
34
|
+
const SELF_ORIGIN = 'https://irc.example.com';
|
|
35
|
+
|
|
36
|
+
function upgradeRequest(origin?: string): Request {
|
|
37
|
+
const headers: Record<string, string> = { Upgrade: 'websocket' };
|
|
38
|
+
if (origin !== undefined) headers.Origin = origin;
|
|
39
|
+
return new Request(UPGRADE_URL, { headers });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('WS Origin policy — explicit allowlist (WEB_ORIGINS set)', () => {
|
|
43
|
+
it('proceeds (101) when the Origin is in WEB_ORIGINS', async () => {
|
|
44
|
+
const response = await worker.fetch(upgradeRequest('https://example.com'), env);
|
|
45
|
+
expect(response.status).toBe(101);
|
|
46
|
+
response.webSocket?.accept();
|
|
47
|
+
response.webSocket?.close();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('rejects with 403 when the Origin is not in WEB_ORIGINS', async () => {
|
|
51
|
+
const response = await worker.fetch(upgradeRequest('https://evil.example.net'), env);
|
|
52
|
+
expect(response.status).toBe(403);
|
|
53
|
+
expect(response.webSocket).toBeNull();
|
|
54
|
+
const body = await response.text();
|
|
55
|
+
expect(body.toLowerCase()).toContain('origin');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('proceeds (101) when the Origin header is missing (non-browser client)', async () => {
|
|
59
|
+
const response = await worker.fetch(upgradeRequest(), env);
|
|
60
|
+
expect(response.status).toBe(101);
|
|
61
|
+
response.webSocket?.accept();
|
|
62
|
+
response.webSocket?.close();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('WS Origin policy — auto-derive (WEB_ORIGINS unset → same-origin)', () => {
|
|
67
|
+
/** Env with WEB_ORIGINS blanked so the Worker falls back to auto-derive. */
|
|
68
|
+
const autoEnv = { ...env, WEB_ORIGINS: '' };
|
|
69
|
+
|
|
70
|
+
it('proceeds (101) for a same-origin upgrade', async () => {
|
|
71
|
+
const response = await worker.fetch(upgradeRequest(SELF_ORIGIN), autoEnv);
|
|
72
|
+
expect(response.status).toBe(101);
|
|
73
|
+
response.webSocket?.accept();
|
|
74
|
+
response.webSocket?.close();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('rejects with 403 for a cross-origin upgrade', async () => {
|
|
78
|
+
const response = await worker.fetch(upgradeRequest('https://evil.example.net'), autoEnv);
|
|
79
|
+
expect(response.status).toBe(403);
|
|
80
|
+
expect(response.webSocket).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('proceeds (101) when the Origin header is missing (non-browser client)', async () => {
|
|
84
|
+
const response = await worker.fetch(upgradeRequest(), autoEnv);
|
|
85
|
+
expect(response.status).toBe(101);
|
|
86
|
+
response.webSocket?.accept();
|
|
87
|
+
response.webSocket?.close();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -11,10 +11,33 @@ main = "src/worker.ts"
|
|
|
11
11
|
compatibility_date = "2024-11-01"
|
|
12
12
|
compatibility_flags = ["nodejs_compat"]
|
|
13
13
|
|
|
14
|
+
# Static-assets binding. Production points at `../../apps/web/dist` (the Kiwi
|
|
15
|
+
# SPA + landing page built by `apps/web`); the test config points at a fixture
|
|
16
|
+
# so the smoke suite does not depend on the (slow) Kiwi upstream build. The
|
|
17
|
+
# fixture mirrors the `app/index.html` + `index.html` layout; the worker's
|
|
18
|
+
# fall-through routing (`/health` → plaintext, else → ASSETS) is what's under
|
|
19
|
+
# test, not the SPA contents.
|
|
20
|
+
[assets]
|
|
21
|
+
directory = "./tests/fixtures/web-dist"
|
|
22
|
+
binding = "ASSETS"
|
|
23
|
+
# `none` returns 404 for unmatched paths — the SPA lives at `/app/` and the
|
|
24
|
+
# landing page at `/`, both with real `index.html` files, so no SPA fallback
|
|
25
|
+
# is required.
|
|
26
|
+
not_found_handling = "none"
|
|
27
|
+
# Mirror production: route every request through the Worker first so WS
|
|
28
|
+
# upgrades at `/` aren't intercepted by the asset platform (which would
|
|
29
|
+
# serve `index.html` with 200 instead of a 101 upgrade).
|
|
30
|
+
run_worker_first = true
|
|
31
|
+
|
|
14
32
|
[vars]
|
|
15
33
|
SERVER_NAME = "irc.example.com"
|
|
16
34
|
NETWORK_NAME = "ServerlessIRCd"
|
|
17
35
|
MOTD_LINES = "Welcome to the cf-worker smoke test."
|
|
36
|
+
# CSWSH allowlist. The ws-origin integration suite asserts an upgrade from
|
|
37
|
+
# `https://example.com` proceeds and `https://evil.example.net` is rejected
|
|
38
|
+
# (the fail-closed branch overrides this per request via `{ ...env,
|
|
39
|
+
# WEB_ORIGINS: '' }`).
|
|
40
|
+
WEB_ORIGINS = "https://example.com"
|
|
18
41
|
|
|
19
42
|
[[durable_objects.bindings]]
|
|
20
43
|
name = "CONNECTION_DO"
|
|
@@ -49,6 +49,15 @@ invocation_logs = true
|
|
|
49
49
|
## (`loadServerConfigFromCfEnv`) fails fast at boot when it is unset.
|
|
50
50
|
## Production deployments MUST override it to the public hostname
|
|
51
51
|
## clients see in numerics (001, 005, etc.).
|
|
52
|
+
##
|
|
53
|
+
## `WEB_ORIGINS` is the OPTIONAL comma-separated allowlist of web origins
|
|
54
|
+
## permitted to open WebSocket upgrades (CSWSH defense). When set, only
|
|
55
|
+
## listed origins proceed. When unset/empty (the default), the Worker
|
|
56
|
+
## auto-derives the expected origin from the request's own `Host` header
|
|
57
|
+
## (same-origin enforcement) — no per-env config needed. Set this only for
|
|
58
|
+
## cross-origin deployments where the SPA is served from a different domain
|
|
59
|
+
## than the Worker. Non-browser clients (curl, WeeChat, tcp-ws-forwarder)
|
|
60
|
+
## omit `Origin` and pass through unchanged regardless.
|
|
52
61
|
[vars]
|
|
53
62
|
SERVER_NAME = "irc.your-domain.invalid"
|
|
54
63
|
NETWORK_NAME = "ServerlessIRCd"
|
|
@@ -57,6 +66,41 @@ Welcome to ServerlessIRCd.
|
|
|
57
66
|
This is a deployed Cloudflare Worker.
|
|
58
67
|
"""
|
|
59
68
|
|
|
69
|
+
## ---------------------------------------------------------------------------
|
|
70
|
+
## Static assets — Kiwi SPA + landing page.
|
|
71
|
+
## ---------------------------------------------------------------------------
|
|
72
|
+
## The `[assets]` binding serves `apps/web/dist` (built by `pnpm --filter web
|
|
73
|
+
## build`). The non-WS, non-`/health` branch of `worker.ts`'s `fetch` falls
|
|
74
|
+
## through to this binding, so:
|
|
75
|
+
## • `/app/` → the Kiwi IRC SPA (`dist/app/index.html` + its
|
|
76
|
+
## hashed assets under `/app/assets/...`).
|
|
77
|
+
## • `/` → the landing page (`dist/index.html`, shipped by
|
|
78
|
+
## the `apps/web` build).
|
|
79
|
+
## • `/health` → plaintext liveness (handled by the worker, not
|
|
80
|
+
## assets).
|
|
81
|
+
## The deploy workflow (`deploy-cf.yml`) runs `pnpm build` before `wrangler
|
|
82
|
+
## deploy` so `apps/web/dist` is freshly populated. `wrangler dev` serves the
|
|
83
|
+
## same asset layer against the locally-built `dist`.
|
|
84
|
+
[assets]
|
|
85
|
+
## Relative to this file (`apps/cf-worker/`), so `../../apps/web/dist`.
|
|
86
|
+
directory = "../../apps/web/dist"
|
|
87
|
+
## Expose the asset `Fetcher` on `env.ASSETS` (see `WorkerEnv` in `worker.ts`).
|
|
88
|
+
binding = "ASSETS"
|
|
89
|
+
## Return 404 for unmatched paths. The SPA lives at `/app/` and the landing
|
|
90
|
+
## page at `/`; both are concrete `index.html` files, so no SPA fallback is
|
|
91
|
+
## required. Kiwi uses query/hash-based client routing, not path routing.
|
|
92
|
+
not_found_handling = "none"
|
|
93
|
+
## Route EVERY request through the Worker before the asset platform serves a
|
|
94
|
+
## matching static file. Without this, the default (`run_worker_first = false`)
|
|
95
|
+
## causes the asset platform to serve `dist/index.html` at `/` with 200 OK for
|
|
96
|
+
## WebSocket upgrade requests — the Worker's `Upgrade: websocket` check never
|
|
97
|
+
## fires and the client sees "HTTP error: 200 OK" instead of a 101 upgrade.
|
|
98
|
+
## The WS endpoint and the landing page share path `/`, so a path-pattern
|
|
99
|
+
## exclusion can't tell them apart; the Worker must run first for everything.
|
|
100
|
+
## The Worker falls through to `env.ASSETS.fetch(request)` for non-WS,
|
|
101
|
+
## non-`/health` requests, so static content is unaffected.
|
|
102
|
+
run_worker_first = true
|
|
103
|
+
|
|
60
104
|
## ---------------------------------------------------------------------------
|
|
61
105
|
## Staging environment.
|
|
62
106
|
## ---------------------------------------------------------------------------
|
|
@@ -79,6 +123,16 @@ This is a deployed Cloudflare Worker.
|
|
|
79
123
|
This Worker was deployed by CI from the main branch.
|
|
80
124
|
"""
|
|
81
125
|
|
|
126
|
+
# Staging serves the same `apps/web/dist` asset layer. Wrangler requires the
|
|
127
|
+
# per-env assets block so `--env staging` deploys both the Worker and its
|
|
128
|
+
# assets in a single `wrangler deploy --env staging` (the same `pnpm build`
|
|
129
|
+
# populates `dist/` ahead of the deploy step).
|
|
130
|
+
[env.staging.assets]
|
|
131
|
+
directory = "../../apps/web/dist"
|
|
132
|
+
binding = "ASSETS"
|
|
133
|
+
not_found_handling = "none"
|
|
134
|
+
run_worker_first = true
|
|
135
|
+
|
|
82
136
|
## ---------------------------------------------------------------------------
|
|
83
137
|
## D1 database — SASL account persistence.
|
|
84
138
|
## ---------------------------------------------------------------------------
|