@stacksjs/browser-extension 0.70.143 → 0.70.145

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
@@ -85,8 +85,9 @@ Set `safariBundleId` in the config (the appex gets `<safariBundleId>.Extension`)
85
85
  and `safariTeamId` to the Apple Developer team used for signing. Set
86
86
  `safariPlatforms: ['macos', 'ios']` for a universal extension; iOS covers both
87
87
  iPhone and iPad. Stacks generates the current Apple-supported universal Xcode
88
- project from the same built extension, archives both platforms, and uploads
89
- both to the same App Store Connect app record. Publishing
88
+ project from the same built extension, archives both platforms, uploads them
89
+ to the same App Store Connect app record, waits for Apple processing, and
90
+ selects each processed build on its matching App Store version. Publishing
90
91
  reads `APP_STORE_CONNECT_API_KEY_ID`, `APP_STORE_CONNECT_API_ISSUER_ID`, and
91
92
  `APP_STORE_CONNECT_API_KEY_PATH` from the environment. Run with
92
93
  `--validate-only` to exercise Apple's validation without uploading a build.
@@ -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
@@ -66,4 +96,6 @@ export declare class AppStoreConnectClient {
66
96
  listAppStoreVersions(appId: string): Promise<Array<AppStoreConnectResource<AppStoreVersionAttributes>>>;
67
97
  createAppStoreVersion(appId: string, platform: AppStoreVersionPlatform, versionString: string): Promise<AppStoreConnectResource<AppStoreVersionAttributes>>;
68
98
  updateAppStoreVersion(versionId: string, versionString: string): Promise<AppStoreConnectResource<AppStoreVersionAttributes>>;
99
+ listBuilds(appId: string, buildNumber: string): Promise<AppStoreBuildResult[]>;
100
+ attachBuild(versionId: string, buildId: string): Promise<void>;
69
101
  }
@@ -47,6 +47,8 @@ export class AppStoreConnectClient {
47
47
  const details = (await response.json().catch(() => ({}))).errors?.map((error) => error.detail ?? error.title ?? error.code).filter(Boolean).join("; ");
48
48
  throw Error(`[browser-extension] App Store Connect ${init.method ?? "GET"} ${path} failed (${response.status})${details ? `: ${details}` : ""}`);
49
49
  }
50
+ if (response.status === 204)
51
+ return;
50
52
  return await response.json();
51
53
  }
52
54
  async findBundleId(identifier) {
@@ -106,6 +108,34 @@ export class AppStoreConnectClient {
106
108
  })
107
109
  })).data;
108
110
  }
111
+ async listBuilds(appId, buildNumber) {
112
+ const query = new URLSearchParams({
113
+ "filter[app]": appId,
114
+ "filter[version]": buildNumber,
115
+ include: "preReleaseVersion",
116
+ limit: "20",
117
+ "fields[builds]": "version,uploadedDate,expired,processingState,preReleaseVersion",
118
+ "fields[preReleaseVersions]": "version,platform"
119
+ }), response = await this.request(`/builds?${query}`), preReleaseVersions = new Map((response.included ?? []).filter((item) => item.type === "preReleaseVersions").map((item) => [item.id, item.attributes]));
120
+ return response.data.flatMap((build) => {
121
+ const preReleaseVersionId = build.relationships?.preReleaseVersion?.data?.id, preReleaseVersion = preReleaseVersionId ? preReleaseVersions.get(preReleaseVersionId) : void 0;
122
+ if (!preReleaseVersion)
123
+ return [];
124
+ return [{
125
+ id: build.id,
126
+ buildNumber: build.attributes.version,
127
+ version: preReleaseVersion.version,
128
+ platform: preReleaseVersion.platform,
129
+ processingState: build.attributes.processingState
130
+ }];
131
+ });
132
+ }
133
+ async attachBuild(versionId, buildId) {
134
+ await this.request(`/appStoreVersions/${versionId}/relationships/build`, {
135
+ method: "PATCH",
136
+ body: JSON.stringify({ data: { type: "builds", id: buildId } })
137
+ });
138
+ }
109
139
  }
