apphx 1.0.0 → 2.0.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 (126) hide show
  1. package/README.md +52 -138
  2. package/benchmarks/peer-context-worker.mjs +186 -0
  3. package/benchmarks/peer-context.mjs +150 -0
  4. package/dist/apple-types.js.map +1 -1
  5. package/dist/cli.js +373 -121
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.js +42 -4
  8. package/dist/config.js.map +1 -1
  9. package/dist/dependencies.js +455 -16
  10. package/dist/dependencies.js.map +1 -1
  11. package/dist/export-options.js +44 -2
  12. package/dist/export-options.js.map +1 -1
  13. package/dist/flutter.js +13 -25
  14. package/dist/flutter.js.map +1 -1
  15. package/dist/go-dependencies.js +389 -0
  16. package/dist/go-dependencies.js.map +1 -0
  17. package/dist/inbox.js +7 -8
  18. package/dist/inbox.js.map +1 -1
  19. package/dist/integrations.js +29 -29
  20. package/dist/integrations.js.map +1 -1
  21. package/dist/isolation.js +190 -0
  22. package/dist/isolation.js.map +1 -0
  23. package/dist/macos.js +57 -0
  24. package/dist/macos.js.map +1 -0
  25. package/dist/mygo-build.js +55 -0
  26. package/dist/mygo-build.js.map +1 -0
  27. package/dist/policy.js +35 -0
  28. package/dist/policy.js.map +1 -0
  29. package/dist/preflight.js +1 -1
  30. package/dist/process.js +3 -3
  31. package/dist/project-profiles.js +163 -0
  32. package/dist/project-profiles.js.map +1 -0
  33. package/dist/proof.js +33 -0
  34. package/dist/proof.js.map +1 -0
  35. package/dist/remote.js +1 -1
  36. package/dist/runner.js +269 -287
  37. package/dist/runner.js.map +1 -1
  38. package/dist/scheduler.js +0 -3
  39. package/dist/scheduler.js.map +1 -1
  40. package/dist/schemas.js +156 -44
  41. package/dist/schemas.js.map +1 -1
  42. package/dist/store.js +166 -44
  43. package/dist/store.js.map +1 -1
  44. package/dist/vendor/obfuscator/dist/cli.js +72 -0
  45. package/dist/vendor/obfuscator/dist/cli.js.map +1 -0
  46. package/dist/vendor/obfuscator/dist/code.js +585 -0
  47. package/dist/vendor/obfuscator/dist/code.js.map +1 -0
  48. package/dist/vendor/obfuscator/dist/common.js +262 -0
  49. package/dist/vendor/obfuscator/dist/common.js.map +1 -0
  50. package/dist/vendor/obfuscator/dist/config.js +123 -0
  51. package/dist/vendor/obfuscator/dist/config.js.map +1 -0
  52. package/dist/vendor/obfuscator/dist/content.js +89 -0
  53. package/dist/vendor/obfuscator/dist/content.js.map +1 -0
  54. package/dist/vendor/obfuscator/dist/obfuscate.js +93 -0
  55. package/dist/vendor/obfuscator/dist/obfuscate.js.map +1 -0
  56. package/dist/vendor/obfuscator/dist/png.js +283 -0
  57. package/dist/vendor/obfuscator/dist/png.js.map +1 -0
  58. package/dist/vendor/obfuscator/dist/project.js +159 -0
  59. package/dist/vendor/obfuscator/dist/project.js.map +1 -0
  60. package/dist/vendor/obfuscator/dist/resources.js +335 -0
  61. package/dist/vendor/obfuscator/dist/resources.js.map +1 -0
  62. package/dist/vendor/obfuscator/package.json +22 -0
  63. package/dist/workspace.js +113 -0
  64. package/dist/workspace.js.map +1 -0
  65. package/evals/control.mjs +73 -0
  66. package/evals/expectations.mjs +37 -0
  67. package/evals/host.mjs +51 -0
  68. package/evals/oracle.mjs +80 -0
  69. package/evals/policy.mjs +268 -0
  70. package/examples/android-java.factory.json +18 -0
  71. package/examples/android-java.json +16 -0
  72. package/examples/existing-project.factory.json +18 -0
  73. package/examples/existing-project.json +17 -0
  74. package/examples/expo.eas.json +17 -0
  75. package/examples/expo.factory.json +18 -0
  76. package/examples/expo.json +16 -0
  77. package/examples/factory.json +0 -2
  78. package/examples/flutter.factory.json +13 -0
  79. package/examples/flutter.json +16 -0
  80. package/examples/luggage.json +5 -3
  81. package/examples/mygo-web.factory.json +18 -0
  82. package/examples/mygo-web.json +16 -0
  83. package/examples/mygo.factory.json +18 -0
  84. package/examples/mygo.json +16 -0
  85. package/examples/react-native.factory.json +18 -0
  86. package/examples/react-native.json +16 -0
  87. package/examples/remote.json +1 -1
  88. package/examples/runtime/android-java/app/build.gradle +7 -0
  89. package/examples/runtime/android-java/app/src/main/AndroidManifest.xml +8 -0
  90. package/examples/runtime/android-java/app/src/main/java/dev/apphx/runtime/MainActivity.java +44 -0
  91. package/examples/runtime/android-java/sdk-build.mjs +29 -0
  92. package/examples/runtime/android-java/settings.gradle +2 -0
  93. package/examples/runtime/android-ui.mjs +70 -0
  94. package/examples/runtime/mygo-native/go.mod +13 -0
  95. package/examples/runtime/mygo-native/go.sum +10 -0
  96. package/examples/runtime/mygo-native/main.go +173 -0
  97. package/examples/runtime/mygo-native/main_test.go +14 -0
  98. package/examples/runtime/mygo-native/mygo.json +1 -0
  99. package/examples/runtime/mygo-web/bun.lock +15 -0
  100. package/examples/runtime/mygo-web/go.mod +9 -0
  101. package/examples/runtime/mygo-web/go.sum +8 -0
  102. package/examples/runtime/mygo-web/index.html +1 -0
  103. package/examples/runtime/mygo-web/main.go +60 -0
  104. package/examples/runtime/mygo-web/mygo.config.ts +10 -0
  105. package/examples/runtime/mygo-web/package.json +1 -0
  106. package/examples/runtime/mygo-web/src/main.ts +23 -0
  107. package/examples/runtime/mygo-web/src/mygo.ts +14 -0
  108. package/examples/swift.factory.json +18 -0
  109. package/examples/swift.json +16 -0
  110. package/examples/uni-app.factory.json +18 -0
  111. package/examples/uni-app.json +16 -0
  112. package/package.json +12 -5
  113. package/scripts/bundle-obfuscator.mjs +19 -0
  114. package/scripts/runtime-smoke.mjs +209 -0
  115. package/skills/apphx/SKILL.md +180 -0
  116. package/skills/apphx/resources/google-play-research-result.json +6 -0
  117. package/skills/apphx/resources/local-research-result.json +6 -0
  118. package/skills/{apple-app-factory → apphx}/resources/requirement.json +5 -8
  119. package/skills/apphx/resources/research-result.json +12 -0
  120. package/skills/apphx/resources/review-result.json +7 -0
  121. package/dist/apple.js +0 -900
  122. package/dist/apple.js.map +0 -1
  123. package/skills/apple-app-factory/SKILL.md +0 -103
  124. package/skills/apple-app-factory/resources/research-result.json +0 -21
  125. package/skills/apple-app-factory/resources/review-result.json +0 -7
  126. /package/skills/{apple-app-factory → apphx}/LICENSE +0 -0
