@mpgd/cli 0.63.0 → 0.65.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 (47) hide show
  1. package/dist/app-store-iap-asc.d.ts +5 -14
  2. package/dist/app-store-iap-asc.js +9 -116
  3. package/dist/app-store-presence-asc.d.ts +23 -0
  4. package/dist/app-store-presence-asc.js +162 -0
  5. package/dist/app-store-presence-port.d.ts +101 -0
  6. package/dist/app-store-presence-port.js +13 -0
  7. package/dist/asc-session.d.ts +43 -0
  8. package/dist/asc-session.js +118 -0
  9. package/dist/index.d.ts +6 -0
  10. package/dist/index.js +75 -1
  11. package/dist/native-build-info.json +2 -2
  12. package/dist/native-build-target.js +492 -413
  13. package/dist/play-listing-port.d.ts +88 -0
  14. package/dist/play-listing-port.js +12 -0
  15. package/dist/play-sdk-adapter.js +103 -17
  16. package/dist/store-presence-app-store.d.ts +139 -0
  17. package/dist/store-presence-app-store.js +537 -0
  18. package/dist/store-presence-command.d.ts +50 -0
  19. package/dist/store-presence-command.js +145 -0
  20. package/dist/store-presence-play.d.ts +116 -0
  21. package/dist/store-presence-play.js +495 -0
  22. package/dist/store-presence.d.ts +106 -0
  23. package/dist/store-presence.js +173 -0
  24. package/dist/store-products-command.d.ts +2 -1
  25. package/dist/store-products-command.js +15 -42
  26. package/dist/store-products.d.ts +3 -11
  27. package/dist/store-products.js +7 -75
  28. package/dist/store-sync.d.ts +50 -0
  29. package/dist/store-sync.js +137 -0
  30. package/package.json +19 -19
  31. package/templates/lite-overlay/README.md +4 -0
  32. package/templates/lite-overlay/apps/target-devvit/README.md +10 -0
  33. package/templates/phaser-game/README.md +32 -3
  34. package/templates/phaser-game/apps/target-ait/package.json +2 -2
  35. package/templates/phaser-game/apps/target-cloudflare-pages/package.json +2 -2
  36. package/templates/phaser-game/apps/target-devvit/README.md +10 -0
  37. package/templates/phaser-game/apps/target-devvit/package.json +2 -2
  38. package/templates/phaser-game/package.json +2 -2
  39. package/templates/phaser-game/src/i18n/messages.ts +3 -0
  40. package/templates/phaser-game/src/main.ts +42 -0
  41. package/templates/phaser-game/src/platform/buildGateways/capacitorAndroid.ts +5 -0
  42. package/templates/phaser-game/src/platform/buildGateways/capacitorIos.ts +5 -0
  43. package/templates/phaser-game/src/platform/commerceAccountBinding.ts +81 -1
  44. package/templates/phaser-game/src/platform/gameServices.ts +88 -4
  45. package/templates/phaser-game/src/platform/guestIdentity.ts +4 -3
  46. package/templates/phaser-game/src/platform/sessionGatedServices.ts +149 -21
  47. package/templates/phaser-game/src/styles.css +12 -0
@@ -1,20 +1,11 @@
1
1
  import type { AscIapPort } from './app-store-iap-port.js';
2
+ import { type AscCommandRunner, type AscCredential } from './asc-session.js';
3
+ export type { AscCommandRunner, AscCredential } from './asc-session.js';
2
4
  /**
3
- * The in-app purchase port on the pinned `asc` 5.5.0 CLI, with the same
4
- * binary pin, isolated environment and API key as the TestFlight
5
- * submission. Each method is one `asc ... --output json` command; the JSON
6
- * is App Store Connect's own resource document, which `asc` prints as it
7
- * received it (`internal/asc/types/resources.go` at the 5.5.0 tag).
5
+ * The in-app purchase port on the pinned `asc` 5.5.0 CLI (`asc-session.ts`).
6
+ * Each method is one `asc ... --output json` command.
8
7
  */
