@extension.dev/deploy 0.3.0 → 1.1.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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0
4
+
5
+ - Chrome: service account authentication (`CHROME_SERVICE_ACCOUNT_JSON` /
6
+ `--chrome-service-account-json`, JSON key content or file path) as an
7
+ alternative to the OAuth client/secret/refresh-token trio. The service
8
+ account wins when both are set. Avoids the 7-day refresh-token expiry of
9
+ OAuth clients whose consent screen is in "Testing" status. Add the
10
+ account's email to your publisher in the dev console (Account section).
11
+
12
+ ## 1.0.0
13
+
14
+ - BREAKING: Chrome Web Store API v2 only (`chromewebstore.googleapis.com`).
15
+ `CHROME_PUBLISHER_ID` is required; the v1.1 path, `publishTarget`, and the
16
+ trusted-testers target were removed (staged rollout via
17
+ `CHROME_DEPLOY_PERCENTAGE` replaces them).
18
+
19
+ ## 0.3.0
20
+
21
+ - Chrome Web Store API v2 support, opt-in via `CHROME_PUBLISHER_ID`
22
+ (v1.1 fallback retained).
23
+
3
24
  ## 0.2.5
4
25
 
5
26
  Current release. Earlier history is tracked in the source monorepo; entries
package/README.md CHANGED
@@ -78,14 +78,22 @@ Run `extension-deploy --help` for the full list of flags.
78
78
  | ---------------------------------- | --------------------------- | --------------------------------- |
79
79
  | `--chrome-zip <path>` | `CHROME_ZIP` | Path to the extension ZIP file |
80
80
  | `--chrome-extension-id <id>` | `CHROME_EXTENSION_ID` | Chrome Web Store extension ID |
81
+ | `--chrome-publisher-id <uuid>` | `CHROME_PUBLISHER_ID` | Publisher UUID from the dev console (required) |
82
+ | `--chrome-service-account-json <v>` | `CHROME_SERVICE_ACCOUNT_JSON` | Service account JSON key (content or file path); alternative to the OAuth trio below |
81
83
  | `--chrome-client-id <id>` | `CHROME_CLIENT_ID` | OAuth2 client ID |
82
84
  | `--chrome-client-secret <secret>` | `CHROME_CLIENT_SECRET` | OAuth2 client secret |
83
85
  | `--chrome-refresh-token <token>` | `CHROME_REFRESH_TOKEN` | OAuth2 refresh token |
84
- | `--chrome-publish-target <target>` | `CHROME_PUBLISH_TARGET` | `"default"` or `"trustedTesters"` |
85
86
  | `--chrome-deploy-percentage <n>` | `CHROME_DEPLOY_PERCENTAGE` | Staged rollout percentage (1-100) |
86
87
  | `--chrome-review-exemption` | `CHROME_REVIEW_EXEMPTION` | Request expedited review |
87
88
  | `--chrome-skip-submit-review` | `CHROME_SKIP_SUBMIT_REVIEW` | Upload only, skip publish step |
88
89
 
90
+ Chrome accepts two credential shapes: a Google Cloud **service account** JSON key
91
+ (recommended for CI; add the account's email to your publisher under
92
+ dev console > Account, one per publisher) or the classic OAuth2
93
+ client ID / client secret / refresh token trio. When both are set, the service
94
+ account is used. Service accounts avoid the 7-day refresh-token expiry that
95
+ affects OAuth clients whose consent screen is in "Testing" status.
96
+
89
97
  ### Firefox AMO
90
98
 
91
99
  | Flag | Env var | Description |
