@stacksjs/browser-extension 0.70.144 → 0.70.146
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -3
- package/dist/app-store-connect.d.ts +34 -0
- package/dist/app-store-connect.js +75 -2
- package/dist/app-store-submission.d.ts +27 -0
- package/dist/app-store-submission.js +443 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/safari.d.ts +7 -0
- package/dist/safari.js +17 -6
- package/dist/types.d.ts +63 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ buddy extension:chrome:publish --upload-only
|
|
|
31
31
|
buddy extension:firefox:publish # build, submit, sign through AMO
|
|
32
32
|
buddy extension:safari:provision # register Bundle IDs + check app record
|
|
33
33
|
buddy extension:safari:publish
|
|
34
|
+
buddy extension:safari:submit # submit an already-uploaded version
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
Chrome reads `CHROME_WEB_STORE_SERVICE_ACCOUNT_PATH` (or
|
|
@@ -79,23 +80,32 @@ buddy extension:safari:provision # register both Bundle IDs + check the app reco
|
|
|
79
80
|
buddy extension:safari:app # build configured macOS/iOS apps
|
|
80
81
|
buddy extension:safari:app --platform ios # build for iPhone/iPad Simulator
|
|
81
82
|
buddy extension:safari:publish # archive + upload every configured Apple platform
|
|
83
|
+
buddy extension:safari:submit # sync listing metadata and submit existing builds
|
|
82
84
|
```
|
|
83
85
|
|
|
84
86
|
Set `safariBundleId` in the config (the appex gets `<safariBundleId>.Extension`)
|
|
85
87
|
and `safariTeamId` to the Apple Developer team used for signing. Set
|
|
86
88
|
`safariPlatforms: ['macos', 'ios']` for a universal extension; iOS covers both
|
|
87
89
|
iPhone and iPad. Stacks generates the current Apple-supported universal Xcode
|
|
88
|
-
project from the same built extension, archives both platforms,
|
|
89
|
-
|
|
90
|
+
project from the same built extension, archives both platforms, uploads them
|
|
91
|
+
to the same App Store Connect app record, waits for Apple processing, and
|
|
92
|
+
selects each processed build on its matching App Store version. Publishing
|
|
90
93
|
reads `APP_STORE_CONNECT_API_KEY_ID`, `APP_STORE_CONNECT_API_ISSUER_ID`, and
|
|
91
94
|
`APP_STORE_CONNECT_API_KEY_PATH` from the environment. Run with
|
|
92
95
|
`--validate-only` to exercise Apple's validation without uploading a build.
|
|
93
|
-
|
|
96
|
+
List any build output that is not part of the extension (marketing pages,
|
|
94
97
|
etc.) in `safariExclude` so it stays out of the appex. The scaffold mirrors
|
|
95
98
|
what `xcrun safari-web-extension-converter` generates, so day-to-day work
|
|
96
99
|
never needs the converter; `--signed` builds need an Apple Development
|
|
97
100
|
identity selected in Xcode.
|
|
98
101
|
|
|
102
|
+
When `safariAppStore` is configured, publishing also synchronizes the app
|
|
103
|
+
description, category, content-rights declaration, pricing, age rating,
|
|
104
|
+
territory availability, review contact, export compliance, and required
|
|
105
|
+
screenshots. With `submitForReview: true`, the macOS and iOS versions are then
|
|
106
|
+
submitted independently to App Review. The workflow is resumable and keeps an
|
|
107
|
+
existing active review submission intact.
|
|
108
|
+
|
|
99
109
|
For device testing, install the generated iOS app from Xcode, then enable the
|
|
100
110
|
extension in Settings > Apps > Safari > Extensions. The iOS target supports
|
|
101
111
|
iPhone and iPad from iOS 15 onward; the extension's configured Safari minimum
|
|
@@ -117,6 +127,27 @@ export default defineExtension({
|
|
|
117
127
|
safariTeamId: 'TEAM123456',
|
|
118
128
|
safariPlatforms: ['macos', 'ios'], // iOS includes iPhone and iPad
|
|
119
129
|
safariAppCategory: 'public.app-category.utilities',
|
|
130
|
+
safariAppStore: {
|
|
131
|
+
subtitle: 'Fast, private blocking',
|
|
132
|
+
privacyPolicyUrl: 'https://example.com/privacy',
|
|
133
|
+
description: 'A private Safari extension.',
|
|
134
|
+
keywords: 'privacy,blocking',
|
|
135
|
+
supportUrl: 'https://example.com/support',
|
|
136
|
+
copyright: '2026 Example',
|
|
137
|
+
primaryCategory: 'UTILITIES',
|
|
138
|
+
contentRightsDeclaration: 'DOES_NOT_USE_THIRD_PARTY_CONTENT',
|
|
139
|
+
price: '0',
|
|
140
|
+
reviewContact: {
|
|
141
|
+
firstName: 'App', lastName: 'Reviewer',
|
|
142
|
+
phone: '+1 555-555-0100', email: 'review@example.com',
|
|
143
|
+
},
|
|
144
|
+
screenshots: {
|
|
145
|
+
APP_DESKTOP: ['resources/store/macos.png'],
|
|
146
|
+
APP_IPHONE_67: ['resources/store/iphone.png'],
|
|
147
|
+
APP_IPAD_PRO_3GEN_129: ['resources/store/ipad.png'],
|
|
148
|
+
},
|
|
149
|
+
submitForReview: true,
|
|
150
|
+
},
|
|
120
151
|
targets: ['chrome', 'firefox'],
|
|
121
152
|
|
|
122
153
|
background: 'src/background/index.ts',
|
|
@@ -2,6 +2,8 @@ import type { AppStoreConnectAuth } from './safari';
|
|
|
2
2
|
import type { ExtensionConfig, SafariPlatform } from './types';
|
|
3
3
|
/** Generate the short-lived ES256 team token required by App Store Connect. */
|
|
4
4
|
export declare function appStoreConnectToken(auth: Required<AppStoreConnectAuth>, now?: unknown): string;
|
|
5
|
+
/** Wait for the uploaded binaries to process, then select them for their App Store versions. */
|
|
6
|
+
export declare function attachSafariBuilds(client: AppStoreConnectClient, appId: string, versions: SafariAppStoreVersionResult[], buildNumber: string, options?: AttachSafariBuildsOptions): Promise<SafariBuildAttachmentResult[]>;
|
|
5
7
|
/**
|
|
6
8
|
* Register the explicit container and extension Bundle IDs required by a
|
|
7
9
|
* Safari Web Extension, then check for the manually-created App Store Connect
|
|
@@ -12,6 +14,7 @@ export declare interface AppStoreConnectResource<T extends Record<string, unknow
|
|
|
12
14
|
type: string
|
|
13
15
|
id: string
|
|
14
16
|
attributes: T
|
|
17
|
+
relationships?: Record<string, { data?: { type: string, id: string } | null }>
|
|
15
18
|
}
|
|
16
19
|
export declare interface BundleIdAttributes extends Record<string, unknown> {
|
|
17
20
|
identifier: string
|
|
@@ -30,6 +33,23 @@ export declare interface AppStoreVersionAttributes extends Record<string, unknow
|
|
|
30
33
|
versionString: string
|
|
31
34
|
appStoreState: string
|
|
32
35
|
}
|
|
36
|
+
export declare interface BuildAttributes extends Record<string, unknown> {
|
|
37
|
+
version: string
|
|
38
|
+
uploadedDate: string
|
|
39
|
+
expired: boolean
|
|
40
|
+
processingState: string
|
|
41
|
+
}
|
|
42
|
+
export declare interface PreReleaseVersionAttributes extends Record<string, unknown> {
|
|
43
|
+
version: string
|
|
44
|
+
platform: AppStoreVersionPlatform
|
|
45
|
+
}
|
|
46
|
+
export declare interface AppStoreBuildResult {
|
|
47
|
+
id: string
|
|
48
|
+
buildNumber: string
|
|
49
|
+
version: string
|
|
50
|
+
platform: AppStoreVersionPlatform
|
|
51
|
+
processingState: string
|
|
52
|
+
}
|
|
33
53
|
export declare interface AppStoreConnectClientOptions extends AppStoreConnectAuth {
|
|
34
54
|
baseUrl?: string
|
|
35
55
|
fetch?: typeof globalThis.fetch
|
|
@@ -42,6 +62,16 @@ export declare interface SafariAppStoreVersionResult {
|
|
|
42
62
|
updated: boolean
|
|
43
63
|
id: string
|
|
44
64
|
}
|
|
65
|
+
export declare interface SafariBuildAttachmentResult {
|
|
66
|
+
platform: SafariPlatform
|
|
67
|
+
versionId: string
|
|
68
|
+
buildId: string
|
|
69
|
+
buildNumber: string
|
|
70
|
+
}
|
|
71
|
+
export declare interface AttachSafariBuildsOptions {
|
|
72
|
+
timeoutMs?: number
|
|
73
|
+
pollIntervalMs?: number
|
|
74
|
+
}
|
|
45
75
|
export declare interface SafariProvisionOptions extends AppStoreConnectClientOptions {
|
|
46
76
|
checkOnly?: boolean
|
|
47
77
|
platform?: BundleIdPlatform
|
|
@@ -59,6 +89,8 @@ export type AppStoreVersionPlatform = 'IOS' | 'MAC_OS';
|
|
|
59
89
|
/** Minimal official App Store Connect client for Safari provisioning checks. */
|
|
60
90
|
export declare class AppStoreConnectClient {
|
|
61
91
|
constructor(options?: AppStoreConnectClientOptions);
|
|
92
|
+
request<T>(path: string, init?: RequestInit): Promise<T>;
|
|
93
|
+
upload(url: string, init: RequestInit): Promise<void>;
|
|
62
94
|
findBundleId(identifier: string): Promise<AppStoreConnectResource<BundleIdAttributes> | undefined>;
|
|
63
95
|
registerBundleId(identifier: string, name: string, platform?: BundleIdPlatform): Promise<AppStoreConnectResource<BundleIdAttributes>>;
|
|
64
96
|
ensureBundleId(identifier: string, name: string, options?: { checkOnly?: boolean, platform?: BundleIdPlatform }): Promise<{ bundleId?: AppStoreConnectResource<BundleIdAttributes>, created: boolean }>;
|
|
@@ -66,4 +98,6 @@ export declare class AppStoreConnectClient {
|
|
|
66
98
|
listAppStoreVersions(appId: string): Promise<Array<AppStoreConnectResource<AppStoreVersionAttributes>>>;
|
|
67
99
|
createAppStoreVersion(appId: string, platform: AppStoreVersionPlatform, versionString: string): Promise<AppStoreConnectResource<AppStoreVersionAttributes>>;
|
|
68
100
|
updateAppStoreVersion(versionId: string, versionString: string): Promise<AppStoreConnectResource<AppStoreVersionAttributes>>;
|
|
101
|
+
listBuilds(appId: string, buildNumber: string): Promise<AppStoreBuildResult[]>;
|
|
102
|
+
attachBuild(versionId: string, buildId: string): Promise<void>;
|
|
69
103
|
}
|
|
@@ -2,6 +2,13 @@ import { createPrivateKey, sign } from "node:crypto";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { resolveAppStoreConnectAuth } from "./safari";
|
|
4
4
|
const appStoreConnectBaseUrl = "https://api.appstoreconnect.apple.com/v1";
|
|
5
|
+
function apiErrorMessages(error) {
|
|
6
|
+
const messages = [error.detail ?? error.title ?? error.code].filter((message) => Boolean(message));
|
|
7
|
+
for (const associated of Object.values(error.meta?.associatedErrors ?? {}))
|
|
8
|
+
for (const nested of associated)
|
|
9
|
+
messages.push(...apiErrorMessages(nested));
|
|
10
|
+
return messages;
|
|
11
|
+
}
|
|
5
12
|
function base64urlJson(value) {
|
|
6
13
|
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
7
14
|
}
|
|
@@ -34,7 +41,7 @@ export class AppStoreConnectClient {
|
|
|
34
41
|
this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
|
|
35
42
|
}
|
|
36
43
|
async request(path, init = {}) {
|
|
37
|
-
const
|
|
44
|
+
const url = /^https?:\/\//.test(path) ? path : /^\/v\d+\//.test(path) ? `${new URL(this.baseUrl).origin}${path}` : `${this.baseUrl}${path}`, response = await this.fetcher(url, {
|
|
38
45
|
...init,
|
|
39
46
|
headers: {
|
|
40
47
|
Accept: "application/json",
|
|
@@ -44,11 +51,18 @@ export class AppStoreConnectClient {
|
|
|
44
51
|
}
|
|
45
52
|
});
|
|
46
53
|
if (!response.ok) {
|
|
47
|
-
const
|
|
54
|
+
const body = await response.json().catch(() => ({})), details = [...new Set(body.errors?.flatMap(apiErrorMessages) ?? [])].join("; ");
|
|
48
55
|
throw Error(`[browser-extension] App Store Connect ${init.method ?? "GET"} ${path} failed (${response.status})${details ? `: ${details}` : ""}`);
|
|
49
56
|
}
|
|
57
|
+
if (response.status === 204)
|
|
58
|
+
return;
|
|
50
59
|
return await response.json();
|
|
51
60
|
}
|
|
61
|
+
async upload(url, init) {
|
|
62
|
+
const response = await this.fetcher(url, init);
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
throw Error(`[browser-extension] App Store Connect asset upload failed (${response.status})`);
|
|
65
|
+
}
|
|
52
66
|
async findBundleId(identifier) {
|
|
53
67
|
const query = new URLSearchParams({ "filter[identifier]": identifier });
|
|
54
68
|
return (await this.request(`/bundleIds?${query}`)).data.find((bundleId) => bundleId.attributes.identifier === identifier);
|
|
@@ -106,6 +120,34 @@ export class AppStoreConnectClient {
|
|
|
106
120
|
})
|
|
107
121
|
})).data;
|
|
108
122
|
}
|
|
123
|
+
async listBuilds(appId, buildNumber) {
|
|
124
|
+
const query = new URLSearchParams({
|
|
125
|
+
"filter[app]": appId,
|
|
126
|
+
"filter[version]": buildNumber,
|
|
127
|
+
include: "preReleaseVersion",
|
|
128
|
+
limit: "20",
|
|
129
|
+
"fields[builds]": "version,uploadedDate,expired,processingState,preReleaseVersion",
|
|
130
|
+
"fields[preReleaseVersions]": "version,platform"
|
|
131
|
+
}), response = await this.request(`/builds?${query}`), preReleaseVersions = new Map((response.included ?? []).filter((item) => item.type === "preReleaseVersions").map((item) => [item.id, item.attributes]));
|
|
132
|
+
return response.data.flatMap((build) => {
|
|
133
|
+
const preReleaseVersionId = build.relationships?.preReleaseVersion?.data?.id, preReleaseVersion = preReleaseVersionId ? preReleaseVersions.get(preReleaseVersionId) : void 0;
|
|
134
|
+
if (!preReleaseVersion)
|
|
135
|
+
return [];
|
|
136
|
+
return [{
|
|
137
|
+
id: build.id,
|
|
138
|
+
buildNumber: build.attributes.version,
|
|
139
|
+
version: preReleaseVersion.version,
|
|
140
|
+
platform: preReleaseVersion.platform,
|
|
141
|
+
processingState: build.attributes.processingState
|
|
142
|
+
}];
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
async attachBuild(versionId, buildId) {
|
|
146
|
+
await this.request(`/appStoreVersions/${versionId}/relationships/build`, {
|
|
147
|
+
method: "PATCH",
|
|
148
|
+
body: JSON.stringify({ data: { type: "builds", id: buildId } })
|
|
149
|
+
});
|
|
150
|
+
}
|
|
109
151
|
}
|
|
110
152
|
const editableVersionStates = new Set([
|
|
111
153
|
"PREPARE_FOR_SUBMISSION",
|
|
@@ -116,6 +158,37 @@ const editableVersionStates = new Set([
|
|
|
116
158
|
function appStorePlatform(platform) {
|
|
117
159
|
return platform === "ios" ? "IOS" : "MAC_OS";
|
|
118
160
|
}
|
|
161
|
+
export async function attachSafariBuilds(client, appId, versions, buildNumber, options = {}) {
|
|
162
|
+
const timeoutMs = options.timeoutMs ?? 1200000, pollIntervalMs = options.pollIntervalMs ?? 15000, deadline = Date.now() + timeoutMs, pending = new Map(versions.map((version) => [appStorePlatform(version.platform), version]));
|
|
163
|
+
while (!0) {
|
|
164
|
+
const builds = await client.listBuilds(appId, buildNumber), attachments = [];
|
|
165
|
+
for (const [platform, version] of pending) {
|
|
166
|
+
const build = builds.find((item) => item.platform === platform && item.version === version.version);
|
|
167
|
+
if (!build)
|
|
168
|
+
continue;
|
|
169
|
+
if (build.processingState === "FAILED" || build.processingState === "INVALID")
|
|
170
|
+
throw Error(`[browser-extension] Safari ${version.platform} build ${buildNumber} failed App Store Connect processing (${build.processingState})`);
|
|
171
|
+
if (build.processingState !== "VALID")
|
|
172
|
+
continue;
|
|
173
|
+
attachments.push({
|
|
174
|
+
platform: version.platform,
|
|
175
|
+
versionId: version.id,
|
|
176
|
+
buildId: build.id,
|
|
177
|
+
buildNumber
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (attachments.length === pending.size) {
|
|
181
|
+
for (const attachment of attachments)
|
|
182
|
+
await client.attachBuild(attachment.versionId, attachment.buildId);
|
|
183
|
+
return attachments;
|
|
184
|
+
}
|
|
185
|
+
if (Date.now() >= deadline) {
|
|
186
|
+
const waiting = [...pending.values()].map((item) => item.platform).join(", ");
|
|
187
|
+
throw Error(`[browser-extension] timed out waiting for Safari ${waiting} build ${buildNumber} to finish App Store Connect processing`);
|
|
188
|
+
}
|
|
189
|
+
await Bun.sleep(pollIntervalMs);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
119
192
|
async function ensureConfiguredAppStoreVersions(client, appId, platforms, version) {
|
|
120
193
|
const versions = await client.listAppStoreVersions(appId), results = [];
|
|
121
194
|
for (const platform of [...new Set(platforms)]) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AppStoreConnectClient } from './app-store-connect';
|
|
2
|
+
import type { AppStoreConnectClientOptions } from './app-store-connect';
|
|
3
|
+
import type { ExtensionConfig, SafariPlatform } from './types';
|
|
4
|
+
/** Synchronize a Safari listing and optionally submit all configured platforms to App Review. */
|
|
5
|
+
export declare function prepareSafariAppStoreSubmission(client: AppStoreConnectClient, config: ExtensionConfig, appId: string, versions: SafariStoreVersion[], options?: SafariStoreSubmissionOptions): Promise<SafariStoreSubmissionResult>;
|
|
6
|
+
/** Prepare and submit an already-uploaded Safari version without rebuilding its binary. */
|
|
7
|
+
export declare function submitSafariAppStore(config: ExtensionConfig, options: SubmitSafariAppStoreOptions): Promise<SafariStoreSubmissionResult>;
|
|
8
|
+
declare interface SafariStoreVersion {
|
|
9
|
+
platform: SafariPlatform
|
|
10
|
+
id: string
|
|
11
|
+
buildId?: string
|
|
12
|
+
}
|
|
13
|
+
export declare interface SafariStoreSubmissionOptions {
|
|
14
|
+
cwd?: string
|
|
15
|
+
submit?: boolean
|
|
16
|
+
screenshotTimeoutMs?: number
|
|
17
|
+
screenshotPollIntervalMs?: number
|
|
18
|
+
}
|
|
19
|
+
export declare interface SafariStoreSubmissionResult {
|
|
20
|
+
appId: string
|
|
21
|
+
versions: Array<{ platform: SafariPlatform, versionId: string, localizationId: string }>
|
|
22
|
+
reviewSubmissionIds: string[]
|
|
23
|
+
}
|
|
24
|
+
export declare interface SubmitSafariAppStoreOptions extends AppStoreConnectClientOptions, SafariStoreSubmissionOptions {
|
|
25
|
+
version: string
|
|
26
|
+
platforms?: SafariPlatform[]
|
|
27
|
+
}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { AppStoreConnectClient } from "./app-store-connect";
|
|
5
|
+
const frequencyFields = [
|
|
6
|
+
"alcoholTobaccoOrDrugUseOrReferences",
|
|
7
|
+
"contests",
|
|
8
|
+
"gamblingSimulated",
|
|
9
|
+
"gunsOrOtherWeapons",
|
|
10
|
+
"medicalOrTreatmentInformation",
|
|
11
|
+
"profanityOrCrudeHumor",
|
|
12
|
+
"sexualContentGraphicAndNudity",
|
|
13
|
+
"sexualContentOrNudity",
|
|
14
|
+
"horrorOrFearThemes",
|
|
15
|
+
"matureOrSuggestiveThemes",
|
|
16
|
+
"violenceCartoonOrFantasy",
|
|
17
|
+
"violenceRealisticProlongedGraphicOrSadistic",
|
|
18
|
+
"violenceRealistic"
|
|
19
|
+
], booleanFields = [
|
|
20
|
+
"advertising",
|
|
21
|
+
"gambling",
|
|
22
|
+
"healthOrWellnessTopics",
|
|
23
|
+
"lootBox",
|
|
24
|
+
"messagingAndChat",
|
|
25
|
+
"parentalControls",
|
|
26
|
+
"ageAssurance",
|
|
27
|
+
"socialMedia",
|
|
28
|
+
"socialMediaAgeRestricted",
|
|
29
|
+
"unrestrictedWebAccess",
|
|
30
|
+
"userGeneratedContent"
|
|
31
|
+
], editableVersionStates = new Set(["PREPARE_FOR_SUBMISSION", "DEVELOPER_REJECTED", "REJECTED", "METADATA_REJECTED"]);
|
|
32
|
+
function ageRatingAttributes(config = {}) {
|
|
33
|
+
const attributes = {
|
|
34
|
+
ageRatingOverrideV2: "NONE",
|
|
35
|
+
koreaAgeRatingOverride: "NONE"
|
|
36
|
+
};
|
|
37
|
+
for (const field of frequencyFields)
|
|
38
|
+
attributes[field] = config[field] ?? "NONE";
|
|
39
|
+
for (const field of booleanFields)
|
|
40
|
+
attributes[field] = config[field] ?? !1;
|
|
41
|
+
return attributes;
|
|
42
|
+
}
|
|
43
|
+
function applePlatform(platform) {
|
|
44
|
+
return platform === "ios" ? "IOS" : "MAC_OS";
|
|
45
|
+
}
|
|
46
|
+
function displayTypesForPlatform(platform) {
|
|
47
|
+
return platform === "macos" ? ["APP_DESKTOP"] : ["APP_IPHONE_67", "APP_IPAD_PRO_3GEN_129"];
|
|
48
|
+
}
|
|
49
|
+
async function updateAppInfo(client, appId, config) {
|
|
50
|
+
await client.request(`/apps/${appId}`, {
|
|
51
|
+
method: "PATCH",
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
data: {
|
|
54
|
+
type: "apps",
|
|
55
|
+
id: appId,
|
|
56
|
+
attributes: { contentRightsDeclaration: config.contentRightsDeclaration }
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
});
|
|
60
|
+
const infos = await client.request(`/apps/${appId}/appInfos?limit=10`), info = infos.data.find((item) => item.attributes.appStoreState === "PREPARE_FOR_SUBMISSION") ?? infos.data[0];
|
|
61
|
+
if (!info)
|
|
62
|
+
throw Error("[browser-extension] App Store Connect has no editable app information resource");
|
|
63
|
+
await client.request(`/appInfos/${info.id}`, {
|
|
64
|
+
method: "PATCH",
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
data: {
|
|
67
|
+
type: "appInfos",
|
|
68
|
+
id: info.id,
|
|
69
|
+
relationships: {
|
|
70
|
+
primaryCategory: { data: { type: "appCategories", id: config.primaryCategory } }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
});
|
|
75
|
+
const localizations = await client.request(`/appInfos/${info.id}/appInfoLocalizations?limit=200`), locale = config.locale ?? "en-US", localization = localizations.data.find((item) => item.attributes.locale === locale);
|
|
76
|
+
if (!localization)
|
|
77
|
+
throw Error(`[browser-extension] App Store Connect app information is missing the ${locale} localization`);
|
|
78
|
+
await client.request(`/appInfoLocalizations/${localization.id}`, {
|
|
79
|
+
method: "PATCH",
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
data: {
|
|
82
|
+
type: "appInfoLocalizations",
|
|
83
|
+
id: localization.id,
|
|
84
|
+
attributes: {
|
|
85
|
+
subtitle: config.subtitle,
|
|
86
|
+
privacyPolicyUrl: config.privacyPolicyUrl
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
await client.request(`/ageRatingDeclarations/${info.id}`, {
|
|
92
|
+
method: "PATCH",
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
data: {
|
|
95
|
+
type: "ageRatingDeclarations",
|
|
96
|
+
id: info.id,
|
|
97
|
+
attributes: ageRatingAttributes(config.ageRating)
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
return info.id;
|
|
102
|
+
}
|
|
103
|
+
async function ensurePrice(client, appId, config) {
|
|
104
|
+
if ((await client.request(`/appPriceSchedules/${appId}/manualPrices?limit=200`)).data.length)
|
|
105
|
+
return;
|
|
106
|
+
const baseTerritory = config.baseTerritory ?? "USA", pricePoints = await client.request(`/apps/${appId}/appPricePoints?filter[territory]=${encodeURIComponent(baseTerritory)}&limit=200`), requestedPrice = Number(config.price), pricePoint = pricePoints.data.find((item) => Number(item.attributes.customerPrice) === requestedPrice);
|
|
107
|
+
if (!pricePoint)
|
|
108
|
+
throw Error(`[browser-extension] App Store price ${config.price} is unavailable in ${baseTerritory}`);
|
|
109
|
+
const localId = "${price}";
|
|
110
|
+
await client.request("/appPriceSchedules", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
body: JSON.stringify({
|
|
113
|
+
data: {
|
|
114
|
+
type: "appPriceSchedules",
|
|
115
|
+
relationships: {
|
|
116
|
+
app: { data: { type: "apps", id: appId } },
|
|
117
|
+
baseTerritory: { data: { type: "territories", id: baseTerritory } },
|
|
118
|
+
manualPrices: { data: [{ type: "appPrices", id: localId }] }
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
included: [{
|
|
122
|
+
type: "appPrices",
|
|
123
|
+
id: localId,
|
|
124
|
+
attributes: { startDate: null, endDate: null },
|
|
125
|
+
relationships: { appPricePoint: { data: { type: "appPricePoints", id: pricePoint.id } } }
|
|
126
|
+
}]
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function ensureAvailability(client, appId, availableInNewTerritories = !0) {
|
|
131
|
+
try {
|
|
132
|
+
if ((await client.request(`/apps/${appId}/appAvailabilityV2`)).data.attributes.availableInNewTerritories === availableInNewTerritories)
|
|
133
|
+
return;
|
|
134
|
+
return;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (!(error instanceof Error) || !error.message.includes("(404)"))
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
const included = (await client.request("/territories?limit=200")).data.map((territory, index) => {
|
|
140
|
+
return {
|
|
141
|
+
type: "territoryAvailabilities",
|
|
142
|
+
id: `\${territory-${index}}`,
|
|
143
|
+
attributes: { available: !0 },
|
|
144
|
+
relationships: { territory: { data: { type: "territories", id: territory.id } } }
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
await client.request("/v2/appAvailabilities", {
|
|
148
|
+
method: "POST",
|
|
149
|
+
body: JSON.stringify({
|
|
150
|
+
data: {
|
|
151
|
+
type: "appAvailabilities",
|
|
152
|
+
attributes: { availableInNewTerritories },
|
|
153
|
+
relationships: {
|
|
154
|
+
app: { data: { type: "apps", id: appId } },
|
|
155
|
+
territoryAvailabilities: {
|
|
156
|
+
data: included.map((item) => ({ type: item.type, id: item.id }))
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
included
|
|
161
|
+
})
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
async function ensureVersionLocalization(client, versionId, config) {
|
|
165
|
+
const locale = config.locale ?? "en-US";
|
|
166
|
+
let localization = (await client.request(`/appStoreVersions/${versionId}/appStoreVersionLocalizations?limit=200`)).data.find((item) => item.attributes.locale === locale);
|
|
167
|
+
if (!localization)
|
|
168
|
+
localization = (await client.request("/appStoreVersionLocalizations", {
|
|
169
|
+
method: "POST",
|
|
170
|
+
body: JSON.stringify({
|
|
171
|
+
data: {
|
|
172
|
+
type: "appStoreVersionLocalizations",
|
|
173
|
+
attributes: { locale },
|
|
174
|
+
relationships: { appStoreVersion: { data: { type: "appStoreVersions", id: versionId } } }
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
})).data;
|
|
178
|
+
await client.request(`/appStoreVersionLocalizations/${localization.id}`, {
|
|
179
|
+
method: "PATCH",
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
data: {
|
|
182
|
+
type: "appStoreVersionLocalizations",
|
|
183
|
+
id: localization.id,
|
|
184
|
+
attributes: {
|
|
185
|
+
description: config.description,
|
|
186
|
+
keywords: config.keywords,
|
|
187
|
+
supportUrl: config.supportUrl,
|
|
188
|
+
...config.marketingUrl ? { marketingUrl: config.marketingUrl } : {},
|
|
189
|
+
...config.promotionalText ? { promotionalText: config.promotionalText } : {},
|
|
190
|
+
...config.whatsNew ? { whatsNew: config.whatsNew } : {}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
return localization.id;
|
|
196
|
+
}
|
|
197
|
+
async function ensureReviewDetail(client, versionId, config) {
|
|
198
|
+
const response = await client.request(`/appStoreVersions/${versionId}/appStoreReviewDetail`), attributes = {
|
|
199
|
+
contactFirstName: config.reviewContact.firstName,
|
|
200
|
+
contactLastName: config.reviewContact.lastName,
|
|
201
|
+
contactPhone: config.reviewContact.phone,
|
|
202
|
+
contactEmail: config.reviewContact.email,
|
|
203
|
+
demoAccountRequired: !1,
|
|
204
|
+
...config.reviewContact.notes ? { notes: config.reviewContact.notes } : {}
|
|
205
|
+
};
|
|
206
|
+
if (response.data) {
|
|
207
|
+
await client.request(`/appStoreReviewDetails/${response.data.id}`, {
|
|
208
|
+
method: "PATCH",
|
|
209
|
+
body: JSON.stringify({ data: { type: "appStoreReviewDetails", id: response.data.id, attributes } })
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
await client.request("/appStoreReviewDetails", {
|
|
214
|
+
method: "POST",
|
|
215
|
+
body: JSON.stringify({
|
|
216
|
+
data: {
|
|
217
|
+
type: "appStoreReviewDetails",
|
|
218
|
+
attributes,
|
|
219
|
+
relationships: { appStoreVersion: { data: { type: "appStoreVersions", id: versionId } } }
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async function ensureScreenshotSet(client, localizationId, displayType) {
|
|
225
|
+
const existing = (await client.request(`/appStoreVersionLocalizations/${localizationId}/appScreenshotSets?limit=200`)).data.find((item) => item.attributes.screenshotDisplayType === displayType);
|
|
226
|
+
if (existing)
|
|
227
|
+
return existing.id;
|
|
228
|
+
return (await client.request("/appScreenshotSets", {
|
|
229
|
+
method: "POST",
|
|
230
|
+
body: JSON.stringify({
|
|
231
|
+
data: {
|
|
232
|
+
type: "appScreenshotSets",
|
|
233
|
+
attributes: { screenshotDisplayType: displayType },
|
|
234
|
+
relationships: {
|
|
235
|
+
appStoreVersionLocalization: { data: { type: "appStoreVersionLocalizations", id: localizationId } }
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
})).data.id;
|
|
240
|
+
}
|
|
241
|
+
async function waitForScreenshot(client, id, timeoutMs, pollIntervalMs) {
|
|
242
|
+
const deadline = Date.now() + timeoutMs;
|
|
243
|
+
while (!0) {
|
|
244
|
+
const response = await client.request(`/appScreenshots/${id}`), state = response.data.attributes.assetDeliveryState?.state;
|
|
245
|
+
if (state === "COMPLETE")
|
|
246
|
+
return;
|
|
247
|
+
if (state === "FAILED") {
|
|
248
|
+
const detail = response.data.attributes.assetDeliveryState?.errors?.map((error) => error.message).filter(Boolean).join("; ");
|
|
249
|
+
throw Error(`[browser-extension] App Store screenshot processing failed${detail ? `: ${detail}` : ""}`);
|
|
250
|
+
}
|
|
251
|
+
if (Date.now() >= deadline)
|
|
252
|
+
throw Error(`[browser-extension] timed out waiting for App Store screenshot ${response.data.attributes.fileName}`);
|
|
253
|
+
await Bun.sleep(pollIntervalMs);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function uploadScreenshot(client, setId, path, timeoutMs, pollIntervalMs) {
|
|
257
|
+
const bytes = await readFile(path), file = basename(path), checksum = createHash("md5").update(bytes).digest("hex"), reserved = await client.request("/appScreenshots", {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
data: {
|
|
261
|
+
type: "appScreenshots",
|
|
262
|
+
attributes: { fileName: file, fileSize: bytes.byteLength },
|
|
263
|
+
relationships: { appScreenshotSet: { data: { type: "appScreenshotSets", id: setId } } }
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
});
|
|
267
|
+
for (const operation of reserved.data.attributes.uploadOperations ?? []) {
|
|
268
|
+
const body = bytes.subarray(operation.offset, operation.offset + operation.length);
|
|
269
|
+
await client.upload(operation.url, {
|
|
270
|
+
method: operation.method,
|
|
271
|
+
headers: Object.fromEntries(operation.requestHeaders.map((header) => [header.name, header.value])),
|
|
272
|
+
body
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
await client.request(`/appScreenshots/${reserved.data.id}`, {
|
|
276
|
+
method: "PATCH",
|
|
277
|
+
body: JSON.stringify({
|
|
278
|
+
data: {
|
|
279
|
+
type: "appScreenshots",
|
|
280
|
+
id: reserved.data.id,
|
|
281
|
+
attributes: { uploaded: !0, sourceFileChecksum: checksum }
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
});
|
|
285
|
+
await waitForScreenshot(client, reserved.data.id, timeoutMs, pollIntervalMs);
|
|
286
|
+
return reserved.data.id;
|
|
287
|
+
}
|
|
288
|
+
async function syncScreenshotSet(client, setId, paths, cwd, timeoutMs, pollIntervalMs) {
|
|
289
|
+
if (paths.length > 10)
|
|
290
|
+
throw Error("[browser-extension] App Store screenshot sets accept at most 10 images");
|
|
291
|
+
const files = await Promise.all(paths.map(async (path) => {
|
|
292
|
+
const absolute = resolve(cwd, path);
|
|
293
|
+
await stat(absolute);
|
|
294
|
+
const bytes = await readFile(absolute);
|
|
295
|
+
return { absolute, name: basename(absolute), checksum: createHash("md5").update(bytes).digest("hex") };
|
|
296
|
+
})), existing = await client.request(`/appScreenshotSets/${setId}/appScreenshots?limit=200`);
|
|
297
|
+
for (const screenshot of existing.data) {
|
|
298
|
+
const desired = files.find((file) => file.name === screenshot.attributes.fileName);
|
|
299
|
+
if (!desired || desired.checksum !== screenshot.attributes.sourceFileChecksum)
|
|
300
|
+
await client.request(`/appScreenshots/${screenshot.id}`, { method: "DELETE" });
|
|
301
|
+
}
|
|
302
|
+
for (const file of files)
|
|
303
|
+
if (!existing.data.find((screenshot) => screenshot.attributes.fileName === file.name && screenshot.attributes.sourceFileChecksum === file.checksum))
|
|
304
|
+
await uploadScreenshot(client, setId, file.absolute, timeoutMs, pollIntervalMs);
|
|
305
|
+
}
|
|
306
|
+
async function updateVersion(client, version, config) {
|
|
307
|
+
await client.request(`/appStoreVersions/${version.id}`, {
|
|
308
|
+
method: "PATCH",
|
|
309
|
+
body: JSON.stringify({
|
|
310
|
+
data: {
|
|
311
|
+
type: "appStoreVersions",
|
|
312
|
+
id: version.id,
|
|
313
|
+
attributes: {
|
|
314
|
+
copyright: config.copyright,
|
|
315
|
+
releaseType: config.releaseType ?? "AFTER_APPROVAL",
|
|
316
|
+
usesIdfa: config.usesIdfa ?? !1
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
});
|
|
321
|
+
if (version.buildId) {
|
|
322
|
+
const encryption = config.usesNonExemptEncryption ?? !1, build = await client.request(`/builds/${version.buildId}?fields[builds]=usesNonExemptEncryption`);
|
|
323
|
+
if (build.data.attributes.usesNonExemptEncryption === void 0 || build.data.attributes.usesNonExemptEncryption === null)
|
|
324
|
+
await client.request(`/builds/${version.buildId}`, {
|
|
325
|
+
method: "PATCH",
|
|
326
|
+
body: JSON.stringify({
|
|
327
|
+
data: {
|
|
328
|
+
type: "builds",
|
|
329
|
+
id: version.buildId,
|
|
330
|
+
attributes: { usesNonExemptEncryption: encryption }
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async function submitVersions(client, appId, versions) {
|
|
337
|
+
const existing = await client.request(`/apps/${appId}/reviewSubmissions?limit=200`), ids = [];
|
|
338
|
+
for (const version of versions) {
|
|
339
|
+
const platform = applePlatform(version.platform), platformSubmissions = existing.data.filter((item) => item.attributes.platform === platform);
|
|
340
|
+
let submission;
|
|
341
|
+
for (const candidate of platformSubmissions) {
|
|
342
|
+
if (!((await client.request(`/reviewSubmissions/${candidate.id}/items?include=appStoreVersion&limit=200`)).included ?? []).some((item) => item.type === "appStoreVersions" && item.id === version.id))
|
|
343
|
+
continue;
|
|
344
|
+
submission = candidate;
|
|
345
|
+
if (candidate.attributes.state !== "READY_FOR_REVIEW") {
|
|
346
|
+
ids.push(candidate.id);
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (submission && submission.attributes.state !== "READY_FOR_REVIEW")
|
|
351
|
+
continue;
|
|
352
|
+
submission ??= platformSubmissions.find((item) => item.attributes.state === "READY_FOR_REVIEW");
|
|
353
|
+
if (!submission)
|
|
354
|
+
submission = (await client.request("/reviewSubmissions", {
|
|
355
|
+
method: "POST",
|
|
356
|
+
body: JSON.stringify({
|
|
357
|
+
data: {
|
|
358
|
+
type: "reviewSubmissions",
|
|
359
|
+
attributes: { platform },
|
|
360
|
+
relationships: { app: { data: { type: "apps", id: appId } } }
|
|
361
|
+
}
|
|
362
|
+
})
|
|
363
|
+
})).data;
|
|
364
|
+
if (!((await client.request(`/reviewSubmissions/${submission.id}/items?include=appStoreVersion&limit=200`)).included ?? []).some((item) => item.type === "appStoreVersions" && item.id === version.id))
|
|
365
|
+
await client.request("/reviewSubmissionItems", {
|
|
366
|
+
method: "POST",
|
|
367
|
+
body: JSON.stringify({
|
|
368
|
+
data: {
|
|
369
|
+
type: "reviewSubmissionItems",
|
|
370
|
+
relationships: {
|
|
371
|
+
reviewSubmission: { data: { type: "reviewSubmissions", id: submission.id } },
|
|
372
|
+
appStoreVersion: { data: { type: "appStoreVersions", id: version.id } }
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
})
|
|
376
|
+
});
|
|
377
|
+
await client.request(`/reviewSubmissions/${submission.id}`, {
|
|
378
|
+
method: "PATCH",
|
|
379
|
+
body: JSON.stringify({
|
|
380
|
+
data: { type: "reviewSubmissions", id: submission.id, attributes: { submitted: !0 } }
|
|
381
|
+
})
|
|
382
|
+
});
|
|
383
|
+
ids.push(submission.id);
|
|
384
|
+
}
|
|
385
|
+
return ids;
|
|
386
|
+
}
|
|
387
|
+
export async function prepareSafariAppStoreSubmission(client, config, appId, versions, options = {}) {
|
|
388
|
+
const store = config.safariAppStore;
|
|
389
|
+
if (!store)
|
|
390
|
+
throw Error("[browser-extension] Safari App Store automation needs safariAppStore in config/extension.ts");
|
|
391
|
+
const cwd = options.cwd ?? process.cwd(), timeoutMs = options.screenshotTimeoutMs ?? 600000, pollIntervalMs = options.screenshotPollIntervalMs ?? 5000;
|
|
392
|
+
await updateAppInfo(client, appId, store);
|
|
393
|
+
await ensurePrice(client, appId, store);
|
|
394
|
+
await ensureAvailability(client, appId, store.availableInNewTerritories ?? !0);
|
|
395
|
+
const results = [];
|
|
396
|
+
for (const version of versions) {
|
|
397
|
+
await updateVersion(client, version, store);
|
|
398
|
+
const localizationId = await ensureVersionLocalization(client, version.id, store);
|
|
399
|
+
await ensureReviewDetail(client, version.id, store);
|
|
400
|
+
for (const displayType of displayTypesForPlatform(version.platform)) {
|
|
401
|
+
const paths = store.screenshots[displayType] ?? [];
|
|
402
|
+
if (!paths.length)
|
|
403
|
+
continue;
|
|
404
|
+
const setId = await ensureScreenshotSet(client, localizationId, displayType);
|
|
405
|
+
await syncScreenshotSet(client, setId, paths, cwd, timeoutMs, pollIntervalMs);
|
|
406
|
+
}
|
|
407
|
+
results.push({ platform: version.platform, versionId: version.id, localizationId });
|
|
408
|
+
}
|
|
409
|
+
const shouldSubmit = options.submit ?? store.submitForReview ?? !1;
|
|
410
|
+
return {
|
|
411
|
+
appId,
|
|
412
|
+
versions: results,
|
|
413
|
+
reviewSubmissionIds: shouldSubmit ? await submitVersions(client, appId, versions) : []
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
export async function submitSafariAppStore(config, options) {
|
|
417
|
+
if (!config.safariBundleId)
|
|
418
|
+
throw Error("[browser-extension] Safari App Store submission needs safariBundleId in config/extension.ts");
|
|
419
|
+
const client = new AppStoreConnectClient(options), app = await client.findApp(config.safariBundleId);
|
|
420
|
+
if (!app)
|
|
421
|
+
throw Error("[browser-extension] the App Store Connect app record is missing");
|
|
422
|
+
const selectedPlatforms = options.platforms ?? config.safariPlatforms ?? ["macos"], availableVersions = await client.listAppStoreVersions(app.id), versions = [], versionStates = [];
|
|
423
|
+
for (const platform of selectedPlatforms) {
|
|
424
|
+
const match = availableVersions.find((item) => item.attributes.platform === applePlatform(platform) && item.attributes.versionString === options.version);
|
|
425
|
+
if (!match)
|
|
426
|
+
throw Error(`[browser-extension] Safari ${platform} App Store version ${options.version} does not exist`);
|
|
427
|
+
const build = await client.request(`/appStoreVersions/${match.id}/build`);
|
|
428
|
+
versions.push({ platform, id: match.id, buildId: build.data?.id });
|
|
429
|
+
versionStates.push(match.attributes.appStoreState);
|
|
430
|
+
}
|
|
431
|
+
if ((options.submit ?? config.safariAppStore?.submitForReview ?? !1) && versionStates.every((state) => !editableVersionStates.has(state))) {
|
|
432
|
+
const preparedVersions = await Promise.all(versions.map(async (version) => {
|
|
433
|
+
const localizations = await client.request(`/appStoreVersions/${version.id}/appStoreVersionLocalizations?limit=200`);
|
|
434
|
+
return { platform: version.platform, versionId: version.id, localizationId: localizations.data[0]?.id ?? "" };
|
|
435
|
+
}));
|
|
436
|
+
return {
|
|
437
|
+
appId: app.id,
|
|
438
|
+
versions: preparedVersions,
|
|
439
|
+
reviewSubmissionIds: await submitVersions(client, app.id, versions)
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return await prepareSafariAppStoreSubmission(client, config, app.id, versions, options);
|
|
443
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/safari.d.ts
CHANGED
|
@@ -107,6 +107,8 @@ export declare interface SafariPublishOptions extends SafariSyncOptions, AppStor
|
|
|
107
107
|
validateOnly?: boolean
|
|
108
108
|
build?: boolean
|
|
109
109
|
platforms?: SafariPlatform[]
|
|
110
|
+
processingTimeoutMs?: number
|
|
111
|
+
processingPollIntervalMs?: number
|
|
110
112
|
}
|
|
111
113
|
export declare interface SafariPublishedArtifact {
|
|
112
114
|
platform: SafariPlatform
|
|
@@ -118,4 +120,9 @@ export declare interface SafariPublishResult {
|
|
|
118
120
|
exportPath: string
|
|
119
121
|
buildNumber: string
|
|
120
122
|
artifacts: SafariPublishedArtifact[]
|
|
123
|
+
attachments: Array<{ platform: SafariPlatform, versionId: string, buildId: string, buildNumber: string }>
|
|
124
|
+
appStoreSubmission?: {
|
|
125
|
+
versions: Array<{ platform: SafariPlatform, versionId: string, localizationId: string }>
|
|
126
|
+
reviewSubmissionIds: string[]
|
|
127
|
+
}
|
|
121
128
|
}
|
package/dist/safari.js
CHANGED
|
@@ -327,12 +327,12 @@ export async function publishSafariApp(config, options) {
|
|
|
327
327
|
const platforms = resolveSafariPlatforms(config, options.platforms);
|
|
328
328
|
if (!platforms.length)
|
|
329
329
|
throw Error("[browser-extension] Safari publishing needs at least one platform");
|
|
330
|
-
const { provisionSafariApp } = await import("./app-store-connect")
|
|
331
|
-
if (!(await provisionSafariApp(config, {
|
|
330
|
+
const { AppStoreConnectClient, attachSafariBuilds, provisionSafariApp } = await import("./app-store-connect"), provisioned = await provisionSafariApp(config, {
|
|
332
331
|
...auth,
|
|
333
332
|
version: options.version,
|
|
334
333
|
platforms
|
|
335
|
-
})
|
|
334
|
+
});
|
|
335
|
+
if (!provisioned.appRecord.exists)
|
|
336
336
|
throw Error("[browser-extension] the App Store Connect app record is missing; create it once in App Store Connect before publishing");
|
|
337
337
|
if (options.build !== !1)
|
|
338
338
|
await buildExtension(config, { target: "safari", version: options.version, cwd });
|
|
@@ -372,6 +372,7 @@ export async function publishSafariApp(config, options) {
|
|
|
372
372
|
`MARKETING_VERSION=${options.version}`,
|
|
373
373
|
`CURRENT_PROJECT_VERSION=${buildNumber}`,
|
|
374
374
|
`DEVELOPMENT_TEAM=${teamId}`,
|
|
375
|
+
"INFOPLIST_KEY_ITSAppUsesNonExemptEncryption=NO",
|
|
375
376
|
...platform === "ios" ? ["CODE_SIGNING_ALLOWED=NO"] : [],
|
|
376
377
|
...platform === "macos" && config.safariAppCategory ? [`INFOPLIST_KEY_LSApplicationCategoryType=${config.safariAppCategory}`] : [],
|
|
377
378
|
...authArgs,
|
|
@@ -390,10 +391,20 @@ export async function publishSafariApp(config, options) {
|
|
|
390
391
|
], projectCwd, `Safari ${platform} export`);
|
|
391
392
|
artifacts.push({ platform, archivePath, exportPath });
|
|
392
393
|
}
|
|
394
|
+
const attachments = options.validateOnly ? [] : await attachSafariBuilds(new AppStoreConnectClient(auth), provisioned.appRecord.id, provisioned.appStoreVersions, buildNumber, {
|
|
395
|
+
timeoutMs: options.processingTimeoutMs,
|
|
396
|
+
pollIntervalMs: options.processingPollIntervalMs
|
|
397
|
+
}), appStoreSubmission = !options.validateOnly && config.safariAppStore ? await (await import("./app-store-submission")).prepareSafariAppStoreSubmission(new AppStoreConnectClient(auth), config, provisioned.appRecord.id, attachments.map((attachment) => ({
|
|
398
|
+
platform: attachment.platform,
|
|
399
|
+
id: attachment.versionId,
|
|
400
|
+
buildId: attachment.buildId
|
|
401
|
+
})), { cwd }) : void 0, firstArtifact = artifacts[0];
|
|
393
402
|
return {
|
|
394
|
-
archivePath:
|
|
395
|
-
exportPath:
|
|
403
|
+
archivePath: firstArtifact.archivePath,
|
|
404
|
+
exportPath: firstArtifact.exportPath,
|
|
396
405
|
buildNumber,
|
|
397
|
-
artifacts
|
|
406
|
+
artifacts,
|
|
407
|
+
attachments,
|
|
408
|
+
appStoreSubmission: appStoreSubmission ? { versions: appStoreSubmission.versions, reviewSubmissionIds: appStoreSubmission.reviewSubmissionIds } : void 0
|
|
398
409
|
};
|
|
399
410
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -68,6 +68,65 @@ export declare interface FirefoxAddonsConfig {
|
|
|
68
68
|
requiresPayment?: boolean
|
|
69
69
|
artifactsDir?: string
|
|
70
70
|
}
|
|
71
|
+
export declare interface SafariAppStoreReviewContact {
|
|
72
|
+
firstName: string
|
|
73
|
+
lastName: string
|
|
74
|
+
phone: string
|
|
75
|
+
email: string
|
|
76
|
+
notes?: string
|
|
77
|
+
}
|
|
78
|
+
/** App Store age-rating answers. Omitted answers default to no content. */
|
|
79
|
+
export declare interface SafariAppStoreAgeRating {
|
|
80
|
+
advertising?: boolean
|
|
81
|
+
alcoholTobaccoOrDrugUseOrReferences?: SafariAgeRatingFrequency
|
|
82
|
+
contests?: SafariAgeRatingFrequency
|
|
83
|
+
gambling?: boolean
|
|
84
|
+
gamblingSimulated?: SafariAgeRatingFrequency
|
|
85
|
+
gunsOrOtherWeapons?: SafariAgeRatingFrequency
|
|
86
|
+
healthOrWellnessTopics?: boolean
|
|
87
|
+
lootBox?: boolean
|
|
88
|
+
medicalOrTreatmentInformation?: SafariAgeRatingFrequency
|
|
89
|
+
messagingAndChat?: boolean
|
|
90
|
+
parentalControls?: boolean
|
|
91
|
+
profanityOrCrudeHumor?: SafariAgeRatingFrequency
|
|
92
|
+
ageAssurance?: boolean
|
|
93
|
+
sexualContentGraphicAndNudity?: SafariAgeRatingFrequency
|
|
94
|
+
sexualContentOrNudity?: SafariAgeRatingFrequency
|
|
95
|
+
socialMedia?: boolean
|
|
96
|
+
socialMediaAgeRestricted?: boolean
|
|
97
|
+
horrorOrFearThemes?: SafariAgeRatingFrequency
|
|
98
|
+
matureOrSuggestiveThemes?: SafariAgeRatingFrequency
|
|
99
|
+
unrestrictedWebAccess?: boolean
|
|
100
|
+
userGeneratedContent?: boolean
|
|
101
|
+
violenceCartoonOrFantasy?: SafariAgeRatingFrequency
|
|
102
|
+
violenceRealisticProlongedGraphicOrSadistic?: SafariAgeRatingFrequency
|
|
103
|
+
violenceRealistic?: SafariAgeRatingFrequency
|
|
104
|
+
}
|
|
105
|
+
/** Metadata and assets managed through the official App Store Connect API. */
|
|
106
|
+
export declare interface SafariAppStoreConfig {
|
|
107
|
+
locale?: string
|
|
108
|
+
subtitle: string
|
|
109
|
+
privacyPolicyUrl: string
|
|
110
|
+
description: string
|
|
111
|
+
keywords: string
|
|
112
|
+
supportUrl: string
|
|
113
|
+
marketingUrl?: string
|
|
114
|
+
promotionalText?: string
|
|
115
|
+
whatsNew?: string
|
|
116
|
+
copyright: string
|
|
117
|
+
primaryCategory: string
|
|
118
|
+
contentRightsDeclaration: 'DOES_NOT_USE_THIRD_PARTY_CONTENT' | 'USES_THIRD_PARTY_CONTENT'
|
|
119
|
+
price: string
|
|
120
|
+
baseTerritory?: string
|
|
121
|
+
releaseType?: SafariAppStoreReleaseType
|
|
122
|
+
availableInNewTerritories?: boolean
|
|
123
|
+
usesIdfa?: boolean
|
|
124
|
+
usesNonExemptEncryption?: boolean
|
|
125
|
+
ageRating?: SafariAppStoreAgeRating
|
|
126
|
+
reviewContact: SafariAppStoreReviewContact
|
|
127
|
+
screenshots: Partial<Record<SafariScreenshotDisplayType, string[]>>
|
|
128
|
+
submitForReview?: boolean
|
|
129
|
+
}
|
|
71
130
|
export declare interface ExtensionConfig {
|
|
72
131
|
name: string
|
|
73
132
|
description: string
|
|
@@ -78,6 +137,7 @@ export declare interface ExtensionConfig {
|
|
|
78
137
|
safariTeamId?: string
|
|
79
138
|
safariPlatforms?: SafariPlatform[]
|
|
80
139
|
safariAppCategory?: string
|
|
140
|
+
safariAppStore?: SafariAppStoreConfig
|
|
81
141
|
safariExclude?: string[]
|
|
82
142
|
targets?: ExtensionTarget[]
|
|
83
143
|
background?: string
|
|
@@ -110,3 +170,6 @@ export declare interface BuildOptions {
|
|
|
110
170
|
*/
|
|
111
171
|
export type ExtensionTarget = 'chrome' | 'firefox' | 'safari';
|
|
112
172
|
export type SafariPlatform = 'macos' | 'ios';
|
|
173
|
+
export type SafariScreenshotDisplayType = 'APP_DESKTOP' | 'APP_IPHONE_67' | 'APP_IPAD_PRO_3GEN_129';
|
|
174
|
+
export type SafariAppStoreReleaseType = 'MANUAL' | 'AFTER_APPROVAL' | 'SCHEDULED';
|
|
175
|
+
export type SafariAgeRatingFrequency = 'NONE' | 'INFREQUENT_OR_MILD' | 'FREQUENT_OR_INTENSE';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/browser-extension",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.146",
|
|
5
5
|
"description": "Build MV3 browser extensions (Chrome, Firefox, Safari) the Stacks way — manifest, content/background scripts, DNR rules, packaging, Safari container app, all config-driven.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|