@stacksjs/browser-extension 0.70.211 → 0.70.213

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 CHANGED
@@ -132,6 +132,11 @@ what `xcrun safari-web-extension-converter` generates, so day-to-day work
132
132
  never needs the converter; `--signed` builds need an Apple Development
133
133
  identity selected in Xcode.
134
134
 
135
+ The provider-neutral App Store Connect client, certificate/profile
136
+ provisioning, version reconciliation, and build attachment live in
137
+ `ts-pantry`. Stacks keeps only the Safari-specific Bundle ID mapping, Xcode
138
+ build orchestration, and listing configuration.
139
+
135
140
  When `safariAppStore` is configured, publishing also synchronizes the app
136
141
  description, category, content-rights declaration, pricing, age rating,
137
142
  territory availability, review contact, export compliance, and required
@@ -1,7 +1,26 @@
1
- import type { AppStoreConnectAuth } from './safari';
1
+ import { AppStoreConnectClient, provisionMacApp } from 'ts-pantry';
2
+ import type { AppStoreConnectClientOptions, AppStoreVersionPlatform, BundleIdPlatform } from 'ts-pantry';
2
3
  import type { ExtensionConfig, SafariPlatform } from './types';
3
- /** Generate the short-lived ES256 team token required by App Store Connect. */
4
- export declare function appStoreConnectToken(auth: Required<AppStoreConnectAuth>, now?: unknown): string;
4
+ export type {
5
+ AppAttributes,
6
+ AppStoreBuildAttachmentResult,
7
+ AppStoreBuildResult,
8
+ AppStoreConnectAuth,
9
+ AppStoreConnectClientOptions,
10
+ AppStoreConnectResource,
11
+ AppStoreVersionAttributes,
12
+ AppStoreVersionPlatform,
13
+ BuildAttributes,
14
+ BundleIdAttributes,
15
+ BundleIdCapabilityAttributes,
16
+ BundleIdPlatform,
17
+ CertificateAttributes,
18
+ MacAppProvisionOptions,
19
+ MacAppProvisionResult,
20
+ MacDistributionCertificateType,
21
+ PreReleaseVersionAttributes,
22
+ ProfileAttributes,
23
+ } from 'ts-pantry';
5
24
  /** Wait for the uploaded binaries to process, then select them for their App Store versions. */
6
25
  export declare function attachSafariBuilds(client: AppStoreConnectClient, appId: string, versions: SafariAppStoreVersionResult[], buildNumber: string, options?: AttachSafariBuildsOptions): Promise<SafariBuildAttachmentResult[]>;
7
26
  /**
@@ -10,51 +29,6 @@ export declare function attachSafariBuilds(client: AppStoreConnectClient, appId:
10
29
  * app record. Apple does not expose an official API for creating that record.
11
30
  */
12
31
  export declare function provisionSafariApp(config: ExtensionConfig, options?: SafariProvisionOptions): Promise<SafariProvisionResult>;
