@extension.dev/deploy 0.2.5 → 0.3.0
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/CHANGELOG.md +13 -0
- package/README.md +34 -1
- package/dist/module.js +436 -321
- package/dist/module.mjs +434 -319
- package/dist/src/chrome.d.ts +19 -0
- package/dist/src/chrome.d.ts.map +1 -1
- package/dist/src/cli-commands.d.ts +5 -0
- package/dist/src/cli-commands.d.ts.map +1 -0
- package/dist/src/cli-parse.d.ts +3 -0
- package/dist/src/cli-parse.d.ts.map +1 -0
- package/dist/src/cli-usage.d.ts +2 -0
- package/dist/src/cli-usage.d.ts.map +1 -0
- package/dist/src/cli.d.ts +1 -2
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/config.d.ts +6 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/deploy.d.ts.map +1 -1
- package/dist/src/edge.d.ts.map +1 -1
- package/dist/src/firefox-api.d.ts +52 -0
- package/dist/src/firefox-api.d.ts.map +1 -0
- package/dist/src/firefox.d.ts +1 -32
- package/dist/src/firefox.d.ts.map +1 -1
- package/dist/src/init-chrome.d.ts +16 -0
- package/dist/src/init-chrome.d.ts.map +1 -0
- package/dist/src/init-edge.d.ts +8 -0
- package/dist/src/init-edge.d.ts.map +1 -0
- package/dist/src/init-firefox.d.ts +8 -0
- package/dist/src/init-firefox.d.ts.map +1 -0
- package/dist/src/init-io.d.ts.map +1 -1
- package/dist/src/init-prompt.d.ts +5 -0
- package/dist/src/init-prompt.d.ts.map +1 -0
- package/dist/src/init.d.ts +3 -26
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/types.d.ts +16 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/fetch.d.ts +2 -2
- package/dist/src/utils/fetch.d.ts.map +1 -1
- package/dist/src/watch.d.ts.map +1 -1
- package/package.json +18 -8
- package/dist/__tests__/cli.test.d.ts +0 -2
- package/dist/__tests__/cli.test.d.ts.map +0 -1
- package/dist/__tests__/config.test.d.ts +0 -2
- package/dist/__tests__/config.test.d.ts.map +0 -1
- package/dist/__tests__/deploy.test.d.ts +0 -2
- package/dist/__tests__/deploy.test.d.ts.map +0 -1
- package/dist/__tests__/dry-run.test.d.ts +0 -2
- package/dist/__tests__/dry-run.test.d.ts.map +0 -1
- package/dist/__tests__/init.test.d.ts +0 -2
- package/dist/__tests__/init.test.d.ts.map +0 -1
- package/dist/__tests__/types.test.d.ts +0 -2
- package/dist/__tests__/types.test.d.ts.map +0 -1
- package/dist/__tests__/utils.test.d.ts +0 -2
- package/dist/__tests__/utils.test.d.ts.map +0 -1
- package/dist/__tests__/verify.test.d.ts +0 -2
- package/dist/__tests__/verify.test.d.ts.map +0 -1
- package/dist/__tests__/watch.test.d.ts +0 -2
- package/dist/__tests__/watch.test.d.ts.map +0 -1
package/dist/module.mjs
CHANGED
|
@@ -17,7 +17,12 @@ const chromeOptionsSchema = z.object({
|
|
|
17
17
|
]).default("default"),
|
|
18
18
|
deployPercentage: z.number().int().min(1).max(100).optional(),
|
|
19
19
|
reviewExemption: z.boolean().default(false),
|
|
20
|
-
skipSubmitReview: z.boolean().default(false)
|
|
20
|
+
skipSubmitReview: z.boolean().default(false),
|
|
21
|
+
publisherId: z.string().trim().optional(),
|
|
22
|
+
apiVersion: z["enum"]([
|
|
23
|
+
"v1.1",
|
|
24
|
+
"v2"
|
|
25
|
+
]).optional()
|
|
21
26
|
});
|
|
22
27
|
const firefoxOptionsSchema = z.object({
|
|
23
28
|
zip: z.string().min(1),
|
|
@@ -67,7 +72,9 @@ function resolveConfig(flags) {
|
|
|
67
72
|
publishTarget: flags.chromePublishTarget ?? envStr("CHROME_PUBLISH_TARGET"),
|
|
68
73
|
deployPercentage: flags.chromeDeployPercentage ?? envInt("CHROME_DEPLOY_PERCENTAGE"),
|
|
69
74
|
reviewExemption: flags.chromeReviewExemption ?? envBool("CHROME_REVIEW_EXEMPTION"),
|
|
70
|
-
skipSubmitReview: flags.chromeSkipSubmitReview ?? envBool("CHROME_SKIP_SUBMIT_REVIEW")
|
|
75
|
+
skipSubmitReview: flags.chromeSkipSubmitReview ?? envBool("CHROME_SKIP_SUBMIT_REVIEW"),
|
|
76
|
+
publisherId: flags.chromePublisherId ?? envStr("CHROME_PUBLISHER_ID"),
|
|
77
|
+
apiVersion: flags.chromeApiVersion ?? envStr("CHROME_API_VERSION")
|
|
71
78
|
},
|
|
72
79
|
firefox: null == firefoxZip ? void 0 : {
|
|
73
80
|
zip: firefoxZip,
|
|
@@ -94,6 +101,7 @@ const SETUP_HINTS = {
|
|
|
94
101
|
CHROME_CLIENT_ID: "Create OAuth 2.0 credentials at https://console.cloud.google.com/apis/credentials",
|
|
95
102
|
CHROME_CLIENT_SECRET: "Generated alongside the Client ID in the Google Cloud Console.",
|
|
96
103
|
CHROME_REFRESH_TOKEN: "Obtain by completing the OAuth consent flow. See the Chrome Web Store API docs.",
|
|
104
|
+
CHROME_PUBLISHER_ID: "The publisher UUID from your Chrome Web Store dev console URL (chrome.google.com/webstore/devconsole/<UUID>). Required for the v2 API.",
|
|
97
105
|
CHROME_ZIP: "Path to the .zip file built for Chrome Web Store upload.",
|
|
98
106
|
FIREFOX_EXTENSION_ID: "The addon GUID ({uuid} format) or email-style ID from your addon's AMO page.",
|
|
99
107
|
FIREFOX_JWT_ISSUER: "API key from https://addons.mozilla.org/developers/addon/api/key/",
|
|
@@ -134,16 +142,24 @@ function envInt(name) {
|
|
|
134
142
|
const val = process.env[name];
|
|
135
143
|
return null == val ? void 0 : parseInt(val, 10);
|
|
136
144
|
}
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
146
|
+
function withTimeout(init, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
147
|
+
if (null == init ? void 0 : init.signal) return init;
|
|
148
|
+
return {
|
|
149
|
+
...init || {},
|
|
150
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async function fetchJson(url, init, timeoutMs) {
|
|
154
|
+
const res = await fetch(url, withTimeout(init, timeoutMs));
|
|
139
155
|
if (!res.ok) {
|
|
140
156
|
const body = await res.text().catch(()=>"");
|
|
141
157
|
throw new Error(`HTTP ${res.status} ${res.statusText} \u{2014} ${(null == init ? void 0 : init.method) ?? "GET"} ${url}\n${body}`);
|
|
142
158
|
}
|
|
143
159
|
return res.json();
|
|
144
160
|
}
|
|
145
|
-
async function fetchRaw(url, init) {
|
|
146
|
-
const res = await fetch(url, init);
|
|
161
|
+
async function fetchRaw(url, init, timeoutMs) {
|
|
162
|
+
const res = await fetch(url, withTimeout(init, timeoutMs));
|
|
147
163
|
if (!res.ok) {
|
|
148
164
|
const body = await res.text().catch(()=>"");
|
|
149
165
|
throw new Error(`HTTP ${res.status} ${res.statusText} \u{2014} ${(null == init ? void 0 : init.method) ?? "GET"} ${url}\n${body}`);
|
|
@@ -194,10 +210,26 @@ function logDryStep(scope, step, details) {
|
|
|
194
210
|
for (const [key, value] of Object.entries(details))if (null != value) console.log(labelLine(key, value));
|
|
195
211
|
}
|
|
196
212
|
}
|
|
213
|
+
const UPLOAD_TIMEOUT_MS = 120000;
|
|
214
|
+
const PUBLISH_TIMEOUT_MS = 60000;
|
|
197
215
|
const CWS_OAUTH_URL = "https://oauth2.googleapis.com/token";
|
|
198
216
|
const CWS_UPLOAD_BASE = "https://www.googleapis.com/upload/chromewebstore/v1.1/items";
|
|
199
217
|
const CWS_PUBLISH_BASE = "https://www.googleapis.com/chromewebstore/v1.1/items";
|
|
200
218
|
const CWS_ITEMS_BASE = "https://www.googleapis.com/chromewebstore/v1.1/items";
|
|
219
|
+
const CWS_V2_BASE = "https://chromewebstore.googleapis.com/v2";
|
|
220
|
+
const CWS_V2_UPLOAD_BASE = "https://chromewebstore.googleapis.com/upload/v2";
|
|
221
|
+
function resolveChromeApiVersion(opts) {
|
|
222
|
+
if ("v1.1" === opts.apiVersion || "v2" === opts.apiVersion) return opts.apiVersion;
|
|
223
|
+
return String(opts.publisherId || "").trim() ? "v2" : "v1.1";
|
|
224
|
+
}
|
|
225
|
+
function v2ItemName(publisherId, extensionId) {
|
|
226
|
+
return `publishers/${encodeURIComponent(publisherId)}/items/${encodeURIComponent(extensionId)}`;
|
|
227
|
+
}
|
|
228
|
+
function requirePublisherId(publisherId, op) {
|
|
229
|
+
const id = String(publisherId || "").trim();
|
|
230
|
+
if (!id) throw new Error(`Chrome ${op} on the v2 API needs a publisher ID (the UUID from your Chrome Web Store dev console URL).`);
|
|
231
|
+
return id;
|
|
232
|
+
}
|
|
201
233
|
function chromeStoreUrl(extensionId) {
|
|
202
234
|
return `https://chromewebstore.google.com/detail/${extensionId}`;
|
|
203
235
|
}
|
|
@@ -216,16 +248,28 @@ async function authenticate(clientId, clientSecret, refreshToken) {
|
|
|
216
248
|
}
|
|
217
249
|
});
|
|
218
250
|
}
|
|
219
|
-
async function chrome_upload(extensionId, zipPath, token) {
|
|
251
|
+
async function chrome_upload(extensionId, zipPath, token, ctx) {
|
|
220
252
|
const body = await promises_default.readFile(zipPath);
|
|
221
|
-
|
|
222
|
-
|
|
253
|
+
let url;
|
|
254
|
+
let method;
|
|
255
|
+
const headers = {
|
|
256
|
+
Authorization: `${token.token_type} ${token.access_token}`,
|
|
257
|
+
"Content-Type": "application/zip"
|
|
258
|
+
};
|
|
259
|
+
if ("v2" === ctx.apiVersion) {
|
|
260
|
+
const pub = requirePublisherId(ctx.publisherId, "upload");
|
|
261
|
+
url = `${CWS_V2_UPLOAD_BASE}/${v2ItemName(pub, extensionId)}:upload`;
|
|
262
|
+
method = "POST";
|
|
263
|
+
} else {
|
|
264
|
+
url = `${CWS_UPLOAD_BASE}/${extensionId}`;
|
|
265
|
+
method = "PUT";
|
|
266
|
+
headers["x-goog-api-version"] = "2";
|
|
267
|
+
}
|
|
268
|
+
const res = await fetch(url, {
|
|
269
|
+
method,
|
|
223
270
|
body,
|
|
224
|
-
headers
|
|
225
|
-
|
|
226
|
-
"x-goog-api-version": "2",
|
|
227
|
-
"Content-Type": "application/zip"
|
|
228
|
-
}
|
|
271
|
+
headers,
|
|
272
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
|
|
229
273
|
});
|
|
230
274
|
if (!res.ok) {
|
|
231
275
|
const text = await res.text().catch(()=>"");
|
|
@@ -233,6 +277,33 @@ async function chrome_upload(extensionId, zipPath, token) {
|
|
|
233
277
|
}
|
|
234
278
|
}
|
|
235
279
|
async function publish(params) {
|
|
280
|
+
if ("v2" === params.apiVersion) {
|
|
281
|
+
const pub = requirePublisherId(params.publisherId, "publish");
|
|
282
|
+
if ("trustedTesters" === params.publishTarget) throw new Error("The Chrome v2 API has no trusted-tester publish target. Use staged publish, or keep this project on the v1.1 API for trusted-tester releases.");
|
|
283
|
+
const reqBody = {
|
|
284
|
+
publishType: "DEFAULT_PUBLISH"
|
|
285
|
+
};
|
|
286
|
+
if (null != params.deployPercentage) reqBody.deployInfos = [
|
|
287
|
+
{
|
|
288
|
+
deployPercentage: params.deployPercentage
|
|
289
|
+
}
|
|
290
|
+
];
|
|
291
|
+
if (params.reviewExemption) reqBody.skipReview = true;
|
|
292
|
+
const res = await fetch(`${CWS_V2_BASE}/${v2ItemName(pub, params.extensionId)}:publish`, {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: {
|
|
295
|
+
Authorization: `${params.token.token_type} ${params.token.access_token}`,
|
|
296
|
+
"Content-Type": "application/json"
|
|
297
|
+
},
|
|
298
|
+
body: JSON.stringify(reqBody),
|
|
299
|
+
signal: AbortSignal.timeout(PUBLISH_TIMEOUT_MS)
|
|
300
|
+
});
|
|
301
|
+
if (!res.ok) {
|
|
302
|
+
const text = await res.text().catch(()=>"");
|
|
303
|
+
throw new Error(`Chrome publish failed: ${res.status} ${res.statusText}\n${text}`);
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
236
307
|
const url = new URL(`${CWS_PUBLISH_BASE}/${params.extensionId}/publish`);
|
|
237
308
|
url.searchParams.set("publishTarget", params.publishTarget);
|
|
238
309
|
if (null != params.deployPercentage) url.searchParams.set("deployPercentage", String(params.deployPercentage));
|
|
@@ -243,7 +314,8 @@ async function publish(params) {
|
|
|
243
314
|
Authorization: `${params.token.token_type} ${params.token.access_token}`,
|
|
244
315
|
"x-goog-api-version": "2",
|
|
245
316
|
"Content-Length": "0"
|
|
246
|
-
}
|
|
317
|
+
},
|
|
318
|
+
signal: AbortSignal.timeout(PUBLISH_TIMEOUT_MS)
|
|
247
319
|
});
|
|
248
320
|
if (!res.ok) {
|
|
249
321
|
const text = await res.text().catch(()=>"");
|
|
@@ -253,7 +325,8 @@ async function publish(params) {
|
|
|
253
325
|
async function publishChrome(options, dryRun) {
|
|
254
326
|
await assertFilePresent(options.zip);
|
|
255
327
|
const zipSize = await getFileSize(options.zip);
|
|
256
|
-
|
|
328
|
+
const apiVersion = resolveChromeApiVersion(options);
|
|
329
|
+
log("chrome", `Authenticating with Chrome Web Store (API ${apiVersion})`);
|
|
257
330
|
const token = await authenticate(options.clientId, options.clientSecret, options.refreshToken);
|
|
258
331
|
const outcome = {
|
|
259
332
|
storeUrl: chromeStoreUrl(options.extensionId),
|
|
@@ -262,25 +335,28 @@ async function publishChrome(options, dryRun) {
|
|
|
262
335
|
if (dryRun) {
|
|
263
336
|
logDryStep("chrome", "Authentication succeeded", {
|
|
264
337
|
tokenType: token.token_type,
|
|
265
|
-
scope: "chromewebstore"
|
|
338
|
+
scope: "chromewebstore",
|
|
339
|
+
apiVersion
|
|
266
340
|
});
|
|
267
341
|
logDryStep("chrome", "Would upload ZIP", {
|
|
268
342
|
file: options.zip,
|
|
269
343
|
size: formatBytes(zipSize),
|
|
270
|
-
|
|
271
|
-
method: "PUT"
|
|
344
|
+
apiVersion
|
|
272
345
|
});
|
|
273
346
|
options.skipSubmitReview ? logDryStep("chrome", "Would skip publish (skipSubmitReview=true)") : logDryStep("chrome", "Would publish for review", {
|
|
274
|
-
|
|
347
|
+
apiVersion,
|
|
275
348
|
publishTarget: options.publishTarget ?? "default",
|
|
276
349
|
deployPercentage: options.deployPercentage,
|
|
277
350
|
reviewExemption: options.reviewExemption
|
|
278
351
|
});
|
|
279
|
-
log("chrome", "Dry run complete
|
|
352
|
+
log("chrome", "Dry run complete - no changes made");
|
|
280
353
|
return outcome;
|
|
281
354
|
}
|
|
282
355
|
log("chrome", `Uploading ZIP file (${formatBytes(zipSize)})`);
|
|
283
|
-
await chrome_upload(options.extensionId, options.zip, token
|
|
356
|
+
await chrome_upload(options.extensionId, options.zip, token, {
|
|
357
|
+
apiVersion,
|
|
358
|
+
publisherId: options.publisherId
|
|
359
|
+
});
|
|
284
360
|
if (options.skipSubmitReview) {
|
|
285
361
|
log("chrome", "Skipping publish step (skipSubmitReview=true)");
|
|
286
362
|
return outcome;
|
|
@@ -291,16 +367,36 @@ async function publishChrome(options, dryRun) {
|
|
|
291
367
|
publishTarget: options.publishTarget,
|
|
292
368
|
token,
|
|
293
369
|
deployPercentage: options.deployPercentage,
|
|
294
|
-
reviewExemption: options.reviewExemption
|
|
370
|
+
reviewExemption: options.reviewExemption,
|
|
371
|
+
apiVersion,
|
|
372
|
+
publisherId: options.publisherId
|
|
295
373
|
});
|
|
296
374
|
return outcome;
|
|
297
375
|
}
|
|
298
376
|
async function getChromeItem(params) {
|
|
299
377
|
const token = await authenticate(params.clientId, params.clientSecret, params.refreshToken);
|
|
378
|
+
const auth = {
|
|
379
|
+
Authorization: `${token.token_type} ${token.access_token}`
|
|
380
|
+
};
|
|
381
|
+
const version = resolveChromeApiVersion(params);
|
|
382
|
+
if ("v2" === version) {
|
|
383
|
+
var _rev_distributionChannels_, _rev_distributionChannels;
|
|
384
|
+
const pub = requirePublisherId(params.publisherId, "status");
|
|
385
|
+
const status = await fetchJson(`${CWS_V2_BASE}/${v2ItemName(pub, params.extensionId)}:fetchStatus`, {
|
|
386
|
+
headers: auth
|
|
387
|
+
});
|
|
388
|
+
const rev = status.submittedItemRevisionStatus || status.publishedItemRevisionStatus;
|
|
389
|
+
return {
|
|
390
|
+
uploadState: status.lastAsyncUploadState,
|
|
391
|
+
crxVersion: null == rev ? void 0 : null == (_rev_distributionChannels = rev.distributionChannels) ? void 0 : null == (_rev_distributionChannels_ = _rev_distributionChannels[0]) ? void 0 : _rev_distributionChannels_.crxVersion,
|
|
392
|
+
publicKey: status.publicKey,
|
|
393
|
+
state: null == rev ? void 0 : rev.state
|
|
394
|
+
};
|
|
395
|
+
}
|
|
300
396
|
const url = `${CWS_ITEMS_BASE}/${encodeURIComponent(params.extensionId)}?projection=${params.projection ?? "DRAFT"}`;
|
|
301
397
|
return fetchJson(url, {
|
|
302
398
|
headers: {
|
|
303
|
-
|
|
399
|
+
...auth,
|
|
304
400
|
"x-goog-api-version": "2"
|
|
305
401
|
}
|
|
306
402
|
});
|
|
@@ -315,11 +411,24 @@ async function verifyChromeCredentials(params) {
|
|
|
315
411
|
message: `OAuth token exchange failed. Verify your Client ID, Client Secret, and Refresh Token are correct.\n${err instanceof Error ? err.message : err}`
|
|
316
412
|
};
|
|
317
413
|
}
|
|
414
|
+
const auth = {
|
|
415
|
+
Authorization: `${token.token_type} ${token.access_token}`
|
|
416
|
+
};
|
|
417
|
+
const version = resolveChromeApiVersion(params);
|
|
318
418
|
try {
|
|
419
|
+
if ("v2" === version) {
|
|
420
|
+
const pub = requirePublisherId(params.publisherId, "verify");
|
|
421
|
+
const status = await fetchJson(`${CWS_V2_BASE}/${v2ItemName(pub, params.extensionId)}:fetchStatus`, {
|
|
422
|
+
headers: auth
|
|
423
|
+
});
|
|
424
|
+
const rev = status.submittedItemRevisionStatus || status.publishedItemRevisionStatus;
|
|
425
|
+
return {
|
|
426
|
+
ok: true,
|
|
427
|
+
message: `Credentials verified (Chrome v2 API). Item state: ${(null == rev ? void 0 : rev.state) || "unknown"}.`
|
|
428
|
+
};
|
|
429
|
+
}
|
|
319
430
|
const item = await fetchJson(`${CWS_ITEMS_BASE}/${encodeURIComponent(params.extensionId)}?projection=DRAFT`, {
|
|
320
|
-
headers:
|
|
321
|
-
Authorization: `${token.token_type} ${token.access_token}`
|
|
322
|
-
}
|
|
431
|
+
headers: auth
|
|
323
432
|
});
|
|
324
433
|
const title = (null == item ? void 0 : item.title) || params.extensionId;
|
|
325
434
|
return {
|
|
@@ -335,7 +444,7 @@ async function verifyChromeCredentials(params) {
|
|
|
335
444
|
};
|
|
336
445
|
if (msg.includes("403")) return {
|
|
337
446
|
ok: false,
|
|
338
|
-
message: `Access denied to extension ${params.extensionId}. You may not be the owner or a developer on this item.`
|
|
447
|
+
message: "v2" === version ? "Access denied. Check the publisher ID matches this extension's account, and that the extension ID is correct." : `Access denied to extension ${params.extensionId}. You may not be the owner or a developer on this item.`
|
|
339
448
|
};
|
|
340
449
|
return {
|
|
341
450
|
ok: false,
|
|
@@ -349,12 +458,6 @@ function delay(ms) {
|
|
|
349
458
|
});
|
|
350
459
|
}
|
|
351
460
|
const AMO_BASE = "https://addons.mozilla.org/api/v5/addons";
|
|
352
|
-
const AMO_PROFILE_URL = "https://addons.mozilla.org/api/v5/accounts/profile/";
|
|
353
|
-
const POLL_INTERVAL_MS = 5000;
|
|
354
|
-
const VALIDATION_TIMEOUT_MS = 600000;
|
|
355
|
-
function firefoxStoreUrl(addonIdOrSlug) {
|
|
356
|
-
return `https://addons.mozilla.org/firefox/addon/${addonIdOrSlug}`;
|
|
357
|
-
}
|
|
358
461
|
function signJwt(issuer, secret, expiresInSeconds = 30) {
|
|
359
462
|
const header = {
|
|
360
463
|
alg: "HS256",
|
|
@@ -439,6 +542,12 @@ async function createAddon(params) {
|
|
|
439
542
|
headers: authHeaders(params.jwtIssuer, params.jwtSecret)
|
|
440
543
|
});
|
|
441
544
|
}
|
|
545
|
+
const AMO_PROFILE_URL = "https://addons.mozilla.org/api/v5/accounts/profile/";
|
|
546
|
+
const POLL_INTERVAL_MS = 5000;
|
|
547
|
+
const VALIDATION_TIMEOUT_MS = 600000;
|
|
548
|
+
function firefoxStoreUrl(addonIdOrSlug) {
|
|
549
|
+
return `https://addons.mozilla.org/firefox/addon/${addonIdOrSlug}`;
|
|
550
|
+
}
|
|
442
551
|
async function publishFirefox(options, dryRun) {
|
|
443
552
|
await assertFilePresent(options.zip);
|
|
444
553
|
const zipSize = await getFileSize(options.zip);
|
|
@@ -804,8 +913,134 @@ async function deploy(config) {
|
|
|
804
913
|
success
|
|
805
914
|
};
|
|
806
915
|
}
|
|
916
|
+
function usage() {
|
|
917
|
+
return `
|
|
918
|
+
extension-deploy \u{2014} Deploy browser extensions to Chrome, Firefox, and Edge.
|
|
919
|
+
|
|
920
|
+
USAGE
|
|
921
|
+
extension-deploy [options] submit to one or more stores
|
|
922
|
+
extension-deploy init [options] interactive credential setup wizard
|
|
923
|
+
extension-deploy watch --chrome [options] poll an in-flight submission
|
|
924
|
+
extension-deploy watch --firefox [options]
|
|
925
|
+
extension-deploy watch --edge [options]
|
|
926
|
+
|
|
927
|
+
GLOBAL
|
|
928
|
+
--dry-run Verify auth without uploading or publishing
|
|
929
|
+
--output-json <path> Write the DeployResult / WatchEvent as JSON to <path>
|
|
930
|
+
--help Show this help message
|
|
931
|
+
|
|
932
|
+
CHROME
|
|
933
|
+
--chrome-zip <path> Path to Chrome extension ZIP
|
|
934
|
+
--chrome-extension-id <id> Chrome extension ID
|
|
935
|
+
--chrome-client-id <id> OAuth2 client ID
|
|
936
|
+
--chrome-client-secret <secret> OAuth2 client secret
|
|
937
|
+
--chrome-refresh-token <token> OAuth2 refresh token
|
|
938
|
+
--chrome-publish-target <target> "default" or "trustedTesters"
|
|
939
|
+
--chrome-deploy-percentage <n> Staged rollout percentage (1-100)
|
|
940
|
+
--chrome-review-exemption Request expedited review
|
|
941
|
+
--chrome-skip-submit-review Upload only, skip publish
|
|
942
|
+
|
|
943
|
+
FIREFOX
|
|
944
|
+
--firefox-zip <path> Path to Firefox extension ZIP
|
|
945
|
+
--firefox-sources-zip <path> Path to sources ZIP (optional)
|
|
946
|
+
--firefox-extension-id <id> Addon GUID or email-style ID
|
|
947
|
+
--firefox-jwt-issuer <issuer> AMO JWT issuer
|
|
948
|
+
--firefox-jwt-secret <secret> AMO JWT secret
|
|
949
|
+
--firefox-channel <channel> "listed" or "unlisted"
|
|
950
|
+
|
|
951
|
+
EDGE
|
|
952
|
+
--edge-zip <path> Path to Edge extension ZIP
|
|
953
|
+
--edge-product-id <id> Partner Center product ID
|
|
954
|
+
--edge-client-id <id> Partner Center client ID
|
|
955
|
+
--edge-api-key <key> Partner Center API key (v1.1)
|
|
956
|
+
--edge-skip-submit-review Upload only, skip publish
|
|
957
|
+
|
|
958
|
+
WATCH
|
|
959
|
+
--watch-submission-id <id> Required for firefox/edge (version id / operation id)
|
|
960
|
+
--watch-interval <seconds> Poll interval (default 60)
|
|
961
|
+
--watch-timeout <seconds> Max time to wait for a terminal state (default 3600)
|
|
962
|
+
|
|
963
|
+
INIT
|
|
964
|
+
--init-stores <list> Comma-separated store list (skip picker)
|
|
965
|
+
--init-output <path> Destination file (default .env.submit)
|
|
966
|
+
--init-skip-verify Don't run verifyCredentials after collection
|
|
967
|
+
--init-force Overwrite existing .env.submit without asking
|
|
968
|
+
|
|
969
|
+
ENVIRONMENT VARIABLES
|
|
970
|
+
All flags can be set via SCREAMING_SNAKE_CASE env vars (e.g. CHROME_ZIP,
|
|
971
|
+
FIREFOX_JWT_SECRET, EDGE_API_KEY). Flags take precedence over env vars.
|
|
972
|
+
A .env.submit file in the current directory is loaded automatically.
|
|
973
|
+
`.trim();
|
|
974
|
+
}
|
|
975
|
+
const BOOLEAN_FLAGS = new Set([
|
|
976
|
+
"help",
|
|
977
|
+
"dryRun",
|
|
978
|
+
"chromeReviewExemption",
|
|
979
|
+
"chromeSkipSubmitReview",
|
|
980
|
+
"edgeSkipSubmitReview",
|
|
981
|
+
"initSkipVerify",
|
|
982
|
+
"initForce"
|
|
983
|
+
]);
|
|
984
|
+
const INT_FLAGS = new Set([
|
|
985
|
+
"chromeDeployPercentage",
|
|
986
|
+
"watchInterval",
|
|
987
|
+
"watchTimeout"
|
|
988
|
+
]);
|
|
989
|
+
function parseArgs(argv) {
|
|
990
|
+
const flags = {};
|
|
991
|
+
let i = 0;
|
|
992
|
+
if ("watch" === argv[0]) {
|
|
993
|
+
flags.command = "watch";
|
|
994
|
+
i = 1;
|
|
995
|
+
} else if ("init" === argv[0]) {
|
|
996
|
+
flags.command = "init";
|
|
997
|
+
i = 1;
|
|
998
|
+
}
|
|
999
|
+
while(i < argv.length){
|
|
1000
|
+
const arg = argv[i];
|
|
1001
|
+
if ("--help" === arg || "-h" === arg) {
|
|
1002
|
+
flags.help = true;
|
|
1003
|
+
i++;
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
if ("--chrome" === arg) {
|
|
1007
|
+
flags.watchStore = "chrome";
|
|
1008
|
+
i++;
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if ("--firefox" === arg) {
|
|
1012
|
+
flags.watchStore = "firefox";
|
|
1013
|
+
i++;
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if ("--edge" === arg) {
|
|
1017
|
+
flags.watchStore = "edge";
|
|
1018
|
+
i++;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (!(null == arg ? void 0 : arg.startsWith("--"))) {
|
|
1022
|
+
i++;
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
const key = arg.slice(2).replace(/-([a-z])/g, (_, c)=>c.toUpperCase());
|
|
1026
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
1027
|
+
flags[key] = true;
|
|
1028
|
+
i++;
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const next = argv[i + 1];
|
|
1032
|
+
if (null == next || next.startsWith("--")) {
|
|
1033
|
+
flags[key] = true;
|
|
1034
|
+
i++;
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
flags[key] = INT_FLAGS.has(key) ? parseInt(next, 10) : next;
|
|
1038
|
+
i += 2;
|
|
1039
|
+
}
|
|
1040
|
+
return flags;
|
|
1041
|
+
}
|
|
807
1042
|
const DEFAULT_POLL_INTERVAL_MS = 60000;
|
|
808
|
-
const
|
|
1043
|
+
const watch_DEFAULT_TIMEOUT_MS = 3600000;
|
|
809
1044
|
function watch_now() {
|
|
810
1045
|
return new Date().toISOString();
|
|
811
1046
|
}
|
|
@@ -932,7 +1167,7 @@ function watchFirefoxSubmission(input, options = {}) {
|
|
|
932
1167
|
}
|
|
933
1168
|
async function runWatchLoop(_store, probe, options) {
|
|
934
1169
|
const pollInterval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
935
|
-
const timeout = options.timeoutMs ??
|
|
1170
|
+
const timeout = options.timeoutMs ?? watch_DEFAULT_TIMEOUT_MS;
|
|
936
1171
|
const deadline = Date.now() + timeout;
|
|
937
1172
|
let lastEvent;
|
|
938
1173
|
while(true){
|
|
@@ -944,125 +1179,30 @@ async function runWatchLoop(_store, probe, options) {
|
|
|
944
1179
|
await delay(pollInterval);
|
|
945
1180
|
}
|
|
946
1181
|
}
|
|
947
|
-
|
|
948
|
-
{
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
value: "firefox",
|
|
954
|
-
label: "Firefox AMO (addons.mozilla.org)"
|
|
955
|
-
},
|
|
956
|
-
{
|
|
957
|
-
value: "edge",
|
|
958
|
-
label: "Edge Add-ons (Partner Center)"
|
|
1182
|
+
async function requireValue(io, label, hint, opts) {
|
|
1183
|
+
const question = hint ? `${label} ${hint}:` : `${label}:`;
|
|
1184
|
+
for(let attempt = 0; attempt < 3; attempt++){
|
|
1185
|
+
const answer = (await io.prompt(question, opts)).trim();
|
|
1186
|
+
if (answer) return answer;
|
|
1187
|
+
io.log(` ${label} is required.`);
|
|
959
1188
|
}
|
|
960
|
-
|
|
1189
|
+
throw new Error(`init: ${label} is required.`);
|
|
1190
|
+
}
|
|
961
1191
|
const CHROME_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
962
1192
|
const CHROME_OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
|
|
963
1193
|
const CHROME_OOB_REDIRECT = "urn:ietf:wg:oauth:2.0:oob";
|
|
964
1194
|
const CHROME_SCOPE = "https://www.googleapis.com/auth/chromewebstore";
|
|
965
|
-
async function
|
|
1195
|
+
async function collectChromeCredentials(deps) {
|
|
966
1196
|
const { io } = deps;
|
|
1197
|
+
io.log("Chrome Web Store needs OAuth credentials. You'll need:");
|
|
1198
|
+
io.log(" 1. A Google Cloud project with the Chrome Web Store API enabled:");
|
|
1199
|
+
io.log(" https://console.cloud.google.com/apis/library/chromewebstore.googleapis.com");
|
|
1200
|
+
io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
|
|
1201
|
+
io.log(" https://console.cloud.google.com/apis/credentials");
|
|
1202
|
+
io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
|
|
967
1203
|
io.log("");
|
|
968
|
-
io.log(
|
|
969
|
-
io.log("");
|
|
970
|
-
io.log("This wizard walks you through creating credentials for each store and");
|
|
971
|
-
io.log("writes them to a .env.submit file. Press Ctrl+C at any time to abort.");
|
|
972
|
-
io.log("");
|
|
973
|
-
const stores = options.stores ? options.stores : await io.multiselect("Which stores do you want to configure?", STORE_CHOICES);
|
|
974
|
-
if (0 === stores.length) throw new Error("init: at least one store must be selected. Use --init-stores chrome,firefox,edge or make a selection.");
|
|
975
|
-
const envLines = [
|
|
976
|
-
"# Generated by `extension-deploy init`.",
|
|
977
|
-
"# Do not commit secrets to source control.",
|
|
978
|
-
""
|
|
979
|
-
];
|
|
980
|
-
const verified = {};
|
|
981
|
-
for (const store of stores){
|
|
982
|
-
io.log("");
|
|
983
|
-
io.log(colors.underline(colors.blue(labelFor(store))));
|
|
984
|
-
io.log("");
|
|
985
|
-
if ("chrome" === store) {
|
|
986
|
-
const creds = await collectChromeCredentials(deps);
|
|
987
|
-
envLines.push(`CHROME_EXTENSION_ID=${creds.extensionId}`, `CHROME_CLIENT_ID=${creds.clientId}`, `CHROME_CLIENT_SECRET=${creds.clientSecret}`, `CHROME_REFRESH_TOKEN=${creds.refreshToken}`, "");
|
|
988
|
-
if (!options.skipVerify) {
|
|
989
|
-
const verify = deps.verifyChrome ?? verifyChromeCredentials;
|
|
990
|
-
verified.chrome = normalizeVerify(await verify(creds));
|
|
991
|
-
logVerify(io, "chrome", verified.chrome);
|
|
992
|
-
}
|
|
993
|
-
} else if ("firefox" === store) {
|
|
994
|
-
const creds = await collectFirefoxCredentials(deps);
|
|
995
|
-
envLines.push(`FIREFOX_EXTENSION_ID=${creds.addonId}`, `FIREFOX_JWT_ISSUER=${creds.jwtIssuer}`, `FIREFOX_JWT_SECRET=${creds.jwtSecret}`, "");
|
|
996
|
-
if (!options.skipVerify) {
|
|
997
|
-
const verify = deps.verifyFirefox ?? verifyFirefoxCredentials;
|
|
998
|
-
verified.firefox = normalizeVerify(await verify({
|
|
999
|
-
jwtIssuer: creds.jwtIssuer,
|
|
1000
|
-
jwtSecret: creds.jwtSecret,
|
|
1001
|
-
addonId: creds.addonId
|
|
1002
|
-
}));
|
|
1003
|
-
logVerify(io, "firefox", verified.firefox);
|
|
1004
|
-
}
|
|
1005
|
-
} else {
|
|
1006
|
-
const creds = await collectEdgeCredentials(deps);
|
|
1007
|
-
envLines.push(`EDGE_PRODUCT_ID=${creds.productId}`, `EDGE_CLIENT_ID=${creds.clientId}`, `EDGE_API_KEY=${creds.apiKey}`, "");
|
|
1008
|
-
if (!options.skipVerify) {
|
|
1009
|
-
const verify = deps.verifyEdge ?? verifyEdgeCredentials;
|
|
1010
|
-
verified.edge = normalizeVerify(await verify({
|
|
1011
|
-
clientId: creds.clientId,
|
|
1012
|
-
apiKey: creds.apiKey,
|
|
1013
|
-
productId: creds.productId
|
|
1014
|
-
}));
|
|
1015
|
-
logVerify(io, "edge", verified.edge);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
const envContent = envLines.join("\n");
|
|
1020
|
-
const envPath = resolveEnvPath(deps.cwd, options.outputPath);
|
|
1021
|
-
const exists = await deps.fileExists(envPath);
|
|
1022
|
-
let wrote = false;
|
|
1023
|
-
if (exists && !options.force) {
|
|
1024
|
-
const overwrite = await deps.io.confirm(`${envPath} already exists. Overwrite?`, false);
|
|
1025
|
-
if (overwrite) {
|
|
1026
|
-
await deps.writeFile(envPath, envContent);
|
|
1027
|
-
wrote = true;
|
|
1028
|
-
} else {
|
|
1029
|
-
io.log("");
|
|
1030
|
-
io.log(`Skipped writing ${envPath}. Credential block:`);
|
|
1031
|
-
io.log("");
|
|
1032
|
-
io.log(envContent);
|
|
1033
|
-
}
|
|
1034
|
-
} else {
|
|
1035
|
-
await deps.writeFile(envPath, envContent);
|
|
1036
|
-
wrote = true;
|
|
1037
|
-
}
|
|
1038
|
-
if (wrote) {
|
|
1039
|
-
io.log("");
|
|
1040
|
-
io.log(`${colors.green("\u23F5\u23F5\u23F5")} ${colors.blue("init")} Wrote ${colors.underline(envPath)}`);
|
|
1041
|
-
io.log("");
|
|
1042
|
-
io.log(colors.gray("Next steps:"));
|
|
1043
|
-
io.log(" - Add .env.submit to .gitignore if it isn't already (it contains secrets).");
|
|
1044
|
-
io.log(" - Run `extension-deploy --dry-run` to sanity-check the full submit flow.");
|
|
1045
|
-
io.log("");
|
|
1046
|
-
}
|
|
1047
|
-
return {
|
|
1048
|
-
stores,
|
|
1049
|
-
envPath,
|
|
1050
|
-
envContent,
|
|
1051
|
-
verified,
|
|
1052
|
-
wrote
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
|
-
async function collectChromeCredentials(deps) {
|
|
1056
|
-
const { io } = deps;
|
|
1057
|
-
io.log("Chrome Web Store needs OAuth credentials. You'll need:");
|
|
1058
|
-
io.log(" 1. A Google Cloud project with the Chrome Web Store API enabled:");
|
|
1059
|
-
io.log(" https://console.cloud.google.com/apis/library/chromewebstore.googleapis.com");
|
|
1060
|
-
io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
|
|
1061
|
-
io.log(" https://console.cloud.google.com/apis/credentials");
|
|
1062
|
-
io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
|
|
1063
|
-
io.log("");
|
|
1064
|
-
io.log("This wizard will take your Client ID + Secret and walk you through");
|
|
1065
|
-
io.log("generating a refresh token automatically.");
|
|
1204
|
+
io.log("This wizard will take your Client ID + Secret and walk you through");
|
|
1205
|
+
io.log("generating a refresh token automatically.");
|
|
1066
1206
|
io.log("");
|
|
1067
1207
|
const extensionId = await requireValue(io, "Chrome extension ID", "(32-character ID from the CWS dashboard URL)");
|
|
1068
1208
|
const clientId = await requireValue(io, "OAuth Client ID");
|
|
@@ -1167,6 +1307,110 @@ async function collectEdgeCredentials(deps) {
|
|
|
1167
1307
|
apiKey
|
|
1168
1308
|
};
|
|
1169
1309
|
}
|
|
1310
|
+
const STORE_CHOICES = [
|
|
1311
|
+
{
|
|
1312
|
+
value: "chrome",
|
|
1313
|
+
label: "Chrome Web Store"
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
value: "firefox",
|
|
1317
|
+
label: "Firefox AMO (addons.mozilla.org)"
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
value: "edge",
|
|
1321
|
+
label: "Edge Add-ons (Partner Center)"
|
|
1322
|
+
}
|
|
1323
|
+
];
|
|
1324
|
+
async function runInit(deps, options = {}) {
|
|
1325
|
+
const { io } = deps;
|
|
1326
|
+
io.log("");
|
|
1327
|
+
io.log(`${colors.green("\u23F5\u23F5\u23F5")} ${colors.blue("init")} ${colors.underline("extension-deploy \u2014 set up store credentials")}`);
|
|
1328
|
+
io.log("");
|
|
1329
|
+
io.log("This wizard walks you through creating credentials for each store and");
|
|
1330
|
+
io.log("writes them to a .env.submit file. Press Ctrl+C at any time to abort.");
|
|
1331
|
+
io.log("");
|
|
1332
|
+
const stores = options.stores ? options.stores : await io.multiselect("Which stores do you want to configure?", STORE_CHOICES);
|
|
1333
|
+
if (0 === stores.length) throw new Error("init: at least one store must be selected. Use --init-stores chrome,firefox,edge or make a selection.");
|
|
1334
|
+
const envLines = [
|
|
1335
|
+
"# Generated by `extension-deploy init`.",
|
|
1336
|
+
"# Do not commit secrets to source control.",
|
|
1337
|
+
""
|
|
1338
|
+
];
|
|
1339
|
+
const verified = {};
|
|
1340
|
+
for (const store of stores){
|
|
1341
|
+
io.log("");
|
|
1342
|
+
io.log(colors.underline(colors.blue(labelFor(store))));
|
|
1343
|
+
io.log("");
|
|
1344
|
+
if ("chrome" === store) {
|
|
1345
|
+
const creds = await collectChromeCredentials(deps);
|
|
1346
|
+
envLines.push(`CHROME_EXTENSION_ID=${creds.extensionId}`, `CHROME_CLIENT_ID=${creds.clientId}`, `CHROME_CLIENT_SECRET=${creds.clientSecret}`, `CHROME_REFRESH_TOKEN=${creds.refreshToken}`, "");
|
|
1347
|
+
if (!options.skipVerify) {
|
|
1348
|
+
const verify = deps.verifyChrome ?? verifyChromeCredentials;
|
|
1349
|
+
verified.chrome = normalizeVerify(await verify(creds));
|
|
1350
|
+
logVerify(io, "chrome", verified.chrome);
|
|
1351
|
+
}
|
|
1352
|
+
} else if ("firefox" === store) {
|
|
1353
|
+
const creds = await collectFirefoxCredentials(deps);
|
|
1354
|
+
envLines.push(`FIREFOX_EXTENSION_ID=${creds.addonId}`, `FIREFOX_JWT_ISSUER=${creds.jwtIssuer}`, `FIREFOX_JWT_SECRET=${creds.jwtSecret}`, "");
|
|
1355
|
+
if (!options.skipVerify) {
|
|
1356
|
+
const verify = deps.verifyFirefox ?? verifyFirefoxCredentials;
|
|
1357
|
+
verified.firefox = normalizeVerify(await verify({
|
|
1358
|
+
jwtIssuer: creds.jwtIssuer,
|
|
1359
|
+
jwtSecret: creds.jwtSecret,
|
|
1360
|
+
addonId: creds.addonId
|
|
1361
|
+
}));
|
|
1362
|
+
logVerify(io, "firefox", verified.firefox);
|
|
1363
|
+
}
|
|
1364
|
+
} else {
|
|
1365
|
+
const creds = await collectEdgeCredentials(deps);
|
|
1366
|
+
envLines.push(`EDGE_PRODUCT_ID=${creds.productId}`, `EDGE_CLIENT_ID=${creds.clientId}`, `EDGE_API_KEY=${creds.apiKey}`, "");
|
|
1367
|
+
if (!options.skipVerify) {
|
|
1368
|
+
const verify = deps.verifyEdge ?? verifyEdgeCredentials;
|
|
1369
|
+
verified.edge = normalizeVerify(await verify({
|
|
1370
|
+
clientId: creds.clientId,
|
|
1371
|
+
apiKey: creds.apiKey,
|
|
1372
|
+
productId: creds.productId
|
|
1373
|
+
}));
|
|
1374
|
+
logVerify(io, "edge", verified.edge);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const envContent = envLines.join("\n");
|
|
1379
|
+
const envPath = resolveEnvPath(deps.cwd, options.outputPath);
|
|
1380
|
+
const exists = await deps.fileExists(envPath);
|
|
1381
|
+
let wrote = false;
|
|
1382
|
+
if (exists && !options.force) {
|
|
1383
|
+
const overwrite = await deps.io.confirm(`${envPath} already exists. Overwrite?`, false);
|
|
1384
|
+
if (overwrite) {
|
|
1385
|
+
await deps.writeFile(envPath, envContent);
|
|
1386
|
+
wrote = true;
|
|
1387
|
+
} else {
|
|
1388
|
+
io.log("");
|
|
1389
|
+
io.log(`Skipped writing ${envPath}. Credential block:`);
|
|
1390
|
+
io.log("");
|
|
1391
|
+
io.log(envContent);
|
|
1392
|
+
}
|
|
1393
|
+
} else {
|
|
1394
|
+
await deps.writeFile(envPath, envContent);
|
|
1395
|
+
wrote = true;
|
|
1396
|
+
}
|
|
1397
|
+
if (wrote) {
|
|
1398
|
+
io.log("");
|
|
1399
|
+
io.log(`${colors.green("\u23F5\u23F5\u23F5")} ${colors.blue("init")} Wrote ${colors.underline(envPath)}`);
|
|
1400
|
+
io.log("");
|
|
1401
|
+
io.log(colors.gray("Next steps:"));
|
|
1402
|
+
io.log(" - Add .env.submit to .gitignore if it isn't already (it contains secrets).");
|
|
1403
|
+
io.log(" - Run `extension-deploy --dry-run` to sanity-check the full submit flow.");
|
|
1404
|
+
io.log("");
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
stores,
|
|
1408
|
+
envPath,
|
|
1409
|
+
envContent,
|
|
1410
|
+
verified,
|
|
1411
|
+
wrote
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1170
1414
|
function labelFor(store) {
|
|
1171
1415
|
var _STORE_CHOICES_find;
|
|
1172
1416
|
return (null == (_STORE_CHOICES_find = STORE_CHOICES.find((c)=>c.value === store)) ? void 0 : _STORE_CHOICES_find.label) ?? String(store);
|
|
@@ -1185,15 +1429,6 @@ function resolveEnvPath(cwd, outputPath) {
|
|
|
1185
1429
|
const target = outputPath ?? ".env.submit";
|
|
1186
1430
|
return external_node_path_default.isAbsolute(target) ? target : external_node_path_default.join(cwd, target);
|
|
1187
1431
|
}
|
|
1188
|
-
async function requireValue(io, label, hint, opts) {
|
|
1189
|
-
const question = hint ? `${label} ${hint}:` : `${label}:`;
|
|
1190
|
-
for(let attempt = 0; attempt < 3; attempt++){
|
|
1191
|
-
const answer = (await io.prompt(question, opts)).trim();
|
|
1192
|
-
if (answer) return answer;
|
|
1193
|
-
io.log(` ${label} is required.`);
|
|
1194
|
-
}
|
|
1195
|
-
throw new Error(`init: ${label} is required.`);
|
|
1196
|
-
}
|
|
1197
1432
|
function createStdinIO() {
|
|
1198
1433
|
const rl = external_node_readline_default.createInterface({
|
|
1199
1434
|
input: process.stdin,
|
|
@@ -1247,7 +1482,13 @@ function createStdinIO() {
|
|
|
1247
1482
|
}
|
|
1248
1483
|
const stdinFs = {
|
|
1249
1484
|
async writeFile (filePath, content) {
|
|
1250
|
-
await promises_default.writeFile(filePath, content,
|
|
1485
|
+
await promises_default.writeFile(filePath, content, {
|
|
1486
|
+
encoding: "utf-8",
|
|
1487
|
+
mode: 384
|
|
1488
|
+
});
|
|
1489
|
+
try {
|
|
1490
|
+
await promises_default.chmod(filePath, 384);
|
|
1491
|
+
} catch {}
|
|
1251
1492
|
},
|
|
1252
1493
|
async fileExists (filePath) {
|
|
1253
1494
|
try {
|
|
@@ -1258,173 +1499,12 @@ const stdinFs = {
|
|
|
1258
1499
|
}
|
|
1259
1500
|
}
|
|
1260
1501
|
};
|
|
1261
|
-
function usage() {
|
|
1262
|
-
return `
|
|
1263
|
-
extension-deploy \u{2014} Deploy browser extensions to Chrome, Firefox, and Edge.
|
|
1264
|
-
|
|
1265
|
-
USAGE
|
|
1266
|
-
extension-deploy [options] submit to one or more stores
|
|
1267
|
-
extension-deploy init [options] interactive credential setup wizard
|
|
1268
|
-
extension-deploy watch --chrome [options] poll an in-flight submission
|
|
1269
|
-
extension-deploy watch --firefox [options]
|
|
1270
|
-
extension-deploy watch --edge [options]
|
|
1271
|
-
|
|
1272
|
-
GLOBAL
|
|
1273
|
-
--dry-run Verify auth without uploading or publishing
|
|
1274
|
-
--output-json <path> Write the DeployResult / WatchEvent as JSON to <path>
|
|
1275
|
-
--help Show this help message
|
|
1276
|
-
|
|
1277
|
-
CHROME
|
|
1278
|
-
--chrome-zip <path> Path to Chrome extension ZIP
|
|
1279
|
-
--chrome-extension-id <id> Chrome extension ID
|
|
1280
|
-
--chrome-client-id <id> OAuth2 client ID
|
|
1281
|
-
--chrome-client-secret <secret> OAuth2 client secret
|
|
1282
|
-
--chrome-refresh-token <token> OAuth2 refresh token
|
|
1283
|
-
--chrome-publish-target <target> "default" or "trustedTesters"
|
|
1284
|
-
--chrome-deploy-percentage <n> Staged rollout percentage (1-100)
|
|
1285
|
-
--chrome-review-exemption Request expedited review
|
|
1286
|
-
--chrome-skip-submit-review Upload only, skip publish
|
|
1287
|
-
|
|
1288
|
-
FIREFOX
|
|
1289
|
-
--firefox-zip <path> Path to Firefox extension ZIP
|
|
1290
|
-
--firefox-sources-zip <path> Path to sources ZIP (optional)
|
|
1291
|
-
--firefox-extension-id <id> Addon GUID or email-style ID
|
|
1292
|
-
--firefox-jwt-issuer <issuer> AMO JWT issuer
|
|
1293
|
-
--firefox-jwt-secret <secret> AMO JWT secret
|
|
1294
|
-
--firefox-channel <channel> "listed" or "unlisted"
|
|
1295
|
-
|
|
1296
|
-
EDGE
|
|
1297
|
-
--edge-zip <path> Path to Edge extension ZIP
|
|
1298
|
-
--edge-product-id <id> Partner Center product ID
|
|
1299
|
-
--edge-client-id <id> Partner Center client ID
|
|
1300
|
-
--edge-api-key <key> Partner Center API key (v1.1)
|
|
1301
|
-
--edge-skip-submit-review Upload only, skip publish
|
|
1302
|
-
|
|
1303
|
-
WATCH
|
|
1304
|
-
--watch-submission-id <id> Required for firefox/edge (version id / operation id)
|
|
1305
|
-
--watch-interval <seconds> Poll interval (default 60)
|
|
1306
|
-
--watch-timeout <seconds> Max time to wait for a terminal state (default 3600)
|
|
1307
|
-
|
|
1308
|
-
INIT
|
|
1309
|
-
--init-stores <list> Comma-separated store list (skip picker)
|
|
1310
|
-
--init-output <path> Destination file (default .env.submit)
|
|
1311
|
-
--init-skip-verify Don't run verifyCredentials after collection
|
|
1312
|
-
--init-force Overwrite existing .env.submit without asking
|
|
1313
|
-
|
|
1314
|
-
ENVIRONMENT VARIABLES
|
|
1315
|
-
All flags can be set via SCREAMING_SNAKE_CASE env vars (e.g. CHROME_ZIP,
|
|
1316
|
-
FIREFOX_JWT_SECRET, EDGE_API_KEY). Flags take precedence over env vars.
|
|
1317
|
-
A .env.submit file in the current directory is loaded automatically.
|
|
1318
|
-
`.trim();
|
|
1319
|
-
}
|
|
1320
|
-
const BOOLEAN_FLAGS = new Set([
|
|
1321
|
-
"help",
|
|
1322
|
-
"dryRun",
|
|
1323
|
-
"chromeReviewExemption",
|
|
1324
|
-
"chromeSkipSubmitReview",
|
|
1325
|
-
"edgeSkipSubmitReview",
|
|
1326
|
-
"initSkipVerify",
|
|
1327
|
-
"initForce"
|
|
1328
|
-
]);
|
|
1329
|
-
const INT_FLAGS = new Set([
|
|
1330
|
-
"chromeDeployPercentage",
|
|
1331
|
-
"watchInterval",
|
|
1332
|
-
"watchTimeout"
|
|
1333
|
-
]);
|
|
1334
|
-
function parseArgs(argv) {
|
|
1335
|
-
const flags = {};
|
|
1336
|
-
let i = 0;
|
|
1337
|
-
if ("watch" === argv[0]) {
|
|
1338
|
-
flags.command = "watch";
|
|
1339
|
-
i = 1;
|
|
1340
|
-
} else if ("init" === argv[0]) {
|
|
1341
|
-
flags.command = "init";
|
|
1342
|
-
i = 1;
|
|
1343
|
-
}
|
|
1344
|
-
while(i < argv.length){
|
|
1345
|
-
const arg = argv[i];
|
|
1346
|
-
if ("--help" === arg || "-h" === arg) {
|
|
1347
|
-
flags.help = true;
|
|
1348
|
-
i++;
|
|
1349
|
-
continue;
|
|
1350
|
-
}
|
|
1351
|
-
if ("--chrome" === arg) {
|
|
1352
|
-
flags.watchStore = "chrome";
|
|
1353
|
-
i++;
|
|
1354
|
-
continue;
|
|
1355
|
-
}
|
|
1356
|
-
if ("--firefox" === arg) {
|
|
1357
|
-
flags.watchStore = "firefox";
|
|
1358
|
-
i++;
|
|
1359
|
-
continue;
|
|
1360
|
-
}
|
|
1361
|
-
if ("--edge" === arg) {
|
|
1362
|
-
flags.watchStore = "edge";
|
|
1363
|
-
i++;
|
|
1364
|
-
continue;
|
|
1365
|
-
}
|
|
1366
|
-
if (!(null == arg ? void 0 : arg.startsWith("--"))) {
|
|
1367
|
-
i++;
|
|
1368
|
-
continue;
|
|
1369
|
-
}
|
|
1370
|
-
const key = arg.slice(2).replace(/-([a-z])/g, (_, c)=>c.toUpperCase());
|
|
1371
|
-
if (BOOLEAN_FLAGS.has(key)) {
|
|
1372
|
-
flags[key] = true;
|
|
1373
|
-
i++;
|
|
1374
|
-
continue;
|
|
1375
|
-
}
|
|
1376
|
-
const next = argv[i + 1];
|
|
1377
|
-
if (null == next || next.startsWith("--")) {
|
|
1378
|
-
flags[key] = true;
|
|
1379
|
-
i++;
|
|
1380
|
-
continue;
|
|
1381
|
-
}
|
|
1382
|
-
flags[key] = INT_FLAGS.has(key) ? parseInt(next, 10) : next;
|
|
1383
|
-
i += 2;
|
|
1384
|
-
}
|
|
1385
|
-
return flags;
|
|
1386
|
-
}
|
|
1387
|
-
function loadDotEnv() {
|
|
1388
|
-
try {
|
|
1389
|
-
const envPath = external_node_path_default.resolve(process.cwd(), ".env.submit");
|
|
1390
|
-
const content = external_node_fs_default.readFileSync(envPath, "utf-8");
|
|
1391
|
-
for (const line of content.split("\n")){
|
|
1392
|
-
const trimmed = line.trim();
|
|
1393
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1394
|
-
const eqIdx = trimmed.indexOf("=");
|
|
1395
|
-
if (-1 === eqIdx) continue;
|
|
1396
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
1397
|
-
let value = trimmed.slice(eqIdx + 1).trim();
|
|
1398
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1399
|
-
if (!(key in process.env)) process.env[key] = value;
|
|
1400
|
-
}
|
|
1401
|
-
} catch {}
|
|
1402
|
-
}
|
|
1403
1502
|
function writeJsonOutput(filePath, payload) {
|
|
1404
1503
|
external_node_fs_default.mkdirSync(external_node_path_default.dirname(external_node_path_default.resolve(filePath)), {
|
|
1405
1504
|
recursive: true
|
|
1406
1505
|
});
|
|
1407
1506
|
external_node_fs_default.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
1408
1507
|
}
|
|
1409
|
-
async function main() {
|
|
1410
|
-
loadDotEnv();
|
|
1411
|
-
const flags = parseArgs(process.argv.slice(2));
|
|
1412
|
-
if (flags.help) {
|
|
1413
|
-
console.log(usage());
|
|
1414
|
-
process.exit(0);
|
|
1415
|
-
}
|
|
1416
|
-
if ("watch" === flags.command) return void await runWatch(flags);
|
|
1417
|
-
if ("init" === flags.command) return void await runInitCommand(flags);
|
|
1418
|
-
const config = resolveConfig(flags);
|
|
1419
|
-
try {
|
|
1420
|
-
const result = await deploy(config);
|
|
1421
|
-
if (flags.outputJson) writeJsonOutput(flags.outputJson, result);
|
|
1422
|
-
if (!result.success) process.exit(1);
|
|
1423
|
-
} catch (err) {
|
|
1424
|
-
logError("deploy", err instanceof Error ? err.message : String(err));
|
|
1425
|
-
process.exit(1);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
1508
|
async function runWatch(flags) {
|
|
1429
1509
|
const store = flags.watchStore;
|
|
1430
1510
|
if (!store) {
|
|
@@ -1520,4 +1600,39 @@ function parseInitStores(raw) {
|
|
|
1520
1600
|
if (0 === parsed.length) throw new Error(`--init-stores: expected a comma-separated list of chrome,firefox,edge (got "${raw}")`);
|
|
1521
1601
|
return parsed;
|
|
1522
1602
|
}
|
|
1603
|
+
function loadDotEnv() {
|
|
1604
|
+
try {
|
|
1605
|
+
const envPath = external_node_path_default.resolve(process.cwd(), ".env.submit");
|
|
1606
|
+
const content = external_node_fs_default.readFileSync(envPath, "utf-8");
|
|
1607
|
+
for (const line of content.split("\n")){
|
|
1608
|
+
const trimmed = line.trim();
|
|
1609
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1610
|
+
const eqIdx = trimmed.indexOf("=");
|
|
1611
|
+
if (-1 === eqIdx) continue;
|
|
1612
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
1613
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
1614
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1615
|
+
if (!(key in process.env)) process.env[key] = value;
|
|
1616
|
+
}
|
|
1617
|
+
} catch {}
|
|
1618
|
+
}
|
|
1619
|
+
async function main() {
|
|
1620
|
+
loadDotEnv();
|
|
1621
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
1622
|
+
if (flags.help) {
|
|
1623
|
+
console.log(usage());
|
|
1624
|
+
process.exit(0);
|
|
1625
|
+
}
|
|
1626
|
+
if ("watch" === flags.command) return void await runWatch(flags);
|
|
1627
|
+
if ("init" === flags.command) return void await runInitCommand(flags);
|
|
1628
|
+
const config = resolveConfig(flags);
|
|
1629
|
+
try {
|
|
1630
|
+
const result = await deploy(config);
|
|
1631
|
+
if (flags.outputJson) writeJsonOutput(flags.outputJson, result);
|
|
1632
|
+
if (!result.success) process.exit(1);
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
logError("deploy", err instanceof Error ? err.message : String(err));
|
|
1635
|
+
process.exit(1);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1523
1638
|
export { chromeOptionsSchema, deploy, deployConfigSchema, edgeOptionsSchema, firefoxOptionsSchema, getChromeSubmissionEvent, getEdgeSubmissionEvent, getFirefoxSubmissionEvent, main, resolveConfig, validateConfig, verifyChromeCredentials, verifyEdgeCredentials, verifyFirefoxCredentials, watchChromeSubmission, watchEdgeSubmission, watchFirefoxSubmission };
|