@mpgd/cli 0.34.0 → 0.36.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.
- package/dist/android-bundle-signer.d.ts +2 -0
- package/dist/android-bundle-signer.js +38 -0
- package/dist/android-signing-session.d.ts +22 -0
- package/dist/android-signing-session.js +198 -0
- package/dist/artifact-inspection-environment.d.ts +2 -0
- package/dist/artifact-inspection-environment.js +31 -0
- package/dist/asc-pin.json +26 -0
- package/dist/capacitor-shell-starter.d.ts +1 -0
- package/dist/capacitor-shell-starter.js +9 -6
- package/dist/deploy-planning.d.ts +64 -0
- package/dist/deploy-planning.js +471 -0
- package/dist/deploy-process.d.ts +28 -0
- package/dist/deploy-process.js +197 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +483 -13
- package/dist/ios-ipa-inspection.js +151 -0
- package/dist/ios-keychain-import.swift +92 -0
- package/dist/ios-plist.d.ts +2 -0
- package/dist/ios-plist.js +75 -0
- package/dist/ios-signing-profile.d.ts +12 -0
- package/dist/ios-signing-profile.js +60 -0
- package/dist/ios-signing-session.d.ts +22 -0
- package/dist/ios-signing-session.js +256 -0
- package/dist/kit-upgrade-target-readiness.d.ts +8 -0
- package/dist/kit-upgrade-target-readiness.js +119 -0
- package/dist/kit-upgrade.d.ts +41 -0
- package/dist/kit-upgrade.js +489 -0
- package/dist/native-build-info.json +5 -0
- package/dist/native-build-target.js +12032 -0
- package/dist/native-deploy-run.d.ts +35 -0
- package/dist/native-deploy-run.js +462 -0
- package/dist/native-deploy-submission.d.ts +40 -0
- package/dist/native-deploy-submission.js +244 -0
- package/dist/native-deploy-workflow.d.ts +11 -0
- package/dist/native-deploy-workflow.js +328 -0
- package/dist/play-internal-submission.d.ts +32 -0
- package/dist/play-internal-submission.js +247 -0
- package/dist/play-publisher-port.d.ts +30 -0
- package/dist/play-publisher-port.js +1 -0
- package/dist/play-sdk-adapter.js +78 -0
- package/dist/release-state.d.ts +140 -0
- package/dist/release-state.js +1046 -0
- package/dist/release-workspace.d.ts +61 -0
- package/dist/release-workspace.js +504 -0
- package/dist/testflight-submission.d.ts +49 -0
- package/dist/testflight-submission.js +462 -0
- package/package.json +20 -13
- package/templates/phaser-game/README.md +23 -8
- package/templates/phaser-game/src/main.ts +34 -33
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmodSync, copyFileSync, createReadStream, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { ReleaseProcessError, runReleaseProcess } from './deploy-process.js';
|
|
7
|
+
export const pinnedAscVersion = '5.5.0';
|
|
8
|
+
const sha256Pattern = /^[0-9a-f]{64}$/u;
|
|
9
|
+
const teamIdPattern = /^[A-Z0-9]{10}$/u;
|
|
10
|
+
const apiKeyIdPattern = /^[A-Z0-9]{10}$/u;
|
|
11
|
+
const ascIdPattern = /^[A-Za-z0-9][A-Za-z0-9-]*$/u;
|
|
12
|
+
const marketingVersionPattern = /^\d+(?:\.\d+){0,2}$/u;
|
|
13
|
+
const uploadTimeoutMs = 12 * 60 * 1000;
|
|
14
|
+
const lookupTimeoutMs = 60_000;
|
|
15
|
+
const maxJsonBytes = 1024 * 1024;
|
|
16
|
+
export class IosSubmissionUncertainError extends Error {
|
|
17
|
+
uploadId;
|
|
18
|
+
constructor(uploadId) {
|
|
19
|
+
super(`iOS upload ${uploadId} was committed but its local checkpoint failed; inspect remote state before retry.`);
|
|
20
|
+
this.uploadId = uploadId;
|
|
21
|
+
this.name = 'IosSubmissionUncertainError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Verify and use exactly the pinned asc release, with isolated environment auth. */
|
|
25
|
+
export async function submitVerifiedIosBuild(input) {
|
|
26
|
+
if (process.platform !== 'darwin') {
|
|
27
|
+
throw new Error('Signed iOS IPA verification requires macOS.');
|
|
28
|
+
}
|
|
29
|
+
const tempRoot = mkdtempSync(path.join(tmpdir(), 'mpgd-asc-session-'));
|
|
30
|
+
try {
|
|
31
|
+
if (!path.isAbsolute(input.ascBinary)) {
|
|
32
|
+
throw new Error('asc binary must be an absolute file path.');
|
|
33
|
+
}
|
|
34
|
+
const binary = path.join(tempRoot, 'asc');
|
|
35
|
+
copyFileSync(input.ascBinary, binary);
|
|
36
|
+
chmodSync(binary, 0o700);
|
|
37
|
+
await verifyPinnedAscBinary(binary);
|
|
38
|
+
const stagedIpa = path.join(tempRoot, 'release.ipa');
|
|
39
|
+
copyFileSync(input.ipaFile, stagedIpa);
|
|
40
|
+
const stagedInput = { ...input, ipaFile: stagedIpa };
|
|
41
|
+
const resolvedPreflight = await preflight(stagedInput);
|
|
42
|
+
const { inspectSignedIosIpa } = await import(new URL('./ios-ipa-inspection.js', import.meta.url).href);
|
|
43
|
+
inspectSignedIosIpa(stagedIpa, {
|
|
44
|
+
expectedBundleId: input.bundleId,
|
|
45
|
+
expectedMarketingVersion: resolvedPreflight.marketingVersion,
|
|
46
|
+
expectedBuildNumber: resolvedPreflight.buildNumber,
|
|
47
|
+
expectedTeamId: input.record.inspectedTeamId ?? '',
|
|
48
|
+
});
|
|
49
|
+
const environment = isolatedAscEnvironment(input, tempRoot);
|
|
50
|
+
const runner = async (args, timeoutMs) => {
|
|
51
|
+
const result = await runReleaseProcess({
|
|
52
|
+
command: binary,
|
|
53
|
+
args: [...args, '--output', 'json'],
|
|
54
|
+
cwd: tempRoot,
|
|
55
|
+
environment,
|
|
56
|
+
timeoutMs,
|
|
57
|
+
maxOutputBytes: maxJsonBytes,
|
|
58
|
+
captureMachineStdout: true,
|
|
59
|
+
secretValues: [input.apiKeyId, input.apiIssuerId, input.apiPrivateKeyBase64],
|
|
60
|
+
signal: input.signal,
|
|
61
|
+
});
|
|
62
|
+
if (result.truncated || result.machineStdout === undefined) {
|
|
63
|
+
throw new Error('asc JSON output was truncated or unavailable.');
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(result.machineStdout);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error('asc returned invalid JSON.');
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return await submitVerifiedIosBuildResolved(stagedInput, runner, resolvedPreflight);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Internal mock boundary used by credential-free submission tests. */
|
|
79
|
+
export async function submitVerifiedIosBuildWithRunner(input, run) {
|
|
80
|
+
return submitVerifiedIosBuildResolved(input, run, await preflight(input));
|
|
81
|
+
}
|
|
82
|
+
async function submitVerifiedIosBuildResolved(input, run, { marketingVersion, buildNumber }) {
|
|
83
|
+
const base = {
|
|
84
|
+
appStoreAppId: input.appStoreAppId,
|
|
85
|
+
bundleId: input.bundleId,
|
|
86
|
+
marketingVersion,
|
|
87
|
+
buildNumber,
|
|
88
|
+
internalGroupId: input.internalGroupId,
|
|
89
|
+
};
|
|
90
|
+
const app = await run(['apps', 'view', '--id', input.appStoreAppId], lookupTimeoutMs);
|
|
91
|
+
if (!isRecord(app) || !isRecord(app.data) || app.data.id !== input.appStoreAppId
|
|
92
|
+
|| app.data.type !== 'apps' || !isRecord(app.data.attributes)
|
|
93
|
+
|| app.data.attributes.bundleId !== input.bundleId) {
|
|
94
|
+
throw new Error('App Store Connect app ID does not match the verified IPA bundle ID.');
|
|
95
|
+
}
|
|
96
|
+
const groupsResponse = await run([
|
|
97
|
+
'testflight',
|
|
98
|
+
'groups',
|
|
99
|
+
'list',
|
|
100
|
+
'--app',
|
|
101
|
+
input.appStoreAppId,
|
|
102
|
+
'--internal',
|
|
103
|
+
'--id',
|
|
104
|
+
input.internalGroupId,
|
|
105
|
+
'--paginate',
|
|
106
|
+
], lookupTimeoutMs);
|
|
107
|
+
if (!findInternalGroup(groupsResponse, input.internalGroupId)) {
|
|
108
|
+
throw new Error('Configured TestFlight group is not an internal group of this app.');
|
|
109
|
+
}
|
|
110
|
+
let uploadId = input.resumeUploadId;
|
|
111
|
+
if (uploadId === undefined && input.resumeBuildId === undefined) {
|
|
112
|
+
const before = await lookupBuild(input, marketingVersion, buildNumber, run);
|
|
113
|
+
if (before !== undefined) {
|
|
114
|
+
return {
|
|
115
|
+
...base,
|
|
116
|
+
status: 'unknown',
|
|
117
|
+
buildId: before.id,
|
|
118
|
+
detail: 'A build already uses this app version and build number; do not reupload.',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const existingUploads = await lookupUploadIds(input, marketingVersion, buildNumber, run);
|
|
122
|
+
if (existingUploads.length > 0) {
|
|
123
|
+
return {
|
|
124
|
+
...base,
|
|
125
|
+
status: 'unknown',
|
|
126
|
+
...(existingUploads.length === 1 ? { uploadId: existingUploads[0] } : {}),
|
|
127
|
+
detail: 'An upload already exists for this app version and build number; reconcile it before retry.',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
let upload;
|
|
131
|
+
try {
|
|
132
|
+
upload = await run([
|
|
133
|
+
'builds', 'upload', '--app', input.appStoreAppId,
|
|
134
|
+
'--ipa', input.ipaFile, '--platform', 'IOS',
|
|
135
|
+
'--version', marketingVersion, '--build-number', buildNumber,
|
|
136
|
+
'--checksum',
|
|
137
|
+
], uploadTimeoutMs);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const observed = await lookupBuild(input, marketingVersion, buildNumber, run).catch(() => undefined);
|
|
141
|
+
return {
|
|
142
|
+
...base,
|
|
143
|
+
status: 'unknown',
|
|
144
|
+
...(observed === undefined ? {} : { buildId: observed.id }),
|
|
145
|
+
detail: `Upload result was not confirmed${failureReason(error)}; inspect App Store Connect before retry.`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
uploadId = assertUploadResult(upload, statSync(input.ipaFile).size);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
const observed = await lookupBuild(input, marketingVersion, buildNumber, run).catch(() => undefined);
|
|
153
|
+
return {
|
|
154
|
+
...base,
|
|
155
|
+
status: 'unknown',
|
|
156
|
+
...(observed === undefined ? {} : { buildId: observed.id }),
|
|
157
|
+
detail: 'Upload output was invalid; inspect App Store Connect before retry.',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await input.onUploadCommitted?.(uploadId);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw new IosSubmissionUncertainError(uploadId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else if (uploadId !== undefined) {
|
|
168
|
+
const matches = (await lookupUploadIds(input, marketingVersion, buildNumber, run))
|
|
169
|
+
.filter((id) => id === uploadId);
|
|
170
|
+
if (matches.length !== 1) {
|
|
171
|
+
return {
|
|
172
|
+
...base,
|
|
173
|
+
status: 'unknown',
|
|
174
|
+
uploadId,
|
|
175
|
+
detail: 'Checkpointed upload is not uniquely visible for this app and version.',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const uploadReference = uploadId === undefined ? {} : { uploadId };
|
|
180
|
+
let build;
|
|
181
|
+
try {
|
|
182
|
+
build = await lookupBuild(input, marketingVersion, buildNumber, run);
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
return {
|
|
186
|
+
...base,
|
|
187
|
+
status: input.resumeBuildId === undefined ? 'uploaded' : 'unknown',
|
|
188
|
+
...uploadReference,
|
|
189
|
+
...(input.resumeBuildId === undefined ? {} : { buildId: input.resumeBuildId }),
|
|
190
|
+
detail: `Upload committed; build processing lookup is temporarily unavailable${failureReason(error)}.`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (input.resumeBuildId !== undefined && build?.id !== input.resumeBuildId) {
|
|
194
|
+
return {
|
|
195
|
+
...base,
|
|
196
|
+
status: 'unknown',
|
|
197
|
+
...uploadReference,
|
|
198
|
+
buildId: input.resumeBuildId,
|
|
199
|
+
detail: 'Checkpointed build ID does not match the reserved App Store build.',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (build === undefined) {
|
|
203
|
+
return {
|
|
204
|
+
...base,
|
|
205
|
+
status: 'uploaded',
|
|
206
|
+
...uploadReference,
|
|
207
|
+
detail: 'Upload committed; build is not yet discoverable.',
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (build.processingState === 'PROCESSING') {
|
|
211
|
+
return { ...base, status: 'processing', ...uploadReference, buildId: build.id };
|
|
212
|
+
}
|
|
213
|
+
if (build.processingState === 'FAILED' || build.processingState === 'INVALID') {
|
|
214
|
+
return {
|
|
215
|
+
...base,
|
|
216
|
+
status: 'failed',
|
|
217
|
+
...uploadReference,
|
|
218
|
+
buildId: build.id,
|
|
219
|
+
detail: `App Store Connect reported ${build.processingState}.`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (build.processingState !== 'VALID') {
|
|
223
|
+
return {
|
|
224
|
+
...base,
|
|
225
|
+
status: 'unknown',
|
|
226
|
+
...uploadReference,
|
|
227
|
+
buildId: build.id,
|
|
228
|
+
detail: 'App Store Connect returned an unknown processing state.',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const added = await run(['builds', 'add-groups', '--build-id', build.id, '--group', input.internalGroupId], lookupTimeoutMs);
|
|
233
|
+
assertGroupAssignment(added, build.id, input.internalGroupId);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
const operatorAction = error instanceof ReleaseProcessError
|
|
237
|
+
&& error.reason === 'exit'
|
|
238
|
+
&& /export compliance|encryption declaration|account agreement|tax and banking/iu
|
|
239
|
+
.test(error.output);
|
|
240
|
+
const status = operatorAction ? 'action-required' : 'unknown';
|
|
241
|
+
return {
|
|
242
|
+
...base,
|
|
243
|
+
status,
|
|
244
|
+
...uploadReference,
|
|
245
|
+
buildId: build.id,
|
|
246
|
+
detail: `Build is processed, but internal group assignment needs inspection${failureReason(error)}.`,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
let membershipFailure;
|
|
250
|
+
try {
|
|
251
|
+
const memberships = await run(['testflight', 'groups', 'list', '--build-id', build.id, '--internal'], lookupTimeoutMs);
|
|
252
|
+
if (findInternalGroup(memberships, input.internalGroupId)) {
|
|
253
|
+
return { ...base, status: 'testflight-ready', ...uploadReference, buildId: build.id };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
membershipFailure = error;
|
|
258
|
+
// Membership can be queried again by explicit build ID.
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
...base,
|
|
262
|
+
status: 'unknown',
|
|
263
|
+
...uploadReference,
|
|
264
|
+
buildId: build.id,
|
|
265
|
+
detail: `Group assignment response was not confirmed by a membership read${failureReason(membershipFailure)}.`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
async function lookupUploadIds(input, marketingVersion, buildNumber, run) {
|
|
269
|
+
const response = await run([
|
|
270
|
+
'builds',
|
|
271
|
+
'uploads',
|
|
272
|
+
'list',
|
|
273
|
+
'--app',
|
|
274
|
+
input.appStoreAppId,
|
|
275
|
+
'--cf-bundle-short-version',
|
|
276
|
+
marketingVersion,
|
|
277
|
+
'--cf-bundle-version',
|
|
278
|
+
buildNumber,
|
|
279
|
+
'--platform',
|
|
280
|
+
'IOS',
|
|
281
|
+
'--paginate',
|
|
282
|
+
], lookupTimeoutMs);
|
|
283
|
+
const items = readDataArray(response, 'builds uploads list');
|
|
284
|
+
if (items.some((item) => !isRecord(item) || item.type !== 'buildUploads'
|
|
285
|
+
|| typeof item.id !== 'string' || !ascIdPattern.test(item.id)
|
|
286
|
+
|| !isRecord(item.attributes)
|
|
287
|
+
|| typeof item.attributes.cfBundleShortVersionString !== 'string'
|
|
288
|
+
|| typeof item.attributes.cfBundleVersion !== 'string'
|
|
289
|
+
|| typeof item.attributes.platform !== 'string')) {
|
|
290
|
+
throw new Error('asc builds uploads list returned a malformed upload.');
|
|
291
|
+
}
|
|
292
|
+
return items.filter((item) => isRecord(item) && isRecord(item.attributes)
|
|
293
|
+
&& item.attributes.cfBundleShortVersionString === marketingVersion
|
|
294
|
+
&& item.attributes.cfBundleVersion === buildNumber
|
|
295
|
+
&& item.attributes.platform === 'IOS')
|
|
296
|
+
.map((item) => item.id);
|
|
297
|
+
}
|
|
298
|
+
async function lookupBuild(input, marketingVersion, buildNumber, run) {
|
|
299
|
+
const response = await run([
|
|
300
|
+
'builds',
|
|
301
|
+
'list',
|
|
302
|
+
'--app',
|
|
303
|
+
input.appStoreAppId,
|
|
304
|
+
'--version',
|
|
305
|
+
marketingVersion,
|
|
306
|
+
'--build-number',
|
|
307
|
+
buildNumber,
|
|
308
|
+
'--platform',
|
|
309
|
+
'IOS',
|
|
310
|
+
'--paginate',
|
|
311
|
+
], lookupTimeoutMs);
|
|
312
|
+
const items = readDataArray(response, 'builds list');
|
|
313
|
+
if (items.some((item) => !isRecord(item) || item.type !== 'builds'
|
|
314
|
+
|| typeof item.id !== 'string' || item.id === ''
|
|
315
|
+
|| !isRecord(item.attributes) || typeof item.attributes.version !== 'string'
|
|
316
|
+
|| typeof item.attributes.processingState !== 'string')) {
|
|
317
|
+
throw new Error('asc builds list returned a malformed build.');
|
|
318
|
+
}
|
|
319
|
+
const matching = items.filter((item) => item
|
|
320
|
+
.attributes.version === buildNumber);
|
|
321
|
+
if (matching.length > 1) {
|
|
322
|
+
throw new Error('App Store Connect returned multiple builds for the reserved version.');
|
|
323
|
+
}
|
|
324
|
+
const item = matching[0];
|
|
325
|
+
if (item === undefined) {
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
if (!isRecord(item) || typeof item.id !== 'string' || item.id === ''
|
|
329
|
+
|| !isRecord(item.attributes) || typeof item.attributes.processingState !== 'string') {
|
|
330
|
+
throw new Error('asc builds list returned a malformed build.');
|
|
331
|
+
}
|
|
332
|
+
return { id: item.id, processingState: item.attributes.processingState };
|
|
333
|
+
}
|
|
334
|
+
function findInternalGroup(response, groupId) {
|
|
335
|
+
return readDataArray(response, 'TestFlight groups list').some((value) => isRecord(value)
|
|
336
|
+
&& value.type === 'betaGroups' && value.id === groupId
|
|
337
|
+
&& isRecord(value.attributes) && value.attributes.isInternalGroup === true);
|
|
338
|
+
}
|
|
339
|
+
function readDataArray(response, command) {
|
|
340
|
+
if (!isRecord(response) || !Array.isArray(response.data)) {
|
|
341
|
+
throw new Error(`asc ${command} returned an invalid JSON schema.`);
|
|
342
|
+
}
|
|
343
|
+
return response.data;
|
|
344
|
+
}
|
|
345
|
+
function assertUploadResult(value, expectedBytes) {
|
|
346
|
+
if (!isRecord(value) || typeof value.uploadId !== 'string' || value.uploadId === ''
|
|
347
|
+
|| typeof value.fileId !== 'string' || value.fileId === ''
|
|
348
|
+
|| value.uploaded !== true || value.fileSize !== expectedBytes
|
|
349
|
+
|| (Array.isArray(value.operations) && value.operations.length > 0)) {
|
|
350
|
+
throw new Error('asc upload result was not a confirmed, redacted committed upload.');
|
|
351
|
+
}
|
|
352
|
+
return value.uploadId;
|
|
353
|
+
}
|
|
354
|
+
function assertGroupAssignment(value, buildId, groupId) {
|
|
355
|
+
if (!isRecord(value) || value.action !== 'added' || value.buildId !== buildId
|
|
356
|
+
|| !Array.isArray(value.groupIds)
|
|
357
|
+
|| value.groupIds.some((id) => typeof id !== 'string'
|
|
358
|
+
|| id !== groupId)) {
|
|
359
|
+
throw new Error('asc returned an invalid TestFlight group assignment.');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function failureReason(error) {
|
|
363
|
+
return error instanceof ReleaseProcessError ? ` (${error.reason})` : '';
|
|
364
|
+
}
|
|
365
|
+
async function preflight(input) {
|
|
366
|
+
const version = input.record.platformVersion;
|
|
367
|
+
const buildNumber = version.buildNumber;
|
|
368
|
+
const marketingVersion = version.marketingVersion;
|
|
369
|
+
if (input.record.target !== 'ios' || input.bundleId !== input.record.inspectedAppId
|
|
370
|
+
|| !teamIdPattern.test(input.record.inspectedTeamId ?? '')
|
|
371
|
+
|| !/^[1-9]\d*$/u.test(String(buildNumber))
|
|
372
|
+
|| typeof marketingVersion !== 'string'
|
|
373
|
+
|| !marketingVersionPattern.test(marketingVersion)
|
|
374
|
+
|| !/^\d+$/u.test(input.appStoreAppId)
|
|
375
|
+
|| !ascIdPattern.test(input.internalGroupId)
|
|
376
|
+
|| (input.resumeUploadId !== undefined && !ascIdPattern.test(input.resumeUploadId))
|
|
377
|
+
|| (input.resumeUploadId === undefined) !== (input.resumeArtifactSha256 === undefined)
|
|
378
|
+
|| (input.resumeArtifactSha256 !== undefined
|
|
379
|
+
&& input.resumeArtifactSha256 !== input.record.artifactSha256)
|
|
380
|
+
|| !sha256Pattern.test(input.record.artifactSha256)
|
|
381
|
+
|| !input.ipaFile.endsWith('.ipa') || !existsSync(input.ipaFile)
|
|
382
|
+
|| !statSync(input.ipaFile).isFile()) {
|
|
383
|
+
throw new Error('TestFlight submission requires a matching verified iOS IPA build record.');
|
|
384
|
+
}
|
|
385
|
+
const hash = createHash('sha256');
|
|
386
|
+
for await (const chunk of createReadStream(input.ipaFile)) {
|
|
387
|
+
hash.update(chunk);
|
|
388
|
+
}
|
|
389
|
+
if (hash.digest('hex') !== input.record.artifactSha256) {
|
|
390
|
+
throw new Error('TestFlight IPA bytes differ from the immutable build record.');
|
|
391
|
+
}
|
|
392
|
+
return { marketingVersion, buildNumber: String(buildNumber) };
|
|
393
|
+
}
|
|
394
|
+
export function isolatedAscEnvironment(input, directory, source = process.env) {
|
|
395
|
+
if (!apiKeyIdPattern.test(input.apiKeyId)
|
|
396
|
+
|| input.apiIssuerId.trim() === '' || input.apiPrivateKeyBase64.trim() === '') {
|
|
397
|
+
throw new Error('App Store Connect API credentials are incomplete.');
|
|
398
|
+
}
|
|
399
|
+
// asc only needs network/locale process settings, never Git, registry, or signing secrets.
|
|
400
|
+
const inheritedNames = [
|
|
401
|
+
'PATH',
|
|
402
|
+
'LANG',
|
|
403
|
+
'LC_ALL',
|
|
404
|
+
'LC_CTYPE',
|
|
405
|
+
'TZ',
|
|
406
|
+
'HTTPS_PROXY',
|
|
407
|
+
'HTTP_PROXY',
|
|
408
|
+
'NO_PROXY',
|
|
409
|
+
'https_proxy',
|
|
410
|
+
'http_proxy',
|
|
411
|
+
'no_proxy',
|
|
412
|
+
'SSL_CERT_FILE',
|
|
413
|
+
'CURL_CA_BUNDLE',
|
|
414
|
+
];
|
|
415
|
+
const environment = Object.fromEntries(inheritedNames.flatMap((name) => {
|
|
416
|
+
const value = source[name];
|
|
417
|
+
return value === undefined ? [] : [[name, value]];
|
|
418
|
+
}));
|
|
419
|
+
return {
|
|
420
|
+
...environment,
|
|
421
|
+
HOME: directory,
|
|
422
|
+
CFFIXED_USER_HOME: directory,
|
|
423
|
+
XDG_CONFIG_HOME: directory,
|
|
424
|
+
TMPDIR: directory,
|
|
425
|
+
TMP: directory,
|
|
426
|
+
TEMP: directory,
|
|
427
|
+
ASC_TELEMETRY_DISABLED: '1',
|
|
428
|
+
ASC_BYPASS_KEYCHAIN: '1',
|
|
429
|
+
ASC_STRICT_AUTH: '1',
|
|
430
|
+
ASC_KEY_ID: input.apiKeyId,
|
|
431
|
+
ASC_ISSUER_ID: input.apiIssuerId,
|
|
432
|
+
ASC_PRIVATE_KEY_B64: input.apiPrivateKeyBase64,
|
|
433
|
+
ASC_TIMEOUT: '60s',
|
|
434
|
+
ASC_UPLOAD_TIMEOUT: '10m',
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/** The package consumes one reviewed asc pin; it does not trust PATH or version text. */
|
|
438
|
+
export async function verifyPinnedAscBinary(binary, pinFile = fileURLToPath(new URL('./asc-pin.json', import.meta.url))) {
|
|
439
|
+
if (!path.isAbsolute(binary) || !existsSync(binary) || !statSync(binary).isFile()) {
|
|
440
|
+
throw new Error('asc binary must be an existing absolute file.');
|
|
441
|
+
}
|
|
442
|
+
const pin = JSON.parse(readFileSync(pinFile, 'utf8'));
|
|
443
|
+
const host = `${process.platform}-${process.arch}`;
|
|
444
|
+
if (!isRecord(pin) || pin.version !== pinnedAscVersion
|
|
445
|
+
|| !isRecord(pin.assets) || !isRecord(pin.assets[host])) {
|
|
446
|
+
throw new Error('No reviewed asc binary pin exists for this host.');
|
|
447
|
+
}
|
|
448
|
+
const expected = pin.assets[host].sha256;
|
|
449
|
+
if (typeof expected !== 'string' || !sha256Pattern.test(expected)) {
|
|
450
|
+
throw new Error('Reviewed asc binary pin is malformed.');
|
|
451
|
+
}
|
|
452
|
+
const hash = createHash('sha256');
|
|
453
|
+
for await (const chunk of createReadStream(realpathSync(binary))) {
|
|
454
|
+
hash.update(chunk);
|
|
455
|
+
}
|
|
456
|
+
if (hash.digest('hex') !== expected) {
|
|
457
|
+
throw new Error('asc binary checksum differs from the reviewed 5.5.0 pin.');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function isRecord(value) {
|
|
461
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
462
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,32 +40,39 @@
|
|
|
40
40
|
"mpgd": "./dist/bin.js"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@googleapis/androidpublisher": "42.0.0",
|
|
43
44
|
"@gunshi/plugin-completion": "^0.35.1",
|
|
44
45
|
"@gunshi/plugin-i18n": "^0.35.1",
|
|
45
46
|
"@gunshi/resources": "^0.35.1",
|
|
46
47
|
"@xmldom/xmldom": "0.9.12",
|
|
48
|
+
"acorn": "8.17.0",
|
|
47
49
|
"entities": "^8.0.0",
|
|
48
50
|
"esbuild": "^0.28.1",
|
|
49
51
|
"gunshi": "^0.35.1",
|
|
52
|
+
"semver": "^7.8.5",
|
|
53
|
+
"sharp": "0.35.3",
|
|
54
|
+
"typia": "13.0.2",
|
|
50
55
|
"undici": "^7.28.0",
|
|
51
|
-
"
|
|
56
|
+
"vite": "8.1.3",
|
|
57
|
+
"@mpgd/catalog": "0.7.6",
|
|
58
|
+
"@mpgd/phaser-assets": "0.7.0",
|
|
59
|
+
"@mpgd/target-config": "0.17.0"
|
|
52
60
|
},
|
|
53
61
|
"devDependencies": {
|
|
54
62
|
"@types/node": "^24.0.0",
|
|
63
|
+
"@types/semver": "^7.8.0",
|
|
55
64
|
"ttsc": "0.30.4",
|
|
56
65
|
"typescript": "7.0.2",
|
|
57
|
-
"@mpgd/adapter-ait": "0.12.
|
|
58
|
-
"@mpgd/adapter-browser": "0.7.
|
|
59
|
-
"@mpgd/adapter-
|
|
60
|
-
"@mpgd/adapter-
|
|
61
|
-
"@mpgd/adapter-verse8": "0.3.
|
|
66
|
+
"@mpgd/adapter-ait": "0.12.6",
|
|
67
|
+
"@mpgd/adapter-browser": "0.7.7",
|
|
68
|
+
"@mpgd/adapter-capacitor": "0.6.0",
|
|
69
|
+
"@mpgd/adapter-devvit": "0.9.12",
|
|
70
|
+
"@mpgd/adapter-verse8": "0.3.10",
|
|
71
|
+
"@mpgd/analytics": "0.4.0",
|
|
62
72
|
"@mpgd/bridge": "0.10.0",
|
|
63
|
-
"@mpgd/
|
|
64
|
-
"@mpgd/i18n": "0.6.
|
|
65
|
-
"@mpgd/platform": "0.
|
|
66
|
-
"@mpgd/target-config": "0.16.0",
|
|
67
|
-
"@mpgd/analytics": "0.3.15",
|
|
68
|
-
"@mpgd/game-services": "0.16.0"
|
|
73
|
+
"@mpgd/game-services": "0.17.0",
|
|
74
|
+
"@mpgd/i18n": "0.6.5",
|
|
75
|
+
"@mpgd/platform": "0.14.0"
|
|
69
76
|
},
|
|
70
77
|
"main": "./dist/index.js",
|
|
71
78
|
"types": "./dist/index.d.ts",
|
|
@@ -149,17 +149,32 @@ preview, Apps in Toss, Devvit, Android, and iOS bundles.
|
|
|
149
149
|
## Viewport And Controls
|
|
150
150
|
|
|
151
151
|
The starter computes an initial viewport snapshot with `@mpgd/target-config`.
|
|
152
|
-
|
|
153
|
-
`window.
|
|
152
|
+
`measureTargetViewport` measures the game container first, then falls back to
|
|
153
|
+
`visualViewport` or the window. It returns `null` while every surface is still
|
|
154
|
+
zero-sized (a hidden iframe, a collapsed embed or a background tab before
|
|
155
|
+
layout), so the starter waits for the first usable size instead of failing
|
|
156
|
+
viewport validation:
|
|
154
157
|
|
|
155
158
|
```ts
|
|
156
|
-
import {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
import {
|
|
160
|
+
measureTargetViewport,
|
|
161
|
+
resolveTargetViewportSnapshot,
|
|
162
|
+
waitForTargetViewportMeasurement,
|
|
163
|
+
} from '@mpgd/target-config';
|
|
164
|
+
|
|
165
|
+
const measured = await waitForTargetViewportMeasurement({
|
|
166
|
+
measure: () =>
|
|
167
|
+
measureTargetViewport({
|
|
168
|
+
container: document.querySelector<HTMLElement>('#game'),
|
|
169
|
+
visualViewport: window.visualViewport,
|
|
170
|
+
window,
|
|
171
|
+
}),
|
|
172
|
+
// Call the listener on resize, visualViewport resize, visibilitychange and a
|
|
173
|
+
// ResizeObserver on #game; return a function that removes those listeners.
|
|
174
|
+
subscribe: subscribeToGameViewportChanges,
|
|
175
|
+
});
|
|
159
176
|
const viewport = resolveTargetViewportSnapshot({
|
|
160
|
-
|
|
161
|
-
height: measured.height,
|
|
162
|
-
source: measured.source,
|
|
177
|
+
...measured,
|
|
163
178
|
runtime: runtime.config.runtime,
|
|
164
179
|
orientationPolicy: {
|
|
165
180
|
mode: 'prefer-landscape',
|
|
@@ -4,7 +4,10 @@ import { createAnalyticsReporter, createBufferedAnalyticsSink } from '@mpgd/anal
|
|
|
4
4
|
import { resolveTargetMpgdLocale, type Locale } from '@mpgd/i18n';
|
|
5
5
|
import type { IdentitySession, LaunchIntent, PlatformGateway } from '@mpgd/platform';
|
|
6
6
|
import {
|
|
7
|
+
measureTargetViewport,
|
|
7
8
|
resolveTargetViewportSnapshot,
|
|
9
|
+
waitForTargetViewportMeasurement,
|
|
10
|
+
type TargetViewportMeasurement,
|
|
8
11
|
type TargetViewportOrientationPolicy,
|
|
9
12
|
} from '@mpgd/target-config';
|
|
10
13
|
|
|
@@ -36,8 +39,13 @@ async function bootstrap(): Promise<void> {
|
|
|
36
39
|
const orientationPolicy = {
|
|
37
40
|
mode: 'prefer-landscape',
|
|
38
41
|
} as const satisfies TargetViewportOrientationPolicy;
|
|
42
|
+
// A hidden iframe or background tab can boot at 0x0; wait for layout instead of failing.
|
|
43
|
+
const measurement = await waitForTargetViewportMeasurement({
|
|
44
|
+
measure: measureGameViewport,
|
|
45
|
+
subscribe: subscribeToGameViewportChanges,
|
|
46
|
+
});
|
|
39
47
|
const viewport = resolveTargetViewportSnapshot({
|
|
40
|
-
...
|
|
48
|
+
...measurement,
|
|
41
49
|
runtime: runtime.config.runtime,
|
|
42
50
|
orientationPolicy,
|
|
43
51
|
});
|
|
@@ -152,40 +160,33 @@ function renderBootstrapError(error: unknown, locale: Locale): void {
|
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
/** Measure the game surface before falling back to browser viewport geometry. */
|
|
155
|
-
function measureGameViewport(): {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
function measureGameViewport(): TargetViewportMeasurement | null {
|
|
164
|
+
return measureTargetViewport({
|
|
165
|
+
container: document.querySelector<HTMLElement>('#game'),
|
|
166
|
+
visualViewport: window.visualViewport,
|
|
167
|
+
window,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Notify when the game surface may have gained a size or become visible. */
|
|
172
|
+
function subscribeToGameViewportChanges(listener: () => void): () => void {
|
|
160
173
|
const container = document.querySelector<HTMLElement>('#game');
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
width: rect.width,
|
|
166
|
-
height: rect.height,
|
|
167
|
-
source: 'container',
|
|
168
|
-
};
|
|
169
|
-
}
|
|
174
|
+
const observer =
|
|
175
|
+
container === null || typeof ResizeObserver !== 'function'
|
|
176
|
+
? undefined
|
|
177
|
+
: new ResizeObserver(listener);
|
|
170
178
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
visualViewport !== undefined &&
|
|
175
|
-
visualViewport !== null &&
|
|
176
|
-
visualViewport.width > 0 &&
|
|
177
|
-
visualViewport.height > 0
|
|
178
|
-
) {
|
|
179
|
-
return {
|
|
180
|
-
width: visualViewport.width,
|
|
181
|
-
height: visualViewport.height,
|
|
182
|
-
source: 'visual-viewport',
|
|
183
|
-
};
|
|
179
|
+
if (container !== null) {
|
|
180
|
+
observer?.observe(container);
|
|
184
181
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
window.addEventListener('resize', listener);
|
|
183
|
+
window.visualViewport?.addEventListener('resize', listener);
|
|
184
|
+
document.addEventListener('visibilitychange', listener);
|
|
185
|
+
|
|
186
|
+
return () => {
|
|
187
|
+
observer?.disconnect();
|
|
188
|
+
window.removeEventListener('resize', listener);
|
|
189
|
+
window.visualViewport?.removeEventListener('resize', listener);
|
|
190
|
+
document.removeEventListener('visibilitychange', listener);
|
|
190
191
|
};
|
|
191
192
|
}
|