9
- /** Runs one asc command with `--output json` appended and returns the parsed JSON. */
10
- export type AscCommandRunner = (args: readonly string[], timeoutMs: number) => Promise<unknown>;
11
- export interface AscCredential {
12
- readonly ascBinary: string;
13
- readonly apiKeyId: string;
14
- readonly apiIssuerId: string;
15
- readonly apiPrivateKeyBase64: string;
16
- }
17
- /** Copy and verify the pinned binary into a private directory; `close` removes it. */
8
+ /** Copy and verify the pinned binary; `close` removes it. */
18
9
  export declare function openAscIapSession(credential: AscCredential, signal?: AbortSignal): Promise<{
19
10
  readonly port: AscIapPort;
20
11
  close(): void;
@@ -1,58 +1,14 @@
1
- import { chmodSync, copyFileSync, mkdtempSync, rmSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import path from 'node:path';
4
- import { ReleaseProcessError, runReleaseProcess } from './deploy-process.js';
1
+ import { ascExitNotFound, isEmptyResource, isRecord, list, ascListTimeoutMs as listTimeoutMs, ascLookupTimeoutMs as lookupTimeoutMs, openAscSession, optionalString, relationshipId, requiredString, single, ascUploadTimeoutMs as uploadTimeoutMs, } from './asc-session.js';
2
+ import { ReleaseProcessError } from './deploy-process.js';
5
3
  import { sameDecimal } from './store-products.js';
6
- import { isolatedAscEnvironment, verifyPinnedAscBinary } from './testflight-submission.js';
7
- const lookupTimeoutMs = 60_000;
8
- const listTimeoutMs = 3 * 60_000;
9
- const uploadTimeoutMs = 10 * 60_000;
10
- const maxJsonBytes = 8 * 1024 * 1024;
11
- /** asc exit code for an App Store Connect 404 (`cmd/exit_codes.go`, 5.5.0). */
12
- const ascExitNotFound = 4;
13
- /** Copy and verify the pinned binary into a private directory; `close` removes it. */
4
+ /**
5
+ * The in-app purchase port on the pinned `asc` 5.5.0 CLI (`asc-session.ts`).
6
+ * Each method is one `asc ... --output json` command.
7
+ */
8
+ /** Copy and verify the pinned binary; `close` removes it. */
14
9
  export async function openAscIapSession(credential, signal) {
15
- if (!path.isAbsolute(credential.ascBinary)) {
16
- throw new Error('asc binary must be an absolute file path.');
17
- }
18
- const tempRoot = mkdtempSync(path.join(tmpdir(), 'mpgd-asc-iap-'));
19
- try {
20
- const binary = path.join(tempRoot, 'asc');
21
- copyFileSync(credential.ascBinary, binary);
22
- chmodSync(binary, 0o700);
23
- await verifyPinnedAscBinary(binary);
24
- const environment = isolatedAscEnvironment(credential, tempRoot);
25
- const run = async (args, timeoutMs) => {
26
- const result = await runReleaseProcess({
27
- command: binary,
28
- args: [...args, '--output', 'json'],
29
- cwd: tempRoot,
30
- environment,
31
- timeoutMs,
32
- maxOutputBytes: maxJsonBytes,
33
- captureMachineStdout: true,
34
- secretValues: [credential.apiKeyId, credential.apiIssuerId, credential.apiPrivateKeyBase64],
35
- signal,
36
- });
37
- if (result.truncated || result.machineStdout === undefined) {
38
- throw new Error('asc JSON output was truncated or unavailable.');
39
- }
40
- try {
41
- return JSON.parse(result.machineStdout);
42
- }
43
- catch {
44
- throw new Error('asc returned invalid JSON.');
45
- }
46
- };
47
- return {
48
- port: createAscIapPort(run),
49
- close: () => rmSync(tempRoot, { recursive: true, force: true }),
50
- };
51
- }
52
- catch (error) {
53
- rmSync(tempRoot, { recursive: true, force: true });
54
- throw error;
55
- }
10
+ const session = await openAscSession(credential, signal);
11
+ return { port: createAscIapPort(session.run), close: () => session.close() };
56
12
  }
57
13
  /** The port over any runner; the tests drive it with recorded asc output. */
58
14
  export function createAscIapPort(run, today = () => new Date().toISOString().slice(0, 10)) {
@@ -261,66 +217,3 @@ export function createAscIapPort(run, today = () => new Date().toISOString().sli
261
217
  },
262
218
  };
263
219
  }
264
- function single(value, command) {
265
- const resource = isRecord(value) ? toResource(value.data) : undefined;
266
- if (resource === undefined) {
267
- throw new Error(`asc ${command} returned no resource.`);
268
- }
269
- return resource;
270
- }
271
- function list(value, command) {
272
- const resources = isRecord(value) && Array.isArray(value.data)
273
- ? value.data.map(toResource)
274
- : undefined;
275
- if (resources === undefined || resources.some((resource) => resource === undefined)) {
276
- throw new Error(`asc ${command} returned no resource list.`);
277
- }
278
- return resources;
279
- }
280
- function toResource(value) {
281
- if (!isRecord(value) || typeof value.id !== 'string' || value.id === '') {
282
- return undefined;
283
- }
284
- return {
285
- id: value.id,
286
- attributes: isRecord(value.attributes) ? value.attributes : {},
287
- ...(value.relationships === undefined ? {} : { relationships: value.relationships }),
288
- };
289
- }
290
- /**
291
- * App Store Connect's `data: null`, or asc's rendering of it: asc 5.5.0
292
- * decodes into a non-pointer resource and prints `{ "type": "", "id": "" }`.
293
- */
294
- function isEmptyResource(value) {
295
- if (!isRecord(value)) {
296
- return false;
297
- }
298
- return value.data === null
299
- || (isRecord(value.data) && (value.data.id === undefined || value.data.id === ''));
300
- }
301
- function relationshipId(relationships, name) {
302
- if (!isRecord(relationships)) {
303
- return undefined;
304
- }
305
- const relationship = relationships[name];
306
- if (!isRecord(relationship) || !isRecord(relationship.data)) {
307
- return undefined;
308
- }
309
- return typeof relationship.data.id === 'string' ? relationship.data.id : undefined;
310
- }
311
- function requiredString(value, label) {
312
- if (typeof value !== 'string') {
313
- throw new Error(`asc output has no ${label}.`);
314
- }
315
- return value;
316
- }
317
- function optionalString(key, value) {
318
- const result = {};
319
- if (typeof value === 'string' && value !== '') {
320
- result[key] = value;
321
- }
322
- return result;
323
- }
324
- function isRecord(value) {
325
- return typeof value === 'object' && value !== null && !Array.isArray(value);
326
- }
@@ -0,0 +1,23 @@
1
+ import type { AscStorePresencePort } from './app-store-presence-port.js';
2
+ import { type AscCommandRunner, type AscCredential } from './asc-session.js';
3
+ /**
4
+ * The store page port on the pinned `asc` 5.5.0 CLI. Commands (5.5.0 source,
5
+ * `internal/cli/...`):
6
+ *
7
+ * - `apps view`, `apps info list` (`apps/app_infos.go`)
8
+ * - `localizations list --type app-info` and `--version` (`localizations/localizations.go`)
9
+ * - `app-setup info set` (`apps/app_setup.go`): creates or updates one app
10
+ * information localization
11
+ * - `versions list` (`versions/versions.go`)
12
+ * - `localizations create` and `localizations update --id` (`localizations/create.go`, `update.go`)
13
+ * - `screenshots list|upload|delete` (`assets/assets_screenshots*.go`): an
14
+ * upload of one file appends it to the set and waits for the asset
15
+ * delivery to reach `COMPLETE`, failing on `FAILED`
16
+ */
17
+ /** Copy and verify the pinned binary; `close` removes it. */
18
+ export declare function openAscStorePresenceSession(credential: AscCredential, signal?: AbortSignal): Promise<{
19
+ readonly port: AscStorePresencePort;
20
+ close(): void;
21
+ }>;
22
+ /** The port over any runner; the tests drive it with recorded asc output. */
23
+ export declare function createAscStorePresencePort(run: AscCommandRunner): AscStorePresencePort;
@@ -0,0 +1,162 @@
1
+ import { ascListTimeoutMs, ascLookupTimeoutMs, ascUploadTimeoutMs, isRecord, list, openAscSession, optionalString, requiredString, single, } from './asc-session.js';
2
+ /**
3
+ * The store page port on the pinned `asc` 5.5.0 CLI. Commands (5.5.0 source,
4
+ * `internal/cli/...`):
5
+ *
6
+ * - `apps view`, `apps info list` (`apps/app_infos.go`)
7
+ * - `localizations list --type app-info` and `--version` (`localizations/localizations.go`)
8
+ * - `app-setup info set` (`apps/app_setup.go`): creates or updates one app
9
+ * information localization
10
+ * - `versions list` (`versions/versions.go`)
11
+ * - `localizations create` and `localizations update --id` (`localizations/create.go`, `update.go`)
12
+ * - `screenshots list|upload|delete` (`assets/assets_screenshots*.go`): an
13
+ * upload of one file appends it to the set and waits for the asset
14
+ * delivery to reach `COMPLETE`, failing on `FAILED`
15
+ */
16
+ /** Copy and verify the pinned binary; `close` removes it. */
17
+ export async function openAscStorePresenceSession(credential, signal) {
18
+ const session = await openAscSession(credential, signal);
19
+ return { port: createAscStorePresencePort(session.run), close: () => session.close() };
20
+ }
21
+ /** The port over any runner; the tests drive it with recorded asc output. */
22
+ export function createAscStorePresencePort(run) {
23
+ return {
24
+ async readAppBundleId(appId) {
25
+ const app = single(await run(['apps', 'view', '--id', appId], ascLookupTimeoutMs), 'apps view');
26
+ const bundleId = app.attributes.bundleId;
27
+ if (app.id !== appId || typeof bundleId !== 'string') {
28
+ throw new Error('asc apps view returned no bundle ID for the app.');
29
+ }
30
+ return bundleId;
31
+ },
32
+ async listAppInfos(appId) {
33
+ return list(await run(['apps', 'info', 'list', '--app', appId], ascLookupTimeoutMs), 'apps info list')
34
+ .map((resource) => ({
35
+ id: resource.id,
36
+ ...optionalString('state', resource.attributes.state ?? resource.attributes.appStoreState),
37
+ }));
38
+ },
39
+ async listAppInfoLocalizations(appId, appInfoId) {
40
+ return list(await run([
41
+ 'localizations', 'list',
42
+ '--type', 'app-info',
43
+ '--app', appId,
44
+ '--app-info', appInfoId,
45
+ '--paginate',
46
+ ], ascListTimeoutMs), 'localizations list --type app-info').map((resource) => ({
47
+ id: resource.id,
48
+ locale: requiredString(resource.attributes.locale, 'app info localization locale'),
49
+ ...optionalString('name', resource.attributes.name),
50
+ ...optionalString('subtitle', resource.attributes.subtitle),
51
+ ...optionalString('privacyPolicyUrl', resource.attributes.privacyPolicyUrl),
52
+ }));
53
+ },
54
+ async listVersions(appId) {
55
+ return list(await run(['versions', 'list', '--app', appId, '--platform', 'IOS', '--paginate'], ascListTimeoutMs), 'versions list').map((resource) => ({
56
+ id: resource.id,
57
+ ...optionalString('versionString', resource.attributes.versionString),
58
+ ...optionalString('state', resource.attributes.appVersionState ?? resource.attributes.appStoreState),
59
+ ...optionalString('appStoreState', resource.attributes.appStoreState),
60
+ ...optionalString('createdDate', resource.attributes.createdDate),
61
+ }));
62
+ },
63
+ async listVersionLocalizations(versionId) {
64
+ return list(await run(['localizations', 'list', '--version', versionId, '--paginate'], ascListTimeoutMs), 'localizations list').map((resource) => ({
65
+ id: resource.id,
66
+ locale: requiredString(resource.attributes.locale, 'version localization locale'),
67
+ ...versionFields(resource),
68
+ }));
69
+ },
70
+ async listScreenshotSets(versionLocalizationId) {
71
+ const result = await run(['screenshots', 'list', '--version-localization', versionLocalizationId], ascListTimeoutMs);
72
+ if (!isRecord(result) || !Array.isArray(result.sets)) {
73
+ throw new Error('asc screenshots list returned no screenshot sets.');
74
+ }
75
+ return result.sets.map((entry) => {
76
+ const set = isRecord(entry) ? single({ data: entry.set }, 'screenshots list set') : undefined;
77
+ const screenshots = isRecord(entry) ? list({ data: entry.screenshots }, 'screenshots list') : undefined;
78
+ if (set === undefined || screenshots === undefined) {
79
+ throw new Error('asc screenshots list returned a malformed set.');
80
+ }
81
+ return {
82
+ id: set.id,
83
+ displayType: requiredString(set.attributes.screenshotDisplayType, 'screenshot display type'),
84
+ screenshots: screenshots.map(toScreenshot),
85
+ };
86
+ });
87
+ },
88
+ async upsertAppInfoLocalization(input) {
89
+ await run([
90
+ 'app-setup', 'info', 'set',
91
+ '--app', input.appId,
92
+ '--app-info', input.appInfoId,
93
+ '--locale', input.locale,
94
+ '--name', input.name,
95
+ ...(input.subtitle === undefined ? [] : ['--subtitle', input.subtitle]),
96
+ ...(input.privacyPolicyUrl === undefined ? [] : ['--privacy-policy-url', input.privacyPolicyUrl]),
97
+ ], ascLookupTimeoutMs);
98
+ },
99
+ async createVersionLocalization(input) {
100
+ return single(await run([
101
+ 'localizations', 'create',
102
+ '--version', input.versionId,
103
+ '--locale', input.locale,
104
+ ...fieldFlags(input.fields),
105
+ ], ascLookupTimeoutMs), 'localizations create').id;
106
+ },
107
+ async updateVersionLocalization(input) {
108
+ const flags = fieldFlags(input.fields);
109
+ if (flags.length === 0) {
110
+ return;
111
+ }
112
+ await run(['localizations', 'update', '--id', input.localizationId, ...flags], ascLookupTimeoutMs);
113
+ },
114
+ async uploadScreenshot(input) {
115
+ const result = await run([
116
+ 'screenshots', 'upload',
117
+ '--version-localization', input.versionLocalizationId,
118
+ '--path', input.file,
119
+ '--device-type', input.displayType,
120
+ ], ascUploadTimeoutMs);
121
+ const results = isRecord(result) && Array.isArray(result.results) ? result.results : [];
122
+ const [item] = results;
123
+ if (results.length !== 1 || !isRecord(item) || typeof item.assetId !== 'string' || item.assetId === '') {
124
+ throw new Error('asc screenshots upload returned no uploaded screenshot.');
125
+ }
126
+ const state = typeof item.state === 'string' ? item.state.toUpperCase() : '';
127
+ if (state !== 'COMPLETE') {
128
+ throw new Error(`App Store Connect reports screenshot ${item.assetId} as ${state || 'not delivered'}.`);
129
+ }
130
+ return { id: item.assetId, deliveryState: state };
131
+ },
132
+ async deleteScreenshot(screenshotId) {
133
+ await run(['screenshots', 'delete', '--id', screenshotId, '--confirm'], ascLookupTimeoutMs);
134
+ },
135
+ };
136
+ }
137
+ const fieldNames = [
138
+ ['description', '--description'],
139
+ ['keywords', '--keywords'],
140
+ ['promotionalText', '--promotional-text'],
141
+ ['whatsNew', '--whats-new'],
142
+ ['supportUrl', '--support-url'],
143
+ ['marketingUrl', '--marketing-url'],
144
+ ];
145
+ function fieldFlags(fields) {
146
+ return fieldNames.flatMap(([field, flag]) => {
147
+ const value = fields[field];
148
+ return value === undefined ? [] : [flag, value];
149
+ });
150
+ }
151
+ function versionFields(resource) {
152
+ return Object.assign({}, ...fieldNames.map(([field]) => optionalString(field, resource.attributes[field])));
153
+ }
154
+ function toScreenshot(resource) {
155
+ const delivery = resource.attributes.assetDeliveryState;
156
+ return {
157
+ id: resource.id,
158
+ ...optionalString('fileName', resource.attributes.fileName),
159
+ ...optionalString('sourceFileChecksum', resource.attributes.sourceFileChecksum),
160
+ ...optionalString('deliveryState', isRecord(delivery) ? delivery.state : undefined),
161
+ };
162
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * App Store Connect store page port: app information localizations (name,
3
+ * subtitle, privacy policy URL), App Store version localizations (description,
4
+ * keywords, promotional text, what's new, URLs) and their screenshot sets.
5
+ * `app-store-presence-asc.ts` implements it with the pinned `asc` CLI; the
6
+ * planner and tests see only these shapes.
7
+ *
8
+ * Resources (App Store Connect API):
9
+ * - App infos and their localizations: https://developer.apple.com/documentation/appstoreconnectapi/app-infos
10
+ * - App Store versions and their localizations: https://developer.apple.com/documentation/appstoreconnectapi/app-store-versions
11
+ * - Screenshot sets and screenshots: https://developer.apple.com/documentation/appstoreconnectapi/app-screenshot-sets
12
+ */
13
+ export interface AscAppInfo {
14
+ readonly id: string;
15
+ /** `AppInfoState` (`state`, or the older `appStoreState`), for example `PREPARE_FOR_SUBMISSION`. */
16
+ readonly state?: string | undefined;
17
+ }
18
+ export interface AscAppInfoLocalization {
19
+ readonly id: string;
20
+ readonly locale: string;
21
+ readonly name?: string | undefined;
22
+ readonly subtitle?: string | undefined;
23
+ readonly privacyPolicyUrl?: string | undefined;
24
+ }
25
+ export interface AscAppStoreVersion {
26
+ readonly id: string;
27
+ readonly versionString?: string | undefined;
28
+ /** `appVersionState`, or `appStoreState` when App Store Connect sends only that. */
29
+ readonly state?: string | undefined;
30
+ /** The legacy `appStoreState`; App Store Connect fills the two inconsistently. */
31
+ readonly appStoreState?: string | undefined;
32
+ readonly createdDate?: string | undefined;
33
+ }
34
+ /** The version localization fields the sync writes. */
35
+ export interface AscVersionLocalizationFields {
36
+ readonly description?: string | undefined;
37
+ readonly keywords?: string | undefined;
38
+ readonly promotionalText?: string | undefined;
39
+ readonly whatsNew?: string | undefined;
40
+ readonly supportUrl?: string | undefined;
41
+ readonly marketingUrl?: string | undefined;
42
+ }
43
+ export interface AscVersionLocalization extends AscVersionLocalizationFields {
44
+ readonly id: string;
45
+ readonly locale: string;
46
+ }
47
+ export interface AscScreenshot {
48
+ readonly id: string;
49
+ readonly fileName?: string | undefined;
50
+ /** MD5 of the uploaded file, as App Store Connect records it. */
51
+ readonly sourceFileChecksum?: string | undefined;
52
+ /** `assetDeliveryState.state`: `AWAITING_UPLOAD`, `UPLOAD_COMPLETE`, `COMPLETE` or `FAILED`. */
53
+ readonly deliveryState?: string | undefined;
54
+ }
55
+ export interface AscScreenshotSet {
56
+ readonly id: string;
57
+ /** `screenshotDisplayType`, for example `APP_IPHONE_67`. */
58
+ readonly displayType: string;
59
+ /** In the set's order. */
60
+ readonly screenshots: readonly AscScreenshot[];
61
+ }
62
+ export interface AscStorePresencePort {
63
+ /** The app record's bundle ID (`apps view`). */
64
+ readAppBundleId(appId: string): Promise<string>;
65
+ listAppInfos(appId: string): Promise<AscAppInfo[]>;
66
+ listAppInfoLocalizations(appId: string, appInfoId: string): Promise<AscAppInfoLocalization[]>;
67
+ /** iOS App Store versions of the app. */
68
+ listVersions(appId: string): Promise<AscAppStoreVersion[]>;
69
+ listVersionLocalizations(versionId: string): Promise<AscVersionLocalization[]>;
70
+ listScreenshotSets(versionLocalizationId: string): Promise<AscScreenshotSet[]>;
71
+ /** Creates the locale's app information localization or updates it. */
72
+ upsertAppInfoLocalization(input: {
73
+ readonly appId: string;
74
+ readonly appInfoId: string;
75
+ readonly locale: string;
76
+ readonly name: string;
77
+ readonly subtitle?: string;
78
+ readonly privacyPolicyUrl?: string;
79
+ }): Promise<void>;
80
+ /** Returns the new localization's ID. */
81
+ createVersionLocalization(input: {
82
+ readonly versionId: string;
83
+ readonly locale: string;
84
+ readonly fields: AscVersionLocalizationFields;
85
+ }): Promise<string>;
86
+ updateVersionLocalization(input: {
87
+ readonly localizationId: string;
88
+ readonly fields: AscVersionLocalizationFields;
89
+ }): Promise<void>;
90
+ /**
91
+ * Appends one screenshot to the set of `displayType` (creating the set),
92
+ * waiting until App Store Connect reports the asset delivered. Throws when
93
+ * the delivery fails.
94
+ */
95
+ uploadScreenshot(input: {
96
+ readonly versionLocalizationId: string;
97
+ readonly displayType: string;
98
+ readonly file: string;
99
+ }): Promise<AscScreenshot>;
100
+ deleteScreenshot(screenshotId: string): Promise<void>;
101
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * App Store Connect store page port: app information localizations (name,
3
+ * subtitle, privacy policy URL), App Store version localizations (description,
4
+ * keywords, promotional text, what's new, URLs) and their screenshot sets.
5
+ * `app-store-presence-asc.ts` implements it with the pinned `asc` CLI; the
6
+ * planner and tests see only these shapes.
7
+ *
8
+ * Resources (App Store Connect API):
9
+ * - App infos and their localizations: https://developer.apple.com/documentation/appstoreconnectapi/app-infos
10
+ * - App Store versions and their localizations: https://developer.apple.com/documentation/appstoreconnectapi/app-store-versions
11
+ * - Screenshot sets and screenshots: https://developer.apple.com/documentation/appstoreconnectapi/app-screenshot-sets
12
+ */
13
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * One session on the pinned `asc` 5.5.0 CLI, with the same binary pin,
3
+ * isolated environment and API key as the TestFlight submission, and the
4
+ * JSON helpers every asc-backed port reads its output with. Each command is
5
+ * one `asc ... --output json` subprocess; the JSON is App Store Connect's
6
+ * own resource document, as `asc` received it
7
+ * (`internal/asc/types/resources.go` at the 5.5.0 tag).
8
+ */
9
+ /** Runs one asc command with `--output json` appended and returns the parsed JSON. */
10
+ export type AscCommandRunner = (args: readonly string[], timeoutMs: number) => Promise<unknown>;
11
+ export interface AscCredential {
12
+ readonly ascBinary: string;
13
+ readonly apiKeyId: string;
14
+ readonly apiIssuerId: string;
15
+ readonly apiPrivateKeyBase64: string;
16
+ }
17
+ export declare const ascLookupTimeoutMs = 60000;
18
+ export declare const ascListTimeoutMs: number;
19
+ export declare const ascUploadTimeoutMs: number;
20
+ /** asc exit code for an App Store Connect 404 (`cmd/exit_codes.go`, 5.5.0). */
21
+ export declare const ascExitNotFound = 4;
22
+ /** Copy and verify the pinned binary into a private directory; `close` removes it. */
23
+ export declare function openAscSession(credential: AscCredential, signal?: AbortSignal): Promise<{
24
+ readonly run: AscCommandRunner;
25
+ close(): void;
26
+ }>;
27
+ export interface AscJsonResource {
28
+ readonly id: string;
29
+ readonly attributes: Record<string, unknown>;
30
+ readonly relationships?: unknown;
31
+ }
32
+ export declare function single(value: unknown, command: string): AscJsonResource;
33
+ export declare function list(value: unknown, command: string): AscJsonResource[];
34
+ export declare function toResource(value: unknown): AscJsonResource | undefined;
35
+ /**
36
+ * App Store Connect's `data: null`, or asc's rendering of it: asc 5.5.0
37
+ * decodes into a non-pointer resource and prints `{ "type": "", "id": "" }`.
38
+ */
39
+ export declare function isEmptyResource(value: unknown): boolean;
40
+ export declare function relationshipId(relationships: unknown, name: string): string | undefined;
41
+ export declare function requiredString(value: unknown, label: string): string;
42
+ export declare function optionalString<K extends string>(key: K, value: unknown): Partial<Record<K, string>>;
43
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,118 @@
1
+ import { chmodSync, copyFileSync, mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { runReleaseProcess } from './deploy-process.js';
5
+ import { isolatedAscEnvironment, verifyPinnedAscBinary } from './testflight-submission.js';
6
+ export const ascLookupTimeoutMs = 60_000;
7
+ export const ascListTimeoutMs = 3 * 60_000;
8
+ export const ascUploadTimeoutMs = 10 * 60_000;
9
+ const maxJsonBytes = 8 * 1024 * 1024;
10
+ /** asc exit code for an App Store Connect 404 (`cmd/exit_codes.go`, 5.5.0). */
11
+ export const ascExitNotFound = 4;
12
+ /** Copy and verify the pinned binary into a private directory; `close` removes it. */
13
+ export async function openAscSession(credential, signal) {
14
+ if (!path.isAbsolute(credential.ascBinary)) {
15
+ throw new Error('asc binary must be an absolute file path.');
16
+ }
17
+ const tempRoot = mkdtempSync(path.join(tmpdir(), 'mpgd-asc-iap-'));
18
+ try {
19
+ const binary = path.join(tempRoot, 'asc');
20
+ copyFileSync(credential.ascBinary, binary);
21
+ chmodSync(binary, 0o700);
22
+ await verifyPinnedAscBinary(binary);
23
+ const environment = isolatedAscEnvironment(credential, tempRoot);
24
+ const run = async (args, timeoutMs) => {
25
+ const result = await runReleaseProcess({
26
+ command: binary,
27
+ args: [...args, '--output', 'json'],
28
+ cwd: tempRoot,
29
+ environment,
30
+ timeoutMs,
31
+ maxOutputBytes: maxJsonBytes,
32
+ captureMachineStdout: true,
33
+ secretValues: [credential.apiKeyId, credential.apiIssuerId, credential.apiPrivateKeyBase64],
34
+ signal,
35
+ });
36
+ if (result.truncated || result.machineStdout === undefined) {
37
+ throw new Error('asc JSON output was truncated or unavailable.');
38
+ }
39
+ try {
40
+ return JSON.parse(result.machineStdout);
41
+ }
42
+ catch {
43
+ throw new Error('asc returned invalid JSON.');
44
+ }
45
+ };
46
+ return {
47
+ run,
48
+ close: () => rmSync(tempRoot, { recursive: true, force: true }),
49
+ };
50
+ }
51
+ catch (error) {
52
+ rmSync(tempRoot, { recursive: true, force: true });
53
+ throw error;
54
+ }
55
+ }
56
+ export function single(value, command) {
57
+ const resource = isRecord(value) ? toResource(value.data) : undefined;
58
+ if (resource === undefined) {
59
+ throw new Error(`asc ${command} returned no resource.`);
60
+ }
61
+ return resource;
62
+ }
63
+ export function list(value, command) {
64
+ const resources = isRecord(value) && Array.isArray(value.data)
65
+ ? value.data.map(toResource)
66
+ : undefined;
67
+ if (resources === undefined || resources.some((resource) => resource === undefined)) {
68
+ throw new Error(`asc ${command} returned no resource list.`);
69
+ }
70
+ return resources;
71
+ }
72
+ export function toResource(value) {
73
+ if (!isRecord(value) || typeof value.id !== 'string' || value.id === '') {
74
+ return undefined;
75
+ }
76
+ return {
77
+ id: value.id,
78
+ attributes: isRecord(value.attributes) ? value.attributes : {},
79
+ ...(value.relationships === undefined ? {} : { relationships: value.relationships }),
80
+ };
81
+ }
82
+ /**
83
+ * App Store Connect's `data: null`, or asc's rendering of it: asc 5.5.0
84
+ * decodes into a non-pointer resource and prints `{ "type": "", "id": "" }`.
85
+ */
86
+ export function isEmptyResource(value) {
87
+ if (!isRecord(value)) {
88
+ return false;
89
+ }
90
+ return value.data === null
91
+ || (isRecord(value.data) && (value.data.id === undefined || value.data.id === ''));
92
+ }
93
+ export function relationshipId(relationships, name) {
94
+ if (!isRecord(relationships)) {
95
+ return undefined;
96
+ }
97
+ const relationship = relationships[name];
98
+ if (!isRecord(relationship) || !isRecord(relationship.data)) {
99
+ return undefined;
100
+ }
101
+ return typeof relationship.data.id === 'string' ? relationship.data.id : undefined;
102
+ }
103
+ export function requiredString(value, label) {
104
+ if (typeof value !== 'string') {
105
+ throw new Error(`asc output has no ${label}.`);
106
+ }
107
+ return value;
108
+ }
109
+ export function optionalString(key, value) {
110
+ const result = {};
111
+ if (typeof value === 'string' && value !== '') {
112
+ result[key] = value;
113
+ }
114
+ return result;
115
+ }
116
+ export function isRecord(value) {
117
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
118
+ }
package/dist/index.d.ts CHANGED
@@ -13,6 +13,12 @@ export { applyStoreProducts, planStoreProducts, saveStoreProductPlan, type Store
13
13
  export { applyPlayProductPlan, formatPlayProductPlan, planPlayProducts, PlayProductApplyError, type PlannedPlayProduct, type PlayProductApplyResult, type PlayProductChange, type PlayProductPlan, type PlayProductPlanInput, } from './store-products-play.js';
14
14
  export { AppStoreProductApplyError, applyAppStoreProductPlan, formatAppStoreProductPlan, planAppStoreProducts, type AppStoreProductApplyResult, type AppStoreProductChange, type AppStoreProductPlan, type AppStoreProductPlanInput, type PlannedAppStoreProduct, } from './store-products-app-store.js';
15
15
  export { StoreProductPlanStaleError } from './store-products.js';
16
+ export { StorePlanStaleError } from './store-sync.js';
17
+ export { applyStorePresence, planStorePresence, saveStorePresencePlan, type StorePresenceApplyOptions, type StorePresencePlanFile, type StorePresencePlanOptions, type StorePresencePorts, type StorePresenceStore, } from './store-presence-command.js';
18
+ export { applyPlayPresence, formatPlayPresencePlan, planPlayPresence, PlayPresenceApplyError, type PlannedPlayImageSlot, type PlannedPlayLanguage, type PlayPresenceApplyResult, type PlayPresencePlan, type PlayPresencePlanInput, } from './store-presence-play.js';
19
+ export { applyAppStorePresence, AppStorePresenceApplyError, formatAppStorePresencePlan, planAppStorePresence, type AppStorePresenceApplyResult, type AppStorePresencePlan, type AppStorePresencePlanInput, type PlannedAppStoreLocale, type PlannedAscScreenshotSet, } from './store-presence-app-store.js';
20
+ export type { AscAppInfo, AscAppInfoLocalization, AscAppStoreVersion, AscScreenshot, AscScreenshotSet, AscStorePresencePort, AscVersionLocalization, AscVersionLocalizationFields, } from './app-store-presence-port.js';
21
+ export type { PlayAppDetails, PlayImage, PlayImageType, PlayListing, PlayStoreListingPort, } from './play-listing-port.js';
16
22
  export { PlayPromoteUncertainError, promotePlayRelease, type PlayPromoteInput, type PlayPromoteResult, } from './play-release-promote.js';
17
23
  export { PlayRolloutUncertainError, changePlayRollout, type PlayRolloutAction, type PlayRolloutInput, type PlayRolloutResult, } from './play-release-rollout.js';
18
24
  export { IosSubmissionUncertainError, submitVerifiedIosBuild, type IosTestFlightStatus, type IosTestFlightSubmissionInput, type IosTestFlightSubmissionResult, } from './testflight-submission.js';