diff --git a/.env.example b/.env.example index c24b05e..952e94d 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,7 @@ APP_ORIGIN=https://your-worker.example.workers.dev ACCESS_TEAM_DOMAIN=your-team.cloudflareaccess.com ACCESS_AUD=your-access-application-audience # github 模式填写 OAuth App 和管理员数字 ID;本地 D1 的 workspace_state 也要选择 github。 -# GITHUB_CLIENT_ID= -# GITHUB_CLIENT_SECRET= -# GITHUB_ADMIN_ID= +# GH_CLIENT_ID= +# GH_CLIENT_SECRET= +# GH_ADMIN_ID= # ENCRYPTION_KEY 必须为 32 字节安全随机数的 Base64;本文件只用于本地开发,生产值在 Cloudflare Worker 控制台中保存为 Secret。 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7a4f8b4..b50d45e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -42,10 +42,10 @@ jobs: CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID || secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} AUTH_PROVIDER: ${{ vars.AUTH_PROVIDER || 'cloudflare' }} - GITHUB_CLIENT_ID: ${{ vars.GITHUB_CLIENT_ID }} - GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }} - GITHUB_ADMIN: ${{ vars.GITHUB_ADMIN }} - GITHUB_ADMIN_ID: ${{ vars.GITHUB_ADMIN_ID }} + GH_CLIENT_ID: ${{ vars.GH_CLIENT_ID }} + GH_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }} + GH_ADMIN: ${{ vars.GH_ADMIN }} + GH_ADMIN_ID: ${{ vars.GH_ADMIN_ID }} ADMIN_EMAIL: ${{ inputs.admin_email || secrets.ADMIN_EMAIL || vars.ADMIN_EMAIL }} ACCESS_IDP_IDS: ${{ vars.ACCESS_IDP_IDS }} WORKER_NAME: ${{ vars.WORKER_NAME }} diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index a12c7ee..5b557f9 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -13,13 +13,15 @@ | `CUSTOM_DOMAIN` | Variable | 推荐填写实际主机名 | 推荐填写实际主机名 | | `PREVIEW_DOMAIN` | Variable | 可选;仅部署独立预览 Worker 时使用 | 可选;仅部署独立预览 Worker 时使用 | | `ADMIN_EMAIL` | Variable | 管理员邮箱 | 不需要 | -| `GITHUB_CLIENT_ID` | Variable | 不需要 | OAuth App 的 Client ID | -| `GITHUB_CLIENT_SECRET` | Secret | 不需要 | OAuth App 的 Client Secret | -| `GITHUB_ADMIN` | Variable | 不需要 | 首次启用时用于解析数字 ID 的 GitHub 用户名 | -| `GITHUB_ADMIN_ID` | Variable | 不需要 | 通常不填;显式更换管理员时填写新的数字用户 ID | +| `GH_CLIENT_ID` | Variable | 不需要 | OAuth App 的 Client ID | +| `GH_CLIENT_SECRET` | Secret | 不需要 | OAuth App 的 Client Secret | +| `GH_ADMIN` | Variable | 不需要 | 首次启用时用于解析数字 ID 的 GitHub 用户名 | +| `GH_ADMIN_ID` | Variable | 不需要 | 通常不填;显式更换管理员时填写新的数字用户 ID | `CUSTOM_DOMAIN` 只填完整主机名,不带 `https://`、路径或通配符。域名必须由部署账户的 Cloudflare Zone 管理;Action 会自动绑定 Worker,Cloudflare 负责 DNS 与证书。需要使用免费 `workers.dev` 地址时将它留空,不能填写 `*.workers.dev`。 +从旧版 GitHub 登录配置升级时,先在仓库 Actions 设置中将 `GITHUB_CLIENT_ID`、`GITHUB_ADMIN`(如有)分别改为 `GH_CLIENT_ID`、`GH_ADMIN`,并将原 `GITHUB_CLIENT_SECRET` 的值重新保存为 **Secret** `GH_CLIENT_SECRET`。若曾显式设置 `GITHUB_ADMIN_ID`,改为 `GH_ADMIN_ID`。完成这些配置后再合并或拉取新版并运行 Deploy;旧 Secret 无法从 GitHub 读回,不要为迁移更换 OAuth App 或 `ENCRYPTION_KEY`。已有管理员数字 ID 会从 D1 沿用。 + 敏感值不要放 Variable。工作流只校验所选方式的配置,另一种方式的旧配置不会参与认证。 ## Cloudflare 模式准备 @@ -37,12 +39,12 @@ Access 应用的策略是唯一授权名单。可以在控制台添加多个明 1. 打开 GitHub **设置(Settings)> 开发者设置(Developer settings)> OAuth 应用(OAuth Apps)> 新建 OAuth 应用(New OAuth App)**。 2. **应用名称(Application name)**自定;**主页 URL(Homepage URL)**填 EdgeSSH 地址,**授权回调 URL(Authorization callback URL)**填 `https://你的入口/auth/callback`。 3. 保存 Client ID,生成一个 Client Secret,按上表分别保存到 Actions Variable 和 Secret。 -4. 设置 `AUTH_PROVIDER=github`、`CUSTOM_DOMAIN=你的主机名`、首次使用的 `GITHUB_ADMIN=你的GitHub用户名`,保存 Cloudflare API Token,然后运行 **Actions > Deploy**。使用 `workers.dev` 时才省略 `CUSTOM_DOMAIN`;邮箱输入框留空。 +4. 设置 `AUTH_PROVIDER=github`、`CUSTOM_DOMAIN=你的主机名`、首次使用的 `GH_ADMIN=你的GitHub用户名`,保存 Cloudflare API Token,然后运行 **Actions > Deploy**。使用 `workers.dev` 时才省略 `CUSTOM_DOMAIN`;邮箱输入框留空。 5. 若首次部署前不知道入口,可先为 OAuth App 使用占位 URL;部署后将 Action 摘要中的正式入口与回调地址复制回 OAuth App 设置,再登录。 GitHub OAuth App 必须由用户在 GitHub 创建;普通 GitHub Token 没有官方“创建 OAuth App”的 REST 接口,工作流不会假装自动完成它。 -登录时仅读取 GitHub 公开身份,不申请仓库、组织或私人邮箱权限。首次部署将用户名解析为数字用户 ID 并固定在 D1;后续普通部署直接复用该 ID,不会因用户名改名或易主而改变管理员。仅在明确更换管理员时设置 `GITHUB_ADMIN_ID` 为新的数字 ID 并部署,部署会同时撤销旧会话;完成后可保留该值作为显式配置。 +登录时仅读取 GitHub 公开身份,不申请仓库、组织或私人邮箱权限。首次部署将用户名解析为数字用户 ID 并固定在 D1;后续普通部署直接复用该 ID,不会因用户名改名或易主而改变管理员。仅在明确更换管理员时设置 `GH_ADMIN_ID` 为新的数字 ID 并部署,部署会同时撤销旧会话;完成后可保留该值作为显式配置。 ## API Token 权限 @@ -118,7 +120,7 @@ EdgeSSH-Auto-Update: true | `D1_DATABASE_NAME` | Variable | `-accounts` | | `D1_DATABASE_ID` | Variable | 指定已有 D1 UUID,不填则按名称查找 | | `ACCESS_IDP_IDS` | Variable | 新应用采用的 IdP UUID,多个用逗号分隔 | -| `GITHUB_ADMIN_ID` | Variable | 仅显式更换 GitHub 管理员时填写数字用户 ID | +| `GH_ADMIN_ID` | Variable | 仅显式更换 GitHub 管理员时填写数字用户 ID | | `PREVIEW_DOMAIN` | Variable | 仅 `部署预览 Worker` 使用;留空为 `-preview.<账户子域>.workers.dev` | | `ENCRYPTION_KEY` | Secret,仅恢复/迁移使用 | 仅 Worker 尚无密钥时使用;已有密钥不会覆盖 | @@ -158,7 +160,7 @@ Cloudflare 模式仍可用 `ACCESS_IDP_IDS` 为新 Access 应用选择现成 IdP - **更换域名后无法登录**:带邮箱重新运行以配置新 hostname 的应用;不要仅改路由而沿用旧 AUD。 - **OTP 未收到**:确认输入邮箱完全匹配 Allow 策略,检查垃圾邮件。GitHub 等其他 IdP 的账户邮箱同样必须匹配授权。 - **GitHub 回调失败**:检查 OAuth App 回调地址是否精确为 `https://实际入口/auth/callback`,Client ID/Secret 是否来自同一 OAuth App;重新从首页登录,不复用旧回调链接。 -- **GitHub 拒绝管理员**:`GITHUB_ADMIN` 应填个人用户名,不是邮箱或组织;用该账号重新授权。 +- **GitHub 拒绝管理员**:`GH_ADMIN` 应填个人用户名,不是邮箱或组织;用该账号重新授权。 - 手工配置、截图与 Access JWT 排查见 [Zero Trust 指南](docs/ZERO_TRUST.md)。 ## 验收清单 diff --git a/scripts/deploy.ts b/scripts/deploy.ts index db7d566..b878b5f 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -59,7 +59,11 @@ async function main(): Promise { throw new Error('已部署的认证方式无效,停止自动覆盖。'); } const deployedGitHubId = workspace.githubAdminId - ?? (settings.authProvider === 'github' ? await readWorkerVariable(api, settings, 'GITHUB_ADMIN_ID') : undefined); + ?? (settings.authProvider === 'github' + ? await readWorkerVariable(api, settings, 'GH_ADMIN_ID') + // 旧版 Worker 若尚未把管理员 ID 写入 D1,迁移时沿用其原绑定。 + ?? await readWorkerVariable(api, settings, 'GITHUB_ADMIN_ID') + : undefined); if (deployedGitHubId && !/^[1-9]\d*$/.test(deployedGitHubId)) { throw new Error('已部署的 GitHub 管理员数字 ID 无效,停止自动覆盖。'); } diff --git a/scripts/deployment-auth.ts b/scripts/deployment-auth.ts index ff6b4c9..8073631 100644 --- a/scripts/deployment-auth.ts +++ b/scripts/deployment-auth.ts @@ -10,7 +10,7 @@ interface AuthenticationOptions { } export function requiredAuthSecrets(provider: DeploymentSettings['authProvider']): string[] { - return provider === 'github' ? ['GITHUB_CLIENT_SECRET'] : ['ACCESS_TEAM_DOMAIN', 'ACCESS_AUD']; + return provider === 'github' ? ['GH_CLIENT_SECRET'] : ['ACCESS_TEAM_DOMAIN', 'ACCESS_AUD']; } export async function prepareAuthentication( @@ -61,19 +61,19 @@ export async function prepareAuthentication( } const fixed = settings.githubAdminId ?? options.fixedGithubAdminId; if (fixed) { - return { githubAdminId: fixed, secrets: { GITHUB_CLIENT_SECRET: settings.secrets.GITHUB_CLIENT_SECRET! } }; + return { githubAdminId: fixed, secrets: { GH_CLIENT_SECRET: settings.secrets.GH_CLIENT_SECRET! } }; } if (!settings.githubAdmin) { - throw new Error('首次配置 GitHub 登录需要 GITHUB_ADMIN 用户名,或显式设置 GITHUB_ADMIN_ID。'); + throw new Error('首次配置 GitHub 登录需要 GH_ADMIN 用户名,或显式设置 GH_ADMIN_ID。'); } const response = await fetcher(`https://api.github.com/users/${settings.githubAdmin}`, { headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'EdgeSSH' }, signal: AbortSignal.timeout(30_000), }); - if (!response.ok) throw new Error(`无法确认 GITHUB_ADMIN(HTTP ${response.status}),请检查用户名或稍后重试。`); + if (!response.ok) throw new Error(`无法确认 GH_ADMIN(HTTP ${response.status}),请检查用户名或稍后重试。`); const user = await response.json() as { id: number; type: string }; if (user.type !== 'User' || !Number.isSafeInteger(user.id) || user.id <= 0) { - throw new Error('GITHUB_ADMIN 必须是个人 GitHub 账号,不能是组织。'); + throw new Error('GH_ADMIN 必须是个人 GitHub 账号,不能是组织。'); } - return { githubAdminId: String(user.id), secrets: { GITHUB_CLIENT_SECRET: settings.secrets.GITHUB_CLIENT_SECRET! } }; + return { githubAdminId: String(user.id), secrets: { GH_CLIENT_SECRET: settings.secrets.GH_CLIENT_SECRET! } }; } diff --git a/scripts/deployment-config.ts b/scripts/deployment-config.ts index 239d0f6..7bae9b0 100644 --- a/scripts/deployment-config.ts +++ b/scripts/deployment-config.ts @@ -1,6 +1,6 @@ import type { TomlTable } from 'smol-toml'; -export const runtimeSecretNames = ['ENCRYPTION_KEY', 'ACCESS_TEAM_DOMAIN', 'ACCESS_AUD', 'GITHUB_CLIENT_SECRET'] as const; +export const runtimeSecretNames = ['ENCRYPTION_KEY', 'ACCESS_TEAM_DOMAIN', 'ACCESS_AUD', 'GH_CLIENT_SECRET'] as const; export type RuntimeSecrets = Record<(typeof runtimeSecretNames)[number], string>; export interface DeploymentSettings { @@ -32,7 +32,7 @@ export function readDeploymentSettings( ): DeploymentSettings { const authProvider = env.AUTH_PROVIDER?.trim() || 'cloudflare'; if (authProvider !== 'cloudflare' && authProvider !== 'github') throw new Error('AUTH_PROVIDER 只能是 cloudflare 或 github。'); - const required = ['CLOUDFLARE_API_TOKEN', ...(authProvider === 'github' ? ['GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET'] : [])]; + const required = ['CLOUDFLARE_API_TOKEN', ...(authProvider === 'github' ? ['GH_CLIENT_ID', 'GH_CLIENT_SECRET'] : [])]; const missing = required.filter((name) => !env[name]?.trim()); if (missing.length) throw new Error(`缺少部署配置:${missing.join(', ')}。请在 GitHub Actions 中配置。`); @@ -80,17 +80,17 @@ export function readDeploymentSettings( if (identityProviderIds.some((id) => !/^[a-f0-9-]{36}$/i.test(id))) { throw new Error('ACCESS_IDP_IDS 必须是逗号分隔的 Cloudflare 身份提供程序 UUID。'); } - const githubClientId = authProvider === 'github' ? env.GITHUB_CLIENT_ID!.trim() : undefined; - const githubAdmin = authProvider === 'github' ? env.GITHUB_ADMIN?.trim().toLowerCase() || undefined : undefined; + const githubClientId = authProvider === 'github' ? env.GH_CLIENT_ID!.trim() : undefined; + const githubAdmin = authProvider === 'github' ? env.GH_ADMIN?.trim().toLowerCase() || undefined : undefined; if (githubAdmin && !/^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/.test(githubAdmin)) { - throw new Error('GITHUB_ADMIN 必须是一个 GitHub 用户名。'); + throw new Error('GH_ADMIN 必须是一个 GitHub 用户名。'); } - const githubAdminId = authProvider === 'github' ? env.GITHUB_ADMIN_ID?.trim() || undefined : undefined; + const githubAdminId = authProvider === 'github' ? env.GH_ADMIN_ID?.trim() || undefined : undefined; if (githubAdminId && !/^[1-9]\d*$/.test(githubAdminId)) { - throw new Error('GITHUB_ADMIN_ID 必须是 GitHub 个人账号的数字用户 ID。'); + throw new Error('GH_ADMIN_ID 必须是 GitHub 个人账号的数字用户 ID。'); } const selectedSecrets = authProvider === 'github' - ? ['ENCRYPTION_KEY', 'GITHUB_CLIENT_SECRET'] : ['ENCRYPTION_KEY', 'ACCESS_TEAM_DOMAIN', 'ACCESS_AUD']; + ? ['ENCRYPTION_KEY', 'GH_CLIENT_SECRET'] : ['ENCRYPTION_KEY', 'ACCESS_TEAM_DOMAIN', 'ACCESS_AUD']; const secrets = Object.fromEntries(selectedSecrets .filter((name) => env[name]?.trim()) .map((name) => [name, env[name]!.trim()])) as Partial; @@ -135,8 +135,8 @@ export function createDeploymentConfig( ...(runtime ? { APP_ORIGIN: `https://${runtime.hostname}` } : {}), ...(runtime?.previewOrigin ? { PREVIEW_ORIGIN: runtime.previewOrigin } : {}), ...(settings.authProvider === 'github' ? { - GITHUB_CLIENT_ID: settings.githubClientId!, - ...(runtime?.githubAdminId ? { GITHUB_ADMIN_ID: runtime.githubAdminId } : {}), + GH_CLIENT_ID: settings.githubClientId!, + ...(runtime?.githubAdminId ? { GH_ADMIN_ID: runtime.githubAdminId } : {}), } : {}), }, ...(settings.customDomain ? { routes: [{ pattern: settings.customDomain, custom_domain: true }] } : {}), diff --git a/src/accounts/github-auth.ts b/src/accounts/github-auth.ts index a222679..b53aac8 100644 --- a/src/accounts/github-auth.ts +++ b/src/accounts/github-auth.ts @@ -9,10 +9,10 @@ const flowCookie = '__Host-edgessh-oauth'; const sessionSeconds = 8 * 60 * 60; const flowSeconds = 10 * 60; const encoder = new TextEncoder(); -type GitHubConfig = Env & Required>; +type GitHubConfig = Env & Required>; function config(env: Env): GitHubConfig { - if (!env.APP_ORIGIN || !env.GITHUB_CLIENT_ID || !env.GITHUB_CLIENT_SECRET || !env.GITHUB_ADMIN_ID || !env.ENCRYPTION_KEY) { + if (!env.APP_ORIGIN || !env.GH_CLIENT_ID || !env.GH_CLIENT_SECRET || !env.GH_ADMIN_ID || !env.ENCRYPTION_KEY) { throw new APIError('管理员尚未配置 GitHub 登录。', 503); } return env as GitHubConfig; @@ -37,7 +37,7 @@ async function signingKey(secret: string): Promise { } function audience(env: GitHubConfig, purpose: string): string { - return `edgessh:${purpose}:${env.GITHUB_CLIENT_ID}:${env.GITHUB_ADMIN_ID}`; + return `edgessh:${purpose}:${env.GH_CLIENT_ID}:${env.GH_ADMIN_ID}`; } async function sign(env: GitHubConfig, purpose: string, payload: JWTPayload, seconds: number): Promise { @@ -68,7 +68,7 @@ export async function githubLogin(request: Request, env: Env): Promise const url = new URL('https://github.com/login/oauth/authorize'); // 只验证公开身份,不申请仓库、邮箱或组织权限。 url.search = new URLSearchParams({ - client_id: settings.GITHUB_CLIENT_ID, redirect_uri: `${settings.APP_ORIGIN}/auth/callback`, + client_id: settings.GH_CLIENT_ID, redirect_uri: `${settings.APP_ORIGIN}/auth/callback`, state, code_challenge: challenge, code_challenge_method: 'S256', scope: '', }).toString(); const flow = await sign(settings, 'flow', { state, verifier }, flowSeconds); @@ -94,7 +94,7 @@ export async function githubCallback(request: Request, env: Env, fetcher: typeof method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ - client_id: settings.GITHUB_CLIENT_ID, client_secret: settings.GITHUB_CLIENT_SECRET, + client_id: settings.GH_CLIENT_ID, client_secret: settings.GH_CLIENT_SECRET, redirect_uri: `${settings.APP_ORIGIN}/auth/callback`, code, code_verifier: flow.verifier, }), signal: AbortSignal.timeout(15_000), @@ -109,7 +109,7 @@ export async function githubCallback(request: Request, env: Env, fetcher: typeof if (!userResponse.ok) throw new APIError('无法验证 GitHub 身份,请重试。', 502); const user = await userResponse.json() as { id: number; login: string }; // 以数字 ID 授权,不信任可改名的用户名,也不把任意 GitHub 用户视为管理员。 - if (!Number.isSafeInteger(user.id) || String(user.id) !== settings.GITHUB_ADMIN_ID || typeof user.login !== 'string') { + if (!Number.isSafeInteger(user.id) || String(user.id) !== settings.GH_ADMIN_ID || typeof user.login !== 'string') { throw new APIError('此 GitHub 账号不是本实例的管理员。', 403); } const workspace = await workspaceState(env); @@ -132,7 +132,7 @@ export async function githubAccount(request: Request, env: Env, authRevision: nu if (!token) throw new APIError('请先使用 GitHub 登录。', 401); try { const payload = await verify(settings, 'session', token); - if (payload.sub !== settings.GITHUB_ADMIN_ID || typeof payload.username !== 'string' + if (payload.sub !== settings.GH_ADMIN_ID || typeof payload.username !== 'string' || payload.revision !== authRevision) throw new Error('Invalid identity'); return { username: payload.username }; } catch { diff --git a/src/types.ts b/src/types.ts index 301730e..78bb3f0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,9 +55,9 @@ export interface Env { PREVIEW_ORIGIN?: string; ACCESS_TEAM_DOMAIN?: string; ACCESS_AUD?: string; - GITHUB_CLIENT_ID?: string; - GITHUB_CLIENT_SECRET?: string; - GITHUB_ADMIN_ID?: string; + GH_CLIENT_ID?: string; + GH_CLIENT_SECRET?: string; + GH_ADMIN_ID?: string; SSH_SESSIONS: DurableObjectNamespace; ASSETS?: Fetcher; CONNECT_TIMEOUT_MS?: string; diff --git a/tests/deployment-auth.test.ts b/tests/deployment-auth.test.ts index 0aa1853..b53f5ff 100644 --- a/tests/deployment-auth.test.ts +++ b/tests/deployment-auth.test.ts @@ -10,7 +10,7 @@ import { readWorkspaceState, updateWorkspaceState, type DeploymentWorkspaceState const template = parse(await readFile(new URL('../wrangler.toml', import.meta.url), 'utf8')); const environment = { CLOUDFLARE_API_TOKEN: 'cf-token', AUTH_PROVIDER: 'github', - GITHUB_CLIENT_ID: 'test-client', GITHUB_CLIENT_SECRET: 'test-client-secret', GITHUB_ADMIN: 'admin', + GH_CLIENT_ID: 'test-client', GH_CLIENT_SECRET: 'test-client-secret', GH_ADMIN: 'admin', }; const settings = readDeploymentSettings(template, environment); const database = { uuid: 'test-db', name: settings.databaseName }; @@ -19,15 +19,15 @@ const queryEnvelope = (results: unknown[]) => envelope([{ success: true, results test('each deployment mode validates only its own inputs', () => { assert.deepEqual(settings.identityProviderIds, []); - assert.equal(readDeploymentSettings(template, { ...environment, GITHUB_ADMIN: '' }).githubAdmin, undefined); + assert.equal(readDeploymentSettings(template, { ...environment, GH_ADMIN: '' }).githubAdmin, undefined); assert.equal(readDeploymentSettings(template, { - ...environment, GITHUB_ADMIN: '', GITHUB_ADMIN_ID: '123', ADMIN_EMAIL: 'invalid', ACCESS_TEAM_DOMAIN: 'invalid', ACCESS_IDP_IDS: 'invalid', + ...environment, GH_ADMIN: '', GH_ADMIN_ID: '123', ADMIN_EMAIL: 'invalid', ACCESS_TEAM_DOMAIN: 'invalid', ACCESS_IDP_IDS: 'invalid', }).githubAdminId, '123'); - assert.equal(readDeploymentSettings(template, { CLOUDFLARE_API_TOKEN: 'token', AUTH_PROVIDER: 'cloudflare', GITHUB_ADMIN: 'invalid value' }).authProvider, 'cloudflare'); - for (const field of ['GITHUB_CLIENT_ID', 'GITHUB_CLIENT_SECRET']) { + assert.equal(readDeploymentSettings(template, { CLOUDFLARE_API_TOKEN: 'token', AUTH_PROVIDER: 'cloudflare', GH_ADMIN: 'invalid value' }).authProvider, 'cloudflare'); + for (const field of ['GH_CLIENT_ID', 'GH_CLIENT_SECRET']) { assert.throws(() => readDeploymentSettings(template, { ...environment, [field]: '' }), new RegExp(field)); } - assert.throws(() => readDeploymentSettings(template, { ...environment, GITHUB_ADMIN_ID: 'name' }), /GITHUB_ADMIN_ID/); + assert.throws(() => readDeploymentSettings(template, { ...environment, GH_ADMIN_ID: 'name' }), /GH_ADMIN_ID/); assert.throws(() => readDeploymentSettings(template, { ...environment, AUTH_PROVIDER: 'other' }), /AUTH_PROVIDER/); }); @@ -43,15 +43,15 @@ test('first GitHub deployment resolves a numeric administrator without Zero Trus }); assert.deepEqual(calls, ['https://ssh.example.com/api/auth/me', 'https://api.github.com/users/admin']); assert.equal(prepared.githubAdminId, '123'); - assert.deepEqual(prepared.secrets, { GITHUB_CLIENT_SECRET: environment.GITHUB_CLIENT_SECRET }); - assert.deepEqual(requiredAuthSecrets('github'), ['GITHUB_CLIENT_SECRET']); + assert.deepEqual(prepared.secrets, { GH_CLIENT_SECRET: environment.GH_CLIENT_SECRET }); + assert.deepEqual(requiredAuthSecrets('github'), ['GH_CLIENT_SECRET']); const config = createDeploymentConfig(template, settings, database, { hostname: 'ssh.example.com', githubAdminId: '123' }); const vars = config.vars as Record; assert.equal(vars.AUTH_PROVIDER, 'github'); assert.equal(vars.ADMIN_ACCOUNT_ID, undefined); assert.equal(vars.APP_ORIGIN, 'https://ssh.example.com'); - assert.equal(vars.GITHUB_ADMIN_ID, '123'); - assert.equal(stringify(config).includes(environment.GITHUB_CLIENT_SECRET), false); + assert.equal(vars.GH_ADMIN_ID, '123'); + assert.equal(stringify(config).includes(environment.GH_CLIENT_SECRET), false); }); test('ordinary GitHub redeployments retain the fixed ID and explicit numeric changes are visible', async () => { @@ -64,7 +64,7 @@ test('ordinary GitHub redeployments retain the fixed ID and explicit numeric cha fixedGithubAdminId: '123', fetcher, }); assert.equal(retained.githubAdminId, '123'); - const explicit = readDeploymentSettings(template, { ...environment, GITHUB_ADMIN_ID: '456', GITHUB_ADMIN: 'renamed-user' }); + const explicit = readDeploymentSettings(template, { ...environment, GH_ADMIN_ID: '456', GH_ADMIN: 'renamed-user' }); assert.equal((await prepareAuthentication(new CloudflareApi('token'), explicit, 'ssh.example.com', { fixedGithubAdminId: '123', fetcher, })).githubAdminId, '456'); @@ -77,9 +77,9 @@ test('organization names and unresolved first administrators are rejected', asyn let calls = 0; await assert.rejects(prepareAuthentication(cloudflare, settings, 'ssh.example.com', { fetcher: async () => (++calls === 1 ? new Response('', { status: 401 }) : response), - }), /GITHUB_ADMIN/); + }), /GH_ADMIN/); } - const noName = readDeploymentSettings(template, { ...environment, GITHUB_ADMIN: '' }); + const noName = readDeploymentSettings(template, { ...environment, GH_ADMIN: '' }); await assert.rejects(prepareAuthentication(cloudflare, noName, 'ssh.example.com', { fetcher: async () => new Response('', { status: 401 }), }), /首次配置/); diff --git a/tests/deployment-bootstrap.test.ts b/tests/deployment-bootstrap.test.ts index ab488b9..ec1c1e4 100644 --- a/tests/deployment-bootstrap.test.ts +++ b/tests/deployment-bootstrap.test.ts @@ -200,13 +200,13 @@ test('existing plain-text administrator ID can seed the persistent deployment st const api = new CloudflareApi('token', async (url) => { if (String(url).endsWith('/settings')) return json({ bindings: [ { name: 'AUTH_PROVIDER', type: 'plain_text', text: 'github' }, - { name: 'GITHUB_ADMIN_ID', type: 'plain_text', text: '123456' }, - { name: 'GITHUB_CLIENT_SECRET', type: 'secret_text' }, + { name: 'GH_ADMIN_ID', type: 'plain_text', text: '123456' }, + { name: 'GH_CLIENT_SECRET', type: 'secret_text' }, ] }); return json([{ id: settings.workerName }]); }); - assert.equal(await readWorkerVariable(api, settings, 'GITHUB_ADMIN_ID'), '123456'); - assert.equal(await readWorkerVariable(api, settings, 'GITHUB_CLIENT_SECRET'), undefined); + assert.equal(await readWorkerVariable(api, settings, 'GH_ADMIN_ID'), '123456'); + assert.equal(await readWorkerVariable(api, settings, 'GH_CLIENT_SECRET'), undefined); }); test('existing encryption key is never overwritten, including stale Actions secrets', async () => { diff --git a/tests/deployment.test.ts b/tests/deployment.test.ts index a44055c..c8e356e 100644 --- a/tests/deployment.test.ts +++ b/tests/deployment.test.ts @@ -221,7 +221,7 @@ test('Actions asks for email, keeps Token private and uses the shared deploy ent } assert.ok(workflow.includes('CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}')); assert.ok(workflow.includes('inputs.admin_email || secrets.ADMIN_EMAIL || vars.ADMIN_EMAIL')); - assert.ok(workflow.includes('GITHUB_ADMIN_ID: ${{ vars.GITHUB_ADMIN_ID }}')); + assert.ok(workflow.includes('GH_ADMIN_ID: ${{ vars.GH_ADMIN_ID }}')); assert.equal(workflow.includes('secrets.ACCESS_AUD'), false); assert.equal(workflow.includes('secrets.ACCESS_TEAM_DOMAIN'), false); assert.ok(workflow.includes('CUSTOM_DOMAIN: ${{ secrets.CUSTOM_DOMAIN || vars.CUSTOM_DOMAIN }}')); diff --git a/tests/oauth.test.ts b/tests/oauth.test.ts index b6e743b..c3f179f 100644 --- a/tests/oauth.test.ts +++ b/tests/oauth.test.ts @@ -11,7 +11,7 @@ const origin = 'https://ssh.example.com'; const baseEnv = { AUTH_PROVIDER: 'github', APP_ORIGIN: origin, ENCRYPTION_KEY: Buffer.alloc(32, 7).toString('base64'), - GITHUB_CLIENT_ID: 'test-client', GITHUB_CLIENT_SECRET: 'test-client-secret', GITHUB_ADMIN_ID: '123', + GH_CLIENT_ID: 'test-client', GH_CLIENT_SECRET: 'test-client-secret', GH_ADMIN_ID: '123', ACCESS_TEAM_DOMAIN: 'test-oauth.cloudflareaccess.com', ACCESS_AUD: 'access-audience', }; @@ -71,7 +71,7 @@ function exchange(id = 123): typeof fetch { if (String(url).endsWith('/access_token')) { assert.equal(init?.method, 'POST'); const body = init?.body as URLSearchParams; - assert.equal(body.get('client_secret'), baseEnv.GITHUB_CLIENT_SECRET); + assert.equal(body.get('client_secret'), baseEnv.GH_CLIENT_SECRET); assert.equal(body.get('redirect_uri'), `${origin}/auth/callback`); assert.equal(body.get('code_verifier')?.length, 43); return Response.json({ access_token: 'private-github-token' }); @@ -126,7 +126,7 @@ test('OAuth starts with random state, S256 PKCE and no privileged scopes', async assert.equal(challenge, first.authorization.searchParams.get('code_challenge')); assert.equal(payload.state, first.authorization.searchParams.get('state')); assert.ok(first.response.headers.get('Set-Cookie')?.includes('Secure; HttpOnly; SameSite=Lax')); - assert.equal(first.authorization.toString().includes(baseEnv.GITHUB_CLIENT_SECRET), false); + assert.equal(first.authorization.toString().includes(baseEnv.GH_CLIENT_SECRET), false); }); test('callback binds state to its browser cookie before any token exchange', async () => { @@ -164,7 +164,7 @@ test('wrong signing key, tampered cookies and changed administrator cannot acces for (const [token, settings] of [ [`${cookie}tampered`, env], [cookie, { ...env, ENCRYPTION_KEY: Buffer.alloc(32, 8).toString('base64') }], - [cookie, { ...env, GITHUB_ADMIN_ID: '456' }], + [cookie, { ...env, GH_ADMIN_ID: '456' }], [cookie, { ...env, APP_ORIGIN: 'https://another.example.com' }], ] as const) { await assert.rejects(currentAccount(request('/api/hosts', token), settings), (error: Error & { status: number }) => error.status === 401); @@ -284,7 +284,7 @@ test('expired sessions are rejected even with a correct signature and revision', name: 'HKDF', hash: 'SHA-256', salt: encoder.encode('edgessh:v1'), info: encoder.encode('github-oauth-cookie'), }, material, 256)); const token = await new SignJWT({ sub: '123', username: 'admin', revision: 1 }).setProtectedHeader({ alg: 'HS256' }) - .setIssuer(origin).setAudience(`edgessh:session:${env.GITHUB_CLIENT_ID}:123`) + .setIssuer(origin).setAudience(`edgessh:session:${env.GH_CLIENT_ID}:123`) .setIssuedAt(1).setExpirationTime(2).sign(key); await assert.rejects(currentAccount(request('/api/auth/me', `__Host-edgessh-session=${token}`), env), /过期/); });