@mpgd/cli 0.35.0 → 0.36.1

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 (53) hide show
  1. package/dist/android-bundle-signer.d.ts +2 -0
  2. package/dist/android-bundle-signer.js +38 -0
  3. package/dist/android-signing-session.d.ts +22 -0
  4. package/dist/android-signing-session.js +198 -0
  5. package/dist/artifact-inspection-environment.d.ts +2 -0
  6. package/dist/artifact-inspection-environment.js +31 -0
  7. package/dist/asc-pin.json +26 -0
  8. package/dist/capacitor-shell-starter.d.ts +22 -0
  9. package/dist/capacitor-shell-starter.js +130 -7
  10. package/dist/deploy-planning.d.ts +64 -0
  11. package/dist/deploy-planning.js +471 -0
  12. package/dist/deploy-process.d.ts +28 -0
  13. package/dist/deploy-process.js +197 -0
  14. package/dist/index.d.ts +8 -0
  15. package/dist/index.js +409 -10
  16. package/dist/ios-ipa-inspection.js +151 -0
  17. package/dist/ios-keychain-import.swift +92 -0
  18. package/dist/ios-plist.d.ts +2 -0
  19. package/dist/ios-plist.js +75 -0
  20. package/dist/ios-signing-profile.d.ts +12 -0
  21. package/dist/ios-signing-profile.js +60 -0
  22. package/dist/ios-signing-session.d.ts +22 -0
  23. package/dist/ios-signing-session.js +256 -0
  24. package/dist/kit-upgrade-target-readiness.d.ts +8 -0
  25. package/dist/kit-upgrade-target-readiness.js +119 -0
  26. package/dist/kit-upgrade.d.ts +2 -0
  27. package/dist/kit-upgrade.js +3 -0
  28. package/dist/native-build-info.json +5 -0
  29. package/dist/native-build-target.js +12121 -0
  30. package/dist/native-deploy-run.d.ts +35 -0
  31. package/dist/native-deploy-run.js +462 -0
  32. package/dist/native-deploy-submission.d.ts +40 -0
  33. package/dist/native-deploy-submission.js +244 -0
  34. package/dist/native-deploy-workflow.d.ts +11 -0
  35. package/dist/native-deploy-workflow.js +328 -0
  36. package/dist/play-internal-submission.d.ts +32 -0
  37. package/dist/play-internal-submission.js +247 -0
  38. package/dist/play-publisher-port.d.ts +30 -0
  39. package/dist/play-publisher-port.js +1 -0
  40. package/dist/play-sdk-adapter.js +78 -0
  41. package/dist/release-state.d.ts +140 -0
  42. package/dist/release-state.js +1046 -0
  43. package/dist/release-workspace.d.ts +61 -0
  44. package/dist/release-workspace.js +504 -0
  45. package/dist/testflight-submission.d.ts +49 -0
  46. package/dist/testflight-submission.js +462 -0
  47. package/package.json +18 -13
  48. package/templates/phaser-game/README.md +30 -8
  49. package/templates/phaser-game/apps/target-devvit/README.md +12 -0
  50. package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +513 -0
  51. package/templates/phaser-game/apps/target-devvit/src/server/index.ts +8 -291
  52. package/templates/phaser-game/src/main.ts +34 -33
  53. package/templates/phaser-game/vite.shared.ts +5 -1
