@extension.dev/deploy 0.2.4 → 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +34 -1
  3. package/dist/module.js +500 -340
  4. package/dist/module.mjs +498 -338
  5. package/dist/src/chrome.d.ts +19 -0
  6. package/dist/src/chrome.d.ts.map +1 -1
  7. package/dist/src/cli-commands.d.ts +5 -0
  8. package/dist/src/cli-commands.d.ts.map +1 -0
  9. package/dist/src/cli-parse.d.ts +3 -0
  10. package/dist/src/cli-parse.d.ts.map +1 -0
  11. package/dist/src/cli-usage.d.ts +2 -0
  12. package/dist/src/cli-usage.d.ts.map +1 -0
  13. package/dist/src/cli.d.ts +1 -2
  14. package/dist/src/cli.d.ts.map +1 -1
  15. package/dist/src/config.d.ts +6 -0
  16. package/dist/src/config.d.ts.map +1 -1
  17. package/dist/src/deploy.d.ts.map +1 -1
  18. package/dist/src/edge.d.ts.map +1 -1
  19. package/dist/src/firefox-api.d.ts +52 -0
  20. package/dist/src/firefox-api.d.ts.map +1 -0
  21. package/dist/src/firefox.d.ts +1 -24
  22. package/dist/src/firefox.d.ts.map +1 -1
  23. package/dist/src/init-chrome.d.ts +16 -0
  24. package/dist/src/init-chrome.d.ts.map +1 -0
  25. package/dist/src/init-edge.d.ts +8 -0
  26. package/dist/src/init-edge.d.ts.map +1 -0
  27. package/dist/src/init-firefox.d.ts +8 -0
  28. package/dist/src/init-firefox.d.ts.map +1 -0
  29. package/dist/src/init-io.d.ts.map +1 -1
  30. package/dist/src/init-prompt.d.ts +5 -0
  31. package/dist/src/init-prompt.d.ts.map +1 -0
  32. package/dist/src/init.d.ts +3 -26
  33. package/dist/src/init.d.ts.map +1 -1
  34. package/dist/src/types.d.ts +49 -5
  35. package/dist/src/types.d.ts.map +1 -1
  36. package/dist/src/utils/fetch.d.ts +2 -2
  37. package/dist/src/utils/fetch.d.ts.map +1 -1
  38. package/dist/src/watch.d.ts.map +1 -1
  39. package/package.json +18 -8
  40. package/dist/__tests__/cli.test.d.ts +0 -2
  41. package/dist/__tests__/cli.test.d.ts.map +0 -1
  42. package/dist/__tests__/config.test.d.ts +0 -2
  43. package/dist/__tests__/config.test.d.ts.map +0 -1
  44. package/dist/__tests__/deploy.test.d.ts +0 -2
  45. package/dist/__tests__/deploy.test.d.ts.map +0 -1
  46. package/dist/__tests__/dry-run.test.d.ts +0 -2
  47. package/dist/__tests__/dry-run.test.d.ts.map +0 -1
  48. package/dist/__tests__/init.test.d.ts +0 -2
  49. package/dist/__tests__/init.test.d.ts.map +0 -1
  50. package/dist/__tests__/types.test.d.ts +0 -2
  51. package/dist/__tests__/types.test.d.ts.map +0 -1
  52. package/dist/__tests__/utils.test.d.ts +0 -2
  53. package/dist/__tests__/utils.test.d.ts.map +0 -1
  54. package/dist/__tests__/verify.test.d.ts +0 -2
  55. package/dist/__tests__/verify.test.d.ts.map +0 -1
  56. package/dist/__tests__/watch.test.d.ts +0 -2
  57. package/dist/__tests__/watch.test.d.ts.map +0 -1
package/dist/module.mjs CHANGED
@@ -17,18 +17,31 @@ 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),
24
29
  sourcesZip: z.string().min(1).optional(),
25
- extensionId: z.string().min(1).trim(),
30
+ extensionId: z.string().trim().default(""),
26
31
  jwtIssuer: z.string().min(1).trim(),
27
32
  jwtSecret: z.string().min(1).trim(),
28
33
  channel: z["enum"]([
29
34
  "listed",
30
35
  "unlisted"
31
36
  ]).default("listed")
37
+ }).superRefine((val, ctx)=>{
38
+ if ("listed" === val.channel && 0 === val.extensionId.length) ctx.addIssue({
39
+ code: z.ZodIssueCode.custom,
40
+ path: [
41
+ "extensionId"
42
+ ],
43
+ message: 'Listed Firefox submissions require an existing add-on GUID. Use channel "unlisted" for a first submission.'
44
+ });
32
45
  });