110
140
  const editableVersionStates = new Set([
111
141
  "PREPARE_FOR_SUBMISSION",
@@ -116,6 +146,37 @@ const editableVersionStates = new Set([
116
146
  function appStorePlatform(platform) {
117
147
  return platform === "ios" ? "IOS" : "MAC_OS";
118
148
  }
149
+ export async function attachSafariBuilds(client, appId, versions, buildNumber, options = {}) {
150
+ const timeoutMs = options.timeoutMs ?? 1200000, pollIntervalMs = options.pollIntervalMs ?? 15000, deadline = Date.now() + timeoutMs, pending = new Map(versions.map((version) => [appStorePlatform(version.platform), version]));
151
+ while (!0) {
152
+ const builds = await client.listBuilds(appId, buildNumber), attachments = [];
153
+ for (const [platform, version] of pending) {
154
+ const build = builds.find((item) => item.platform === platform && item.version === version.version);
155
+ if (!build)
156
+ continue;
157
+ if (build.processingState === "FAILED" || build.processingState === "INVALID")
158
+ throw Error(`[browser-extension] Safari ${version.platform} build ${buildNumber} failed App Store Connect processing (${build.processingState})`);
159
+ if (build.processingState !== "VALID")
160
+ continue;
161
+ attachments.push({
162
+ platform: version.platform,
163
+ versionId: version.id,
164
+ buildId: build.id,
165
+ buildNumber
166
+ });
167
+ }
168
+ if (attachments.length === pending.size) {
169
+ for (const attachment of attachments)
170
+ await client.attachBuild(attachment.versionId, attachment.buildId);
171
+ return attachments;
172
+ }
173
+ if (Date.now() >= deadline) {
174
+ const waiting = [...pending.values()].map((item) => item.platform).join(", ");
175
+ throw Error(`[browser-extension] timed out waiting for Safari ${waiting} build ${buildNumber} to finish App Store Connect processing`);
176
+ }
177
+ await Bun.sleep(pollIntervalMs);
178
+ }
179
+ }
119
180
  async function ensureConfiguredAppStoreVersions(client, appId, platforms, version) {
120
181
  const versions = await client.listAppStoreVersions(appId), results = [];
121
182
  for (const platform of [...new Set(platforms)]) {
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionConfig, FirefoxAddonsConfig } from './types';
2
+ export declare function firefoxSignArgs(options: FirefoxSignArgsOptions): string[];
2
3
  /** Metadata accepted by AMO v5 when web-ext creates the first listed version. */
3
4
  export declare function firefoxListingMetadata(config: ExtensionConfig, store: FirefoxAddonsConfig): Record<string, unknown> | undefined;
4
5
  /** Resolve web-ext from PATH or from this package's declared dependency. */
@@ -22,3 +23,11 @@ export declare interface FirefoxPublishResult {
22
23
  artifacts: string[]
23
24
  channel: 'listed' | 'unlisted'
24
25
  }
26
+ declare interface FirefoxSignArgsOptions {
27
+ executable: string
28
+ sourceDir: string
29
+ artifactsDir: string
30
+ channel: 'listed' | 'unlisted'
31
+ timeout: number
32
+ approvalTimeout: number
33
+ }
@@ -4,6 +4,23 @@ import { createRequire } from "node:module";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { buildExtension, resolveOutdir } from "./build";
7
+ export function firefoxSignArgs(options) {
8
+ return [
9
+ options.executable,
10
+ "sign",
11
+ "--source-dir",
12
+ options.sourceDir,
13
+ "--artifacts-dir",
14
+ options.artifactsDir,
15
+ "--channel",
16
+ options.channel,
17
+ "--timeout",
18
+ String(options.timeout),
19
+ "--approval-timeout",
20
+ String(options.approvalTimeout),
21
+ "--no-input"
22
+ ];
23
+ }
7
24
  function resolveFirefoxAuth(options) {
8
25
  const issuer = options.issuer ?? process.env.AMO_JWT_ISSUER ?? process.env.WEB_EXT_API_KEY, secret = options.secret ?? process.env.AMO_JWT_SECRET ?? process.env.WEB_EXT_API_SECRET, missing = [!issuer && "AMO_JWT_ISSUER", !secret && "AMO_JWT_SECRET"].filter(Boolean);
9
26
  if (missing.length)
@@ -50,22 +67,14 @@ export async function publishFirefoxExtension(config, options) {
50
67
  await mkdir(artifactsDir, { recursive: !0 });
51
68
  const before = new Set(existsSync(artifactsDir) ? readdirSync(artifactsDir) : []), tempDir = await mkdtemp(join(tmpdir(), "stacks-firefox-publish-"));
52
69
  try {
53
- const args = [
70
+ const args = firefoxSignArgs({
54
71
  executable,
55
- "sign",
56
- "--source-dir",
57
72
  sourceDir,
58
- "--artifacts-dir",
59
73
  artifactsDir,
60
- "--channel",
61
- store.channel ?? "listed",
62
- "--timeout",
63
- String(options.timeout ?? 300000),
64
- "--approval-timeout",
65
- String(options.approvalTimeout ?? 0),
66
- "--no-input",
67
- "--boring"
68
- ], metadata = firefoxListingMetadata(config, store);
74
+ channel: store.channel ?? "listed",
75
+ timeout: options.timeout ?? 300000,
76
+ approvalTimeout: options.approvalTimeout ?? 0
77
+ }), metadata = firefoxListingMetadata(config, store);
69
78
  if (metadata) {
70
79
  const metadataPath = join(tempDir, "amo-metadata.json");
71
80
  await Bun.write(metadataPath, `${JSON.stringify(metadata, null, 2)}
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,5 @@ 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 }>
121
124
  }
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
- })).appRecord.exists)
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 });
@@ -390,10 +390,15 @@ export async function publishSafariApp(config, options) {
390
390
  ], projectCwd, `Safari ${platform} export`);
391
391
  artifacts.push({ platform, archivePath, exportPath });
392
392
  }
393
+ const attachments = options.validateOnly ? [] : await attachSafariBuilds(new AppStoreConnectClient(auth), provisioned.appRecord.id, provisioned.appStoreVersions, buildNumber, {
394
+ timeoutMs: options.processingTimeoutMs,
395
+ pollIntervalMs: options.processingPollIntervalMs
396
+ }), firstArtifact = artifacts[0];
393
397
  return {
394
- archivePath: artifacts[0].archivePath,
395
- exportPath: artifacts[0].exportPath,
398
+ archivePath: firstArtifact.archivePath,
399
+ exportPath: firstArtifact.exportPath,
396
400
  buildNumber,
397
- artifacts
401
+ artifacts,
402
+ attachments
398
403
  };
399
404
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/browser-extension",
3
3
  "type": "module",
4
- "version": "0.70.143",
4
+ "version": "0.70.145",
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": [