@extension.dev/deploy 0.2.5 → 1.0.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 +35 -2
- package/dist/module.js +407 -369
- package/dist/module.mjs +409 -371
- package/dist/src/chrome.d.ts +6 -1
- 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 +3 -2
- 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 +17 -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 +8 -8
- 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 +1 -0
- package/dist/src/watch.d.ts.map +1 -1
- package/package.json +18 -8
- package/dist/__tests__/cli.test.d.ts +0 -2
- package/dist/__tests__/cli.test.d.ts.map +0 -1
- package/dist/__tests__/config.test.d.ts +0 -2
- package/dist/__tests__/config.test.d.ts.map +0 -1
- package/dist/__tests__/deploy.test.d.ts +0 -2
- package/dist/__tests__/deploy.test.d.ts.map +0 -1
- package/dist/__tests__/dry-run.test.d.ts +0 -2
- package/dist/__tests__/dry-run.test.d.ts.map +0 -1
- package/dist/__tests__/init.test.d.ts +0 -2
- package/dist/__tests__/init.test.d.ts.map +0 -1
- package/dist/__tests__/types.test.d.ts +0 -2
- package/dist/__tests__/types.test.d.ts.map +0 -1
- package/dist/__tests__/utils.test.d.ts +0 -2
- package/dist/__tests__/utils.test.d.ts.map +0 -1
- package/dist/__tests__/verify.test.d.ts +0 -2
- package/dist/__tests__/verify.test.d.ts.map +0 -1
- package/dist/__tests__/watch.test.d.ts +0 -2
- package/dist/__tests__/watch.test.d.ts.map +0 -1
package/dist/module.mjs
CHANGED
|
@@ -11,10 +11,7 @@ const chromeOptionsSchema = z.object({
|
|
|
11
11
|
clientId: z.string().min(1).trim(),
|
|
12
12
|
clientSecret: z.string().min(1).trim(),
|
|
13
13
|
refreshToken: z.string().min(1).trim(),
|
|
14
|
-
|
|
15
|
-
"default",
|
|
16
|
-
"trustedTesters"
|
|
17
|
-
]).default("default"),
|
|
14
|
+
publisherId: z.string().min(1).trim(),
|
|
18
15
|
deployPercentage: z.number().int().min(1).max(100).optional(),
|
|
19
16
|
reviewExemption: z.boolean().default(false),
|
|
20
17
|
skipSubmitReview: z.boolean().default(false)
|
|
@@ -64,7 +61,7 @@ function resolveConfig(flags) {
|
|
|
64
61
|
clientId: flags.chromeClientId ?? envStr("CHROME_CLIENT_ID") ?? "",
|
|
65
62
|
clientSecret: flags.chromeClientSecret ?? envStr("CHROME_CLIENT_SECRET") ?? "",
|
|
66
63
|
refreshToken: flags.chromeRefreshToken ?? envStr("CHROME_REFRESH_TOKEN") ?? "",
|
|
67
|
-
|
|
64
|
+
publisherId: flags.chromePublisherId ?? envStr("CHROME_PUBLISHER_ID") ?? "",
|
|
68
65
|
deployPercentage: flags.chromeDeployPercentage ?? envInt("CHROME_DEPLOY_PERCENTAGE"),
|
|
69
66
|
reviewExemption: flags.chromeReviewExemption ?? envBool("CHROME_REVIEW_EXEMPTION"),
|
|
70
67
|
skipSubmitReview: flags.chromeSkipSubmitReview ?? envBool("CHROME_SKIP_SUBMIT_REVIEW")
|
|
@@ -94,6 +91,7 @@ const SETUP_HINTS = {
|
|
|
94
91
|
CHROME_CLIENT_ID: "Create OAuth 2.0 credentials at https://console.cloud.google.com/apis/credentials",
|
|
95
92
|
CHROME_CLIENT_SECRET: "Generated alongside the Client ID in the Google Cloud Console.",
|
|
96
93
|
CHROME_REFRESH_TOKEN: "Obtain by completing the OAuth consent flow. See the Chrome Web Store API docs.",
|
|
94
|
+
CHROME_PUBLISHER_ID: "The publisher UUID from your Chrome Web Store dev console URL (chrome.google.com/webstore/devconsole/<UUID>).",
|
|
97
95
|
CHROME_ZIP: "Path to the .zip file built for Chrome Web Store upload.",
|
|
98
96
|
FIREFOX_EXTENSION_ID: "The addon GUID ({uuid} format) or email-style ID from your addon's AMO page.",
|
|
99
97
|
FIREFOX_JWT_ISSUER: "API key from https://addons.mozilla.org/developers/addon/api/key/",
|
|
@@ -134,16 +132,24 @@ function envInt(name) {
|
|
|
134
132
|
const val = process.env[name];
|
|
135
133
|
return null == val ? void 0 : parseInt(val, 10);
|
|
136
134
|
}
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
136
|
+
function withTimeout(init, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
137
|
+
if (null == init ? void 0 : init.signal) return init;
|
|
138
|
+
return {
|
|
139
|
+
...init || {},
|
|
140
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function fetchJson(url, init, timeoutMs) {
|
|
144
|
+
const res = await fetch(url, withTimeout(init, timeoutMs));
|
|
139
145
|
if (!res.ok) {
|
|
140
146
|
const body = await res.text().catch(()=>"");
|
|
141
147
|
throw new Error(`HTTP ${res.status} ${res.statusText} \u{2014} ${(null == init ? void 0 : init.method) ?? "GET"} ${url}\n${body}`);
|
|
142
148
|
}
|
|
143
149
|
return res.json();
|
|
144
150
|
}
|
|
145
|
-
async function fetchRaw(url, init) {
|
|
146
|
-
const res = await fetch(url, init);
|
|
151
|
+
async function fetchRaw(url, init, timeoutMs) {
|
|
152
|
+
const res = await fetch(url, withTimeout(init, timeoutMs));
|
|
147
153
|
if (!res.ok) {
|
|
148
154
|
const body = await res.text().catch(()=>"");
|
|
149
155
|
throw new Error(`HTTP ${res.status} ${res.statusText} \u{2014} ${(null == init ? void 0 : init.method) ?? "GET"} ${url}\n${body}`);
|
|
@@ -194,10 +200,19 @@ function logDryStep(scope, step, details) {
|
|
|
194
200
|
for (const [key, value] of Object.entries(details))if (null != value) console.log(labelLine(key, value));
|
|
195
201
|
}
|
|
196
202
|
}
|
|
203
|
+
const UPLOAD_TIMEOUT_MS = 120000;
|
|
204
|
+
const PUBLISH_TIMEOUT_MS = 60000;
|
|
197
205
|
const CWS_OAUTH_URL = "https://oauth2.googleapis.com/token";
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
206
|
+
const CWS_V2_BASE = "https://chromewebstore.googleapis.com/v2";
|
|
207
|
+
const CWS_V2_UPLOAD_BASE = "https://chromewebstore.googleapis.com/upload/v2";
|
|
208
|
+
function v2ItemName(publisherId, extensionId) {
|
|
209
|
+
return `publishers/${encodeURIComponent(publisherId)}/items/${encodeURIComponent(extensionId)}`;
|
|
210
|
+
}
|
|
211
|
+
function requirePublisherId(publisherId, op) {
|
|
212
|
+
const id = String(publisherId || "").trim();
|
|
213
|
+
if (!id) throw new Error(`Chrome ${op} needs a publisher ID (the UUID from your Chrome Web Store dev console URL).`);
|
|
214
|
+
return id;
|
|
215
|
+
}
|
|
201
216
|
function chromeStoreUrl(extensionId) {
|
|
202
217
|
return `https://chromewebstore.google.com/detail/${extensionId}`;
|
|
203
218
|
}
|
|
@@ -216,16 +231,17 @@ async function authenticate(clientId, clientSecret, refreshToken) {
|
|
|
216
231
|
}
|
|
217
232
|
});
|
|
218
233
|
}
|
|
219
|
-
async function chrome_upload(extensionId, zipPath, token) {
|
|
234
|
+
async function chrome_upload(extensionId, publisherId, zipPath, token) {
|
|
220
235
|
const body = await promises_default.readFile(zipPath);
|
|
221
|
-
const
|
|
222
|
-
|
|
236
|
+
const url = `${CWS_V2_UPLOAD_BASE}/${v2ItemName(publisherId, extensionId)}:upload`;
|
|
237
|
+
const res = await fetch(url, {
|
|
238
|
+
method: "POST",
|
|
223
239
|
body,
|
|
224
240
|
headers: {
|
|
225
241
|
Authorization: `${token.token_type} ${token.access_token}`,
|
|
226
|
-
"x-goog-api-version": "2",
|
|
227
242
|
"Content-Type": "application/zip"
|
|
228
|
-
}
|
|
243
|
+
},
|
|
244
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
|
|
229
245
|
});
|
|
230
246
|
if (!res.ok) {
|
|
231
247
|
const text = await res.text().catch(()=>"");
|
|
@@ -233,17 +249,23 @@ async function chrome_upload(extensionId, zipPath, token) {
|
|
|
233
249
|
}
|
|
234
250
|
}
|
|
235
251
|
async function publish(params) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (null != params.
|
|
240
|
-
|
|
252
|
+
const reqBody = {
|
|
253
|
+
publishType: "DEFAULT_PUBLISH"
|
|
254
|
+
};
|
|
255
|
+
if (null != params.deployPercentage) reqBody.deployInfos = [
|
|
256
|
+
{
|
|
257
|
+
deployPercentage: params.deployPercentage
|
|
258
|
+
}
|
|
259
|
+
];
|
|
260
|
+
if (params.reviewExemption) reqBody.skipReview = true;
|
|
261
|
+
const res = await fetch(`${CWS_V2_BASE}/${v2ItemName(params.publisherId, params.extensionId)}:publish`, {
|
|
241
262
|
method: "POST",
|
|
242
263
|
headers: {
|
|
243
264
|
Authorization: `${params.token.token_type} ${params.token.access_token}`,
|
|
244
|
-
"
|
|
245
|
-
|
|
246
|
-
|
|
265
|
+
"Content-Type": "application/json"
|
|
266
|
+
},
|
|
267
|
+
body: JSON.stringify(reqBody),
|
|
268
|
+
signal: AbortSignal.timeout(PUBLISH_TIMEOUT_MS)
|
|
247
269
|
});
|
|
248
270
|
if (!res.ok) {
|
|
249
271
|
const text = await res.text().catch(()=>"");
|
|
@@ -253,7 +275,8 @@ async function publish(params) {
|
|
|
253
275
|
async function publishChrome(options, dryRun) {
|
|
254
276
|
await assertFilePresent(options.zip);
|
|
255
277
|
const zipSize = await getFileSize(options.zip);
|
|
256
|
-
|
|
278
|
+
const publisherId = requirePublisherId(options.publisherId, "submit");
|
|
279
|
+
log("chrome", "Authenticating with Chrome Web Store (API v2)");
|
|
257
280
|
const token = await authenticate(options.clientId, options.clientSecret, options.refreshToken);
|
|
258
281
|
const outcome = {
|
|
259
282
|
storeUrl: chromeStoreUrl(options.extensionId),
|
|
@@ -266,21 +289,17 @@ async function publishChrome(options, dryRun) {
|
|
|
266
289
|
});
|
|
267
290
|
logDryStep("chrome", "Would upload ZIP", {
|
|
268
291
|
file: options.zip,
|
|
269
|
-
size: formatBytes(zipSize)
|
|
270
|
-
target: `${CWS_UPLOAD_BASE}/${options.extensionId}`,
|
|
271
|
-
method: "PUT"
|
|
292
|
+
size: formatBytes(zipSize)
|
|
272
293
|
});
|
|
273
294
|
options.skipSubmitReview ? logDryStep("chrome", "Would skip publish (skipSubmitReview=true)") : logDryStep("chrome", "Would publish for review", {
|
|
274
|
-
target: `${CWS_PUBLISH_BASE}/${options.extensionId}/publish`,
|
|
275
|
-
publishTarget: options.publishTarget ?? "default",
|
|
276
295
|
deployPercentage: options.deployPercentage,
|
|
277
296
|
reviewExemption: options.reviewExemption
|
|
278
297
|
});
|
|
279
|
-
log("chrome", "Dry run complete
|
|
298
|
+
log("chrome", "Dry run complete - no changes made");
|
|
280
299
|
return outcome;
|
|
281
300
|
}
|
|
282
301
|
log("chrome", `Uploading ZIP file (${formatBytes(zipSize)})`);
|
|
283
|
-
await chrome_upload(options.extensionId, options.zip, token);
|
|
302
|
+
await chrome_upload(options.extensionId, publisherId, options.zip, token);
|
|
284
303
|
if (options.skipSubmitReview) {
|
|
285
304
|
log("chrome", "Skipping publish step (skipSubmitReview=true)");
|
|
286
305
|
return outcome;
|
|
@@ -288,7 +307,7 @@ async function publishChrome(options, dryRun) {
|
|
|
288
307
|
log("chrome", "Publishing extension");
|
|
289
308
|
await publish({
|
|
290
309
|
extensionId: options.extensionId,
|
|
291
|
-
|
|
310
|
+
publisherId,
|
|
292
311
|
token,
|
|
293
312
|
deployPercentage: options.deployPercentage,
|
|
294
313
|
reviewExemption: options.reviewExemption
|
|
@@ -296,14 +315,22 @@ async function publishChrome(options, dryRun) {
|
|
|
296
315
|
return outcome;
|
|
297
316
|
}
|
|
298
317
|
async function getChromeItem(params) {
|
|
318
|
+
var _rev_distributionChannels_, _rev_distributionChannels;
|
|
299
319
|
const token = await authenticate(params.clientId, params.clientSecret, params.refreshToken);
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
320
|
+
const auth = {
|
|
321
|
+
Authorization: `${token.token_type} ${token.access_token}`
|
|
322
|
+
};
|
|
323
|
+
const pub = requirePublisherId(params.publisherId, "status");
|
|
324
|
+
const status = await fetchJson(`${CWS_V2_BASE}/${v2ItemName(pub, params.extensionId)}:fetchStatus`, {
|
|
325
|
+
headers: auth
|
|
306
326
|
});
|
|
327
|
+
const rev = status.submittedItemRevisionStatus || status.publishedItemRevisionStatus;
|
|
328
|
+
return {
|
|
329
|
+
uploadState: status.lastAsyncUploadState,
|
|
330
|
+
crxVersion: null == rev ? void 0 : null == (_rev_distributionChannels = rev.distributionChannels) ? void 0 : null == (_rev_distributionChannels_ = _rev_distributionChannels[0]) ? void 0 : _rev_distributionChannels_.crxVersion,
|
|
331
|
+
publicKey: status.publicKey,
|
|
332
|
+
state: null == rev ? void 0 : rev.state
|
|
333
|
+
};
|
|
307
334
|
}
|
|
308
335
|
async function verifyChromeCredentials(params) {
|
|
309
336
|
let token;
|
|
@@ -315,17 +342,18 @@ async function verifyChromeCredentials(params) {
|
|
|
315
342
|
message: `OAuth token exchange failed. Verify your Client ID, Client Secret, and Refresh Token are correct.\n${err instanceof Error ? err.message : err}`
|
|
316
343
|
};
|
|
317
344
|
}
|
|
345
|
+
const auth = {
|
|
346
|
+
Authorization: `${token.token_type} ${token.access_token}`
|
|
347
|
+
};
|
|
318
348
|
try {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
349
|
+
const pub = requirePublisherId(params.publisherId, "verify");
|
|
350
|
+
const status = await fetchJson(`${CWS_V2_BASE}/${v2ItemName(pub, params.extensionId)}:fetchStatus`, {
|
|
351
|
+
headers: auth
|
|
323
352
|
});
|
|
324
|
-
const
|
|
353
|
+
const rev = status.submittedItemRevisionStatus || status.publishedItemRevisionStatus;
|
|
325
354
|
return {
|
|
326
355
|
ok: true,
|
|
327
|
-
message: `Credentials verified.
|
|
328
|
-
extensionTitle: title
|
|
356
|
+
message: `Credentials verified (Chrome Web Store API v2). Item state: ${(null == rev ? void 0 : rev.state) || "unknown"}.`
|
|
329
357
|
};
|
|
330
358
|
} catch (err) {
|
|
331
359
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -335,7 +363,7 @@ async function verifyChromeCredentials(params) {
|
|
|
335
363
|
};
|
|
336
364
|
if (msg.includes("403")) return {
|
|
337
365
|
ok: false,
|
|
338
|
-
message:
|
|
366
|
+
message: "Access denied. Check the publisher ID matches this extension's account, and that the extension ID is correct."
|
|
339
367
|
};
|
|
340
368
|
return {
|
|
341
369
|
ok: false,
|
|
@@ -349,12 +377,6 @@ function delay(ms) {
|
|
|
349
377
|
});
|
|
350
378
|
}
|
|
351
379
|
const AMO_BASE = "https://addons.mozilla.org/api/v5/addons";
|
|
352
|
-
const AMO_PROFILE_URL = "https://addons.mozilla.org/api/v5/accounts/profile/";
|
|
353
|
-
const POLL_INTERVAL_MS = 5000;
|
|
354
|
-
const VALIDATION_TIMEOUT_MS = 600000;
|
|
355
|
-
function firefoxStoreUrl(addonIdOrSlug) {
|
|
356
|
-
return `https://addons.mozilla.org/firefox/addon/${addonIdOrSlug}`;
|
|
357
|
-
}
|
|
358
380
|
function signJwt(issuer, secret, expiresInSeconds = 30) {
|
|
359
381
|
const header = {
|
|
360
382
|
alg: "HS256",
|
|
@@ -439,6 +461,12 @@ async function createAddon(params) {
|
|
|
439
461
|
headers: authHeaders(params.jwtIssuer, params.jwtSecret)
|
|
440
462
|
});
|
|
441
463
|
}
|
|
464
|
+
const AMO_PROFILE_URL = "https://addons.mozilla.org/api/v5/accounts/profile/";
|
|
465
|
+
const POLL_INTERVAL_MS = 5000;
|
|
466
|
+
const VALIDATION_TIMEOUT_MS = 600000;
|
|
467
|
+
function firefoxStoreUrl(addonIdOrSlug) {
|
|
468
|
+
return `https://addons.mozilla.org/firefox/addon/${addonIdOrSlug}`;
|
|
469
|
+
}
|
|
442
470
|
async function publishFirefox(options, dryRun) {
|
|
443
471
|
await assertFilePresent(options.zip);
|
|
444
472
|
const zipSize = await getFileSize(options.zip);
|
|
@@ -804,8 +832,134 @@ async function deploy(config) {
|
|
|
804
832
|
success
|
|
805
833
|
};
|
|
806
834
|
}
|
|
835
|
+
function usage() {
|
|
836
|
+
return `
|
|
837
|
+
extension-deploy \u{2014} Deploy browser extensions to Chrome, Firefox, and Edge.
|
|
838
|
+
|
|
839
|
+
USAGE
|
|
840
|
+
extension-deploy [options] submit to one or more stores
|
|
841
|
+
extension-deploy init [options] interactive credential setup wizard
|
|
842
|
+
extension-deploy watch --chrome [options] poll an in-flight submission
|
|
843
|
+
extension-deploy watch --firefox [options]
|
|
844
|
+
extension-deploy watch --edge [options]
|
|
845
|
+
|
|
846
|
+
GLOBAL
|
|
847
|
+
--dry-run Verify auth without uploading or publishing
|
|
848
|
+
--output-json <path> Write the DeployResult / WatchEvent as JSON to <path>
|
|
849
|
+
--help Show this help message
|
|
850
|
+
|
|
851
|
+
CHROME
|
|
852
|
+
--chrome-zip <path> Path to Chrome extension ZIP
|
|
853
|
+
--chrome-extension-id <id> Chrome extension ID
|
|
854
|
+
--chrome-publisher-id <uuid> Publisher UUID from the dev console (required)
|
|
855
|
+
--chrome-client-id <id> OAuth2 client ID
|
|
856
|
+
--chrome-client-secret <secret> OAuth2 client secret
|
|
857
|
+
--chrome-refresh-token <token> OAuth2 refresh token
|
|
858
|
+
--chrome-deploy-percentage <n> Staged rollout percentage (1-100)
|
|
859
|
+
--chrome-review-exemption Request expedited review
|
|
860
|
+
--chrome-skip-submit-review Upload only, skip publish
|
|
861
|
+
|
|
862
|
+
FIREFOX
|
|
863
|
+
--firefox-zip <path> Path to Firefox extension ZIP
|
|
864
|
+
--firefox-sources-zip <path> Path to sources ZIP (optional)
|
|
865
|
+
--firefox-extension-id <id> Addon GUID or email-style ID
|
|
866
|
+
--firefox-jwt-issuer <issuer> AMO JWT issuer
|
|
867
|
+
--firefox-jwt-secret <secret> AMO JWT secret
|
|
868
|
+
--firefox-channel <channel> "listed" or "unlisted"
|
|
869
|
+
|
|
870
|
+
EDGE
|
|
871
|
+
--edge-zip <path> Path to Edge extension ZIP
|
|
872
|
+
--edge-product-id <id> Partner Center product ID
|
|
873
|
+
--edge-client-id <id> Partner Center client ID
|
|
874
|
+
--edge-api-key <key> Partner Center API key (v1.1)
|
|
875
|
+
--edge-skip-submit-review Upload only, skip publish
|
|
876
|
+
|
|
877
|
+
WATCH
|
|
878
|
+
--watch-submission-id <id> Required for firefox/edge (version id / operation id)
|
|
879
|
+
--watch-interval <seconds> Poll interval (default 60)
|
|
880
|
+
--watch-timeout <seconds> Max time to wait for a terminal state (default 3600)
|
|
881
|
+
|
|
882
|
+
INIT
|
|
883
|
+
--init-stores <list> Comma-separated store list (skip picker)
|
|
884
|
+
--init-output <path> Destination file (default .env.submit)
|
|
885
|
+
--init-skip-verify Don't run verifyCredentials after collection
|
|
886
|
+
--init-force Overwrite existing .env.submit without asking
|
|
887
|
+
|
|
888
|
+
ENVIRONMENT VARIABLES
|
|
889
|
+
All flags can be set via SCREAMING_SNAKE_CASE env vars (e.g. CHROME_ZIP,
|
|
890
|
+
FIREFOX_JWT_SECRET, EDGE_API_KEY). Flags take precedence over env vars.
|
|
891
|
+
A .env.submit file in the current directory is loaded automatically.
|
|
892
|
+
`.trim();
|
|
893
|
+
}
|
|
894
|
+
const BOOLEAN_FLAGS = new Set([
|
|
895
|
+
"help",
|
|
896
|
+
"dryRun",
|
|
897
|
+
"chromeReviewExemption",
|
|
898
|
+
"chromeSkipSubmitReview",
|
|
899
|
+
"edgeSkipSubmitReview",
|
|
900
|
+
"initSkipVerify",
|
|
901
|
+
"initForce"
|
|
902
|
+
]);
|
|
903
|
+
const INT_FLAGS = new Set([
|
|
904
|
+
"chromeDeployPercentage",
|
|
905
|
+
"watchInterval",
|
|
906
|
+
"watchTimeout"
|
|
907
|
+
]);
|
|
908
|
+
function parseArgs(argv) {
|
|
909
|
+
const flags = {};
|
|
910
|
+
let i = 0;
|
|
911
|
+
if ("watch" === argv[0]) {
|
|
912
|
+
flags.command = "watch";
|
|
913
|
+
i = 1;
|
|
914
|
+
} else if ("init" === argv[0]) {
|
|
915
|
+
flags.command = "init";
|
|
916
|
+
i = 1;
|
|
917
|
+
}
|
|
918
|
+
while(i < argv.length){
|
|
919
|
+
const arg = argv[i];
|
|
920
|
+
if ("--help" === arg || "-h" === arg) {
|
|
921
|
+
flags.help = true;
|
|
922
|
+
i++;
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if ("--chrome" === arg) {
|
|
926
|
+
flags.watchStore = "chrome";
|
|
927
|
+
i++;
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if ("--firefox" === arg) {
|
|
931
|
+
flags.watchStore = "firefox";
|
|
932
|
+
i++;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
if ("--edge" === arg) {
|
|
936
|
+
flags.watchStore = "edge";
|
|
937
|
+
i++;
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (!(null == arg ? void 0 : arg.startsWith("--"))) {
|
|
941
|
+
i++;
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const key = arg.slice(2).replace(/-([a-z])/g, (_, c)=>c.toUpperCase());
|
|
945
|
+
if (BOOLEAN_FLAGS.has(key)) {
|
|
946
|
+
flags[key] = true;
|
|
947
|
+
i++;
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
const next = argv[i + 1];
|
|
951
|
+
if (null == next || next.startsWith("--")) {
|
|
952
|
+
flags[key] = true;
|
|
953
|
+
i++;
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
flags[key] = INT_FLAGS.has(key) ? parseInt(next, 10) : next;
|
|
957
|
+
i += 2;
|
|
958
|
+
}
|
|
959
|
+
return flags;
|
|
960
|
+
}
|
|
807
961
|
const DEFAULT_POLL_INTERVAL_MS = 60000;
|
|
808
|
-
const
|
|
962
|
+
const watch_DEFAULT_TIMEOUT_MS = 3600000;
|
|
809
963
|
function watch_now() {
|
|
810
964
|
return new Date().toISOString();
|
|
811
965
|
}
|
|
@@ -932,7 +1086,7 @@ function watchFirefoxSubmission(input, options = {}) {
|
|
|
932
1086
|
}
|
|
933
1087
|
async function runWatchLoop(_store, probe, options) {
|
|
934
1088
|
const pollInterval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
935
|
-
const timeout = options.timeoutMs ??
|
|
1089
|
+
const timeout = options.timeoutMs ?? watch_DEFAULT_TIMEOUT_MS;
|
|
936
1090
|
const deadline = Date.now() + timeout;
|
|
937
1091
|
let lastEvent;
|
|
938
1092
|
while(true){
|
|
@@ -944,47 +1098,174 @@ async function runWatchLoop(_store, probe, options) {
|
|
|
944
1098
|
await delay(pollInterval);
|
|
945
1099
|
}
|
|
946
1100
|
}
|
|
947
|
-
|
|
948
|
-
{
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
value: "firefox",
|
|
954
|
-
label: "Firefox AMO (addons.mozilla.org)"
|
|
955
|
-
},
|
|
956
|
-
{
|
|
957
|
-
value: "edge",
|
|
958
|
-
label: "Edge Add-ons (Partner Center)"
|
|
1101
|
+
async function requireValue(io, label, hint, opts) {
|
|
1102
|
+
const question = hint ? `${label} ${hint}:` : `${label}:`;
|
|
1103
|
+
for(let attempt = 0; attempt < 3; attempt++){
|
|
1104
|
+
const answer = (await io.prompt(question, opts)).trim();
|
|
1105
|
+
if (answer) return answer;
|
|
1106
|
+
io.log(` ${label} is required.`);
|
|
959
1107
|
}
|
|
960
|
-
|
|
1108
|
+
throw new Error(`init: ${label} is required.`);
|
|
1109
|
+
}
|
|
961
1110
|
const CHROME_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
962
1111
|
const CHROME_OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
|
|
963
1112
|
const CHROME_OOB_REDIRECT = "urn:ietf:wg:oauth:2.0:oob";
|
|
964
1113
|
const CHROME_SCOPE = "https://www.googleapis.com/auth/chromewebstore";
|
|
965
|
-
async function
|
|
1114
|
+
async function collectChromeCredentials(deps) {
|
|
966
1115
|
const { io } = deps;
|
|
1116
|
+
io.log("Chrome Web Store needs OAuth credentials. You'll need:");
|
|
1117
|
+
io.log(" 1. A Google Cloud project with the Chrome Web Store API enabled:");
|
|
1118
|
+
io.log(" https://console.cloud.google.com/apis/library/chromewebstore.googleapis.com");
|
|
1119
|
+
io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
|
|
1120
|
+
io.log(" https://console.cloud.google.com/apis/credentials");
|
|
1121
|
+
io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
|
|
1122
|
+
io.log(" 4. Your publisher ID (the UUID in your dev console URL: chrome.google.com/webstore/devconsole/<UUID>).");
|
|
967
1123
|
io.log("");
|
|
968
|
-
io.log(
|
|
1124
|
+
io.log("This wizard will take your Client ID + Secret and walk you through");
|
|
1125
|
+
io.log("generating a refresh token automatically.");
|
|
969
1126
|
io.log("");
|
|
970
|
-
io
|
|
971
|
-
|
|
1127
|
+
const extensionId = await requireValue(io, "Chrome extension ID", "(32-character ID from the CWS dashboard URL)");
|
|
1128
|
+
const publisherId = await requireValue(io, "Chrome publisher ID", "(UUID from your dev console URL)");
|
|
1129
|
+
const clientId = await requireValue(io, "OAuth Client ID");
|
|
1130
|
+
const clientSecret = await requireValue(io, "OAuth Client Secret", void 0, {
|
|
1131
|
+
secret: true
|
|
1132
|
+
});
|
|
1133
|
+
const refreshToken = await obtainChromeRefreshToken(deps, clientId, clientSecret);
|
|
1134
|
+
return {
|
|
1135
|
+
extensionId,
|
|
1136
|
+
publisherId,
|
|
1137
|
+
clientId,
|
|
1138
|
+
clientSecret,
|
|
1139
|
+
refreshToken
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function buildChromeAuthUrl(clientId) {
|
|
1143
|
+
const params = new URLSearchParams({
|
|
1144
|
+
response_type: "code",
|
|
1145
|
+
client_id: clientId,
|
|
1146
|
+
redirect_uri: CHROME_OOB_REDIRECT,
|
|
1147
|
+
scope: CHROME_SCOPE,
|
|
1148
|
+
access_type: "offline",
|
|
1149
|
+
prompt: "consent"
|
|
1150
|
+
});
|
|
1151
|
+
return `${CHROME_OAUTH_AUTH_URL}?${params.toString()}`;
|
|
1152
|
+
}
|
|
1153
|
+
async function exchangeChromeAuthCode(input) {
|
|
1154
|
+
const fetchFn = input.fetchImpl ?? fetch;
|
|
1155
|
+
const body = new URLSearchParams({
|
|
1156
|
+
code: input.authCode,
|
|
1157
|
+
client_id: input.clientId,
|
|
1158
|
+
client_secret: input.clientSecret,
|
|
1159
|
+
redirect_uri: CHROME_OOB_REDIRECT,
|
|
1160
|
+
grant_type: "authorization_code"
|
|
1161
|
+
});
|
|
1162
|
+
const res = await fetchFn(CHROME_OAUTH_TOKEN_URL, {
|
|
1163
|
+
method: "POST",
|
|
1164
|
+
headers: {
|
|
1165
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1166
|
+
},
|
|
1167
|
+
body: body.toString()
|
|
1168
|
+
});
|
|
1169
|
+
if (!res.ok) {
|
|
1170
|
+
const text = await res.text().catch(()=>"");
|
|
1171
|
+
throw new Error(`Chrome token exchange failed (HTTP ${res.status}): ${text || res.statusText}`);
|
|
1172
|
+
}
|
|
1173
|
+
const json = await res.json();
|
|
1174
|
+
if (!json.refresh_token) throw new Error(`Chrome token exchange returned no refresh_token${json.error ? `: ${json.error} \u{2014} ${json.error_description ?? ""}` : ""}. This usually means the authorization code was already used or expired; try the init wizard again.`);
|
|
1175
|
+
return json.refresh_token;
|
|
1176
|
+
}
|
|
1177
|
+
async function obtainChromeRefreshToken(deps, clientId, clientSecret) {
|
|
1178
|
+
const { io } = deps;
|
|
1179
|
+
const authUrl = buildChromeAuthUrl(clientId);
|
|
972
1180
|
io.log("");
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1181
|
+
io.log("Open the URL below in your browser, approve the scope, and paste the");
|
|
1182
|
+
io.log("authorization code shown on the page:");
|
|
1183
|
+
io.log("");
|
|
1184
|
+
io.log(` ${authUrl}`);
|
|
1185
|
+
io.log("");
|
|
1186
|
+
const authCode = await requireValue(io, "Authorization code", void 0, {
|
|
1187
|
+
secret: true
|
|
1188
|
+
});
|
|
1189
|
+
return exchangeChromeAuthCode({
|
|
1190
|
+
clientId,
|
|
1191
|
+
clientSecret,
|
|
1192
|
+
authCode,
|
|
1193
|
+
fetchImpl: deps.fetchImpl
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
async function collectFirefoxCredentials(deps) {
|
|
1197
|
+
const { io } = deps;
|
|
1198
|
+
io.log("Firefox AMO needs JWT API credentials:");
|
|
1199
|
+
io.log(" 1. Sign in at https://addons.mozilla.org");
|
|
1200
|
+
io.log(" 2. Create API credentials at https://addons.mozilla.org/developers/addon/api/key/");
|
|
1201
|
+
io.log(" 3. Copy the JWT issuer and JWT secret (the secret is only shown once).");
|
|
1202
|
+
io.log("");
|
|
1203
|
+
const addonId = await requireValue(io, "Firefox addon ID", "({uuid} GUID or email-style ID from your addon page)");
|
|
1204
|
+
const jwtIssuer = await requireValue(io, "JWT issuer");
|
|
1205
|
+
const jwtSecret = await requireValue(io, "JWT secret", void 0, {
|
|
1206
|
+
secret: true
|
|
1207
|
+
});
|
|
1208
|
+
return {
|
|
1209
|
+
addonId,
|
|
1210
|
+
jwtIssuer,
|
|
1211
|
+
jwtSecret
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
async function collectEdgeCredentials(deps) {
|
|
1215
|
+
const { io } = deps;
|
|
1216
|
+
io.log("Edge Partner Center needs the v1.1 API credentials:");
|
|
1217
|
+
io.log(" 1. Open the Partner Center dashboard: https://partner.microsoft.com/dashboard/microsoftedge/overview");
|
|
1218
|
+
io.log(" 2. Under your extension, go to Settings \u2192 API access \u2192 API v1.1 and generate credentials.");
|
|
1219
|
+
io.log(" 3. Copy the Client ID, API key, and Product ID.");
|
|
1220
|
+
io.log("");
|
|
1221
|
+
const productId = await requireValue(io, "Edge product ID");
|
|
1222
|
+
const clientId = await requireValue(io, "Edge client ID");
|
|
1223
|
+
const apiKey = await requireValue(io, "Edge API key", void 0, {
|
|
1224
|
+
secret: true
|
|
1225
|
+
});
|
|
1226
|
+
return {
|
|
1227
|
+
productId,
|
|
1228
|
+
clientId,
|
|
1229
|
+
apiKey
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
const STORE_CHOICES = [
|
|
1233
|
+
{
|
|
1234
|
+
value: "chrome",
|
|
1235
|
+
label: "Chrome Web Store"
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
value: "firefox",
|
|
1239
|
+
label: "Firefox AMO (addons.mozilla.org)"
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
value: "edge",
|
|
1243
|
+
label: "Edge Add-ons (Partner Center)"
|
|
1244
|
+
}
|
|
1245
|
+
];
|
|
1246
|
+
async function runInit(deps, options = {}) {
|
|
1247
|
+
const { io } = deps;
|
|
1248
|
+
io.log("");
|
|
1249
|
+
io.log(`${colors.green("\u23F5\u23F5\u23F5")} ${colors.blue("init")} ${colors.underline("extension-deploy \u2014 set up store credentials")}`);
|
|
1250
|
+
io.log("");
|
|
1251
|
+
io.log("This wizard walks you through creating credentials for each store and");
|
|
1252
|
+
io.log("writes them to a .env.submit file. Press Ctrl+C at any time to abort.");
|
|
1253
|
+
io.log("");
|
|
1254
|
+
const stores = options.stores ? options.stores : await io.multiselect("Which stores do you want to configure?", STORE_CHOICES);
|
|
1255
|
+
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.");
|
|
1256
|
+
const envLines = [
|
|
1257
|
+
"# Generated by `extension-deploy init`.",
|
|
1258
|
+
"# Do not commit secrets to source control.",
|
|
1259
|
+
""
|
|
1260
|
+
];
|
|
1261
|
+
const verified = {};
|
|
1262
|
+
for (const store of stores){
|
|
1263
|
+
io.log("");
|
|
983
1264
|
io.log(colors.underline(colors.blue(labelFor(store))));
|
|
984
1265
|
io.log("");
|
|
985
1266
|
if ("chrome" === store) {
|
|
986
1267
|
const creds = await collectChromeCredentials(deps);
|
|
987
|
-
envLines.push(`CHROME_EXTENSION_ID=${creds.extensionId}`, `CHROME_CLIENT_ID=${creds.clientId}`, `CHROME_CLIENT_SECRET=${creds.clientSecret}`, `CHROME_REFRESH_TOKEN=${creds.refreshToken}`, "");
|
|
1268
|
+
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}`, "");
|
|
988
1269
|
if (!options.skipVerify) {
|
|
989
1270
|
const verify = deps.verifyChrome ?? verifyChromeCredentials;
|
|
990
1271
|
verified.chrome = normalizeVerify(await verify(creds));
|
|
@@ -1052,121 +1333,6 @@ async function runInit(deps, options = {}) {
|
|
|
1052
1333
|
wrote
|
|
1053
1334
|
};
|
|
1054
1335
|
}
|
|
1055
|
-
async function collectChromeCredentials(deps) {
|
|
1056
|
-
const { io } = deps;
|
|
1057
|
-
io.log("Chrome Web Store needs OAuth credentials. You'll need:");
|
|
1058
|
-
io.log(" 1. A Google Cloud project with the Chrome Web Store API enabled:");
|
|
1059
|
-
io.log(" https://console.cloud.google.com/apis/library/chromewebstore.googleapis.com");
|
|
1060
|
-
io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
|
|
1061
|
-
io.log(" https://console.cloud.google.com/apis/credentials");
|
|
1062
|
-
io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
|
|
1063
|
-
io.log("");
|
|
1064
|
-
io.log("This wizard will take your Client ID + Secret and walk you through");
|
|
1065
|
-
io.log("generating a refresh token automatically.");
|
|
1066
|
-
io.log("");
|
|
1067
|
-
const extensionId = await requireValue(io, "Chrome extension ID", "(32-character ID from the CWS dashboard URL)");
|
|
1068
|
-
const clientId = await requireValue(io, "OAuth Client ID");
|
|
1069
|
-
const clientSecret = await requireValue(io, "OAuth Client Secret", void 0, {
|
|
1070
|
-
secret: true
|
|
1071
|
-
});
|
|
1072
|
-
const refreshToken = await obtainChromeRefreshToken(deps, clientId, clientSecret);
|
|
1073
|
-
return {
|
|
1074
|
-
extensionId,
|
|
1075
|
-
clientId,
|
|
1076
|
-
clientSecret,
|
|
1077
|
-
refreshToken
|
|
1078
|
-
};
|
|
1079
|
-
}
|
|
1080
|
-
function buildChromeAuthUrl(clientId) {
|
|
1081
|
-
const params = new URLSearchParams({
|
|
1082
|
-
response_type: "code",
|
|
1083
|
-
client_id: clientId,
|
|
1084
|
-
redirect_uri: CHROME_OOB_REDIRECT,
|
|
1085
|
-
scope: CHROME_SCOPE,
|
|
1086
|
-
access_type: "offline",
|
|
1087
|
-
prompt: "consent"
|
|
1088
|
-
});
|
|
1089
|
-
return `${CHROME_OAUTH_AUTH_URL}?${params.toString()}`;
|
|
1090
|
-
}
|
|
1091
|
-
async function exchangeChromeAuthCode(input) {
|
|
1092
|
-
const fetchFn = input.fetchImpl ?? fetch;
|
|
1093
|
-
const body = new URLSearchParams({
|
|
1094
|
-
code: input.authCode,
|
|
1095
|
-
client_id: input.clientId,
|
|
1096
|
-
client_secret: input.clientSecret,
|
|
1097
|
-
redirect_uri: CHROME_OOB_REDIRECT,
|
|
1098
|
-
grant_type: "authorization_code"
|
|
1099
|
-
});
|
|
1100
|
-
const res = await fetchFn(CHROME_OAUTH_TOKEN_URL, {
|
|
1101
|
-
method: "POST",
|
|
1102
|
-
headers: {
|
|
1103
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
1104
|
-
},
|
|
1105
|
-
body: body.toString()
|
|
1106
|
-
});
|
|
1107
|
-
if (!res.ok) {
|
|
1108
|
-
const text = await res.text().catch(()=>"");
|
|
1109
|
-
throw new Error(`Chrome token exchange failed (HTTP ${res.status}): ${text || res.statusText}`);
|
|
1110
|
-
}
|
|
1111
|
-
const json = await res.json();
|
|
1112
|
-
if (!json.refresh_token) throw new Error(`Chrome token exchange returned no refresh_token${json.error ? `: ${json.error} \u{2014} ${json.error_description ?? ""}` : ""}. This usually means the authorization code was already used or expired; try the init wizard again.`);
|
|
1113
|
-
return json.refresh_token;
|
|
1114
|
-
}
|
|
1115
|
-
async function obtainChromeRefreshToken(deps, clientId, clientSecret) {
|
|
1116
|
-
const { io } = deps;
|
|
1117
|
-
const authUrl = buildChromeAuthUrl(clientId);
|
|
1118
|
-
io.log("");
|
|
1119
|
-
io.log("Open the URL below in your browser, approve the scope, and paste the");
|
|
1120
|
-
io.log("authorization code shown on the page:");
|
|
1121
|
-
io.log("");
|
|
1122
|
-
io.log(` ${authUrl}`);
|
|
1123
|
-
io.log("");
|
|
1124
|
-
const authCode = await requireValue(io, "Authorization code", void 0, {
|
|
1125
|
-
secret: true
|
|
1126
|
-
});
|
|
1127
|
-
return exchangeChromeAuthCode({
|
|
1128
|
-
clientId,
|
|
1129
|
-
clientSecret,
|
|
1130
|
-
authCode,
|
|
1131
|
-
fetchImpl: deps.fetchImpl
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
async function collectFirefoxCredentials(deps) {
|
|
1135
|
-
const { io } = deps;
|
|
1136
|
-
io.log("Firefox AMO needs JWT API credentials:");
|
|
1137
|
-
io.log(" 1. Sign in at https://addons.mozilla.org");
|
|
1138
|
-
io.log(" 2. Create API credentials at https://addons.mozilla.org/developers/addon/api/key/");
|
|
1139
|
-
io.log(" 3. Copy the JWT issuer and JWT secret (the secret is only shown once).");
|
|
1140
|
-
io.log("");
|
|
1141
|
-
const addonId = await requireValue(io, "Firefox addon ID", "({uuid} GUID or email-style ID from your addon page)");
|
|
1142
|
-
const jwtIssuer = await requireValue(io, "JWT issuer");
|
|
1143
|
-
const jwtSecret = await requireValue(io, "JWT secret", void 0, {
|
|
1144
|
-
secret: true
|
|
1145
|
-
});
|
|
1146
|
-
return {
|
|
1147
|
-
addonId,
|
|
1148
|
-
jwtIssuer,
|
|
1149
|
-
jwtSecret
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
async function collectEdgeCredentials(deps) {
|
|
1153
|
-
const { io } = deps;
|
|
1154
|
-
io.log("Edge Partner Center needs the v1.1 API credentials:");
|
|
1155
|
-
io.log(" 1. Open the Partner Center dashboard: https://partner.microsoft.com/dashboard/microsoftedge/overview");
|
|
1156
|
-
io.log(" 2. Under your extension, go to Settings \u2192 API access \u2192 API v1.1 and generate credentials.");
|
|
1157
|
-
io.log(" 3. Copy the Client ID, API key, and Product ID.");
|
|
1158
|
-
io.log("");
|
|
1159
|
-
const productId = await requireValue(io, "Edge product ID");
|
|
1160
|
-
const clientId = await requireValue(io, "Edge client ID");
|
|
1161
|
-
const apiKey = await requireValue(io, "Edge API key", void 0, {
|
|
1162
|
-
secret: true
|
|
1163
|
-
});
|
|
1164
|
-
return {
|
|
1165
|
-
productId,
|
|
1166
|
-
clientId,
|
|
1167
|
-
apiKey
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
1336
|
function labelFor(store) {
|
|
1171
1337
|
var _STORE_CHOICES_find;
|
|
1172
1338
|
return (null == (_STORE_CHOICES_find = STORE_CHOICES.find((c)=>c.value === store)) ? void 0 : _STORE_CHOICES_find.label) ?? String(store);
|
|
@@ -1185,15 +1351,6 @@ function resolveEnvPath(cwd, outputPath) {
|
|
|
1185
1351
|
const target = outputPath ?? ".env.submit";
|
|
1186
1352
|
return external_node_path_default.isAbsolute(target) ? target : external_node_path_default.join(cwd, target);
|
|
1187
1353
|
}
|
|
1188
|
-
async function requireValue(io, label, hint, opts) {
|
|
1189
|
-
const question = hint ? `${label} ${hint}:` : `${label}:`;
|
|
1190
|
-
for(let attempt = 0; attempt < 3; attempt++){
|
|
1191
|
-
const answer = (await io.prompt(question, opts)).trim();
|
|
1192
|
-
if (answer) return answer;
|
|
1193
|
-
io.log(` ${label} is required.`);
|
|
1194
|
-
}
|
|
1195
|
-
throw new Error(`init: ${label} is required.`);
|
|
1196
|
-
}
|
|
1197
1354
|
function createStdinIO() {
|
|
1198
1355
|
const rl = external_node_readline_default.createInterface({
|
|
1199
1356
|
input: process.stdin,
|
|
@@ -1247,7 +1404,13 @@ function createStdinIO() {
|
|
|
1247
1404
|
}
|
|
1248
1405
|
const stdinFs = {
|
|
1249
1406
|
async writeFile (filePath, content) {
|
|
1250
|
-
await promises_default.writeFile(filePath, content,
|
|
1407
|
+
await promises_default.writeFile(filePath, content, {
|
|
1408
|
+
encoding: "utf-8",
|
|
1409
|
+
mode: 384
|
|
1410
|
+
});
|
|
1411
|
+
try {
|
|
1412
|
+
await promises_default.chmod(filePath, 384);
|
|
1413
|
+
} catch {}
|
|
1251
1414
|
},
|
|
1252
1415
|
async fileExists (filePath) {
|
|
1253
1416
|
try {
|
|
@@ -1258,173 +1421,12 @@ const stdinFs = {
|
|
|
1258
1421
|
}
|
|
1259
1422
|
}
|
|
1260
1423
|
};
|
|
1261
|
-
function usage() {
|
|
1262
|
-
return `
|
|
1263
|
-
extension-deploy \u{2014} Deploy browser extensions to Chrome, Firefox, and Edge.
|
|
1264
|
-
|
|
1265
|
-
USAGE
|
|
1266
|
-
extension-deploy [options] submit to one or more stores
|
|
1267
|
-
extension-deploy init [options] interactive credential setup wizard
|
|
1268
|
-
extension-deploy watch --chrome [options] poll an in-flight submission
|
|
1269
|
-
extension-deploy watch --firefox [options]
|
|
1270
|
-
extension-deploy watch --edge [options]
|
|
1271
|
-
|
|
1272
|
-
GLOBAL
|
|
1273
|
-
--dry-run Verify auth without uploading or publishing
|
|
1274
|
-
--output-json <path> Write the DeployResult / WatchEvent as JSON to <path>
|
|
1275
|
-
--help Show this help message
|
|
1276
|
-
|
|
1277
|
-
CHROME
|
|
1278
|
-
--chrome-zip <path> Path to Chrome extension ZIP
|
|
1279
|
-
--chrome-extension-id <id> Chrome extension ID
|
|
1280
|
-
--chrome-client-id <id> OAuth2 client ID
|
|
1281
|
-
--chrome-client-secret <secret> OAuth2 client secret
|
|
1282
|
-
--chrome-refresh-token <token> OAuth2 refresh token
|
|
1283
|
-
--chrome-publish-target <target> "default" or "trustedTesters"
|
|
1284
|
-
--chrome-deploy-percentage <n> Staged rollout percentage (1-100)
|
|
1285
|
-
--chrome-review-exemption Request expedited review
|
|
1286
|
-
--chrome-skip-submit-review Upload only, skip publish
|
|
1287
|
-
|
|
1288
|
-
FIREFOX
|
|
1289
|
-
--firefox-zip <path> Path to Firefox extension ZIP
|
|
1290
|
-
--firefox-sources-zip <path> Path to sources ZIP (optional)
|
|
1291
|
-
--firefox-extension-id <id> Addon GUID or email-style ID
|
|
1292
|
-
--firefox-jwt-issuer <issuer> AMO JWT issuer
|
|
1293
|
-
--firefox-jwt-secret <secret> AMO JWT secret
|
|
1294
|
-
--firefox-channel <channel> "listed" or "unlisted"
|
|
1295
|
-
|
|
1296
|
-
EDGE
|
|
1297
|
-
--edge-zip <path> Path to Edge extension ZIP
|
|
1298
|
-
--edge-product-id <id> Partner Center product ID
|
|
1299
|
-
--edge-client-id <id> Partner Center client ID
|
|
1300
|
-
--edge-api-key <key> Partner Center API key (v1.1)
|
|
1301
|
-
--edge-skip-submit-review Upload only, skip publish
|
|
1302
|
-
|
|
1303
|
-
WATCH
|
|
1304
|
-
--watch-submission-id <id> Required for firefox/edge (version id / operation id)
|
|
1305
|
-
--watch-interval <seconds> Poll interval (default 60)
|
|
1306
|
-
--watch-timeout <seconds> Max time to wait for a terminal state (default 3600)
|
|
1307
|
-
|
|
1308
|
-
INIT
|
|
1309
|
-
--init-stores <list> Comma-separated store list (skip picker)
|
|
1310
|
-
--init-output <path> Destination file (default .env.submit)
|
|
1311
|
-
--init-skip-verify Don't run verifyCredentials after collection
|
|
1312
|
-
--init-force Overwrite existing .env.submit without asking
|
|
1313
|
-
|
|
1314
|
-
ENVIRONMENT VARIABLES
|
|
1315
|
-
All flags can be set via SCREAMING_SNAKE_CASE env vars (e.g. CHROME_ZIP,
|
|
1316
|
-
FIREFOX_JWT_SECRET, EDGE_API_KEY). Flags take precedence over env vars.
|
|
1317
|
-
A .env.submit file in the current directory is loaded automatically.
|
|
1318
|
-
`.trim();
|
|
1319
|
-
}
|
|
1320
|
-
const BOOLEAN_FLAGS = new Set([
|
|
1321
|
-
"help",
|
|
1322
|
-
"dryRun",
|
|
1323
|
-
"chromeReviewExemption",
|
|
1324
|
-
"chromeSkipSubmitReview",
|
|
1325
|
-
"edgeSkipSubmitReview",
|
|
1326
|
-
"initSkipVerify",
|
|
1327
|
-
"initForce"
|
|
1328
|
-
]);
|
|
1329
|
-
const INT_FLAGS = new Set([
|
|
1330
|
-
"chromeDeployPercentage",
|
|
1331
|
-
"watchInterval",
|
|
1332
|
-
"watchTimeout"
|
|
1333
|
-
]);
|
|
1334
|
-
function parseArgs(argv) {
|
|
1335
|
-
const flags = {};
|
|
1336
|
-
let i = 0;
|
|
1337
|
-
if ("watch" === argv[0]) {
|
|
1338
|
-
flags.command = "watch";
|
|
1339
|
-
i = 1;
|
|
1340
|
-
} else if ("init" === argv[0]) {
|
|
1341
|
-
flags.command = "init";
|
|
1342
|
-
i = 1;
|
|
1343
|
-
}
|
|
1344
|
-
while(i < argv.length){
|
|
1345
|
-
const arg = argv[i];
|
|
1346
|
-
if ("--help" === arg || "-h" === arg) {
|
|
1347
|
-
flags.help = true;
|
|
1348
|
-
i++;
|
|
1349
|
-
continue;
|
|
1350
|
-
}
|
|
1351
|
-
if ("--chrome" === arg) {
|
|
1352
|
-
flags.watchStore = "chrome";
|
|
1353
|
-
i++;
|
|
1354
|
-
continue;
|
|
1355
|
-
}
|
|
1356
|
-
if ("--firefox" === arg) {
|
|
1357
|
-
flags.watchStore = "firefox";
|
|
1358
|
-
i++;
|
|
1359
|
-
continue;
|
|
1360
|
-
}
|
|
1361
|
-
if ("--edge" === arg) {
|
|
1362
|
-
flags.watchStore = "edge";
|
|
1363
|
-
i++;
|
|
1364
|
-
continue;
|
|
1365
|
-
}
|
|
1366
|
-
if (!(null == arg ? void 0 : arg.startsWith("--"))) {
|
|
1367
|
-
i++;
|
|
1368
|
-
continue;
|
|
1369
|
-
}
|
|
1370
|
-
const key = arg.slice(2).replace(/-([a-z])/g, (_, c)=>c.toUpperCase());
|
|
1371
|
-
if (BOOLEAN_FLAGS.has(key)) {
|
|
1372
|
-
flags[key] = true;
|
|
1373
|
-
i++;
|
|
1374
|
-
continue;
|
|
1375
|
-
}
|
|
1376
|
-
const next = argv[i + 1];
|
|
1377
|
-
if (null == next || next.startsWith("--")) {
|
|
1378
|
-
flags[key] = true;
|
|
1379
|
-
i++;
|
|
1380
|
-
continue;
|
|
1381
|
-
}
|
|
1382
|
-
flags[key] = INT_FLAGS.has(key) ? parseInt(next, 10) : next;
|
|
1383
|
-
i += 2;
|
|
1384
|
-
}
|
|
1385
|
-
return flags;
|
|
1386
|
-
}
|
|
1387
|
-
function loadDotEnv() {
|
|
1388
|
-
try {
|
|
1389
|
-
const envPath = external_node_path_default.resolve(process.cwd(), ".env.submit");
|
|
1390
|
-
const content = external_node_fs_default.readFileSync(envPath, "utf-8");
|
|
1391
|
-
for (const line of content.split("\n")){
|
|
1392
|
-
const trimmed = line.trim();
|
|
1393
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1394
|
-
const eqIdx = trimmed.indexOf("=");
|
|
1395
|
-
if (-1 === eqIdx) continue;
|
|
1396
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
1397
|
-
let value = trimmed.slice(eqIdx + 1).trim();
|
|
1398
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1399
|
-
if (!(key in process.env)) process.env[key] = value;
|
|
1400
|
-
}
|
|
1401
|
-
} catch {}
|
|
1402
|
-
}
|
|
1403
1424
|
function writeJsonOutput(filePath, payload) {
|
|
1404
1425
|
external_node_fs_default.mkdirSync(external_node_path_default.dirname(external_node_path_default.resolve(filePath)), {
|
|
1405
1426
|
recursive: true
|
|
1406
1427
|
});
|
|
1407
1428
|
external_node_fs_default.writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
1408
1429
|
}
|
|
1409
|
-
async function main() {
|
|
1410
|
-
loadDotEnv();
|
|
1411
|
-
const flags = parseArgs(process.argv.slice(2));
|
|
1412
|
-
if (flags.help) {
|
|
1413
|
-
console.log(usage());
|
|
1414
|
-
process.exit(0);
|
|
1415
|
-
}
|
|
1416
|
-
if ("watch" === flags.command) return void await runWatch(flags);
|
|
1417
|
-
if ("init" === flags.command) return void await runInitCommand(flags);
|
|
1418
|
-
const config = resolveConfig(flags);
|
|
1419
|
-
try {
|
|
1420
|
-
const result = await deploy(config);
|
|
1421
|
-
if (flags.outputJson) writeJsonOutput(flags.outputJson, result);
|
|
1422
|
-
if (!result.success) process.exit(1);
|
|
1423
|
-
} catch (err) {
|
|
1424
|
-
logError("deploy", err instanceof Error ? err.message : String(err));
|
|
1425
|
-
process.exit(1);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
1430
|
async function runWatch(flags) {
|
|
1429
1431
|
const store = flags.watchStore;
|
|
1430
1432
|
if (!store) {
|
|
@@ -1447,7 +1449,8 @@ async function runWatch(flags) {
|
|
|
1447
1449
|
clientId: mustEnv("CHROME_CLIENT_ID", flags.chromeClientId),
|
|
1448
1450
|
clientSecret: mustEnv("CHROME_CLIENT_SECRET", flags.chromeClientSecret),
|
|
1449
1451
|
refreshToken: mustEnv("CHROME_REFRESH_TOKEN", flags.chromeRefreshToken),
|
|
1450
|
-
extensionId: mustEnv("CHROME_EXTENSION_ID", flags.chromeExtensionId)
|
|
1452
|
+
extensionId: mustEnv("CHROME_EXTENSION_ID", flags.chromeExtensionId),
|
|
1453
|
+
publisherId: mustEnv("CHROME_PUBLISHER_ID", flags.chromePublisherId)
|
|
1451
1454
|
}, {
|
|
1452
1455
|
pollIntervalMs,
|
|
1453
1456
|
timeoutMs,
|
|
@@ -1520,4 +1523,39 @@ function parseInitStores(raw) {
|
|
|
1520
1523
|
if (0 === parsed.length) throw new Error(`--init-stores: expected a comma-separated list of chrome,firefox,edge (got "${raw}")`);
|
|
1521
1524
|
return parsed;
|
|
1522
1525
|
}
|
|
1526
|
+
function loadDotEnv() {
|
|
1527
|
+
try {
|
|
1528
|
+
const envPath = external_node_path_default.resolve(process.cwd(), ".env.submit");
|
|
1529
|
+
const content = external_node_fs_default.readFileSync(envPath, "utf-8");
|
|
1530
|
+
for (const line of content.split("\n")){
|
|
1531
|
+
const trimmed = line.trim();
|
|
1532
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1533
|
+
const eqIdx = trimmed.indexOf("=");
|
|
1534
|
+
if (-1 === eqIdx) continue;
|
|
1535
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
1536
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
1537
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
1538
|
+
if (!(key in process.env)) process.env[key] = value;
|
|
1539
|
+
}
|
|
1540
|
+
} catch {}
|
|
1541
|
+
}
|
|
1542
|
+
async function main() {
|
|
1543
|
+
loadDotEnv();
|
|
1544
|
+
const flags = parseArgs(process.argv.slice(2));
|
|
1545
|
+
if (flags.help) {
|
|
1546
|
+
console.log(usage());
|
|
1547
|
+
process.exit(0);
|
|
1548
|
+
}
|
|
1549
|
+
if ("watch" === flags.command) return void await runWatch(flags);
|
|
1550
|
+
if ("init" === flags.command) return void await runInitCommand(flags);
|
|
1551
|
+
const config = resolveConfig(flags);
|
|
1552
|
+
try {
|
|
1553
|
+
const result = await deploy(config);
|
|
1554
|
+
if (flags.outputJson) writeJsonOutput(flags.outputJson, result);
|
|
1555
|
+
if (!result.success) process.exit(1);
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
logError("deploy", err instanceof Error ? err.message : String(err));
|
|
1558
|
+
process.exit(1);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1523
1561
|
export { chromeOptionsSchema, deploy, deployConfigSchema, edgeOptionsSchema, firefoxOptionsSchema, getChromeSubmissionEvent, getEdgeSubmissionEvent, getFirefoxSubmissionEvent, main, resolveConfig, validateConfig, verifyChromeCredentials, verifyEdgeCredentials, verifyFirefoxCredentials, watchChromeSubmission, watchEdgeSubmission, watchFirefoxSubmission };
|