13
- export declare interface AppStoreConnectResource<T extends Record<string, unknown>> {
14
- type: string
15
- id: string
16
- attributes: T
17
- relationships?: Record<string, { data?: { type: string, id: string } | null }>
18
- }
19
- export declare interface BundleIdAttributes extends Record<string, unknown> {
20
- identifier: string
21
- name: string
22
- platform: BundleIdPlatform
23
- seedId?: string
24
- }
25
- export declare interface AppAttributes extends Record<string, unknown> {
26
- bundleId: string
27
- name: string
28
- primaryLocale: string
29
- sku: string
30
- }
31
- export declare interface AppStoreVersionAttributes extends Record<string, unknown> {
32
- platform: AppStoreVersionPlatform
33
- versionString: string
34
- appStoreState: string
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
- }
53
- export declare interface AppStoreConnectClientOptions extends AppStoreConnectAuth {
54
- baseUrl?: string
55
- fetch?: typeof globalThis.fetch
56
- now?: () => number
57
- }
58
32
  export declare interface SafariAppStoreVersionResult {
59
33
  platform: SafariPlatform
60
34
  version: string
@@ -87,20 +61,8 @@ export declare interface SafariProvisionResult {
87
61
  appRecord: { exists: boolean, id?: string }
88
62
  appStoreVersions: SafariAppStoreVersionResult[]
89
63
  }
90
- export type BundleIdPlatform = 'IOS' | 'MAC_OS' | 'UNIVERSAL';
91
- export type AppStoreVersionPlatform = 'IOS' | 'MAC_OS';
92
- /** Minimal official App Store Connect client for Safari provisioning checks. */
93
- export declare class AppStoreConnectClient {
94
- constructor(options?: AppStoreConnectClientOptions);
95
- request<T>(path: string, init?: RequestInit): Promise<T>;
96
- upload(url: string, init: RequestInit): Promise<void>;
97
- findBundleId(identifier: string): Promise<AppStoreConnectResource<BundleIdAttributes> | undefined>;
98
- registerBundleId(identifier: string, name: string, platform?: BundleIdPlatform): Promise<AppStoreConnectResource<BundleIdAttributes>>;
99
- ensureBundleId(identifier: string, name: string, options?: { checkOnly?: boolean, platform?: BundleIdPlatform }): Promise<{ bundleId?: AppStoreConnectResource<BundleIdAttributes>, created: boolean }>;
100
- findApp(bundleId: string): Promise<AppStoreConnectResource<AppAttributes> | undefined>;
101
- listAppStoreVersions(appId: string): Promise<Array<AppStoreConnectResource<AppStoreVersionAttributes>>>;
102
- createAppStoreVersion(appId: string, platform: AppStoreVersionPlatform, versionString: string): Promise<AppStoreConnectResource<AppStoreVersionAttributes>>;
103
- updateAppStoreVersion(versionId: string, versionString: string): Promise<AppStoreConnectResource<AppStoreVersionAttributes>>;
104
- listBuilds(appId: string, buildNumber: string): Promise<AppStoreBuildResult[]>;
105
- attachBuild(versionId: string, buildId: string): Promise<void>;
106
- }
64
+ export {
65
+ AppStoreConnectClient,
66
+ provisionMacApp,
67
+ };
68
+ export { appStoreConnectToken } from 'ts-pantry';
@@ -1,244 +1,40 @@
1
- import { createPrivateKey, sign } from "node:crypto";
2
- import { readFileSync } from "node:fs";
3
- import { resolveAppStoreConnectAuth } from "./safari";
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
- }
12
- function base64urlJson(value) {
13
- return Buffer.from(JSON.stringify(value)).toString("base64url");
14
- }
15
- export function appStoreConnectToken(auth, now = Math.floor(Date.now() / 1000)) {
16
- const header = { alg: "ES256", kid: auth.keyId, typ: "JWT" }, payload = {
17
- iss: auth.issuerId,
18
- iat: now,
19
- exp: now + 120,
20
- aud: "appstoreconnect-v1"
21
- }, input = `${base64urlJson(header)}.${base64urlJson(payload)}`;
22
- let key;
23
- try {
24
- key = createPrivateKey(readFileSync(auth.keyPath, "utf8"));
25
- } catch (error) {
26
- throw Error(`[browser-extension] App Store Connect API key could not be parsed: ${error instanceof Error ? error.message : String(error)}`);
27
- }
28
- const signature = sign("sha256", Buffer.from(input), { key, dsaEncoding: "ieee-p1363" });
29
- return `${input}.${signature.toString("base64url")}`;
30
- }
1
+ import {
2
+ AppStoreConnectClient,
3
+ ensureAppStoreVersions,
4
+ provisionMacApp,
5
+ waitForAppStoreBuilds
6
+ } from "ts-pantry";
31
7
 
