游戏发行入口改为平台同源路径
Project CI / AI game creator shell Rust crates (push) Successful in 1m26s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m11s
Project CI / Backend tests (push) Successful in 5m12s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m26s
Project CI / Native shell tests (push) Successful in 6m31s
Project CI / Frontend tests (push) Successful in 2m20s
Project CI / Repository checks (push) Successful in 2m25s
Project CI / AI game creator shell web tests (push) Successful in 1m21s
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 1m26s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m11s
Project CI / Backend tests (push) Successful in 5m12s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m26s
Project CI / Native shell tests (push) Successful in 6m31s
Project CI / Frontend tests (push) Successful in 2m20s
Project CI / Repository checks (push) Successful in 2m25s
Project CI / AI game creator shell web tests (push) Successful in 1m21s
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
- 审核通过时由 api-server 按 gameId 派生 /games/{gameId}/ 相对路径写入公开投影,删除 AppConfig 的发行入口模板字段与读取逻辑
- 删除 deploy/env 两份示例中的 GENARRATIVE_GAME_DISTRIBUTION_RELEASE_ENTRY_TEMPLATE
- 三份 nginx 模板内联同源发行入口 location,把 /games/<gameId>/ 与子资源转发到发行网关并在边缘清空 Cookie
- SPA allowlist 补齐 components、design-system、games、games/detail、games/mine、games/play、games/publish
- 前端 normalizeGameEntryUrl 支持相对路径与同源发行路径,按当前 origin 解析并补尾斜杠,继续兼容历史绝对 URL
- 删除退役的独立来源模板 deploy/nginx/genarrative-release-origin.conf、门禁脚本 scripts/check-release-origin-config.mjs 与其 npm 脚本
- 游戏分发 e2e 脚本改为在公开投影上断言 entryUrl 等于 /games/{gameId}/
- 同步平台主规范、运维主规范、nginx README 与共享决策记录
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { normalizeGameEntryUrl } from './gameDistributionGuards';
|
||||
|
||||
const GAME_ID = `game_${'a'.repeat(32)}`;
|
||||
const GAME_ENTRY_PATH = `/games/${GAME_ID}/`;
|
||||
|
||||
describe('normalizeGameEntryUrl', () => {
|
||||
it('resolves a release gateway relative path against the current origin', () => {
|
||||
expect(normalizeGameEntryUrl(GAME_ENTRY_PATH)).toBe(
|
||||
new URL(GAME_ENTRY_PATH, window.location.origin).href,
|
||||
);
|
||||
});
|
||||
|
||||
it('adds the trailing slash required for relative game assets', () => {
|
||||
expect(normalizeGameEntryUrl(`/games/${GAME_ID}`)).toBe(
|
||||
new URL(GAME_ENTRY_PATH, window.location.origin).href,
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes a same-origin absolute release gateway URL', () => {
|
||||
expect(
|
||||
normalizeGameEntryUrl(`${window.location.origin}/games/${GAME_ID}`),
|
||||
).toBe(new URL(GAME_ENTRY_PATH, window.location.origin).href);
|
||||
});
|
||||
|
||||
it.each([
|
||||
`/games/${GAME_ID}/assets/x.js`,
|
||||
'/games/detail',
|
||||
'/creation',
|
||||
'/',
|
||||
'javascript:alert(1)',
|
||||
'https://user:pass@x/y',
|
||||
'http://evil.test/x',
|
||||
])('rejects an invalid or unsafe entry URL: %s', (entryUrl) => {
|
||||
expect(normalizeGameEntryUrl(entryUrl)).toBeNull();
|
||||
});
|
||||
|
||||
it('passes through an absolute HTTPS URL on another origin', () => {
|
||||
const entryUrl = 'https://play.example.test/releases/version-1/index.html';
|
||||
|
||||
expect(normalizeGameEntryUrl(entryUrl)).toBe(entryUrl);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,12 @@ import { useEffect, useState } from 'react';
|
||||
|
||||
const MAX_GAME_ID_LENGTH = 128;
|
||||
const MAX_ENTRY_URL_LENGTH = 4096;
|
||||
const GAME_ENTRY_PATH_PATTERN = /^\/games\/(game_[0-9a-f]{32})\/?$/;
|
||||
|
||||
function normalizeGameEntryPath(pathname: string) {
|
||||
const match = GAME_ENTRY_PATH_PATTERN.exec(pathname);
|
||||
return match ? `/games/${match[1]}/` : null;
|
||||
}
|
||||
|
||||
function containsControlCharacter(value: string) {
|
||||
for (const character of value) {
|
||||
@@ -45,13 +51,18 @@ export function normalizeGameEntryUrl(value: string | null | undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentOrigin =
|
||||
typeof window === 'undefined' ? null : window.location.origin;
|
||||
if (normalized.startsWith('/')) {
|
||||
if (!currentOrigin) {
|
||||
return null;
|
||||
}
|
||||
const normalizedPath = normalizeGameEntryPath(normalized);
|
||||
return normalizedPath ? new URL(normalizedPath, currentOrigin).href : null;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(
|
||||
normalized,
|
||||
typeof window === 'undefined'
|
||||
? 'http://localhost'
|
||||
: window.location.origin,
|
||||
);
|
||||
const url = new URL(normalized);
|
||||
const isLocalDevelopmentHttp =
|
||||
url.protocol === 'http:' &&
|
||||
(url.hostname === 'localhost' ||
|
||||
@@ -60,13 +71,16 @@ export function normalizeGameEntryUrl(value: string | null | undefined) {
|
||||
if (
|
||||
(url.protocol !== 'https:' && !isLocalDevelopmentHttp) ||
|
||||
url.username ||
|
||||
url.password ||
|
||||
(typeof window !== 'undefined' &&
|
||||
url.origin === window.location.origin &&
|
||||
!import.meta.env.DEV)
|
||||
url.password
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (currentOrigin && url.origin === currentOrigin) {
|
||||
const normalizedPath = normalizeGameEntryPath(url.pathname);
|
||||
return normalizedPath
|
||||
? new URL(normalizedPath, currentOrigin).href
|
||||
: null;
|
||||
}
|
||||
return url.href;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user