Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
python3 scripts/sync_strategy_switch_page_asset.py
git diff --exit-code -- web/strategy-switch-console/page_asset.js web/strategy-switch-console/strategy_profiles_asset.js
jq empty web/strategy-switch-console/strategy-profiles.example.json
node --experimental-default-type=module tests/strategy_switch_worker_validation.mjs
sed -n '/<script>/,/<\/script>/p' web/strategy-switch-console/index.html | sed '1d;$d' | node --check --input-type=commonjs
node --check --input-type=module < web/strategy-switch-console/page_asset.js
node --check --input-type=module < web/strategy-switch-console/strategy_profiles_asset.js
Expand Down
3 changes: 2 additions & 1 deletion docs/strategy_switch_admin_backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Goal: keep the open-source switch page public and read-only by default, while al
- Public access: unsigned visitors can view the page, but cannot dispatch the workflow.
- Allowed switch users/orgs: `ALLOWED_GITHUB_LOGINS`, `ALLOWED_GITHUB_ORGS`, KV `auth_config.allowed_logins`, KV `auth_config.allowed_orgs`, and all admins.
- Admin users/orgs: `STRATEGY_SWITCH_ADMIN_LOGINS`, `STRATEGY_SWITCH_ADMIN_ORGS`, KV `auth_config.admin_logins`, and KV `auth_config.admin_orgs`.
- Account dropdowns: KV `account_options` first, falling back to `STRATEGY_SWITCH_ACCOUNT_OPTIONS_JSON`.
- Account dropdowns and account strategy domains: KV `account_options` first, falling back to `STRATEGY_SWITCH_ACCOUNT_OPTIONS_JSON`.
- Audit log: each admin save appends to KV `audit_log`, capped at 50 entries.

## Cloudflare KV
Expand Down Expand Up @@ -42,6 +42,7 @@ Without the KV binding, `/admin` is read-only and the Worker falls back to secre
## Security Boundary

- The admin backend stores GitHub logins, GitHub organization names, and account routing metadata only.
- Account config may include `supported_domains`, such as `us_equity` or `hk_equity`, so unsupported strategies are filtered in the UI and rejected by the Worker.
- OAuth requests the `read:org` scope to verify membership in configured admin or allowlist organizations.
- Broker passwords, tokens, API keys, and cloud credentials stay out of this config.
- Admin writes use POST and same-origin checks.
Expand Down
3 changes: 2 additions & 1 deletion docs/strategy_switch_admin_backend.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- 公开访问:未登录用户只能看到只读切换页,不能触发 workflow。
- 可切换用户/组织:来自 `ALLOWED_GITHUB_LOGINS`、`ALLOWED_GITHUB_ORGS`、KV `auth_config.allowed_logins`、KV `auth_config.allowed_orgs` 和管理员配置。
- 管理员用户/组织:来自 `STRATEGY_SWITCH_ADMIN_LOGINS`、`STRATEGY_SWITCH_ADMIN_ORGS`、KV `auth_config.admin_logins` 和 KV `auth_config.admin_orgs`。
- 账号下拉:优先读取 KV `account_options`,没有 KV 配置时回退 `STRATEGY_SWITCH_ACCOUNT_OPTIONS_JSON`。
- 账号下拉和账号策略市场范围:优先读取 KV `account_options`,没有 KV 配置时回退 `STRATEGY_SWITCH_ACCOUNT_OPTIONS_JSON`。
- 审计:管理员保存配置后写入 KV `audit_log`,保留最近 50 条。

## Cloudflare KV
Expand Down Expand Up @@ -42,6 +42,7 @@ audit_log
## 安全边界

- 后台只保存 GitHub login、GitHub 组织名和账号路由信息。
- 账号配置可以包含 `supported_domains`,例如 `us_equity` 或 `hk_equity`,用于前端过滤不支持的策略,并由 Worker 后端再次拒绝非法组合。
- OAuth 会请求 `read:org` scope,用于校验登录用户是否属于配置的管理员组织或 allowlist 组织。
- 不保存 broker 密码、token、API key 或云密钥。
- 后台写操作使用 POST,并校验 Same-Origin。
Expand Down
124 changes: 124 additions & 0 deletions tests/strategy_switch_worker_validation.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import assert from "node:assert/strict";

import { __test } from "../web/strategy-switch-console/worker.js";

const strategyProfiles = __test.normalizeStrategyProfilesPayload(
[
{
profile: "tqqq_growth_income",
label: "TQQQ Growth Income",
domain: "us_equity",
runtime_enabled: true,
},
{
profile: "hk_low_vol_dividend_quality_snapshot",
label: "HK Low-Vol Dividend Quality Snapshot",
domain: "hk_equity",
runtime_enabled: true,
},
],
"test_strategy_profiles",
);