32
- export class AppStoreConnectClient {
33
- auth;
34
- baseUrl;
35
- fetcher;
36
- now;
37
- constructor(options = {}) {
38
- this.auth = resolveAppStoreConnectAuth(options);
39
- this.baseUrl = (options.baseUrl ?? appStoreConnectBaseUrl).replace(/\/$/, "");
40
- this.fetcher = options.fetch ?? globalThis.fetch;
41
- this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
42
- }
43
- async request(path, init = {}) {
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, {
45
- ...init,
46
- headers: {
47
- Accept: "application/json",
48
- Authorization: `Bearer ${appStoreConnectToken(this.auth, this.now())}`,
49
- ...init.body ? { "Content-Type": "application/json" } : {},
50
- ...init.headers
51
- }
52
- });
53
- if (!response.ok) {
54
- const body = await response.json().catch(() => ({})), details = [...new Set(body.errors?.flatMap(apiErrorMessages) ?? [])].join("; ");
55
- throw Error(`[browser-extension] App Store Connect ${init.method ?? "GET"} ${path} failed (${response.status})${details ? `: ${details}` : ""}`);
56
- }
57
- if (response.status === 204)
58
- return;
59
- return await response.json();
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
- }
66
- async findBundleId(identifier) {
67
- const query = new URLSearchParams({ "filter[identifier]": identifier });
68
- return (await this.request(`/bundleIds?${query}`)).data.find((bundleId) => bundleId.attributes.identifier === identifier);
69
- }
70
- async registerBundleId(identifier, name, platform = "MAC_OS") {
71
- return (await this.request("/bundleIds", {
72
- method: "POST",
73
- body: JSON.stringify({
74
- data: {
75
- type: "bundleIds",
76
- attributes: { identifier, name, platform }
77
- }
78
- })
79
- })).data;
80
- }
81
- async ensureBundleId(identifier, name, options = {}) {
82
- const existing = await this.findBundleId(identifier);
83
- if (existing)
84
- return { bundleId: existing, created: !1 };
85
- if (options.checkOnly)
86
- return { created: !1 };
87
- return {
88
- bundleId: await this.registerBundleId(identifier, name, options.platform),
89
- created: !0
90
- };
91
- }
92
- async findApp(bundleId) {
93
- const query = new URLSearchParams({ "filter[bundleId]": bundleId });
94
- return (await this.request(`/apps?${query}`)).data.find((app) => app.attributes.bundleId === bundleId);
95
- }
96
- async listAppStoreVersions(appId) {
97
- return (await this.request(`/apps/${appId}/appStoreVersions?limit=200`)).data;
98
- }
99
- async createAppStoreVersion(appId, platform, versionString) {
100
- return (await this.request("/appStoreVersions", {
101
- method: "POST",
102
- body: JSON.stringify({
103
- data: {
104
- type: "appStoreVersions",
105
- attributes: { platform, versionString },
106
- relationships: { app: { data: { type: "apps", id: appId } } }
107
- }
108
- })
109
- })).data;
110
- }
111
- async updateAppStoreVersion(versionId, versionString) {
112
- return (await this.request(`/appStoreVersions/${versionId}`, {
113
- method: "PATCH",
114
- body: JSON.stringify({
115
- data: {
116
- type: "appStoreVersions",
117
- id: versionId,
118
- attributes: { versionString }
119
- }
120
- })
121
- })).data;
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
- }
151
- }
152
- const editableVersionStates = new Set([
153
- "PREPARE_FOR_SUBMISSION",
154
- "DEVELOPER_REJECTED",
155
- "REJECTED",
156
- "METADATA_REJECTED",
157
- "INVALID_BINARY"
158
- ]), releasedVersionStates = new Set([
159
- "READY_FOR_DISTRIBUTION",
160
- "READY_FOR_SALE",
161
- "DEVELOPER_REMOVED_FROM_SALE",
162
- "REMOVED_FROM_SALE"
163
- ]);
8
+ export {
9
+ AppStoreConnectClient,
10
+ provisionMacApp
11
+ };
12
+ export { appStoreConnectToken } from "ts-pantry";
164
13
  function appStorePlatform(platform) {
165
14
  return platform === "ios" ? "IOS" : "MAC_OS";
166
15
  }
167
16
  export async function attachSafariBuilds(client, appId, versions, buildNumber, options = {}) {
168
- const timeoutMs = options.timeoutMs ?? 1200000, pollIntervalMs = options.pollIntervalMs ?? 15000, deadline = Date.now() + timeoutMs, pending = new Map(versions.map((version) => [appStorePlatform(version.platform), version]));
169
- while (!0) {
170
- const builds = await client.listBuilds(appId, buildNumber), attachments = [];
171
- for (const [platform, version] of pending) {
172
- const build = builds.find((item) => item.platform === platform && item.version === version.version);
173
- if (!build)
174
- continue;
175
- if (build.processingState === "FAILED" || build.processingState === "INVALID")
176
- throw Error(`[browser-extension] Safari ${version.platform} build ${buildNumber} failed App Store Connect processing (${build.processingState})`);
177
- if (build.processingState !== "VALID")
178
- continue;
179
- attachments.push({
180
- platform: version.platform,
181
- versionId: version.id,
182
- buildId: build.id,
183
- buildNumber
184
- });
185
- }
186
- if (attachments.length === pending.size) {
187
- for (const attachment of attachments)
188
- await client.attachBuild(attachment.versionId, attachment.buildId);
189
- return attachments;
190
- }
191
- if (Date.now() >= deadline) {
192
- const waiting = [...pending.values()].map((item) => item.platform).join(", ");
193
- throw Error(`[browser-extension] timed out waiting for Safari ${waiting} build ${buildNumber} to finish App Store Connect processing`);
194
- }
195
- await Bun.sleep(pollIntervalMs);
196
- }
17
+ const pantryVersions = versions.map((version) => ({
18
+ platform: appStorePlatform(version.platform),
19
+ version: version.version,
20
+ id: version.id
21
+ }));
22
+ return (await waitForAppStoreBuilds(client, appId, pantryVersions, buildNumber, options)).map((attachment) => ({
23
+ platform: attachment.platform === "IOS" ? "ios" : "macos",
24
+ versionId: attachment.versionId,
25
+ buildId: attachment.buildId,
26
+ buildNumber: attachment.buildNumber
27
+ }));
197
28
  }
