@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.js
CHANGED
|
@@ -68,7 +68,12 @@ const chromeOptionsSchema = external_zod_namespaceObject.z.object({
|
|
|
68
68
|
]).default("default"),
|
|
69
69
|
deployPercentage: external_zod_namespaceObject.z.number().int().min(1).max(100).optional(),
|
|
70
70
|
reviewExemption: external_zod_namespaceObject.z.boolean().default(false),
|
|
71
|
-
skipSubmitReview: external_zod_namespaceObject.z.boolean().default(false)
|
|
71
|
+
skipSubmitReview: external_zod_namespaceObject.z.boolean().default(false),
|
|
72
|
+
publisherId: external_zod_namespaceObject.z.string().trim().optional(),
|
|
73
|
+
apiVersion: external_zod_namespaceObject.z["enum"]([
|
|
74
|
+
"v1.1",
|
|
75
|
+
"v2"
|
|
76
|
+
]).optional()
|
|
72
77
|
});
|
|
73
78
|
const firefoxOptionsSchema = external_zod_namespaceObject.z.object({
|
|
74
79
|
zip: external_zod_namespaceObject.z.string().min(1),
|
|
@@ -118,7 +123,9 @@ function resolveConfig(flags) {
|
|
|
118
123
|
publishTarget: flags.chromePublishTarget ?? envStr("CHROME_PUBLISH_TARGET"),
|
|
119
124
|
deployPercentage: flags.chromeDeployPercentage ?? envInt("CHROME_DEPLOY_PERCENTAGE"),
|
|
120
125
|
reviewExemption: flags.chromeReviewExemption ?? envBool("CHROME_REVIEW_EXEMPTION"),
|
|
121
|
-
skipSubmitReview: flags.chromeSkipSubmitReview ?? envBool("CHROME_SKIP_SUBMIT_REVIEW")
|
|
126
|
+
skipSubmitReview: flags.chromeSkipSubmitReview ?? envBool("CHROME_SKIP_SUBMIT_REVIEW"),
|
|
127
|
+
publisherId: flags.chromePublisherId ?? envStr("CHROME_PUBLISHER_ID"),
|
|
128
|
+
apiVersion: flags.chromeApiVersion ?? envStr("CHROME_API_VERSION")
|
|
122
129
|
},
|
|
123
130
|
firefox: null == firefoxZip ? void 0 : {
|
|
124
131
|
zip: firefoxZip,
|
|
@@ -145,6 +152,7 @@ const SETUP_HINTS = {
|
|
|
145
152
|
CHROME_CLIENT_ID: "Create OAuth 2.0 credentials at https://console.cloud.google.com/apis/credentials",
|
|
146
153
|
CHROME_CLIENT_SECRET: "Generated alongside the Client ID in the Google Cloud Console.",
|
|
147
154
|
CHROME_REFRESH_TOKEN: "Obtain by completing the OAuth consent flow. See the Chrome Web Store API docs.",
|
|
155
|
+
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.",
|
|
148
156
|
CHROME_ZIP: "Path to the .zip file built for Chrome Web Store upload.",
|
|
149
157
|
FIREFOX_EXTENSION_ID: "The addon GUID ({uuid} format) or email-style ID from your addon's AMO page.",
|
|
150
158
|
FIREFOX_JWT_ISSUER: "API key from https://addons.mozilla.org/developers/addon/api/key/",
|
|
@@ -187,16 +195,24 @@ function envInt(name) {
|
|
|
187
195
|
}
|
|
188
196
|
const promises_namespaceObject = require("node:fs/promises");
|
|
189
197
|
var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
199
|
+
function withTimeout(init, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
200
|
+
if (null == init ? void 0 : init.signal) return init;
|
|
201
|
+
return {
|
|
202
|
+
...init || {},
|
|
203
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async function fetchJson(url, init, timeoutMs) {
|
|
207
|
+
const res = await fetch(url, withTimeout(init, timeoutMs));
|
|
192
208
|
if (!res.ok) {
|
|
193
209
|
const body = await res.text().catch(()=>"");
|
|
194
210
|
throw new Error(`HTTP ${res.status} ${res.statusText} \u{2014} ${(null == init ? void 0 : init.method) ?? "GET"} ${url}\n${body}`);
|
|
195
211
|
}
|
|
196
212
|
return res.json();
|
|
197
213
|
}
|
|
198
|
-
async function fetchRaw(url, init) {
|
|
199
|
-
const res = await fetch(url, init);
|
|
214
|
+
async function fetchRaw(url, init, timeoutMs) {
|
|
215
|
+
const res = await fetch(url, withTimeout(init, timeoutMs));
|
|
200
216
|
if (!res.ok) {
|
|
201
217
|
const body = await res.text().catch(()=>"");
|
|
202
218
|
throw new Error(`HTTP ${res.status} ${res.statusText} \u{2014} ${(null == init ? void 0 : init.method) ?? "GET"} ${url}\n${body}`);
|
|
@@ -249,10 +265,26 @@ function logDryStep(scope, step, details) {
|
|
|
249
265
|
for (const [key, value] of Object.entries(details))if (null != value) console.log(labelLine(key, value));
|
|
250
266
|
}
|
|
251
267
|
}
|
|
268
|
+
const UPLOAD_TIMEOUT_MS = 120000;
|
|
269
|
+
const PUBLISH_TIMEOUT_MS = 60000;
|
|
252
270
|
const CWS_OAUTH_URL = "https://oauth2.googleapis.com/token";
|
|
253
271
|
const CWS_UPLOAD_BASE = "https://www.googleapis.com/upload/chromewebstore/v1.1/items";
|
|
254
272
|
const CWS_PUBLISH_BASE = "https://www.googleapis.com/chromewebstore/v1.1/items";
|
|
255
273
|
const CWS_ITEMS_BASE = "https://www.googleapis.com/chromewebstore/v1.1/items";
|
|
274
|
+
const CWS_V2_BASE = "https://chromewebstore.googleapis.com/v2";
|
|
275
|
+
const CWS_V2_UPLOAD_BASE = "https://chromewebstore.googleapis.com/upload/v2";
|
|
276
|
+
function resolveChromeApiVersion(opts) {
|
|
277
|
+
if ("v1.1" === opts.apiVersion || "v2" === opts.apiVersion) return opts.apiVersion;
|
|
278
|
+
return String(opts.publisherId || "").trim() ? "v2" : "v1.1";
|
|
279
|
+
}
|
|
280
|
+
function v2ItemName(publisherId, extensionId) {
|
|
281
|
+
return `publishers/${encodeURIComponent(publisherId)}/items/${encodeURIComponent(extensionId)}`;
|
|
282
|
+
}
|
|
283
|
+
function requirePublisherId(publisherId, op) {
|
|
284
|
+
const id = String(publisherId || "").trim();
|
|
285
|
+
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).`);
|
|
286
|
+
return id;
|
|
287
|
+
}
|
|
256
288
|
function chromeStoreUrl(extensionId) {
|
|
257
289
|
return `https://chromewebstore.google.com/detail/${extensionId}`;
|
|
258
290
|
}
|
|
@@ -271,16 +303,28 @@ async function authenticate(clientId, clientSecret, refreshToken) {
|
|
|
271
303
|
}
|
|
272
304
|
});
|
|
273
305
|
}
|
|
274
|
-
async function chrome_upload(extensionId, zipPath, token) {
|
|
306
|
+
async function chrome_upload(extensionId, zipPath, token, ctx) {
|
|
275
307
|
const body = await promises_default().readFile(zipPath);
|
|
276
|
-
|
|
277
|
-
|
|
308
|
+
let url;
|
|
309
|
+
let method;
|
|
310
|
+
const headers = {
|
|
311
|
+
Authorization: `${token.token_type} ${token.access_token}`,
|
|
312
|
+
"Content-Type": "application/zip"
|
|
313
|
+
};
|
|
314
|
+
if ("v2" === ctx.apiVersion) {
|
|
315
|
+
const pub = requirePublisherId(ctx.publisherId, "upload");
|
|
316
|
+
url = `${CWS_V2_UPLOAD_BASE}/${v2ItemName(pub, extensionId)}:upload`;
|
|
317
|
+
method = "POST";
|
|
318
|
+
} else {
|
|
319
|
+
url = `${CWS_UPLOAD_BASE}/${extensionId}`;
|
|
320
|
+
method = "PUT";
|
|
321
|
+
headers["x-goog-api-version"] = "2";
|
|
322
|
+
}
|
|
323
|
+
const res = await fetch(url, {
|
|
324
|
+
method,
|
|
278
325
|
body,
|
|
279
|
-
headers
|
|
280
|
-
|
|
281
|
-
"x-goog-api-version": "2",
|
|
282
|
-
"Content-Type": "application/zip"
|
|
283
|
-
}
|
|
326
|
+
headers,
|
|
327
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
|
|
284
328
|
});
|
|
285
329
|
if (!res.ok) {
|
|
286
330
|
const text = await res.text().catch(()=>"");
|
|
@@ -288,6 +332,33 @@ async function chrome_upload(extensionId, zipPath, token) {
|
|
|
288
332
|
}
|
|
289
333
|
}
|
|
290
334
|
async function publish(params) {
|
|
335
|
+
if ("v2" === params.apiVersion) {
|
|
336
|
+
const pub = requirePublisherId(params.publisherId, "publish");
|
|
337
|
+
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.");
|
|
338
|
+
const reqBody = {
|
|
339
|
+
publishType: "DEFAULT_PUBLISH"
|
|
340
|
+
};
|
|
341
|
+
if (null != params.deployPercentage) reqBody.deployInfos = [
|
|
342
|
+
{
|
|
343
|
+
deployPercentage: params.deployPercentage
|
|
344
|
+
}
|
|
345
|
+
];
|
|
346
|
+
if (params.reviewExemption) reqBody.skipReview = true;
|
|
347
|
+
const res = await fetch(`${CWS_V2_BASE}/${v2ItemName(pub, params.extensionId)}:publish`, {
|
|
348
|
+
method: "POST",
|
|
349
|
+
headers: {
|
|
350
|
+
Authorization: `${params.token.token_type} ${params.token.access_token}`,
|
|
351
|
+
"Content-Type": "application/json"
|
|
352
|
+
},
|
|
353
|
+
body: JSON.stringify(reqBody),
|
|
354
|
+
signal: AbortSignal.timeout(PUBLISH_TIMEOUT_MS)
|
|
355
|
+
});
|
|
356
|
+
if (!res.ok) {
|
|
357
|
+
const text = await res.text().catch(()=>"");
|
|
358
|
+
throw new Error(`Chrome publish failed: ${res.status} ${res.statusText}\n${text}`);
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
291
362
|
const url = new URL(`${CWS_PUBLISH_BASE}/${params.extensionId}/publish`);
|
|
292
363
|
url.searchParams.set("publishTarget", params.publishTarget);
|
|
293
364
|
if (null != params.deployPercentage) url.searchParams.set("deployPercentage", String(params.deployPercentage));
|
|
@@ -298,7 +369,8 @@ async function publish(params) {
|
|
|
298
369
|
Authorization: `${params.token.token_type} ${params.token.access_token}`,
|
|
299
370
|
"x-goog-api-version": "2",
|
|
300
371
|
"Content-Length": "0"
|
|
301
|
-
}
|
|
372
|
+
},
|
|
373
|
+
signal: AbortSignal.timeout(PUBLISH_TIMEOUT_MS)
|
|
302
374
|
});
|
|
303
375
|
if (!res.ok) {
|
|
304
376
|
const text = await res.text().catch(()=>"");
|
|
@@ -308,7 +380,8 @@ async function publish(params) {
|
|
|
308
380
|
async function publishChrome(options, dryRun) {
|
|
309
381
|
await assertFilePresent(options.zip);
|
|
310
382
|
const zipSize = await getFileSize(options.zip);
|
|
311
|
-
|
|
383
|
+
const apiVersion = resolveChromeApiVersion(options);
|
|
384
|
+
log("chrome", `Authenticating with Chrome Web Store (API ${apiVersion})`);
|
|
312
385
|
const token = await authenticate(options.clientId, options.clientSecret, options.refreshToken);
|
|
313
386
|
const outcome = {
|
|
314
387
|
storeUrl: chromeStoreUrl(options.extensionId),
|
|
@@ -317,25 +390,28 @@ async function publishChrome(options, dryRun) {
|
|
|
317
390
|
if (dryRun) {
|
|
318
391
|
logDryStep("chrome", "Authentication succeeded", {
|
|
319
392
|
tokenType: token.token_type,
|
|
320
|
-
scope: "chromewebstore"
|
|
393
|
+
scope: "chromewebstore",
|
|
394
|
+
apiVersion
|
|
321
395
|
});
|
|
322
396
|
logDryStep("chrome", "Would upload ZIP", {
|
|
323
397
|
file: options.zip,
|
|
324
398
|
size: formatBytes(zipSize),
|
|
325
|
-
|
|
326
|
-
method: "PUT"
|
|
399
|
+
apiVersion
|
|
327
400
|
});
|
|
328
401
|
options.skipSubmitReview ? logDryStep("chrome", "Would skip publish (skipSubmitReview=true)") : logDryStep("chrome", "Would publish for review", {
|
|
329
|
-
|
|
402
|
+
apiVersion,
|
|
330
403
|
publishTarget: options.publishTarget ?? "default",
|
|
331
404
|
deployPercentage: options.deployPercentage,
|
|
332
405
|
reviewExemption: options.reviewExemption
|
|
333
406
|
});
|
|
334
|
-
log("chrome", "Dry run complete
|
|
407
|
+
log("chrome", "Dry run complete - no changes made");
|
|
335
408
|
return outcome;
|
|
336
409
|
}
|
|
337
410
|
log("chrome", `Uploading ZIP file (${formatBytes(zipSize)})`);
|
|
338
|
-
await chrome_upload(options.extensionId, options.zip, token
|
|
411
|
+
await chrome_upload(options.extensionId, options.zip, token, {
|
|
412
|
+
apiVersion,
|
|
413
|
+
publisherId: options.publisherId
|
|
414
|
+
});
|
|
339
415
|
if (options.skipSubmitReview) {
|
|
340
416
|
log("chrome", "Skipping publish step (skipSubmitReview=true)");
|
|
341
417
|
return outcome;
|
|
@@ -346,16 +422,36 @@ async function publishChrome(options, dryRun) {
|
|
|
346
422
|
publishTarget: options.publishTarget,
|
|
347
423
|
token,
|
|
348
424
|
deployPercentage: options.deployPercentage,
|
|
349
|
-
reviewExemption: options.reviewExemption
|
|
425
|
+
reviewExemption: options.reviewExemption,
|
|
426
|
+
apiVersion,
|
|
427
|
+
publisherId: options.publisherId
|
|
350
428
|
});
|
|
351
429
|
return outcome;
|
|
352
430
|
}
|
|
353
431
|
async function getChromeItem(params) {
|
|
354
432
|
const token = await authenticate(params.clientId, params.clientSecret, params.refreshToken);
|
|
433
|
+
const auth = {
|
|
434
|
+
Authorization: `${token.token_type} ${token.access_token}`
|
|
435
|
+
};
|
|
436
|
+
const version = resolveChromeApiVersion(params);
|
|
437
|
+
if ("v2" === version) {
|
|
438
|
+
var _rev_distributionChannels_, _rev_distributionChannels;
|
|
439
|
+
const pub = requirePublisherId(params.publisherId, "status");
|
|
440
|
+
const status = await fetchJson(`${CWS_V2_BASE}/${v2ItemName(pub, params.extensionId)}:fetchStatus`, {
|
|
441
|
+
headers: auth
|
|
442
|
+
});
|
|
443
|
+
const rev = status.submittedItemRevisionStatus || status.publishedItemRevisionStatus;
|
|
444
|
+
return {
|
|
445
|
+
uploadState: status.lastAsyncUploadState,
|
|
446
|
+
crxVersion: null == rev ? void 0 : null == (_rev_distributionChannels = rev.distributionChannels) ? void 0 : null == (_rev_distributionChannels_ = _rev_distributionChannels[0]) ? void 0 : _rev_distributionChannels_.crxVersion,
|
|
447
|
+
publicKey: status.publicKey,
|
|
448
|
+
state: null == rev ? void 0 : rev.state
|
|
449
|
+
};
|
|
450
|
+
}
|
|
355
451
|
const url = `${CWS_ITEMS_BASE}/${encodeURIComponent(params.extensionId)}?projection=${params.projection ?? "DRAFT"}`;
|
|
356
452
|
return fetchJson(url, {
|
|
357
453
|
headers: {
|
|
358
|
-
|
|
454
|
+
...auth,
|
|
359
455
|
"x-goog-api-version": "2"
|
|
360
456
|
}
|
|
361
457
|
});
|
|
@@ -370,11 +466,24 @@ async function verifyChromeCredentials(params) {
|
|
|
370
466
|
message: `OAuth token exchange failed. Verify your Client ID, Client Secret, and Refresh Token are correct.\n${err instanceof Error ? err.message : err}`
|
|
371
467
|
};
|
|
372
468
|
}
|
|
469
|
+
const auth = {
|
|
470
|
+
Authorization: `${token.token_type} ${token.access_token}`
|
|
471
|
+
};
|
|
472
|
+
const version = resolveChromeApiVersion(params);
|
|
373
473
|
try {
|
|
474
|
+
if ("v2" === version) {
|
|
475
|
+
const pub = requirePublisherId(params.publisherId, "verify");
|
|
476
|
+
const status = await fetchJson(`${CWS_V2_BASE}/${v2ItemName(pub, params.extensionId)}:fetchStatus`, {
|
|
477
|
+
headers: auth
|
|
478
|
+
});
|
|
479
|
+
const rev = status.submittedItemRevisionStatus || status.publishedItemRevisionStatus;
|
|
480
|
+
return {
|
|
481
|
+
ok: true,
|
|
482
|
+
message: `Credentials verified (Chrome v2 API). Item state: ${(null == rev ? void 0 : rev.state) || "unknown"}.`
|
|
483
|
+
};
|
|
484
|
+
}
|
|
374
485
|
const item = await fetchJson(`${CWS_ITEMS_BASE}/${encodeURIComponent(params.extensionId)}?projection=DRAFT`, {
|
|
375
|
-
headers:
|
|
376
|
-
Authorization: `${token.token_type} ${token.access_token}`
|
|
377
|
-
}
|
|
486
|
+
headers: auth
|
|
378
487
|
});
|
|
379
488
|
const title = (null == item ? void 0 : item.title) || params.extensionId;
|
|
380
489
|
return {
|
|
@@ -390,7 +499,7 @@ async function verifyChromeCredentials(params) {
|
|
|
390
499
|
};
|
|
391
500
|
if (msg.includes("403")) return {
|
|
392
501
|
ok: false,
|
|
393
|
-
message: `Access denied to extension ${params.extensionId}. You may not be the owner or a developer on this item.`
|
|
502
|
+
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.`
|
|
394
503
|
};
|
|
395
504
|
return {
|
|
396
505
|
ok: false,
|
|
@@ -398,20 +507,14 @@ async function verifyChromeCredentials(params) {
|
|
|
398
507
|
};
|
|
399
508
|
}
|
|
400
509
|
}
|
|
401
|
-
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
402
|
-
var external_node_crypto_default = /*#__PURE__*/ __webpack_require__.n(external_node_crypto_namespaceObject);
|
|
403
510
|
function delay(ms) {
|
|
404
511
|
return new Promise((resolve)=>{
|
|
405
512
|
setTimeout(resolve, ms);
|
|
406
513
|
});
|
|
407
514
|
}
|
|
515
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
516
|
+
var external_node_crypto_default = /*#__PURE__*/ __webpack_require__.n(external_node_crypto_namespaceObject);
|
|
408
517
|
const AMO_BASE = "https://addons.mozilla.org/api/v5/addons";
|
|
409
|
-
const AMO_PROFILE_URL = "https://addons.mozilla.org/api/v5/accounts/profile/";
|
|
410
|
-
const POLL_INTERVAL_MS = 5000;
|
|
411
|
-
const VALIDATION_TIMEOUT_MS = 600000;
|
|
412
|
-
function firefoxStoreUrl(addonIdOrSlug) {
|
|
413
|
-
return `https://addons.mozilla.org/firefox/addon/${addonIdOrSlug}`;
|
|
414
|
-
}
|
|
415
518
|
function signJwt(issuer, secret, expiresInSeconds = 30) {
|
|
416
519
|
const header = {
|
|
417
520
|
alg: "HS256",
|
|
@@ -496,6 +599,12 @@ async function createAddon(params) {
|
|
|
496
599
|
headers: authHeaders(params.jwtIssuer, params.jwtSecret)
|
|
497
600
|
});
|
|
498
601
|
}
|
|
602
|
+
const AMO_PROFILE_URL = "https://addons.mozilla.org/api/v5/accounts/profile/";
|
|
603
|
+
const POLL_INTERVAL_MS = 5000;
|
|
604
|
+
const VALIDATION_TIMEOUT_MS = 600000;
|
|
605
|
+
function firefoxStoreUrl(addonIdOrSlug) {
|
|
606
|
+
return `https://addons.mozilla.org/firefox/addon/${addonIdOrSlug}`;
|
|
607
|
+
}
|
|
499
608
|
async function publishFirefox(options, dryRun) {
|
|
500
609
|
await assertFilePresent(options.zip);
|
|
501
610
|
const zipSize = await getFileSize(options.zip);
|
|
@@ -861,8 +970,134 @@ async function deploy(config) {
|
|
|
861
970
|
success
|
|
862
971
|
};
|
|
863
972
|
}
|
|
973
|
+
function usage() {
|
|
974
|
+
return `
|
|
975
|
+
extension-deploy \u{2014} Deploy browser extensions to Chrome, Firefox, and Edge.
|
|
976
|
+
|
|
977
|
+
USAGE
|
|
978
|
+
extension-deploy [options] submit to one or more stores
|
|
979
|
+
extension-deploy init [options] interactive credential setup wizard
|
|
980
|
+
extension-deploy watch --chrome [options] poll an in-flight submission
|
|
981
|
+
extension-deploy watch --firefox [options]
|
|
982
|
+
extension-deploy watch --edge [options]
|
|
983
|
+
|
|
984
|
+
GLOBAL
|
|
985
|
+
--dry-run Verify auth without uploading or publishing
|
|
986
|
+
--output-json <path> Write the DeployResult / WatchEvent as JSON to <path>
|
|
987
|
+
--help Show this help message
|
|
988
|
+
|
|
989
|
+
CHROME
|
|
990
|
+
--chrome-zip <path> Path to Chrome extension ZIP
|
|
991
|
+
--chrome-extension-id <id> Chrome extension ID
|
|
992
|
+
--chrome-client-id <id> OAuth2 client ID
|
|
993
|
+
--chrome-client-secret <secret> OAuth2 client secret
|
|
994
|
+
--chrome-refresh-token <token> OAuth2 refresh token
|
|
995
|
+
--chrome-publish-target <target> "default" or "trustedTesters"
|
|
996
|
+
--chrome-deploy-percentage <n> Staged rollout percentage (1-100)
|
|
997
|
+
--chrome-review-exemption Request expedited review
|
|
998
|
+
--chrome-skip-submit-review Upload only, skip publish
|
|
999
|
+
|
|
1000
|
+
FIREFOX
|
|
1001
|
+
--firefox-zip <path> Path to Firefox extension ZIP
|
|
1002
|
+
--firefox-sources-zip <path> Path to sources ZIP (optional)
|
|
1003
|
+
--firefox-extension-id <id> Addon GUID or email-style ID
|
|
1004
|
+
--firefox-jwt-issuer <issuer> AMO JWT issuer
|
|
1005
|
+
--firefox-jwt-secret <secret> AMO JWT secret
|
|
1006
|
+
--firefox-channel <channel> "listed" or "unlisted"
|
|
1007
|
+
|
|
1008
|
+
EDGE
|
|
1009
|
+
--edge-zip <path> Path to Edge extension ZIP
|
|
1010
|
+
--edge-product-id <id> Partner Center product ID
|
|
1011
|
+
--edge-client-id <id> Partner Center client ID
|
|
1012
|
+
--edge-api-key <key> Partner Center API key (v1.1)
|
|
1013
|
+
--edge-skip-submit-review Upload only, skip publish
|
|
1014
|
+
|
|
1015
|
+
WATCH
|
|
1016
|
+
--watch-submission-id <id> Required for firefox/edge (version id / operation id)
|
|
1017
|
+
--watch-interval <seconds> Poll interval (default 60)
|
|
1018
|
+
--watch-timeout <seconds> Max time to wait for a terminal state (default 3600)
|
|
1019
|
+
|
|
1020
|
+
INIT
|
|
1021
|
+
--init-stores <list> Comma-separated store list (skip picker)
|
|
1022
|
+
--init-output <path> Destination file (default .env.submit)
|
|
1023
|
+
--init-skip-verify Don't run verifyCredentials after collection
|
|
1024
|
+
--init-force Overwrite existing .env.submit without asking
|
|
1025
|
+
|
|
1026
|
+
ENVIRONMENT VARIABLES
|
|
1027
|
+
All flags can be set via SCREAMING_SNAKE_CASE env vars (e.g. CHROME_ZIP,
|
|
1028
|
+
FIREFOX_JWT_SECRET, EDGE_API_KEY). Flags take precedence over env vars.
|
|
1029
|
+
A .env.submit file in the current directory is loaded automatically.
|
|
1030
|
+
`.trim();
|
|
1031
|
+
}
|
|
1032
|
+
const BOOLEAN_FLAGS = new Set([
|
|
1033
|
+
"help",
|
|
1034
|
+
"dryRun",
|
|
1035
|
+
"chromeReviewExemption",
|
|
1036
|
+
"chromeSkipSubmitReview",
|
|
1037
|
+
"edgeSkipSubmitReview",
|
|
1038
|
+
"initSkipVerify",
|
|
1039
|
+
"initForce"
|
|
1040
|
+
]);
|
|
1041
|
+
const INT_FLAGS = new Set([
|
|
1042
|
+
"chromeDeployPercentage",
|
|
1043
|
+
"watchInterval",
|
|
1044
|
+
"watchTimeout"
|
|
1045
|
+
]);
|
|
1046
|
+
function parseArgs(argv) {
|
|
1047
|
+
const flags = {};
|
|
1048
|
+
let i = 0;
|
|
1049
|
+
if ("watch" === argv[0]) {
|
|
1050
|
+
flags.command = "watch";
|
|
1051
|
+
i = 1;
|
|
1052
|
+
} else if ("init" === argv[0]) {
|
|
1053
|
+
flags.command = "init";
|
|
1054
|
+
i = 1;
|
|
1055
|
+
}
|
|
1056
|
+
while(i < argv.length){
|
|
1057
|
+
const arg = argv[i];
|
|
1058
|
+
if ("--help" === arg || "-h" === arg) {
|
|
1059
|
+
flags.help = true;
|
|
1060
|
+
i++;
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if ("--chrome" === arg) {
|
|
1064
|
+
flags.watchStore = "chrome";
|
|
1065
|
+
i++;
|
|
1066
|
+
continue;
|
|
1067
|
+
}
|
|
1068
|
+
if ("--firefox" === arg) {
|
|
1069
|
+
flags.watchStore = "firefox";
|
|
1070
|
+
i++;
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
if ("--edge" === arg) {
|
|
1074
|
+
flags.watchStore = "edge";
|
|
1075
|
+
i++;
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (!(null == arg ? void 0 : arg.startsWith("--"))) {
|
|
1079
|
+
i++;
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
const key = arg.slice(2).replace(/-([a-z])/g, (_, c)=>c.toUpperCase());
|
|
1083
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
1084
|
+
flags[key] = true;
|
|
1085
|
+
i++;
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const next = argv[i + 1];
|
|
1089
|
+
if (null == next || next.startsWith("--")) {
|
|
1090
|
+
flags[key] = true;
|
|
1091
|
+
i++;
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
flags[key] = INT_FLAGS.has(key) ? parseInt(next, 10) : next;
|
|
1095
|
+
i += 2;
|
|
1096
|
+
}
|
|
1097
|
+
return flags;
|
|
1098
|
+
}
|
|
864
1099
|
const DEFAULT_POLL_INTERVAL_MS = 60000;
|
|
865
|
-
const
|
|
1100
|
+
const watch_DEFAULT_TIMEOUT_MS = 3600000;
|
|
866
1101
|
function watch_now() {
|
|
867
1102
|
return new Date().toISOString();
|
|
868
1103
|
}
|
|
@@ -989,7 +1224,7 @@ function watchFirefoxSubmission(input, options = {}) {
|
|
|
989
1224
|
}
|
|
990
1225
|
async function runWatchLoop(_store, probe, options) {
|
|
991
1226
|
const pollInterval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
992
|
-
const timeout = options.timeoutMs ??
|
|
1227
|
+
const timeout = options.timeoutMs ?? watch_DEFAULT_TIMEOUT_MS;
|
|
993
1228
|
const deadline = Date.now() + timeout;
|
|
994
1229
|
let lastEvent;
|
|
995
1230
|
while(true){
|
|
@@ -1001,125 +1236,30 @@ async function runWatchLoop(_store, probe, options) {
|
|
|
1001
1236
|
await delay(pollInterval);
|
|
1002
1237
|
}
|
|
1003
1238
|
}
|
|
1004
|
-
|
|
1005
|
-
{
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
value: "firefox",
|
|
1011
|
-
label: "Firefox AMO (addons.mozilla.org)"
|
|
1012
|
-
},
|
|
1013
|
-
{
|
|
1014
|
-
value: "edge",
|
|
1015
|
-
label: "Edge Add-ons (Partner Center)"
|
|
1239
|
+
async function requireValue(io, label, hint, opts) {
|
|
1240
|
+
const question = hint ? `${label} ${hint}:` : `${label}:`;
|
|
1241
|
+
for(let attempt = 0; attempt < 3; attempt++){
|
|
1242
|
+
const answer = (await io.prompt(question, opts)).trim();
|
|
1243
|
+
if (answer) return answer;
|
|
1244
|
+
io.log(` ${label} is required.`);
|
|
1016
1245
|
}
|
|
1017
|
-
|
|
1246
|
+
throw new Error(`init: ${label} is required.`);
|
|
1247
|
+
}
|
|
1018
1248
|
const CHROME_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
1019
1249
|
const CHROME_OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
|
|
1020
1250
|
const CHROME_OOB_REDIRECT = "urn:ietf:wg:oauth:2.0:oob";
|
|
1021
1251
|
const CHROME_SCOPE = "https://www.googleapis.com/auth/chromewebstore";
|
|
1022
|
-
async function
|
|
1252
|
+
async function collectChromeCredentials(deps) {
|
|
1023
1253
|
const { io } = deps;
|
|
1254
|
+
io.log("Chrome Web Store needs OAuth credentials. You'll need:");
|
|
1255
|
+
io.log(" 1. A Google Cloud project with the Chrome Web Store API enabled:");
|
|
1256
|
+
io.log(" https://console.cloud.google.com/apis/library/chromewebstore.googleapis.com");
|
|
1257
|
+
io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
|
|
1258
|
+
io.log(" https://console.cloud.google.com/apis/credentials");
|
|
1259
|
+
io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
|
|
1024
1260
|
io.log("");
|
|
1025
|
-
io.log(
|
|
1026
|
-
io.log("");
|
|
1027
|
-
io.log("This wizard walks you through creating credentials for each store and");
|
|
1028
|
-
io.log("writes them to a .env.submit file. Press Ctrl+C at any time to abort.");
|
|
1029
|
-
io.log("");
|
|
1030
|
-
const stores = options.stores ? options.stores : await io.multiselect("Which stores do you want to configure?", STORE_CHOICES);
|
|
1031
|
-
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.");
|
|
1032
|
-
const envLines = [
|
|
1033
|
-
"# Generated by `extension-deploy init`.",
|
|
1034
|
-
"# Do not commit secrets to source control.",
|
|
1035
|
-
""
|
|
1036
|
-
];
|
|
1037
|
-
const verified = {};
|
|
1038
|
-
for (const store of stores){
|
|
1039
|
-
io.log("");
|
|
1040
|
-
io.log(colors.underline(colors.blue(labelFor(store))));
|
|
1041
|
-
io.log("");
|
|
1042
|
-
if ("chrome" === store) {
|
|
1043
|
-
const creds = await collectChromeCredentials(deps);
|
|
1044
|
-
envLines.push(`CHROME_EXTENSION_ID=${creds.extensionId}`, `CHROME_CLIENT_ID=${creds.clientId}`, `CHROME_CLIENT_SECRET=${creds.clientSecret}`, `CHROME_REFRESH_TOKEN=${creds.refreshToken}`, "");
|
|
1045
|
-
if (!options.skipVerify) {
|
|
1046
|
-
const verify = deps.verifyChrome ?? verifyChromeCredentials;
|
|
1047
|
-
verified.chrome = normalizeVerify(await verify(creds));
|
|
1048
|
-
logVerify(io, "chrome", verified.chrome);
|
|
1049
|
-
}
|
|
1050
|
-
} else if ("firefox" === store) {
|
|
1051
|
-
const creds = await collectFirefoxCredentials(deps);
|
|
1052
|
-
envLines.push(`FIREFOX_EXTENSION_ID=${creds.addonId}`, `FIREFOX_JWT_ISSUER=${creds.jwtIssuer}`, `FIREFOX_JWT_SECRET=${creds.jwtSecret}`, "");
|
|
1053
|
-
if (!options.skipVerify) {
|
|
1054
|
-
const verify = deps.verifyFirefox ?? verifyFirefoxCredentials;
|
|
1055
|
-
verified.firefox = normalizeVerify(await verify({
|
|
1056
|
-
jwtIssuer: creds.jwtIssuer,
|
|
1057
|
-
jwtSecret: creds.jwtSecret,
|
|
1058
|
-
addonId: creds.addonId
|
|
1059
|
-
}));
|
|
1060
|
-
logVerify(io, "firefox", verified.firefox);
|
|
1061
|
-
}
|
|
1062
|
-
} else {
|
|
1063
|
-
const creds = await collectEdgeCredentials(deps);
|
|
1064
|
-
envLines.push(`EDGE_PRODUCT_ID=${creds.productId}`, `EDGE_CLIENT_ID=${creds.clientId}`, `EDGE_API_KEY=${creds.apiKey}`, "");
|
|
1065
|
-
if (!options.skipVerify) {
|
|
1066
|
-
const verify = deps.verifyEdge ?? verifyEdgeCredentials;
|
|
1067
|
-
verified.edge = normalizeVerify(await verify({
|
|
1068
|
-
clientId: creds.clientId,
|
|
1069
|
-
apiKey: creds.apiKey,
|
|
1070
|
-
productId: creds.productId
|
|
1071
|
-
}));
|
|
1072
|
-
logVerify(io, "edge", verified.edge);
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
const envContent = envLines.join("\n");
|
|
1077
|
-
const envPath = resolveEnvPath(deps.cwd, options.outputPath);
|
|
1078
|
-
const exists = await deps.fileExists(envPath);
|
|
1079
|
-
let wrote = false;
|
|
1080
|
-
if (exists && !options.force) {
|
|
1081
|
-
const overwrite = await deps.io.confirm(`${envPath} already exists. Overwrite?`, false);
|
|
1082
|
-
if (overwrite) {
|
|
1083
|
-
await deps.writeFile(envPath, envContent);
|
|
1084
|
-
wrote = true;
|
|
1085
|
-
} else {
|
|
1086
|
-
io.log("");
|
|
1087
|
-
io.log(`Skipped writing ${envPath}. Credential block:`);
|
|
1088
|
-
io.log("");
|
|
1089
|
-
io.log(envContent);
|
|
1090
|
-
}
|
|
1091
|
-
} else {
|
|
1092
|
-
await deps.writeFile(envPath, envContent);
|
|
1093
|
-
wrote = true;
|
|
1094
|
-
}
|
|
1095
|
-
if (wrote) {
|
|
1096
|
-
io.log("");
|
|
1097
|
-
io.log(`${colors.green("\u23F5\u23F5\u23F5")} ${colors.blue("init")} Wrote ${colors.underline(envPath)}`);
|
|
1098
|
-
io.log("");
|
|
1099
|
-
io.log(colors.gray("Next steps:"));
|
|
1100
|
-
io.log(" - Add .env.submit to .gitignore if it isn't already (it contains secrets).");
|
|
1101
|
-
io.log(" - Run `extension-deploy --dry-run` to sanity-check the full submit flow.");
|
|
1102
|
-
io.log("");
|
|
1103
|
-
}
|
|
1104
|
-
return {
|
|
1105
|
-
stores,
|
|
1106
|
-
envPath,
|
|
1107
|
-
envContent,
|
|
1108
|
-
verified,
|
|
1109
|
-
wrote
|
|
1110
|
-
};
|
|
1111
|
-
}
|
|
1112
|
-
async function collectChromeCredentials(deps) {
|
|
1113
|
-
const { io } = deps;
|
|
1114
|
-
io.log("Chrome Web Store needs OAuth credentials. You'll need:");
|
|
1115
|
-
io.log(" 1. A Google Cloud project with the Chrome Web Store API enabled:");
|
|
1116
|
-
io.log(" https://console.cloud.google.com/apis/library/chromewebstore.googleapis.com");
|
|
1117
|
-
io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
|
|
1118
|
-
io.log(" https://console.cloud.google.com/apis/credentials");
|
|
1119
|
-
io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
|
|
1120
|
-
io.log("");
|
|
1121
|
-
io.log("This wizard will take your Client ID + Secret and walk you through");
|
|
1122
|
-
io.log("generating a refresh token automatically.");
|
|
1261
|
+
io.log("This wizard will take your Client ID + Secret and walk you through");
|
|
1262
|
+
io.log("generating a refresh token automatically.");
|
|
1123
1263
|
io.log("");
|
|
1124
1264
|
const extensionId = await requireValue(io, "Chrome extension ID", "(32-character ID from the CWS dashboard URL)");
|
|
1125
1265
|
const clientId = await requireValue(io, "OAuth Client ID");
|
|
@@ -1224,6 +1364,110 @@ async function collectEdgeCredentials(deps) {
|
|
|
1224
1364
|
apiKey
|
|
1225
1365
|
};
|
|
1226
1366
|
}
|
|
1367
|
+
const STORE_CHOICES = [
|
|
1368
|
+
{
|
|
1369
|
+
value: "chrome",
|
|
1370
|
+
label: "Chrome Web Store"
|
|
1371
|
+
},
|
|
1372
|
+
{
|
|
1373
|
+
value: "firefox",
|
|
1374
|
+
label: "Firefox AMO (addons.mozilla.org)"
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
value: "edge",
|
|
1378
|
+
label: "Edge Add-ons (Partner Center)"
|
|
1379
|
+
}
|
|
1380
|
+
];
|
|
1381
|
+
async function runInit(deps, options = {}) {
|
|
1382
|
+
const { io } = deps;
|
|
1383
|
+
io.log("");
|
|
1384
|
+
io.log(`${colors.green("\u23F5\u23F5\u23F5")} ${colors.blue("init")} ${colors.underline("extension-deploy \u2014 set up store credentials")}`);
|
|
1385
|
+
io.log("");
|
|
1386
|
+
io.log("This wizard walks you through creating credentials for each store and");
|
|
1387
|
+
io.log("writes them to a .env.submit file. Press Ctrl+C at any time to abort.");
|
|
1388
|
+
io.log("");
|
|
1389
|
+
const stores = options.stores ? options.stores : await io.multiselect("Which stores do you want to configure?", STORE_CHOICES);
|
|
1390
|
+
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.");
|
|
1391
|
+
const envLines = [
|
|
1392
|
+
"# Generated by `extension-deploy init`.",
|
|
1393
|
+
"# Do not commit secrets to source control.",
|
|
1394
|
+
""
|
|
1395
|
+
];
|
|
1396
|
+
const verified = {};
|
|
1397
|
+
for (const store of stores){
|
|
1398
|
+
io.log("");
|
|
1399
|
+
io.log(colors.underline(colors.blue(labelFor(store))));
|
|
1400
|
+
io.log("");
|
|
1401
|
+
if ("chrome" === store) {
|
|
1402
|
+
const creds = await collectChromeCredentials(deps);
|
|
1403
|
+
envLines.push(`CHROME_EXTENSION_ID=${creds.extensionId}`, `CHROME_CLIENT_ID=${creds.clientId}`, `CHROME_CLIENT_SECRET=${creds.clientSecret}`, `CHROME_REFRESH_TOKEN=${creds.refreshToken}`, "");
|
|
1404
|
+
if (!options.skipVerify) {
|
|
1405
|
+
const verify = deps.verifyChrome ?? verifyChromeCredentials;
|
|
1406
|
+
verified.chrome = normalizeVerify(await verify(creds));
|
|
1407
|
+
logVerify(io, "chrome", verified.chrome);
|
|
1408
|
+
}
|
|
1409
|
+
} else if ("firefox" === store) {
|
|
1410
|
+
const creds = await collectFirefoxCredentials(deps);
|
|
1411
|
+
envLines.push(`FIREFOX_EXTENSION_ID=${creds.addonId}`, `FIREFOX_JWT_ISSUER=${creds.jwtIssuer}`, `FIREFOX_JWT_SECRET=${creds.jwtSecret}`, "");
|
|
1412
|
+
if (!options.skipVerify) {
|
|
1413
|
+
const verify = deps.verifyFirefox ?? verifyFirefoxCredentials;
|
|
1414
|
+
verified.firefox = normalizeVerify(await verify({
|
|
1415
|
+
jwtIssuer: creds.jwtIssuer,
|
|
1416
|
+
jwtSecret: creds.jwtSecret,
|
|
1417
|
+
addonId: creds.addonId
|
|
1418
|
+
}));
|
|
1419
|
+
logVerify(io, "firefox", verified.firefox);
|
|
1420
|
+
}
|
|
1421
|
+
} else {
|
|
1422
|
+
const creds = await collectEdgeCredentials(deps);
|
|
1423
|
+
envLines.push(`EDGE_PRODUCT_ID=${creds.productId}`, `EDGE_CLIENT_ID=${creds.clientId}`, `EDGE_API_KEY=${creds.apiKey}`, "");
|
|
1424
|
+
if (!options.skipVerify) {
|
|
1425
|
+
const verify = deps.verifyEdge ?? verifyEdgeCredentials;
|
|
1426
|
+
verified.edge = normalizeVerify(await verify({
|
|
1427
|
+
clientId: creds.clientId,
|
|
1428
|
+
apiKey: creds.apiKey,
|
|
1429
|
+
productId: creds.productId
|
|
1430
|
+
}));
|
|
1431
|
+
logVerify(io, "edge", verified.edge);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
const envContent = envLines.join("\n");
|
|
1436
|
+
const envPath = resolveEnvPath(deps.cwd, options.outputPath);
|
|
1437
|
+
const exists = await deps.fileExists(envPath);
|
|
1438
|
+
let wrote = false;
|
|
1439
|
+
if (exists && !options.force) {
|
|
1440
|
+
const overwrite = await deps.io.confirm(`${envPath} already exists. Overwrite?`, false);
|
|
1441
|
+
if (overwrite) {
|
|
1442
|
+
await deps.writeFile(envPath, envContent);
|
|
1443
|
+
wrote = true;
|
|
1444
|
+
} else {
|
|
1445
|
+
io.log("");
|
|
1446
|
+
io.log(`Skipped writing ${envPath}. Credential block:`);
|
|
1447
|
+
io.log("");
|
|
1448
|
+
io.log(envContent);
|
|
1449
|
+
}
|
|
1450
|
+
} else {
|
|
1451
|
+
await deps.writeFile(envPath, envContent);
|
|
1452
|
+
wrote = true;
|
|
1453
|
+
}
|
|
1454
|
+
if (wrote) {
|
|
1455
|
+
io.log("");
|
|
1456
|
+
io.log(`${colors.green("\u23F5\u23F5\u23F5")} ${colors.blue("init")} Wrote ${colors.underline(envPath)}`);
|
|
1457
|
+
io.log("");
|
|
1458
|
+
io.log(colors.gray("Next steps:"));
|
|
1459
|
+
io.log(" - Add .env.submit to .gitignore if it isn't already (it contains secrets).");
|
|
1460
|
+
io.log(" - Run `extension-deploy --dry-run` to sanity-check the full submit flow.");
|
|
1461
|
+
io.log("");
|
|
1462
|
+
}
|
|
1463
|
+
return {
|
|
1464
|
+
stores,
|
|
1465
|
+
envPath,
|
|
1466
|
+
envContent,
|
|
1467
|
+
verified,
|
|
1468
|
+
wrote
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1227
1471
|
function labelFor(store) {
|
|
1228
1472
|
var _STORE_CHOICES_find;
|
|
1229
1473
|
return (null == (_STORE_CHOICES_find = STORE_CHOICES.find((c)=>c.value === store)) ? void 0 : _STORE_CHOICES_find.label) ?? String(store);
|
|
@@ -1242,15 +1486,6 @@ function resolveEnvPath(cwd, outputPath) {
|
|
|
1242
1486
|
const target = outputPath ?? ".env.submit";
|
|
1243
1487
|
return external_node_path_default().isAbsolute(target) ? target : external_node_path_default().join(cwd, target);
|
|
1244
1488
|
}
|
|
1245
|
-
async function requireValue(io, label, hint, opts) {
|
|
1246
|
-
const question = hint ? `${label} ${hint}:` : `${label}:`;
|
|
1247
|
-
for(let attempt = 0; attempt < 3; attempt++){
|
|
1248
|
-
const answer = (await io.prompt(question, opts)).trim();
|
|
1249
|
-
if (answer) return answer;
|
|
1250
|
-
io.log(` ${label} is required.`);
|
|
1251
|
-
}
|
|
1252
|
-
throw new Error(`init: ${label} is required.`);
|
|
1253
|
-
}
|
|
1254
1489
|
const external_node_readline_namespaceObject = require("node:readline");
|
|
1255
1490
|
var external_node_readline_default = /*#__PURE__*/ __webpack_require__.n(external_node_readline_namespaceObject);
|
|
1256
1491
|
function createStdinIO() {
|
|
@@ -1306,7 +1541,13 @@ function createStdinIO() {
|
|
|
1306
1541
|
}
|
|
1307
1542
|
const stdinFs = {
|
|
1308
1543
|
async writeFile (filePath, content) {
|
|
1309
|
-
await promises_default().writeFile(filePath, content,
|
|
1544
|
+
await promises_default().writeFile(filePath, content, {
|
|
1545
|
+
encoding: "utf-8",
|
|
1546
|
+
mode: 384
|
|
1547
|
+
});
|
|
1548
|
+
try {
|
|
1549
|
+
await promises_default().chmod(filePath, 384);
|
|
1550
|
+
} catch {}
|
|
1310
1551
|
},
|
|
1311
1552
|
async fileExists (filePath) {
|
|
1312
1553
|
try {
|
|
@@ -1317,173 +1558,12 @@ const stdinFs = {
|
|
|
1317
1558
|
}
|
|
1318
1559
|
}
|
|
1319
1560
|
};
|
|
1320
|
-
function usage() {
|
|
1321
|
-
return `
|
|
1322
|
-
extension-deploy \u{2014} Deploy browser extensions to Chrome, Firefox, and Edge.
|
|
1323
|
-
|
|
1324
|
-
USAGE
|
|
1325
|
-
extension-deploy [options] submit to one or more stores
|
|
1326
|
-
extension-deploy init [options] interactive credential setup wizard
|
|
1327
|
-
extension-deploy watch --chrome [options] poll an in-flight submission
|
|
1328
|
-
extension-deploy watch --firefox [options]
|
|
1329
|
-
extension-deploy watch --edge [options]
|
|
1330
|
-
|
|
1331
|
-
GLOBAL
|
|
1332
|
-
--dry-run Verify auth without uploading or publishing
|
|
1333
|
-
--output-json <path> Write the DeployResult / WatchEvent as JSON to <path>
|
|
1334
|
-
--help Show this help message
|
|
1335
|
-
|
|
1336
|
-
CHROME
|
|
1337
|
-
--chrome-zip <path> Path to Chrome extension ZIP
|
|
1338
|
-
--chrome-extension-id <id> Chrome extension ID
|
|
1339
|
-
--chrome-client-id <id> OAuth2 client ID
|
|
1340
|
-
--chrome-client-secret <secret> OAuth2 client secret
|
|
1341
|
-
--chrome-refresh-token <token> OAuth2 refresh token
|
|
1342
|
-
--chrome-publish-target <target> "default" or "trustedTesters"
|
|
1343
|
-
--chrome-deploy-percentage <n> Staged rollout percentage (1-100)
|
|
1344
|
-
--chrome-review-exemption Request expedited review
|
|
1345
|
-
--chrome-skip-submit-review Upload only, skip publish
|
|
1346
|
-
|
|
1347
|
-
FIREFOX
|
|
1348
|
-
--firefox-zip <path> Path to Firefox extension ZIP
|
|
1349
|
-
--firefox-sources-zip <path> Path to sources ZIP (optional)
|
|
1350
|
-
--firefox-extension-id <id> Addon GUID or email-style ID
|
|
1351
|
-
--firefox-jwt-issuer <issuer> AMO JWT issuer
|
|
1352
|
-
--firefox-jwt-secret <secret> AMO JWT secret
|
|
1353
|
-
--firefox-channel <channel> "listed" or "unlisted"
|
|
1354
|
-
|
|
1355
|
-
EDGE
|
|
1356
|
-
--edge-zip <path> Path to Edge extension ZIP
|
|
1357
|
-
--edge-product-id <id> Partner Center product ID
|
|
1358
|
-
--edge-client-id <id> Partner Center client ID
|
|
1359
|
-
--edge-api-key <key> Partner Center API key (v1.1)
|
|
1360
|
-
--edge-skip-submit-review Upload only, skip publish
|
|
1361
|
-
|
|
1362
|
-
WATCH
|
|
1363
|
-
--watch-submission-id <id> Required for firefox/edge (version id / operation id)
|
|
1364
|
-
--watch-interval <seconds> Poll interval (default 60)
|
|
1365
|
-
--watch-timeout <seconds> Max time to wait for a terminal state (default 3600)
|
|
1366
|
-
|
|
1367
|
-
INIT
|
|
1368
|
-
--init-stores <list> Comma-separated store list (skip picker)
|
|
1369
|
-
--init-output <path> Destination file (default .env.submit)
|
|
1370
|
-
--init-skip-verify Don't run verifyCredentials after collection
|
|
1371
|
-
--init-force Overwrite existing .env.submit without asking
|
|
1372
|
-
|
|
1373
|
-
ENVIRONMENT VARIABLES
|
|
1374
|
-
All flags can be set via SCREAMING_SNAKE_CASE env vars (e.g. CHROME_ZIP,
|
|
1375
|
-
FIREFOX_JWT_SECRET, EDGE_API_KEY). Flags take precedence over env vars.
|
|
1376
|
-
A .env.submit file in the current directory is loaded automatically.
|
|
1377
|
-
`.trim();
|
|
1378
|
-
}
|
|
1379
|
-
const BOOLEAN_FLAGS = new Set([
|
|
1380
|
-
"help",
|
|
1381
|
-
"dryRun",
|
|
1382
|
-
"chromeReviewExemption",
|
|
1383
|
-
"chromeSkipSubmitReview",
|
|
1384
|
-
"edgeSkipSubmitReview",
|
|
1385
|
-
"initSkipVerify",
|
|
1386
|
-
"initForce"
|
|
1387
|
-
]);
|
|
1388
|
-
const INT_FLAGS = new Set([
|
|
1389
|
-
"chromeDeployPercentage",
|
|
1390
|
-
"watchInterval",
|
|
1391
|
-
"watchTimeout"
|
|
1392
|
-
]);
|
|
1393
|
-
function parseArgs(argv) {
|
|
1394
|
-
const flags = {};
|
|
1395
|
-
let i = 0;
|
|
1396
|
-
if ("watch" === argv[0]) {
|
|
1397
|
-
flags.command = "watch";
|
|
1398
|
-
i = 1;
|
|
1399
|
-
} else if ("init" === argv[0]) {
|
|
1400
|
-
flags.command = "init";
|
|
1401
|
-
i = 1;
|
|
1402
|
-
}
|
|
1403
|
-
while(i < argv.length){
|
|
1404
|
-
const arg = argv[i];
|
|
1405
|
-
if ("--help" === arg || "-h" === arg) {
|
|
1406
|
-
flags.help = true;
|
|
1407
|
-
i++;
|
|
1408
|
-
continue;
|
|
1409
|
-
}
|
|
1410
|
-
if ("--chrome" === arg) {
|
|
1411
|
-
flags.watchStore = "chrome";
|
|
1412
|
-
i++;
|
|
1413
|
-
continue;
|
|
1414
|
-
}
|
|
1415
|
-
if ("--firefox" === arg) {
|
|
1416
|
-
flags.watchStore = "firefox";
|
|
1417
|
-
i++;
|
|
1418
|
-
continue;
|
|
1419
|
-
}
|
|
1420
|
-
if ("--edge" === arg) {
|
|
1421
|
-
flags.watchStore = "edge";
|
|
1422
|
-
i++;
|
|
1423
|
-
continue;
|
|
1424
|
-
}
|
|
1425
|
-
if (!(null == arg ? void 0 : arg.startsWith("--"))) {
|
|
1426
|
-
i++;
|
|
1427
|
-
continue;
|
|
1428
|
-
}
|
|
1429
|
-
const key = arg.slice(2).replace(/-([a-z])/g, (_, c)=>c.toUpperCase());
|
|
1430
|
-
if (BOOLEAN_FLAGS.has(key)) {
|
|
1431
|
-
flags[key] = true;
|
|
1432
|
-
i++;
|
|
1433
|
-
continue;
|
|
1434
|
-
}
|
|
1435
|
-
const next = argv[i + 1];
|
|
1436
|
-
if (null == next || next.startsWith("--")) {
|
|
1437
|
-
flags[key] = true;
|
|
1438
|
-
i++;
|
|
1439
|
-
continue;
|
|
1440
|
-
}
|
|
1441
|
-
flags[key] = INT_FLAGS.has(key) ? parseInt(next, 10) : next;
|
|
1442
|
-
i += 2;
|
|
1443
|
-
}
|
|
1444
|
-
return flags;
|
|
1445
|
-
}
|
|
1446
|
-
function loadDotEnv() {
|
|
1447
|
-
try {
|
|
1448
|
-
const envPath = external_node_path_default().resolve(process.cwd(), ".env.submit");
|
|
1449
|
-
const content = external_node_fs_default().readFileSync(envPath, "utf-8");
|
|
1450
|
-
for (const line of content.split("\n")){
|
|
1451
|
-
const trimmed = line.trim();
|
|
1452
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1453
|
-
const eqIdx = trimmed.indexOf("=");
|
|
1454
|
-
if (-1 === eqIdx) continue;
|
|
1455
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
1456
|
-
let value = trimmed.slice(eqIdx + 1).trim();
|
|
1457
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1458
|
-
if (!(key in process.env)) process.env[key] = value;
|
|
1459
|
-
}
|
|
1460
|
-
} catch {}
|
|
1461
|
-
}
|
|
1462
1561
|
function writeJsonOutput(filePath, payload) {
|
|
1463
1562
|
external_node_fs_default().mkdirSync(external_node_path_default().dirname(external_node_path_default().resolve(filePath)), {
|
|
1464
1563
|
recursive: true
|
|
1465
1564
|
});
|
|
1466
1565
|
external_node_fs_default().writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
1467
1566
|
}
|
|
1468
|
-
async function main() {
|
|
1469
|
-
loadDotEnv();
|
|
1470
|
-
const flags = parseArgs(process.argv.slice(2));
|
|
1471
|
-
if (flags.help) {
|
|
1472
|
-
console.log(usage());
|
|
1473
|
-
process.exit(0);
|
|
1474
|
-
}
|
|
1475
|
-
if ("watch" === flags.command) return void await runWatch(flags);
|
|
1476
|
-
if ("init" === flags.command) return void await runInitCommand(flags);
|
|
1477
|
-
const config = resolveConfig(flags);
|
|
1478
|
-
try {
|
|
1479
|
-
const result = await deploy(config);
|
|
1480
|
-
if (flags.outputJson) writeJsonOutput(flags.outputJson, result);
|
|
1481
|
-
if (!result.success) process.exit(1);
|
|
1482
|
-
} catch (err) {
|
|
1483
|
-
logError("deploy", err instanceof Error ? err.message : String(err));
|
|
1484
|
-
process.exit(1);
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
1567
|
async function runWatch(flags) {
|
|
1488
1568
|
const store = flags.watchStore;
|
|
1489
1569
|
if (!store) {
|
|
@@ -1579,6 +1659,41 @@ function parseInitStores(raw) {
|
|
|
1579
1659
|
if (0 === parsed.length) throw new Error(`--init-stores: expected a comma-separated list of chrome,firefox,edge (got "${raw}")`);
|
|
1580
1660
|
return parsed;
|
|
1581
1661
|
}
|
|
1662
|
+
function loadDotEnv() {
|
|
1663
|
+
try {
|
|
1664
|
+
const envPath = external_node_path_default().resolve(process.cwd(), ".env.submit");
|
|
1665
|
+
const content = external_node_fs_default().readFileSync(envPath, "utf-8");
|
|
1666
|
+
for (const line of content.split("\n")){
|
|
1667
|
+
const trimmed = line.trim();
|
|
1668
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1669
|
+
const eqIdx = trimmed.indexOf("=");
|
|
1670
|
+
if (-1 === eqIdx) continue;
|
|
1671
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
1672
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
1673
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1674
|
+
if (!(key in process.env)) process.env[key] = value;
|
|
1675
|
+
}
|
|
1676
|
+
} catch {}
|
|
1677
|
+
}
|
|
1678
|
+
async function main() {
|
|
1679
|
+
loadDotEnv();
|
|
1680
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
1681
|
+
if (flags.help) {
|
|
1682
|
+
console.log(usage());
|
|
1683
|
+
process.exit(0);
|
|
1684
|
+
}
|
|
1685
|
+
if ("watch" === flags.command) return void await runWatch(flags);
|
|
1686
|
+
if ("init" === flags.command) return void await runInitCommand(flags);
|
|
1687
|
+
const config = resolveConfig(flags);
|
|
1688
|
+
try {
|
|
1689
|
+
const result = await deploy(config);
|
|
1690
|
+
if (flags.outputJson) writeJsonOutput(flags.outputJson, result);
|
|
1691
|
+
if (!result.success) process.exit(1);
|
|
1692
|
+
} catch (err) {
|
|
1693
|
+
logError("deploy", err instanceof Error ? err.message : String(err));
|
|
1694
|
+
process.exit(1);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1582
1697
|
exports.chromeOptionsSchema = __webpack_exports__.chromeOptionsSchema;
|
|
1583
1698
|
exports.deploy = __webpack_exports__.deploy;
|
|
1584
1699
|
exports.deployConfigSchema = __webpack_exports__.deployConfigSchema;
|