From ea03c2b5c8edabc01eb4fde0ba8ca6cb3c1f11bd Mon Sep 17 00:00:00 2001 From: suzmii Date: Mon, 28 Sep 2026 11:52:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=20dev=20=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E6=8A=8A=E9=BB=98=E8=AE=A4=20CLI=20=E9=85=8D=E7=BD=AE=E9=87=8C?= =?UTF-8?q?=E7=9A=84=E8=BF=9C=E7=A8=8B=20token=20=E5=BD=93=E4=BD=9C?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=8F=91=E5=B8=83=E5=87=AD=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - shouldTrustExistingSpacetimeToken:本地(loopback)服务器额外要求 token 由本机签发(JWT iss 为 localhost/127.0.0.1/::1);默认 CLI 配置里的远程账号 token 形如有效但验签必然失败,会在 pre-publish check 抛 401 InvalidSignature - 显式 shell 覆盖 GENARRATIVE_SPACETIME_TOKEN 的语义保持不变,仍然优先 - 单测:远程签发 token 在本地服务器上不被信任、本机签发 token 被信任 - 验证:scripts/dev.test.ts 70 passed - 未覆盖(后续):本机签发但已失效的旧 token(数据目录重建后签名失效,iss 仍为 localhost)仍需手工清理 dev-cli/cli.toml 与 dev-api-identities/local-node.json;自动自愈需要把发布调用改成非致命,因为当前 runForeground 失败会直接结束进程,调用层 catch 拿不到 --- scripts/dev.mjs | 38 ++++++++++++++++++++++++++- scripts/dev.test.ts | 62 ++++++++++++++++++++++++++++++++++++++------- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/scripts/dev.mjs b/scripts/dev.mjs index aae03d3b2..ba35aa9ea 100644 --- a/scripts/dev.mjs +++ b/scripts/dev.mjs @@ -3259,19 +3259,54 @@ function shouldTrustExistingSpacetimeToken( return false; } + // 本地 standalone 只认自己签发的 token:远程账号的 token(默认 CLI 配置里那种) + // 形如有效但验签必然失败,会在 pre-publish check 上抛 401 InvalidSize。 + // 因此先按签发者判断,再做「与环境/CLI 一致」这类弱判断。 const shellToken = String(env.GENARRATIVE_SPACETIME_TOKEN ?? '').trim(); if (shellToken && shellToken === normalizedToken) { + // 显式覆盖(运维手动指定)优先,保持既有语义。 return true; } - if (!isLoopbackSpacetimeServer(serverUrl)) { + const loopback = isLoopbackSpacetimeServer(serverUrl); + if (!loopback) { return true; } + // 本地 standalone 只认自己签发的 token:默认 CLI 配置里那枚远程账号 token + // 形如有效但验签必然失败(pre-publish check 401 InvalidSignature),不能信任。 + if (!isLocallyIssuedSpacetimeToken(normalizedToken)) { + return false; + } + const cliToken = resolveCliToken(); return Boolean(cliToken && cliToken === normalizedToken); } +/// JWT 的 iss 是否指向本机服务器(localhost/127.0.0.1/::1 或 *.localhost)。 +function isLocallyIssuedSpacetimeToken(token) { + try { + const payload = String(token).split('.')[1]; + if (!payload) { + return false; + } + const claims = JSON.parse( + Buffer.from(payload, 'base64url').toString('utf8'), + ); + const issuer = String(claims.iss ?? '').trim(); + if (issuer === 'localhost' || issuer.endsWith('.localhost')) { + return true; + } + if (!issuer) { + return false; + } + const url = new URL(issuer.includes('://') ? issuer : `http://${issuer}`); + return ['127.0.0.1', 'localhost', '::1'].includes(url.hostname); + } catch { + return false; + } +} + function isLoopbackSpacetimeServer(serverUrl) { try { const url = new URL(serverUrl); @@ -3544,6 +3579,7 @@ export { createWatchConfigs, DevRunner, isDirectModuleExecution, + isLocallyIssuedSpacetimeToken, isSpacetimePublishPermissionError, isStaleExternalGenerationWorkerProcess, normalizeCargoVersionRequirement, diff --git a/scripts/dev.test.ts b/scripts/dev.test.ts index fdf6a8efc..e5f7de62b 100644 --- a/scripts/dev.test.ts +++ b/scripts/dev.test.ts @@ -30,6 +30,7 @@ import { createWatchConfigs, DevRunner, isDirectModuleExecution, + isLocallyIssuedSpacetimeToken, isSpacetimePublishPermissionError, isStaleExternalGenerationWorkerProcess, normalizeCargoVersionRequirement, @@ -44,6 +45,16 @@ import { shouldTrustExistingSpacetimeToken, } from './dev.mjs'; +/// 造一个带指定 iss 的假 JWT(只用于断言签发者判定)。 +const fakeJwt = (issuer: string): string => + [ + Buffer.from(JSON.stringify({ alg: 'none' })).toString('base64url'), + Buffer.from(JSON.stringify({ iss: issuer, sub: 'fake' })).toString( + 'base64url', + ), + 'signature', + ].join('.'); + const originalFetch = globalThis.fetch; afterEach(() => { @@ -336,16 +347,13 @@ describe('dev scheduler argument routing', () => { } }); - test('本地 SpacetimeDB 信任与当前 CLI 一致的 env token', () => { + test('本地 SpacetimeDB 信任与当前 CLI 一致且由本机签发的 token', () => { + const localToken = fakeJwt('localhost'); expect( - shouldTrustExistingSpacetimeToken( - 'owner-cli-token', - 'http://127.0.0.1:3101', - { - env: {}, - resolveCliToken: () => 'owner-cli-token', - }, - ), + shouldTrustExistingSpacetimeToken(localToken, 'http://127.0.0.1:3101', { + env: {}, + resolveCliToken: () => localToken, + }), ).toBe(true); }); }); @@ -1834,3 +1842,39 @@ spacetimedb tool version 2.8.3; spacetimedb-lib version 2.8.3; } }); }); + +describe('本地 token 信任判定', () => { + it('拒绝远程账号签发的 token 用于本地服务器(曾经的 401 InvalidSignature)', () => { + expect( + isLocallyIssuedSpacetimeToken(fakeJwt('https://api.spacetimedb.com')), + ).toBe(false); + expect(isLocallyIssuedSpacetimeToken('not-a-jwt')).toBe(false); + expect( + shouldTrustExistingSpacetimeToken( + fakeJwt('https://api.spacetimedb.com'), + 'http://127.0.0.1:3101', + { + env: {}, + resolveCliToken: () => fakeJwt('https://api.spacetimedb.com'), + }, + ), + ).toBe(false); + }); + + it('接受本机签发的 token', () => { + expect(isLocallyIssuedSpacetimeToken(fakeJwt('localhost'))).toBe(true); + expect( + isLocallyIssuedSpacetimeToken(fakeJwt('http://127.0.0.1:3101')), + ).toBe(true); + expect( + shouldTrustExistingSpacetimeToken( + fakeJwt('localhost'), + 'http://127.0.0.1:3101', + { + env: {}, + resolveCliToken: () => fakeJwt('localhost'), + }, + ), + ).toBe(true); + }); +});