198
29
  async function ensureConfiguredAppStoreVersions(client, appId, platforms, version) {
199
- const versions = await client.listAppStoreVersions(appId), results = [];
200
- for (const platform of [...new Set(platforms)]) {
201
- const applePlatform = appStorePlatform(platform), exact = versions.find((item) => item.attributes.platform === applePlatform && item.attributes.versionString === version);
202
- if (exact) {
203
- const appStoreState = exact.attributes.appStoreState, status = editableVersionStates.has(appStoreState) ? "ready" : releasedVersionStates.has(appStoreState) ? "published" : "deferred";
204
- results.push({
205
- platform,
206
- version,
207
- created: !1,
208
- updated: !1,
209
- id: exact.id,
210
- status,
211
- appStoreState,
212
- ...status === "deferred" ? { reason: `Safari ${platform} version ${version} is already ${appStoreState}` } : {}
213
- });
214
- continue;
215
- }
216
- const editable = versions.find((item) => item.attributes.platform === applePlatform && editableVersionStates.has(item.attributes.appStoreState));
217
- if (editable) {
218
- const updated = await client.updateAppStoreVersion(editable.id, version);
219
- editable.attributes.versionString = version;
220
- results.push({ platform, version, created: !1, updated: !0, id: updated.id, status: "ready", appStoreState: updated.attributes.appStoreState });
221
- continue;
222
- }
223
- const blocking = versions.find((item) => item.attributes.platform === applePlatform && !releasedVersionStates.has(item.attributes.appStoreState));
224
- if (blocking) {
225
- results.push({
226
- platform,
227
- version,
228
- created: !1,
229
- updated: !1,
230
- id: blocking.id,
231
- status: "deferred",
232
- appStoreState: blocking.attributes.appStoreState,
233
- reason: `Safari ${platform} version ${version} is queued behind ${blocking.attributes.versionString} (${blocking.attributes.appStoreState})`
234
- });
235
- continue;
236
- }
237
- const created = await client.createAppStoreVersion(appId, applePlatform, version);
238
- versions.push(created);
239
- results.push({ platform, version, created: !0, updated: !1, id: created.id, status: "ready", appStoreState: created.attributes.appStoreState });
240
- }
241
- return results;
30
+ return (await ensureAppStoreVersions(client, appId, platforms.map((platform) => ({ platform: appStorePlatform(platform), version })))).map((result) => {
31
+ const platform = result.platform === "IOS" ? "ios" : "macos";
32
+ return {
33
+ ...result,
34
+ platform,
35
+ ...result.reason ? { reason: result.reason.replace(`${result.platform} version`, `Safari ${platform} version`) } : {}
36
+ };
37
+ });
242
38
  }
243
39
  export async function provisionSafariApp(config, options = {}) {
244
40
  if (!config.safariBundleId)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/browser-extension",
3
3
  "type": "module",
4
- "version": "0.70.211",
4
+ "version": "0.70.213",
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": [
@@ -52,6 +52,9 @@
52
52
  "dist",
53
53
  "safari-template"
54
54
  ],
55
+ "dependencies": {
56
+ "ts-pantry": "^0.11.0"
57
+ },
55
58
  "scripts": {
56
59
  "build": "bun build.ts",
57
60
  "typecheck": "bun tsc --noEmit",