@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.
- package/CHANGELOG.md +13 -0
- package/README.md +34 -1
- package/dist/module.js +500 -340
- package/dist/module.mjs +498 -338
- package/dist/src/chrome.d.ts +19 -0
- package/dist/src/chrome.d.ts.map +1 -1
- package/dist/src/cli-commands.d.ts +5 -0
- package/dist/src/cli-commands.d.ts.map +1 -0
- package/dist/src/cli-parse.d.ts +3 -0
- package/dist/src/cli-parse.d.ts.map +1 -0
- package/dist/src/cli-usage.d.ts +2 -0
- package/dist/src/cli-usage.d.ts.map +1 -0
- package/dist/src/cli.d.ts +1 -2
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/config.d.ts +6 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/deploy.d.ts.map +1 -1
- package/dist/src/edge.d.ts.map +1 -1
- package/dist/src/firefox-api.d.ts +52 -0
- package/dist/src/firefox-api.d.ts.map +1 -0
- package/dist/src/firefox.d.ts +1 -24
- package/dist/src/firefox.d.ts.map +1 -1
- package/dist/src/init-chrome.d.ts +16 -0
- package/dist/src/init-chrome.d.ts.map +1 -0
- package/dist/src/init-edge.d.ts +8 -0
- package/dist/src/init-edge.d.ts.map +1 -0
- package/dist/src/init-firefox.d.ts +8 -0
- package/dist/src/init-firefox.d.ts.map +1 -0
- package/dist/src/init-io.d.ts.map +1 -1
- package/dist/src/init-prompt.d.ts +5 -0
- package/dist/src/init-prompt.d.ts.map +1 -0
- package/dist/src/init.d.ts +3 -26
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/types.d.ts +49 -5
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/fetch.d.ts +2 -2
- package/dist/src/utils/fetch.d.ts.map +1 -1
- package/dist/src/watch.d.ts.map +1 -1
- package/package.json +18 -8
- package/dist/__tests__/cli.test.d.ts +0 -2
- package/dist/__tests__/cli.test.d.ts.map +0 -1
- package/dist/__tests__/config.test.d.ts +0 -2
- package/dist/__tests__/config.test.d.ts.map +0 -1
- package/dist/__tests__/deploy.test.d.ts +0 -2
- package/dist/__tests__/deploy.test.d.ts.map +0 -1
- package/dist/__tests__/dry-run.test.d.ts +0 -2
- package/dist/__tests__/dry-run.test.d.ts.map +0 -1
- package/dist/__tests__/init.test.d.ts +0 -2
- package/dist/__tests__/init.test.d.ts.map +0 -1
- package/dist/__tests__/types.test.d.ts +0 -2
- package/dist/__tests__/types.test.d.ts.map +0 -1
- package/dist/__tests__/utils.test.d.ts +0 -2
- package/dist/__tests__/utils.test.d.ts.map +0 -1
- package/dist/__tests__/verify.test.d.ts +0 -2
- package/dist/__tests__/verify.test.d.ts.map +0 -1
- package/dist/__tests__/watch.test.d.ts +0 -2
- package/dist/__tests__/watch.test.d.ts.map +0 -1
package/dist/module.mjs
CHANGED
|
@@ -17,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().
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
|
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
|
|
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 ??
|
|
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
|
-
|
|
903
|
-
{
|
|
904
|
-
|
|
905
|
-
|
|
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
|
|
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(
|
|
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,
|
|
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 };
|