@@ -0,0 +1,35 @@
1
+ import type { PlatformVersionLedger } from '@mpgd/target-config';
2
+ import { type NativeDeploymentPlan, type NativeDeployTarget } from './deploy-planning.js';
3
+ import { type NativeStoreCredential } from './native-deploy-submission.js';
4
+ import { type NativeReleaseStatus } from './release-state.js';
5
+ export interface RunNativeDeploymentInput {
6
+ readonly plan: NativeDeploymentPlan;
7
+ readonly releaseKey: string;
8
+ readonly gameId: string;
9
+ readonly gameVersion: string;
10
+ readonly kit: {
11
+ readonly packageVersion: string;
12
+ readonly gitSha: string;
13
+ };
14
+ /** Required only when no game ledger exists on release-state. */
15
+ readonly initialLedger?: PlatformVersionLedger;
16
+ readonly approved: boolean;
17
+ readonly environment?: NodeJS.ProcessEnv;
18
+ readonly signal?: AbortSignal;
19
+ /** Called only after a newly built target has an immutable, committed build record. */
20
+ readonly onBuildRecorded?: (target: NativeDeployTarget) => void;
21
+ }
22
+ /** Reserve once, build only missing targets, then submit the recorded binaries. */
23
+ export declare function runNativeDeployment(input: RunNativeDeploymentInput): Promise<NativeReleaseStatus>;
24
+ /** Explain the next safe work without treating a missing or unknown store status as success. */
25
+ export declare function planNativeDeploymentSteps(plan: NativeDeploymentPlan, status: NativeReleaseStatus): {
26
+ readonly buildTargets: readonly NativeDeployTarget[];
27
+ readonly submitTargets: readonly NativeDeployTarget[];
28
+ };
29
+ export declare function readStoreCredential(plan: NativeDeploymentPlan, target: NativeDeployTarget, environment: NodeJS.ProcessEnv): NativeStoreCredential;
30
+ /** Install hooks must never inherit signing or store credentials. */
31
+ export declare function dependencyInstallEnvironment(environment: NodeJS.ProcessEnv, plan?: NativeDeploymentPlan): NodeJS.ProcessEnv;
32
+ /** Signing needs build keys, but neither web nor native build needs store API keys. */
33
+ export declare function withoutStoreSubmissionCredentials(environment: NodeJS.ProcessEnv, plan: NativeDeploymentPlan): NodeJS.ProcessEnv;
34
+ /** Internal artifact copy primitive; exported for boundary tests, not from the package root. */
35
+ export declare function persistImmutableFile(gameRoot: string, location: string, source: string): string;
@@ -0,0 +1,462 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { closeSync, constants, copyFileSync, existsSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, statSync, } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { doctorNativeDeployment, readNativeDeployCredentialNames, readNativeDeployTargetProfile, } from './deploy-planning.js';
5
+ import { withAndroidUploadSigningSession } from './android-signing-session.js';
6
+ import { withIosSigningSession } from './ios-signing-session.js';
7
+ import { submitRecordedNativeTarget, } from './native-deploy-submission.js';
8
+ import { isNativeSubmissionSettled, readNativeReleaseStatus, recordNativeReleaseBuild, reserveNativeRelease, } from './release-state.js';
9
+ import { installPinnedReleaseDependencies, pinNativeDeploymentPlan, runPinnedNativeBuild, withPinnedReleaseWorkspace, } from './release-workspace.js';
10
+ import { inspectAndroidBundleSigner } from './android-bundle-signer.js';
11
+ const gitAuthEnvironmentNames = ['GITHUB_TOKEN', 'GH_TOKEN', 'GIT_ASKPASS'];
12
+ /** Reserve once, build only missing targets, then submit the recorded binaries. */
13
+ export async function runNativeDeployment(input) {
14
+ if (!input.approved) {
15
+ throw new Error('Native deployment requires explicit internal-test approval.');
16
+ }
17
+ const environment = input.environment ?? process.env;
18
+ const pinned = await pinNativeDeploymentPlan(input.plan, input.kit, {
19
+ environment,
20
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
21
+ });
22
+ const statusInput = {
23
+ gameRoot: input.plan.gameRoot,
24
+ gameId: input.gameId,
25
+ releaseKey: input.releaseKey,
26
+ environment,
27
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
28
+ };
29
+ let existing;
30
+ try {
31
+ existing = await readNativeReleaseStatus(statusInput);
32
+ }
33
+ catch (error) {
34
+ if (!(error instanceof Error)
35
+ || error.message !== 'The requested native release has not been reserved.') {
36
+ throw error;
37
+ }
38
+ }
39
+ const pendingSteps = existing === undefined
40
+ ? {
41
+ buildTargets: input.plan.targets.map((entry) => entry.target),
42
+ submitTargets: input.plan.targets.map((entry) => entry.target),
43
+ }
44
+ : planNativeDeploymentSteps(input.plan, existing);
45
+ if (pendingSteps.buildTargets.length > 0) {
46
+ const doctor = doctorNativeDeployment({
47
+ game: input.plan.gameRoot,
48
+ profile: input.plan.profile,
49
+ targets: pendingSteps.buildTargets,
50
+ environment,
51
+ });
52
+ if (!doctor.healthy) {
53
+ throw new Error(`Native deployment environment is incomplete: ${doctor.checks
54
+ .filter((check) => check.status === 'missing')
55
+ .map((check) => check.name)
56
+ .join(', ')}.`);
57
+ }
58
+ }
59
+ for (const target of pendingSteps.buildTargets) {
60
+ const profile = readNativeDeployTargetProfile(input.plan, target);
61
+ requiredExistingFile(environment, profile.signingCredential.env);
62
+ if (target === 'ios') {
63
+ requiredExistingFile(environment, 'MPGD_IOS_PROVISIONING_PROFILE');
64
+ }
65
+ }
66
+ for (const target of pendingSteps.submitTargets) {
67
+ const profile = readNativeDeployTargetProfile(input.plan, target);
68
+ if (target === 'android') {
69
+ requiredExistingFile(environment, profile.submissionCredential.env);
70
+ }
71
+ else {
72
+ requiredExistingFile(environment, 'MPGD_ASC_BINARY');
73
+ }
74
+ readStoreCredential(input.plan, target, environment);
75
+ }
76
+ const reserved = await reserveNativeRelease({
77
+ gameRoot: input.plan.gameRoot,
78
+ gameId: input.gameId,
79
+ releaseKey: input.releaseKey,
80
+ gameVersion: input.gameVersion,
81
+ sourceGitSha: pinned.gameGitSha,
82
+ kitGitSha: pinned.kitGitSha,
83
+ targetConfigDigest: pinned.targetConfigSha256,
84
+ targets: input.plan.targets.map((entry) => ({ target: entry.target })),
85
+ ...(input.initialLedger === undefined ? {} : { initialLedger: input.initialLedger }),
86
+ environment,
87
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
88
+ });
89
+ let status = await readNativeReleaseStatus({
90
+ gameRoot: input.plan.gameRoot,
91
+ gameId: input.gameId,
92
+ releaseKey: input.releaseKey,
93
+ environment,
94
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
95
+ });
96
+ const missing = planNativeDeploymentSteps(input.plan, status).buildTargets;
97
+ if (missing.length > 0) {
98
+ await withPinnedReleaseWorkspace(pinned, async (workspace) => {
99
+ const installEnvironment = dependencyInstallEnvironment(environment, input.plan);
100
+ const registrySecretValues = environment.MPGD_DEPENDENCY_INSTALL_ENV_NAMES?.split(',')
101
+ .map((name) => installEnvironment[name.trim()])
102
+ .filter((value) => value !== undefined) ?? [];
103
+ await installPinnedReleaseDependencies(workspace, {
104
+ environment: installEnvironment,
105
+ secretValues: registrySecretValues,
106
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
107
+ });
108
+ for (const target of missing) {
109
+ await buildAndRecordTarget(input, workspace, target, reserved.plan, environment);
110
+ }
111
+ }, {
112
+ environment,
113
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
114
+ });
115
+ }
116
+ for (const entry of input.plan.targets) {
117
+ status = await readNativeReleaseStatus({
118
+ gameRoot: input.plan.gameRoot,
119
+ gameId: input.gameId,
120
+ releaseKey: input.releaseKey,
121
+ environment,
122
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
123
+ });
124
+ if (status.builds[entry.target] === undefined) {
125
+ throw new Error(`${entry.target} has no verified build after the native build stage.`);
126
+ }
127
+ const submission = status.submissions[entry.target];
128
+ if (submission !== undefined && isNativeSubmissionSettled(submission.status)) {
129
+ continue;
130
+ }
131
+ await submitRecordedNativeTarget({
132
+ plan: input.plan,
133
+ gameId: input.gameId,
134
+ releaseKey: input.releaseKey,
135
+ credential: readStoreCredential(input.plan, entry.target, environment),
136
+ approved: input.approved,
137
+ environment,
138
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
139
+ });
140
+ }
141
+ return readNativeReleaseStatus({
142
+ gameRoot: input.plan.gameRoot,
143
+ gameId: input.gameId,
144
+ releaseKey: input.releaseKey,
145
+ environment,
146
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
147
+ });
148
+ }
149
+ /** Explain the next safe work without treating a missing or unknown store status as success. */
150
+ export function planNativeDeploymentSteps(plan, status) {
151
+ const expected = plan.targets.map((entry) => entry.target);
152
+ const reserved = Object.keys(status.plan.targets).sort();
153
+ if (JSON.stringify([...expected].sort()) !== JSON.stringify(reserved)
154
+ || status.plan.targetConfigDigest !== plan.targetConfigSha256) {
155
+ throw new Error('Native release reservation differs from the saved deployment plan.');
156
+ }
157
+ const buildTargets = [];
158
+ const submitTargets = [];
159
+ for (const entry of plan.targets) {
160
+ const build = status.builds[entry.target];
161
+ if (build === undefined) {
162
+ buildTargets.push(entry.target);
163
+ }
164
+ else if (build.inspectedAppId !== entry.appId) {
165
+ throw new Error(`${entry.target} build record app ID differs from the deployment plan.`);
166
+ }
167
+ const submission = status.submissions[entry.target];
168
+ if (submission !== undefined && build === undefined) {
169
+ throw new Error(`${entry.target} store checkpoint has no immutable build.`);
170
+ }
171
+ if (submission === undefined || !isNativeSubmissionSettled(submission.status)) {
172
+ submitTargets.push(entry.target);
173
+ }
174
+ }
175
+ return { buildTargets, submitTargets };
176
+ }
177
+ async function buildAndRecordTarget(input, workspace, target, reserved, environment) {
178
+ const planned = input.plan.targets.find((entry) => entry.target === target);
179
+ const version = reserved.targets[target];
180
+ if (planned === undefined || version === undefined) {
181
+ throw new Error('Native build target lacks its reserved version.');
182
+ }
183
+ const profile = readNativeDeployTargetProfile(input.plan, target);
184
+ const buildEnvironment = {
185
+ ...withoutStoreSubmissionCredentials(environment, input.plan),
186
+ APP_VERSION: input.gameVersion,
187
+ BUILD_ID: reserved.buildId,
188
+ MPGD_RELEASE_REVISION: String(reserved.releaseRevision),
189
+ MPGD_RELEASE_LABEL: reserved.releaseLabel,
190
+ ...(target === 'android' ? {
191
+ MPGD_TARGET_VERSION_CODE: String(version.versionCode),
192
+ MPGD_TARGET_VERSION_NAME: String(version.versionName),
193
+ } : {
194
+ MPGD_TARGET_BUILD_NUMBER: String(version.buildNumber),
195
+ MPGD_TARGET_MARKETING_VERSION: String(version.marketingVersion),
196
+ }),
197
+ };
198
+ const run = async (signedEnvironment, secretValues, signerOrTeam) => {
199
+ const built = await runPinnedNativeBuild(workspace, {
200
+ target,
201
+ profile: 'production',
202
+ mode: target === 'android' ? 'signed-archive' : 'store-export',
203
+ environment: signedEnvironment,
204
+ secretValues,
205
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
206
+ });
207
+ // Content-addressed candidates never block a rebuild after a crash before
208
+ // the immutable build record is committed. Unreferenced copies stay inert.
209
+ const artifactLocation = `.mpgd/releases/${input.releaseKey}/${target}-${sha256(built.artifact)}.${target === 'android' ? 'aab' : 'ipa'}`;
210
+ const manifestLocation = `.mpgd/releases/${input.releaseKey}/${target}-manifest-${sha256(built.releaseManifest)}.json`;
211
+ const artifactFile = persistImmutableFile(input.plan.gameRoot, artifactLocation, built.artifact);
212
+ const manifestFile = persistImmutableFile(input.plan.gameRoot, manifestLocation, built.releaseManifest);
213
+ const configDigest = createHash('sha256').update(JSON.stringify({
214
+ gameGitSha: workspace.input.gameGitSha,
215
+ lockfileSha256: workspace.input.lockfileSha256,
216
+ targetConfigSha256: workspace.input.targetConfigSha256,
217
+ deployConfigSha256: workspace.input.deployConfigSha256,
218
+ kitPackageVersion: workspace.input.kitPackageVersion,
219
+ kitGitSha: workspace.input.kitGitSha,
220
+ releaseKey: input.releaseKey,
221
+ target,
222
+ version,
223
+ })).digest('hex');
224
+ const inspectedSignerSha256 = target === 'android'
225
+ ? await inspectAndroidBundleSigner(artifactFile)
226
+ : undefined;
227
+ if (target === 'android' && inspectedSignerSha256 !== signerOrTeam.toLowerCase()) {
228
+ throw new Error('Native Android artifact signer differs from the prepared upload key.');
229
+ }
230
+ await recordNativeReleaseBuild({
231
+ gameRoot: input.plan.gameRoot,
232
+ gameId: input.gameId,
233
+ releaseKey: input.releaseKey,
234
+ target,
235
+ buildRunId: built.runId,
236
+ kitPackageVersion: workspace.input.kitPackageVersion,
237
+ buildConfigDigest: configDigest,
238
+ deployConfigSha256: input.plan.deployConfigSha256,
239
+ deploymentProfile: input.plan.profile,
240
+ deploymentDestination: planned.destination,
241
+ ...(planned.testGroup === undefined ? {} : { internalTestGroupId: planned.testGroup }),
242
+ artifactFile,
243
+ expectedArtifactSha256: sha256(artifactFile),
244
+ artifactLocation,
245
+ releaseManifestFile: manifestFile,
246
+ expectedReleaseManifestSha256: sha256(manifestFile),
247
+ inspectedAppId: planned.appId,
248
+ ...(target === 'android' ? { inspectedSignerSha256 } : { inspectedTeamId: signerOrTeam }),
249
+ environment,
250
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
251
+ });
252
+ input.onBuildRecorded?.(target);
253
+ };
254
+ if (target === 'android') {
255
+ await withAndroidUploadSigningSession({
256
+ keystoreFile: requiredEnvironment(environment, profile.signingCredential.env),
257
+ storePassword: requiredEnvironment(environment, 'MPGD_ANDROID_UPLOAD_STORE_PASSWORD'),
258
+ keyAlias: requiredEnvironment(environment, 'MPGD_ANDROID_UPLOAD_KEY_ALIAS'),
259
+ keyPassword: requiredEnvironment(environment, 'MPGD_ANDROID_UPLOAD_KEY_PASSWORD'),
260
+ expectedCertSha256: requiredEnvironment(environment, 'MPGD_ANDROID_UPLOAD_CERT_SHA256'),
261
+ environment: buildEnvironment,
262
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
263
+ }, async (session) => {
264
+ await run(session.environment, session.secretValues, session.expectedCertSha256);
265
+ });
266
+ }
267
+ else {
268
+ await withIosSigningSession({
269
+ p12File: requiredEnvironment(environment, profile.signingCredential.env),
270
+ p12Password: requiredEnvironment(environment, 'MPGD_IOS_SIGNING_P12_PASSWORD'),
271
+ provisioningProfileFile: requiredEnvironment(environment, 'MPGD_IOS_PROVISIONING_PROFILE'),
272
+ bundleId: planned.appId,
273
+ teamId: requiredEnvironment(environment, 'MPGD_IOS_TEAM_ID'),
274
+ environment: buildEnvironment,
275
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
276
+ }, async (session) => {
277
+ await run(session.environment, [
278
+ requiredEnvironment(environment, 'MPGD_IOS_SIGNING_P12_PASSWORD'),
279
+ ], requiredEnvironment(environment, 'MPGD_IOS_TEAM_ID'));
280
+ });
281
+ }
282
+ }
283
+ export function readStoreCredential(plan, target, environment) {
284
+ const profile = readNativeDeployTargetProfile(plan, target);
285
+ return target === 'android'
286
+ ? {
287
+ target,
288
+ serviceAccountFile: requiredEnvironment(environment, profile.submissionCredential.env),
289
+ }
290
+ : {
291
+ target,
292
+ ascBinary: requiredEnvironment(environment, 'MPGD_ASC_BINARY'),
293
+ appStoreAppId: requiredEnvironment(environment, 'MPGD_ASC_APP_ID'),
294
+ apiKeyId: requiredEnvironment(environment, 'MPGD_ASC_KEY_ID'),
295
+ apiIssuerId: requiredEnvironment(environment, 'MPGD_ASC_ISSUER_ID'),
296
+ apiPrivateKeyBase64: requiredEnvironment(environment, profile.submissionCredential.env),
297
+ };
298
+ }
299
+ function requiredEnvironment(environment, name) {
300
+ const value = environment[name];
301
+ if (value === undefined || value.trim() === '') {
302
+ throw new Error(`Native deployment requires ${name}.`);
303
+ }
304
+ return value;
305
+ }
306
+ /** Install hooks must never inherit signing or store credentials. */
307
+ export function dependencyInstallEnvironment(environment, plan) {
308
+ const allowed = [
309
+ 'PATH',
310
+ 'Path',
311
+ 'HOME',
312
+ 'TMPDIR',
313
+ 'TMP',
314
+ 'TEMP',
315
+ 'USER',
316
+ 'SHELL',
317
+ 'CI',
318
+ 'COREPACK_HOME',
319
+ 'PNPM_HOME',
320
+ 'npm_execpath',
321
+ 'npm_config_store_dir',
322
+ 'JAVA_HOME',
323
+ 'ANDROID_HOME',
324
+ 'ANDROID_SDK_ROOT',
325
+ 'SystemRoot',
326
+ 'windir',
327
+ 'ComSpec',
328
+ 'PATHEXT',
329
+ 'USERPROFILE',
330
+ 'APPDATA',
331
+ 'LOCALAPPDATA',
332
+ ];
333
+ const entries = allowed.flatMap((name) => {
334
+ const value = environment[name];
335
+ return value === undefined ? [] : [[name, value]];
336
+ });
337
+ const forbidden = new Set([
338
+ ...(plan === undefined ? [] : readNativeDeployCredentialNames(plan)),
339
+ 'GOOGLE_APPLICATION_CREDENTIALS',
340
+ ...gitAuthEnvironmentNames,
341
+ ]);
342
+ for (const name of environment.MPGD_DEPENDENCY_INSTALL_ENV_NAMES?.split(',') ?? []) {
343
+ const normalized = name.trim();
344
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(normalized)
345
+ || normalized.startsWith('MPGD_') || normalized.startsWith('GIT_CONFIG_')
346
+ || forbidden.has(normalized)) {
347
+ throw new Error(`Dependency installation environment name ${normalized} is not allowed.`);
348
+ }
349
+ const value = environment[normalized];
350
+ if (value === undefined || value === '') {
351
+ throw new Error(`Dependency installation requires ${normalized}.`);
352
+ }
353
+ entries.push([normalized, value]);
354
+ }
355
+ return Object.fromEntries(entries);
356
+ }
357
+ /** Signing needs build keys, but neither web nor native build needs store API keys. */
358
+ export function withoutStoreSubmissionCredentials(environment, plan) {
359
+ const result = { ...environment };
360
+ for (const name of environment.MPGD_DEPENDENCY_INSTALL_ENV_NAMES?.split(',') ?? []) {
361
+ delete result[name.trim()];
362
+ }
363
+ delete result.MPGD_DEPENDENCY_INSTALL_ENV_NAMES;
364
+ delete result.MPGD_RECORDED_BUILD_MARKER;
365
+ delete result.NPM_TOKEN;
366
+ delete result.NODE_AUTH_TOKEN;
367
+ delete result.SIGNING_B64;
368
+ delete result.SUBMISSION_B64;
369
+ delete result.PROFILE_B64;
370
+ for (const name of readNativeDeployCredentialNames(plan)) {
371
+ delete result[name];
372
+ }
373
+ for (const name of [
374
+ 'MPGD_ASC_KEY_ID',
375
+ 'MPGD_ASC_ISSUER_ID',
376
+ 'MPGD_ASC_APP_ID',
377
+ 'MPGD_ASC_BINARY',
378
+ 'GOOGLE_APPLICATION_CREDENTIALS',
379
+ 'MPGD_ANDROID_UPLOAD_KEYSTORE',
380
+ 'MPGD_ANDROID_UPLOAD_STORE_PASSWORD',
381
+ 'MPGD_ANDROID_UPLOAD_KEY_ALIAS',
382
+ 'MPGD_ANDROID_UPLOAD_KEY_PASSWORD',
383
+ 'MPGD_IOS_SIGNING_P12',
384
+ 'MPGD_IOS_SIGNING_P12_PASSWORD',
385
+ 'MPGD_IOS_PROVISIONING_PROFILE',
386
+ ...gitAuthEnvironmentNames,
387
+ ]) {
388
+ delete result[name];
389
+ }
390
+ for (const name of Object.keys(result)) {
391
+ if (name.startsWith('MPGD_ANDROID_SIGNING_')
392
+ || name.startsWith('MPGD_IOS_SESSION_')
393
+ || name.startsWith('GIT_CONFIG_')) {
394
+ delete result[name];
395
+ }
396
+ }
397
+ return result;
398
+ }
399
+ function requiredExistingFile(environment, name) {
400
+ const file = requiredEnvironment(environment, name);
401
+ if (!existsSync(file) || !statSync(file).isFile()) {
402
+ throw new Error(`Native deployment ${name} must name an existing regular file.`);
403
+ }
404
+ return file;
405
+ }
406
+ /** Internal artifact copy primitive; exported for boundary tests, not from the package root. */
407
+ export function persistImmutableFile(gameRoot, location, source) {
408
+ const root = realpathSync(gameRoot);
409
+ const destination = path.resolve(root, location);
410
+ const relative = path.relative(root, destination);
411
+ if (relative === '' || relative === '..' || relative.startsWith(`..${path.sep}`)
412
+ || path.isAbsolute(relative)) {
413
+ throw new Error('Native release output escapes the game directory.');
414
+ }
415
+ const directory = path.dirname(destination);
416
+ let current = root;
417
+ for (const segment of path.relative(root, directory).split(path.sep)) {
418
+ current = path.join(current, segment);
419
+ if (!existsSync(current)) {
420
+ mkdirSync(current, { mode: 0o700 });
421
+ }
422
+ const entry = lstatSync(current);
423
+ if (entry.isSymbolicLink() || !entry.isDirectory()) {
424
+ throw new Error('Native release output path contains a symlink or non-directory.');
425
+ }
426
+ }
427
+ if (existsSync(destination) && !lstatSync(destination).isFile()) {
428
+ throw new Error('Native release output must be a regular file.');
429
+ }
430
+ if (!existsSync(destination)) {
431
+ try {
432
+ copyFileSync(source, destination, constants.COPYFILE_EXCL);
433
+ }
434
+ catch (error) {
435
+ if (error.code !== 'EEXIST') {
436
+ throw error;
437
+ }
438
+ }
439
+ }
440
+ if (!lstatSync(destination).isFile() || sha256(destination) !== sha256(source)) {
441
+ throw new Error('Existing native release output has different bytes.');
442
+ }
443
+ return destination;
444
+ }
445
+ function sha256(file) {
446
+ const hash = createHash('sha256');
447
+ const descriptor = openSync(file, 'r');
448
+ const chunk = Buffer.allocUnsafe(1024 * 1024);
449
+ try {
450
+ for (;;) {
451
+ const bytes = readSync(descriptor, chunk, 0, chunk.byteLength, null);
452
+ if (bytes === 0) {
453
+ break;
454
+ }
455
+ hash.update(chunk.subarray(0, bytes));
456
+ }
457
+ }
458
+ finally {
459
+ closeSync(descriptor);
460
+ }
461
+ return hash.digest('hex');
462
+ }
@@ -0,0 +1,40 @@
1
+ import type { NativeDeploymentPlan } from './deploy-planning.js';
2
+ import { checkpointNativeSubmission, readNativeReleaseStatus, reclaimNativeSubmission, releaseNativeSubmissionLease, type ImmutableNativeBuildRecord, type NativeSubmissionCheckpoint } from './release-state.js';
3
+ import { submitVerifiedAndroidBundle } from './play-internal-submission.js';
4
+ import { submitVerifiedIosBuild } from './testflight-submission.js';
5
+ export type NativeStoreCredential = {
6
+ readonly target: 'android';
7
+ readonly serviceAccountFile: string;
8
+ } | {
9
+ readonly target: 'ios';
10
+ readonly ascBinary: string;
11
+ readonly appStoreAppId: string;
12
+ readonly apiKeyId: string;
13
+ readonly apiIssuerId: string;
14
+ readonly apiPrivateKeyBase64: string;
15
+ };
16
+ export interface SubmitRecordedNativeTargetInput {
17
+ readonly plan: NativeDeploymentPlan;
18
+ readonly gameId: string;
19
+ readonly releaseKey: string;
20
+ readonly credential: NativeStoreCredential;
21
+ readonly approved: boolean;
22
+ /** Allows the same caller to continue an in-process attempt without taking over its lease. */
23
+ readonly attemptId?: string;
24
+ readonly environment?: NodeJS.ProcessEnv;
25
+ readonly signal?: AbortSignal;
26
+ }
27
+ /** Credential-free injection boundary for recovery tests. */
28
+ export interface NativeSubmissionPorts {
29
+ readonly readStatus: typeof readNativeReleaseStatus;
30
+ readonly checkpoint: typeof checkpointNativeSubmission;
31
+ readonly reclaim: typeof reclaimNativeSubmission;
32
+ readonly releaseLease: typeof releaseNativeSubmissionLease;
33
+ readonly submitAndroid: typeof submitVerifiedAndroidBundle;
34
+ readonly submitIos: typeof submitVerifiedIosBuild;
35
+ }
36
+ /** Resume exactly one verified build; never rebuild or allocate another version here. */
37
+ export declare function submitRecordedNativeTarget(input: SubmitRecordedNativeTargetInput): Promise<NativeSubmissionCheckpoint>;
38
+ export declare function submitRecordedNativeTargetWithPorts(input: SubmitRecordedNativeTargetInput, ports: NativeSubmissionPorts): Promise<NativeSubmissionCheckpoint>;
39
+ /** Validate the workflow-restored manifest against the immutable release-state hash. */
40
+ export declare function verifyRecordedReleaseManifest(gameRoot: string, record: Pick<ImmutableNativeBuildRecord, 'artifactLocation' | 'releaseManifestSha256' | 'target'>): void;