@playdrop/playdrop-cli 0.11.8 → 0.12.0

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.
Files changed (116) hide show
  1. package/config/client-meta.json +2 -2
  2. package/dist/apps/build.d.ts +1 -1
  3. package/dist/apps/build.js +4 -6
  4. package/dist/apps/index.d.ts +4 -5
  5. package/dist/apps/index.js +14 -16
  6. package/dist/apps/loadCheck.d.ts +76 -0
  7. package/dist/apps/loadCheck.js +874 -0
  8. package/dist/apps/upload.d.ts +3 -2
  9. package/dist/apps/upload.js +112 -9
  10. package/dist/assetSpecs.js +2 -5
  11. package/dist/catalogue-utils.d.ts +1 -0
  12. package/dist/catalogue-utils.js +1 -0
  13. package/dist/catalogue.d.ts +3 -1
  14. package/dist/catalogue.js +19 -3
  15. package/dist/commands/browse.js +2 -2
  16. package/dist/commands/build.js +4 -1
  17. package/dist/commands/captureListing.d.ts +30 -1
  18. package/dist/commands/captureListing.js +308 -125
  19. package/dist/commands/check.d.ts +8 -0
  20. package/dist/commands/check.js +188 -0
  21. package/dist/commands/create.js +17 -15
  22. package/dist/commands/createRemixContent.js +2 -2
  23. package/dist/commands/detail.js +2 -2
  24. package/dist/commands/dev.js +9 -12
  25. package/dist/commands/devServer.d.ts +2 -2
  26. package/dist/commands/devServer.js +3 -4
  27. package/dist/commands/marketing.js +10 -11
  28. package/dist/commands/play.js +2 -2
  29. package/dist/commands/review.d.ts +20 -0
  30. package/dist/commands/review.js +112 -0
  31. package/dist/commands/upload-content.d.ts +1 -1
  32. package/dist/commands/upload-content.js +1 -1
  33. package/dist/commands/upload.d.ts +2 -1
  34. package/dist/commands/upload.js +9 -52
  35. package/dist/commands/validate.js +4 -161
  36. package/dist/commands/worker/runtime.d.ts +1 -0
  37. package/dist/commands/worker/runtime.js +48 -5
  38. package/dist/commands/worker.d.ts +5 -6
  39. package/dist/commands/worker.js +42 -92
  40. package/dist/config.js +1 -2
  41. package/dist/index.js +33 -49
  42. package/dist/playwright.d.ts +5 -0
  43. package/dist/playwright.js +60 -1
  44. package/node_modules/@playdrop/api-client/dist/client.d.ts +5 -3
  45. package/node_modules/@playdrop/api-client/dist/client.d.ts.map +1 -1
  46. package/node_modules/@playdrop/api-client/dist/client.js +10 -27
  47. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts +1 -1
  48. package/node_modules/@playdrop/api-client/dist/domains/admin.d.ts.map +1 -1
  49. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts +2 -1
  50. package/node_modules/@playdrop/api-client/dist/domains/apps.d.ts.map +1 -1
  51. package/node_modules/@playdrop/api-client/dist/domains/apps.js +11 -0
  52. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts +1 -1
  53. package/node_modules/@playdrop/api-client/dist/domains/assets.d.ts.map +1 -1
  54. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts +3 -1
  55. package/node_modules/@playdrop/api-client/dist/domains/auth.d.ts.map +1 -1
  56. package/node_modules/@playdrop/api-client/dist/domains/auth.js +22 -0
  57. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts +1 -1
  58. package/node_modules/@playdrop/api-client/dist/domains/payments.d.ts.map +1 -1
  59. package/node_modules/@playdrop/api-client/dist/index.d.ts +7 -6
  60. package/node_modules/@playdrop/api-client/dist/index.d.ts.map +1 -1
  61. package/node_modules/@playdrop/api-client/dist/index.js +11 -6
  62. package/node_modules/@playdrop/boxel-core/dist/src/entity-cleaner.js +0 -7
  63. package/node_modules/@playdrop/boxel-core/dist/src/entity-cleaner.js.map +1 -1
  64. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/textured-builder.js +2 -46
  65. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/textured-builder.js.map +1 -1
  66. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/voxel-builder.js +2 -22
  67. package/node_modules/@playdrop/boxel-core/dist/src/humanoid/r15/voxel-builder.js.map +1 -1
  68. package/node_modules/@playdrop/boxel-core/dist/src/palette_tools.js.map +1 -1
  69. package/node_modules/@playdrop/boxel-core/dist/src/serialization.js +1 -1
  70. package/node_modules/@playdrop/boxel-core/dist/src/serialization.js.map +1 -1
  71. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/infer-face.js.map +1 -1
  72. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/layer-mode.js +2 -2
  73. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/layer-mode.js.map +1 -1
  74. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/slice.js +0 -11
  75. package/node_modules/@playdrop/boxel-core/dist/src/transforms/textured-boxes/slice.js.map +1 -1
  76. package/node_modules/@playdrop/boxel-core/dist/src/transforms/upscale.js.map +1 -1
  77. package/node_modules/@playdrop/boxel-core/dist/src/validation.d.ts +1 -1
  78. package/node_modules/@playdrop/boxel-core/dist/src/validation.js +0 -20
  79. package/node_modules/@playdrop/boxel-core/dist/src/validation.js.map +1 -1
  80. package/node_modules/@playdrop/boxel-three/dist/src/exporters/glb.d.ts +2 -2
  81. package/node_modules/@playdrop/boxel-three/dist/src/exporters/glb.js +2 -12
  82. package/node_modules/@playdrop/boxel-three/dist/src/voxels/faces.js +1 -125
  83. package/node_modules/@playdrop/config/client-meta.json +2 -2
  84. package/node_modules/@playdrop/config/dist/src/client-meta.d.ts +8 -0
  85. package/node_modules/@playdrop/config/dist/src/client-meta.d.ts.map +1 -0
  86. package/node_modules/@playdrop/config/dist/src/client-meta.js +51 -0
  87. package/node_modules/@playdrop/config/dist/src/deployment.d.ts.map +1 -1
  88. package/node_modules/@playdrop/config/dist/src/deployment.js +2 -32
  89. package/node_modules/@playdrop/config/dist/src/index.d.ts +2 -2
  90. package/node_modules/@playdrop/config/dist/src/index.d.ts.map +1 -1
  91. package/node_modules/@playdrop/config/dist/src/index.js +3 -29
  92. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.d.ts +0 -1
  93. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.d.ts.map +1 -1
  94. package/node_modules/@playdrop/config/dist/src/runtime-service-origins.js +21 -5
  95. package/node_modules/@playdrop/config/dist/src/server/fastify.d.ts +2 -2
  96. package/node_modules/@playdrop/config/dist/src/server/fastify.d.ts.map +1 -1
  97. package/node_modules/@playdrop/config/dist/src/server/fastify.js +0 -1
  98. package/node_modules/@playdrop/config/dist/test/runtime-service-origins.test.js +8 -0
  99. package/node_modules/@playdrop/config/dist/tsconfig.tsbuildinfo +1 -1
  100. package/node_modules/@playdrop/types/dist/api.d.ts +25 -24
  101. package/node_modules/@playdrop/types/dist/api.d.ts.map +1 -1
  102. package/node_modules/@playdrop/types/dist/api.js +5 -10
  103. package/node_modules/@playdrop/types/dist/version.d.ts +14 -0
  104. package/node_modules/@playdrop/types/dist/version.d.ts.map +1 -1
  105. package/package.json +1 -1
  106. package/dist/apps/launchCheck.d.ts +0 -39
  107. package/dist/apps/launchCheck.js +0 -414
  108. package/dist/captureRuntime.d.ts +0 -73
  109. package/dist/captureRuntime.js +0 -1021
  110. package/dist/commands/capture.d.ts +0 -14
  111. package/dist/commands/capture.js +0 -550
  112. package/dist/commands/captureRemote.d.ts +0 -17
  113. package/dist/commands/captureRemote.js +0 -394
  114. package/node_modules/@playdrop/types/dist/entity.d.ts +0 -31
  115. package/node_modules/@playdrop/types/dist/entity.d.ts.map +0 -1
  116. package/node_modules/@playdrop/types/dist/entity.js +0 -67