33
46
  const edgeOptionsSchema = z.object({
34
47
  zip: z.string().min(1),
@@ -59,7 +72,9 @@ function resolveConfig(flags) {
59
72
  publishTarget: flags.chromePublishTarget ?? envStr("CHROME_PUBLISH_TARGET"),
60
73
  deployPercentage: flags.chromeDeployPercentage ?? envInt("CHROME_DEPLOY_PERCENTAGE"),
61
74
  reviewExemption: flags.chromeReviewExemption ?? envBool("CHROME_REVIEW_EXEMPTION"),
62
- 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")
63
78
  },
64
79
  firefox: null == firefoxZip ? void 0 : {
65
80
  zip: firefoxZip,
@@ -86,6 +101,7 @@ const SETUP_HINTS = {
86
101
  CHROME_CLIENT_ID: "Create OAuth 2.0 credentials at https://console.cloud.google.com/apis/credentials",
87
102
  CHROME_CLIENT_SECRET: "Generated alongside the Client ID in the Google Cloud Console.",
88
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.",
89
105
  CHROME_ZIP: "Path to the .zip file built for Chrome Web Store upload.",
90
106
  FIREFOX_EXTENSION_ID: "The addon GUID ({uuid} format) or email-style ID from your addon's AMO page.",
91
107
  FIREFOX_JWT_ISSUER: "API key from https://addons.mozilla.org/developers/addon/api/key/",
@@ -126,16 +142,24 @@ function envInt(name) {
126
142
  const val = process.env[name];
127
143
  return null == val ? void 0 : parseInt(val, 10);
128
144
  }
129
- async function fetchJson(url, init) {
130
- const res = await fetch(url, init);
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));
131
155
  if (!res.ok) {
132
156
  const body = await res.text().catch(()=>"");
133
157
  throw new Error(`HTTP ${res.status} ${res.statusText} \u{2014} ${(null == init ? void 0 : init.method) ?? "GET"} ${url}\n${body}`);
134
158
  }
135
159
  return res.json();
136
160
  }
137
- async function fetchRaw(url, init) {
138
- const res = await fetch(url, init);
161
+ async function fetchRaw(url, init, timeoutMs) {
162
+ const res = await fetch(url, withTimeout(init, timeoutMs));
139
163
  if (!res.ok) {
140
164
  const body = await res.text().catch(()=>"");
141
165
  throw new Error(`HTTP ${res.status} ${res.statusText} \u{2014} ${(null == init ? void 0 : init.method) ?? "GET"} ${url}\n${body}`);
@@ -186,10 +210,26 @@ function logDryStep(scope, step, details) {
186
210
  for (const [key, value] of Object.entries(details))if (null != value) console.log(labelLine(key, value));
187
211
  }
188
212
  }
213
+ const UPLOAD_TIMEOUT_MS = 120000;
214
+ const PUBLISH_TIMEOUT_MS = 60000;
189
215
  const CWS_OAUTH_URL = "https://oauth2.googleapis.com/token";
190
216
  const CWS_UPLOAD_BASE = "https://www.googleapis.com/upload/chromewebstore/v1.1/items";
191
217
  const CWS_PUBLISH_BASE = "https://www.googleapis.com/chromewebstore/v1.1/items";
192
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
+ }
193
233
  function chromeStoreUrl(extensionId) {
194
234
  return `https://chromewebstore.google.com/detail/${extensionId}`;
195
235
  }
@@ -208,16 +248,28 @@ async function authenticate(clientId, clientSecret, refreshToken) {
208
248
  }
209
249
  });
210
250
  }
211
- async function chrome_upload(extensionId, zipPath, token) {
251
+ async function chrome_upload(extensionId, zipPath, token, ctx) {
212
252
  const body = await promises_default.readFile(zipPath);
213
- const res = await fetch(`${CWS_UPLOAD_BASE}/${extensionId}`, {
214
- method: "PUT",
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,
215
270
  body,
216
- headers: {
217
- Authorization: `${token.token_type} ${token.access_token}`,
218
- "x-goog-api-version": "2",
219
- "Content-Type": "application/zip"
220
- }
271
+ headers,
272
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
221
273
  });
222
274
  if (!res.ok) {
223
275
  const text = await res.text().catch(()=>"");
@@ -225,6 +277,33 @@ async function chrome_upload(extensionId, zipPath, token) {
225
277
  }
226
278
  }
227
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
+ }
228
307
  const url = new URL(`${CWS_PUBLISH_BASE}/${params.extensionId}/publish`);
229
308
  url.searchParams.set("publishTarget", params.publishTarget);
230
309
  if (null != params.deployPercentage) url.searchParams.set("deployPercentage", String(params.deployPercentage));
@@ -235,7 +314,8 @@ async function publish(params) {
235
314
  Authorization: `${params.token.token_type} ${params.token.access_token}`,
236
315
  "x-goog-api-version": "2",
237
316
  "Content-Length": "0"
238
- }
317
+ },
318
+ signal: AbortSignal.timeout(PUBLISH_TIMEOUT_MS)
239
319
  });
