mirror of
https://github.com/wuzf/2fa.git
synced 2026-09-03 07:17:09 +08:00
fix(deploy): make KV detection env-aware to prevent misreporting
Step 2.5 in deploy.js was env-blind, so 'npm run deploy:dev' falsely reported reusing the production KV while wrangler silently fell back to the env binding. Combined with [env.development] inheriting top-level routes, dev deploys could also hijack the production custom domain. Make extractWorkerName / injectKvNamespaceId / findExistingKvId accept an envName parameter, and preserve [env.development].routes across Sync Upstream merges so 'routes = []' overrides survive upgrades.
This commit is contained in:
@@ -11,55 +11,71 @@ export function injectWorkerVersion(configText, version) {
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function extractWorkerName(configText) {
|
||||
/**
|
||||
* 提取 worker 名称。
|
||||
* - envName=null(默认):返回顶层 `name`(生产环境名)
|
||||
* - envName="X":在 `[env.X]` 块内查找 `name`,找不到则回落到顶层
|
||||
*/
|
||||
export function extractWorkerName(configText, envName = null) {
|
||||
if (envName) {
|
||||
const lines = configText.split('\n');
|
||||
const envHeader = `[env.${envName}]`;
|
||||
let inEnvBlock = false;
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === envHeader) {
|
||||
inEnvBlock = true;
|
||||
continue;
|
||||
}
|
||||
if (!inEnvBlock) continue;
|
||||
if (/^\[/.test(trimmed)) break; // 进入下一个 section,env 块结束
|
||||
const nameMatch = trimmed.match(/^name\s*=\s*"([^"]+)"/);
|
||||
if (nameMatch) return nameMatch[1];
|
||||
}
|
||||
// 未在 env 块内找到 name,回落到顶层
|
||||
}
|
||||
const match = configText.match(/^name\s*=\s*"([^"]+)"/m);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export function injectKvNamespaceId(configText, id) {
|
||||
// Match the first [[kv_namespaces]] block that contains binding = "SECRETS_KV"
|
||||
// and is NOT inside an [env.*] section.
|
||||
/**
|
||||
* 把 KV namespace id 注入到指定块内(首个 binding = "SECRETS_KV" 的 [[kv_namespaces]] 数组项)。
|
||||
* - envName=null(默认):目标为顶层 `[[kv_namespaces]]`
|
||||
* - envName="X":目标为 `[[env.X.kv_namespaces]]`
|
||||
*
|
||||
* 已存在 id 时覆盖;不存在时插入到 binding 行后。其他块(含其它 env 的 KV 块)不会被改动。
|
||||
*/
|
||||
export function injectKvNamespaceId(configText, id, envName = null) {
|
||||
const lines = configText.split('\n');
|
||||
let inEnvSection = false;
|
||||
let kvBlockStart = -1;
|
||||
const targetHeader = envName
|
||||
? `[[env.${envName}.kv_namespaces]]`
|
||||
: '[[kv_namespaces]]';
|
||||
|
||||
let inTargetBlock = false;
|
||||
let bindingLine = -1;
|
||||
let existingIdLine = -1;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const trimmed = lines[i].trim();
|
||||
|
||||
// Track whether we're inside an [env.*] section
|
||||
if (/^\[env\./.test(trimmed)) {
|
||||
inEnvSection = true;
|
||||
continue;
|
||||
}
|
||||
if (/^\[(?!env\.)/.test(trimmed) || /^\[\[(?!env\.)/.test(trimmed)) {
|
||||
inEnvSection = false;
|
||||
}
|
||||
|
||||
if (inEnvSection) continue;
|
||||
|
||||
if (trimmed === '[[kv_namespaces]]') {
|
||||
kvBlockStart = i;
|
||||
// 任何 section header 都视为当前块结束
|
||||
if (/^\[/.test(trimmed)) {
|
||||
if (inTargetBlock && bindingLine >= 0) {
|
||||
break; // 已经在目标块里找到 binding,停止扫描
|
||||
}
|
||||
inTargetBlock = trimmed === targetHeader;
|
||||
bindingLine = -1;
|
||||
existingIdLine = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (kvBlockStart >= 0) {
|
||||
// End of block: next section header or empty line after content
|
||||
if (/^\[/.test(trimmed) || /^\[\[/.test(trimmed)) {
|
||||
if (bindingLine >= 0) break;
|
||||
kvBlockStart = -1;
|
||||
continue;
|
||||
}
|
||||
if (!inTargetBlock) continue;
|
||||
|
||||
if (/^binding\s*=\s*"SECRETS_KV"/.test(trimmed)) {
|
||||
bindingLine = i;
|
||||
}
|
||||
if (/^id\s*=\s*"/.test(trimmed)) {
|
||||
existingIdLine = i;
|
||||
}
|
||||
if (/^binding\s*=\s*"SECRETS_KV"/.test(trimmed)) {
|
||||
bindingLine = i;
|
||||
}
|
||||
if (/^id\s*=\s*"/.test(trimmed)) {
|
||||
existingIdLine = i;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ const versionStrategy = args.includes('--git') ? '--git' :
|
||||
'';
|
||||
|
||||
const envIndex = args.indexOf('--env');
|
||||
const envArg = envIndex !== -1 && args[envIndex + 1] ? `--env ${args[envIndex + 1]}` : '';
|
||||
const envName = envIndex !== -1 && args[envIndex + 1] ? args[envIndex + 1] : null;
|
||||
const envArg = envName ? `--env ${envName}` : '';
|
||||
|
||||
console.log('');
|
||||
console.log('🚀 ========================================');
|
||||
@@ -56,9 +57,10 @@ try {
|
||||
|
||||
// Step 2.5: 自动检测并绑定已有 KV namespace,防止重复创建
|
||||
console.log('🔍 Step 2.5: 检测已有 KV namespace...');
|
||||
const existingKv = findExistingKvId(extractWorkerName(modifiedConfig));
|
||||
const workerName = extractWorkerName(modifiedConfig, envName);
|
||||
const existingKv = findExistingKvId(workerName, envName);
|
||||
if (existingKv) {
|
||||
modifiedConfig = injectKvNamespaceId(modifiedConfig, existingKv.id);
|
||||
modifiedConfig = injectKvNamespaceId(modifiedConfig, existingKv.id, envName);
|
||||
console.log(` ✅ 复用已有 KV: ${existingKv.title} (${existingKv.id})`);
|
||||
} else {
|
||||
console.log(' ℹ️ 未检测到已有 KV,将由 Wrangler 自动创建');
|
||||
@@ -112,27 +114,64 @@ function generateVersion(versionStrategyArg) {
|
||||
return execSync(versionCmd, { encoding: 'utf-8' }).trim().split('\n')[0];
|
||||
}
|
||||
|
||||
function findExistingKvId(workerName) {
|
||||
function findExistingKvId(workerName, envName = null) {
|
||||
if (!workerName) return null;
|
||||
|
||||
let namespaces;
|
||||
try {
|
||||
const output = execSync('npx wrangler kv namespace list', {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
const namespaces = JSON.parse(output);
|
||||
if (!namespaces.length) return null;
|
||||
|
||||
// 按优先级匹配,覆盖 wrangler auto-provision、Dashboard 创建、老版本等命名格式
|
||||
const match =
|
||||
namespaces.find(ns => ns.title === `${workerName}-SECRETS_KV`) ||
|
||||
namespaces.find(ns => ns.title === `${workerName}-secrets-kv`) ||
|
||||
namespaces.find(ns => ns.title === 'SECRETS_KV') ||
|
||||
namespaces.find(ns => ns.title === workerName) ||
|
||||
namespaces.find(ns => ns.title.includes('SECRETS_KV')) ||
|
||||
namespaces.find(ns => ns.title.includes('secrets-kv')) ||
|
||||
(namespaces.length === 1 ? namespaces[0] : null);
|
||||
|
||||
return match ? { id: match.id, title: match.title } : null;
|
||||
namespaces = JSON.parse(output);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!namespaces.length) return null;
|
||||
|
||||
// 短名映射:开发环境常缩写为 dev / prod
|
||||
const ENV_ALIASES = { development: 'dev', production: 'prod' };
|
||||
const envAlias = envName ? (ENV_ALIASES[envName] || envName) : null;
|
||||
|
||||
// 推断 base 名(去除可能的 env 后缀):worker "2fa-dev" + envAlias "dev" → base "2fa"
|
||||
const stripSuffix = (name, suffix) =>
|
||||
suffix && name.endsWith(`-${suffix}`) ? name.slice(0, -(suffix.length + 1)) : name;
|
||||
const baseName = envAlias ? stripSuffix(workerName, envAlias) : workerName;
|
||||
|
||||
// 候选 title 列表,越靠前越优先
|
||||
const candidates = [];
|
||||
if (envName) {
|
||||
candidates.push(
|
||||
`${workerName}-secrets-kv`, // 2fa-dev-secrets-kv
|
||||
`${workerName}-SECRETS_KV`,
|
||||
`${baseName}-secrets-kv-${envAlias}`, // 2fa-secrets-kv-dev ← 当前命名
|
||||
`${baseName}-secrets-kv-${envName}`, // 2fa-secrets-kv-development
|
||||
`${envAlias}-${baseName}-SECRETS_KV`,
|
||||
`${envName}-${baseName}-SECRETS_KV`,
|
||||
`${envName}-SECRETS_KV`, // development-SECRETS_KV(旧命名)
|
||||
);
|
||||
} else {
|
||||
candidates.push(
|
||||
`${workerName}-secrets-kv`, // 2fa-secrets-kv ← 当前命名
|
||||
`${workerName}-SECRETS_KV`,
|
||||
'SECRETS_KV',
|
||||
workerName,
|
||||
);
|
||||
}
|
||||
|
||||
for (const title of candidates) {
|
||||
const match = namespaces.find(ns => ns.title === title);
|
||||
if (match) return { id: match.id, title: match.title };
|
||||
}
|
||||
|
||||
// env 部署只走精确匹配,避免误把生产 KV 命中给 dev
|
||||
if (envName) return null;
|
||||
|
||||
// 顶层部署的 fuzzy 兜底(保持原有兼容性)
|
||||
const fuzzy =
|
||||
namespaces.find(ns => ns.title.includes('SECRETS_KV')) ||
|
||||
namespaces.find(ns => ns.title.includes('secrets-kv')) ||
|
||||
(namespaces.length === 1 ? namespaces[0] : null);
|
||||
|
||||
return fuzzy ? { id: fuzzy.id, title: fuzzy.title } : null;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ merged = mergeVarsSection(merged, local, '[vars]', ['SW_VERSION']);
|
||||
|
||||
// Preserve environment-specific names and vars.
|
||||
merged = preserveSectionLineAssignment(merged, local, '[env.development]', 'name');
|
||||
// 保留本地维护者在 dev 块内显式声明的 routes(典型用法:routes = [] 防止继承顶层自定义域名,
|
||||
// 避免 deploy:dev 抢占生产域名)。注意:当前实现只支持单行赋值(含 routes = [])。
|
||||
merged = preserveSectionLineAssignment(merged, local, '[env.development]', 'routes');
|
||||
merged = mergeVarsSection(merged, local, '[env.development.vars]', ['SW_VERSION']);
|
||||
|
||||
// Merge KV bindings, preserving existing IDs while adopting upstream structure.
|
||||
|
||||
@@ -36,6 +36,36 @@ describe('extractWorkerName', () => {
|
||||
it('returns null when name is missing', () => {
|
||||
expect(extractWorkerName('main = "src/worker.js"')).toBeNull();
|
||||
});
|
||||
|
||||
it('extracts env-specific name when envName is provided', () => {
|
||||
const config = `name = "2fa"
|
||||
|
||||
[env.development]
|
||||
name = "2fa-dev"
|
||||
|
||||
[env.development.vars]
|
||||
SW_VERSION = "v1"
|
||||
`;
|
||||
expect(extractWorkerName(config, 'development')).toBe('2fa-dev');
|
||||
});
|
||||
|
||||
it('falls back to top-level name when env block has no name', () => {
|
||||
const config = `name = "2fa"
|
||||
|
||||
[env.staging]
|
||||
|
||||
[env.staging.vars]
|
||||
SW_VERSION = "v1"
|
||||
`;
|
||||
expect(extractWorkerName(config, 'staging')).toBe('2fa');
|
||||
});
|
||||
|
||||
it('returns top-level name when env block does not exist', () => {
|
||||
const config = `name = "2fa"
|
||||
main = "src/worker.js"
|
||||
`;
|
||||
expect(extractWorkerName(config, 'production')).toBe('2fa');
|
||||
});
|
||||
});
|
||||
|
||||
describe('injectKvNamespaceId', () => {
|
||||
@@ -81,4 +111,36 @@ binding = "SECRETS_KV"
|
||||
const noKvConfig = 'name = "2fa"\n[vars]\nSW_VERSION = "v1"\n';
|
||||
expect(injectKvNamespaceId(noKvConfig, 'abc123')).toBe(noKvConfig);
|
||||
});
|
||||
|
||||
it('injects id into env.development block when envName is provided', () => {
|
||||
const result = injectKvNamespaceId(baseConfig, 'dev-id', 'development');
|
||||
const devBlock = result.split('[[env.development.kv_namespaces]]')[1];
|
||||
expect(devBlock).toContain('id = "dev-id"');
|
||||
});
|
||||
|
||||
it('does not modify top-level kv_namespaces when envName is provided', () => {
|
||||
const result = injectKvNamespaceId(baseConfig, 'dev-id', 'development');
|
||||
const topBlock = result.split('[[kv_namespaces]]')[1].split('[')[0];
|
||||
expect(topBlock).not.toContain('id = "dev-id"');
|
||||
});
|
||||
|
||||
it('replaces existing env id when envName is provided', () => {
|
||||
const configWithDevId = baseConfig.replace(
|
||||
'[[env.development.kv_namespaces]]\nbinding = "SECRETS_KV"',
|
||||
'[[env.development.kv_namespaces]]\nbinding = "SECRETS_KV"\nid = "old-dev-id"'
|
||||
);
|
||||
const result = injectKvNamespaceId(configWithDevId, 'new-dev-id', 'development');
|
||||
expect(result).toContain('id = "new-dev-id"');
|
||||
expect(result).not.toContain('old-dev-id');
|
||||
});
|
||||
|
||||
it('does not match a different env block when envName is provided', () => {
|
||||
const configWithStaging = baseConfig + `
|
||||
[[env.staging.kv_namespaces]]
|
||||
binding = "SECRETS_KV"
|
||||
`;
|
||||
const result = injectKvNamespaceId(configWithStaging, 'dev-id', 'development');
|
||||
const stagingBlock = result.split('[[env.staging.kv_namespaces]]')[1];
|
||||
expect(stagingBlock).not.toContain('id = "dev-id"');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user