const accountOptions = __test.normalizeAccountOptionsPayload(
{
longbridge: [
{
key: "hk",
label: "hk",
target_name: "hk",
account_selector: "HK",
default_strategy_profile: "hk_low_vol_dividend_quality_snapshot",
},
{
key: "sg",
label: "sg",
target_name: "sg",
account_selector: "SG",
default_strategy_profile: "tqqq_growth_income",
},
],
ibkr: [
{
key: "u15998061",
label: "u15998061",
target_name: "u15998061",
account_selector: "U15998061",
deployment_selector: "live-u1599-tqqq",
account_scope: "live-u1599-tqqq",
service_name: "interactive-brokers-live-u1599-tqqq-service",
},
],
schwab: [
{
key: "default",
label: "default",
target_name: "default",
supported_domains: ["us_equity"],
},
],
firstrade: [
{
key: "default",
label: "default",
target_name: "default",
supported_domains: ["us_equity"],
},
],
},
"test_account_options",
);

assert.deepEqual(accountOptions.longbridge[0].supported_domains, ["hk_equity"]);
assert.deepEqual(accountOptions.longbridge[1].supported_domains, ["us_equity"]);
assert.deepEqual(accountOptions.ibkr[0].supported_domains, ["us_equity"]);

const longbridgeHk = __test.assertConfiguredAccount(
{
platform: "longbridge",
target_name: "hk",
account_selector: "HK",
strategy_profile: "hk_low_vol_dividend_quality_snapshot",
},
accountOptions,
);
__test.assertStrategyAllowedForAccount(
{
platform: "longbridge",
strategy_profile: "hk_low_vol_dividend_quality_snapshot",
},
longbridgeHk,
strategyProfiles,
);

const ibkrAccount = __test.assertConfiguredAccount(
{
platform: "ibkr",
target_name: "u15998061",
account_selector: "U15998061",
deployment_selector: "live-u1599-tqqq",
account_scope: "live-u1599-tqqq",
service_name: "interactive-brokers-live-u1599-tqqq-service",
strategy_profile: "tqqq_growth_income",
},
accountOptions,
);
__test.assertStrategyAllowedForAccount(
{
platform: "ibkr",
strategy_profile: "tqqq_growth_income",
},
ibkrAccount,
strategyProfiles,
);
assert.throws(
() => __test.assertStrategyAllowedForAccount(
{
platform: "ibkr",
strategy_profile: "hk_low_vol_dividend_quality_snapshot",
},
ibkrAccount,
strategyProfiles,
),
/not supported/,
);
10 changes: 6 additions & 4 deletions web/strategy-switch-console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,12 @@ Each account item supports:
"deployment_selector": "live-u1599-tqqq",
"account_scope": "live-u1599-tqqq",
"service_name": "interactive-brokers-live-u1599-tqqq-service",
"default_strategy_profile": "tqqq_growth_income"
"default_strategy_profile": "tqqq_growth_income",
"supported_domains": ["us_equity"]
}
```

The Worker validates dispatch inputs against this config. Keep only routing metadata here. Do not store broker passwords, tokens, or API keys in this config.
The Worker validates dispatch inputs against this config, including whether the selected strategy domain is supported by the selected account. Keep only routing metadata here. Do not store broker passwords, tokens, or API keys in this config.

`/api/strategy-profiles` returns the public live-enabled strategy catalog for the dropdown. It reads the KV `strategy_profiles` key first, then `STRATEGY_SWITCH_STRATEGY_PROFILES_JSON`, then `strategy-profiles.example.json`.

Expand All @@ -121,12 +122,13 @@ When adding or renaming a strategy profile:

- Add the runtime-enabled profile id and display label to `strategy-profiles.example.json`.
- Run `python3 scripts/sync_strategy_switch_page_asset.py` so `strategy_profiles_asset.js` is regenerated.
- Set each affected account's `default_strategy_profile` in `account-options.example.json` and the deployed KV account config.
- Set `domain` on each strategy profile. Current values are `us_equity` and `hk_equity`.
- Set each affected account's `default_strategy_profile` and `supported_domains` in `account-options.example.json` and the deployed KV account config.
- Update the deployed KV `strategy_profiles` key from `strategy-profiles.example.json`.
- Make sure the platform repository's current `RUNTIME_TARGET_JSON.strategy_profile` or account-specific `CLOUD_RUN_SERVICE_TARGETS_JSON` uses the same id.
- Use lower-case ids with letters, numbers, dot, underscore, dash, or equals only. Do not encode account names or secrets in profile ids.

The console can display a dynamically read unknown profile, but the profile should still be added to the catalog so the UI and docs stay aligned.
The console only allows live-enabled profiles whose `domain` is included in the selected account's `supported_domains`. If a profile is dynamically read from GitHub Variables but is missing from the catalog, add it to the catalog before switching to it.

## GitHub OAuth App

Expand Down
10 changes: 6 additions & 4 deletions web/strategy-switch-console/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,12 @@ wrangler secret put STRATEGY_SWITCH_ACCOUNT_OPTIONS_JSON < /tmp/strategy-switch-
"deployment_selector": "live-u1599-tqqq",
"account_scope": "live-u1599-tqqq",
"service_name": "interactive-brokers-live-u1599-tqqq-service",
"default_strategy_profile": "tqqq_growth_income"
"default_strategy_profile": "tqqq_growth_income",
"supported_domains": ["us_equity"]
}
```

