@extension.dev/deploy 0.1.0 → 0.2.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/README.md +30 -0
- package/dist/__tests__/init.test.d.ts +2 -0
- package/dist/__tests__/init.test.d.ts.map +1 -0
- package/dist/module.js +583 -2
- package/dist/module.mjs +576 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/config.d.ts +10 -2
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/init-io.d.ts +21 -0
- package/dist/src/init-io.d.ts.map +1 -0
- package/dist/src/init.d.ts +75 -0
- package/dist/src/init.d.ts.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -22,8 +22,38 @@ Deploy browser extensions to Chrome Web Store, Firefox AMO, and Edge Partner Cen
|
|
|
22
22
|
npm install @extension.dev/deploy
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
If you're setting up a new project, run the interactive wizard. It walks you
|
|
28
|
+
through each store's credential acquisition (including auto-generating a
|
|
29
|
+
Chrome refresh token from your OAuth client), runs a verification check
|
|
30
|
+
against each store's API, and writes everything to a `.env.submit` file:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx extension-deploy init
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Non-interactive use (e.g. CI setup scripts):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx extension-deploy init \
|
|
40
|
+
--init-stores chrome,firefox \
|
|
41
|
+
--init-output config/.env.submit \
|
|
42
|
+
--init-skip-verify \
|
|
43
|
+
--init-force
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
After `init` has written `.env.submit`, a full submission is a one-liner:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx extension-deploy --dry-run # validate creds and ZIPs
|
|
50
|
+
npx extension-deploy # actually submit
|
|
51
|
+
```
|
|
52
|
+
|
|
25
53
|
## CLI usage
|
|
26
54
|
|
|
55
|
+
If you'd rather skip the wizard and pass credentials directly:
|
|
56
|
+
|
|
27
57
|
```bash
|
|
28
58
|
npx extension-deploy \
|
|
29
59
|
--chrome-zip dist/chrome.zip \
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.test.d.ts","sourceRoot":"","sources":["../../__tests__/init.test.ts"],"names":[],"mappings":""}
|
package/dist/module.js
CHANGED
|
@@ -33,7 +33,8 @@ var __webpack_require__ = {};
|
|
|
33
33
|
var __webpack_exports__ = {};
|
|
34
34
|
__webpack_require__.r(__webpack_exports__);
|
|
35
35
|
__webpack_require__.d(__webpack_exports__, {
|
|
36
|
-
|
|
36
|
+
watchChromeSubmission: ()=>watchChromeSubmission,
|
|
37
|
+
main: ()=>main,
|
|
37
38
|
resolveConfig: ()=>resolveConfig,
|
|
38
39
|
chromeOptionsSchema: ()=>chromeOptionsSchema,
|
|
39
40
|
getFirefoxSubmissionEvent: ()=>getFirefoxSubmissionEvent,
|
|
@@ -48,8 +49,12 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
48
49
|
verifyFirefoxCredentials: ()=>verifyFirefoxCredentials,
|
|
49
50
|
verifyEdgeCredentials: ()=>verifyEdgeCredentials,
|
|
50
51
|
verifyChromeCredentials: ()=>verifyChromeCredentials,
|
|
51
|
-
|
|
52
|
+
getEdgeSubmissionEvent: ()=>getEdgeSubmissionEvent
|
|
52
53
|
});
|
|
54
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
55
|
+
var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject);
|
|
56
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
57
|
+
var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
|
|
53
58
|
const external_zod_namespaceObject = require("zod");
|
|
54
59
|
const chromeOptionsSchema = external_zod_namespaceObject.z.object({
|
|
55
60
|
zip: external_zod_namespaceObject.z.string().min(1),
|
|
@@ -937,6 +942,580 @@ async function runWatchLoop(_store, probe, options) {
|
|
|
937
942
|
await delay(pollInterval);
|
|
938
943
|
}
|
|
939
944
|
}
|
|
945
|
+
const STORE_CHOICES = [
|
|
946
|
+
{
|
|
947
|
+
value: "chrome",
|
|
948
|
+
label: "Chrome Web Store"
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
value: "firefox",
|
|
952
|
+
label: "Firefox AMO (addons.mozilla.org)"
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
value: "edge",
|
|
956
|
+
label: "Edge Add-ons (Partner Center)"
|
|
957
|
+
}
|
|
958
|
+
];
|
|
959
|
+
const CHROME_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
960
|
+
const CHROME_OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
|
|
961
|
+
const CHROME_OOB_REDIRECT = "urn:ietf:wg:oauth:2.0:oob";
|
|
962
|
+
const CHROME_SCOPE = "https://www.googleapis.com/auth/chromewebstore";
|
|
963
|
+
async function runInit(deps, options = {}) {
|
|
964
|
+
const { io } = deps;
|
|
965
|
+
io.log("");
|
|
966
|
+
io.log("extension-deploy init \u2014 set up store credentials");
|
|
967
|
+
io.log("");
|
|
968
|
+
io.log("This wizard walks you through creating credentials for each store and");
|
|
969
|
+
io.log("writes them to a .env.submit file. Press Ctrl+C at any time to abort.");
|
|
970
|
+
io.log("");
|
|
971
|
+
const stores = options.stores ? options.stores : await io.multiselect("Which stores do you want to configure?", STORE_CHOICES);
|
|
972
|
+
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.");
|
|
973
|
+
const envLines = [
|
|
974
|
+
"# Generated by `extension-deploy init`.",
|
|
975
|
+
"# Do not commit secrets to source control.",
|
|
976
|
+
""
|
|
977
|
+
];
|
|
978
|
+
const verified = {};
|
|
979
|
+
for (const store of stores){
|
|
980
|
+
io.log("");
|
|
981
|
+
io.log(`=== ${labelFor(store)} ===`);
|
|
982
|
+
io.log("");
|
|
983
|
+
if ("chrome" === store) {
|
|
984
|
+
const creds = await collectChromeCredentials(deps);
|
|
985
|
+
envLines.push(`CHROME_EXTENSION_ID=${creds.extensionId}`, `CHROME_CLIENT_ID=${creds.clientId}`, `CHROME_CLIENT_SECRET=${creds.clientSecret}`, `CHROME_REFRESH_TOKEN=${creds.refreshToken}`, "");
|
|
986
|
+
if (!options.skipVerify) {
|
|
987
|
+
const verify = deps.verifyChrome ?? verifyChromeCredentials;
|
|
988
|
+
verified.chrome = normalizeVerify(await verify(creds));
|
|
989
|
+
logVerify(io, "chrome", verified.chrome);
|
|
990
|
+
}
|
|
991
|
+
} else if ("firefox" === store) {
|
|
992
|
+
const creds = await collectFirefoxCredentials(deps);
|
|
993
|
+
envLines.push(`FIREFOX_EXTENSION_ID=${creds.addonId}`, `FIREFOX_JWT_ISSUER=${creds.jwtIssuer}`, `FIREFOX_JWT_SECRET=${creds.jwtSecret}`, "");
|
|
994
|
+
if (!options.skipVerify) {
|
|
995
|
+
const verify = deps.verifyFirefox ?? verifyFirefoxCredentials;
|
|
996
|
+
verified.firefox = normalizeVerify(await verify({
|
|
997
|
+
jwtIssuer: creds.jwtIssuer,
|
|
998
|
+
jwtSecret: creds.jwtSecret,
|
|
999
|
+
addonId: creds.addonId
|
|
1000
|
+
}));
|
|
1001
|
+
logVerify(io, "firefox", verified.firefox);
|
|
1002
|
+
}
|
|
1003
|
+
} else {
|
|
1004
|
+
const creds = await collectEdgeCredentials(deps);
|
|
1005
|
+
envLines.push(`EDGE_PRODUCT_ID=${creds.productId}`, `EDGE_CLIENT_ID=${creds.clientId}`, `EDGE_API_KEY=${creds.apiKey}`, "");
|
|
1006
|
+
if (!options.skipVerify) {
|
|
1007
|
+
const verify = deps.verifyEdge ?? verifyEdgeCredentials;
|
|
1008
|
+
verified.edge = normalizeVerify(await verify({
|
|
1009
|
+
clientId: creds.clientId,
|
|
1010
|
+
apiKey: creds.apiKey,
|
|
1011
|
+
productId: creds.productId
|
|
1012
|
+
}));
|
|
1013
|
+
logVerify(io, "edge", verified.edge);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const envContent = envLines.join("\n");
|
|
1018
|
+
const envPath = resolveEnvPath(deps.cwd, options.outputPath);
|
|
1019
|
+
const exists = await deps.fileExists(envPath);
|
|
1020
|
+
let wrote = false;
|
|
1021
|
+
if (exists && !options.force) {
|
|
1022
|
+
const overwrite = await deps.io.confirm(`${envPath} already exists. Overwrite?`, false);
|
|
1023
|
+
if (overwrite) {
|
|
1024
|
+
await deps.writeFile(envPath, envContent);
|
|
1025
|
+
wrote = true;
|
|
1026
|
+
} else {
|
|
1027
|
+
io.log("");
|
|
1028
|
+
io.log(`Skipped writing ${envPath}. Credential block:`);
|
|
1029
|
+
io.log("");
|
|
1030
|
+
io.log(envContent);
|
|
1031
|
+
}
|
|
1032
|
+
} else {
|
|
1033
|
+
await deps.writeFile(envPath, envContent);
|
|
1034
|
+
wrote = true;
|
|
1035
|
+
}
|
|
1036
|
+
if (wrote) {
|
|
1037
|
+
io.log("");
|
|
1038
|
+
io.log(`Wrote ${envPath}`);
|
|
1039
|
+
io.log("");
|
|
1040
|
+
io.log("Next steps:");
|
|
1041
|
+
io.log(" - Add .env.submit to .gitignore if it isn't already (it contains secrets).");
|
|
1042
|
+
io.log(" - Run `extension-deploy --dry-run` to sanity-check the full submit flow.");
|
|
1043
|
+
io.log("");
|
|
1044
|
+
}
|
|
1045
|
+
return {
|
|
1046
|
+
stores,
|
|
1047
|
+
envPath,
|
|
1048
|
+
envContent,
|
|
1049
|
+
verified,
|
|
1050
|
+
wrote
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
async function collectChromeCredentials(deps) {
|
|
1054
|
+
const { io } = deps;
|
|
1055
|
+
io.log("Chrome Web Store needs OAuth credentials. You'll need:");
|
|
1056
|
+
io.log(" 1. A Google Cloud project with the Chrome Web Store API enabled:");
|
|
1057
|
+
io.log(" https://console.cloud.google.com/apis/library/chromewebstore.googleapis.com");
|
|
1058
|
+
io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
|
|
1059
|
+
io.log(" https://console.cloud.google.com/apis/credentials");
|
|
1060
|
+
io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
|
|
1061
|
+
io.log("");
|
|
1062
|
+
io.log("This wizard will take your Client ID + Secret and walk you through");
|
|
1063
|
+
io.log("generating a refresh token automatically.");
|
|
1064
|
+
io.log("");
|
|
1065
|
+
const extensionId = await requireValue(io, "Chrome extension ID", "(32-character ID from the CWS dashboard URL)");
|
|
1066
|
+
const clientId = await requireValue(io, "OAuth Client ID");
|
|
1067
|
+
const clientSecret = await requireValue(io, "OAuth Client Secret", void 0, {
|
|
1068
|
+
secret: true
|
|
1069
|
+
});
|
|
1070
|
+
const refreshToken = await obtainChromeRefreshToken(deps, clientId, clientSecret);
|
|
1071
|
+
return {
|
|
1072
|
+
extensionId,
|
|
1073
|
+
clientId,
|
|
1074
|
+
clientSecret,
|
|
1075
|
+
refreshToken
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function buildChromeAuthUrl(clientId) {
|
|
1079
|
+
const params = new URLSearchParams({
|
|
1080
|
+
response_type: "code",
|
|
1081
|
+
client_id: clientId,
|
|
1082
|
+
redirect_uri: CHROME_OOB_REDIRECT,
|
|
1083
|
+
scope: CHROME_SCOPE,
|
|
1084
|
+
access_type: "offline",
|
|
1085
|
+
prompt: "consent"
|
|
1086
|
+
});
|
|
1087
|
+
return `${CHROME_OAUTH_AUTH_URL}?${params.toString()}`;
|
|
1088
|
+
}
|
|
1089
|
+
async function exchangeChromeAuthCode(input) {
|
|
1090
|
+
const fetchFn = input.fetchImpl ?? fetch;
|
|
1091
|
+
const body = new URLSearchParams({
|
|
1092
|
+
code: input.authCode,
|
|
1093
|
+
client_id: input.clientId,
|
|
1094
|
+
client_secret: input.clientSecret,
|
|
1095
|
+
redirect_uri: CHROME_OOB_REDIRECT,
|
|
1096
|
+
grant_type: "authorization_code"
|
|
1097
|
+
});
|
|
1098
|
+
const res = await fetchFn(CHROME_OAUTH_TOKEN_URL, {
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
headers: {
|
|
1101
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1102
|
+
},
|
|
1103
|
+
body: body.toString()
|
|
1104
|
+
});
|
|
1105
|
+
if (!res.ok) {
|
|
1106
|
+
const text = await res.text().catch(()=>"");
|
|
1107
|
+
throw new Error(`Chrome token exchange failed (HTTP ${res.status}): ${text || res.statusText}`);
|
|
1108
|
+
}
|
|
1109
|
+
const json = await res.json();
|
|
1110
|
+
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.`);
|
|
1111
|
+
return json.refresh_token;
|
|
1112
|
+
}
|
|
1113
|
+
async function obtainChromeRefreshToken(deps, clientId, clientSecret) {
|
|
1114
|
+
const { io } = deps;
|
|
1115
|
+
const authUrl = buildChromeAuthUrl(clientId);
|
|
1116
|
+
io.log("");
|
|
1117
|
+
io.log("Open the URL below in your browser, approve the scope, and paste the");
|
|
1118
|
+
io.log("authorization code shown on the page:");
|
|
1119
|
+
io.log("");
|
|
1120
|
+
io.log(` ${authUrl}`);
|
|
1121
|
+
io.log("");
|
|
1122
|
+
const authCode = await requireValue(io, "Authorization code", void 0, {
|
|
1123
|
+
secret: true
|
|
1124
|
+
});
|
|
1125
|
+
return exchangeChromeAuthCode({
|
|
1126
|
+
clientId,
|
|
1127
|
+
clientSecret,
|
|
1128
|
+
authCode,
|
|
1129
|
+
fetchImpl: deps.fetchImpl
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
async function collectFirefoxCredentials(deps) {
|
|
1133
|
+
const { io } = deps;
|
|
1134
|
+
io.log("Firefox AMO needs JWT API credentials:");
|
|
1135
|
+
io.log(" 1. Sign in at https://addons.mozilla.org");
|
|
1136
|
+
io.log(" 2. Create API credentials at https://addons.mozilla.org/developers/addon/api/key/");
|
|
1137
|
+
io.log(" 3. Copy the JWT issuer and JWT secret (the secret is only shown once).");
|
|
1138
|
+
io.log("");
|
|
1139
|
+
const addonId = await requireValue(io, "Firefox addon ID", "({uuid} GUID or email-style ID from your addon page)");
|
|
1140
|
+
const jwtIssuer = await requireValue(io, "JWT issuer");
|
|
1141
|
+
const jwtSecret = await requireValue(io, "JWT secret", void 0, {
|
|
1142
|
+
secret: true
|
|
1143
|
+
});
|
|
1144
|
+
return {
|
|
1145
|
+
addonId,
|
|
1146
|
+
jwtIssuer,
|
|
1147
|
+
jwtSecret
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
async function collectEdgeCredentials(deps) {
|
|
1151
|
+
const { io } = deps;
|
|
1152
|
+
io.log("Edge Partner Center needs the v1.1 API credentials:");
|
|
1153
|
+
io.log(" 1. Open the Partner Center dashboard: https://partner.microsoft.com/dashboard/microsoftedge/overview");
|
|
1154
|
+
io.log(" 2. Under your extension, go to Settings \u2192 API access \u2192 API v1.1 and generate credentials.");
|
|
1155
|
+
io.log(" 3. Copy the Client ID, API key, and Product ID.");
|
|
1156
|
+
io.log("");
|
|
1157
|
+
const productId = await requireValue(io, "Edge product ID");
|
|
1158
|
+
const clientId = await requireValue(io, "Edge client ID");
|
|
1159
|
+
const apiKey = await requireValue(io, "Edge API key", void 0, {
|
|
1160
|
+
secret: true
|
|
1161
|
+
});
|
|
1162
|
+
return {
|
|
1163
|
+
productId,
|
|
1164
|
+
clientId,
|
|
1165
|
+
apiKey
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function labelFor(store) {
|
|
1169
|
+
var _STORE_CHOICES_find;
|
|
1170
|
+
return (null == (_STORE_CHOICES_find = STORE_CHOICES.find((c)=>c.value === store)) ? void 0 : _STORE_CHOICES_find.label) ?? String(store);
|
|
1171
|
+
}
|
|
1172
|
+
function normalizeVerify(result) {
|
|
1173
|
+
return {
|
|
1174
|
+
ok: result.ok,
|
|
1175
|
+
message: result.message
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function logVerify(io, store, record) {
|
|
1179
|
+
const marker = record.ok ? "[ok]" : "[fail]";
|
|
1180
|
+
io.log(` ${marker} verify ${store}: ${record.message}`);
|
|
1181
|
+
}
|
|
1182
|
+
function resolveEnvPath(cwd, outputPath) {
|
|
1183
|
+
const target = outputPath ?? ".env.submit";
|
|
1184
|
+
return external_node_path_default().isAbsolute(target) ? target : external_node_path_default().join(cwd, target);
|
|
1185
|
+
}
|
|
1186
|
+
async function requireValue(io, label, hint, opts) {
|
|
1187
|
+
const question = hint ? `${label} ${hint}:` : `${label}:`;
|
|
1188
|
+
for(let attempt = 0; attempt < 3; attempt++){
|
|
1189
|
+
const answer = (await io.prompt(question, opts)).trim();
|
|
1190
|
+
if (answer) return answer;
|
|
1191
|
+
io.log(` ${label} is required.`);
|
|
1192
|
+
}
|
|
1193
|
+
throw new Error(`init: ${label} is required.`);
|
|
1194
|
+
}
|
|
1195
|
+
const external_node_readline_namespaceObject = require("node:readline");
|
|
1196
|
+
var external_node_readline_default = /*#__PURE__*/ __webpack_require__.n(external_node_readline_namespaceObject);
|
|
1197
|
+
function createStdinIO() {
|
|
1198
|
+
const rl = external_node_readline_default().createInterface({
|
|
1199
|
+
input: process.stdin,
|
|
1200
|
+
output: process.stdout,
|
|
1201
|
+
terminal: false
|
|
1202
|
+
});
|
|
1203
|
+
const lineIter = rl[Symbol.asyncIterator]();
|
|
1204
|
+
async function readLine() {
|
|
1205
|
+
const { value, done } = await lineIter.next();
|
|
1206
|
+
if (done) return "";
|
|
1207
|
+
return value;
|
|
1208
|
+
}
|
|
1209
|
+
function write(chunk) {
|
|
1210
|
+
process.stdout.write(chunk);
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
async prompt (question, opts) {
|
|
1214
|
+
const suffix = (null == opts ? void 0 : opts.default) ? ` [${opts.default}]` : "";
|
|
1215
|
+
const secretNote = (null == opts ? void 0 : opts.secret) ? " (input visible)" : "";
|
|
1216
|
+
write(`${question}${suffix}${secretNote} `);
|
|
1217
|
+
const answer = await readLine();
|
|
1218
|
+
if (0 === answer.length && (null == opts ? void 0 : opts.default)) return opts.default;
|
|
1219
|
+
return answer;
|
|
1220
|
+
},
|
|
1221
|
+
async confirm (question, defaultValue = false) {
|
|
1222
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
1223
|
+
write(`${question} ${suffix} `);
|
|
1224
|
+
const raw = (await readLine()).trim().toLowerCase();
|
|
1225
|
+
if ("" === raw) return defaultValue;
|
|
1226
|
+
return "y" === raw || "yes" === raw;
|
|
1227
|
+
},
|
|
1228
|
+
async multiselect (question, choices) {
|
|
1229
|
+
write(`${question}\n`);
|
|
1230
|
+
choices.forEach((choice, idx)=>{
|
|
1231
|
+
write(` ${idx + 1}) ${choice.label}\n`);
|
|
1232
|
+
});
|
|
1233
|
+
write(' Enter comma-separated numbers (e.g. 1,2 or "all"): ');
|
|
1234
|
+
const raw = (await readLine()).trim().toLowerCase();
|
|
1235
|
+
if ("all" === raw) return choices.map((c)=>c.value);
|
|
1236
|
+
const indices = raw.split(/[,\s]+/).map((token)=>Number.parseInt(token, 10)).filter((n)=>Number.isInteger(n) && n >= 1 && n <= choices.length);
|
|
1237
|
+
const unique = Array.from(new Set(indices));
|
|
1238
|
+
return unique.map((i)=>choices[i - 1].value);
|
|
1239
|
+
},
|
|
1240
|
+
log (message) {
|
|
1241
|
+
write(`${message}\n`);
|
|
1242
|
+
},
|
|
1243
|
+
dispose () {
|
|
1244
|
+
rl.close();
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
const stdinFs = {
|
|
1249
|
+
async writeFile (filePath, content) {
|
|
1250
|
+
await promises_default().writeFile(filePath, content, "utf-8");
|
|
1251
|
+
},
|
|
1252
|
+
async fileExists (filePath) {
|
|
1253
|
+
try {
|
|
1254
|
+
await promises_default().access(filePath);
|
|
1255
|
+
return true;
|
|
1256
|
+
} catch {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
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
|
+
function writeJsonOutput(filePath, payload) {
|
|
1404
|
+
external_node_fs_default().mkdirSync(external_node_path_default().dirname(external_node_path_default().resolve(filePath)), {
|
|
1405
|
+
recursive: true
|
|
1406
|
+
});
|
|
1407
|
+
external_node_fs_default().writeFileSync(filePath, JSON.stringify(payload, null, 2) + "\n", "utf-8");
|
|
1408
|
+
}
|
|
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
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function runWatch(flags) {
|
|
1429
|
+
const store = flags.watchStore;
|
|
1430
|
+
if (!store) {
|
|
1431
|
+
console.error("watch: one of --chrome, --firefox, or --edge must be provided.");
|
|
1432
|
+
process.exit(2);
|
|
1433
|
+
}
|
|
1434
|
+
const onEvent = (event)=>{
|
|
1435
|
+
const line = `[watch:${event.store}] ${event.status}${event.nativeStatus ? ` (${event.nativeStatus})` : ""}${event.message ? ` \u{2014} ${event.message}` : ""}`;
|
|
1436
|
+
console.log(line);
|
|
1437
|
+
};
|
|
1438
|
+
const pollIntervalMs = (flags.watchInterval ?? 60) * 1000;
|
|
1439
|
+
const timeoutMs = (flags.watchTimeout ?? 3600) * 1000;
|
|
1440
|
+
let terminal;
|
|
1441
|
+
try {
|
|
1442
|
+
terminal = "chrome" === store ? await watchChromeSubmission({
|
|
1443
|
+
clientId: mustEnv("CHROME_CLIENT_ID", flags.chromeClientId),
|
|
1444
|
+
clientSecret: mustEnv("CHROME_CLIENT_SECRET", flags.chromeClientSecret),
|
|
1445
|
+
refreshToken: mustEnv("CHROME_REFRESH_TOKEN", flags.chromeRefreshToken),
|
|
1446
|
+
extensionId: mustEnv("CHROME_EXTENSION_ID", flags.chromeExtensionId)
|
|
1447
|
+
}, {
|
|
1448
|
+
pollIntervalMs,
|
|
1449
|
+
timeoutMs,
|
|
1450
|
+
onEvent
|
|
1451
|
+
}) : "firefox" === store ? await watchFirefoxSubmission({
|
|
1452
|
+
jwtIssuer: mustEnv("FIREFOX_JWT_ISSUER", flags.firefoxJwtIssuer),
|
|
1453
|
+
jwtSecret: mustEnv("FIREFOX_JWT_SECRET", flags.firefoxJwtSecret),
|
|
1454
|
+
extensionId: mustEnv("FIREFOX_EXTENSION_ID", flags.firefoxExtensionId),
|
|
1455
|
+
versionId: mustValue("--watch-submission-id", flags.watchSubmissionId)
|
|
1456
|
+
}, {
|
|
1457
|
+
pollIntervalMs,
|
|
1458
|
+
timeoutMs,
|
|
1459
|
+
onEvent
|
|
1460
|
+
}) : await watchEdgeSubmission({
|
|
1461
|
+
clientId: mustEnv("EDGE_CLIENT_ID", flags.edgeClientId),
|
|
1462
|
+
apiKey: mustEnv("EDGE_API_KEY", flags.edgeApiKey),
|
|
1463
|
+
productId: mustEnv("EDGE_PRODUCT_ID", flags.edgeProductId),
|
|
1464
|
+
operationId: mustValue("--watch-submission-id", flags.watchSubmissionId)
|
|
1465
|
+
}, {
|
|
1466
|
+
pollIntervalMs,
|
|
1467
|
+
timeoutMs,
|
|
1468
|
+
onEvent
|
|
1469
|
+
});
|
|
1470
|
+
} catch (err) {
|
|
1471
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1472
|
+
process.exit(1);
|
|
1473
|
+
}
|
|
1474
|
+
if (flags.outputJson) writeJsonOutput(flags.outputJson, terminal);
|
|
1475
|
+
if ("failed" === terminal.status || "rejected" === terminal.status) process.exit(1);
|
|
1476
|
+
}
|
|
1477
|
+
function mustEnv(envName, flagValue) {
|
|
1478
|
+
const v = flagValue ?? process.env[envName];
|
|
1479
|
+
if (!v) throw new Error(`Missing required credential: ${envName}. Set the env var or pass the matching flag.`);
|
|
1480
|
+
return v;
|
|
1481
|
+
}
|
|
1482
|
+
function mustValue(flagName, value) {
|
|
1483
|
+
if (!value) throw new Error(`Missing required flag: ${flagName}`);
|
|
1484
|
+
return value;
|
|
1485
|
+
}
|
|
1486
|
+
async function runInitCommand(flags) {
|
|
1487
|
+
const stores = parseInitStores(flags.initStores);
|
|
1488
|
+
const io = createStdinIO();
|
|
1489
|
+
try {
|
|
1490
|
+
await runInit({
|
|
1491
|
+
io,
|
|
1492
|
+
cwd: process.cwd(),
|
|
1493
|
+
writeFile: stdinFs.writeFile,
|
|
1494
|
+
fileExists: stdinFs.fileExists
|
|
1495
|
+
}, {
|
|
1496
|
+
stores,
|
|
1497
|
+
outputPath: flags.initOutput,
|
|
1498
|
+
skipVerify: flags.initSkipVerify,
|
|
1499
|
+
force: flags.initForce
|
|
1500
|
+
});
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1503
|
+
process.exit(1);
|
|
1504
|
+
} finally{
|
|
1505
|
+
io.dispose();
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
function parseInitStores(raw) {
|
|
1509
|
+
if (!raw) return;
|
|
1510
|
+
const allowed = [
|
|
1511
|
+
"chrome",
|
|
1512
|
+
"firefox",
|
|
1513
|
+
"edge"
|
|
1514
|
+
];
|
|
1515
|
+
const parsed = raw.split(",").map((s)=>s.trim().toLowerCase()).filter((s)=>allowed.includes(s));
|
|
1516
|
+
if (0 === parsed.length) throw new Error(`--init-stores: expected a comma-separated list of chrome,firefox,edge (got "${raw}")`);
|
|
1517
|
+
return parsed;
|
|
1518
|
+
}
|
|
940
1519
|
exports.chromeOptionsSchema = __webpack_exports__.chromeOptionsSchema;
|
|
941
1520
|
exports.deploy = __webpack_exports__.deploy;
|
|
942
1521
|
exports.deployConfigSchema = __webpack_exports__.deployConfigSchema;
|
|
@@ -945,6 +1524,7 @@ exports.firefoxOptionsSchema = __webpack_exports__.firefoxOptionsSchema;
|
|
|
945
1524
|
exports.getChromeSubmissionEvent = __webpack_exports__.getChromeSubmissionEvent;
|
|
946
1525
|
exports.getEdgeSubmissionEvent = __webpack_exports__.getEdgeSubmissionEvent;
|
|
947
1526
|
exports.getFirefoxSubmissionEvent = __webpack_exports__.getFirefoxSubmissionEvent;
|
|
1527
|
+
exports.main = __webpack_exports__.main;
|
|
948
1528
|
exports.resolveConfig = __webpack_exports__.resolveConfig;
|
|
949
1529
|
exports.validateConfig = __webpack_exports__.validateConfig;
|
|
950
1530
|
exports.verifyChromeCredentials = __webpack_exports__.verifyChromeCredentials;
|
|
@@ -962,6 +1542,7 @@ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
|
|
|
962
1542
|
"getChromeSubmissionEvent",
|
|
963
1543
|
"getEdgeSubmissionEvent",
|
|
964
1544
|
"getFirefoxSubmissionEvent",
|
|
1545
|
+
"main",
|
|
965
1546
|
"resolveConfig",
|
|
966
1547
|
"validateConfig",
|
|
967
1548
|
"verifyChromeCredentials",
|