@stacksjs/browser-extension 0.70.119 → 0.70.121

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
@@ -17,6 +17,29 @@ buddy extension:build --target safari # → dist-safari/ (browser.* namespace,
17
17
  buddy extension:package # build + zip store-ready archives
18
18
  ```
19
19
 
20
+ ## Store publishing
21
+
22
+ Stacks owns the store-specific upload clients as well as the builds. Chrome
23
+ uses Web Store API v2 with a service account; Firefox uses Mozilla's official
24
+ `web-ext` client and AMO v5 API; Safari uses App Store Connect and Xcode.
25
+
26
+ ```sh
27
+ buddy extension:chrome:status
28
+ buddy extension:chrome:publish # build, zip, upload, submit for review
29
+ buddy extension:chrome:publish --upload-only
30
+ buddy extension:firefox:publish # build, submit, sign through AMO
31
+ buddy extension:safari:provision # register Bundle IDs + check app record
32
+ buddy extension:safari:publish
33
+ ```
34
+
35
+ Chrome reads `CHROME_WEB_STORE_SERVICE_ACCOUNT_PATH` (or
36
+ `GOOGLE_APPLICATION_CREDENTIALS`) and the configured
37
+ `chromeWebStore.publisherId`/`itemId`. The API only updates existing items, so
38
+ create the initial Developer Dashboard item once and link the service-account
39
+ email to the publisher account. Firefox reads `AMO_JWT_ISSUER` and
40
+ `AMO_JWT_SECRET`; `web-ext` can create the initial listing when
41
+ `firefoxAddons.license` and `firefoxAddons.categories` are configured.
42
+
20
43
  ## Safari
21
44
 
22
45
  Safari Web Extensions ship inside a macOS app, so the safari target has two
@@ -28,6 +51,7 @@ Safari with MAIN-world content scripts + `match_about_blank`).
28
51
 
29
52
  ```sh
30
53
  buddy extension:safari:init # scaffold the Xcode container app into safari/
54
+ buddy extension:safari:provision # register both Bundle IDs + check the app record
31
55
  buddy extension:safari:app # build + sync into the appex + xcodebuild
32
56
  buddy extension:safari:publish # signed archive + App Store Connect upload
