补齐游戏分发安全下架取证并记录发行包对象可匿名直取
Project CI / AI game creator shell Rust crates (push) Successful in 1m32s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m6s
Project CI / Backend tests (push) Successful in 4m55s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 8m34s
Project CI / Frontend tests (push) Successful in 2m19s
Project CI / Native shell tests (push) Successful in 7m14s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 9m55s
Project CI / AI game creator shell web tests (push) Successful in 1m57s
Project CI / Repository checks (push) Successful in 2m28s

- 媒体链路脚本新增管理员安全下架与恢复段:suspend 后目录/详情/发行读取全部关闭、过期 revision 的 suspend 被 CAS 拒绝、restore 后重新公开且可玩
- 新增匿名直取发行包对象探测:本地 dev bucket 返回 200(同 bucket 不存在 key 为 404),默认只打 WARN,E2E_REQUIRE_PRIVATE_BUCKET=1 时按失败处理
- 游戏分发里程碑阶段 B 第 7 条补上「不能绕过网关直取 OSS 在当前环境不成立」的实测、根因与剩余缺口
- pitfalls 记录 platform-oss 的 put_internal_object_bytes 从不发送 x-oss-object-acl 的问题与上线前处理
This commit is contained in:
kdletters
2026-09-28 19:08:51 +08:00
parent 6cbc56dd98
commit fb87e1d4ff
3 changed files with 145 additions and 2 deletions
@@ -1069,6 +1069,141 @@ async function main() {
),
);
// 13. 管理员安全下架 / 恢复,以及匿名直取发行包对象
const revisionForV4 = await adminRevisionOfGame();
const approveV4 = await api(
`/admin/api/game-distribution/versions/${versionIdV4}/review`,
{
method: 'POST',
token: admin,
headers: { 'Idempotency-Key': `e2e-v4-approve-${id}` },
body: {
decision: 'approve',
expectedPublicationRevision: revisionForV4,
},
},
);
const detailAfterV4 = await api(`/api/game-distribution/games/${gameId}`);
check(
'用当前 revision 批准 v4 后新版本上线',
approveV4.status === 200 &&
detailAfterV4.data?.currentVersion?.sha256 === sha256V4,
`status=${approveV4.status} sha=${String(detailAfterV4.data?.currentVersion?.sha256).slice(0, 12)}`,
);
const revisionBeforeSuspend = detailAfterV4.data?.publicationRevision ?? 0;
const suspended = await api(
`/admin/api/game-distribution/games/${gameId}/suspend`,
{
method: 'POST',
token: admin,
headers: { 'Idempotency-Key': `e2e-suspend-${id}` },
body: {
expectedPublicationRevision: revisionBeforeSuspend,
reason: 'E2E 安全下架',
},
},
);
check(
'管理员安全下架成功',
suspended.status === 200 && suspended.data?.game?.status !== 'published',
`status=${suspended.status} gameStatus=${suspended.data?.game?.status ?? ''}`,
);
const catalogAfterSuspend = await api('/api/game-distribution/games');
const detailAfterSuspend = await api(
`/api/game-distribution/games/${gameId}`,
);
const releaseAfterSuspend = await fetch(
`${API}/api/game-distribution/releases/${gameId}/index.html`,
);
check(
'安全下架后目录 / 详情 / 发行读取全部关闭',
!(catalogAfterSuspend.data?.games ?? []).some(
(game) => game.id === gameId,
) &&
detailAfterSuspend.status === 404 &&
releaseAfterSuspend.status === 404,
`detail=${detailAfterSuspend.status} gateway=${releaseAfterSuspend.status}`,
);
const staleSuspend = await api(
`/admin/api/game-distribution/games/${gameId}/suspend`,
{
method: 'POST',
token: admin,
headers: { 'Idempotency-Key': `e2e-stale-suspend-${id}` },
body: {
expectedPublicationRevision: revisionBeforeSuspend - 1,
reason: 'E2E 过期安全下架',
},
},
);
check(
'过期 revision 的安全下架被 CAS 拒绝',
staleSuspend.status === 409 &&
staleSuspend.text.includes('PUBLICATION_CONFLICT'),
`status=${staleSuspend.status} body=${staleSuspend.text.slice(0, 160)}`,
);
const restoreRevision = await adminRevisionOfGame();
const restored = await api(
`/admin/api/game-distribution/games/${gameId}/restore`,
{
method: 'POST',
token: admin,
headers: { 'Idempotency-Key': `e2e-restore-${id}` },
body: { expectedPublicationRevision: restoreRevision },
},
);
const catalogAfterRestore = await api('/api/game-distribution/games');
const releaseAfterRestore = await fetch(
`${API}/api/game-distribution/releases/${gameId}/index.html`,
);
check(
'管理员恢复后游戏重新公开且可玩',
restored.status === 200 &&
(catalogAfterRestore.data?.games ?? []).some(
(game) => game.id === gameId,
) &&
releaseAfterRestore.status === 200,
`status=${restored.status} gateway=${releaseAfterRestore.status}`,
);
// 发行包落在 api-server 的快照 bucket(默认 agc-dev,见 config.rs 的默认值),
// 公开读取必须走网关;这里直接用对象键构造匿名请求,验证私有 bucket 不给直取。
const ossBucket = (
process.env.GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_BUCKET ?? 'agc-dev'
).trim();
const ossEndpoint = (
process.env.GENARRATIVE_AGC_PROJECT_SNAPSHOT_OSS_ENDPOINT ??
'oss-rg-china-mainland.aliyuncs.com'
).trim();
const objectKey = `agc/project-snapshots/v1/game-distribution/${gameId}/${versionIdV4}.zip`;
const directObject = await fetch(
`https://${ossBucket}.${ossEndpoint}/${objectKey}`,
);
// 这一条验的是「环境里的 bucket/对象 ACL」,不是接口行为:本地 dev bucket 允许匿名读,
// 所以默认只报 WARN;上线前把 E2E_REQUIRE_PRIVATE_BUCKET=1 打开,让它在非私有环境里失败。
if (directObject.status >= 400) {
check(
'匿名直取私有 bucket 里的发行包对象被拒绝',
true,
`status=${directObject.status} bucket=${ossBucket}`,
);
} else if ((process.env.E2E_REQUIRE_PRIVATE_BUCKET ?? '').trim() === '1') {
check(
'匿名直取私有 bucket 里的发行包对象被拒绝',
false,
`status=${directObject.status} bucket=${ossBucket}`,
);
} else {
console.log(
`WARN 匿名直取发行包对象返回 status=${directObject.status}(bucket=${ossBucket}):` +
'本地 dev bucket 允许匿名读,生产上线前必须确认 bucket 与对象 ACL 都是私有(可用 E2E_REQUIRE_PRIVATE_BUCKET=1 复验)。',
);
}
console.log(`\n结果:${failures === 0 ? '全部通过' : `${failures} 项失败`}`);
process.exitCode = failures === 0 ? 0 : 1;
}