240
320
  if (!res.ok) {
241
321
  const text = await res.text().catch(()=>"");
@@ -245,7 +325,8 @@ async function publish(params) {
245
325
  async function publishChrome(options, dryRun) {
246
326
  await assertFilePresent(options.zip);
247
327
  const zipSize = await getFileSize(options.zip);
248
- log("chrome", "Authenticating with Chrome Web Store");
328
+ const apiVersion = resolveChromeApiVersion(options);
329
+ log("chrome", `Authenticating with Chrome Web Store (API ${apiVersion})`);
249
330
  const token = await authenticate(options.clientId, options.clientSecret, options.refreshToken);
250
331
  const outcome = {
251
332
  storeUrl: chromeStoreUrl(options.extensionId),
@@ -254,25 +335,28 @@ async function publishChrome(options, dryRun) {
254
335
  if (dryRun) {
255
336
  logDryStep("chrome", "Authentication succeeded", {
256
337
  tokenType: token.token_type,
257
- scope: "chromewebstore"
338
+ scope: "chromewebstore",
339
+ apiVersion
258
340
  });
259
341
  logDryStep("chrome", "Would upload ZIP", {
260
342
  file: options.zip,
261
343
  size: formatBytes(zipSize),
262
- target: `${CWS_UPLOAD_BASE}/${options.extensionId}`,
263
- method: "PUT"
344
+ apiVersion
264
345
  });
265
346
  options.skipSubmitReview ? logDryStep("chrome", "Would skip publish (skipSubmitReview=true)") : logDryStep("chrome", "Would publish for review", {
266
- target: `${CWS_PUBLISH_BASE}/${options.extensionId}/publish`,
347
+ apiVersion,
267
348
  publishTarget: options.publishTarget ?? "default",
268
349
  deployPercentage: options.deployPercentage,
269
350
  reviewExemption: options.reviewExemption
270
351
  });
271
- log("chrome", "Dry run complete \u2014 no changes made");
352
+ log("chrome", "Dry run complete - no changes made");
272
353
  return outcome;
273
354
  }
274
355
  log("chrome", `Uploading ZIP file (${formatBytes(zipSize)})`);
275
- await chrome_upload(options.extensionId, options.zip, token);
356
+ await chrome_upload(options.extensionId, options.zip, token, {
357
+ apiVersion,
358
+ publisherId: options.publisherId
359
+ });
276
360
  if (options.skipSubmitReview) {
277
361
  log("chrome", "Skipping publish step (skipSubmitReview=true)");
278
362
  return outcome;
@@ -283,16 +367,36 @@ async function publishChrome(options, dryRun) {
283
367
  publishTarget: options.publishTarget,
284
368
  token,
285
369
  deployPercentage: options.deployPercentage,
286
- reviewExemption: options.reviewExemption
370
+ reviewExemption: options.reviewExemption,
371
+ apiVersion,
372
+ publisherId: options.publisherId
287
373
  });
288
374
  return outcome;
289
375
  }
290
376
  async function getChromeItem(params) {
291
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
+ }
292
396
  const url = `${CWS_ITEMS_BASE}/${encodeURIComponent(params.extensionId)}?projection=${params.projection ?? "DRAFT"}`;
293
397
  return fetchJson(url, {
294
398
  headers: {
295
- Authorization: `${token.token_type} ${token.access_token}`,
399
+ ...auth,
296
400
  "x-goog-api-version": "2"
297
401
  }
298
402
  });
@@ -307,11 +411,24 @@ async function verifyChromeCredentials(params) {
307
411
  message: `OAuth token exchange failed. Verify your Client ID, Client Secret, and Refresh Token are correct.\n${err instanceof Error ? err.message : err}`
308
412
  };
309
413
  }
414
+ const auth = {
415
+ Authorization: `${token.token_type} ${token.access_token}`
416
+ };
417
+ const version = resolveChromeApiVersion(params);
310
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
+ }
311
430
  const item = await fetchJson(`${CWS_ITEMS_BASE}/${encodeURIComponent(params.extensionId)}?projection=DRAFT`, {
312
- headers: {
313
- Authorization: `${token.token_type} ${token.access_token}`
314
- }
431
+ headers: auth
315
432
  });
316
433
  const title = (null == item ? void 0 : item.title) || params.extensionId;
317
434
  return {
@@ -327,7 +444,7 @@ async function verifyChromeCredentials(params) {
327
444
  };
328
445
  if (msg.includes("403")) return {
329
446
  ok: false,
330
- 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.`
331
448
  };
332
449
  return {
333
450
  ok: false,
@@ -341,12 +458,6 @@ function delay(ms) {
341
458
  });
342
459
  }
343
460
  const AMO_BASE = "https://addons.mozilla.org/api/v5/addons";
344
- const AMO_PROFILE_URL = "https://addons.mozilla.org/api/v5/accounts/profile/";
345
- const POLL_INTERVAL_MS = 5000;
346
- const VALIDATION_TIMEOUT_MS = 600000;
347
- function firefoxStoreUrl(addonIdOrSlug) {
348
- return `https://addons.mozilla.org/firefox/addon/${addonIdOrSlug}`;
349
- }
350
461
  function signJwt(issuer, secret, expiresInSeconds = 30) {
351
462
  const header = {
352
463
  alg: "HS256",
@@ -413,6 +524,30 @@ async function createVersion(params) {
413
524
  headers: authHeaders(params.jwtIssuer, params.jwtSecret)
414
525
  });
415
526
  }
527
+ async function createAddon(params) {
528
+ const form = new FormData();
529
+ form.set("version.upload", params.uploadUuid);
530
+ if (params.sourcesZip) {
531
+ const srcBuf = await promises_default.readFile(params.sourcesZip);
532
+ const srcBlob = new Blob([
533
+ srcBuf
534
+ ], {
535
+ type: "application/zip"
536
+ });
537
+ form.set("version.source", srcBlob, "sources.zip");
538
+ }
539
+ return fetchJson(`${AMO_BASE}/addon/`, {
540
+ method: "POST",
541
+ body: form,
542
+ headers: authHeaders(params.jwtIssuer, params.jwtSecret)
543
+ });
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
+ }
416
551
  async function publishFirefox(options, dryRun) {
417
552
  await assertFilePresent(options.zip);
418
553
  const zipSize = await getFileSize(options.zip);
@@ -421,22 +556,28 @@ async function publishFirefox(options, dryRun) {
421
556
  await assertFilePresent(options.sourcesZip);
422
557
  sourcesSize = await getFileSize(options.sourcesZip);
423
558
  }
424
- const addonId = normalizeAddonId(options.extensionId);
559
+ const addonId = normalizeAddonId(options.extensionId ?? "");
425
560
  const { jwtIssuer, jwtSecret } = options;
426
- log("firefox", "Verifying addon exists");
427
- await getAddon(addonId, jwtIssuer, jwtSecret);
561
+ const channel = options.channel ?? "listed";
562
+ const isNewAddon = 0 === addonId.length;
563
+ if (isNewAddon && "listed" === channel) throw new Error('Listed Firefox submissions require an existing add-on GUID. Use channel "unlisted" for a first submission, or set the add-on GUID.');
564
+ if (!isNewAddon) {
565
+ log("firefox", "Verifying addon exists");
566
+ await getAddon(addonId, jwtIssuer, jwtSecret);
567
+ }
428
568
  const outcome = {
429
- storeUrl: firefoxStoreUrl(addonId),
569
+ storeUrl: isNewAddon ? firefoxStoreUrl("new") : firefoxStoreUrl(addonId),
430
570
  submissionId: addonId
431
571
  };
432
572
  if (dryRun) {
433
- logDryStep("firefox", "Addon verified", {
434
- addonId
573
+ logDryStep("firefox", isNewAddon ? "New add-on (no GUID)" : "Addon verified", {
574
+ addonId: isNewAddon ? "(assigned by AMO)" : addonId,
575
+ channel
435
576
  });
436
577
  logDryStep("firefox", "Would upload ZIP", {
437
578
  file: options.zip,
438
579
  size: formatBytes(zipSize),
439
- channel: options.channel ?? "listed",
580
+ channel,
440
581
  target: `${AMO_BASE}/upload/`,
441
582
  method: "POST"
442
583
  });
@@ -444,8 +585,8 @@ async function publishFirefox(options, dryRun) {
444
585
  interval: `${POLL_INTERVAL_MS / 1000}s`,
445
586
  timeout: `${VALIDATION_TIMEOUT_MS / 60000}min`
446
587
  });
447
- logDryStep("firefox", "Would create new version", {
448
- target: `${AMO_BASE}/addon/${addonId}/versions/`,
588
+ logDryStep("firefox", isNewAddon ? "Would create new add-on" : "Would create new version", {
589
+ target: isNewAddon ? `${AMO_BASE}/addon/` : `${AMO_BASE}/addon/${addonId}/versions/`,
449
590
  method: "POST",
450
591
  sourcesZip: options.sourcesZip ? `${options.sourcesZip} (${formatBytes(sourcesSize)})` : "(none)"
451
592
  });
@@ -454,18 +595,31 @@ async function publishFirefox(options, dryRun) {
454
595
  }
455
596
  const upload = await uploadAndAwaitProcessing(options, jwtIssuer, jwtSecret);
456
597
  outcome.submissionId = upload.uuid;
457
- log("firefox", "Creating new version");
458
- const version = await createVersion({
459
- addonId,
460
- sourcesZip: options.sourcesZip,
461
- uploadUuid: upload.uuid,
462
- jwtIssuer,
463
- jwtSecret
464
- });
465
- outcome.submissionId = String(version.id);
466
598
  const { errors, warnings, notices } = upload.validation;
467
599
  log("firefox", `Validation: ${errors} error(s), ${warnings} warning(s), ${notices} notice(s)`);
468
600
  if (!upload.valid) throw new Error("Firefox extension failed validation");
601
+ if (isNewAddon) {
602
+ var _created_version;
603
+ log("firefox", "Creating new add-on");
604
+ const created = await createAddon({
605
+ sourcesZip: options.sourcesZip,
606
+ uploadUuid: upload.uuid,
607
+ jwtIssuer,
608
+ jwtSecret
609
+ });
610
+ outcome.submissionId = String((null == (_created_version = created.version) ? void 0 : _created_version.id) ?? created.id);
611
+ outcome.storeUrl = firefoxStoreUrl(created.slug ?? created.guid);
612
+ } else {
613
+ log("firefox", "Creating new version");
614
+ const version = await createVersion({
615
+ addonId,
616
+ sourcesZip: options.sourcesZip,
617
+ uploadUuid: upload.uuid,
618
+ jwtIssuer,
619
+ jwtSecret
620
+ });
621
+ outcome.submissionId = String(version.id);
622
+ }
469
623
  return outcome;
470
624
  }
471
625
  function getFirefoxVersion(params) {
@@ -759,8 +913,134 @@ async function deploy(config) {
759
913
  success
760
914
  };
761
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
+ }
762
1042
  const DEFAULT_POLL_INTERVAL_MS = 60000;
763
- const DEFAULT_TIMEOUT_MS = 3600000;
1043
+ const watch_DEFAULT_TIMEOUT_MS = 3600000;
764
1044
  function watch_now() {
765
1045
  return new Date().toISOString();
766
1046
  }
@@ -887,7 +1167,7 @@ function watchFirefoxSubmission(input, options = {}) {
887
1167
  }
888
1168
  async function runWatchLoop(_store, probe, options) {
889
1169
  const pollInterval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
890
- const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1170
+ const timeout = options.timeoutMs ?? watch_DEFAULT_TIMEOUT_MS;
891
1171
  const deadline = Date.now() + timeout;
892
1172
  let lastEvent;
893
1173
  while(true){
@@ -899,125 +1179,30 @@ async function runWatchLoop(_store, probe, options) {
899
1179
  await delay(pollInterval);
900
1180
  }
901
1181
  }
902
- const STORE_CHOICES = [
903
- {
904
- value: "chrome",
905
- label: "Chrome Web Store"
906
- },
907
- {
908
- value: "firefox",
909
- label: "Firefox AMO (addons.mozilla.org)"
910
- },
911
- {
912
- value: "edge",
913
- 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.`);
914
1188
  }
915
- ];
1189
+ throw new Error(`init: ${label} is required.`);
1190
+ }
916
1191
  const CHROME_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
917
1192
  const CHROME_OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
918
1193
  const CHROME_OOB_REDIRECT = "urn:ietf:wg:oauth:2.0:oob";
919
1194
  const CHROME_SCOPE = "https://www.googleapis.com/auth/chromewebstore";
920
- async function runInit(deps, options = {}) {
1195
+ async function collectChromeCredentials(deps) {
921
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.");
922
1203
  io.log("");
923
- io.log(`${colors.green("\u23F5\u23F5\u23F5")} ${colors.blue("init")} ${colors.underline("extension-deploy \u2014 set up store credentials")}`);
924
- io.log("");
925
- io.log("This wizard walks you through creating credentials for each store and");
926
- io.log("writes them to a .env.submit file. Press Ctrl+C at any time to abort.");
927
- io.log("");
928
- const stores = options.stores ? options.stores : await io.multiselect("Which stores do you want to configure?", STORE_CHOICES);
929
- 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.");
930
- const envLines = [
931
- "# Generated by `extension-deploy init`.",
932
- "# Do not commit secrets to source control.",
933
- ""
934
- ];
935
- const verified = {};
936
- for (const store of stores){
937
- io.log("");
938
- io.log(colors.underline(colors.blue(labelFor(store))));
939
- io.log("");
940
- if ("chrome" === store) {
941
- const creds = await collectChromeCredentials(deps);
942
- envLines.push(`CHROME_EXTENSION_ID=${creds.extensionId}`, `CHROME_CLIENT_ID=${creds.clientId}`, `CHROME_CLIENT_SECRET=${creds.clientSecret}`, `CHROME_REFRESH_TOKEN=${creds.refreshToken}`, "");
943
- if (!options.skipVerify) {
944
- const verify = deps.verifyChrome ?? verifyChromeCredentials;
945
- verified.chrome = normalizeVerify(await verify(creds));
946
- logVerify(io, "chrome", verified.chrome);
947
- }
948
- } else if ("firefox" === store) {
949
- const creds = await collectFirefoxCredentials(deps);
950
- envLines.push(`FIREFOX_EXTENSION_ID=${creds.addonId}`, `FIREFOX_JWT_ISSUER=${creds.jwtIssuer}`, `FIREFOX_JWT_SECRET=${creds.jwtSecret}`, "");
951
- if (!options.skipVerify) {
952
- const verify = deps.verifyFirefox ?? verifyFirefoxCredentials;
953
- verified.firefox = normalizeVerify(await verify({
954
- jwtIssuer: creds.jwtIssuer,
955
- jwtSecret: creds.jwtSecret,
956
- addonId: creds.addonId
957
- }));
958
- logVerify(io, "firefox", verified.firefox);
959
- }
960
- } else {
961
- const creds = await collectEdgeCredentials(deps);
962
- envLines.push(`EDGE_PRODUCT_ID=${creds.productId}`, `EDGE_CLIENT_ID=${creds.clientId}`, `EDGE_API_KEY=${creds.apiKey}`, "");
963
- if (!options.skipVerify) {
964
- const verify = deps.verifyEdge ?? verifyEdgeCredentials;
965
- verified.edge = normalizeVerify(await verify({
966
- clientId: creds.clientId,
967
- apiKey: creds.apiKey,
968
- productId: creds.productId
969
- }));
970
- logVerify(io, "edge", verified.edge);
971
- }
972
- }
973
- }
974
- const envContent = envLines.join("\n");
975
- const envPath = resolveEnvPath(deps.cwd, options.outputPath);
976
- const exists = await deps.fileExists(envPath);
977
- let wrote = false;
978
- if (exists && !options.force) {
979
- const overwrite = await deps.io.confirm(`${envPath} already exists. Overwrite?`, false);
980
- if (overwrite) {
981
- await deps.writeFile(envPath, envContent);
982
- wrote = true;
983
- } else {
984
- io.log("");
985
- io.log(`Skipped writing ${envPath}. Credential block:`);
986
- io.log("");
987
- io.log(envContent);
988
- }
989
- } else {
990
- await deps.writeFile(envPath, envContent);
991
- wrote = true;
992
- }
993
- if (wrote) {
994
- io.log("");
995
- io.log(`${colors.green("\u23F5\u23F5\u23F5")} ${colors.blue("init")} Wrote ${colors.underline(envPath)}`);
996
- io.log("");
997
- io.log(colors.gray("Next steps:"));
998
- io.log(" - Add .env.submit to .gitignore if it isn't already (it contains secrets).");
999
- io.log(" - Run `extension-deploy --dry-run` to sanity-check the full submit flow.");
1000
- io.log("");
1001
- }
1002
- return {
1003
- stores,
1004
- envPath,
1005
- envContent,
1006
- verified,
1007
- wrote
1008
- };
1009
- }
1010
- async function collectChromeCredentials(deps) {
1011
- const { io } = deps;
1012
- io.log("Chrome Web Store needs OAuth credentials. You'll need:");
1013
- io.log(" 1. A Google Cloud project with the Chrome Web Store API enabled:");
1014
- io.log(" https://console.cloud.google.com/apis/library/chromewebstore.googleapis.com");
1015
- io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
1016
- io.log(" https://console.cloud.google.com/apis/credentials");
1017
- io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
1018
- io.log("");
1019
- io.log("This wizard will take your Client ID + Secret and walk you through");
1020
- 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.");
1021
1206
  io.log("");
1022
1207
  const extensionId = await requireValue(io, "Chrome extension ID", "(32-character ID from the CWS dashboard URL)");
1023
1208
  const clientId = await requireValue(io, "OAuth Client ID");
@@ -1122,6 +1307,110 @@ async function collectEdgeCredentials(deps) {
1122
1307
  apiKey
1123
1308
  };
1124
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
+ }
1125
1414
  function labelFor(store) {
1126
1415
  var _STORE_CHOICES_find;
1127
1416
  return (null == (_STORE_CHOICES_find = STORE_CHOICES.find((c)=>c.value === store)) ? void 0 : _STORE_CHOICES_find.label) ?? String(store);
@@ -1140,15 +1429,6 @@ function resolveEnvPath(cwd, outputPath) {
1140
1429
  const target = outputPath ?? ".env.submit";
1141
1430
  return external_node_path_default.isAbsolute(target) ? target : external_node_path_default.join(cwd, target);
1142
1431
  }
1143
- async function requireValue(io, label, hint, opts) {
1144
- const question = hint ? `${label} ${hint}:` : `${label}:`;
1145
- for(let attempt = 0; attempt < 3; attempt++){
1146
- const answer = (await io.prompt(question, opts)).trim();
1147
- if (answer) return answer;
1148
- io.log(` ${label} is required.`);
1149
- }
1150
- throw new Error(`init: ${label} is required.`);
1151
- }
1152
1432
  function createStdinIO() {
1153
1433
  const rl = external_node_readline_default.createInterface({
1154
1434
  input: process.stdin,
@@ -1202,7 +1482,13 @@ function createStdinIO() {
1202
1482
  }
1203
1483
  const stdinFs = {
1204
1484
  async writeFile (filePath, content) {
1205
- await promises_default.writeFile(filePath, content, "utf-8");
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 {}
1206
1492
  },
1207
1493
  async fileExists (filePath) {
1208
1494
  try {
@@ -1213,173 +1499,12 @@ const stdinFs = {
1213
1499
  }
1214
1500
  }
1215
1501
  };
1216
- function usage() {
1217
- return `
1218
- extension-deploy \u{2014} Deploy browser extensions to Chrome, Firefox, and Edge.
1219
-
1220
- USAGE
1221
- extension-deploy [options] submit to one or more stores
1222
- extension-deploy init [options] interactive credential setup wizard
1223
- extension-deploy watch --chrome [options] poll an in-flight submission
1224
- extension-deploy watch --firefox [options]
1225
- extension-deploy watch --edge [options]
1226
-
1227
- GLOBAL
1228
- --dry-run Verify auth without uploading or publishing
1229
- --output-json <path> Write the DeployResult / WatchEvent as JSON to <path>
1230
- --help Show this help message
1231
-
1232
- CHROME
1233
- --chrome-zip <path> Path to Chrome extension ZIP
1234
- --chrome-extension-id <id> Chrome extension ID
1235
- --chrome-client-id <id> OAuth2 client ID
1236
- --chrome-client-secret <secret> OAuth2 client secret
1237
- --chrome-refresh-token <token> OAuth2 refresh token
1238
- --chrome-publish-target <target> "default" or "trustedTesters"
1239
- --chrome-deploy-percentage <n> Staged rollout percentage (1-100)
1240
- --chrome-review-exemption Request expedited review
1241
- --chrome-skip-submit-review Upload only, skip publish
1242
-
1243
- FIREFOX
1244
- --firefox-zip <path> Path to Firefox extension ZIP
1245
- --firefox-sources-zip <path> Path to sources ZIP (optional)
1246
- --firefox-extension-id <id> Addon GUID or email-style ID
1247
- --firefox-jwt-issuer <issuer> AMO JWT issuer
1248
- --firefox-jwt-secret <secret> AMO JWT secret
1249
- --firefox-channel <channel> "listed" or "unlisted"
1250
-
1251
- EDGE
1252
- --edge-zip <path> Path to Edge extension ZIP
1253
- --edge-product-id <id> Partner Center product ID
1254
- --edge-client-id <id> Partner Center client ID
1255
- --edge-api-key <key> Partner Center API key (v1.1)
1256
- --edge-skip-submit-review Upload only, skip publish
1257
-
1258
- WATCH
1259
- --watch-submission-id <id> Required for firefox/edge (version id / operation id)
1260
- --watch-interval <seconds> Poll interval (default 60)
1261
- --watch-timeout <seconds> Max time to wait for a terminal state (default 3600)
1262
-
1263
- INIT
1264
- --init-stores <list> Comma-separated store list (skip picker)
1265
- --init-output <path> Destination file (default .env.submit)
1266
- --init-skip-verify Don't run verifyCredentials after collection
1267
- --init-force Overwrite existing .env.submit without asking
1268
-
1269
- ENVIRONMENT VARIABLES
1270
- All flags can be set via SCREAMING_SNAKE_CASE env vars (e.g. CHROME_ZIP,
1271
- FIREFOX_JWT_SECRET, EDGE_API_KEY). Flags take precedence over env vars.
1272
- A .env.submit file in the current directory is loaded automatically.
1273
- `.trim();
1274
- }
1275
- const BOOLEAN_FLAGS = new Set([
1276
- "help",
1277
- "dryRun",
1278
- "chromeReviewExemption",
1279
- "chromeSkipSubmitReview",
1280
- "edgeSkipSubmitReview",
1281
- "initSkipVerify",
1282
- "initForce"
1283
- ]);
1284
- const INT_FLAGS = new Set([
1285
- "chromeDeployPercentage",
1286
- "watchInterval",
1287
- "watchTimeout"
1288
- ]);
1289
- function parseArgs(argv) {
1290
- const flags = {};
1291
- let i = 0;
1292
- if ("watch" === argv[0]) {
1293
- flags.command = "watch";
1294
- i = 1;
1295
- } else if ("init" === argv[0]) {
1296
- flags.command = "init";
1297
- i = 1;
1298
- }
1299
- while(i < argv.length){
1300
- const arg = argv[i];
1301
- if ("--help" === arg || "-h" === arg) {
1302
- flags.help = true;
1303
- i++;
1304
- continue;
1305
- }
1306
- if ("--chrome" === arg) {
1307
- flags.watchStore = "chrome";
1308
- i++;
1309
- continue;
1310
- }
1311
- if ("--firefox" === arg) {
1312
- flags.watchStore = "firefox";
1313
- i++;
1314
- continue;
1315
- }
1316
- if ("--edge" === arg) {
1317
- flags.watchStore = "edge";
1318
- i++;
1319
- continue;
1320
- }
1321
- if (!(null == arg ? void 0 : arg.startsWith("--"))) {
1322
- i++;
1323
- continue;
1324
- }
1325
- const key = arg.slice(2).replace(/-([a-z])/g, (_, c)=>c.toUpperCase());
1326
- if (BOOLEAN_FLAGS.has(key)) {
1327
- flags[key] = true;
1328
- i++;
1329
- continue;
1330
- }
1331
- const next = argv[i + 1];
1332
- if (null == next || next.startsWith("--")) {
1333
- flags[key] = true;
1334
- i++;
1335
- continue;
1336
- }
1337
- flags[key] = INT_FLAGS.has(key) ? parseInt(next, 10) : next;
1338
- i += 2;
1339
- }
1340
- return flags;
1341
- }
1342
- function loadDotEnv() {
1343
- try {
1344
- const envPath = external_node_path_default.resolve(process.cwd(), ".env.submit");
1345
- const content = external_node_fs_default.readFileSync(envPath, "utf-8");
1346
- for (const line of content.split("\n")){
1347
- const trimmed = line.trim();
1348
- if (!trimmed || trimmed.startsWith("#")) continue;
1349
- const eqIdx = trimmed.indexOf("=");
1350
- if (-1 === eqIdx) continue;
1351
- const key = trimmed.slice(0, eqIdx).trim();
1352
- let value = trimmed.slice(eqIdx + 1).trim();
1353
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1354
- if (!(key in process.env)) process.env[key] = value;
1355
- }
1356
- } catch {}
1357
- }
1358
1502
  function writeJsonOutput(filePath, payload) {
1359
1503
  external_node_fs_default.mkdirSync(external_node_path_default.dirname(external_node_path_default.resolve(filePath)), {
1360
1504
  recursive: true
1361
1505
  });
1362
1506
  external_node_fs_default.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
1363
1507
  }
1364
- async function main() {
1365
- loadDotEnv();
1366
- const flags = parseArgs(process.argv.slice(2));
1367
- if (flags.help) {
1368
- console.log(usage());
1369
- process.exit(0);
1370
- }
1371
- if ("watch" === flags.command) return void await runWatch(flags);
1372
- if ("init" === flags.command) return void await runInitCommand(flags);
1373
- const config = resolveConfig(flags);
1374
- try {
1375
- const result = await deploy(config);
1376
- if (flags.outputJson) writeJsonOutput(flags.outputJson, result);
1377
- if (!result.success) process.exit(1);
1378
- } catch (err) {
1379
- logError("deploy", err instanceof Error ? err.message : String(err));
1380
- process.exit(1);
1381
- }
1382
- }
1383
1508
  async function runWatch(flags) {
1384
1509
  const store = flags.watchStore;
1385
1510
  if (!store) {
@@ -1475,4 +1600,39 @@ function parseInitStores(raw) {
1475
1600
  if (0 === parsed.length) throw new Error(`--init-stores: expected a comma-separated list of chrome,firefox,edge (got "${raw}")`);
1476
1601
  return parsed;
1477
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
+ }
1478
1638
  export { chromeOptionsSchema, deploy, deployConfigSchema, edgeOptionsSchema, firefoxOptionsSchema, getChromeSubmissionEvent, getEdgeSubmissionEvent, getFirefoxSubmissionEvent, main, resolveConfig, validateConfig, verifyChromeCredentials, verifyEdgeCredentials, verifyFirefoxCredentials, watchChromeSubmission, watchEdgeSubmission, watchFirefoxSubmission };