package/dist/apple.js DELETED
@@ -1,900 +0,0 @@
1
- import { AppleReceiptSchema } from "./apple-types.js";
2
- import { run } from "./process.js";
3
- import { z } from "zod";
4
- import { createHash, createPrivateKey, sign } from "node:crypto";
5
- import { chmod, copyFile, mkdtemp, readFile, readdir, rm, stat } from "node:fs/promises";
6
- import { tmpdir } from "node:os";
7
- import { basename, join } from "node:path";
8
- // src/apple.ts
9
- const UploadOperationSchema = z.object({
10
- url: z.string(),
11
- method: z.string(),
12
- offset: z.number().finite(),
13
- length: z.number().finite(),
14
- requestHeaders: z.array(z.object({
15
- name: z.string(),
16
- value: z.string()
17
- })).nullish()
18
- });
19
- const AttributesSchema = z.object({
20
- bundleId: z.string().optional(),
21
- appVersionState: z.string().nullish(),
22
- appStoreState: z.string().nullish(),
23
- state: z.string().nullish(),
24
- processingState: z.string().optional(),
25
- usesNonExemptEncryption: z.boolean().nullish(),
26
- copyright: z.string().optional(),
27
- releaseType: z.string().optional(),
28
- locale: z.string().optional(),
29
- description: z.string().optional(),
30
- keywords: z.string().optional(),
31
- supportUrl: z.string().optional(),
32
- whatsNew: z.string().optional(),
33
- privacyPolicyUrl: z.string().optional(),
34
- contactFirstName: z.string().optional(),
35
- contactLastName: z.string().optional(),
36
- contactEmail: z.string().optional(),
37
- contactPhone: z.string().optional(),
38
- notes: z.string().optional(),
39
- demoAccountRequired: z.boolean().optional(),
40
- screenshotDisplayType: z.string().optional(),
41
- fileName: z.string().optional(),
42
- sourceFileChecksum: z.string().nullish(),
43
- assetDeliveryState: z.object({ state: z.string().optional() }).nullish(),
44
- uploadOperations: z.array(UploadOperationSchema).nullish()
45
- });
46
- const ResourceSchema = z.object({
47
- id: z.string().min(1),
48
- type: z.string().optional(),
49
- attributes: AttributesSchema.default({}),
50
- relationships: z.object({ appStoreVersion: z.object({ data: z.object({
51
- id: z.string(),
52
- type: z.string().optional()
53
- }).nullish() }).optional() }).optional()
54
- });
55
- const ResponseSchema = z.object({
56
- data: z.union([ResourceSchema, z.array(ResourceSchema)]).nullish(),
57
- links: z.object({ next: z.string().nullish() }).optional()
58
- });
59
- const ErrorResponseSchema = z.object({ errors: z.array(z.object({
60
- code: z.string().optional(),
61
- detail: z.string().nullish(),
62
- title: z.string().optional()
63
- })).optional() });
64
- const ListingSchema = z.object({ results: z.array(z.object({
65
- bundleId: z.string().optional(),
66
- version: z.string().optional()
67
- })) });
68
- const publishingFields = [
69
- "appId",
70
- "bundleId",
71
- "version",
72
- "buildNumber",
73
- "locale",
74
- "description",
75
- "keywords",
76
- "supportUrl",
77
- "privacyPolicyUrl",
78
- "reviewNotes",
79
- "copyright"
80
- ];
81
- const contactFields = [
82
- "firstName",
83
- "lastName",
84
- "email",
85
- "phone"
86
- ];
87
- function message(error) {
88
- return error instanceof Error ? error.message : String(error);
89
- }
90
- function textAttribute(value) {
91
- if (value === void 0 || value === null) throw failure("App Store Connect returned a resource missing a required text field.", "APPLE_API_ERROR");
92
- return value;
93
- }
94
- function resource(value) {
95
- if (!value || Array.isArray(value)) throw failure("App Store Connect returned an invalid resource.", "APPLE_API_ERROR");
96
- return value;
97
- }
98
- function optionalResource(response) {
99
- return response?.data == null ? void 0 : resource(response.data);
100
- }
101
- const API = "https://api.appstoreconnect.apple.com";
102
- const editable = /* @__PURE__ */ new Set([
103
- "PREPARE_FOR_SUBMISSION",
104
- "DEVELOPER_REJECTED",
105
- "METADATA_REJECTED",
106
- "REJECTED"
107
- ]);
108
- const approved = /* @__PURE__ */ new Set([
109
- "ACCEPTED",
110
- "PENDING_DEVELOPER_RELEASE",
111
- "PENDING_APPLE_RELEASE",
112
- "READY_FOR_SALE",
113
- "READY_FOR_DISTRIBUTION",
114
- "PROCESSING_FOR_DISTRIBUTION",
115
- "PROCESSING_FOR_APP_STORE"
116
- ]);
117
- const rejected = /* @__PURE__ */ new Set([
118
- "REJECTED",
119
- "METADATA_REJECTED",
120
- "INVALID_BINARY",
121
- "DEVELOPER_REJECTED"
122
- ]);
123
- const versionState = (resource) => resource.attributes.appVersionState ?? resource.attributes.appStoreState;
124
- const ref = (type, id) => ({ data: {
125
- type,
126
- id
127
- } });
128
- const matches = (resource, attributes) => Object.keys(attributes).every((key) => resource.attributes[key] === attributes[key]);
129
- const query = (path, values) => `${path}?${new URLSearchParams(Object.entries(values).map(([key, value]) => [key, String(value)]))}`;
130
- function failure(message, code = "APPLE_SETUP_REQUIRED") {
131
- return Object.assign(new Error(message), { code });
132
- }
133
- function requestFailure(message, code, httpStatus = null, transient = httpStatus === 408 || httpStatus === 429 || httpStatus !== null && httpStatus >= 500) {
134
- return Object.assign(failure(message, code), {
135
- httpStatus,
136
- transient,
137
- outcome: httpStatus !== null && httpStatus >= 400 && httpStatus < 500 && httpStatus !== 408 ? "rejected" : "unknown"
138
- });
139
- }
140
- function credential(env, key) {
141
- const value = env[key];
142
- if (!value) throw failure(`Set ${key} in the publisher environment. App Store Connect requires a team API key with App Manager or Admin access.`);
143
- return value;
144
- }
145
- async function client(env, signal) {
146
- const keyId = credential(env, "ASC_KEY_ID");
147
- const issuerId = credential(env, "ASC_ISSUER_ID");
148
- const keyPath = credential(env, "ASC_PRIVATE_KEY_PATH");
149
- if (!/^[A-Za-z0-9]+$/.test(keyId)) throw failure("ASC_KEY_ID must be an App Store Connect key identifier.");
150
- let key;
151
- try {
152
- key = createPrivateKey(await readFile(keyPath));
153
- } catch (error) {
154
- throw failure(`Cannot load ASC_PRIVATE_KEY_PATH as a private signing key: ${message(error)}`);
155
- }
156
- if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") throw failure("ASC_PRIVATE_KEY_PATH must contain the P-256 private key downloaded from App Store Connect for ES256 authentication.");
157
- let token;
158
- let expires = 0;
159
- async function request(path, { method = "GET", body, optional = false } = {}) {
160
- const url = new URL(path, API);
161
- if (url.origin !== API || !/^\/v[12]\//.test(url.pathname)) throw failure("Apple returned an unexpected API URL; refusing to send publisher credentials.", "APPLE_API_ERROR");
162
- const now = Math.floor(Date.now() / 1e3);
163
- if (expires < now + 30) {
164
- expires = now + 600;
165
- const encode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
166
- const input = `${encode({
167
- alg: "ES256",
168
- kid: env.ASC_KEY_ID,
169
- typ: "JWT"
170
- })}.${encode({
171
- iss: env.ASC_ISSUER_ID,
172
- iat: now,
173
- exp: expires,
174
- aud: "appstoreconnect-v1"
175
- })}`;
176
- token = `${input}.${sign("sha256", Buffer.from(input), {
177
- key,
178
- dsaEncoding: "ieee-p1363"
179
- }).toString("base64url")}`;
180
- }
181
- let response;
182
- try {
183
- response = await fetch(url, {
184
- method,
185
- redirect: "error",
186
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4),
187
- headers: {
188
- Authorization: `Bearer ${token}`,
189
- Accept: "application/json",
190
- ...body ? { "Content-Type": "application/json" } : {}
191
- },
192
- ...body ? { body: JSON.stringify(body) } : {}
193
- });
194
- } catch (error) {
195
- throw requestFailure(`App Store Connect ${method} ${url.pathname} did not complete: ${message(error)}. Resume to reconcile saved writes; do not repeat an upload manually.`, "APPLE_API_ERROR", null, true);
196
- }
197
- if (optional && response.status === 404) return null;
198
- const httpCode = response.status === 429 ? "APPLE_RATE_LIMIT" : response.status >= 500 || response.status === 408 || response.ok ? "APPLE_API_ERROR" : "APPLE_SETUP_REQUIRED";
199
- let text;
200
- try {
201
- text = await response.text();
202
- } catch (error) {
203
- throw requestFailure(`App Store Connect ${method} ${url.pathname} response body was lost (HTTP ${response.status}): ${message(error)}. Resume to reconcile saved writes.`, httpCode, response.status, response.ok || response.status === 408 || response.status === 429 || response.status >= 500);
204
- }
205
- let data;
206
- try {
207
- data = text ? JSON.parse(text) : {};
208
- } catch {
209
- throw requestFailure(`App Store Connect returned non-JSON data for ${url.pathname} (HTTP ${response.status}).`, httpCode, response.status);
210
- }
211
- if (!response.ok) {
212
- const errors = ErrorResponseSchema.safeParse(data);
213
- const detail = (errors.success ? errors.data.errors?.map((error) => `${error.code}: ${error.detail ?? error.title}`).join("; ") : "") || response.statusText;
214
- throw requestFailure(`App Store Connect ${method} ${url.pathname} failed (HTTP ${response.status}): ${detail}${response.status === 429 ? ". Request limit reached; resume later. No write was retried." : ""}`, httpCode, response.status);
215
- }
216
- const parsed = ResponseSchema.safeParse(data);
217
- if (!parsed.success) throw requestFailure(`App Store Connect returned invalid resource data for ${url.pathname} (HTTP ${response.status}): ${parsed.error.message}`, httpCode, response.status);
218
- return parsed.data;
219
- }
220
- async function list(path) {
221
- const result = [];
222
- for (let page = 0; path; page++) {
223
- if (page >= 100) throw failure("App Store Connect pagination exceeded 100 pages; narrow the app/version selection.", "APPLE_API_ERROR");
224
- const response = await request(path);
225
- if (!Array.isArray(response.data)) throw failure("App Store Connect returned an invalid resource list.", "APPLE_API_ERROR");
226
- result.push(...response.data);
227
- path = response.links?.next;
228
- }
229
- return result;
230
- }
231
- return {
232
- request,
233
- list,
234
- credentials: {
235
- keyId,
236
- issuerId,
237
- keyPath
238
- }
239
- };
240
- }
241
- async function appleDoctor({ env = process.env } = {}) {
242
- const checks = [];
243
- try {
244
- await (await client(env)).request("/v1/apps?limit=1");
245
- checks.push({
246
- name: "App Store Connect API",
247
- available: true,
248
- detail: "Team key authenticated with read access. Submission permissions and app setup are checked when shipping."
249
- });
250
- } catch (error) {
251
- checks.push({
252
- name: "App Store Connect API",
253
- available: false,
254
- detail: message(error)
255
- });
256
- }
257
- try {
258
- const result = await run([
259
- "xcrun",
260
- "altool",
261
- "--help"
262
- ], {
263
- env,
264
- timeoutMs: 3e4
265
- });
266
- const supported = result.code === 0 && `${result.stdout}\n${result.stderr}`.includes("--upload-package");
267
- checks.push({
268
- name: "Apple binary uploader",
269
- available: supported,
270
- detail: supported ? "xcrun altool supports package upload. No binary was uploaded." : "Install/select full Xcode with a current altool supporting --upload-package; Command Line Tools alone are insufficient."
271
- });
272
- } catch (error) {
273
- checks.push({
274
- name: "Apple binary uploader",
275
- available: false,
276
- detail: `Cannot invoke xcrun altool: ${message(error)}. Install/select full Xcode.`
277
- });
278
- }
279
- return checks;
280
- }
281
- function validateAppleSpec(spec) {
282
- const apple = spec?.apple;
283
- for (const field of publishingFields) {
284
- const value = apple?.[field];
285
- if (typeof value !== "string" || !value.trim()) throw failure(`spec.apple.${field} is required before shipping. Provide real publisher metadata, not generated placeholders.`);
286
- }
287
- for (const field of contactFields) {
288
- const value = apple?.contact?.[field];
289
- if (typeof value !== "string" || !value.trim()) throw failure(`spec.apple.contact.${field} is required for App Review.`);
290
- }
291
- for (const field of ["supportUrl", "privacyPolicyUrl"]) {
292
- let url;
293
- try {
294
- url = new URL(apple?.[field] ?? "");
295
- } catch {
296
- throw failure(`spec.apple.${field} must be a public HTTPS URL.`);
297
- }
298
- if (url.protocol !== "https:" || url.username || url.password) throw failure(`spec.apple.${field} must be a public HTTPS URL without credentials.`);
299
- }
300
- if (apple?.manualSetupConfirmed !== true) throw failure("Complete App Store Connect agreements, app privacy disclosures, age rating, content rights, export compliance, pricing/availability and applicable trader/legal declarations. Then set spec.apple.manualSetupConfirmed=true as the publisher’s attestation. Automation cannot answer these declarations for you.");
301
- return apple;
302
- }
303
- async function findVersion(api, appId, version) {
304
- const versions = await api.list(query(`/v1/apps/${encodeURIComponent(appId)}/appStoreVersions`, {
305
- "filter[platform]": "IOS",
306
- "filter[versionString]": version,
307
- limit: 200
308
- }));
309
- if (versions.length > 1) throw failure("Multiple matching iOS App Store versions were returned; resolve the app record in App Store Connect.", "APPLE_API_ERROR");
310
- return versions[0];
311
- }
312
- async function requireSelectedBuild(api, versionId, buildId) {
313
- if (!buildId) throw failure("The saved Apple receipt has no build ID. Restore the receipt for the verified upload before resuming.", "APPLE_STATE_MISMATCH");
314
- if (optionalResource(await api.request(`/v1/appStoreVersions/${encodeURIComponent(versionId)}/build`, { optional: true }))?.id !== buildId) throw failure("The selected App Store build differs from this job’s saved upload. Restore the verified build in App Store Connect before resuming.", "APPLE_STATE_MISMATCH");
315
- }
316
- function requireDeliveryProvenance(receipt) {
317
- if (receipt.upload && receipt.upload.status !== "delivered") throw failure("The saved upload has no successful delivery receipt. App Store Connect cannot prove that a matching build came from this IPA. Check Apple delivery history; do not select a build, submit, or upload again from this job.", "APPLE_WRITE_UNCERTAIN");
318
- }
319
- async function shipApple({ spec, ipaPath, state = {}, save, run: run$1 = run, env = process.env, signal, allowRelease = false }) {
320
- const apple = validateAppleSpec(spec);
321
- const releaseType = allowRelease === true ? "AFTER_APPROVAL" : "MANUAL";
322
- if (allowRelease && !/^[A-Za-z]{2}$/.test(apple.storefrontCountry ?? "")) throw failure("Automatic release requires apple.storefrontCountry as a two-letter country code so public listing availability can be checked.");
323
- if (typeof save !== "function") throw failure("shipApple requires a durable save callback before it can make external writes.", "APPLE_STATE_REQUIRED");
324
- let receipt = structuredClone(state);
325
- const identity = {
326
- appId: apple.appId,
327
- bundleId: apple.bundleId,
328
- version: apple.version,
329
- buildNumber: apple.buildNumber
330
- };
331
- if (receipt.identity && JSON.stringify(receipt.identity) !== JSON.stringify(identity)) throw failure("The saved Apple receipt belongs to another app/version/build. Resume the original job or create a new job.", "APPLE_STATE_MISMATCH");
332
- requireDeliveryProvenance(receipt);
333
- receipt.identity = identity;
334
- async function persist(patch = {}) {
335
- receipt = {
336
- ...receipt,
337
- ...patch
338
- };
339
- await save(structuredClone(receipt));
340
- }
341
- async function reconcile(key, resource) {
342
- if (receipt.pending?.key === key) await persist({
343
- pending: null,
344
- effects: {
345
- ...receipt.effects,
346
- [key]: { id: resource?.id }
347
- },
348
- lastEffect: {
349
- key,
350
- id: resource?.id,
351
- reconciled: true
352
- }
353
- });
354
- return resource;
355
- }
356
- async function write(key, path, method, body) {
357
- if (receipt.pending) throw failure(`The saved write “${receipt.pending.key}” has an uncertain outcome. Resume after checking App Store Connect; this factory will not duplicate it.`, "APPLE_WRITE_UNCERTAIN");
358
- await persist({ pending: {
359
- key,
360
- path,
361
- method,
362
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
363
- } });
364
- let result;
365
- try {
366
- result = optionalResource(await api.request(path, {
367
- method,
368
- body
369
- }));
370
- if (method === "POST" && !result) throw requestFailure("App Store Connect returned no resource for a creation request. Resume to reconcile saved writes.", "APPLE_API_ERROR");
371
- } catch (error) {
372
- if (error instanceof Error && "outcome" in error && error.outcome === "rejected") await persist({ pending: null });
373
- throw error;
374
- }
375
- await persist({
376
- pending: null,
377
- effects: {
378
- ...receipt.effects,
379
- [key]: { id: result?.id }
380
- },
381
- lastEffect: {
382
- key,
383
- id: result?.id,
384
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
385
- }
386
- });
387
- return result;
388
- }
389
- async function ensureAttributes(current, attributes, key) {
390
- if (matches(current, attributes)) return reconcile(key, current);
391
- const type = textAttribute(current.type);
392
- return resource(await write(key, `/v1/${type}/${encodeURIComponent(current.id)}`, "PATCH", { data: {
393
- type,
394
- id: current.id,
395
- attributes
396
- } }));
397
- }
398
- const api = await client(env, signal);
399
- if (resource((await api.request(`/v1/apps/${encodeURIComponent(apple.appId)}`)).data).attributes.bundleId !== apple.bundleId) throw failure("spec.apple.appId and bundleId identify different apps. Correct the publisher specification before uploading.");
400
- await persist();
401
- let version = await findVersion(api, apple.appId, apple.version);
402
- if (version && !editable.has(versionState(version))) {
403
- const submissionId = receipt.submissionId ?? receipt.effects?.["create-review-submission"]?.id;
404
- if (!submissionId || receipt.versionId && receipt.versionId !== version.id) throw failure(`Version ${apple.version} is ${versionState(version)} and has no matching submission receipt. Inspect it in App Store Connect; this factory will not overwrite or resubmit it.`, "APPLE_WRITE_UNCERTAIN");
405
- await requireSelectedBuild(api, version.id, receipt.buildId);
406
- const submission = resource((await api.request(`/v1/reviewSubmissions/${encodeURIComponent(submissionId)}`)).data);
407
- const items = await api.list(`/v1/reviewSubmissions/${submission.id}/items?include=appStoreVersion&limit=200`);
408
- const versionId = version.id;
409
- const item = items.find((item) => item.relationships?.appStoreVersion?.data?.id === versionId);
410
- if (!item || items.length !== 1) throw failure("The saved submission does not contain only this app version. Inspect it in App Store Connect before submitting.", "APPLE_WRITE_UNCERTAIN");
411
- await reconcile("add-review-item", item);
412
- if (submission.attributes.state === "READY_FOR_REVIEW") {
413
- if (apple.screenshotsDir) {
414
- const localization = (await api.list(`/v1/appStoreVersions/${version.id}/appStoreVersionLocalizations?limit=200`)).find((item) => item.attributes.locale === apple.locale);
415
- if (!localization) throw failure(`No ${apple.locale} App Store version localization exists. Restore the screenshot localization in App Store Connect before submitting.`, "APPLE_STATE_MISMATCH");
416
- await verifyScreenshots({
417
- api,
418
- apple,
419
- localization,
420
- localInventory: await readLocalScreenshots(apple.screenshotsDir)
421
- });
422
- }
423
- await write("submit-review", `/v1/reviewSubmissions/${submission.id}`, "PATCH", { data: {
424
- type: "reviewSubmissions",
425
- id: submission.id,
426
- attributes: { submitted: true }
427
- } });
428
- } else if ([
429
- "WAITING_FOR_REVIEW",
430
- "IN_REVIEW",
431
- "COMPLETING",
432
- "COMPLETE"
433
- ].includes(submission.attributes.state ?? "")) await reconcile("submit-review", submission);
434
- else throw failure(`Review submission is ${submission.attributes.state}. Resolve review issues in App Store Connect; no resubmission was attempted.`);
435
- await persist({ submissionId: submission.id });
436
- await persist({
437
- versionId: version.id,
438
- phase: "submitted"
439
- });
440
- return receipt;
441
- }
442
- const ipa = await stat(ipaPath);
443
- if (!ipa.isFile() || ipa.size === 0) throw failure("The IPA archive is missing or empty. Package the verified app before shipping.");
444
- const builds = await api.list(query("/v1/builds", {
445
- "filter[app]": apple.appId,
446
- "filter[version]": apple.buildNumber,
447
- "filter[preReleaseVersion.version]": apple.version,
448
- "filter[preReleaseVersion.platform]": "IOS",
449
- limit: 200
450
- }));
451
- if (builds.length > 1) throw failure("Multiple builds match this app/version/build number. Inspect App Store Connect before resuming.", "APPLE_STATE_MISMATCH");
452
- let build = builds[0];
453
- if (receipt.buildId && build?.id !== receipt.buildId) throw failure("The remote build differs from this job’s saved build binding. Restore the verified build before resuming; no upload was attempted.", "APPLE_STATE_MISMATCH");
454
- if (build && !receipt.upload && !receipt.buildId) throw failure(`Build ${apple.version} (${apple.buildNumber}) already exists without this job’s upload receipt. Use a new build number; do not reuse an unverified remote binary.`, "APPLE_WRITE_UNCERTAIN");
455
- if (!build && !receipt.upload && !receipt.buildId) {
456
- const help = await run$1([
457
- "xcrun",
458
- "altool",
459
- "--help"
460
- ], {
461
- env,
462
- signal,
463
- timeoutMs: 3e4
464
- });
465
- if (help.code !== 0 || !`${help.stdout}\n${help.stderr}`.includes("--upload-package")) throw failure("Select full Xcode with altool supporting --upload-package before shipping. No upload was attempted.");
466
- const keyDirectory = await mkdtemp(join(tmpdir(), "factory-asc-"));
467
- try {
468
- const keyFile = join(keyDirectory, `AuthKey_${env.ASC_KEY_ID}.p8`);
469
- await copyFile(api.credentials.keyPath, keyFile);
470
- await chmod(keyFile, 384);
471
- const upload = {
472
- status: "intent",
473
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
474
- };
475
- await persist({ upload });
476
- const result = await run$1([
477
- "xcrun",
478
- "altool",
479
- "--upload-package",
480
- ipaPath,
481
- "--platform",
482
- "ios",
483
- "--apple-id",
484
- apple.appId,
485
- "--bundle-id",
486
- apple.bundleId,
487
- "--bundle-version",
488
- apple.buildNumber,
489
- "--bundle-short-version-string",
490
- apple.version,
491
- "--api-key",
492
- api.credentials.keyId,
493
- "--api-issuer",
494
- api.credentials.issuerId,
495
- "--wait",
496
- "--output-format",
497
- "json"
498
- ], {
499
- env: {
500
- ...env,
501
- API_PRIVATE_KEYS_DIR: keyDirectory
502
- },
503
- signal,
504
- timeoutMs: 12e5
505
- });
506
- if (result.code !== 0) throw failure(`Apple’s uploader exited with code ${result.code}. Inspect its delivery history and App Store Connect for ${apple.version} (${apple.buildNumber}); the saved upload intent prevents a duplicate upload. ${result.stderr || result.stdout}`, "APPLE_WRITE_UNCERTAIN");
507
- await persist({
508
- upload: {
509
- ...upload,
510
- status: "delivered"
511
- },
512
- phase: "processing"
513
- });
514
- } finally {
515
- await rm(keyDirectory, {
516
- recursive: true,
517
- force: true
518
- });
519
- }
520
- }
521
- if (!build) {
522
- const deliveredBuilds = await api.list(query("/v1/builds", {
523
- "filter[app]": apple.appId,
524
- "filter[version]": apple.buildNumber,
525
- "filter[preReleaseVersion.version]": apple.version,
526
- "filter[preReleaseVersion.platform]": "IOS",
527
- limit: 200
528
- }));
529
- if (deliveredBuilds.length > 1) throw failure("Multiple builds match the successful delivery. Inspect App Store Connect before resuming.", "APPLE_STATE_MISMATCH");
530
- build = deliveredBuilds[0];
531
- }
532
- if (!build) throw failure(`Build ${apple.version} (${apple.buildNumber}) is not visible in App Store Connect. Apple is processing the delivery; resume this job later.`, "APPLE_PROCESSING");
533
- await persist({ buildId: build.id });
534
- if (["FAILED", "INVALID"].includes(build.attributes.processingState ?? "")) throw failure(`Apple marked build ${apple.buildNumber} ${build.attributes.processingState}. Inspect processing errors in App Store Connect and package a corrected build with a new build number.`);
535
- if (build.attributes.processingState !== "VALID") throw failure(`Apple is processing build ${apple.buildNumber}; resume this job later. No second upload will be made.`, "APPLE_PROCESSING");
536
- if (build.attributes.usesNonExemptEncryption == null) throw failure("Answer export compliance for this build in App Store Connect, or set the correct ITSAppUsesNonExemptEncryption value in the app and upload a new build. The factory cannot infer your encryption declaration.");
537
- if (!version) version = resource(await write("create-version", "/v1/appStoreVersions", "POST", { data: {
538
- type: "appStoreVersions",
539
- attributes: {
540
- platform: "IOS",
541
- versionString: apple.version,
542
- copyright: apple.copyright,
543
- releaseType
544
- },
545
- relationships: { app: ref("apps", apple.appId) }
546
- } }));
547
- else await reconcile("create-version", version);
548
- await persist({ versionId: version.id });
549
- version = await ensureAttributes(version, {
550
- copyright: apple.copyright,
551
- releaseType
552
- }, "version-metadata");
553
- let localization = (await api.list(`/v1/appStoreVersions/${version.id}/appStoreVersionLocalizations?limit=200`)).find((item) => item.attributes.locale === apple.locale);
554
- const metadata = {
555
- description: apple.description,
556
- keywords: apple.keywords,
557
- supportUrl: apple.supportUrl,
558
- ...apple.whatsNew ? { whatsNew: apple.whatsNew } : {}
559
- };
560
- if (!localization) localization = resource(await write("create-localization", "/v1/appStoreVersionLocalizations", "POST", { data: {
561
- type: "appStoreVersionLocalizations",
562
- attributes: {
563
- locale: apple.locale,
564
- ...metadata
565
- },
566
- relationships: { appStoreVersion: ref("appStoreVersions", version.id) }
567
- } }));
568
- else await reconcile("create-localization", localization);
569
- await ensureAttributes(localization, metadata, "localization-metadata");
570
- const info = (await api.list(`/v1/apps/${encodeURIComponent(apple.appId)}/appInfos?limit=200`)).find((item) => editable.has(item.attributes.state ?? item.attributes.appStoreState));
571
- if (!info) throw failure("No editable app information record exists. Check app status and age rating in App Store Connect.");
572
- const infoLocale = (await api.list(`/v1/appInfos/${info.id}/appInfoLocalizations?limit=200`)).find((item) => item.attributes.locale === apple.locale);
573
- if (!infoLocale) throw failure(`Create the ${apple.locale} app information localization in App Store Connect, including the app name and privacy policy.`);
574
- await ensureAttributes(infoLocale, { privacyPolicyUrl: apple.privacyPolicyUrl }, "privacy-url");
575
- let review = optionalResource(await api.request(`/v1/appStoreVersions/${version.id}/appStoreReviewDetail`, { optional: true }));
576
- const contact = apple.contact;
577
- const reviewAttributes = {
578
- contactFirstName: contact.firstName,
579
- contactLastName: contact.lastName,
580
- contactEmail: contact.email,
581
- contactPhone: contact.phone,
582
- notes: apple.reviewNotes,
583
- demoAccountRequired: false
584
- };
585
- if (!review) review = resource(await write("create-review-details", "/v1/appStoreReviewDetails", "POST", { data: {
586
- type: "appStoreReviewDetails",
587
- attributes: reviewAttributes,
588
- relationships: { appStoreVersion: ref("appStoreVersions", version.id) }
589
- } }));
590
- else await reconcile("create-review-details", review);
591
- if (review.attributes?.demoAccountRequired) throw failure("This app requires a demo account. Configure review access in App Store Connect; the offline utility pipeline does not handle account credentials.");
592
- await ensureAttributes(review, reviewAttributes, "review-details");
593
- await screenshots({
594
- api,
595
- apple,
596
- localization,
597
- receipt: () => receipt,
598
- persist,
599
- write,
600
- reconcile,
601
- signal
602
- });
603
- const selected = optionalResource(await api.request(`/v1/appStoreVersions/${version.id}/build`, { optional: true }));
604
- if (selected?.id === build.id) await reconcile("select-build", selected);
605
- else await write("select-build", `/v1/appStoreVersions/${version.id}/relationships/build`, "PATCH", { data: {
606
- type: "builds",
607
- id: build.id
608
- } });
609
- let submission;
610
- const submissions = await api.list(query("/v1/reviewSubmissions", {
611
- "filter[app]": apple.appId,
612
- limit: 200
613
- }));
614
- for (const candidate of submissions) {
615
- const item = (await api.list(`/v1/reviewSubmissions/${candidate.id}/items?include=appStoreVersion&limit=200`)).find((item) => item.relationships?.appStoreVersion?.data?.id === version.id);
616
- if (candidate.id === receipt.submissionId || candidate.id === receipt.effects?.["create-review-submission"]?.id || item) {
617
- if (submission && submission.id !== candidate.id) throw failure("Multiple review submissions reference this version. Resolve them in App Store Connect before resuming.", "APPLE_WRITE_UNCERTAIN");
618
- submission = candidate;
619
- if (item) await reconcile("add-review-item", item);
620
- }
621
- }
622
- if (!submission && receipt.pending?.key === "create-review-submission") throw failure("Review submission creation has an uncertain outcome. Check App Store Connect and resume after the submission becomes visible; no duplicate will be created.", "APPLE_WRITE_UNCERTAIN");
623
- if (!submission) {
624
- if (submissions.some((item) => [
625
- "READY_FOR_REVIEW",
626
- "WAITING_FOR_REVIEW",
627
- "IN_REVIEW",
628
- "UNRESOLVED_ISSUES"
629
- ].includes(item.attributes.state ?? ""))) throw failure("Another active review submission exists for this app. Resolve it in App Store Connect before starting this version.");
630
- submission = resource(await write("create-review-submission", "/v1/reviewSubmissions", "POST", { data: {
631
- type: "reviewSubmissions",
632
- attributes: { platform: "IOS" },
633
- relationships: { app: ref("apps", apple.appId) }
634
- } }));
635
- }
636
- await reconcile("create-review-submission", submission);
637
- await persist({ submissionId: submission.id });
638
- if (submission.attributes.state === "READY_FOR_REVIEW") {
639
- const items = await api.list(`/v1/reviewSubmissions/${submission.id}/items?include=appStoreVersion&limit=200`);
640
- const item = items.find((item) => item.relationships?.appStoreVersion?.data?.id === version.id);
641
- if (items.some((item) => item.relationships?.appStoreVersion?.data?.id !== version.id)) throw failure("The review submission contains other items. Inspect and submit it in App Store Connect; the factory will not submit unrelated content.");
642
- if (item) await reconcile("add-review-item", item);
643
- else await write("add-review-item", "/v1/reviewSubmissionItems", "POST", { data: {
644
- type: "reviewSubmissionItems",
645
- relationships: {
646
- reviewSubmission: ref("reviewSubmissions", submission.id),
647
- appStoreVersion: ref("appStoreVersions", version.id)
648
- }
649
- } });
650
- await write("submit-review", `/v1/reviewSubmissions/${submission.id}`, "PATCH", { data: {
651
- type: "reviewSubmissions",
652
- id: submission.id,
653
- attributes: { submitted: true }
654
- } });
655
- } else if ([
656
- "WAITING_FOR_REVIEW",
657
- "IN_REVIEW",
658
- "COMPLETING",
659
- "COMPLETE"
660
- ].includes(submission.attributes.state ?? "")) await reconcile("submit-review", submission);
661
- else throw failure(`Review submission is ${submission.attributes.state}. Resolve review issues in App Store Connect; no resubmission was attempted.`);
662
- await persist({ phase: "submitted" });
663
- return receipt;
664
- }
665
- async function readLocalScreenshots(screenshotsDir) {
666
- const directories = await readdir(screenshotsDir, { withFileTypes: true });
667
- if (!directories.length || directories.some((entry) => !entry.isDirectory() || !/^(APP|IMESSAGE_APP)_[A-Z0-9_]+$/.test(entry.name))) throw failure("screenshotsDir must contain display-type directories such as APP_IPHONE_67, each containing real app screenshots.");
668
- const localInventory = /* @__PURE__ */ new Map();
669
- for (const directory of directories) {
670
- const files = await readdir(join(screenshotsDir, directory.name), { withFileTypes: true });
671
- if (!files.length || files.length > 10 || files.some((file) => !file.isFile() || !/\.(png|jpe?g)$/i.test(file.name))) throw failure(`Screenshot display set ${directory.name} must contain 1–10 PNG/JPEG files and no other entries.`);
672
- const inventory = /* @__PURE__ */ new Map();
673
- for (const file of files.sort((a, b) => a.name.localeCompare(b.name))) {
674
- const bytes = await readFile(join(screenshotsDir, directory.name, file.name));
675
- inventory.set(file.name, {
676
- bytes,
677
- checksum: createHash("md5").update(bytes).digest("hex")
678
- });
679
- }
680
- localInventory.set(directory.name, inventory);
681
- }
682
- return localInventory;
683
- }
684
- async function screenshots({ api, apple, localization, receipt, persist, write, reconcile, signal }) {
685
- const sets = await api.list(`/v1/appStoreVersionLocalizations/${localization.id}/appScreenshotSets?limit=200`);
686
- let localInventory;
687
- if (apple.screenshotsDir) {
688
- localInventory = await readLocalScreenshots(apple.screenshotsDir);
689
- const displayTypes = /* @__PURE__ */ new Set();
690
- for (const set of sets) {
691
- const displayType = textAttribute(set.attributes.screenshotDisplayType);
692
- const inventory = localInventory.get(displayType);
693
- if (!inventory || displayTypes.has(displayType)) throw failure(`Remote screenshot display set ${displayType} is extra or duplicated. Match App Store Connect to screenshotsDir before resuming; the factory will not delete publisher assets.`, "APPLE_STATE_MISMATCH");
694
- displayTypes.add(displayType);
695
- const names = /* @__PURE__ */ new Set();
696
- for (const shot of await api.list(`/v1/appScreenshotSets/${set.id}/appScreenshots?limit=200`)) {
697
- const name = textAttribute(shot.attributes.fileName);
698
- const local = inventory.get(name);
699
- if (!local || names.has(name) || shot.attributes.sourceFileChecksum && shot.attributes.sourceFileChecksum !== local.checksum) throw failure(`Remote screenshot ${displayType}/${name} is extra, duplicated or differs from screenshotsDir. Match the inventories in App Store Connect before resuming; the factory will not delete publisher assets.`, "APPLE_STATE_MISMATCH");
700
- names.add(name);
701
- }
702
- }
703
- for (const [displayType, files] of [...localInventory].sort(([a], [b]) => a.localeCompare(b))) {
704
- const setKey = `screenshot-set:${displayType}`;
705
- let set = sets.find((set) => set.attributes.screenshotDisplayType === displayType);
706
- if (set) await reconcile(setKey, set);
707
- else {
708
- set = resource(await write(setKey, "/v1/appScreenshotSets", "POST", { data: {
709
- type: "appScreenshotSets",
710
- attributes: { screenshotDisplayType: displayType },
711
- relationships: { appStoreVersionLocalization: ref("appStoreVersionLocalizations", localization.id) }
712
- } }));
713
- sets.push(set);
714
- }
715
- const existing = await api.list(`/v1/appScreenshotSets/${set.id}/appScreenshots?limit=200`);
716
- for (const [name, { bytes, checksum }] of files) {
717
- const key = `screenshot:${displayType}:${name}:${checksum}`;
718
- let shot = existing.find((item) => item.attributes.fileName === name);
719
- if (shot?.attributes.sourceFileChecksum && shot.attributes.sourceFileChecksum !== checksum) throw failure(`Remote screenshot ${displayType}/${name} differs from the workspace. Replace it in App Store Connect or use a new filename; the factory will not delete publisher assets.`, "APPLE_STATE_MISMATCH");
720
- if (shot && !shot.attributes.sourceFileChecksum && receipt().effects?.[key]?.id !== shot.id && receipt().pending?.key !== key) throw failure(`Screenshot ${displayType}/${name} has an unfinished reservation not owned by this job. Finish or remove it in App Store Connect before resuming.`, "APPLE_WRITE_UNCERTAIN");
721
- if (shot) await reconcile(key, shot);
722
- else {
723
- shot = resource(await write(key, "/v1/appScreenshots", "POST", { data: {
724
- type: "appScreenshots",
725
- attributes: {
726
- fileName: basename(name),
727
- fileSize: bytes.length
728
- },
729
- relationships: { appScreenshotSet: ref("appScreenshotSets", set.id) }
730
- } }));
731
- existing.push(shot);
732
- }
733
- const delivery = shot.attributes.assetDeliveryState?.state;
734
- if (delivery === "FAILED") throw failure(`Apple rejected screenshot ${displayType}/${name}. Inspect its asset errors in App Store Connect and replace the failed reservation.`);
735
- if (delivery === "COMPLETE") {
736
- await reconcile(`${key}:commit`, shot);
737
- continue;
738
- }
739
- if (delivery === "AWAITING_UPLOAD") {
740
- if (!shot.attributes.uploadOperations?.length) throw failure("Apple returned no screenshot upload operations. Inspect the reservation in App Store Connect.", "APPLE_API_ERROR");
741
- for (const [index, operation] of (shot.attributes.uploadOperations ?? []).entries()) {
742
- if (index < (receipt().assetParts?.[shot.id] ?? 0)) continue;
743
- const url = new URL(operation.url);
744
- if (url.protocol !== "https:" || !url.hostname.endsWith(".apple.com") || operation.method !== "PUT" || !Number.isInteger(operation.offset) || !Number.isInteger(operation.length) || operation.offset < 0 || operation.length <= 0 || operation.offset + operation.length > bytes.length) throw failure("Apple returned invalid screenshot upload instructions; no data was sent.", "APPLE_API_ERROR");
745
- const partKey = `${key}:part:${index}`;
746
- const pending = receipt().pending;
747
- if (pending && pending.key !== partKey) throw failure(`A previous Apple write “${pending.key}” is unresolved. Resume after checking its outcome.`, "APPLE_WRITE_UNCERTAIN");
748
- await persist({ pending: {
749
- key: partKey,
750
- method: "PUT",
751
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
752
- } });
753
- let response;
754
- try {
755
- const part = new Uint8Array(bytes.buffer, bytes.byteOffset + operation.offset, operation.length);
756
- response = await fetch(url, {
757
- method: operation.method,
758
- redirect: "error",
759
- headers: Object.fromEntries((operation.requestHeaders ?? []).map((header) => [header.name, header.value])),
760
- body: part,
761
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(6e4)]) : AbortSignal.timeout(6e4)
762
- });
763
- } catch (error) {
764
- throw requestFailure(`Apple screenshot part upload did not complete: ${message(error)}. Resume to resend the uncommitted part.`, "APPLE_API_ERROR");
765
- }
766
- if (!response.ok) {
767
- const error = requestFailure(`Apple screenshot part upload failed (HTTP ${response.status}); resume to resend the uncommitted part.`, response.status === 429 ? "APPLE_RATE_LIMIT" : "APPLE_API_ERROR", response.status);
768
- if (error.outcome === "rejected") await persist({ pending: null });
769
- throw error;
770
- }
771
- try {
772
- await response.arrayBuffer();
773
- } catch (error) {
774
- throw Object.assign(requestFailure(`Apple screenshot part upload response was lost (HTTP ${response.status}): ${message(error)}. Resume to resend the uncommitted part.`, "APPLE_API_ERROR", response.status), { outcome: "unknown" });
775
- }
776
- await persist({
777
- pending: null,
778
- assetParts: {
779
- ...receipt().assetParts,
780
- [shot.id]: index + 1
781
- },
782
- lastEffect: {
783
- key: partKey,
784
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
785
- }
786
- });
787
- }
788
- await write(`${key}:commit`, `/v1/appScreenshots/${shot.id}`, "PATCH", { data: {
789
- type: "appScreenshots",
790
- id: shot.id,
791
- attributes: {
792
- uploaded: true,
793
- sourceFileChecksum: checksum
794
- }
795
- } });
796
- } else if (delivery === "UPLOAD_COMPLETE") await reconcile(`${key}:commit`, shot);
797
- }
798
- }
799
- }
800
- await verifyScreenshots({
801
- api,
802
- apple,
803
- localization,
804
- localInventory,
805
- sets
806
- });
807
- }
808
- async function verifyScreenshots({ api, apple, localization, localInventory, sets = [] }) {
809
- let complete = 0;
810
- if (localInventory) sets = await api.list(`/v1/appStoreVersionLocalizations/${localization.id}/appScreenshotSets?limit=200`);
811
- const displayTypes = /* @__PURE__ */ new Set();
812
- for (const set of sets) {
813
- const displayType = textAttribute(set.attributes.screenshotDisplayType);
814
- const inventory = localInventory?.get(displayType);
815
- if (localInventory && (!inventory || displayTypes.has(displayType))) throw failure(`Remote screenshot display set ${displayType} is extra or duplicated. Match App Store Connect to screenshotsDir before submitting; the factory will not delete publisher assets.`, "APPLE_STATE_MISMATCH");
816
- displayTypes.add(displayType);
817
- const names = /* @__PURE__ */ new Set();
818
- for (const shot of await api.list(`/v1/appScreenshotSets/${set.id}/appScreenshots?limit=200`)) {
819
- if (inventory) {
820
- const name = textAttribute(shot.attributes.fileName);
821
- const local = inventory.get(name);
822
- if (!local || names.has(name) || shot.attributes.sourceFileChecksum !== local.checksum) throw failure(`Remote screenshot ${displayType}/${name} does not exactly match screenshotsDir. Match filenames and checksums in App Store Connect before submitting; the factory will not delete publisher assets.`, "APPLE_STATE_MISMATCH");
823
- names.add(name);
824
- }
825
- if (shot.attributes.assetDeliveryState?.state !== "COMPLETE") throw failure("Apple has not finished processing all screenshots. Check failed assets in App Store Connect or resume when processing finishes.", "APPLE_PROCESSING");
826
- complete++;
827
- }
828
- if (inventory && names.size !== inventory.size) throw failure(`Remote screenshots for ${displayType} are missing files from screenshotsDir. Resume after the uploaded assets become visible.`, "APPLE_PROCESSING");
829
- }
830
- if (localInventory && displayTypes.size !== localInventory.size) throw failure("Remote screenshot display sets are missing entries from screenshotsDir. Resume after the uploaded sets become visible.", "APPLE_PROCESSING");
831
- if (!complete) throw failure(`No processed screenshots exist for ${apple.locale}. Set spec.apple.screenshotsDir to real screenshots grouped by Apple display type, or upload screenshots in App Store Connect.`);
832
- }
833
- async function pollApple({ spec, state = {}, env = process.env, signal }) {
834
- requireDeliveryProvenance(state);
835
- const apple = spec?.apple;
836
- if (!apple?.appId || !apple.version) throw failure("Polling requires spec.apple.appId and version.");
837
- const api = await client(env, signal);
838
- const version = await findVersion(api, apple.appId, apple.version);
839
- if (!version) return {
840
- status: "waiting",
841
- detail: `Version ${apple.version} is not visible in App Store Connect.`
842
- };
843
- if (state.versionId && version.id !== state.versionId) throw failure("The App Store version differs from the saved receipt. Inspect the app record before resuming.", "APPLE_STATE_MISMATCH");
844
- const status = versionState(version);
845
- if (approved.has(status)) {
846
- await requireSelectedBuild(api, version.id, state.buildId);
847
- const distributionReady = ["READY_FOR_SALE", "READY_FOR_DISTRIBUTION"].includes(status ?? "");
848
- let available = false;
849
- if (distributionReady && apple.storefrontCountry) {
850
- if (!/^[A-Za-z]{2}$/.test(apple.storefrontCountry)) throw failure("apple.storefrontCountry must be a two-letter country code.");
851
- const url = new URL("https://itunes.apple.com/lookup");
852
- url.search = new URLSearchParams({
853
- id: apple.appId,
854
- country: apple.storefrontCountry
855
- }).toString();
856
- let response;
857
- try {
858
- response = await fetch(url, {
859
- redirect: "error",
860
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4)
861
- });
862
- } catch (error) {
863
- throw requestFailure(`Public App Store lookup did not complete: ${message(error)}. Resume to check publication.`, "APPLE_API_ERROR", null, true);
864
- }
865
- if (!response.ok) throw requestFailure(`Public App Store lookup failed (HTTP ${response.status}); publication has not been proven.`, "APPLE_API_ERROR", response.status);
866
- let text;
867
- try {
868
- text = await response.text();
869
- } catch (error) {
870
- throw requestFailure(`Public App Store lookup response was lost: ${message(error)}. Resume to check publication.`, "APPLE_API_ERROR", response.status, true);
871
- }
872
- let data;
873
- try {
874
- data = JSON.parse(text);
875
- } catch {
876
- throw failure("Public App Store lookup returned non-JSON data; publication has not been proven.", "APPLE_API_ERROR");
877
- }
878
- const parsed = ListingSchema.safeParse(data);
879
- if (!parsed.success) throw failure(`Public App Store lookup returned invalid data: ${parsed.error.message}`, "APPLE_API_ERROR");
880
- available = parsed.data.results.some((item) => item.bundleId === apple.bundleId && item.version === apple.version);
881
- }
882
- return {
883
- status: "approved",
884
- distributionReady,
885
- available,
886
- detail: `Apple version state: ${status}. Approval does not imply public availability; lookup of the exact bundle/version ${available ? "confirmed the configured storefront" : "has not confirmed the configured storefront"}.`
887
- };
888
- }
889
- if (rejected.has(status)) return {
890
- status: "rejected",
891
- detail: `Apple version state: ${status}. Read App Review messages in App Store Connect before changing or resubmitting the app.`
892
- };
893
- return {
894
- status: "waiting",
895
- detail: `Apple version state: ${status}.`
896
- };
897
- }
898
- export { AppleReceiptSchema, appleDoctor, pollApple, shipApple, validateAppleSpec };
899
-
900
- //# sourceMappingURL=apple.js.map