@@ -156,7 +164,7 @@ const result = await deploy({
156
164
  clientId: "123.apps.googleusercontent.com",
157
165
  clientSecret: "GOCSPX-xxx",
158
166
  refreshToken: "1//0xxx",
159
- publishTarget: "default",
167
+ publisherId: "00000000-0000-0000-0000-000000000000", // required
160
168
  deployPercentage: 10, // optional: staged rollout
161
169
  },
162
170
  firefox: {
package/dist/module.js CHANGED
@@ -59,21 +59,28 @@ const external_zod_namespaceObject = require("zod");
59
59
  const chromeOptionsSchema = external_zod_namespaceObject.z.object({
60
60
  zip: external_zod_namespaceObject.z.string().min(1),
61
61
  extensionId: external_zod_namespaceObject.z.string().min(1).trim(),
62
- clientId: external_zod_namespaceObject.z.string().min(1).trim(),
63
- clientSecret: external_zod_namespaceObject.z.string().min(1).trim(),
64
- refreshToken: external_zod_namespaceObject.z.string().min(1).trim(),
65
- publishTarget: external_zod_namespaceObject.z["enum"]([
66
- "default",
67
- "trustedTesters"
68
- ]).default("default"),
62
+ serviceAccountJson: external_zod_namespaceObject.z.string().trim().default(""),
63
+ clientId: external_zod_namespaceObject.z.string().trim().default(""),
64
+ clientSecret: external_zod_namespaceObject.z.string().trim().default(""),
65
+ refreshToken: external_zod_namespaceObject.z.string().trim().default(""),
66
+ publisherId: external_zod_namespaceObject.z.string().min(1).trim(),
69
67
  deployPercentage: external_zod_namespaceObject.z.number().int().min(1).max(100).optional(),
70
68
  reviewExemption: 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()
69
+ skipSubmitReview: external_zod_namespaceObject.z.boolean().default(false)
70
+ }).superRefine((val, ctx)=>{
71
+ if (val.serviceAccountJson) return;
72
+ const missing = [
73
+ "clientId",
74
+ "clientSecret",
75
+ "refreshToken"
76
+ ].filter((k)=>!val[k]);
77
+ for (const key of missing)ctx.addIssue({
78
+ code: external_zod_namespaceObject.z.ZodIssueCode.custom,
79
+ path: [
80
+ key
81
+ ],
82
+ message: "Required unless a service account key is provided (CHROME_SERVICE_ACCOUNT_JSON)."
83
+ });
77
84
  });
78
85
  const firefoxOptionsSchema = external_zod_namespaceObject.z.object({
79
86
  zip: external_zod_namespaceObject.z.string().min(1),
@@ -117,15 +124,14 @@ function resolveConfig(flags) {
117
124
  chrome: null == chromeZip ? void 0 : {
118
125
  zip: chromeZip,
119
126
  extensionId: flags.chromeExtensionId ?? envStr("CHROME_EXTENSION_ID") ?? "",
127
+ serviceAccountJson: flags.chromeServiceAccountJson ?? envStr("CHROME_SERVICE_ACCOUNT_JSON") ?? "",
120
128
  clientId: flags.chromeClientId ?? envStr("CHROME_CLIENT_ID") ?? "",
121
129
  clientSecret: flags.chromeClientSecret ?? envStr("CHROME_CLIENT_SECRET") ?? "",
122
130
  refreshToken: flags.chromeRefreshToken ?? envStr("CHROME_REFRESH_TOKEN") ?? "",
123
- publishTarget: flags.chromePublishTarget ?? envStr("CHROME_PUBLISH_TARGET"),
131
+ publisherId: flags.chromePublisherId ?? envStr("CHROME_PUBLISHER_ID") ?? "",
124
132
  deployPercentage: flags.chromeDeployPercentage ?? envInt("CHROME_DEPLOY_PERCENTAGE"),
125
133
  reviewExemption: flags.chromeReviewExemption ?? envBool("CHROME_REVIEW_EXEMPTION"),
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")
134
+ skipSubmitReview: flags.chromeSkipSubmitReview ?? envBool("CHROME_SKIP_SUBMIT_REVIEW")
129
135
  },
130
136
  firefox: null == firefoxZip ? void 0 : {
131
137
  zip: firefoxZip,
@@ -149,10 +155,11 @@ function pathToEnvName(segments) {
149
155
  }
150
156
  const SETUP_HINTS = {
151
157
  CHROME_EXTENSION_ID: "Find this in the Chrome Web Store Developer Dashboard URL for your extension.",
158
+ CHROME_SERVICE_ACCOUNT_JSON: "Optional alternative to OAuth: a Google Cloud service account JSON key (content or file path), added to your publisher under dev console > Account.",
152
159
  CHROME_CLIENT_ID: "Create OAuth 2.0 credentials at https://console.cloud.google.com/apis/credentials",
153
160
  CHROME_CLIENT_SECRET: "Generated alongside the Client ID in the Google Cloud Console.",
154
161
  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.",
162
+ CHROME_PUBLISHER_ID: "The publisher UUID from your Chrome Web Store dev console URL (chrome.google.com/webstore/devconsole/<UUID>).",
156
163
  CHROME_ZIP: "Path to the .zip file built for Chrome Web Store upload.",
157
164
  FIREFOX_EXTENSION_ID: "The addon GUID ({uuid} format) or email-style ID from your addon's AMO page.",
158
165
  FIREFOX_JWT_ISSUER: "API key from https://addons.mozilla.org/developers/addon/api/key/",
@@ -193,6 +200,8 @@ function envInt(name) {
193
200
  const val = process.env[name];
194
201
  return null == val ? void 0 : parseInt(val, 10);
195
202
  }
203
+ const external_node_crypto_namespaceObject = require("node:crypto");
204
+ var external_node_crypto_default = /*#__PURE__*/ __webpack_require__.n(external_node_crypto_namespaceObject);
196
205
  const promises_namespaceObject = require("node:fs/promises");
197
206
  var promises_default = /*#__PURE__*/ __webpack_require__.n(promises_namespaceObject);
198
207
  const DEFAULT_TIMEOUT_MS = 60000;
@@ -268,27 +277,64 @@ function logDryStep(scope, step, details) {
268
277
  const UPLOAD_TIMEOUT_MS = 120000;
269
278
  const PUBLISH_TIMEOUT_MS = 60000;
270
279
  const CWS_OAUTH_URL = "https://oauth2.googleapis.com/token";
271
- const CWS_UPLOAD_BASE = "https://www.googleapis.com/upload/chromewebstore/v1.1/items";
272
- const CWS_PUBLISH_BASE = "https://www.googleapis.com/chromewebstore/v1.1/items";
273
- const CWS_ITEMS_BASE = "https://www.googleapis.com/chromewebstore/v1.1/items";
274
280
  const CWS_V2_BASE = "https://chromewebstore.googleapis.com/v2";
275
281
  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
282
  function v2ItemName(publisherId, extensionId) {
281
283
  return `publishers/${encodeURIComponent(publisherId)}/items/${encodeURIComponent(extensionId)}`;
282
284
  }
283
285
  function requirePublisherId(publisherId, op) {
284
286
  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).`);
287
+ if (!id) throw new Error(`Chrome ${op} needs a publisher ID (the UUID from your Chrome Web Store dev console URL).`);
286
288
  return id;
287
289
  }
288
290
  function chromeStoreUrl(extensionId) {
289
291
  return `https://chromewebstore.google.com/detail/${extensionId}`;
290
292
  }
291
- async function authenticate(clientId, clientSecret, refreshToken) {
293
+ const CWS_SCOPE = "https://www.googleapis.com/auth/chromewebstore";
294
+ async function resolveServiceAccountKey(value) {
295
+ const trimmed = value.trim();
296
+ const raw = trimmed.startsWith("{") ? trimmed : await promises_default().readFile(trimmed, "utf-8").catch(()=>{
297
+ throw new Error(`Chrome service account key: "${trimmed.slice(0, 80)}" is neither JSON nor a readable file path.`);
298
+ });
299
+ let parsed;
300
+ try {
301
+ parsed = JSON.parse(raw);
302
+ } catch {
303
+ throw new Error("Chrome service account key is not valid JSON.");
304
+ }
305
+ if (!parsed.client_email || !parsed.private_key) throw new Error("Chrome service account key is missing client_email or private_key. Download the JSON key from the Google Cloud Console.");
306
+ return {
307
+ client_email: parsed.client_email,
308
+ private_key: parsed.private_key
309
+ };
310
+ }
311
+ async function authenticateServiceAccount(serviceAccountJson) {
312
+ const key = await resolveServiceAccountKey(serviceAccountJson);
313
+ const iat = Math.floor(Date.now() / 1000);
314
+ const encode = (obj)=>Buffer.from(JSON.stringify(obj)).toString("base64url");
315
+ const unsigned = `${encode({
316
+ alg: "RS256",
317
+ typ: "JWT"
318
+ })}.${encode({
319
+ iss: key.client_email,
320
+ scope: CWS_SCOPE,
321
+ aud: CWS_OAUTH_URL,
322
+ iat,
323
+ exp: iat + 3600
324
+ })}`;
325
+ const signature = external_node_crypto_default().createSign("RSA-SHA256").update(unsigned).sign(key.private_key).toString("base64url");
326
+ return fetchJson(CWS_OAUTH_URL, {
327
+ method: "POST",
328
+ body: new URLSearchParams({
329
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
330
+ assertion: `${unsigned}.${signature}`
331
+ }).toString(),
332
+ headers: {
333
+ "Content-Type": "application/x-www-form-urlencoded"
334
+ }
335
+ });
336
+ }
337
+ async function authenticateOauth(clientId, clientSecret, refreshToken) {
292
338
  return fetchJson(CWS_OAUTH_URL, {
293
339
  method: "POST",
294
340
  body: JSON.stringify({
@@ -303,27 +349,21 @@ async function authenticate(clientId, clientSecret, refreshToken) {
303
349
  }
304
350
  });
305
351
  }
306
- async function chrome_upload(extensionId, zipPath, token, ctx) {
352
+ async function authenticate(creds) {
353
+ if (creds.serviceAccountJson) return authenticateServiceAccount(creds.serviceAccountJson);
354
+ if (creds.clientId && creds.clientSecret && creds.refreshToken) return authenticateOauth(creds.clientId, creds.clientSecret, creds.refreshToken);
355
+ throw new Error("Chrome credentials missing: provide a service account key (CHROME_SERVICE_ACCOUNT_JSON) or the OAuth client ID, client secret, and refresh token.");
356
+ }
357
+ async function chrome_upload(extensionId, publisherId, zipPath, token) {
307
358
  const body = await promises_default().readFile(zipPath);
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
- }
359
+ const url = `${CWS_V2_UPLOAD_BASE}/${v2ItemName(publisherId, extensionId)}:upload`;
323
360
  const res = await fetch(url, {
324
- method,
361
+ method: "POST",
325
362
  body,
326
- headers,
363
+ headers: {
364
+ Authorization: `${token.token_type} ${token.access_token}`,
365
+ "Content-Type": "application/zip"
366
+ },
327
367
  signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
328
368
  });
329
369
  if (!res.ok) {
@@ -332,44 +372,22 @@ async function chrome_upload(extensionId, zipPath, token, ctx) {
332
372
  }
333
373
  }
334
374
  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}`);
375
+ const reqBody = {
376
+ publishType: "DEFAULT_PUBLISH"
377
+ };
378
+ if (null != params.deployPercentage) reqBody.deployInfos = [
379
+ {
380
+ deployPercentage: params.deployPercentage
359
381
  }
360
- return;
361
- }
362
- const url = new URL(`${CWS_PUBLISH_BASE}/${params.extensionId}/publish`);
363
- url.searchParams.set("publishTarget", params.publishTarget);
364
- if (null != params.deployPercentage) url.searchParams.set("deployPercentage", String(params.deployPercentage));
365
- if (null != params.reviewExemption) url.searchParams.set("reviewExemption", String(params.reviewExemption));
366
- const res = await fetch(url.href, {
382
+ ];
383
+ if (params.reviewExemption) reqBody.skipReview = true;
384
+ const res = await fetch(`${CWS_V2_BASE}/${v2ItemName(params.publisherId, params.extensionId)}:publish`, {
367
385
  method: "POST",
368
386
  headers: {
369
387
  Authorization: `${params.token.token_type} ${params.token.access_token}`,
370
- "x-goog-api-version": "2",
371
- "Content-Length": "0"
388
+ "Content-Type": "application/json"
372
389
  },
390
+ body: JSON.stringify(reqBody),
373
391
  signal: AbortSignal.timeout(PUBLISH_TIMEOUT_MS)
374
392
  });
375
393
  if (!res.ok) {
@@ -380,9 +398,9 @@ async function publish(params) {
380
398
  async function publishChrome(options, dryRun) {
381
399
  await assertFilePresent(options.zip);
382
400
  const zipSize = await getFileSize(options.zip);
383
- const apiVersion = resolveChromeApiVersion(options);
384
- log("chrome", `Authenticating with Chrome Web Store (API ${apiVersion})`);
385
- const token = await authenticate(options.clientId, options.clientSecret, options.refreshToken);
401
+ const publisherId = requirePublisherId(options.publisherId, "submit");
402
+ log("chrome", `Authenticating with Chrome Web Store (API v2, ${options.serviceAccountJson ? "service account" : "OAuth"})`);
403
+ const token = await authenticate(options);
386
404
  const outcome = {
387
405
  storeUrl: chromeStoreUrl(options.extensionId),
388
406
  submissionId: options.extensionId
@@ -390,17 +408,13 @@ async function publishChrome(options, dryRun) {
390
408
  if (dryRun) {
391
409
  logDryStep("chrome", "Authentication succeeded", {
392
410
  tokenType: token.token_type,
393
- scope: "chromewebstore",
394
- apiVersion
411
+ scope: "chromewebstore"
395
412
  });
396
413
  logDryStep("chrome", "Would upload ZIP", {
397
414
  file: options.zip,
398
- size: formatBytes(zipSize),
399
- apiVersion
415
+ size: formatBytes(zipSize)
400
416
  });
401
417
  options.skipSubmitReview ? logDryStep("chrome", "Would skip publish (skipSubmitReview=true)") : logDryStep("chrome", "Would publish for review", {
402
- apiVersion,
403
- publishTarget: options.publishTarget ?? "default",
404
418
  deployPercentage: options.deployPercentage,
405
419
  reviewExemption: options.reviewExemption
406
420
  });
@@ -408,10 +422,7 @@ async function publishChrome(options, dryRun) {
408
422
  return outcome;
409
423
  }
410
424
  log("chrome", `Uploading ZIP file (${formatBytes(zipSize)})`);
411
- await chrome_upload(options.extensionId, options.zip, token, {
412
- apiVersion,
413
- publisherId: options.publisherId
414
- });
425
+ await chrome_upload(options.extensionId, publisherId, options.zip, token);
415
426
  if (options.skipSubmitReview) {
416
427
  log("chrome", "Skipping publish step (skipSubmitReview=true)");
417
428
  return outcome;
@@ -419,77 +430,54 @@ async function publishChrome(options, dryRun) {
419
430
  log("chrome", "Publishing extension");
420
431
  await publish({
421
432
  extensionId: options.extensionId,
422
- publishTarget: options.publishTarget,
433
+ publisherId,
423
434
  token,
424
435
  deployPercentage: options.deployPercentage,
425
- reviewExemption: options.reviewExemption,
426
- apiVersion,
427
- publisherId: options.publisherId
436
+ reviewExemption: options.reviewExemption
428
437
  });
429
438
  return outcome;
430
439
  }
431
440
  async function getChromeItem(params) {
432
- const token = await authenticate(params.clientId, params.clientSecret, params.refreshToken);
441
+ var _rev_distributionChannels_, _rev_distributionChannels;
442
+ const token = await authenticate(params);
433
443
  const auth = {
434
444
  Authorization: `${token.token_type} ${token.access_token}`
435
445
  };
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
- }
451
- const url = `${CWS_ITEMS_BASE}/${encodeURIComponent(params.extensionId)}?projection=${params.projection ?? "DRAFT"}`;
452
- return fetchJson(url, {
453
- headers: {
454
- ...auth,
455
- "x-goog-api-version": "2"
456
- }
446
+ const pub = requirePublisherId(params.publisherId, "status");
447
+ const status = await fetchJson(`${CWS_V2_BASE}/${v2ItemName(pub, params.extensionId)}:fetchStatus`, {
448
+ headers: auth
457
449
  });
450
+ const rev = status.submittedItemRevisionStatus || status.publishedItemRevisionStatus;
451
+ return {
452
+ uploadState: status.lastAsyncUploadState,
453
+ crxVersion: null == rev ? void 0 : null == (_rev_distributionChannels = rev.distributionChannels) ? void 0 : null == (_rev_distributionChannels_ = _rev_distributionChannels[0]) ? void 0 : _rev_distributionChannels_.crxVersion,
454
+ publicKey: status.publicKey,
455
+ state: null == rev ? void 0 : rev.state
456
+ };
458
457
  }
459
458
  async function verifyChromeCredentials(params) {
460
459
  let token;
461
460
  try {
462
- token = await authenticate(params.clientId, params.clientSecret, params.refreshToken);
461
+ token = await authenticate(params);
463
462
  } catch (err) {
463
+ const detail = err instanceof Error ? err.message : String(err);
464
464
  return {
465
465
  ok: false,
466
- message: `OAuth token exchange failed. Verify your Client ID, Client Secret, and Refresh Token are correct.\n${err instanceof Error ? err.message : err}`
466
+ message: params.serviceAccountJson ? `Service account token exchange failed. Verify the JSON key is intact and the account was added to your publisher (dev console > Account).\n${detail}` : `OAuth token exchange failed. Verify your Client ID, Client Secret, and Refresh Token are correct.\n${detail}`
467
467
  };
468
468
  }
469
469
  const auth = {
470
470
  Authorization: `${token.token_type} ${token.access_token}`
471
471
  };
472
- const version = resolveChromeApiVersion(params);
473
472
  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
- }
485
- const item = await fetchJson(`${CWS_ITEMS_BASE}/${encodeURIComponent(params.extensionId)}?projection=DRAFT`, {
473
+ const pub = requirePublisherId(params.publisherId, "verify");
474
+ const status = await fetchJson(`${CWS_V2_BASE}/${v2ItemName(pub, params.extensionId)}:fetchStatus`, {
486
475
  headers: auth
487
476
  });
488
- const title = (null == item ? void 0 : item.title) || params.extensionId;
477
+ const rev = status.submittedItemRevisionStatus || status.publishedItemRevisionStatus;
489
478
  return {
490
479
  ok: true,
491
- message: `Credentials verified. You have access to "${title}".`,
492
- extensionTitle: title
480
+ message: `Credentials verified (Chrome Web Store API v2). Item state: ${(null == rev ? void 0 : rev.state) || "unknown"}.`
493
481
  };
494
482
  } catch (err) {
495
483
  const msg = err instanceof Error ? err.message : String(err);
@@ -499,7 +487,7 @@ async function verifyChromeCredentials(params) {
499
487
  };
500
488
  if (msg.includes("403")) return {
501
489
  ok: false,
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.`
490
+ message: params.serviceAccountJson ? "Access denied. Check the publisher ID, and that this service account's email was added to the publisher (dev console > Account)." : "Access denied. Check the publisher ID matches this extension's account, and that the extension ID is correct."
503
491
  };
504
492
  return {
505
493
  ok: false,
@@ -512,8 +500,6 @@ function delay(ms) {
512
500
  setTimeout(resolve, ms);
513
501
  });
514
502
  }
515
- const external_node_crypto_namespaceObject = require("node:crypto");
516
- var external_node_crypto_default = /*#__PURE__*/ __webpack_require__.n(external_node_crypto_namespaceObject);
517
503
  const AMO_BASE = "https://addons.mozilla.org/api/v5/addons";
518
504
  function signJwt(issuer, secret, expiresInSeconds = 30) {
519
505
  const header = {
@@ -989,10 +975,12 @@ GLOBAL
989
975
  CHROME
990
976
  --chrome-zip <path> Path to Chrome extension ZIP
991
977
  --chrome-extension-id <id> Chrome extension ID
978
+ --chrome-publisher-id <uuid> Publisher UUID from the dev console (required)
979
+ --chrome-service-account-json <v> Service account JSON key content or file path
980
+ (alternative to the OAuth flags below)
992
981
  --chrome-client-id <id> OAuth2 client ID
993
982
  --chrome-client-secret <secret> OAuth2 client secret
994
983
  --chrome-refresh-token <token> OAuth2 refresh token
995
- --chrome-publish-target <target> "default" or "trustedTesters"
996
984
  --chrome-deploy-percentage <n> Staged rollout percentage (1-100)
997
985
  --chrome-review-exemption Request expedited review
998
986
  --chrome-skip-submit-review Upload only, skip publish
@@ -1257,11 +1245,13 @@ async function collectChromeCredentials(deps) {
1257
1245
  io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
1258
1246
  io.log(" https://console.cloud.google.com/apis/credentials");
1259
1247
  io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
1248
+ io.log(" 4. Your publisher ID (the UUID in your dev console URL: chrome.google.com/webstore/devconsole/<UUID>).");
1260
1249
  io.log("");
1261
1250
  io.log("This wizard will take your Client ID + Secret and walk you through");
1262
1251
  io.log("generating a refresh token automatically.");
1263
1252
  io.log("");
1264
1253
  const extensionId = await requireValue(io, "Chrome extension ID", "(32-character ID from the CWS dashboard URL)");
1254
+ const publisherId = await requireValue(io, "Chrome publisher ID", "(UUID from your dev console URL)");
1265
1255
  const clientId = await requireValue(io, "OAuth Client ID");
1266
1256
  const clientSecret = await requireValue(io, "OAuth Client Secret", void 0, {
1267
1257
  secret: true
@@ -1269,6 +1259,7 @@ async function collectChromeCredentials(deps) {
1269
1259
  const refreshToken = await obtainChromeRefreshToken(deps, clientId, clientSecret);
1270
1260
  return {
1271
1261
  extensionId,
1262
+ publisherId,
1272
1263
  clientId,
1273
1264
  clientSecret,
1274
1265
  refreshToken
@@ -1400,7 +1391,7 @@ async function runInit(deps, options = {}) {
1400
1391
  io.log("");
1401
1392
  if ("chrome" === store) {
1402
1393
  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}`, "");
1394
+ envLines.push(`CHROME_EXTENSION_ID=${creds.extensionId}`, `CHROME_PUBLISHER_ID=${creds.publisherId}`, `CHROME_CLIENT_ID=${creds.clientId}`, `CHROME_CLIENT_SECRET=${creds.clientSecret}`, `CHROME_REFRESH_TOKEN=${creds.refreshToken}`, "");
1404
1395
  if (!options.skipVerify) {
1405
1396
  const verify = deps.verifyChrome ?? verifyChromeCredentials;
1406
1397
  verified.chrome = normalizeVerify(await verify(creds));
@@ -1582,16 +1573,21 @@ async function runWatch(flags) {
1582
1573
  const timeoutMs = (flags.watchTimeout ?? 3600) * 1000;
1583
1574
  let terminal;
1584
1575
  try {
1585
- terminal = "chrome" === store ? await watchChromeSubmission({
1586
- clientId: mustEnv("CHROME_CLIENT_ID", flags.chromeClientId),
1587
- clientSecret: mustEnv("CHROME_CLIENT_SECRET", flags.chromeClientSecret),
1588
- refreshToken: mustEnv("CHROME_REFRESH_TOKEN", flags.chromeRefreshToken),
1589
- extensionId: mustEnv("CHROME_EXTENSION_ID", flags.chromeExtensionId)
1590
- }, {
1591
- pollIntervalMs,
1592
- timeoutMs,
1593
- onEvent
1594
- }) : "firefox" === store ? await watchFirefoxSubmission({
1576
+ if ("chrome" === store) {
1577
+ const serviceAccountJson = flags.chromeServiceAccountJson ?? process.env.CHROME_SERVICE_ACCOUNT_JSON;
1578
+ terminal = await watchChromeSubmission({
1579
+ serviceAccountJson,
1580
+ clientId: serviceAccountJson ? flags.chromeClientId : mustEnv("CHROME_CLIENT_ID", flags.chromeClientId),
1581
+ clientSecret: serviceAccountJson ? flags.chromeClientSecret : mustEnv("CHROME_CLIENT_SECRET", flags.chromeClientSecret),
1582
+ refreshToken: serviceAccountJson ? flags.chromeRefreshToken : mustEnv("CHROME_REFRESH_TOKEN", flags.chromeRefreshToken),
1583
+ extensionId: mustEnv("CHROME_EXTENSION_ID", flags.chromeExtensionId),
1584
+ publisherId: mustEnv("CHROME_PUBLISHER_ID", flags.chromePublisherId)
1585
+ }, {
1586
+ pollIntervalMs,
1587
+ timeoutMs,
1588
+ onEvent
1589
+ });
1590
+ } else terminal = "firefox" === store ? await watchFirefoxSubmission({
1595
1591
  jwtIssuer: mustEnv("FIREFOX_JWT_ISSUER", flags.firefoxJwtIssuer),
1596
1592
  jwtSecret: mustEnv("FIREFOX_JWT_SECRET", flags.firefoxJwtSecret),
1597
1593
  extensionId: mustEnv("FIREFOX_EXTENSION_ID", flags.firefoxExtensionId),