Worker 会校验 dispatch 参数必须匹配这里的某个账号项。只放路由信息,不放 broker 密码、token、API key。
Worker 会校验 dispatch 参数必须匹配这里的某个账号项,也会校验所选策略的 `domain` 是否在该账号的 `supported_domains` 内。只放路由信息,不放 broker 密码、token、API key。

`/api/strategy-profiles` 会返回公开的 live-enabled 策略目录,用于生成策略下拉框。读取优先级是 KV `strategy_profiles`、`STRATEGY_SWITCH_STRATEGY_PROFILES_JSON`、`strategy-profiles.example.json`。

Expand All @@ -128,12 +129,13 @@ Worker 会校验 dispatch 参数必须匹配这里的某个账号项。只放路

- 在 `strategy-profiles.example.json` 增加 runtime-enabled profile id 和显示名称。
- 运行 `python3 scripts/sync_strategy_switch_page_asset.py` 重新生成 `strategy_profiles_asset.js`。
- 在 `account-options.example.json` 和已部署的 KV 账号配置里更新对应账号的 `default_strategy_profile`。
- 给每个策略 profile 设置 `domain`。当前支持 `us_equity` 和 `hk_equity`。
- 在 `account-options.example.json` 和已部署的 KV 账号配置里更新对应账号的 `default_strategy_profile` 和 `supported_domains`。
- 用 `strategy-profiles.example.json` 更新已部署 KV 的 `strategy_profiles` key。
- 确认平台仓库当前的 `RUNTIME_TARGET_JSON.strategy_profile` 或账号级 `CLOUD_RUN_SERVICE_TARGETS_JSON` 使用同一个 id。
- profile id 只使用小写字母、数字、点、下划线、短横线或等号。不要把账号名、密码、token、密钥信息写进 profile id。

切换页可以临时显示动态读取到但未登记的 profile,但后续仍应补进策略目录,保持 UI 和文档一致
切换页只允许选择 runtime-enabled 且 `domain` 属于当前账号 `supported_domains` 的策略。如果从 GitHub Variables 动态读到了未登记 profile,先补进策略目录再切换

## GitHub OAuth App

Expand Down
24 changes: 16 additions & 8 deletions web/strategy-switch-console/account-options.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,24 @@
"label": "hk",
"target_name": "hk",
"account_selector": "HK",
"default_strategy_profile": "hk_low_vol_dividend_quality_snapshot"
"default_strategy_profile": "hk_low_vol_dividend_quality_snapshot",
"supported_domains": ["hk_equity"]
},
{
"key": "sg",
"label": "sg",
"target_name": "sg",
"account_selector": "SG",
"default_strategy_profile": "tqqq_growth_income"
"default_strategy_profile": "tqqq_growth_income",
"supported_domains": ["us_equity"]
},
{
"key": "paper",
"label": "paper",
"target_name": "paper",
"account_selector": "PAPER",
"default_strategy_profile": "mega_cap_leader_rotation_top50_balanced"
"default_strategy_profile": "mega_cap_leader_rotation_top50_balanced",
"supported_domains": ["us_equity"]
}
],
"ibkr": [
Expand All @@ -31,7 +34,8 @@
"deployment_selector": "live-u1599-tqqq",
"account_scope": "live-u1599-tqqq",
"service_name": "interactive-brokers-live-u1599-tqqq-service",
"default_strategy_profile": "tqqq_growth_income"
"default_strategy_profile": "tqqq_growth_income",
"supported_domains": ["us_equity"]
},
{
"key": "u16608560",
Expand All @@ -41,7 +45,8 @@
"deployment_selector": "live-u1660-soxl",
"account_scope": "live-u1660-soxl",
"service_name": "interactive-brokers-live-u1660-soxl-service",
"default_strategy_profile": "soxl_soxx_trend_income"
"default_strategy_profile": "soxl_soxx_trend_income",
"supported_domains": ["us_equity"]
},
{
"key": "u18336562",
Expand All @@ -51,23 +56,26 @@
"deployment_selector": "live-u1833-smart-dca",
"account_scope": "live-u1833-smart-dca",
"service_name": "interactive-brokers-live-u1833-smart-dca-service",
"default_strategy_profile": "nasdaq_sp500_smart_dca"
"default_strategy_profile": "nasdaq_sp500_smart_dca",
"supported_domains": ["us_equity"]
}
],
"schwab": [
{
"key": "default",
"label": "default",
"target_name": "default",
"default_strategy_profile": "soxl_soxx_trend_income"
"default_strategy_profile": "soxl_soxx_trend_income",
"supported_domains": ["us_equity"]
}
],
"firstrade": [
{
"key": "default",
"label": "default",
"target_name": "default",
"default_strategy_profile": "mega_cap_leader_rotation_top50_balanced"
"default_strategy_profile": "mega_cap_leader_rotation_top50_balanced",
"supported_domains": ["us_equity"]
}
]
}
Loading