UNPKG

publish-browser-extension

Version:
763 lines (748 loc) 28.6 kB
import { Listr } from "listr2"; import { createReadStream } from "node:fs"; import { FetchError, createFetch, ofetch } from "ofetch"; import { z } from "zod/v4"; import fs, { copyFile, readFile, writeFile } from "node:fs/promises"; import fs$1 from "fs"; import { FormData } from "formdata-node"; import { fileFromPath } from "formdata-node/file-from-path"; import jwt from "jsonwebtoken"; import { FormDataEncoder } from "form-data-encoder"; import { Readable } from "node:stream"; import { consola } from "consola"; //#region src/utils/fetch.ts const fetch = createFetch({ defaults: { onResponseError: (ctx) => { console.log("Request:", ctx.request); console.log("Response:", JSON.stringify(ctx.response, null, 2)); } } }); //#endregion //#region src/chrome/chrome-api.ts var CwsApi = class { constructor(options) { this.options = options; } tokenEndpoint() { return new URL("https://oauth2.googleapis.com/token"); } uploadEndpoint(extensionId) { return new URL(`https://www.googleapis.com/upload/chromewebstore/v1.1/items/${extensionId}`); } publishEndpoint(extensionId) { return new URL(`https://www.googleapis.com/chromewebstore/v1.1/items/${extensionId}/publish`); } async uploadZip(params) { const Authorization = await this.getAuthHeader(params.token); const endpoint = this.uploadEndpoint(params.extensionId); const file = createReadStream(params.zipFile); const res = await fetch(endpoint.href, { method: "PUT", body: file, headers: { Authorization, "x-goog-api-version": "2" } }); if (res.uploadState === "FAILURE") { const errors = res.itemError?.map((e) => `${e.error_code}: ${e.error_detail}`).join("\n"); throw new Error(`Chrome Web Store upload failed:\n${errors}`); } } async submitForReview(params) { const Authorization = await this.getAuthHeader(params.token); const endpoint = this.publishEndpoint(params.extensionId); if (params.publishTarget) endpoint.searchParams.append("publishTarget", params.publishTarget); if (params.deployPercentage != null) endpoint.searchParams.set("deployPercentage", String(params.deployPercentage)); if (params.reviewExemption != null) endpoint.searchParams.set("reviewExemption", String(params.reviewExemption)); const res = await fetch(endpoint.href, { method: "POST", headers: { Authorization, "x-goog-api-version": "2", "Content-Length": "0" } }); if (res.uploadState === "FAILURE") { const errors = res.itemError?.map((e) => `${e.error_code}: ${e.error_detail}`).join("\n"); throw new Error(`Chrome Web Store publish failed:\n${errors}`); } } getToken() { return fetch(this.tokenEndpoint().href, { method: "POST", body: JSON.stringify({ client_id: this.options.clientId, client_secret: this.options.clientSecret, refresh_token: this.options.refreshToken, grant_type: "refresh_token", redirect_uri: "urn:ietf:wg:oauth:2.0:oob" }), headers: { "Content-Type": "application/json" } }); } async getAuthHeader(token) { return `${token.token_type} ${token.access_token}`; } }; //#endregion //#region src/utils/fs.ts async function ensureZipExists(path) { try { await fs.lstat(path); } catch { throw Error("ZIP file does not exist: " + path); } } //#endregion //#region src/chrome/chrome-web-store.ts const ChromeWebStoreOptions = z.object({ zip: z.string().min(1), extensionId: z.string().min(1).trim(), clientId: z.string().min(1).trim(), clientSecret: z.string().min(1).trim(), refreshToken: z.string().min(1).trim(), publishTarget: z.enum(["default", "trustedTesters"]).default("default"), deployPercentage: z.int().min(1).max(100).optional(), reviewExemption: z.boolean().default(false), skipSubmitReview: z.boolean().default(false) }); var ChromeWebStore = class { constructor(options, setStatus) { this.options = options; this.setStatus = setStatus; } async submit(dryRun) { const api = new CwsApi(this.options); this.setStatus("Getting an access token"); const token = await api.getToken(); if (dryRun) { this.setStatus("DRY RUN: Skipped upload and publishing"); return; } this.setStatus("Uploading new ZIP file"); await api.uploadZip({ extensionId: this.options.extensionId, zipFile: this.options.zip, token }); if (this.options.skipSubmitReview) { this.setStatus("Skipping submission (skipSubmitReview=true)"); return; } this.setStatus("Submitting for review"); await api.submitForReview({ extensionId: this.options.extensionId, publishTarget: this.options.publishTarget, token, deployPercentage: this.options.deployPercentage, reviewExemption: this.options.reviewExemption }); } async ensureZipsExist() { await ensureZipExists(this.options.zip); } }; //#endregion //#region src/edge/edge-api.ts var EdgeApi = class { constructor(options) { this.options = options; } /** * Docs: https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api#sample-request */ getToken() { return Promise.resolve({ access_token: this.options.apiKey, expires_in: 0, token_type: "ApiKey" }); } /** * Docs: https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api#uploading-a-package-to-update-an-existing-submission */ async uploadDraft(params) { const endpoint = `https://api.addons.microsoftedge.microsoft.com/v1/products/${params.productId}/submissions/draft/package`; const file = fs$1.createReadStream(params.zipFile); const operationId = (await fetch.raw(endpoint, { method: "POST", body: file, headers: { ...this.getAuthHeaders(params.token), "Content-Type": "application/zip" } })).headers.get("Location"); if (!operationId) throw Error("Edge API did not return an operation ID in the Location header."); return { operationId }; } /** * Docs: https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api#checking-the-status-of-a-package-upload */ uploadDraftOperation(params) { return fetch(`https://api.addons.microsoftedge.microsoft.com/v1/products/${params.productId}/submissions/draft/package/operations/${params.operationId}`, { headers: this.getAuthHeaders(params.token) }); } /** * Docs: https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api#publishing-the-submission */ async publish(params) { await fetch(`https://api.addons.microsoftedge.microsoft.com/v1/products/${params.productId}/submissions`, { method: "POST", body: JSON.stringify({}), headers: this.getAuthHeaders(params.token) }).catch((err) => { if (!(err instanceof FetchError)) throw err; const data = typeof err.data === "string" ? err.data : JSON.stringify(err.data); throw new Error(`Edge API failed to publish: ${err.status} ${err.statusText} - ${data}`); }); } getAuthHeaders(token) { return { Authorization: `${token.token_type} ${token.access_token}`, ...token.token_type === "ApiKey" ? { "X-ClientID": this.options.clientId } : {} }; } }; //#endregion //#region src/utils/sleep.ts function sleep(ms) { return new Promise((res) => setTimeout(res, ms)); } //#endregion //#region src/utils/withTimeout.ts function withTimeout(promise, ms) { return new Promise((res, rej) => { const timeout = setTimeout(() => rej(`Timed out after ${ms}ms`), ms); promise.then(res, rej).finally(() => clearTimeout(timeout)); }); } //#endregion //#region src/edge/edge-addon-store.ts const EdgeAddonStoreOptions = z.object({ zip: z.string().min(1), productId: z.string().min(1).trim(), clientId: z.string().min(1).trim(), skipSubmitReview: z.boolean().default(false), apiKey: z.string().min(1).trim(), clientSecret: z.string().optional(), accessTokenUrl: z.string().optional() }); var EdgeAddonStore = class { api; constructor(options, setStatus) { this.options = options; this.setStatus = setStatus; this.api = new EdgeApi(options); } async ensureZipsExist() { await ensureZipExists(this.options.zip); } async submit(dryRun) { this.setStatus("Getting authorization token"); const token = await this.api.getToken(); if (dryRun) { this.setStatus("DRY RUN: Skipped upload and publishing"); return; } await withTimeout(this.uploadAndPollValidation(token, 5e3), 10 * 6e4); if (this.options.skipSubmitReview) { this.setStatus("Skipping submission (skipSubmitReview=true)"); return; } this.setStatus("Submitting new version"); await this.api.publish({ token, productId: this.options.productId }); } async uploadAndPollValidation(token, pollIntervalMs) { this.setStatus("Uploading new ZIP file"); const { operationId } = await this.api.uploadDraft({ token, productId: this.options.productId, zipFile: this.options.zip }); this.setStatus("Waiting for validation results"); let operation; do { await sleep(pollIntervalMs); operation = await this.api.uploadDraftOperation({ token, operationId, productId: this.options.productId }); } while (operation.status === "InProgress"); if (operation.status === "Failed") throw Error(`Validation failed: ${JSON.stringify(operation, null, 2)}`); else this.setStatus("Extension is valid"); } }; //#endregion //#region src/firefox/firefox-api.ts var AddonsApi = class { constructor(options) { this.options = options; } addonDetailEndpoint(extensionId) { return new URL(`https://addons.mozilla.org/api/v5/addons/addon/${extensionId}`); } addonsUploadCreateEndpoint() { return new URL(`https://addons.mozilla.org/api/v5/addons/upload/`); } addonsUploadDetailsEndpoint(uploadUuid) { return new URL(`https://addons.mozilla.org/api/v5/addons/upload/${uploadUuid}`); } addonVersionCreateEndpoint(extensionId) { return new URL(`https://addons.mozilla.org/api/v5/addons/addon/${extensionId}/versions/`); } /** * Docs: https://addons-server.readthedocs.io/en/latest/topics/api/addons.html#detail */ details(params) { return fetch(this.addonDetailEndpoint(params.extensionId).href, { headers: { Authorization: this.getAuthHeader() } }); } /** * Docs: https://addons-server.readthedocs.io/en/latest/topics/api/addons.html#upload-create */ async uploadCreate(params) { const endpoint = this.addonsUploadCreateEndpoint(); const form = new FormData(); form.set("channel", params.channel); form.set("upload", await fileFromPath(params.file)); const encoder = new FormDataEncoder(form); return await fetch(endpoint.href, { method: "POST", body: Readable.from(encoder), headers: { ...encoder.headers, Authorization: this.getAuthHeader() } }); } /** * Docs: https://addons-server.readthedocs.io/en/latest/topics/api/addons.html#upload-detail */ uploadDetail(params) { return fetch(this.addonsUploadDetailsEndpoint(params.uuid).href, { headers: { Authorization: this.getAuthHeader() } }); } async versionCreate(params) { const endpoint = this.addonVersionCreateEndpoint(params.extensionId); const form = new FormData(); form.set("upload", params.uploadUuid); if (params.sourceFile) form.set("source", await fileFromPath(params.sourceFile)); else form.set("source", ""); const encoder = new FormDataEncoder(form); return await fetch(endpoint.href, { method: "POST", body: Readable.from(encoder), headers: { ...encoder.headers, Authorization: this.getAuthHeader() } }); } /** * See https://addons-server.readthedocs.io/en/latest/topics/api/auth.html */ createJwt(timeoutInS = 30) { const issuedAt = Math.floor(Date.now() / 1e3); const payload = { iss: this.options.jwtIssuer, jti: Math.random().toString(), iat: issuedAt, exp: issuedAt + timeoutInS }; const secret = this.options.jwtSecret ?? ""; return jwt.sign(payload, secret, { algorithm: "HS256" }); } getAuthHeader() { return `JWT ${this.createJwt()}`; } }; //#endregion //#region src/utils/plural.ts function plural(count, word, pluralForm) { if (count === 1) return `${count} ${word}`; return `${count} ${pluralForm ?? `${word}s`}`; } //#endregion //#region src/firefox/firefox-addon-store.ts const FirefoxAddonStoreOptions = z.object({ zip: z.string().min(1), sourcesZip: z.string().min(1).optional(), extensionId: z.string().min(1).trim(), jwtIssuer: z.string().min(1).trim(), jwtSecret: z.string().min(1).trim(), channel: z.enum(["listed", "unlisted"]).default("listed") }); var FirefoxAddonStore = class { api; constructor(options, setStatus) { this.options = options; this.setStatus = setStatus; this.api = new AddonsApi(options); } async ensureZipsExist() { await ensureZipExists(this.options.zip); if (this.options.sourcesZip) await ensureZipExists(this.options.sourcesZip); } async submit(dryRun) { this.setStatus("Getting addon details"); const addon = await this.api.details({ extensionId: this.extensionId }); if (dryRun) { this.setStatus("DRY RUN: Skipped upload and publishing"); return; } const upload = await withTimeout(this.uploadAndPollValidation(5e3), 10 * 6e4); this.setStatus("Submitting new version"); const version = await this.api.versionCreate({ extensionId: this.extensionId, sourceFile: this.options.sourcesZip, uploadUuid: upload.uuid }); const validationUrl = `https://addons.mozilla.org/en-US/developers/addon/${addon.id}/file/${version.file.id}/validation`; const { errors, notices, warnings } = upload.validation; this.setStatus(`Validation results: ${plural(errors, "error")}, ${plural(warnings, "warning")}, ${plural(notices, "notice")}`); if (!upload.valid) throw Error(`Extension is invalid: ${validationUrl}`); else console.log("Firefox validation results: " + validationUrl); } async uploadAndPollValidation(pollIntervalMs) { this.setStatus("Uploading new ZIP file"); let details = await this.api.uploadCreate({ file: this.options.zip, channel: this.options.channel }); this.setStatus("Waiting for validation results"); while (!details.processed) { await sleep(pollIntervalMs); details = await this.api.uploadDetail(details); } return details; } /** * Ensure the extension id is not wrapped in curly braces, since that's what * the addon store API is expecting. * * @example * "{test}" -> "test" * "test" -> "test" * "test@123" -> "test@123" */ get extensionId() { let id = this.options.extensionId; if (id.startsWith("{")) id = id.slice(1); if (id.endsWith("}")) id = id.slice(0, -1); return id; } }; //#endregion //#region src/config.ts /** * Given inline config, read environment variables and apply defaults. Throws an error if any config * is missing. */ function resolveConfig(config) { const dryRun = config.dryRun ?? booleanEnv("DRY_RUN") ?? false; const chromeZip = config.chrome?.zip ?? stringEnv("CHROME_ZIP"); const firefoxZip = config.firefox?.zip ?? stringEnv("FIREFOX_ZIP"); const edgeZip = config.edge?.zip ?? stringEnv("EDGE_ZIP"); return { dryRun, chrome: chromeZip == null ? void 0 : { zip: chromeZip, extensionId: config.chrome?.extensionId ?? stringEnv("CHROME_EXTENSION_ID"), clientId: config.chrome?.clientId ?? stringEnv("CHROME_CLIENT_ID"), clientSecret: config.chrome?.clientSecret ?? stringEnv("CHROME_CLIENT_SECRET"), refreshToken: config.chrome?.refreshToken ?? stringEnv("CHROME_REFRESH_TOKEN"), publishTarget: config.chrome?.publishTarget ?? stringEnv("CHROME_PUBLISH_TARGET") ?? "default", deployPercentage: config.chrome?.deployPercentage ?? intEnv("CHROME_DEPLOY_PERCENTAGE"), reviewExemption: config.chrome?.reviewExemption ?? booleanEnv("CHROME_REVIEW_EXEMPTION") ?? false, skipSubmitReview: config.chrome?.skipSubmitReview ?? booleanEnv("CHROME_SKIP_SUBMIT_REVIEW") ?? false }, firefox: firefoxZip == null ? void 0 : { zip: firefoxZip, sourcesZip: config.firefox?.sourcesZip ?? stringEnv("FIREFOX_SOURCES_ZIP"), extensionId: config.firefox?.extensionId ?? stringEnv("FIREFOX_EXTENSION_ID"), jwtIssuer: config.firefox?.jwtIssuer ?? stringEnv("FIREFOX_JWT_ISSUER"), jwtSecret: config.firefox?.jwtSecret ?? stringEnv("FIREFOX_JWT_SECRET"), channel: config.firefox?.channel ?? stringEnv("FIREFOX_CHANNEL") ?? "listed" }, edge: edgeZip == null ? void 0 : { zip: edgeZip, productId: config.edge?.productId ?? stringEnv("EDGE_PRODUCT_ID"), clientId: config.edge?.clientId ?? stringEnv("EDGE_CLIENT_ID"), apiKey: config.edge?.apiKey ?? stringEnv("EDGE_API_KEY"), clientSecret: config.edge?.clientSecret ?? stringEnv("EDGE_CLIENT_SECRET"), accessTokenUrl: config.edge?.accessTokenUrl ?? stringEnv("EDGE_ACCESS_TOKEN_URL"), skipSubmitReview: config.edge?.skipSubmitReview ?? booleanEnv("EDGE_SKIP_SUBMIT_REVIEW") ?? false } }; } function toScreamingSnakeCase(str) { return str.replace(/([A-Z])/g, "_$1").replace(/-/g, "_").toUpperCase(); } function validateConfig(config) { const result = InternalConfig.safeParse(config); if (!result.success) throw Error("Missing required config: " + result.error.issues.map((i) => i.path.map((j) => toScreamingSnakeCase(String(j))).join("_")).join(", "), { cause: result.error }); return result.data; } function booleanEnv(name) { return !process.env[name] ? void 0 : process.env[name] === "true"; } function stringEnv(name) { return !process.env[name] ? void 0 : process.env[name]; } function intEnv(name) { return !process.env[name] ? void 0 : parseInt(process.env[name]); } const InlineConfig = z.object({ dryRun: z.boolean().optional(), chrome: ChromeWebStoreOptions.partial().optional(), firefox: FirefoxAddonStoreOptions.partial().optional(), edge: EdgeAddonStoreOptions.partial().optional() }); const InternalConfig = z.object({ dryRun: z.boolean(), chrome: ChromeWebStoreOptions.optional(), firefox: FirefoxAddonStoreOptions.optional(), edge: EdgeAddonStoreOptions.optional() }); //#endregion //#region src/submit.ts async function submit(config) { const internalConfig = validateConfig(resolveConfig(config)); console.log(""); consola.info("Publishing Extension"); if (internalConfig.dryRun) consola.warn("Dry run, skipping submission"); if (internalConfig.edge?.clientSecret || internalConfig.edge?.accessTokenUrl) consola.warn([ "Edge API v1.0 was deprecated Jan 1, 2025. v1.1 of the API requires different authentication. To upgrade:", " 1. Remove the `--edge-client-secret` or `EDGE_CLIENT_SECRET` environment variable", " 2. Remove the `--edge-access-token-url` or `EDGE_ACCESS_TOKEN_URL` environment variable", " 3. Follow the instructions below to add the `--edge-api-key` flag or `EDGE_API_KEY` environment variable", "Or run `publish-extension init` and re-initialize the edge store.", "", "To generate an API key:", " 1. Visit https://partner.microsoft.com/en-us/dashboard/microsoftedge/publishapi", " 2. Enable the v1.1 API if necessary", " 3. Create an new API key", "", "Refer to Microsoft API reference for more details: https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api?tabs=v1-1#overview-of-using-the-update-rest-api" ].join("\n")); const stores = []; if (internalConfig.chrome) stores.push({ id: "chrome", name: "Chrome Web Store", getStore: (setStatus) => new ChromeWebStore(internalConfig.chrome, setStatus) }); if (internalConfig.firefox) stores.push({ id: "firefox", name: "Firefox Addon Store", getStore: (setStatus) => new FirefoxAddonStore(internalConfig.firefox, setStatus) }); if (internalConfig.edge) stores.push({ id: "edge", name: "Edge Addon Store", getStore: (setStatus) => new EdgeAddonStore(internalConfig.edge, setStatus) }); if (stores.length === 0) throw Error("No ZIP files detected to upload"); const results = {}; await new Listr(stores.map(({ id, name, getStore }) => ({ title: name, async task(_ctx, task) { try { const setStatus = (text) => { task.output = `[${id}] ${text}`; }; const store = getStore(setStatus); setStatus("Checking ZIP files exist"); await store.ensureZipsExist(); await store.submit(internalConfig.dryRun); results[id] = { success: true }; } catch (err) { consola.error(err); results[id] = { success: false, err }; throw err; } } })), { exitOnError: false, collectErrors: "minimal", concurrent: true }).run(); const errors = Object.entries(results).filter(([_id, result]) => { return !result.success; }); if (errors.length > 0) throw Error(`Submissions failed: ${errors.length}`, { cause: errors }); return results; } //#endregion //#region src/init.ts const envFile = ".env.submit"; async function init(config) { consola.info(`Initialize or update an existing \`${envFile}\` file.`); const previousConfig = resolveConfig(config); const stores = await prompt("What stores do you want to configure? \x1B[2m(use ↑/↓ and space to select)\x1B[0m", { type: "multiselect", options: [ { value: "chrome", label: "Chrome Web Store" }, { value: "firefox", label: "Firefox Addon Store" }, { value: "edge", label: "Edge Addon Store" } ], required: true }); if (!stores?.length) { consola.warn("No stores selected, exiting without making any changes."); process.exit(1); } const replacements = []; if (stores?.includes("chrome")) replacements.push(...await initChrome(previousConfig.chrome)); if (stores?.includes("firefox")) replacements.push(...await initFirefox(previousConfig.firefox)); if (stores?.includes("edge")) replacements.push(...await initEdge(previousConfig.edge)); await updateEnvFile(replacements); console.log(); consola.log("To submit an update, run:\n\n `publish-extension --chrome-zip path/to/extension.zip \\`\n `--firefox-zip path/to/extension.zip \\`\n `--edge-zip path/to/extension.zip`"); } async function prompt(message, options, previousValue) { let result = await consola.prompt(message, { default: previousValue, placeholder: previousValue, ...options }); if (typeof result === "symbol") throw Error("Canceled"); if (typeof result === "string") result = result.trim(); return result; } async function initChrome(previousOptions) { const entries = []; console.log(); consola.start("Chrome Web Store\n"); consola.log("`--chrome-extension-id` can be found:"); consola.log(" 1. Under the extension name in the CWS developer console"); consola.log(" 2. In the URL of the CWS page for the item"); consola.log("Example: `ocfdgncpifmegplaglcnglhioflaimkd`"); const extensionId = await prompt("Enter the extension ID:", { type: "text" }, previousOptions?.extensionId); entries.push(["CHROME_EXTENSION_ID", extensionId]); console.log(); consola.log("`--chrome-client-id` and `--chrome-client-secret` are generated by following the \"Initial Setup\" from:"); console.log("https://developer.chrome.com/docs/webstore/using-api#setup"); const clientId = await prompt("Enter your client ID:", { type: "text" }, previousOptions?.clientId); entries.push(["CHROME_CLIENT_ID", clientId]); const clientSecret = await prompt("Enter your client secret:", { type: "text" }, previousOptions?.clientSecret); entries.push(["CHROME_CLIENT_SECRET", clientSecret]); if (await prompt("Generate new refresh token?", { type: "confirm" })) { const authCodeUrl = `https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https://www.googleapis.com/auth/chromewebstore&client_id=${clientId}&redirect_uri=urn:ietf:wg:oauth:2.0:oob`; consola.log(authCodeUrl); const authCode = await consola.prompt("Open the above URL, login, and enter the auth code:", { type: "text", required: true }); const data = new URLSearchParams(); data.set("client_id", clientId); data.set("client_secret", clientSecret); data.set("code", authCode); data.set("grant_type", "authorization_code"); data.set("redirect_uri", "urn:ietf:wg:oauth:2.0:oob"); const refreshToken = (await ofetch(`https://accounts.google.com/o/oauth2/token`, { method: "POST", body: data })).refresh_token; consola.info(`Refresh token: \`${refreshToken}\``); entries.push(["CHROME_REFRESH_TOKEN", refreshToken]); } const publishTarget = await prompt("`--chrome-publish-target`: Where do you want to release to?", { type: "select", options: [{ label: "Default", value: "default", hint: "Public release channel" }, { label: "Trusted Testers", value: "trustedTesters", hint: "Prerelease, internal channel" }], initial: previousOptions?.publishTarget }, previousOptions?.publishTarget); entries.push(["CHROME_PUBLISH_TARGET", publishTarget]); const submitForReview = await prompt("When uploading, automatically submit new update for review?", { type: "confirm" }, String(!previousOptions?.skipSubmitReview)); entries.push(["CHROME_SKIP_SUBMIT_REVIEW", !submitForReview]); return entries; } async function initFirefox(previousOptions) { const entries = []; console.log(); consola.start("Firefox Addon Store\n"); consola.info("Your `--firefox-extension-id` is listed at the bottom of the details page on:"); console.log("https://addons.mozilla.org/en-US/developers/"); const extensionId = await prompt("Enter extension ID:", { type: "text" }, previousOptions?.extensionId); entries.push(["FIREFOX_EXTENSION_ID", extensionId]); console.log(); consola.log("`--firefox-jwt-issuer` and `--firefox-jwt-secret` are available at:"); console.log("https://addons.mozilla.org/developers/addon/api/key/"); const jwtIssuer = await prompt("Enter your JWT issuer:", { type: "text" }, previousOptions?.jwtIssuer); entries.push(["FIREFOX_JWT_ISSUER", jwtIssuer]); const jwtSecret = await prompt("Enter your JWT secret:", { type: "text" }, previousOptions?.jwtSecret); entries.push(["FIREFOX_JWT_SECRET", jwtSecret]); const channel = await prompt("`--firefox-channel`: Which channel do you want to release to?", { type: "select", options: [{ label: "Listed", value: "listed", hint: "Hosted on addons.mozilla.com" }, { label: "Unlisted", value: "unlisted", hint: "For self-hosting" }], initial: previousOptions?.channel }, previousOptions?.channel); entries.push(["FIREFOX_CHANNEL", channel]); return entries; } async function initEdge(previousOptions) { const entries = []; console.log(); consola.start("Edge Addon Store\n"); consola.info("Your `--edge-product-id` is listed On the developer dashboard, at the top of the page under the extension name"); console.log("https://partner.microsoft.com/dashboard/microsoftedge/overview"); const productId = await prompt("Enter product ID:", { type: "text" }, previousOptions?.productId); entries.push(["EDGE_PRODUCT_ID", productId]); console.log(); consola.log("`--edge-client-id` and either `--edge-api-key` (API v1.1) or `--edge-client-secret` and `--edge-access-token-url` (API v1.0) can be created following:"); console.log("https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/api/using-addons-api#before-you-begin"); const clientId = await prompt("Enter your client ID:", { type: "text" }, previousOptions?.clientId); entries.push(["EDGE_CLIENT_ID", clientId]); const apiKey = await prompt("Enter your API key:", { type: "text" }, previousOptions?.apiKey); entries.push(["EDGE_API_KEY", apiKey]); const submitForReview = await prompt("When uploading, automatically submit new update for review?", { type: "confirm" }, String(!previousOptions?.skipSubmitReview)); entries.push(["EDGE_SKIP_SUBMIT_REVIEW", !submitForReview]); return entries; } async function updateEnvFile(entries) { consola.start(`Writing to \`${envFile}\`...`); let template = await readFile(envFile, "utf-8").catch(() => ""); for (const [name, value] of entries) { const replacement = `${name}=${typeof value === "string" ? `"${value}"` : value}`; const pattern = new RegExp(`^${name}=.*$`, "m"); const existing = template.match(pattern); if (existing) template = template.replace(existing[0], replacement); else template += `\n${replacement}`; } const backupFilename = `${envFile}.backup-${Date.now()}`; await copyFile(envFile, backupFilename).then(() => { consola.info(`Backed up old \`${envFile}\` to \`${backupFilename}\``); }).catch(() => {}); await writeFile(envFile, template, "utf-8"); console.log(); consola.success("Wrote config to `.env.submit`"); } //#endregion export { FirefoxAddonStoreOptions as a, EdgeAddonStoreOptions as c, ChromeWebStoreOptions as d, CwsApi as f, FirefoxAddonStore as i, EdgeApi as l, submit as n, AddonsApi as o, InlineConfig as r, EdgeAddonStore as s, init as t, ChromeWebStore as u };