@extension.dev/deploy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/bin/extension-deploy.cjs +3 -0
- package/dist/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/deploy.test.d.ts +2 -0
- package/dist/__tests__/deploy.test.d.ts.map +1 -0
- package/dist/__tests__/dry-run.test.d.ts +2 -0
- package/dist/__tests__/dry-run.test.d.ts.map +1 -0
- package/dist/__tests__/types.test.d.ts +2 -0
- package/dist/__tests__/types.test.d.ts.map +1 -0
- package/dist/__tests__/utils.test.d.ts +2 -0
- package/dist/__tests__/utils.test.d.ts.map +1 -0
- package/dist/__tests__/verify.test.d.ts +2 -0
- package/dist/__tests__/verify.test.d.ts.map +1 -0
- package/dist/__tests__/watch.test.d.ts +2 -0
- package/dist/__tests__/watch.test.d.ts.map +1 -0
- package/dist/module.d.ts +2 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +976 -0
- package/dist/module.mjs +886 -0
- package/dist/src/chrome.d.ts +42 -0
- package/dist/src/chrome.d.ts.map +1 -0
- package/dist/src/cli.d.ts +4 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/config.d.ts +68 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/deploy.d.ts +3 -0
- package/dist/src/deploy.d.ts.map +1 -0
- package/dist/src/edge.d.ts +39 -0
- package/dist/src/edge.d.ts.map +1 -0
- package/dist/src/firefox.d.ts +56 -0
- package/dist/src/firefox.d.ts.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/types.d.ts +242 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils/async.d.ts +2 -0
- package/dist/src/utils/async.d.ts.map +1 -0
- package/dist/src/utils/fetch.d.ts +3 -0
- package/dist/src/utils/fetch.d.ts.map +1 -0
- package/dist/src/utils/fs.d.ts +4 -0
- package/dist/src/utils/fs.d.ts.map +1 -0
- package/dist/src/utils/log.d.ts +6 -0
- package/dist/src/utils/log.d.ts.map +1 -0
- package/dist/src/watch.d.ts +41 -0
- package/dist/src/watch.d.ts.map +1 -0
- package/package.json +78 -0
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import promises_default from "node:fs/promises";
|
|
3
|
+
import external_node_crypto_default from "node:crypto";
|
|
4
|
+
const chromeOptionsSchema = z.object({
|
|
5
|
+
zip: z.string().min(1),
|
|
6
|
+
extensionId: z.string().min(1).trim(),
|
|
7
|
+
clientId: z.string().min(1).trim(),
|
|
8
|
+
clientSecret: z.string().min(1).trim(),
|
|
9
|
+
refreshToken: z.string().min(1).trim(),
|
|
10
|
+
publishTarget: z["enum"]([
|
|
11
|
+
"default",
|
|
12
|
+
"trustedTesters"
|
|
13
|
+
]).default("default"),
|
|
14
|
+
deployPercentage: z.number().int().min(1).max(100).optional(),
|
|
15
|
+
reviewExemption: z.boolean().default(false),
|
|
16
|
+
skipSubmitReview: z.boolean().default(false)
|
|
17
|
+
});
|
|
18
|
+
const firefoxOptionsSchema = z.object({
|
|
19
|
+
zip: z.string().min(1),
|
|
20
|
+
sourcesZip: z.string().min(1).optional(),
|
|
21
|
+
extensionId: z.string().min(1).trim(),
|
|
22
|
+
jwtIssuer: z.string().min(1).trim(),
|
|
23
|
+
jwtSecret: z.string().min(1).trim(),
|
|
24
|
+
channel: z["enum"]([
|
|
25
|
+
"listed",
|
|
26
|
+
"unlisted"
|
|
27
|
+
]).default("listed")
|
|
28
|
+
});
|
|
29
|
+
const edgeOptionsSchema = z.object({
|
|
30
|
+
zip: z.string().min(1),
|
|
31
|
+
productId: z.string().min(1).trim(),
|
|
32
|
+
clientId: z.string().min(1).trim(),
|
|
33
|
+
apiKey: z.string().min(1).trim(),
|
|
34
|
+
skipSubmitReview: z.boolean().default(false)
|
|
35
|
+
});
|
|
36
|
+
const deployConfigSchema = z.object({
|
|
37
|
+
dryRun: z.boolean().default(false),
|
|
38
|
+
chrome: chromeOptionsSchema.optional(),
|
|
39
|
+
firefox: firefoxOptionsSchema.optional(),
|
|
40
|
+
edge: edgeOptionsSchema.optional()
|
|
41
|
+
});
|
|
42
|
+
function resolveConfig(flags) {
|
|
43
|
+
const dryRun = flags.dryRun ?? envBool("DRY_RUN") ?? false;
|
|
44
|
+
const chromeZip = flags.chromeZip ?? envStr("CHROME_ZIP");
|
|
45
|
+
const firefoxZip = flags.firefoxZip ?? envStr("FIREFOX_ZIP");
|
|
46
|
+
const edgeZip = flags.edgeZip ?? envStr("EDGE_ZIP");
|
|
47
|
+
return {
|
|
48
|
+
dryRun,
|
|
49
|
+
chrome: null == chromeZip ? void 0 : {
|
|
50
|
+
zip: chromeZip,
|
|
51
|
+
extensionId: flags.chromeExtensionId ?? envStr("CHROME_EXTENSION_ID") ?? "",
|
|
52
|
+
clientId: flags.chromeClientId ?? envStr("CHROME_CLIENT_ID") ?? "",
|
|
53
|
+
clientSecret: flags.chromeClientSecret ?? envStr("CHROME_CLIENT_SECRET") ?? "",
|
|
54
|
+
refreshToken: flags.chromeRefreshToken ?? envStr("CHROME_REFRESH_TOKEN") ?? "",
|
|
55
|
+
publishTarget: flags.chromePublishTarget ?? envStr("CHROME_PUBLISH_TARGET"),
|
|
56
|
+
deployPercentage: flags.chromeDeployPercentage ?? envInt("CHROME_DEPLOY_PERCENTAGE"),
|
|
57
|
+
reviewExemption: flags.chromeReviewExemption ?? envBool("CHROME_REVIEW_EXEMPTION"),
|
|
58
|
+
skipSubmitReview: flags.chromeSkipSubmitReview ?? envBool("CHROME_SKIP_SUBMIT_REVIEW")
|
|
59
|
+
},
|
|
60
|
+
firefox: null == firefoxZip ? void 0 : {
|
|
61
|
+
zip: firefoxZip,
|
|
62
|
+
sourcesZip: flags.firefoxSourcesZip ?? envStr("FIREFOX_SOURCES_ZIP"),
|
|
63
|
+
extensionId: flags.firefoxExtensionId ?? envStr("FIREFOX_EXTENSION_ID") ?? "",
|
|
64
|
+
jwtIssuer: flags.firefoxJwtIssuer ?? envStr("FIREFOX_JWT_ISSUER") ?? "",
|
|
65
|
+
jwtSecret: flags.firefoxJwtSecret ?? envStr("FIREFOX_JWT_SECRET") ?? "",
|
|
66
|
+
channel: flags.firefoxChannel ?? envStr("FIREFOX_CHANNEL")
|
|
67
|
+
},
|
|
68
|
+
edge: null == edgeZip ? void 0 : {
|
|
69
|
+
zip: edgeZip,
|
|
70
|
+
productId: flags.edgeProductId ?? envStr("EDGE_PRODUCT_ID") ?? "",
|
|
71
|
+
clientId: flags.edgeClientId ?? envStr("EDGE_CLIENT_ID") ?? "",
|
|
72
|
+
apiKey: flags.edgeApiKey ?? envStr("EDGE_API_KEY") ?? "",
|
|
73
|
+
skipSubmitReview: flags.edgeSkipSubmitReview ?? envBool("EDGE_SKIP_SUBMIT_REVIEW")
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function pathToEnvName(segments) {
|
|
78
|
+
return segments.map((s)=>String(s).replace(/[A-Z]/g, (ch)=>`_${ch}`).toUpperCase()).join("_");
|
|
79
|
+
}
|
|
80
|
+
const SETUP_HINTS = {
|
|
81
|
+
CHROME_EXTENSION_ID: "Find this in the Chrome Web Store Developer Dashboard URL for your extension.",
|
|
82
|
+
CHROME_CLIENT_ID: "Create OAuth 2.0 credentials at https://console.cloud.google.com/apis/credentials",
|
|
83
|
+
CHROME_CLIENT_SECRET: "Generated alongside the Client ID in the Google Cloud Console.",
|
|
84
|
+
CHROME_REFRESH_TOKEN: "Obtain by completing the OAuth consent flow. See the Chrome Web Store API docs.",
|
|
85
|
+
CHROME_ZIP: "Path to the .zip file built for Chrome Web Store upload.",
|
|
86
|
+
FIREFOX_EXTENSION_ID: "The addon GUID ({uuid} format) or email-style ID from your addon's AMO page.",
|
|
87
|
+
FIREFOX_JWT_ISSUER: "API key from https://addons.mozilla.org/developers/addon/api/key/",
|
|
88
|
+
FIREFOX_JWT_SECRET: "API secret generated alongside the JWT issuer on AMO.",
|
|
89
|
+
FIREFOX_ZIP: "Path to the .zip file built for Firefox AMO upload.",
|
|
90
|
+
EDGE_PRODUCT_ID: "Product ID from the Edge Partner Center dashboard.",
|
|
91
|
+
EDGE_CLIENT_ID: "Client ID from the Edge Partner Center API settings.",
|
|
92
|
+
EDGE_API_KEY: "API key (v1.1) from https://partner.microsoft.com/dashboard/microsoftedge/publishapi",
|
|
93
|
+
EDGE_ZIP: "Path to the .zip file built for Edge Partner Center upload."
|
|
94
|
+
};
|
|
95
|
+
function validateConfig(raw) {
|
|
96
|
+
const result = deployConfigSchema.safeParse(raw);
|
|
97
|
+
if (!result.success) {
|
|
98
|
+
const lines = result.error.issues.map((issue)=>{
|
|
99
|
+
const envName = pathToEnvName(issue.path);
|
|
100
|
+
const hint = SETUP_HINTS[envName];
|
|
101
|
+
const base = ` - ${envName}: ${issue.message}`;
|
|
102
|
+
return hint ? `${base}\n ${hint}` : base;
|
|
103
|
+
});
|
|
104
|
+
throw new Error([
|
|
105
|
+
"Missing required config. Set via CLI flags or environment variables:",
|
|
106
|
+
...lines,
|
|
107
|
+
"",
|
|
108
|
+
"Tip: create a .env.submit file in your project root, or pass --help for all flags."
|
|
109
|
+
].join("\n"));
|
|
110
|
+
}
|
|
111
|
+
return result.data;
|
|
112
|
+
}
|
|
113
|
+
function envBool(name) {
|
|
114
|
+
const val = process.env[name];
|
|
115
|
+
return null == val ? void 0 : "true" === val;
|
|
116
|
+
}
|
|
117
|
+
function envStr(name) {
|
|
118
|
+
const val = process.env[name];
|
|
119
|
+
return val || void 0;
|
|
120
|
+
}
|
|
121
|
+
function envInt(name) {
|
|
122
|
+
const val = process.env[name];
|
|
123
|
+
return null == val ? void 0 : parseInt(val, 10);
|
|
124
|
+
}
|
|
125
|
+
async function fetchJson(url, init) {
|
|
126
|
+
const res = await fetch(url, init);
|
|
127
|
+
if (!res.ok) {
|
|
128
|
+
const body = await res.text().catch(()=>"");
|
|
129
|
+
throw new Error(`HTTP ${res.status} ${res.statusText} \u{2014} ${(null == init ? void 0 : init.method) ?? "GET"} ${url}\n${body}`);
|
|
130
|
+
}
|
|
131
|
+
return res.json();
|
|
132
|
+
}
|
|
133
|
+
async function fetchRaw(url, init) {
|
|
134
|
+
const res = await fetch(url, init);
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
const body = await res.text().catch(()=>"");
|
|
137
|
+
throw new Error(`HTTP ${res.status} ${res.statusText} \u{2014} ${(null == init ? void 0 : init.method) ?? "GET"} ${url}\n${body}`);
|
|
138
|
+
}
|
|
139
|
+
return res;
|
|
140
|
+
}
|
|
141
|
+
async function assertFilePresent(filePath) {
|
|
142
|
+
try {
|
|
143
|
+
await promises_default.access(filePath);
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error(`File not found: ${filePath}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function getFileSize(filePath) {
|
|
149
|
+
const stat = await promises_default.stat(filePath);
|
|
150
|
+
return stat.size;
|
|
151
|
+
}
|
|
152
|
+
function formatBytes(bytes) {
|
|
153
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
154
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
155
|
+
return `${(bytes / 1048576).toFixed(2)} MB`;
|
|
156
|
+
}
|
|
157
|
+
function log(store, message) {
|
|
158
|
+
const tag = `[${store}]`;
|
|
159
|
+
console.log(`${tag} ${message}`);
|
|
160
|
+
}
|
|
161
|
+
function logDryStep(store, step, details) {
|
|
162
|
+
const tag = `[${store}] DRY RUN`;
|
|
163
|
+
console.log(`${tag} \u{2192} ${step}`);
|
|
164
|
+
if (details) {
|
|
165
|
+
for (const [key, value] of Object.entries(details))if (null != value) console.log(`${tag} ${key}: ${value}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const CWS_OAUTH_URL = "https://oauth2.googleapis.com/token";
|
|
169
|
+
const CWS_UPLOAD_BASE = "https://www.googleapis.com/upload/chromewebstore/v1.1/items";
|
|
170
|
+
const CWS_PUBLISH_BASE = "https://www.googleapis.com/chromewebstore/v1.1/items";
|
|
171
|
+
const CWS_ITEMS_BASE = "https://www.googleapis.com/chromewebstore/v1.1/items";
|
|
172
|
+
function chromeStoreUrl(extensionId) {
|
|
173
|
+
return `https://chromewebstore.google.com/detail/${extensionId}`;
|
|
174
|
+
}
|
|
175
|
+
async function authenticate(clientId, clientSecret, refreshToken) {
|
|
176
|
+
return fetchJson(CWS_OAUTH_URL, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
client_id: clientId,
|
|
180
|
+
client_secret: clientSecret,
|
|
181
|
+
refresh_token: refreshToken,
|
|
182
|
+
grant_type: "refresh_token",
|
|
183
|
+
redirect_uri: "urn:ietf:wg:oauth:2.0:oob"
|
|
184
|
+
}),
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": "application/json"
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async function chrome_upload(extensionId, zipPath, token) {
|
|
191
|
+
const body = await promises_default.readFile(zipPath);
|
|
192
|
+
const res = await fetch(`${CWS_UPLOAD_BASE}/${extensionId}`, {
|
|
193
|
+
method: "PUT",
|
|
194
|
+
body,
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `${token.token_type} ${token.access_token}`,
|
|
197
|
+
"x-goog-api-version": "2",
|
|
198
|
+
"Content-Type": "application/zip"
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
if (!res.ok) {
|
|
202
|
+
const text = await res.text().catch(()=>"");
|
|
203
|
+
throw new Error(`Chrome upload failed: ${res.status} ${res.statusText}\n${text}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function publish(params) {
|
|
207
|
+
const url = new URL(`${CWS_PUBLISH_BASE}/${params.extensionId}/publish`);
|
|
208
|
+
url.searchParams.set("publishTarget", params.publishTarget);
|
|
209
|
+
if (null != params.deployPercentage) url.searchParams.set("deployPercentage", String(params.deployPercentage));
|
|
210
|
+
if (null != params.reviewExemption) url.searchParams.set("reviewExemption", String(params.reviewExemption));
|
|
211
|
+
const res = await fetch(url.href, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: {
|
|
214
|
+
Authorization: `${params.token.token_type} ${params.token.access_token}`,
|
|
215
|
+
"x-goog-api-version": "2",
|
|
216
|
+
"Content-Length": "0"
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
if (!res.ok) {
|
|
220
|
+
const text = await res.text().catch(()=>"");
|
|
221
|
+
throw new Error(`Chrome publish failed: ${res.status} ${res.statusText}\n${text}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function publishChrome(options, dryRun) {
|
|
225
|
+
await assertFilePresent(options.zip);
|
|
226
|
+
const zipSize = await getFileSize(options.zip);
|
|
227
|
+
log("chrome", "Authenticating with Chrome Web Store");
|
|
228
|
+
const token = await authenticate(options.clientId, options.clientSecret, options.refreshToken);
|
|
229
|
+
const outcome = {
|
|
230
|
+
storeUrl: chromeStoreUrl(options.extensionId),
|
|
231
|
+
submissionId: options.extensionId
|
|
232
|
+
};
|
|
233
|
+
if (dryRun) {
|
|
234
|
+
logDryStep("chrome", "Authentication succeeded", {
|
|
235
|
+
tokenType: token.token_type,
|
|
236
|
+
scope: "chromewebstore"
|
|
237
|
+
});
|
|
238
|
+
logDryStep("chrome", "Would upload ZIP", {
|
|
239
|
+
file: options.zip,
|
|
240
|
+
size: formatBytes(zipSize),
|
|
241
|
+
target: `${CWS_UPLOAD_BASE}/${options.extensionId}`,
|
|
242
|
+
method: "PUT"
|
|
243
|
+
});
|
|
244
|
+
options.skipSubmitReview ? logDryStep("chrome", "Would skip publish (skipSubmitReview=true)") : logDryStep("chrome", "Would publish for review", {
|
|
245
|
+
target: `${CWS_PUBLISH_BASE}/${options.extensionId}/publish`,
|
|
246
|
+
publishTarget: options.publishTarget ?? "default",
|
|
247
|
+
deployPercentage: options.deployPercentage,
|
|
248
|
+
reviewExemption: options.reviewExemption
|
|
249
|
+
});
|
|
250
|
+
log("chrome", "Dry run complete \u2014 no changes made");
|
|
251
|
+
return outcome;
|
|
252
|
+
}
|
|
253
|
+
log("chrome", `Uploading ZIP file (${formatBytes(zipSize)})`);
|
|
254
|
+
await chrome_upload(options.extensionId, options.zip, token);
|
|
255
|
+
if (options.skipSubmitReview) {
|
|
256
|
+
log("chrome", "Skipping publish step (skipSubmitReview=true)");
|
|
257
|
+
return outcome;
|
|
258
|
+
}
|
|
259
|
+
log("chrome", "Publishing extension");
|
|
260
|
+
await publish({
|
|
261
|
+
extensionId: options.extensionId,
|
|
262
|
+
publishTarget: options.publishTarget,
|
|
263
|
+
token,
|
|
264
|
+
deployPercentage: options.deployPercentage,
|
|
265
|
+
reviewExemption: options.reviewExemption
|
|
266
|
+
});
|
|
267
|
+
return outcome;
|
|
268
|
+
}
|
|
269
|
+
async function getChromeItem(params) {
|
|
270
|
+
const token = await authenticate(params.clientId, params.clientSecret, params.refreshToken);
|
|
271
|
+
const url = `${CWS_ITEMS_BASE}/${encodeURIComponent(params.extensionId)}?projection=${params.projection ?? "DRAFT"}`;
|
|
272
|
+
return fetchJson(url, {
|
|
273
|
+
headers: {
|
|
274
|
+
Authorization: `${token.token_type} ${token.access_token}`,
|
|
275
|
+
"x-goog-api-version": "2"
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
async function verifyChromeCredentials(params) {
|
|
280
|
+
let token;
|
|
281
|
+
try {
|
|
282
|
+
token = await authenticate(params.clientId, params.clientSecret, params.refreshToken);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
message: `OAuth token exchange failed. Verify your Client ID, Client Secret, and Refresh Token are correct.\n${err instanceof Error ? err.message : err}`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const item = await fetchJson(`${CWS_ITEMS_BASE}/${encodeURIComponent(params.extensionId)}?projection=DRAFT`, {
|
|
291
|
+
headers: {
|
|
292
|
+
Authorization: `${token.token_type} ${token.access_token}`
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
const title = (null == item ? void 0 : item.title) || params.extensionId;
|
|
296
|
+
return {
|
|
297
|
+
ok: true,
|
|
298
|
+
message: `Credentials verified. You have access to "${title}".`,
|
|
299
|
+
extensionTitle: title
|
|
300
|
+
};
|
|
301
|
+
} catch (err) {
|
|
302
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
303
|
+
if (msg.includes("404")) return {
|
|
304
|
+
ok: false,
|
|
305
|
+
message: "Extension ID not found. Verify the ID is correct and belongs to your account."
|
|
306
|
+
};
|
|
307
|
+
if (msg.includes("403")) return {
|
|
308
|
+
ok: false,
|
|
309
|
+
message: `Access denied to extension ${params.extensionId}. You may not be the owner or a developer on this item.`
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
message: `Chrome Web Store API check failed: ${msg}`
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function delay(ms) {
|
|
318
|
+
return new Promise((resolve)=>{
|
|
319
|
+
setTimeout(resolve, ms);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const AMO_BASE = "https://addons.mozilla.org/api/v5/addons";
|
|
323
|
+
const AMO_PROFILE_URL = "https://addons.mozilla.org/api/v5/accounts/profile/";
|
|
324
|
+
const POLL_INTERVAL_MS = 5000;
|
|
325
|
+
const VALIDATION_TIMEOUT_MS = 600000;
|
|
326
|
+
function firefoxStoreUrl(addonIdOrSlug) {
|
|
327
|
+
return `https://addons.mozilla.org/firefox/addon/${addonIdOrSlug}`;
|
|
328
|
+
}
|
|
329
|
+
function signJwt(issuer, secret, expiresInSeconds = 30) {
|
|
330
|
+
const header = {
|
|
331
|
+
alg: "HS256",
|
|
332
|
+
typ: "JWT"
|
|
333
|
+
};
|
|
334
|
+
const now = Math.floor(Date.now() / 1000);
|
|
335
|
+
const payload = {
|
|
336
|
+
iss: issuer,
|
|
337
|
+
jti: Math.random().toString(),
|
|
338
|
+
iat: now,
|
|
339
|
+
exp: now + expiresInSeconds
|
|
340
|
+
};
|
|
341
|
+
const b64 = (obj)=>Buffer.from(JSON.stringify(obj)).toString("base64url");
|
|
342
|
+
const segments = `${b64(header)}.${b64(payload)}`;
|
|
343
|
+
const sig = external_node_crypto_default.createHmac("sha256", secret).update(segments).digest("base64url");
|
|
344
|
+
return `${segments}.${sig}`;
|
|
345
|
+
}
|
|
346
|
+
function authHeaders(jwtIssuer, jwtSecret) {
|
|
347
|
+
return {
|
|
348
|
+
Authorization: `JWT ${signJwt(jwtIssuer, jwtSecret)}`
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function getAddon(addonId, jwtIssuer, jwtSecret) {
|
|
352
|
+
return fetchJson(`${AMO_BASE}/addon/${addonId}`, {
|
|
353
|
+
headers: authHeaders(jwtIssuer, jwtSecret)
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async function createUpload(zipPath, channel, jwtIssuer, jwtSecret) {
|
|
357
|
+
const buf = await promises_default.readFile(zipPath);
|
|
358
|
+
const blob = new Blob([
|
|
359
|
+
buf
|
|
360
|
+
], {
|
|
361
|
+
type: "application/zip"
|
|
362
|
+
});
|
|
363
|
+
const form = new FormData();
|
|
364
|
+
form.set("channel", channel);
|
|
365
|
+
form.set("upload", blob, "extension.zip");
|
|
366
|
+
return fetchJson(`${AMO_BASE}/upload/`, {
|
|
367
|
+
method: "POST",
|
|
368
|
+
body: form,
|
|
369
|
+
headers: authHeaders(jwtIssuer, jwtSecret)
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function getUpload(uuid, jwtIssuer, jwtSecret) {
|
|
373
|
+
return fetchJson(`${AMO_BASE}/upload/${uuid}`, {
|
|
374
|
+
headers: authHeaders(jwtIssuer, jwtSecret)
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
async function createVersion(params) {
|
|
378
|
+
const form = new FormData();
|
|
379
|
+
form.set("upload", params.uploadUuid);
|
|
380
|
+
if (params.sourcesZip) {
|
|
381
|
+
const srcBuf = await promises_default.readFile(params.sourcesZip);
|
|
382
|
+
const srcBlob = new Blob([
|
|
383
|
+
srcBuf
|
|
384
|
+
], {
|
|
385
|
+
type: "application/zip"
|
|
386
|
+
});
|
|
387
|
+
form.set("source", srcBlob, "sources.zip");
|
|
388
|
+
} else form.set("source", "");
|
|
389
|
+
return fetchJson(`${AMO_BASE}/addon/${params.addonId}/versions/`, {
|
|
390
|
+
method: "POST",
|
|
391
|
+
body: form,
|
|
392
|
+
headers: authHeaders(params.jwtIssuer, params.jwtSecret)
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async function publishFirefox(options, dryRun) {
|
|
396
|
+
await assertFilePresent(options.zip);
|
|
397
|
+
const zipSize = await getFileSize(options.zip);
|
|
398
|
+
let sourcesSize;
|
|
399
|
+
if (options.sourcesZip) {
|
|
400
|
+
await assertFilePresent(options.sourcesZip);
|
|
401
|
+
sourcesSize = await getFileSize(options.sourcesZip);
|
|
402
|
+
}
|
|
403
|
+
const addonId = normalizeAddonId(options.extensionId);
|
|
404
|
+
const { jwtIssuer, jwtSecret } = options;
|
|
405
|
+
log("firefox", "Verifying addon exists");
|
|
406
|
+
await getAddon(addonId, jwtIssuer, jwtSecret);
|
|
407
|
+
const outcome = {
|
|
408
|
+
storeUrl: firefoxStoreUrl(addonId),
|
|
409
|
+
submissionId: addonId
|
|
410
|
+
};
|
|
411
|
+
if (dryRun) {
|
|
412
|
+
logDryStep("firefox", "Addon verified", {
|
|
413
|
+
addonId
|
|
414
|
+
});
|
|
415
|
+
logDryStep("firefox", "Would upload ZIP", {
|
|
416
|
+
file: options.zip,
|
|
417
|
+
size: formatBytes(zipSize),
|
|
418
|
+
channel: options.channel ?? "listed",
|
|
419
|
+
target: `${AMO_BASE}/upload/`,
|
|
420
|
+
method: "POST"
|
|
421
|
+
});
|
|
422
|
+
logDryStep("firefox", "Would poll for validation", {
|
|
423
|
+
interval: `${POLL_INTERVAL_MS / 1000}s`,
|
|
424
|
+
timeout: `${VALIDATION_TIMEOUT_MS / 60000}min`
|
|
425
|
+
});
|
|
426
|
+
logDryStep("firefox", "Would create new version", {
|
|
427
|
+
target: `${AMO_BASE}/addon/${addonId}/versions/`,
|
|
428
|
+
method: "POST",
|
|
429
|
+
sourcesZip: options.sourcesZip ? `${options.sourcesZip} (${formatBytes(sourcesSize)})` : "(none)"
|
|
430
|
+
});
|
|
431
|
+
log("firefox", "Dry run complete \u2014 no changes made");
|
|
432
|
+
return outcome;
|
|
433
|
+
}
|
|
434
|
+
const upload = await uploadAndAwaitProcessing(options, jwtIssuer, jwtSecret);
|
|
435
|
+
outcome.submissionId = upload.uuid;
|
|
436
|
+
log("firefox", "Creating new version");
|
|
437
|
+
const version = await createVersion({
|
|
438
|
+
addonId,
|
|
439
|
+
sourcesZip: options.sourcesZip,
|
|
440
|
+
uploadUuid: upload.uuid,
|
|
441
|
+
jwtIssuer,
|
|
442
|
+
jwtSecret
|
|
443
|
+
});
|
|
444
|
+
outcome.submissionId = String(version.id);
|
|
445
|
+
const { errors, warnings, notices } = upload.validation;
|
|
446
|
+
log("firefox", `Validation: ${errors} error(s), ${warnings} warning(s), ${notices} notice(s)`);
|
|
447
|
+
if (!upload.valid) throw new Error("Firefox extension failed validation");
|
|
448
|
+
return outcome;
|
|
449
|
+
}
|
|
450
|
+
function getFirefoxVersion(params) {
|
|
451
|
+
const normalized = normalizeAddonId(params.addonId);
|
|
452
|
+
return fetchJson(`${AMO_BASE}/addon/${normalized}/versions/${params.versionId}/`, {
|
|
453
|
+
headers: authHeaders(params.jwtIssuer, params.jwtSecret)
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
async function uploadAndAwaitProcessing(options, jwtIssuer, jwtSecret) {
|
|
457
|
+
const size = await getFileSize(options.zip);
|
|
458
|
+
log("firefox", `Uploading ZIP file (${formatBytes(size)})`);
|
|
459
|
+
let result = await createUpload(options.zip, options.channel, jwtIssuer, jwtSecret);
|
|
460
|
+
log("firefox", "Waiting for validation");
|
|
461
|
+
const deadline = Date.now() + VALIDATION_TIMEOUT_MS;
|
|
462
|
+
while(!result.processed){
|
|
463
|
+
if (Date.now() > deadline) throw new Error(`Firefox validation timed out after ${VALIDATION_TIMEOUT_MS}ms`);
|
|
464
|
+
await delay(POLL_INTERVAL_MS);
|
|
465
|
+
result = await getUpload(result.uuid, jwtIssuer, jwtSecret);
|
|
466
|
+
}
|
|
467
|
+
return result;
|
|
468
|
+
}
|
|
469
|
+
function normalizeAddonId(id) {
|
|
470
|
+
if (id.startsWith("{") && id.endsWith("}")) return id.slice(1, -1);
|
|
471
|
+
return id;
|
|
472
|
+
}
|
|
473
|
+
async function verifyFirefoxCredentials(params) {
|
|
474
|
+
try {
|
|
475
|
+
const jwt = signJwt(params.jwtIssuer, params.jwtSecret, 60);
|
|
476
|
+
await fetchJson(AMO_PROFILE_URL, {
|
|
477
|
+
headers: {
|
|
478
|
+
Authorization: `JWT ${jwt}`
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
} catch (err) {
|
|
482
|
+
return {
|
|
483
|
+
ok: false,
|
|
484
|
+
message: `AMO API credential check failed. Verify your JWT Issuer and Secret are correct.\n${err instanceof Error ? err.message : err}`
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (params.addonId) {
|
|
488
|
+
const normalizedId = normalizeAddonId(params.addonId);
|
|
489
|
+
try {
|
|
490
|
+
const addon = await getAddon(normalizedId, params.jwtIssuer, params.jwtSecret);
|
|
491
|
+
const name = addon.name ?? params.addonId;
|
|
492
|
+
const displayName = "object" == typeof name && null !== name ? name.en_US ?? params.addonId : String(name);
|
|
493
|
+
return {
|
|
494
|
+
ok: true,
|
|
495
|
+
message: `API credentials and add-on ownership verified. You have access to "${displayName}".`,
|
|
496
|
+
addonName: displayName
|
|
497
|
+
};
|
|
498
|
+
} catch (err) {
|
|
499
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
500
|
+
if (msg.includes("404")) return {
|
|
501
|
+
ok: false,
|
|
502
|
+
message: "Add-on GUID not found. Verify the GUID is correct in your add-on's Developer Hub page."
|
|
503
|
+
};
|
|
504
|
+
if (msg.includes("403") || msg.includes("401")) return {
|
|
505
|
+
ok: false,
|
|
506
|
+
message: "You don't have permission to access this add-on. Verify you're using credentials for the correct account."
|
|
507
|
+
};
|
|
508
|
+
return {
|
|
509
|
+
ok: false,
|
|
510
|
+
message: `Failed to verify add-on ownership: ${msg}`
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
ok: true,
|
|
516
|
+
message: "API credentials verified. Provide an addon GUID to also verify add-on ownership."
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const EDGE_API_BASE = "https://api.addons.microsoftedge.microsoft.com/v1/products";
|
|
520
|
+
const edge_POLL_INTERVAL_MS = 5000;
|
|
521
|
+
const edge_VALIDATION_TIMEOUT_MS = 600000;
|
|
522
|
+
function edgeStoreUrl(productId) {
|
|
523
|
+
return `https://microsoftedge.microsoft.com/addons/detail/${productId}`;
|
|
524
|
+
}
|
|
525
|
+
function partnerHeaders(clientId, apiKey) {
|
|
526
|
+
return {
|
|
527
|
+
Authorization: `ApiKey ${apiKey}`,
|
|
528
|
+
"X-ClientID": clientId
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
async function uploadDraft(productId, zipPath, clientId, apiKey) {
|
|
532
|
+
const body = await promises_default.readFile(zipPath);
|
|
533
|
+
const res = await fetchRaw(`${EDGE_API_BASE}/${productId}/submissions/draft/package`, {
|
|
534
|
+
method: "POST",
|
|
535
|
+
body,
|
|
536
|
+
headers: {
|
|
537
|
+
...partnerHeaders(clientId, apiKey),
|
|
538
|
+
"Content-Type": "application/zip"
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
const operationId = res.headers.get("Location");
|
|
542
|
+
if (!operationId) throw new Error("Edge API did not return an operation ID in the Location header.");
|
|
543
|
+
return operationId;
|
|
544
|
+
}
|
|
545
|
+
function getDraftOperation(productId, operationId, clientId, apiKey) {
|
|
546
|
+
return fetchJson(`${EDGE_API_BASE}/${productId}/submissions/draft/package/operations/${operationId}`, {
|
|
547
|
+
headers: partnerHeaders(clientId, apiKey)
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
async function submitForReview(productId, clientId, apiKey) {
|
|
551
|
+
const res = await fetchRaw(`${EDGE_API_BASE}/${productId}/submissions`, {
|
|
552
|
+
method: "POST",
|
|
553
|
+
body: JSON.stringify({}),
|
|
554
|
+
headers: {
|
|
555
|
+
...partnerHeaders(clientId, apiKey),
|
|
556
|
+
"Content-Type": "application/json"
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
const location = res.headers.get("Location");
|
|
560
|
+
if (!location) return;
|
|
561
|
+
const segments = location.split("/").filter(Boolean);
|
|
562
|
+
return segments[segments.length - 1];
|
|
563
|
+
}
|
|
564
|
+
async function publishEdge(options, dryRun) {
|
|
565
|
+
await assertFilePresent(options.zip);
|
|
566
|
+
const zipSize = await getFileSize(options.zip);
|
|
567
|
+
const outcome = {
|
|
568
|
+
storeUrl: edgeStoreUrl(options.productId),
|
|
569
|
+
submissionId: options.productId
|
|
570
|
+
};
|
|
571
|
+
if (dryRun) {
|
|
572
|
+
logDryStep("edge", "Credentials assumed valid (no preflight endpoint)", {
|
|
573
|
+
productId: options.productId,
|
|
574
|
+
clientId: options.clientId
|
|
575
|
+
});
|
|
576
|
+
logDryStep("edge", "Would upload draft ZIP", {
|
|
577
|
+
file: options.zip,
|
|
578
|
+
size: formatBytes(zipSize),
|
|
579
|
+
target: `${EDGE_API_BASE}/${options.productId}/submissions/draft/package`,
|
|
580
|
+
method: "POST"
|
|
581
|
+
});
|
|
582
|
+
logDryStep("edge", "Would poll draft operation", {
|
|
583
|
+
interval: `${edge_POLL_INTERVAL_MS / 1000}s`,
|
|
584
|
+
timeout: `${edge_VALIDATION_TIMEOUT_MS / 60000}min`
|
|
585
|
+
});
|
|
586
|
+
options.skipSubmitReview ? logDryStep("edge", "Would skip submit (skipSubmitReview=true)") : logDryStep("edge", "Would submit for review", {
|
|
587
|
+
target: `${EDGE_API_BASE}/${options.productId}/submissions`,
|
|
588
|
+
method: "POST"
|
|
589
|
+
});
|
|
590
|
+
log("edge", "Dry run complete \u2014 no changes made");
|
|
591
|
+
return outcome;
|
|
592
|
+
}
|
|
593
|
+
const uploadOperationId = await edge_uploadAndAwaitProcessing(options);
|
|
594
|
+
outcome.submissionId = uploadOperationId;
|
|
595
|
+
if (options.skipSubmitReview) {
|
|
596
|
+
log("edge", "Skipping publish step (skipSubmitReview=true)");
|
|
597
|
+
return outcome;
|
|
598
|
+
}
|
|
599
|
+
log("edge", "Submitting for review");
|
|
600
|
+
const publishOperationId = await submitForReview(options.productId, options.clientId, options.apiKey);
|
|
601
|
+
if (publishOperationId) outcome.submissionId = publishOperationId;
|
|
602
|
+
return outcome;
|
|
603
|
+
}
|
|
604
|
+
function getEdgePublishOperation(params) {
|
|
605
|
+
return fetchJson(`${EDGE_API_BASE}/${params.productId}/submissions/operations/${params.operationId}`, {
|
|
606
|
+
headers: partnerHeaders(params.clientId, params.apiKey)
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
async function verifyEdgeCredentials(params) {
|
|
610
|
+
const headers = partnerHeaders(params.clientId, params.apiKey);
|
|
611
|
+
if (!params.productId) {
|
|
612
|
+
const res = await fetch(EDGE_API_BASE, {
|
|
613
|
+
headers
|
|
614
|
+
});
|
|
615
|
+
if (401 === res.status || 403 === res.status) return {
|
|
616
|
+
ok: false,
|
|
617
|
+
message: "Invalid credentials. Verify your Client ID and API Key are correct."
|
|
618
|
+
};
|
|
619
|
+
if (!res.ok) return {
|
|
620
|
+
ok: false,
|
|
621
|
+
message: `Edge Partner API check failed (HTTP ${res.status}).`
|
|
622
|
+
};
|
|
623
|
+
return {
|
|
624
|
+
ok: true,
|
|
625
|
+
message: "Credentials verified. Provide a Product ID to also verify extension ownership."
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const res = await fetch(`${EDGE_API_BASE}/${encodeURIComponent(params.productId)}/submissions/draft`, {
|
|
629
|
+
headers
|
|
630
|
+
});
|
|
631
|
+
if (401 === res.status) return {
|
|
632
|
+
ok: false,
|
|
633
|
+
message: "Unauthorized. Verify your API credentials are correct."
|
|
634
|
+
};
|
|
635
|
+
if (403 === res.status) {
|
|
636
|
+
const body = await res.text().catch(()=>"");
|
|
637
|
+
if (/product not found|not found/i.test(body)) return {
|
|
638
|
+
ok: false,
|
|
639
|
+
message: "Product ID not found or not owned by this account. Verify the GUID matches your Partner Center extension."
|
|
640
|
+
};
|
|
641
|
+
return {
|
|
642
|
+
ok: false,
|
|
643
|
+
message: "Invalid credentials. Verify your Client ID and API Key are correct."
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
if (200 === res.status) return {
|
|
647
|
+
ok: true,
|
|
648
|
+
message: "Credentials and Product ID verified. Draft submission exists."
|
|
649
|
+
};
|
|
650
|
+
if (404 === res.status) return {
|
|
651
|
+
ok: true,
|
|
652
|
+
message: "Credentials and Product ID verified. Ready to submit."
|
|
653
|
+
};
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
message: `Edge Partner API check failed (HTTP ${res.status}).`
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
async function edge_uploadAndAwaitProcessing(options) {
|
|
660
|
+
const size = await getFileSize(options.zip);
|
|
661
|
+
log("edge", `Uploading ZIP file (${formatBytes(size)})`);
|
|
662
|
+
const operationId = await uploadDraft(options.productId, options.zip, options.clientId, options.apiKey);
|
|
663
|
+
log("edge", "Waiting for validation");
|
|
664
|
+
const deadline = Date.now() + edge_VALIDATION_TIMEOUT_MS;
|
|
665
|
+
let operation;
|
|
666
|
+
do {
|
|
667
|
+
if (Date.now() > deadline) throw new Error(`Edge validation timed out after ${edge_VALIDATION_TIMEOUT_MS}ms`);
|
|
668
|
+
await delay(edge_POLL_INTERVAL_MS);
|
|
669
|
+
operation = await getDraftOperation(options.productId, operationId, options.clientId, options.apiKey);
|
|
670
|
+
}while ("InProgress" === operation.status);
|
|
671
|
+
if ("Failed" === operation.status) throw new Error(`Edge validation failed: ${JSON.stringify(operation, null, 2)}`);
|
|
672
|
+
log("edge", "Extension is valid");
|
|
673
|
+
return operationId;
|
|
674
|
+
}
|
|
675
|
+
async function deploy(config) {
|
|
676
|
+
const validated = validateConfig(config);
|
|
677
|
+
const { dryRun } = validated;
|
|
678
|
+
const jobs = [];
|
|
679
|
+
if (validated.chrome) {
|
|
680
|
+
const opts = validated.chrome;
|
|
681
|
+
jobs.push({
|
|
682
|
+
key: "chrome",
|
|
683
|
+
run: ()=>publishChrome(opts, dryRun)
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
if (validated.firefox) {
|
|
687
|
+
const opts = validated.firefox;
|
|
688
|
+
jobs.push({
|
|
689
|
+
key: "firefox",
|
|
690
|
+
run: ()=>publishFirefox(opts, dryRun)
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
if (validated.edge) {
|
|
694
|
+
const opts = validated.edge;
|
|
695
|
+
jobs.push({
|
|
696
|
+
key: "edge",
|
|
697
|
+
run: ()=>publishEdge(opts, dryRun)
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
if (0 === jobs.length) throw new Error("No stores configured. Provide at least one store to deploy to.\n\nEach store is activated by providing its ZIP path:\n Chrome: --chrome-zip <path> or CHROME_ZIP=<path>\n Firefox: --firefox-zip <path> or FIREFOX_ZIP=<path>\n Edge: --edge-zip <path> or EDGE_ZIP=<path>\n\nRun extension-deploy --help for all options, or create a .env.submit file.");
|
|
701
|
+
if (dryRun) console.log("\n[deploy] Dry run enabled \u2014 will verify auth without uploading\n");
|
|
702
|
+
else console.log("\n[deploy] Deploying extension\n");
|
|
703
|
+
const settled = await Promise.allSettled(jobs.map(async ({ key, run })=>{
|
|
704
|
+
const start = Date.now();
|
|
705
|
+
try {
|
|
706
|
+
const outcome = await run();
|
|
707
|
+
return {
|
|
708
|
+
store: key,
|
|
709
|
+
success: true,
|
|
710
|
+
status: dryRun ? "dry_run" : "submitted",
|
|
711
|
+
duration: Date.now() - start,
|
|
712
|
+
storeUrl: outcome.storeUrl,
|
|
713
|
+
submissionId: outcome.submissionId
|
|
714
|
+
};
|
|
715
|
+
} catch (err) {
|
|
716
|
+
return {
|
|
717
|
+
store: key,
|
|
718
|
+
success: false,
|
|
719
|
+
status: "failed",
|
|
720
|
+
error: err instanceof Error ? err.message : String(err),
|
|
721
|
+
duration: Date.now() - start
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
}));
|
|
725
|
+
const stores = settled.map((s, i)=>"fulfilled" === s.status ? s.value : {
|
|
726
|
+
store: jobs[i].key,
|
|
727
|
+
success: false,
|
|
728
|
+
status: "failed",
|
|
729
|
+
error: s.reason instanceof Error ? s.reason.message : String(s.reason),
|
|
730
|
+
duration: 0
|
|
731
|
+
});
|
|
732
|
+
const success = stores.every((s)=>s.success);
|
|
733
|
+
console.log("\n--- Deploy Summary ---");
|
|
734
|
+
for (const s of stores){
|
|
735
|
+
const icon = s.success ? "OK" : "FAIL";
|
|
736
|
+
console.log(` ${icon} ${s.store} (${s.status}, ${s.duration}ms)`);
|
|
737
|
+
if (s.error) console.error(` ${s.error}`);
|
|
738
|
+
}
|
|
739
|
+
console.log("");
|
|
740
|
+
return {
|
|
741
|
+
dryRun,
|
|
742
|
+
stores,
|
|
743
|
+
success
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
const DEFAULT_POLL_INTERVAL_MS = 60000;
|
|
747
|
+
const DEFAULT_TIMEOUT_MS = 3600000;
|
|
748
|
+
function watch_now() {
|
|
749
|
+
return new Date().toISOString();
|
|
750
|
+
}
|
|
751
|
+
function isTerminal(status) {
|
|
752
|
+
return "published" === status || "rejected" === status || "failed" === status;
|
|
753
|
+
}
|
|
754
|
+
async function getChromeSubmissionEvent(input) {
|
|
755
|
+
try {
|
|
756
|
+
const item = await getChromeItem(input);
|
|
757
|
+
const uploadState = item.uploadState;
|
|
758
|
+
let status = "unknown";
|
|
759
|
+
let message;
|
|
760
|
+
if ("SUCCESS" === uploadState) {
|
|
761
|
+
status = "in_review";
|
|
762
|
+
message = "Upload accepted by the Chrome Web Store. Review state is not exposed via API; check the developer dashboard for final outcome.";
|
|
763
|
+
} else if ("IN_PROGRESS" === uploadState) status = "processing";
|
|
764
|
+
else if ("FAILURE" === uploadState) {
|
|
765
|
+
var _item_itemError;
|
|
766
|
+
status = "failed";
|
|
767
|
+
message = (null == (_item_itemError = item.itemError) ? void 0 : _item_itemError.map((e)=>e.error_detail).join("; ")) ?? "Upload reported FAILURE by Chrome Web Store.";
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
store: "chrome",
|
|
771
|
+
status,
|
|
772
|
+
nativeStatus: uploadState,
|
|
773
|
+
message,
|
|
774
|
+
polledAt: watch_now(),
|
|
775
|
+
submissionId: input.extensionId,
|
|
776
|
+
storeUrl: chromeStoreUrl(input.extensionId)
|
|
777
|
+
};
|
|
778
|
+
} catch (err) {
|
|
779
|
+
return {
|
|
780
|
+
store: "chrome",
|
|
781
|
+
status: "failed",
|
|
782
|
+
message: err instanceof Error ? err.message : String(err),
|
|
783
|
+
polledAt: watch_now(),
|
|
784
|
+
submissionId: input.extensionId,
|
|
785
|
+
storeUrl: chromeStoreUrl(input.extensionId)
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function watchChromeSubmission(input, options = {}) {
|
|
790
|
+
return runWatchLoop("chrome", ()=>getChromeSubmissionEvent(input), options);
|
|
791
|
+
}
|
|
792
|
+
function mapEdgeStatus(native) {
|
|
793
|
+
switch(native){
|
|
794
|
+
case "Succeeded":
|
|
795
|
+
return "published";
|
|
796
|
+
case "Failed":
|
|
797
|
+
return "failed";
|
|
798
|
+
case "InProgress":
|
|
799
|
+
default:
|
|
800
|
+
return "in_review";
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
async function getEdgeSubmissionEvent(input) {
|
|
804
|
+
try {
|
|
805
|
+
const op = await getEdgePublishOperation({
|
|
806
|
+
productId: input.productId,
|
|
807
|
+
operationId: input.operationId,
|
|
808
|
+
clientId: input.clientId,
|
|
809
|
+
apiKey: input.apiKey
|
|
810
|
+
});
|
|
811
|
+
return {
|
|
812
|
+
store: "edge",
|
|
813
|
+
status: mapEdgeStatus(op.status),
|
|
814
|
+
nativeStatus: op.status,
|
|
815
|
+
message: op.message ?? void 0,
|
|
816
|
+
polledAt: watch_now(),
|
|
817
|
+
submissionId: input.operationId,
|
|
818
|
+
storeUrl: edgeStoreUrl(input.productId)
|
|
819
|
+
};
|
|
820
|
+
} catch (err) {
|
|
821
|
+
return {
|
|
822
|
+
store: "edge",
|
|
823
|
+
status: "failed",
|
|
824
|
+
message: err instanceof Error ? err.message : String(err),
|
|
825
|
+
polledAt: watch_now(),
|
|
826
|
+
submissionId: input.operationId,
|
|
827
|
+
storeUrl: edgeStoreUrl(input.productId)
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function watchEdgeSubmission(input, options = {}) {
|
|
832
|
+
return runWatchLoop("edge", ()=>getEdgeSubmissionEvent(input), options);
|
|
833
|
+
}
|
|
834
|
+
function mapFirefoxFileStatus(native) {
|
|
835
|
+
if (!native) return "unknown";
|
|
836
|
+
if ("public" === native) return "published";
|
|
837
|
+
if ("unreviewed" === native) return "in_review";
|
|
838
|
+
if ("disabled" === native) return "rejected";
|
|
839
|
+
return "unknown";
|
|
840
|
+
}
|
|
841
|
+
async function getFirefoxSubmissionEvent(input) {
|
|
842
|
+
try {
|
|
843
|
+
const version = await getFirefoxVersion({
|
|
844
|
+
addonId: input.extensionId,
|
|
845
|
+
versionId: input.versionId,
|
|
846
|
+
jwtIssuer: input.jwtIssuer,
|
|
847
|
+
jwtSecret: input.jwtSecret
|
|
848
|
+
});
|
|
849
|
+
const fileStatus = version.file.status;
|
|
850
|
+
return {
|
|
851
|
+
store: "firefox",
|
|
852
|
+
status: mapFirefoxFileStatus(fileStatus),
|
|
853
|
+
nativeStatus: fileStatus,
|
|
854
|
+
polledAt: watch_now(),
|
|
855
|
+
submissionId: String(version.id),
|
|
856
|
+
storeUrl: firefoxStoreUrl(input.extensionId)
|
|
857
|
+
};
|
|
858
|
+
} catch (err) {
|
|
859
|
+
return {
|
|
860
|
+
store: "firefox",
|
|
861
|
+
status: "failed",
|
|
862
|
+
message: err instanceof Error ? err.message : String(err),
|
|
863
|
+
polledAt: watch_now(),
|
|
864
|
+
submissionId: String(input.versionId),
|
|
865
|
+
storeUrl: firefoxStoreUrl(input.extensionId)
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function watchFirefoxSubmission(input, options = {}) {
|
|
870
|
+
return runWatchLoop("firefox", ()=>getFirefoxSubmissionEvent(input), options);
|
|
871
|
+
}
|
|
872
|
+
async function runWatchLoop(_store, probe, options) {
|
|
873
|
+
const pollInterval = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
874
|
+
const timeout = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
875
|
+
const deadline = Date.now() + timeout;
|
|
876
|
+
let lastEvent;
|
|
877
|
+
while(true){
|
|
878
|
+
var _options_onEvent;
|
|
879
|
+
lastEvent = await probe();
|
|
880
|
+
null == (_options_onEvent = options.onEvent) || _options_onEvent.call(options, lastEvent);
|
|
881
|
+
if (isTerminal(lastEvent.status)) return lastEvent;
|
|
882
|
+
if (Date.now() >= deadline) return lastEvent;
|
|
883
|
+
await delay(pollInterval);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
export { chromeOptionsSchema, deploy, deployConfigSchema, edgeOptionsSchema, firefoxOptionsSchema, getChromeSubmissionEvent, getEdgeSubmissionEvent, getFirefoxSubmissionEvent, resolveConfig, validateConfig, verifyChromeCredentials, verifyEdgeCredentials, verifyFirefoxCredentials, watchChromeSubmission, watchEdgeSubmission, watchFirefoxSubmission };
|