@@ -0,0 +1,874 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSoftwareWebGlRenderer = isSoftwareWebGlRenderer;
4
+ exports.normalizeProjectCheckActions = normalizeProjectCheckActions;
5
+ exports.focusProjectCheckGameFrame = focusProjectCheckGameFrame;
6
+ exports.dispatchProjectCheckActionsToFrame = dispatchProjectCheckActionsToFrame;
7
+ exports.formatHostedLoadCheckFailure = formatHostedLoadCheckFailure;
8
+ exports.runLocalHostedLoadCheck = runLocalHostedLoadCheck;
9
+ exports.runUploadedHostedLoadCheck = runUploadedHostedLoadCheck;
10
+ const promises_1 = require("node:fs/promises");
11
+ const node_path_1 = require("node:path");
12
+ const appUrls_1 = require("../appUrls");
13
+ const devShared_1 = require("../commands/devShared");
14
+ const dev_1 = require("../commands/dev");
15
+ const devServer_1 = require("../commands/devServer");
16
+ const devRuntimeAssets_1 = require("../commands/devRuntimeAssets");
17
+ const devAuth_1 = require("../devAuth");
18
+ const playwright_1 = require("../playwright");
19
+ const sessionCookie_1 = require("../sessionCookie");
20
+ const registration_1 = require("./registration");
21
+ const FRAME_SELECTOR = 'iframe[title="Game"]';
22
+ const DEFAULT_HOSTED_LOAD_TIMEOUT_MS = 15000;
23
+ const POST_READY_SETTLE_MS = 750;
24
+ const MOBILE_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1';
25
+ const LOAD_CHECK_SURFACE_ORDER = ['DESKTOP', 'MOBILE_LANDSCAPE', 'MOBILE_PORTRAIT'];
26
+ const LOAD_CHECK_SURFACE_CONTEXT_OPTIONS = {
27
+ DESKTOP: {
28
+ viewport: { width: 1280, height: 720 },
29
+ deviceScaleFactor: 1,
30
+ isMobile: false,
31
+ hasTouch: false,
32
+ },
33
+ MOBILE_LANDSCAPE: {
34
+ viewport: { width: 896, height: 414 },
35
+ deviceScaleFactor: 2,
36
+ isMobile: true,
37
+ hasTouch: true,
38
+ userAgent: MOBILE_USER_AGENT,
39
+ },
40
+ MOBILE_PORTRAIT: {
41
+ viewport: { width: 414, height: 896 },
42
+ deviceScaleFactor: 2,
43
+ isMobile: true,
44
+ hasTouch: true,
45
+ userAgent: MOBILE_USER_AGENT,
46
+ },
47
+ };
48
+ const GOOGLE_TELEMETRY_HOSTS = [
49
+ 'analytics.google.com',
50
+ 'googletagmanager.com',
51
+ 'google-analytics.com',
52
+ 'googleadservices.com',
53
+ 'doubleclick.net',
54
+ ];
55
+ const SOFTWARE_WEBGL_RENDERER_PATTERNS = [
56
+ /\bswiftshader\b/i,
57
+ /\bllvmpipe\b/i,
58
+ /\bsoftware\b/i,
59
+ /\bswrast\b/i,
60
+ /\bmesa offscreen\b/i,
61
+ /\bsoftpipe\b/i,
62
+ /\bchromium rasterizer\b/i,
63
+ ];
64
+ function isHostOrSubdomain(hostname, domain) {
65
+ return hostname === domain || hostname.endsWith(`.${domain}`);
66
+ }
67
+ function isKnownGoogleTelemetryUrl(rawUrl) {
68
+ let parsed;
69
+ try {
70
+ parsed = new URL(rawUrl);
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ const hostname = parsed.hostname.toLowerCase();
76
+ if (hostname === 'accounts.google.com' || hostname.endsWith('.accounts.google.com')) {
77
+ return false;
78
+ }
79
+ return GOOGLE_TELEMETRY_HOSTS.some((domain) => isHostOrSubdomain(hostname, domain));
80
+ }
81
+ function formatConsoleValue(value) {
82
+ if (typeof value === 'string')
83
+ return value;
84
+ if (typeof value === 'number' || typeof value === 'boolean' || value === null) {
85
+ return String(value);
86
+ }
87
+ if (typeof value === 'undefined') {
88
+ return 'undefined';
89
+ }
90
+ if (typeof value === 'object') {
91
+ try {
92
+ return JSON.stringify(value);
93
+ }
94
+ catch {
95
+ return '[object]';
96
+ }
97
+ }
98
+ return String(value);
99
+ }
100
+ function serializePayload(payload) {
101
+ if (payload === undefined) {
102
+ return '';
103
+ }
104
+ if (typeof payload === 'string') {
105
+ return payload;
106
+ }
107
+ try {
108
+ return JSON.stringify(payload);
109
+ }
110
+ catch {
111
+ return String(payload);
112
+ }
113
+ }
114
+ function normalizeHostedLaunchStatePayload(payload) {
115
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
116
+ return null;
117
+ }
118
+ const state = typeof payload.state === 'string'
119
+ ? payload.state.trim().toLowerCase()
120
+ : '';
121
+ if (state === 'ready') {
122
+ return { state: 'ready' };
123
+ }
124
+ if (state === 'login_required') {
125
+ return { state: 'login_required' };
126
+ }
127
+ if (state === 'controller_required') {
128
+ return { state: 'controller_required' };
129
+ }
130
+ if (state === 'surface_unsupported') {
131
+ return { state: 'surface_unsupported' };
132
+ }
133
+ if (state !== 'error') {
134
+ return null;
135
+ }
136
+ const errorCode = typeof payload.errorCode === 'string'
137
+ ? payload.errorCode.trim() || null
138
+ : null;
139
+ const message = typeof payload.message === 'string'
140
+ ? payload.message.trim() || null
141
+ : null;
142
+ return {
143
+ state: 'error',
144
+ errorCode,
145
+ message,
146
+ };
147
+ }
148
+ function formatHostedLaunchError(state) {
149
+ const parts = ['Hosted app failed to launch.'];
150
+ if (state.errorCode) {
151
+ parts.push(`errorCode=${state.errorCode}.`);
152
+ }
153
+ if (state.message) {
154
+ parts.push(state.message);
155
+ }
156
+ return parts.join(' ');
157
+ }
158
+ function formatHostedLaunchStateMismatch(actualState, expectedState) {
159
+ if (expectedState === 'login_required') {
160
+ return `Hosted app reported ${actualState} instead of login_required.`;
161
+ }
162
+ if (expectedState === 'controller_required') {
163
+ return `Hosted app reported ${actualState} instead of controller_required.`;
164
+ }
165
+ if (expectedState === 'surface_unsupported') {
166
+ return `Hosted app reported ${actualState} instead of surface_unsupported.`;
167
+ }
168
+ return `Hosted app reported ${actualState} instead of ready.`;
169
+ }
170
+ function shouldSettleHostedLaunchWaiter(state, expectedState) {
171
+ if (state.state === 'error' || !expectedState) {
172
+ return true;
173
+ }
174
+ return state.state === expectedState;
175
+ }
176
+ function normalizeLoadCheckSurface(value) {
177
+ if (typeof value !== 'string') {
178
+ return null;
179
+ }
180
+ const normalized = value.trim().toUpperCase();
181
+ return LOAD_CHECK_SURFACE_ORDER.includes(normalized)
182
+ ? normalized
183
+ : null;
184
+ }
185
+ function normalizeLoadCheckSurfaceTargets(value) {
186
+ if (!Array.isArray(value)) {
187
+ return [];
188
+ }
189
+ return Array.from(new Set(value.map(normalizeLoadCheckSurface).filter((entry) => Boolean(entry))));
190
+ }
191
+ function resolveLoadCheckSurface(surfaceTargets) {
192
+ const targets = normalizeLoadCheckSurfaceTargets(surfaceTargets);
193
+ for (const surface of LOAD_CHECK_SURFACE_ORDER) {
194
+ if (targets.includes(surface)) {
195
+ return surface;
196
+ }
197
+ }
198
+ return 'DESKTOP';
199
+ }
200
+ function cloneLoadCheckContextOptions(surface) {
201
+ const source = LOAD_CHECK_SURFACE_CONTEXT_OPTIONS[surface];
202
+ return {
203
+ ...source,
204
+ viewport: source.viewport ? { ...source.viewport } : source.viewport,
205
+ };
206
+ }
207
+ function resolvePostAuthHostedLaunchState(controllerMode) {
208
+ return controllerMode === 'REQUIRED' ? 'controller_required' : 'ready';
209
+ }
210
+ function resolveAnonymousHostedLaunchState(input) {
211
+ const surfaceTargets = normalizeLoadCheckSurfaceTargets(input.surfaceTargets);
212
+ if (!surfaceTargets.includes(input.captureSurface)) {
213
+ return 'surface_unsupported';
214
+ }
215
+ if (input.authMode === 'REQUIRED') {
216
+ return 'login_required';
217
+ }
218
+ return resolvePostAuthHostedLaunchState(input.controllerMode);
219
+ }
220
+ function normalizeLoadCheckMessage(message) {
221
+ const normalized = typeof message === 'string' ? message.trim() : '';
222
+ return normalized || 'Hosted app failed to launch.';
223
+ }
224
+ function buildPassedLoadCheckResult(input) {
225
+ return {
226
+ status: 'PASSED',
227
+ errorCode: null,
228
+ message: null,
229
+ checkedAt: new Date().toISOString(),
230
+ artifactFingerprint: input.artifactFingerprint,
231
+ screenshotPath: input.screenshotPath ?? null,
232
+ webglRenderer: input.webglRenderer ?? null,
233
+ targetUrl: input.targetUrl ?? null,
234
+ warnings: input.warnings ?? [],
235
+ errors: [],
236
+ };
237
+ }
238
+ function buildFailedLoadCheckResult(errorCode, message, artifactFingerprint, extra = {}) {
239
+ return {
240
+ status: 'FAILED',
241
+ errorCode,
242
+ message,
243
+ checkedAt: new Date().toISOString(),
244
+ artifactFingerprint,
245
+ screenshotPath: extra.screenshotPath ?? null,
246
+ webglRenderer: extra.webglRenderer ?? null,
247
+ targetUrl: extra.targetUrl ?? null,
248
+ errors: extra.errors ?? [],
249
+ warnings: extra.warnings ?? [],
250
+ };
251
+ }
252
+ function formatLoadCheckErrors(errors) {
253
+ if (!Array.isArray(errors) || errors.length === 0) {
254
+ return '';
255
+ }
256
+ const normalized = errors
257
+ .map((entry) => entry.replace(/\s+/g, ' ').trim())
258
+ .filter(Boolean)
259
+ .slice(0, 3)
260
+ .map((entry) => entry.length > 240 ? `${entry.slice(0, 237)}...` : entry);
261
+ if (normalized.length === 0) {
262
+ return '';
263
+ }
264
+ return ` First error${normalized.length === 1 ? '' : 's'}: ${normalized.join(' | ')}`;
265
+ }
266
+ function isSoftwareWebGlRenderer(renderer) {
267
+ const normalized = typeof renderer === 'string' ? renderer.trim() : '';
268
+ return normalized.length > 0 && SOFTWARE_WEBGL_RENDERER_PATTERNS.some((pattern) => pattern.test(normalized));
269
+ }
270
+ function normalizeProjectCheckActions(value) {
271
+ if (value === undefined || value === null) {
272
+ return [];
273
+ }
274
+ if (!Array.isArray(value)) {
275
+ throw new Error('project_check_actions_invalid: expected a JSON array.');
276
+ }
277
+ return value.map((entry, index) => {
278
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
279
+ throw new Error(`project_check_action_invalid:${index}`);
280
+ }
281
+ const type = typeof entry.type === 'string'
282
+ ? entry.type.trim().toLowerCase()
283
+ : '';
284
+ if (type === 'click') {
285
+ const x = Number(entry.x);
286
+ const y = Number(entry.y);
287
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
288
+ throw new Error(`project_check_action_invalid:${index}:click_coordinates_required`);
289
+ }
290
+ const rawButton = typeof entry.button === 'string'
291
+ ? entry.button.trim().toLowerCase()
292
+ : '';
293
+ const button = rawButton === 'right' || rawButton === 'middle' ? rawButton : 'left';
294
+ return { type: 'click', x, y, button };
295
+ }
296
+ if (type === 'press') {
297
+ const key = typeof entry.key === 'string'
298
+ ? entry.key.trim()
299
+ : '';
300
+ if (!key) {
301
+ throw new Error(`project_check_action_invalid:${index}:key_required`);
302
+ }
303
+ return { type: 'press', key };
304
+ }
305
+ if (type === 'wait') {
306
+ const ms = Number(entry.ms);
307
+ if (!Number.isInteger(ms) || ms < 0 || ms > 60000) {
308
+ throw new Error(`project_check_action_invalid:${index}:wait_ms_invalid`);
309
+ }
310
+ return { type: 'wait', ms };
311
+ }
312
+ throw new Error(`project_check_action_type_unknown:${index}`);
313
+ });
314
+ }
315
+ async function focusProjectCheckGameFrame(page, frame) {
316
+ const element = await frame.frameElement();
317
+ try {
318
+ await element.scrollIntoViewIfNeeded().catch(() => { });
319
+ const box = await element.boundingBox();
320
+ if (!box) {
321
+ throw new Error('project_check_game_frame_not_visible');
322
+ }
323
+ await page.mouse.click(box.x + (box.width / 2), box.y + (box.height / 2), { button: 'left' });
324
+ await frame.evaluate(() => window.focus()).catch(() => { });
325
+ return box;
326
+ }
327
+ finally {
328
+ await element.dispose().catch(() => { });
329
+ }
330
+ }
331
+ async function dispatchProjectCheckActionsToFrame(page, frame, actions) {
332
+ for (const action of actions) {
333
+ if (action.type === 'wait') {
334
+ await page.waitForTimeout(action.ms);
335
+ continue;
336
+ }
337
+ const box = await focusProjectCheckGameFrame(page, frame);
338
+ if (action.type === 'click') {
339
+ await page.mouse.click(box.x + action.x, box.y + action.y, { button: action.button ?? 'left' });
340
+ continue;
341
+ }
342
+ if (action.type === 'press') {
343
+ await page.keyboard.press(action.key);
344
+ continue;
345
+ }
346
+ throw new Error('project_check_action_type_unknown');
347
+ }
348
+ }
349
+ async function findGameFrame(page, timeoutMs) {
350
+ const handle = await page.waitForSelector(FRAME_SELECTOR, { timeout: timeoutMs });
351
+ const frame = await handle.contentFrame();
352
+ await handle.dispose().catch(() => { });
353
+ if (!frame) {
354
+ throw new Error('project_check_game_frame_missing');
355
+ }
356
+ return frame;
357
+ }
358
+ async function readWebGlRenderer(frame) {
359
+ const renderer = await frame.evaluate(() => {
360
+ const canvas = document.createElement('canvas');
361
+ const gl = (canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
362
+ if (!gl) {
363
+ return '';
364
+ }
365
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
366
+ if (debugInfo) {
367
+ const unmaskedRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
368
+ if (typeof unmaskedRenderer === 'string' && unmaskedRenderer.trim()) {
369
+ return unmaskedRenderer;
370
+ }
371
+ }
372
+ const rendererValue = gl.getParameter(gl.RENDERER);
373
+ return typeof rendererValue === 'string' ? rendererValue : String(rendererValue ?? '');
374
+ });
375
+ const normalized = typeof renderer === 'string' ? renderer.trim() : '';
376
+ if (!normalized) {
377
+ throw new Error('project_check_webgl_renderer_missing');
378
+ }
379
+ return normalized;
380
+ }
381
+ async function saveScreenshot(page, screenshotPath) {
382
+ const normalizedPath = typeof screenshotPath === 'string' ? screenshotPath.trim() : '';
383
+ if (!normalizedPath) {
384
+ return null;
385
+ }
386
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(normalizedPath), { recursive: true });
387
+ await page.screenshot({ path: normalizedPath, fullPage: true });
388
+ console.log(`[check] Saved screenshot to ${(0, node_path_1.relative)(process.cwd(), normalizedPath) || normalizedPath}`);
389
+ return normalizedPath;
390
+ }
391
+ function normalizeLoadCheckFailure(error, artifactFingerprint) {
392
+ const rawMessage = error instanceof Error ? error.message : String(error);
393
+ const message = normalizeLoadCheckMessage(rawMessage);
394
+ const errorCodeMatch = message.match(/\berrorCode=([a-z0-9_]+)\b/i);
395
+ if (errorCodeMatch?.[1]) {
396
+ return buildFailedLoadCheckResult(errorCodeMatch[1], message, artifactFingerprint);
397
+ }
398
+ const prefixedCodeMatch = message.match(/^([a-z0-9_]+):/i);
399
+ if (prefixedCodeMatch?.[1]
400
+ && (prefixedCodeMatch[1].startsWith('hosted_app_')
401
+ || prefixedCodeMatch[1].startsWith('local_dev_')
402
+ || prefixedCodeMatch[1].startsWith('dev_auth_')
403
+ || prefixedCodeMatch[1].startsWith('project_check_'))) {
404
+ return buildFailedLoadCheckResult(prefixedCodeMatch[1], message, artifactFingerprint);
405
+ }
406
+ if (/missing_init\b/i.test(message) || /hosted_app_missing_client_init:/i.test(message)) {
407
+ return buildFailedLoadCheckResult('missing_init', message, artifactFingerprint);
408
+ }
409
+ if (/missing_ready\b/i.test(message) || /hosted_app_missing_ready:/i.test(message)) {
410
+ return buildFailedLoadCheckResult('missing_ready', message, artifactFingerprint);
411
+ }
412
+ if (/sdk_mismatch\b/i.test(message) || /hosted_app_sdk_version_mismatch:/i.test(message)) {
413
+ return buildFailedLoadCheckResult('sdk_mismatch', message, artifactFingerprint);
414
+ }
415
+ if (/software renderer|swiftshader|webgl_renderer_software/i.test(message)) {
416
+ return buildFailedLoadCheckResult('webgl_renderer_software', message, artifactFingerprint);
417
+ }
418
+ return buildFailedLoadCheckResult('startup_failure', message, artifactFingerprint);
419
+ }
420
+ function formatHostedLoadCheckFailure(taskName, result, scope = 'local') {
421
+ const scopeLabel = scope === 'final'
422
+ ? 'final owner play route'
423
+ : scope === 'staged'
424
+ ? 'staged upload preview'
425
+ : 'local hosted preview';
426
+ const errorCode = result.errorCode?.trim() || 'startup_failure';
427
+ const message = normalizeLoadCheckMessage(result.message);
428
+ return `hosted_load_check_failed: ${taskName} failed the ${scopeLabel} check (${errorCode}). ${message}`;
429
+ }
430
+ async function runHostedLoadCheck(options) {
431
+ const expectedHostedLaunchState = options.expectedHostedLaunchState ?? 'ready';
432
+ const artifactFingerprint = options.artifactFingerprint ?? null;
433
+ const timeoutMs = options.timeoutMs ?? DEFAULT_HOSTED_LOAD_TIMEOUT_MS;
434
+ const errors = [];
435
+ const warnings = [];
436
+ let finalUrl = options.targetUrl;
437
+ let screenshotPath = null;
438
+ let webglRenderer = null;
439
+ try {
440
+ await (0, playwright_1.withInstalledChromePage)(async ({ context, page }) => {
441
+ const hasExplicitToken = Boolean(options.token?.trim());
442
+ const shouldBootstrapSavedSession = options.savedSessionBootstrap !== false && hasExplicitToken;
443
+ const bootstrapToken = shouldBootstrapSavedSession ? options.token?.trim() || null : null;
444
+ const bootstrapUser = shouldBootstrapSavedSession ? options.user ?? null : null;
445
+ let hostedLaunchWaiterSettled = false;
446
+ let resolveHostedLaunchWaiter = null;
447
+ const hostedLaunchWaiter = new Promise((resolve) => {
448
+ resolveHostedLaunchWaiter = resolve;
449
+ });
450
+ const settleHostedLaunchWaiter = (state) => {
451
+ if (hostedLaunchWaiterSettled) {
452
+ return;
453
+ }
454
+ if (!shouldSettleHostedLaunchWaiter(state, expectedHostedLaunchState)) {
455
+ return;
456
+ }
457
+ hostedLaunchWaiterSettled = true;
458
+ resolveHostedLaunchWaiter?.(state);
459
+ };
460
+ if (bootstrapToken) {
461
+ await context.addCookies((0, sessionCookie_1.buildCaptureAccessTokenCookies)(options.targetUrl, bootstrapToken));
462
+ }
463
+ if (bootstrapToken || bootstrapUser) {
464
+ await page.addInitScript(({ token, user }) => {
465
+ try {
466
+ if (token) {
467
+ window.localStorage.setItem('playdrop.accessToken', token);
468
+ }
469
+ }
470
+ catch {
471
+ // Storage can be blocked by browser policy; the cookie remains authoritative.
472
+ }
473
+ try {
474
+ if (user) {
475
+ window.localStorage.setItem('playdrop.user', JSON.stringify(user));
476
+ }
477
+ }
478
+ catch {
479
+ // Storage can be blocked by browser policy; the cookie remains authoritative.
480
+ }
481
+ try {
482
+ window.sessionStorage.removeItem('playdrop.logoutReason');
483
+ }
484
+ catch {
485
+ // Ignore session storage failures.
486
+ }
487
+ }, { token: bootstrapToken, user: bootstrapUser });
488
+ }
489
+ await page.exposeBinding('__playdropCaptureLog', async (_source, type, payload) => {
490
+ if (type === 'hosted-launch-state') {
491
+ const normalizedHostedState = normalizeHostedLaunchStatePayload(payload);
492
+ if (normalizedHostedState) {
493
+ settleHostedLaunchWaiter(normalizedHostedState);
494
+ }
495
+ }
496
+ const serialized = serializePayload(payload);
497
+ const line = ['[check][custom]', typeof type === 'string' ? type.toLowerCase() : 'info', serialized].filter(Boolean).join(' ');
498
+ console.log(line);
499
+ });
500
+ await page.addInitScript(() => {
501
+ window.addEventListener('unhandledrejection', (event) => {
502
+ try {
503
+ console.error('[check][unhandledrejection]', event.reason);
504
+ }
505
+ catch {
506
+ console.error('[check][unhandledrejection]');
507
+ }
508
+ });
509
+ const bridgeWindow = window;
510
+ bridgeWindow.playdrop = bridgeWindow.playdrop || {};
511
+ bridgeWindow.__playdropCaptureHostedLaunchState = null;
512
+ const bridge = (type, payload) => {
513
+ if (type === 'hosted-launch-state') {
514
+ bridgeWindow.__playdropCaptureHostedLaunchState = payload;
515
+ }
516
+ try {
517
+ const binding = bridgeWindow.__playdropCaptureLog;
518
+ if (typeof binding === 'function') {
519
+ binding(type, payload);
520
+ }
521
+ }
522
+ catch (error) {
523
+ console.error('[check][bridge-error]', error);
524
+ }
525
+ };
526
+ Object.defineProperty(bridgeWindow.playdrop, 'capture', {
527
+ value: bridge,
528
+ configurable: true,
529
+ writable: true,
530
+ });
531
+ });
532
+ const handleConsoleMessage = async (message) => {
533
+ const type = message.type();
534
+ const args = await Promise.all(message.args().map((arg) => arg.jsonValue().catch(() => arg.toString())));
535
+ const rendered = args.length > 0 ? args.map(formatConsoleValue).join(' ') : message.text();
536
+ const line = `[check][console:${type}] ${rendered}`;
537
+ if (type === 'error' || type === 'assert' || type === 'trace') {
538
+ errors.push(line);
539
+ console.error(line);
540
+ return;
541
+ }
542
+ console.log(line);
543
+ };
544
+ page.on('console', (message) => {
545
+ void handleConsoleMessage(message).catch((error) => {
546
+ const text = error instanceof Error ? error.message : String(error);
547
+ errors.push(`[check][console-handler] ${text}`);
548
+ });
549
+ });
550
+ page.on('pageerror', (error) => {
551
+ const text = error?.message ?? String(error);
552
+ errors.push(`[check][pageerror] ${text}`);
553
+ console.error(`[check][pageerror] ${text}`);
554
+ });
555
+ page.on('requestfailed', (request) => {
556
+ const failure = request.failure();
557
+ const errorText = failure?.errorText ?? '';
558
+ const text = `${request.method()} ${request.url()} - ${errorText || 'failed'}`;
559
+ if (isKnownGoogleTelemetryUrl(request.url())) {
560
+ warnings.push(`[check][requestfailed] ${text}`);
561
+ return;
562
+ }
563
+ if (/ERR_ABORTED/i.test(errorText) || /ERR_HTTP2_PROTOCOL_ERROR/i.test(errorText)) {
564
+ return;
565
+ }
566
+ errors.push(`[check][requestfailed] ${text}`);
567
+ console.error(`[check][requestfailed] ${text}`);
568
+ });
569
+ page.on('response', (response) => {
570
+ const status = response.status();
571
+ if (status < 400) {
572
+ return;
573
+ }
574
+ const text = `${status} ${response.statusText()} ${response.url()}`;
575
+ if (isKnownGoogleTelemetryUrl(response.url())) {
576
+ warnings.push(`[check][response] ${text}`);
577
+ return;
578
+ }
579
+ errors.push(`[check][response] ${text}`);
580
+ console.error(`[check][response] ${text}`);
581
+ });
582
+ console.log(`[check] Opening ${options.targetUrl}`);
583
+ const response = await page.goto(options.targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
584
+ if (response && !response.ok()) {
585
+ errors.push(`[check][navigation] ${response.status()} ${response.statusText()} ${options.targetUrl}`);
586
+ }
587
+ const readinessTimeoutMs = Math.min(Math.max(timeoutMs, 1000), 30000);
588
+ await page.waitForFunction(({ frameSelector }) => {
589
+ const frame = document.querySelector(frameSelector);
590
+ const state = window.__playdropCaptureHostedLaunchState;
591
+ if (frame) {
592
+ return 'frame';
593
+ }
594
+ if (state && typeof state.state === 'string' && state.state.trim().length > 0) {
595
+ return `hosted:${state.state.trim().toLowerCase()}`;
596
+ }
597
+ return null;
598
+ }, { frameSelector: FRAME_SELECTOR }, { timeout: readinessTimeoutMs });
599
+ const launchState = await Promise.race([
600
+ hostedLaunchWaiter,
601
+ page.waitForTimeout(timeoutMs).then(() => {
602
+ throw new Error(expectedHostedLaunchState === 'ready'
603
+ ? 'hosted_app_missing_ready: Hosted app did not report ready before the load-check timeout.'
604
+ : `hosted_app_gate_missing: Hosted app did not report ${expectedHostedLaunchState} before the load-check timeout.`);
605
+ }),
606
+ ]);
607
+ if (launchState.state === 'error') {
608
+ throw new Error(formatHostedLaunchError(launchState));
609
+ }
610
+ if (launchState.state !== expectedHostedLaunchState) {
611
+ throw new Error(formatHostedLaunchStateMismatch(launchState.state, expectedHostedLaunchState));
612
+ }
613
+ finalUrl = page.url();
614
+ if (expectedHostedLaunchState !== 'ready') {
615
+ screenshotPath = await saveScreenshot(page, options.screenshotPath);
616
+ return;
617
+ }
618
+ const frame = await findGameFrame(page, readinessTimeoutMs);
619
+ webglRenderer = await readWebGlRenderer(frame);
620
+ console.log(`[check] WebGL renderer: ${webglRenderer}`);
621
+ if (isSoftwareWebGlRenderer(webglRenderer)) {
622
+ throw new Error(`project_check_webgl_renderer_software: WebGL renderer is "${webglRenderer}". Real hardware acceleration is required.`);
623
+ }
624
+ if (options.actions?.length) {
625
+ await dispatchProjectCheckActionsToFrame(page, frame, options.actions);
626
+ await page.waitForTimeout(POST_READY_SETTLE_MS);
627
+ }
628
+ screenshotPath = await saveScreenshot(page, options.screenshotPath);
629
+ }, {
630
+ ...options.contextOptions,
631
+ automationOrigin: new URL(options.targetUrl).origin,
632
+ });
633
+ }
634
+ catch (error) {
635
+ const launchFailureMessage = error instanceof Error ? error.message : String(error);
636
+ if (/Failed to launch installed Google Chrome/i.test(launchFailureMessage)) {
637
+ throw error;
638
+ }
639
+ const normalized = normalizeLoadCheckFailure(error, artifactFingerprint);
640
+ return {
641
+ ...normalized,
642
+ screenshotPath,
643
+ webglRenderer,
644
+ targetUrl: finalUrl,
645
+ errors: errors.length > 0 ? errors : normalized.errors,
646
+ warnings,
647
+ message: normalized.message ? `${normalized.message}${formatLoadCheckErrors(errors)}` : normalized.message,
648
+ };
649
+ }
650
+ if (errors.length > 0) {
651
+ return buildFailedLoadCheckResult('startup_failure', `Hosted app emitted ${errors.length} console, page, or network error(s) during startup.${formatLoadCheckErrors(errors)}`, artifactFingerprint, {
652
+ screenshotPath,
653
+ webglRenderer,
654
+ targetUrl: finalUrl,
655
+ errors,
656
+ warnings,
657
+ });
658
+ }
659
+ return buildPassedLoadCheckResult({
660
+ artifactFingerprint,
661
+ screenshotPath,
662
+ webglRenderer,
663
+ targetUrl: finalUrl,
664
+ warnings,
665
+ });
666
+ }
667
+ async function startOrReuseMountedDevRuntime(input) {
668
+ const appTypeSlug = (0, appUrls_1.getAppTypeSlug)(input.task.type ?? 'GAME');
669
+ const devRouterPort = input.devRouterPort ?? devServer_1.DEV_ROUTER_PORT;
670
+ const runtimeAssetManifest = input.task.hostingMode === 'EXTERNAL' || input.task.externalUrl
671
+ ? (0, devRuntimeAssets_1.createEmptyDevRuntimeAssetManifest)()
672
+ : await (0, devRuntimeAssets_1.buildDevRuntimeAssetManifest)({
673
+ client: input.client,
674
+ apiBase: input.apiBase,
675
+ task: input.task,
676
+ creatorUsername: input.creatorUsername,
677
+ appBaseUrl: `http://127.0.0.1:${devRouterPort}/apps/dev/${encodeURIComponent(input.creatorUsername)}/${encodeURIComponent(appTypeSlug)}/${encodeURIComponent(input.task.name)}/`,
678
+ });
679
+ const serverAlreadyRunning = await (0, devServer_1.isDevServerAvailable)({
680
+ creatorUsername: input.creatorUsername,
681
+ appType: appTypeSlug,
682
+ appName: input.task.name,
683
+ port: devRouterPort,
684
+ }, 750);
685
+ if (serverAlreadyRunning) {
686
+ await (0, devServer_1.updateMountedDevRuntimeAssetManifest)({
687
+ creatorUsername: input.creatorUsername,
688
+ appName: input.task.name,
689
+ runtimeAssetManifest,
690
+ port: devRouterPort,
691
+ });
692
+ return { handle: null, runtimeAssetManifest };
693
+ }
694
+ try {
695
+ const handle = await (0, devServer_1.startDevServer)({
696
+ appName: input.task.name,
697
+ appType: appTypeSlug,
698
+ creatorUsername: input.creatorUsername,
699
+ htmlPath: input.task.filePath,
700
+ port: devRouterPort,
701
+ projectInfo: (0, devShared_1.findProjectInfo)(input.task.filePath),
702
+ runtimeAssetManifest,
703
+ });
704
+ await new Promise((resolve) => setTimeout(resolve, 1000));
705
+ return { handle, runtimeAssetManifest };
706
+ }
707
+ catch (error) {
708
+ const formatted = (0, dev_1.formatDevRuntimeAssetManifestFailure)(error);
709
+ const mountConflict = (0, devServer_1.parseMountConflictError)(error instanceof Error ? error.message : '');
710
+ if (mountConflict) {
711
+ throw new Error(`A different dev session already owns ${mountConflict.ref}. Active owner pid: ${mountConflict.ownerPid}.`);
712
+ }
713
+ throw new Error(formatted.message);
714
+ }
715
+ }
716
+ async function runLocalHostedLoadCheck(input) {
717
+ let mountedRuntime = null;
718
+ try {
719
+ mountedRuntime = await startOrReuseMountedDevRuntime({
720
+ client: input.client,
721
+ apiBase: input.apiBase,
722
+ creatorUsername: input.creatorUsername,
723
+ task: input.task,
724
+ devRouterPort: input.devRouterPort,
725
+ });
726
+ const localDevPort = input.devRouterPort === undefined || input.devRouterPort === devServer_1.DEV_ROUTER_PORT
727
+ ? null
728
+ : input.devRouterPort;
729
+ const localAppMetadata = mountedRuntime.runtimeAssetManifest.response.localAppMetadata;
730
+ const captureSurface = resolveLoadCheckSurface(localAppMetadata.surfaceTargets);
731
+ const contextOptions = cloneLoadCheckContextOptions(captureSurface);
732
+ const anonymousExpectedState = resolveAnonymousHostedLaunchState({
733
+ ...localAppMetadata,
734
+ captureSurface,
735
+ });
736
+ if (anonymousExpectedState === 'login_required') {
737
+ const registeredApp = await (0, registration_1.fetchRegisteredAppShell)(input.client, input.creatorUsername, input.task.name);
738
+ const canLaunchUnregisteredViewer = input.allowUnregisteredViewerLaunch === true
739
+ && Boolean(input.token)
740
+ && Boolean(input.currentUser);
741
+ if (!registeredApp?.id && !canLaunchUnregisteredViewer) {
742
+ return buildFailedLoadCheckResult('app_registration_required_for_auth_validation', `Auth-required hosted app "${input.task.name}" must be registered on PlayDrop before viewer load-check can run. Run "playdrop project create app ${input.task.name}" and try again.`, null);
743
+ }
744
+ const anonymousGateResult = await runHostedLoadCheck({
745
+ targetUrl: (0, appUrls_1.buildPlatformDevUrl)(input.webBase, {
746
+ creatorUsername: input.creatorUsername,
747
+ appName: input.task.name,
748
+ appType: input.task.type ?? 'GAME',
749
+ devAuth: 'anonymous',
750
+ launchCheck: true,
751
+ localDevPort,
752
+ }),
753
+ timeoutMs: input.timeoutMs,
754
+ expectedHostedLaunchState: anonymousExpectedState,
755
+ contextOptions,
756
+ });
757
+ if (anonymousGateResult.status !== 'PASSED') {
758
+ return anonymousGateResult;
759
+ }
760
+ return await runHostedLoadCheck({
761
+ targetUrl: (0, appUrls_1.buildPlatformDevUrl)(input.webBase, {
762
+ creatorUsername: input.creatorUsername,
763
+ appName: input.task.name,
764
+ appType: input.task.type ?? 'GAME',
765
+ devAuth: 'viewer',
766
+ launchCheck: true,
767
+ localDevPort,
768
+ }),
769
+ timeoutMs: input.timeoutMs,
770
+ expectedHostedLaunchState: resolvePostAuthHostedLaunchState(localAppMetadata.controllerMode),
771
+ token: input.token ?? null,
772
+ user: input.currentUser ?? null,
773
+ savedSessionBootstrap: true,
774
+ contextOptions,
775
+ screenshotPath: input.screenshotPath,
776
+ actions: input.actions,
777
+ });
778
+ }
779
+ return await runHostedLoadCheck({
780
+ targetUrl: (0, appUrls_1.buildPlatformDevUrl)(input.webBase, {
781
+ creatorUsername: input.creatorUsername,
782
+ appName: input.task.name,
783
+ appType: input.task.type ?? 'GAME',
784
+ devAuth: 'anonymous',
785
+ launchCheck: true,
786
+ localDevPort,
787
+ }),
788
+ timeoutMs: input.timeoutMs,
789
+ expectedHostedLaunchState: anonymousExpectedState,
790
+ contextOptions,
791
+ screenshotPath: input.screenshotPath,
792
+ actions: input.actions,
793
+ });
794
+ }
795
+ finally {
796
+ if (mountedRuntime?.handle) {
797
+ await mountedRuntime.handle.close().catch(() => { });
798
+ }
799
+ }
800
+ }
801
+ function appendLoadCheckParams(targetUrl, devAuth) {
802
+ return (0, devAuth_1.applyHostedDevAuthSelectionToUrl)(targetUrl, {
803
+ devAuth,
804
+ player: null,
805
+ });
806
+ }
807
+ async function runUploadedHostedLoadCheck(input) {
808
+ const preview = await input.client.fetchAppUploadLaunchCheckPreview(input.creatorUsername, input.task.name, input.sessionId);
809
+ const captureSurface = resolveLoadCheckSurface(preview.localAppMetadata.surfaceTargets);
810
+ const contextOptions = cloneLoadCheckContextOptions(captureSurface);
811
+ const anonymousExpectedState = resolveAnonymousHostedLaunchState({
812
+ ...preview.localAppMetadata,
813
+ captureSurface,
814
+ });
815
+ let result;
816
+ if (anonymousExpectedState === 'login_required') {
817
+ if (!preview.app?.id) {
818
+ result = buildFailedLoadCheckResult('app_registration_required_for_auth_validation', `Auth-required hosted app "${input.task.name}" must be registered on PlayDrop before viewer load-check can run.`, preview.session.manifestHash);
819
+ }
820
+ else {
821
+ const anonymousGateResult = await runHostedLoadCheck({
822
+ targetUrl: appendLoadCheckParams(preview.launchUrl, 'anonymous'),
823
+ artifactFingerprint: preview.session.manifestHash,
824
+ timeoutMs: input.timeoutMs,
825
+ expectedHostedLaunchState: anonymousExpectedState,
826
+ contextOptions,
827
+ });
828
+ if (anonymousGateResult.status !== 'PASSED') {
829
+ result = anonymousGateResult;
830
+ }
831
+ else {
832
+ result = await runHostedLoadCheck({
833
+ targetUrl: appendLoadCheckParams(preview.launchUrl, 'viewer'),
834
+ artifactFingerprint: preview.session.manifestHash,
835
+ timeoutMs: input.timeoutMs,
836
+ expectedHostedLaunchState: resolvePostAuthHostedLaunchState(preview.localAppMetadata.controllerMode),
837
+ token: input.token ?? null,
838
+ user: input.currentUser ?? null,
839
+ savedSessionBootstrap: true,
840
+ contextOptions,
841
+ screenshotPath: input.screenshotPath,
842
+ });
843
+ }
844
+ }
845
+ }
846
+ else {
847
+ result = await runHostedLoadCheck({
848
+ targetUrl: appendLoadCheckParams(preview.launchUrl, 'anonymous'),
849
+ artifactFingerprint: preview.session.manifestHash,
850
+ timeoutMs: input.timeoutMs,
851
+ expectedHostedLaunchState: anonymousExpectedState,
852
+ contextOptions,
853
+ screenshotPath: input.screenshotPath,
854
+ });
855
+ }
856
+ const recorded = await input.client.recordAppUploadLaunchCheck(input.creatorUsername, input.task.name, input.sessionId, {
857
+ status: result.status,
858
+ artifactFingerprint: preview.session.manifestHash,
859
+ errorCode: result.errorCode,
860
+ message: result.message,
861
+ });
862
+ const recordedLaunchCheck = recorded.session.launchCheck;
863
+ if (recordedLaunchCheck.status !== 'PASSED' && recordedLaunchCheck.status !== 'FAILED') {
864
+ return buildFailedLoadCheckResult('startup_failure', 'Upload load-check was not recorded on the staged session.', preview.session.manifestHash);
865
+ }
866
+ return {
867
+ ...result,
868
+ status: recordedLaunchCheck.status,
869
+ errorCode: recordedLaunchCheck.errorCode,
870
+ message: recordedLaunchCheck.message,
871
+ checkedAt: recordedLaunchCheck.checkedAt ?? new Date().toISOString(),
872
+ artifactFingerprint: recordedLaunchCheck.artifactFingerprint,
873
+ };
874
+ }