33
57
  ```
@@ -53,6 +77,8 @@ export default defineExtension({
53
77
  name: 'My Extension',
54
78
  description: 'Does something useful.',
55
79
  geckoId: 'my-ext@example.com', // required to ship on Firefox
80
+ chromeWebStore: { publisherId: 'publisher-id', itemId: 'extension-id' },
81
+ firefoxAddons: { license: 'MIT', categories: ['privacy-security'] },
56
82
  safariBundleId: 'com.example.MyExtension', // Safari container app bundle id
57
83
  targets: ['chrome', 'firefox'],
58
84
 
@@ -108,6 +134,8 @@ import {
108
134
  buildExtension,
109
135
  buildAllTargets,
110
136
  packageExtension,
137
+ publishChromeExtension,
138
+ publishFirefoxExtension,
111
139
  generateManifest,
112
140
  rewriteBrowserNamespace,
113
141
  scaffoldSafariApp,
@@ -0,0 +1,50 @@
1
+ import type { AppStoreConnectAuth } from './safari';
2
+ import type { ExtensionConfig } 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;
5
+ /**
6
+ * Register the explicit container and extension Bundle IDs required by a
7
+ * Safari Web Extension, then check for the manually-created App Store Connect
8
+ * app record. Apple does not expose an official API for creating that record.
9
+ */
10
+ export declare function provisionSafariApp(config: ExtensionConfig, options?: SafariProvisionOptions): Promise<SafariProvisionResult>;
11
+ export declare interface AppStoreConnectResource<T extends Record<string, unknown>> {
12
+ type: string
13
+ id: string
14
+ attributes: T
15
+ }
16
+ export declare interface BundleIdAttributes extends Record<string, unknown> {
17
+ identifier: string
18
+ name: string
19
+ platform: BundleIdPlatform
20
+ seedId?: string
21
+ }
22
+ export declare interface AppAttributes extends Record<string, unknown> {
23
+ bundleId: string
24
+ name: string
25
+ primaryLocale: string
26
+ sku: string
27
+ }
28
+ export declare interface AppStoreConnectClientOptions extends AppStoreConnectAuth {
29
+ baseUrl?: string
30
+ fetch?: typeof globalThis.fetch
31
+ now?: () => number
32
+ }
33
+ export declare interface SafariProvisionOptions extends AppStoreConnectClientOptions {
34
+ checkOnly?: boolean
35
+ platform?: BundleIdPlatform
36
+ }
37
+ export declare interface SafariProvisionResult {
38
+ container: { identifier: string, exists: boolean, created: boolean }
39
+ extension: { identifier: string, exists: boolean, created: boolean }
40
+ appRecord: { exists: boolean, id?: string }
41
+ }
42
+ export type BundleIdPlatform = 'IOS' | 'MAC_OS' | 'UNIVERSAL';
43
+ /** Minimal official App Store Connect client for Safari provisioning checks. */
44
+ export declare class AppStoreConnectClient {
45
+ constructor(options?: AppStoreConnectClientOptions);
46
+ findBundleId(identifier: string): Promise<AppStoreConnectResource<BundleIdAttributes> | undefined>;
47
+ registerBundleId(identifier: string, name: string, platform?: BundleIdPlatform): Promise<AppStoreConnectResource<BundleIdAttributes>>;
48
+ ensureBundleId(identifier: string, name: string, options?: { checkOnly?: boolean, platform?: BundleIdPlatform }): Promise<{ bundleId?: AppStoreConnectResource<BundleIdAttributes>, created: boolean }>;
49
+ findApp(bundleId: string): Promise<AppStoreConnectResource<AppAttributes> | undefined>;
50
+ }
@@ -0,0 +1,92 @@
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 base64urlJson(value) {
6
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
7
+ }
8
+ export function appStoreConnectToken(auth, now = Math.floor(Date.now() / 1000)) {
9
+ const header = { alg: "ES256", kid: auth.keyId, typ: "JWT" }, payload = {
10
+ iss: auth.issuerId,
11
+ iat: now,
12
+ exp: now + 120,
13
+ aud: "appstoreconnect-v1"
14
+ }, input = `${base64urlJson(header)}.${base64urlJson(payload)}`;
15
+ let key;
16
+ try {
17
+ key = createPrivateKey(readFileSync(auth.keyPath, "utf8"));
18
+ } catch (error) {
19
+ throw Error(`[browser-extension] App Store Connect API key could not be parsed: ${error instanceof Error ? error.message : String(error)}`);
20
+ }
21
+ const signature = sign("sha256", Buffer.from(input), { key, dsaEncoding: "ieee-p1363" });
22
+ return `${input}.${signature.toString("base64url")}`;
23
+ }
24
+
25
+ export class AppStoreConnectClient {
26
+ auth;
27
+ baseUrl;
28
+ fetcher;
29
+ now;
30
+ constructor(options = {}) {
31
+ this.auth = resolveAppStoreConnectAuth(options);
32
+ this.baseUrl = (options.baseUrl ?? appStoreConnectBaseUrl).replace(/\/$/, "");
33
+ this.fetcher = options.fetch ?? globalThis.fetch;
34
+ this.now = options.now ?? (() => Math.floor(Date.now() / 1000));
35
+ }
36
+ async request(path, init = {}) {
37
+ const response = await this.fetcher(`${this.baseUrl}${path}`, {
38
+ ...init,
39
+ headers: {
40
+ Accept: "application/json",
41
+ Authorization: `Bearer ${appStoreConnectToken(this.auth, this.now())}`,
42
+ ...init.body ? { "Content-Type": "application/json" } : {},
43
+ ...init.headers
44
+ }
45
+ });
46
+ if (!response.ok) {
47
+ const details = (await response.json().catch(() => ({}))).errors?.map((error) => error.detail ?? error.title ?? error.code).filter(Boolean).join("; ");
48
+ throw Error(`[browser-extension] App Store Connect ${init.method ?? "GET"} ${path} failed (${response.status})${details ? `: ${details}` : ""}`);
49
+ }
50
+ return await response.json();
51
+ }
52
+ async findBundleId(identifier) {
53
+ const query = new URLSearchParams({ "filter[identifier]": identifier });
54
+ return (await this.request(`/bundleIds?${query}`)).data.find((bundleId) => bundleId.attributes.identifier === identifier);
55
+ }
56
+ async registerBundleId(identifier, name, platform = "MAC_OS") {
57
+ return (await this.request("/bundleIds", {
58
+ method: "POST",
59
+ body: JSON.stringify({
60
+ data: {
61
+ type: "bundleIds",
62
+ attributes: { identifier, name, platform }
63
+ }
64
+ })
65
+ })).data;
66
+ }
67
+ async ensureBundleId(identifier, name, options = {}) {
68
+ const existing = await this.findBundleId(identifier);
69
+ if (existing)
70
+ return { bundleId: existing, created: !1 };
71
+ if (options.checkOnly)
72
+ return { created: !1 };
73
+ return {
74
+ bundleId: await this.registerBundleId(identifier, name, options.platform),
75
+ created: !0
76
+ };
77
+ }
78
+ async findApp(bundleId) {
79
+ const query = new URLSearchParams({ "filter[bundleId]": bundleId });
80
+ return (await this.request(`/apps?${query}`)).data.find((app) => app.attributes.bundleId === bundleId);
81
+ }
82
+ }
83
+ export async function provisionSafariApp(config, options = {}) {
84
+ if (!config.safariBundleId)
85
+ throw Error("[browser-extension] Safari provisioning needs safariBundleId in config/extension.ts");
86
+ const client = new AppStoreConnectClient(options), identifier = config.safariBundleId, extensionIdentifier = `${identifier}.Extension`, container = await client.ensureBundleId(identifier, config.name, options), extension = await client.ensureBundleId(extensionIdentifier, `${config.name} Safari Extension`, options), app = await client.findApp(identifier);
87
+ return {
88
+ container: { identifier, exists: Boolean(container.bundleId), created: container.created },
89
+ extension: { identifier: extensionIdentifier, exists: Boolean(extension.bundleId), created: extension.created },
90
+ appRecord: { exists: Boolean(app), id: app?.id }
91
+ };
92
+ }
@@ -0,0 +1,55 @@
1
+ import type { ChromeWebStoreConfig, ExtensionConfig } from './types';
2
+ /** Build the RS256 assertion Google exchanges for a short-lived OAuth token. */
3
+ export declare function chromeWebStoreServiceAccountAssertion(serviceAccount: GoogleServiceAccount, now?: unknown): string;
4
+ /** Build, package, upload, and optionally submit an existing Chrome Web Store item. */
5
+ export declare function publishChromeExtension(config: ExtensionConfig, options: ChromeWebStorePublishOptions): Promise<{ packagePath: string, upload: ChromeUploadResult, publish?: ChromePublishResult }>;
6
+ export declare interface GoogleServiceAccount {
7
+ client_email: string
8
+ private_key: string
9
+ token_uri?: string
10
+ }
11
+ export declare interface ChromeWebStoreAuth {
12
+ accessToken?: string
13
+ serviceAccountPath?: string
14
+ }
15
+ export declare interface ChromeWebStoreClientOptions extends ChromeWebStoreAuth {
16
+ baseUrl?: string
17
+ fetch?: typeof globalThis.fetch
18
+ now?: () => number
19
+ wait?: (milliseconds: number) => Promise<void>
20
+ }
21
+ export declare interface ChromeWebStoreStatus {
22
+ name: string
23
+ itemId: string
24
+ lastAsyncUploadState?: 'UPLOAD_STATE_UNSPECIFIED' | 'SUCCEEDED' | 'IN_PROGRESS' | 'FAILED' | 'NOT_FOUND'
25
+ publishedItemRevisionStatus?: { state: string, distributionChannels?: Array<{ deployPercentage: number, crxVersion: string }> }
26
+ submittedItemRevisionStatus?: { state: string, distributionChannels?: Array<{ deployPercentage: number, crxVersion: string }> }
27
+ takenDown?: boolean
28
+ warned?: boolean
29
+ }
30
+ export declare interface ChromeUploadResult {
31
+ name: string
32
+ itemId: string
33
+ crxVersion?: string
34
+ uploadState: 'UPLOAD_STATE_UNSPECIFIED' | 'SUCCEEDED' | 'IN_PROGRESS' | 'FAILED' | 'NOT_FOUND'
35
+ }
36
+ export declare interface ChromePublishResult {
37
+ name: string
38
+ itemId: string
39
+ state: string
40
+ warningInfo?: { warnings?: Array<{ reason: string, description: string }> }
41
+ }
42
+ export declare interface ChromeWebStorePublishOptions extends ChromeWebStoreClientOptions {
43
+ version: string
44
+ cwd?: string
45
+ packagePath?: string
46
+ uploadOnly?: boolean
47
+ blockOnWarnings?: boolean
48
+ }
49
+ export declare class ChromeWebStoreClient {
50
+ constructor(options?: ChromeWebStoreClientOptions);
51
+ fetchStatus(config: ChromeWebStoreConfig): Promise<ChromeWebStoreStatus>;
52
+ upload(config: ChromeWebStoreConfig, packagePath: string): Promise<ChromeUploadResult>;
53
+ publish(config: ChromeWebStoreConfig, options?: { blockOnWarnings?: boolean }): Promise<ChromePublishResult>;
54
+ waitForUpload(config: ChromeWebStoreConfig, maxAttempts?: number): Promise<ChromeWebStoreStatus>;
55
+ }
@@ -0,0 +1,137 @@
1
+ import { createPrivateKey, sign } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { packageExtension } from "./package";
5
+ const chromeWebStoreScope = "https://www.googleapis.com/auth/chromewebstore", chromeWebStoreBaseUrl = "https://chromewebstore.googleapis.com";
6
+ function base64urlJson(value) {
7
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
8
+ }
9
+ export function chromeWebStoreServiceAccountAssertion(serviceAccount, now = Math.floor(Date.now() / 1000)) {
10
+ const tokenUri = serviceAccount.token_uri ?? "https://oauth2.googleapis.com/token", input = `${base64urlJson({ alg: "RS256", typ: "JWT" })}.${base64urlJson({
11
+ iss: serviceAccount.client_email,
12
+ scope: chromeWebStoreScope,
13
+ aud: tokenUri,
14
+ iat: now,
15
+ exp: now + 3600
16
+ })}`;
17
+ let key;
18
+ try {
19
+ key = createPrivateKey(serviceAccount.private_key);
20
+ } catch (error) {
21
+ throw Error(`[browser-extension] Chrome Web Store service-account key could not be parsed: ${error instanceof Error ? error.message : String(error)}`);
22
+ }
23
+ return `${input}.${sign("sha256", Buffer.from(input), key).toString("base64url")}`;
24
+ }
25
+ async function resolveChromeAccessToken(options) {
26
+ const supplied = options.accessToken ?? process.env.CHROME_WEB_STORE_ACCESS_TOKEN;
27
+ if (supplied)
28
+ return supplied;
29
+ const configuredPath = options.serviceAccountPath ?? process.env.CHROME_WEB_STORE_SERVICE_ACCOUNT_PATH ?? process.env.GOOGLE_APPLICATION_CREDENTIALS;
30
+ if (!configuredPath)
31
+ throw Error("[browser-extension] missing Chrome Web Store credentials: CHROME_WEB_STORE_SERVICE_ACCOUNT_PATH or CHROME_WEB_STORE_ACCESS_TOKEN");
32
+ const keyPath = resolve(configuredPath);
33
+ if (!existsSync(keyPath))
34
+ throw Error(`[browser-extension] Chrome Web Store service-account key not found: ${keyPath}`);
35
+ let serviceAccount;
36
+ try {
37
+ serviceAccount = JSON.parse(readFileSync(keyPath, "utf8"));
38
+ } catch (error) {
39
+ throw Error(`[browser-extension] Chrome Web Store service-account JSON could not be parsed: ${error instanceof Error ? error.message : String(error)}`);
40
+ }
41
+ if (!serviceAccount.client_email || !serviceAccount.private_key)
42
+ throw Error("[browser-extension] Chrome Web Store service-account JSON needs client_email and private_key");
43
+ const tokenUri = serviceAccount.token_uri ?? "https://oauth2.googleapis.com/token", response = await (options.fetch ?? globalThis.fetch)(tokenUri, {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
46
+ body: new URLSearchParams({
47
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
48
+ assertion: chromeWebStoreServiceAccountAssertion(serviceAccount, options.now?.())
49
+ })
50
+ }), body = await response.json().catch(() => ({}));
51
+ if (!response.ok || !body.access_token)
52
+ throw Error(`[browser-extension] Google OAuth token exchange failed (${response.status})${body.error_description ? `: ${body.error_description}` : ""}`);
53
+ return body.access_token;
54
+ }
55
+ function storeItemName(config) {
56
+ return `publishers/${encodeURIComponent(config.publisherId)}/items/${encodeURIComponent(config.itemId)}`;
57
+ }
58
+
59
+ export class ChromeWebStoreClient {
60
+ options;
61
+ baseUrl;
62
+ accessToken;
63
+ constructor(options = {}) {
64
+ this.options = options;
65
+ this.baseUrl = (options.baseUrl ?? chromeWebStoreBaseUrl).replace(/\/$/, "");
66
+ }
67
+ async request(path, init = {}) {
68
+ this.accessToken ??= resolveChromeAccessToken(this.options);
69
+ const response = await (this.options.fetch ?? globalThis.fetch)(`${this.baseUrl}${path}`, {
70
+ ...init,
71
+ headers: {
72
+ Authorization: `Bearer ${await this.accessToken}`,
73
+ ...init.headers
74
+ }
75
+ });
76
+ if (!response.ok) {
77
+ const body = await response.json().catch(() => ({})), detail = body.error?.message ?? body.message;
78
+ throw Error(`[browser-extension] Chrome Web Store ${init.method ?? "GET"} ${path} failed (${response.status})${detail ? `: ${detail}` : ""}`);
79
+ }
80
+ return await response.json();
81
+ }
82
+ fetchStatus(config) {
83
+ return this.request(`/v2/${storeItemName(config)}:fetchStatus`);
84
+ }
85
+ async upload(config, packagePath) {
86
+ const file = Bun.file(resolve(packagePath));
87
+ if (!await file.exists())
88
+ throw Error(`[browser-extension] Chrome package not found: ${resolve(packagePath)}`);
89
+ return await this.request(`/upload/v2/${storeItemName(config)}:upload`, {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/zip" },
92
+ body: new Uint8Array(await file.arrayBuffer())
93
+ });
94
+ }
95
+ publish(config, options = {}) {
96
+ if (config.deployPercentage !== void 0 && (!Number.isInteger(config.deployPercentage) || config.deployPercentage < 0 || config.deployPercentage > 100))
97
+ throw Error("[browser-extension] chromeWebStore.deployPercentage must be an integer from 0 to 100");
98
+ const body = {
99
+ publishType: config.publishType ?? "DEFAULT_PUBLISH",
100
+ skipReview: config.skipReview ?? !1,
101
+ blockOnWarnings: options.blockOnWarnings ?? !0
102
+ };
103
+ if (config.deployPercentage !== void 0)
104
+ body.deployInfos = [{ deployPercentage: config.deployPercentage }];
105
+ return this.request(`/v2/${storeItemName(config)}:publish`, {
106
+ method: "POST",
107
+ headers: { "Content-Type": "application/json" },
108
+ body: JSON.stringify(body)
109
+ });
110
+ }
111
+ async waitForUpload(config, maxAttempts = 30) {
112
+ for (let attempt = 0;attempt < maxAttempts; attempt += 1) {
113
+ const status = await this.fetchStatus(config);
114
+ if (status.lastAsyncUploadState !== "IN_PROGRESS") {
115
+ if (status.lastAsyncUploadState === "FAILED")
116
+ throw Error("[browser-extension] Chrome Web Store package processing failed");
117
+ return status;
118
+ }
119
+ await (this.options.wait ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))))(2000);
120
+ }
121
+ throw Error("[browser-extension] Chrome Web Store package processing timed out");
122
+ }
123
+ }
124
+ export async function publishChromeExtension(config, options) {
125
+ if (!config.chromeWebStore)
126
+ throw Error("[browser-extension] Chrome publishing needs chromeWebStore.publisherId and chromeWebStore.itemId in config/extension.ts");
127
+ const cwd = options.cwd ?? process.cwd(), packagePath = options.packagePath ?? await packageExtension(config, { target: "chrome", version: options.version, cwd }), client = new ChromeWebStoreClient(options), upload = await client.upload(config.chromeWebStore, packagePath);
128
+ if (upload.uploadState === "IN_PROGRESS")
129
+ await client.waitForUpload(config.chromeWebStore);
130
+ else if (upload.uploadState !== "SUCCEEDED")
131
+ throw Error(`[browser-extension] Chrome Web Store upload failed with state ${upload.uploadState}`);
132
+ return {
133
+ packagePath,
134
+ upload,
135
+ publish: options.uploadOnly ? void 0 : await client.publish(config.chromeWebStore, { blockOnWarnings: options.blockOnWarnings })
136
+ };
137
+ }
@@ -0,0 +1,22 @@
1
+ import type { ExtensionConfig, FirefoxAddonsConfig } from './types';
2
+ /** Metadata accepted by AMO v5 when web-ext creates the first listed version. */
3
+ export declare function firefoxListingMetadata(config: ExtensionConfig, store: FirefoxAddonsConfig): Record<string, unknown> | undefined;
4
+ /** Build and submit a Firefox extension through Mozilla's official web-ext/AMO v5 flow. */
5
+ export declare function publishFirefoxExtension(config: ExtensionConfig, options: FirefoxPublishOptions): Promise<FirefoxPublishResult>;
6
+ export declare interface FirefoxAddonsAuth {
7
+ issuer?: string
8
+ secret?: string
9
+ }
10
+ export declare interface FirefoxPublishOptions extends FirefoxAddonsAuth {
11
+ version: string
12
+ cwd?: string
13
+ build?: boolean
14
+ sourceCodePath?: string
15
+ timeout?: number
16
+ approvalTimeout?: number
17
+ }
18
+ export declare interface FirefoxPublishResult {
19
+ artifactsDir: string
20
+ artifacts: string[]
21
+ channel: 'listed' | 'unlisted'
22
+ }
@@ -0,0 +1,82 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { mkdir, mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { buildExtension, resolveOutdir } from "./build";
6
+ function resolveFirefoxAuth(options) {
7
+ 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);
8
+ if (missing.length)
9
+ throw Error(`[browser-extension] missing Firefox Add-ons credentials: ${missing.join(", ")}`);
10
+ return { issuer, secret };
11
+ }
12
+ export function firefoxListingMetadata(config, store) {
13
+ if (!store.license && !store.categories?.length)
14
+ return;
15
+ if (!store.license || !store.categories?.length)
16
+ throw Error("[browser-extension] a new Firefox listing needs both firefoxAddons.license and firefoxAddons.categories");
17
+ return {
18
+ version: { license: store.license },
19
+ categories: { firefox: store.categories },
20
+ summary: { "en-US": config.description },
21
+ ...store.homepage ? { homepage: { "en-US": store.homepage } } : {},
22
+ ...store.supportEmail ? { support_email: { "en-US": store.supportEmail } } : {},
23
+ requires_payment: store.requiresPayment ?? !1
24
+ };
25
+ }
26
+ export async function publishFirefoxExtension(config, options) {
27
+ if (!config.geckoId)
28
+ throw Error("[browser-extension] Firefox publishing needs geckoId in config/extension.ts");
29
+ const store = config.firefoxAddons ?? {}, auth = resolveFirefoxAuth(options), cwd = options.cwd ?? process.cwd();
30
+ if (options.build !== !1)
31
+ await buildExtension(config, { target: "firefox", version: options.version, cwd });
32
+ const sourceDir = resolve(cwd, resolveOutdir(config, "firefox")), artifactsDir = resolve(cwd, store.artifactsDir ?? "web-ext-artifacts"), executable = Bun.which("web-ext");
33
+ if (!executable)
34
+ throw Error("[browser-extension] web-ext is unavailable; reinstall @stacksjs/browser-extension dependencies");
35
+ await mkdir(artifactsDir, { recursive: !0 });
36
+ const before = new Set(existsSync(artifactsDir) ? readdirSync(artifactsDir) : []), tempDir = await mkdtemp(join(tmpdir(), "stacks-firefox-publish-"));
37
+ try {
38
+ const args = [
39
+ executable,
40
+ "sign",
41
+ "--source-dir",
42
+ sourceDir,
43
+ "--artifacts-dir",
44
+ artifactsDir,
45
+ "--channel",
46
+ store.channel ?? "listed",
47
+ "--timeout",
48
+ String(options.timeout ?? 300000),
49
+ "--approval-timeout",
50
+ String(options.approvalTimeout ?? 0),
51
+ "--no-input",
52
+ "--boring"
53
+ ], metadata = firefoxListingMetadata(config, store);
54
+ if (metadata) {
55
+ const metadataPath = join(tempDir, "amo-metadata.json");
56
+ await Bun.write(metadataPath, `${JSON.stringify(metadata, null, 2)}
57
+ `);
58
+ args.push("--amo-metadata", metadataPath);
59
+ }
60
+ if (options.sourceCodePath)
61
+ args.push("--upload-source-code", resolve(cwd, options.sourceCodePath));
62
+ const exitCode = await Bun.spawn(args, {
63
+ cwd,
64
+ env: {
65
+ ...process.env,
66
+ WEB_EXT_API_KEY: auth.issuer,
67
+ WEB_EXT_API_SECRET: auth.secret
68
+ },
69
+ stdout: "inherit",
70
+ stderr: "inherit"
71
+ }).exited;
72
+ if (exitCode !== 0)
73
+ throw Error(`[browser-extension] Firefox Add-ons submission failed (${exitCode})`);
74
+ } finally {
75
+ await rm(tempDir, { recursive: !0, force: !0 });
76
+ }
77
+ return {
78
+ artifactsDir,
79
+ artifacts: readdirSync(artifactsDir).filter((file) => !before.has(file)),
80
+ channel: store.channel ?? "listed"
81
+ };
82
+ }
package/dist/index.d.ts CHANGED
@@ -8,7 +8,10 @@
8
8
  * app scaffold, appex resource sync, and xcodebuild pipeline.
9
9
  */
10
10
  export * from './build';
11
+ export * from './app-store-connect';
12
+ export * from './chrome-web-store';
11
13
  export * from './config';
14
+ export * from './firefox-addons';
12
15
  export * from './manifest';
13
16
  export * from './package';
14
17
  export * from './safari';
package/dist/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  export * from "./build";
2
+ export * from "./app-store-connect";
3
+ export * from "./chrome-web-store";
2
4
  export * from "./config";
5
+ export * from "./firefox-addons";
3
6
  export * from "./manifest";
4
7
  export * from "./package";
5
8
  export * from "./safari";
package/dist/safari.d.ts CHANGED
@@ -38,6 +38,8 @@ export declare function syncSafariResources(config: ExtensionConfig, options?: S
38
38
  * built `.app` path (undefined when xcodebuild was skipped/unavailable).
39
39
  */
40
40
  export declare function buildSafariApp(config: ExtensionConfig, options?: SafariAppBuildOptions): Promise<{ appPath?: string, resources: string }>;
41
+ /** Resolve and validate App Store Connect credentials from options or environment variables. */
42
+ export declare function resolveAppStoreConnectAuth(options: AppStoreConnectAuth): Required<AppStoreConnectAuth>;
41
43
  /**
42
44
  * Create a signed Release archive and either validate it or upload it to App
43
45
  * Store Connect. Xcode owns certificate/profile creation and the upload so the
package/dist/safari.js CHANGED
@@ -132,7 +132,7 @@ export async function buildSafariApp(config, options = {}) {
132
132
  await Bun.$`xcodebuild -project ${join(dir, `${appName}.xcodeproj`)} -scheme ${appName} -configuration ${configuration} -derivedDataPath ${derivedData} ${signing} build`;
133
133
  return { appPath: join(derivedData, "Build", "Products", configuration, `${appName}.app`), resources };
134
134
  }
135
- function appStoreConnectAuth(options) {
135
+ export function resolveAppStoreConnectAuth(options) {
136
136
  const keyId = options.keyId ?? process.env.APP_STORE_CONNECT_API_KEY_ID, issuerId = options.issuerId ?? process.env.APP_STORE_CONNECT_API_ISSUER_ID, keyPath = options.keyPath ?? process.env.APP_STORE_CONNECT_API_KEY_PATH, missing = [
137
137
  !keyId && "APP_STORE_CONNECT_API_KEY_ID",
138
138
  !issuerId && "APP_STORE_CONNECT_API_ISSUER_ID",
@@ -180,7 +180,7 @@ export async function publishSafariApp(config, options) {
180
180
  const cwd = options.cwd ?? process.cwd(), teamId = options.teamId ?? config.safariTeamId;
181
181
  if (!teamId)
182
182
  throw Error("[browser-extension] Safari publishing needs safariTeamId in config/extension.ts or --team-id");
183
- const auth = appStoreConnectAuth(options), buildNumber = options.buildNumber ?? process.env.GITHUB_RUN_NUMBER ?? String(Math.floor(Date.now() / 1000));
183
+ const auth = resolveAppStoreConnectAuth(options), buildNumber = options.buildNumber ?? process.env.GITHUB_RUN_NUMBER ?? String(Math.floor(Date.now() / 1000));
184
184
  if (!/^\d+(?:\.\d+){0,2}$/.test(buildNumber))
185
185
  throw Error(`[browser-extension] invalid Safari build number ${buildNumber}; use one to three dot-separated integers`);
186
186
  if (options.build !== !1)
package/dist/types.d.ts CHANGED
@@ -52,10 +52,28 @@ export declare interface BuildContext {
52
52
  version: string
53
53
  cwd: string
54
54
  }
55
+ export declare interface ChromeWebStoreConfig {
56
+ publisherId: string
57
+ itemId: string
58
+ publishType?: 'DEFAULT_PUBLISH' | 'STAGED_PUBLISH'
59
+ deployPercentage?: number
60
+ skipReview?: boolean
61
+ }
62
+ export declare interface FirefoxAddonsConfig {
63
+ channel?: 'listed' | 'unlisted'
64
+ license?: string
65
+ categories?: string[]
66
+ homepage?: string
67
+ supportEmail?: string
68
+ requiresPayment?: boolean
69
+ artifactsDir?: string
70
+ }
55
71
  export declare interface ExtensionConfig {
56
72
  name: string
57
73
  description: string
58
74
  geckoId?: string
75
+ chromeWebStore?: ChromeWebStoreConfig
76
+ firefoxAddons?: FirefoxAddonsConfig
59
77
  safariBundleId?: string
60
78
  safariTeamId?: string
61
79
  safariExclude?: string[]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/browser-extension",
3
3
  "type": "module",
4
- "version": "0.70.119",
4
+ "version": "0.70.121",
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": [
@@ -59,5 +59,8 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "better-dx": "^0.2.17"
62
+ },
63
+ "dependencies": {
64
+ "web-ext": "^10.5.0"
62
65
  }
63
66
  }