@ttmg/cli 0.4.6-vibe-beta.11 → 0.4.6-vibe-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -11
- package/dist/index.js +161 -111
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/scripts/verify-agent-contract.js +8 -0
- package/dist/scripts/vibe-upload-smoke.js +2 -2
- package/package.json +1 -1
- package/vibe-upload.md +48 -16
package/README.md
CHANGED
|
@@ -9,8 +9,21 @@ Vibe real web authorization and upload: use `--web` with the title, local icon a
|
|
|
9
9
|
|
|
10
10
|
#### Installation 安装
|
|
11
11
|
|
|
12
|
-
For
|
|
13
|
-
|
|
12
|
+
For Vibe Web authorization and upload, install the `vibe-beta` release:
|
|
13
|
+
Vibe 网页授权上传安装 `vibe-beta` 版本:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @ttmg/cli@vibe-beta
|
|
17
|
+
ttmg --version
|
|
18
|
+
ttmg capabilities --format json
|
|
19
|
+
ttmg upload --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The package includes [vibe-upload.md](vibe-upload.md). Keep the Web upload process running during authorization and read NDJSON until a terminal event. The `beta` and `vibe-beta` tags point to different published versions.
|
|
23
|
+
安装包包含 [vibe-upload.md](vibe-upload.md)。授权期间保持原 Web 上传进程运行,持续读取 NDJSON,直到收到终态。`beta` 与 `vibe-beta` 指向不同发布版本。
|
|
24
|
+
|
|
25
|
+
For Agent and Monetization workflows, the published beta is:
|
|
26
|
+
Agent、商业化流程的已发布 beta 为:
|
|
14
27
|
|
|
15
28
|
```bash
|
|
16
29
|
npm install -g @ttmg/cli@0.4.6-agent-beta.2
|
|
@@ -55,14 +68,10 @@ Keep the upload process running; this command creates no task and reports only
|
|
|
55
68
|
Skill 如需手动打开网页授权,读取上传进程 `waiting_authorization` 事件的 `pageUrl`,
|
|
56
69
|
执行 `ttmg auth open --page-url <pageUrl> --format json`。保持原上传进程运行;
|
|
57
70
|
新命令在 CLI 所在电脑打开系统浏览器,不新建任务,`open_requested` 不代表授权成功。
|
|
58
|
-
|
|
71
|
+
`--web` 已自动打开授权页;上述辅助命令只用于重新打开同一任务的授权页。
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
task creation/polling use the built-in PPE lane; authorization and uploads are
|
|
63
|
-
simulated. See [Vibe binding/upload](vibe-upload.md) for schema-v2 results.
|
|
64
|
-
实验性 `upload --vibe --mock` 不要求平台登录或环境配置。CLI 内置 PPE 创建/查询任务,
|
|
65
|
-
模拟授权和上传;本次结束于模拟上传完成,不含预览和发布。详见 [Vibe 绑定上传](vibe-upload.md)。
|
|
73
|
+
`ttmg upload --vibe --web` uses real browser authorization and uploads to PPE without a prior platform login or environment setup. `--mock` is a separate demo: task creation/polling are real, but authorization and material uploads are simulated. See [Vibe binding/upload](vibe-upload.md) for schema-v2 results.
|
|
74
|
+
`ttmg upload --vibe --web` 无需预先平台登录或配置环境,使用真实浏览器授权并上传到 PPE。`--mock` 是独立演示:真实创建/查询任务,模拟授权和素材上传。详见 [Vibe 绑定上传](vibe-upload.md)。
|
|
66
75
|
|
|
67
76
|
`--mock` 现在会真实创建 PPE 任务,不再表示全程离线;真实请求失败仍报错。
|
|
68
77
|
上线时由 CLI 发布版本去掉 PPE 路由,Skill 无需承担环境切换。
|
|
@@ -72,8 +81,8 @@ still fail. Production routing is a future CLI release change, not Skill configu
|
|
|
72
81
|
当前上传只需要标题、头像和游戏目录或代码包,其他应用资料由 CLI 使用既有 PPE 试用默认值补齐。Skill 读取 `inputRequirements`,不向用户索要额外资料;本地预检使用 `--dry-run`。
|
|
73
82
|
Current uploads need only the title, icon and game directory or ZIP. The CLI fills other metadata with existing PPE trial defaults. Read `inputRequirements` and do not request extra fields. Use `--dry-run` for local preparation.
|
|
74
83
|
|
|
75
|
-
For the existing authenticated platform workflow, log in first;
|
|
76
|
-
|
|
84
|
+
For the existing authenticated platform workflow, log in first; Vibe Web upload uses its own browser authorization.
|
|
85
|
+
原有需要平台鉴权的流程仍先登录;Vibe Web 上传使用本次任务的浏览器授权。
|
|
77
86
|
|
|
78
87
|
```
|
|
79
88
|
ttmg login
|
package/dist/index.js
CHANGED
|
@@ -7233,6 +7233,18 @@ const messages = {
|
|
|
7233
7233
|
'cli.command.auth.desc': 'Inspect local authentication state',
|
|
7234
7234
|
'cli.command.auth.open.desc': 'Open an existing Vibe task authorization page in the system browser',
|
|
7235
7235
|
'cli.command.upload.vibeWeb': 'Authorize in the browser, then upload to PPE (--vibe; no Mock or --no-open)',
|
|
7236
|
+
'cli.command.upload.vibeWebGuide': `
|
|
7237
|
+
Vibe web upload (real PPE trial):
|
|
7238
|
+
ttmg upload --vibe --web --dir ./game --title "My game" --icon ./avatar.png --format ndjson
|
|
7239
|
+
Use --package ./game.zip instead of --dir for an existing ZIP.
|
|
7240
|
+
Collect only title and icon; the CLI fills other app_info fields with PPE trial defaults.
|
|
7241
|
+
Keep this process running. At waiting_authorization, complete authorization in the system browser on the CLI computer.
|
|
7242
|
+
pageUrl is the local binding/progress page. To reopen authorization, execute the returned authorizationOpenCommand.
|
|
7243
|
+
The CLI polls and uploads avatar, app_info and package automatically after authorization. Do not start a second upload.
|
|
7244
|
+
Real success: terminal=true, status=uploaded, uploaded=true, simulation=false. On failure, read errorCode and nextAction.
|
|
7245
|
+
--web opens the browser for every output format. --mock is a separate simulation mode.
|
|
7246
|
+
Upload completion is not review approval or production release. See bundled vibe-upload.md and ttmg capabilities --format json.
|
|
7247
|
+
`,
|
|
7236
7248
|
'cli.command.auth.open.pageUrl': 'Local pageUrl returned by the running Vibe upload command',
|
|
7237
7249
|
'cli.command.auth.open.requested': 'Browser open requested. Complete authorization there; the existing upload process continues polling.',
|
|
7238
7250
|
'cli.command.auth.open.invalidPage': 'Pass the local pageUrl from the running Vibe upload command, not the platform URL.',
|
|
@@ -7646,6 +7658,18 @@ const messages = {
|
|
|
7646
7658
|
'cli.command.auth.desc': '查看本地登录状态',
|
|
7647
7659
|
'cli.command.auth.open.desc': '在系统浏览器打开已有 Vibe 任务的授权页',
|
|
7648
7660
|
'cli.command.upload.vibeWeb': '网页授权就绪后自动上传到 PPE(--vibe;不能与 Mock 或 --no-open 同用)',
|
|
7661
|
+
'cli.command.upload.vibeWebGuide': `
|
|
7662
|
+
Vibe 网页上传(真实 PPE 试用):
|
|
7663
|
+
ttmg upload --vibe --web --dir ./game --title "游戏名称" --icon ./avatar.png --format ndjson
|
|
7664
|
+
已有 ZIP 时,用 --package ./game.zip 替换 --dir。
|
|
7665
|
+
只收集标题和头像;其他 app_info 字段由 CLI 使用 PPE 试用默认值补齐。
|
|
7666
|
+
保持原进程运行。在 waiting_authorization 阶段,在 CLI 所在电脑的系统浏览器完成授权。
|
|
7667
|
+
pageUrl 是本机绑定/进度页;重新打开授权页时,执行返回的 authorizationOpenCommand。
|
|
7668
|
+
CLI 负责轮询,授权后自动上传 avatar、app_info、package,不再启动第二次上传。
|
|
7669
|
+
真实成功:terminal=true、status=uploaded、uploaded=true、simulation=false。失败时读取 errorCode 和 nextAction。
|
|
7670
|
+
--web 在所有输出格式下都会打开浏览器;--mock 是独立的模拟模式。
|
|
7671
|
+
上传完成不代表审核通过或正式上线。详见包内 vibe-upload.md 和 ttmg capabilities --format json。
|
|
7672
|
+
`,
|
|
7649
7673
|
'cli.command.auth.open.pageUrl': '正在运行的 Vibe 上传命令返回的本地 pageUrl',
|
|
7650
7674
|
'cli.command.auth.open.requested': '已请求浏览器打开授权页。请在页面完成授权,原上传进程继续轮询。',
|
|
7651
7675
|
'cli.command.auth.open.invalidPage': '请传入正在运行的 Vibe 上传命令返回的本地 pageUrl,不是平台授权链接。',
|
|
@@ -16308,7 +16332,7 @@ async function authStatus(options = {}) {
|
|
|
16308
16332
|
return result;
|
|
16309
16333
|
}
|
|
16310
16334
|
|
|
16311
|
-
var version = "0.4.6-vibe-beta.
|
|
16335
|
+
var version = "0.4.6-vibe-beta.13";
|
|
16312
16336
|
var engines = {
|
|
16313
16337
|
node: ">=20.19.1 <21 || >=22"
|
|
16314
16338
|
};
|
|
@@ -16332,6 +16356,113 @@ class VibeError extends Error {
|
|
|
16332
16356
|
}
|
|
16333
16357
|
}
|
|
16334
16358
|
|
|
16359
|
+
// Current PPE trial defaults: the owner-approved metadata used by the successful
|
|
16360
|
+
// real upload fixture on 2026-09-14, with owner-specified legal URLs updated on
|
|
16361
|
+
// 2026-09-15. This is not a production metadata profile.
|
|
16362
|
+
const VIBE_PPE_DEFAULT_APP_INFO = Object.freeze({
|
|
16363
|
+
description: 'DevTool PPE integration test fixture. Not a production game.',
|
|
16364
|
+
category: 'Other',
|
|
16365
|
+
subcategory: 'Other',
|
|
16366
|
+
terms_of_service: 'https://www.tiktok.com',
|
|
16367
|
+
privacy_policy: 'https://www.tiktok.com',
|
|
16368
|
+
copyright_confirmation: true,
|
|
16369
|
+
contact_email: 'devtool-test@example.invalid',
|
|
16370
|
+
});
|
|
16371
|
+
// BFF IDL + Import Service ValidateAppInfo, feat-minis-import (2026-09-14).
|
|
16372
|
+
// Keep credentials, app_name and icon_uri out of this file: CLI owns those fields.
|
|
16373
|
+
const requiredStrings = [
|
|
16374
|
+
'description', 'category', 'subcategory', 'terms_of_service', 'privacy_policy',
|
|
16375
|
+
];
|
|
16376
|
+
const optionalStrings = ['apple_team_id', 'contact_email', 'region', 'min_sdk_version'];
|
|
16377
|
+
function readVibeAppInfo(file) {
|
|
16378
|
+
if (file === undefined)
|
|
16379
|
+
return undefined;
|
|
16380
|
+
try {
|
|
16381
|
+
const stat = fs.statSync(file);
|
|
16382
|
+
if (!stat.isFile() || stat.size > 64 * 1024)
|
|
16383
|
+
throw new Error();
|
|
16384
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
16385
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
16386
|
+
throw new Error();
|
|
16387
|
+
const allowed = new Set([...requiredStrings, ...optionalStrings, 'copyright_confirmation']);
|
|
16388
|
+
if (Object.keys(value).some(key => !allowed.has(key)))
|
|
16389
|
+
throw new Error();
|
|
16390
|
+
// JSON can encode lone surrogates that Go rejects as invalid UTF-8 input.
|
|
16391
|
+
for (const item of Object.values(value)) {
|
|
16392
|
+
if (typeof item === 'string' && [...item].some(char => /^[\uD800-\uDFFF]$/.test(char)))
|
|
16393
|
+
throw new Error();
|
|
16394
|
+
}
|
|
16395
|
+
for (const key of requiredStrings) {
|
|
16396
|
+
if (typeof value[key] !== 'string' || !value[key].trim())
|
|
16397
|
+
throw new Error();
|
|
16398
|
+
}
|
|
16399
|
+
for (const key of optionalStrings) {
|
|
16400
|
+
if (value[key] !== undefined && (typeof value[key] !== 'string' || !value[key].trim()))
|
|
16401
|
+
throw new Error();
|
|
16402
|
+
}
|
|
16403
|
+
// An explicit declaration is required; never infer consent or supply a default.
|
|
16404
|
+
if (value.copyright_confirmation !== true)
|
|
16405
|
+
throw new Error();
|
|
16406
|
+
for (const key of ['terms_of_service', 'privacy_policy']) {
|
|
16407
|
+
if (/\s/.test(value[key]))
|
|
16408
|
+
throw new Error();
|
|
16409
|
+
const url = new URL(value[key]);
|
|
16410
|
+
if (!['http:', 'https:'].includes(url.protocol) || !url.hostname || url.username || url.password)
|
|
16411
|
+
throw new Error();
|
|
16412
|
+
}
|
|
16413
|
+
if (value.contact_email !== undefined && !/^[^\s<>@]+@[^\s<>@]+$/.test(value.contact_email))
|
|
16414
|
+
throw new Error();
|
|
16415
|
+
return value;
|
|
16416
|
+
}
|
|
16417
|
+
catch {
|
|
16418
|
+
// Do not expose paths, file contents or parser errors in structured output.
|
|
16419
|
+
throw new VibeError('VIBE_INVALID_APP_INFO');
|
|
16420
|
+
}
|
|
16421
|
+
}
|
|
16422
|
+
const VIBE_PLATFORM_ICON_LIMIT = 5 * 1024 * 1024;
|
|
16423
|
+
// Header preflight only. The BFF remains responsible for decoding the image.
|
|
16424
|
+
function validateVibeUploadInput(input) {
|
|
16425
|
+
if ([...input.title].length > 30)
|
|
16426
|
+
throw new VibeError('VIBE_TITLE_TOO_LONG');
|
|
16427
|
+
const image = input.icon.bytes;
|
|
16428
|
+
if (image.length > VIBE_PLATFORM_ICON_LIMIT)
|
|
16429
|
+
throw new VibeError('VIBE_PLATFORM_ICON_INVALID');
|
|
16430
|
+
let width = 0, height = 0;
|
|
16431
|
+
if (image.length >= 33 && image.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex')) &&
|
|
16432
|
+
image.readUInt32BE(8) === 13 && image.toString('ascii', 12, 16) === 'IHDR') {
|
|
16433
|
+
width = image.readUInt32BE(16);
|
|
16434
|
+
height = image.readUInt32BE(20);
|
|
16435
|
+
}
|
|
16436
|
+
else if (image[0] === 0xff && image[1] === 0xd8) {
|
|
16437
|
+
let offset = 2;
|
|
16438
|
+
while (offset + 4 <= image.length) {
|
|
16439
|
+
if (image[offset++] !== 0xff)
|
|
16440
|
+
break;
|
|
16441
|
+
while (image[offset] === 0xff)
|
|
16442
|
+
offset++;
|
|
16443
|
+
const marker = image[offset++];
|
|
16444
|
+
if (marker === 0xda || marker === 0xd9)
|
|
16445
|
+
break;
|
|
16446
|
+
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7))
|
|
16447
|
+
continue;
|
|
16448
|
+
if (offset + 2 > image.length)
|
|
16449
|
+
break;
|
|
16450
|
+
const length = image.readUInt16BE(offset);
|
|
16451
|
+
if (length < 2 || offset + length > image.length)
|
|
16452
|
+
break;
|
|
16453
|
+
// Go image/jpeg accepts baseline, extended sequential and progressive SOF.
|
|
16454
|
+
if ([0xc0, 0xc1, 0xc2].includes(marker) && length >= 8) {
|
|
16455
|
+
height = image.readUInt16BE(offset + 3);
|
|
16456
|
+
width = image.readUInt16BE(offset + 5);
|
|
16457
|
+
break;
|
|
16458
|
+
}
|
|
16459
|
+
offset += length;
|
|
16460
|
+
}
|
|
16461
|
+
}
|
|
16462
|
+
if (width !== 1024 || height !== 1024)
|
|
16463
|
+
throw new VibeError('VIBE_PLATFORM_ICON_INVALID');
|
|
16464
|
+
}
|
|
16465
|
+
|
|
16335
16466
|
function getCapabilities() {
|
|
16336
16467
|
return {
|
|
16337
16468
|
schemaVersion: '1',
|
|
@@ -16344,7 +16475,9 @@ function getCapabilities() {
|
|
|
16344
16475
|
},
|
|
16345
16476
|
commands: {
|
|
16346
16477
|
vibeUpload: {
|
|
16347
|
-
command: 'ttmg upload --vibe --
|
|
16478
|
+
command: 'ttmg upload --vibe --web --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
|
|
16479
|
+
mockCommand: 'ttmg upload --vibe --mock --dir <gameDir> --title <title> --icon <iconPath> --format ndjson',
|
|
16480
|
+
guide: 'vibe-upload.md',
|
|
16348
16481
|
option: '--vibe',
|
|
16349
16482
|
available: true,
|
|
16350
16483
|
platformWrite: true,
|
|
@@ -16375,11 +16508,32 @@ function getCapabilities() {
|
|
|
16375
16508
|
metadataInputs: ['title', 'icon', 'dir|package'],
|
|
16376
16509
|
inputRequirements: VIBE_INPUT_REQUIREMENTS,
|
|
16377
16510
|
webAuthorization: { option: '--web', backend: 'ppe', defaultLane: 'ppe_op_vc', simulation: false, opensSystemBrowser: true, continuesPolling: true, formatIndependent: true, conflictsWith: ['--mock', '--mock-scenario', '--vibe-api', '--no-open'], mockBehaviorUnchanged: true, materialUploadEnabled: true },
|
|
16511
|
+
skillWorkflow: {
|
|
16512
|
+
preferredFormat: 'ndjson',
|
|
16513
|
+
packageAlternative: '--package <zipPath> replaces --dir <gameDir>',
|
|
16514
|
+
keepUploadProcessRunning: true,
|
|
16515
|
+
pollingOwner: 'cli',
|
|
16516
|
+
uploadOwner: 'cli',
|
|
16517
|
+
waitingAuthorization: {
|
|
16518
|
+
status: 'waiting_authorization',
|
|
16519
|
+
userAction: 'complete authorization in the system browser on the CLI computer',
|
|
16520
|
+
pageUrlField: 'pageUrl',
|
|
16521
|
+
pageUrlPurpose: 'local binding and upload progress page',
|
|
16522
|
+
reopenCommandField: 'authorizationOpenCommand',
|
|
16523
|
+
browserOpenRequestedProvesAuthorization: false,
|
|
16524
|
+
createAnotherUploadTask: false,
|
|
16525
|
+
},
|
|
16526
|
+
realUploadSuccess: { terminal: true, status: 'uploaded', uploaded: true, simulation: false },
|
|
16527
|
+
failure: 'Read errorCode and nextAction; do not automatically create another task or reupload',
|
|
16528
|
+
completionMeaning: 'materials uploaded to PPE; review and production release are separate',
|
|
16529
|
+
sameHostRequired: true,
|
|
16530
|
+
},
|
|
16378
16531
|
optionalMetadataFile: '--app-info',
|
|
16379
16532
|
appInfoFields: {
|
|
16380
16533
|
required: [],
|
|
16381
16534
|
optional: [],
|
|
16382
16535
|
suppliedByCli: ['app_name', 'icon_uri'],
|
|
16536
|
+
defaultedByCli: Object.keys(VIBE_PPE_DEFAULT_APP_INFO),
|
|
16383
16537
|
additionalMetadataSource: 'cli-defaults',
|
|
16384
16538
|
defaultMetadataProfile: 'ppe-trial',
|
|
16385
16539
|
requestFromUser: false,
|
|
@@ -16410,8 +16564,9 @@ function getCapabilities() {
|
|
|
16410
16564
|
httpContract: 'minis-import-task',
|
|
16411
16565
|
authorizationQr: true,
|
|
16412
16566
|
previewQr: false,
|
|
16413
|
-
realAuthorizationVerified:
|
|
16414
|
-
realUploadVerified:
|
|
16567
|
+
realAuthorizationVerified: true,
|
|
16568
|
+
realUploadVerified: true,
|
|
16569
|
+
verificationScope: { backend: 'ppe', assets: 'test-fixtures', date: '2026-09-15', production: false },
|
|
16415
16570
|
autoUploadAfterAuthorization: true,
|
|
16416
16571
|
boundedPolling: true,
|
|
16417
16572
|
cancellation: true,
|
|
@@ -17155,7 +17310,8 @@ withVerboseOption(program
|
|
|
17155
17310
|
.option('--vibe-authorize-url <url>', t('cli.command.upload.vibeAuthorizeUrl'))
|
|
17156
17311
|
.option('--poll-interval <seconds>', t('cli.command.upload.vibePollInterval'))
|
|
17157
17312
|
.option('--timeout <seconds>', t('cli.command.upload.vibeTimeout'))
|
|
17158
|
-
.option('--format <format>', t('cli.command.upload.format'), 'text')
|
|
17313
|
+
.option('--format <format>', t('cli.command.upload.format'), 'text')
|
|
17314
|
+
.addHelpText('after', t('cli.command.upload.vibeWebGuide'))).action(async (cmd) => {
|
|
17159
17315
|
if (cmd.vibe) {
|
|
17160
17316
|
const { vibeUpload } = await Promise.resolve().then(function () { return index$1; });
|
|
17161
17317
|
await vibeUpload(cmd);
|
|
@@ -17707,112 +17863,6 @@ var authOpen = /*#__PURE__*/Object.freeze({
|
|
|
17707
17863
|
vibeAuthOpen: vibeAuthOpen
|
|
17708
17864
|
});
|
|
17709
17865
|
|
|
17710
|
-
// Current PPE trial defaults: the owner-approved metadata used by the successful
|
|
17711
|
-
// real upload fixture on 2026-09-14. This is not a production metadata profile.
|
|
17712
|
-
const VIBE_PPE_DEFAULT_APP_INFO = Object.freeze({
|
|
17713
|
-
description: 'DevTool PPE integration test fixture. Not a production game.',
|
|
17714
|
-
category: 'Other',
|
|
17715
|
-
subcategory: 'Other',
|
|
17716
|
-
terms_of_service: 'https://example.invalid/devtool-ppe-test/terms',
|
|
17717
|
-
privacy_policy: 'https://example.invalid/devtool-ppe-test/privacy',
|
|
17718
|
-
copyright_confirmation: true,
|
|
17719
|
-
contact_email: 'devtool-test@example.invalid',
|
|
17720
|
-
});
|
|
17721
|
-
// BFF IDL + Import Service ValidateAppInfo, feat-minis-import (2026-09-14).
|
|
17722
|
-
// Keep credentials, app_name and icon_uri out of this file: CLI owns those fields.
|
|
17723
|
-
const requiredStrings = [
|
|
17724
|
-
'description', 'category', 'subcategory', 'terms_of_service', 'privacy_policy',
|
|
17725
|
-
];
|
|
17726
|
-
const optionalStrings = ['apple_team_id', 'contact_email', 'region', 'min_sdk_version'];
|
|
17727
|
-
function readVibeAppInfo(file) {
|
|
17728
|
-
if (file === undefined)
|
|
17729
|
-
return undefined;
|
|
17730
|
-
try {
|
|
17731
|
-
const stat = fs.statSync(file);
|
|
17732
|
-
if (!stat.isFile() || stat.size > 64 * 1024)
|
|
17733
|
-
throw new Error();
|
|
17734
|
-
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
17735
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
17736
|
-
throw new Error();
|
|
17737
|
-
const allowed = new Set([...requiredStrings, ...optionalStrings, 'copyright_confirmation']);
|
|
17738
|
-
if (Object.keys(value).some(key => !allowed.has(key)))
|
|
17739
|
-
throw new Error();
|
|
17740
|
-
// JSON can encode lone surrogates that Go rejects as invalid UTF-8 input.
|
|
17741
|
-
for (const item of Object.values(value)) {
|
|
17742
|
-
if (typeof item === 'string' && [...item].some(char => /^[\uD800-\uDFFF]$/.test(char)))
|
|
17743
|
-
throw new Error();
|
|
17744
|
-
}
|
|
17745
|
-
for (const key of requiredStrings) {
|
|
17746
|
-
if (typeof value[key] !== 'string' || !value[key].trim())
|
|
17747
|
-
throw new Error();
|
|
17748
|
-
}
|
|
17749
|
-
for (const key of optionalStrings) {
|
|
17750
|
-
if (value[key] !== undefined && (typeof value[key] !== 'string' || !value[key].trim()))
|
|
17751
|
-
throw new Error();
|
|
17752
|
-
}
|
|
17753
|
-
// An explicit declaration is required; never infer consent or supply a default.
|
|
17754
|
-
if (value.copyright_confirmation !== true)
|
|
17755
|
-
throw new Error();
|
|
17756
|
-
for (const key of ['terms_of_service', 'privacy_policy']) {
|
|
17757
|
-
if (/\s/.test(value[key]))
|
|
17758
|
-
throw new Error();
|
|
17759
|
-
const url = new URL(value[key]);
|
|
17760
|
-
if (!['http:', 'https:'].includes(url.protocol) || !url.hostname || url.username || url.password)
|
|
17761
|
-
throw new Error();
|
|
17762
|
-
}
|
|
17763
|
-
if (value.contact_email !== undefined && !/^[^\s<>@]+@[^\s<>@]+$/.test(value.contact_email))
|
|
17764
|
-
throw new Error();
|
|
17765
|
-
return value;
|
|
17766
|
-
}
|
|
17767
|
-
catch {
|
|
17768
|
-
// Do not expose paths, file contents or parser errors in structured output.
|
|
17769
|
-
throw new VibeError('VIBE_INVALID_APP_INFO');
|
|
17770
|
-
}
|
|
17771
|
-
}
|
|
17772
|
-
const VIBE_PLATFORM_ICON_LIMIT = 5 * 1024 * 1024;
|
|
17773
|
-
// Header preflight only. The BFF remains responsible for decoding the image.
|
|
17774
|
-
function validateVibeUploadInput(input) {
|
|
17775
|
-
if ([...input.title].length > 30)
|
|
17776
|
-
throw new VibeError('VIBE_TITLE_TOO_LONG');
|
|
17777
|
-
const image = input.icon.bytes;
|
|
17778
|
-
if (image.length > VIBE_PLATFORM_ICON_LIMIT)
|
|
17779
|
-
throw new VibeError('VIBE_PLATFORM_ICON_INVALID');
|
|
17780
|
-
let width = 0, height = 0;
|
|
17781
|
-
if (image.length >= 33 && image.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex')) &&
|
|
17782
|
-
image.readUInt32BE(8) === 13 && image.toString('ascii', 12, 16) === 'IHDR') {
|
|
17783
|
-
width = image.readUInt32BE(16);
|
|
17784
|
-
height = image.readUInt32BE(20);
|
|
17785
|
-
}
|
|
17786
|
-
else if (image[0] === 0xff && image[1] === 0xd8) {
|
|
17787
|
-
let offset = 2;
|
|
17788
|
-
while (offset + 4 <= image.length) {
|
|
17789
|
-
if (image[offset++] !== 0xff)
|
|
17790
|
-
break;
|
|
17791
|
-
while (image[offset] === 0xff)
|
|
17792
|
-
offset++;
|
|
17793
|
-
const marker = image[offset++];
|
|
17794
|
-
if (marker === 0xda || marker === 0xd9)
|
|
17795
|
-
break;
|
|
17796
|
-
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7))
|
|
17797
|
-
continue;
|
|
17798
|
-
if (offset + 2 > image.length)
|
|
17799
|
-
break;
|
|
17800
|
-
const length = image.readUInt16BE(offset);
|
|
17801
|
-
if (length < 2 || offset + length > image.length)
|
|
17802
|
-
break;
|
|
17803
|
-
// Go image/jpeg accepts baseline, extended sequential and progressive SOF.
|
|
17804
|
-
if ([0xc0, 0xc1, 0xc2].includes(marker) && length >= 8) {
|
|
17805
|
-
height = image.readUInt16BE(offset + 3);
|
|
17806
|
-
width = image.readUInt16BE(offset + 5);
|
|
17807
|
-
break;
|
|
17808
|
-
}
|
|
17809
|
-
offset += length;
|
|
17810
|
-
}
|
|
17811
|
-
}
|
|
17812
|
-
if (width !== 1024 || height !== 1024)
|
|
17813
|
-
throw new VibeError('VIBE_PLATFORM_ICON_INVALID');
|
|
17814
|
-
}
|
|
17815
|
-
|
|
17816
17866
|
// BFF IDL: feat-minis-import. PPE import flow; no production adapter.
|
|
17817
17867
|
const prefix = '/bff/app_info/minis_import/';
|
|
17818
17868
|
const MINIS_IMPORT_PATHS = {
|