@stacksjs/browser-extension 0.70.246 → 0.70.247
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/dist/firefox-previews.d.ts +61 -0
- package/dist/firefox-previews.js +100 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ExtensionConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Pixel dimensions of a PNG or JPEG, read from its header.
|
|
4
|
+
*
|
|
5
|
+
* Enough to answer "does the listing already show these images", which is the
|
|
6
|
+
* only reason this file needs to know. Decoding them properly would mean
|
|
7
|
+
* making the whole codec stack a dependency of the publisher, to compare two
|
|
8
|
+
* pairs of integers.
|
|
9
|
+
*/
|
|
10
|
+
export declare function imageSize(bytes: Uint8Array): { width: number, height: number } | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* A short-lived HS256 token, which is what AMO's external API takes.
|
|
13
|
+
*
|
|
14
|
+
* Signed here rather than pulled from a JWT library: the payload is four
|
|
15
|
+
* claims and the signature is one HMAC, and `web-ext` — the only other AMO
|
|
16
|
+
* caller in this package — does not expose its own.
|
|
17
|
+
*/
|
|
18
|
+
export declare function amoToken(issuer: string, secret: string, now?: number): string;
|
|
19
|
+
/** Read the previews a listing currently shows. */
|
|
20
|
+
export declare function listFirefoxPreviews(addonId: string, token: string): Promise<FirefoxPreview[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Bring the listing's screenshots in line with the config.
|
|
23
|
+
*
|
|
24
|
+
* Skips the whole exchange when the listing already shows the same images at
|
|
25
|
+
* the same sizes in the same order — a publish should not churn a public
|
|
26
|
+
* listing's preview ids just because it ran again.
|
|
27
|
+
*/
|
|
28
|
+
export declare function syncFirefoxPreviews(config: ExtensionConfig, options?: FirefoxPreviewSyncOptions): Promise<FirefoxPreviewSyncResult>;
|
|
29
|
+
/**
|
|
30
|
+
* Listing screenshots on addons.mozilla.org.
|
|
31
|
+
*
|
|
32
|
+
* Of the three stores this package publishes to, AMO is the middle case:
|
|
33
|
+
* Apple's API takes screenshots as part of a version's metadata, Chrome's has
|
|
34
|
+
* no endpoint for them at all, and AMO has a writable previews collection that
|
|
35
|
+
* nothing was using. So a Firefox listing kept whatever was dragged into the
|
|
36
|
+
* Developer Hub the first time — for very-good-adblock, two images at mismatched
|
|
37
|
+
* sizes and captions that said "A screenshot showcasing the extension" — while
|
|
38
|
+
* every other surface regenerated from the product.
|
|
39
|
+
*
|
|
40
|
+
* Replacement is upload-then-delete rather than delete-then-upload. Either
|
|
41
|
+
* order is fine when it works; only one of them leaves a public listing with no
|
|
42
|
+
* screenshots at all if the process dies in the middle.
|
|
43
|
+
*/
|
|
44
|
+
export declare interface FirefoxPreviewAuth {
|
|
45
|
+
issuer?: string
|
|
46
|
+
secret?: string
|
|
47
|
+
}
|
|
48
|
+
export declare interface FirefoxPreviewSyncOptions extends FirefoxPreviewAuth {
|
|
49
|
+
cwd?: string
|
|
50
|
+
dryRun?: boolean
|
|
51
|
+
}
|
|
52
|
+
export declare interface FirefoxPreview {
|
|
53
|
+
id: number
|
|
54
|
+
position: number
|
|
55
|
+
size: [number, number]
|
|
56
|
+
}
|
|
57
|
+
export declare interface FirefoxPreviewSyncResult {
|
|
58
|
+
unchanged: boolean
|
|
59
|
+
uploaded: FirefoxPreview[]
|
|
60
|
+
removed: number[]
|
|
61
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHmac, randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { basename, isAbsolute, resolve } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
const AMO_API = "https://addons.mozilla.org/api/v5";
|
|
6
|
+
export function imageSize(bytes) {
|
|
7
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
8
|
+
if (bytes.length > 24 && view.getUint32(0) === 2303741511)
|
|
9
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
10
|
+
if (bytes.length > 4 && view.getUint16(0) === 65496) {
|
|
11
|
+
let offset = 2;
|
|
12
|
+
while (offset + 9 < bytes.length) {
|
|
13
|
+
if (view.getUint8(offset) !== 255) {
|
|
14
|
+
offset++;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const marker = view.getUint8(offset + 1);
|
|
18
|
+
if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204)
|
|
19
|
+
return { height: view.getUint16(offset + 5), width: view.getUint16(offset + 7) };
|
|
20
|
+
offset += 2 + view.getUint16(offset + 2);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
export function amoToken(issuer, secret, now = Date.now()) {
|
|
26
|
+
const base64url = (value) => Buffer.from(value).toString("base64url"), issuedAt = Math.floor(now / 1000), header = base64url(JSON.stringify({ alg: "HS256", typ: "JWT" })), payload = base64url(JSON.stringify({
|
|
27
|
+
iss: issuer,
|
|
28
|
+
jti: randomUUID(),
|
|
29
|
+
iat: issuedAt,
|
|
30
|
+
exp: issuedAt + 240
|
|
31
|
+
})), signature = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
|
|
32
|
+
return `${header}.${payload}.${signature}`;
|
|
33
|
+
}
|
|
34
|
+
function resolveAuth(options) {
|
|
35
|
+
const issuer = options.issuer ?? process.env.AMO_JWT_ISSUER ?? process.env.WEB_EXT_API_KEY, secret = options.secret ?? process.env.AMO_JWT_SECRET ?? process.env.WEB_EXT_API_SECRET, missing = [!issuer && "AMO_JWT_ISSUER", !secret && "AMO_JWT_SECRET"].filter(Boolean);
|
|
36
|
+
if (missing.length)
|
|
37
|
+
throw Error(`[browser-extension] missing Firefox Add-ons credentials: ${missing.join(", ")}`);
|
|
38
|
+
return { issuer, secret };
|
|
39
|
+
}
|
|
40
|
+
async function amo(path, token, init = {}) {
|
|
41
|
+
const response = await fetch(`${AMO_API}${path}`, {
|
|
42
|
+
...init,
|
|
43
|
+
headers: { Authorization: `JWT ${token}`, ...init.headers }
|
|
44
|
+
});
|
|
45
|
+
if (!response.ok && response.status !== 404) {
|
|
46
|
+
const detail = await response.text().catch(() => "");
|
|
47
|
+
throw Error(`[browser-extension] AMO ${init.method ?? "GET"} ${path} failed (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
|
|
48
|
+
}
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
export async function listFirefoxPreviews(addonId, token) {
|
|
52
|
+
const response = await amo(`/addons/addon/${encodeURIComponent(addonId)}/`, token);
|
|
53
|
+
if (response.status === 404)
|
|
54
|
+
return [];
|
|
55
|
+
return ((await response.json()).previews ?? []).map((preview, index) => ({
|
|
56
|
+
id: preview.id,
|
|
57
|
+
position: preview.position ?? index,
|
|
58
|
+
size: preview.image_size ?? [0, 0]
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
export async function syncFirefoxPreviews(config, options = {}) {
|
|
62
|
+
const store = config.firefoxAddons ?? {}, screenshots = store.screenshots ?? [];
|
|
63
|
+
if (!screenshots.length)
|
|
64
|
+
return { unchanged: !0, uploaded: [], removed: [] };
|
|
65
|
+
if (!config.geckoId)
|
|
66
|
+
throw Error("[browser-extension] Firefox previews need geckoId in config/extension.ts");
|
|
67
|
+
const cwd = options.cwd ?? process.cwd(), files = await Promise.all(screenshots.map(async (path) => {
|
|
68
|
+
const file = isAbsolute(path) ? path : resolve(cwd, path), bytes = new Uint8Array(await readFile(file).catch(() => {
|
|
69
|
+
throw Error(`[browser-extension] Firefox screenshot not found: ${path}`);
|
|
70
|
+
})), size = imageSize(bytes);
|
|
71
|
+
return { path: file, bytes, width: size?.width ?? 0, height: size?.height ?? 0 };
|
|
72
|
+
})), { issuer, secret } = resolveAuth(options), existing = await listFirefoxPreviews(config.geckoId, amoToken(issuer, secret));
|
|
73
|
+
if (files.every((file) => file.width > 0 && file.height > 0) && existing.length === files.length && existing.slice().sort((a, b) => a.position - b.position).every((preview, index) => preview.size[0] === files[index].width && preview.size[1] === files[index].height))
|
|
74
|
+
return { unchanged: !0, uploaded: [], removed: [] };
|
|
75
|
+
if (options.dryRun)
|
|
76
|
+
return { unchanged: !1, uploaded: [], removed: existing.map((preview) => preview.id) };
|
|
77
|
+
const uploaded = [];
|
|
78
|
+
for (const [index, file] of files.entries()) {
|
|
79
|
+
const form = new FormData;
|
|
80
|
+
form.append("image", new Blob([file.bytes], { type: "image/png" }), basename(file.path));
|
|
81
|
+
form.append("position", String(index));
|
|
82
|
+
const created = await (await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/`, amoToken(issuer, secret), {
|
|
83
|
+
method: "POST",
|
|
84
|
+
body: form
|
|
85
|
+
})).json(), caption = store.screenshotCaptions?.[index];
|
|
86
|
+
if (caption)
|
|
87
|
+
await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/${created.id}/`, amoToken(issuer, secret), {
|
|
88
|
+
method: "PATCH",
|
|
89
|
+
headers: { "Content-Type": "application/json" },
|
|
90
|
+
body: JSON.stringify({ caption: { "en-US": caption } })
|
|
91
|
+
});
|
|
92
|
+
uploaded.push({ id: created.id, position: index, size: [file.width, file.height] });
|
|
93
|
+
}
|
|
94
|
+
const removed = [];
|
|
95
|
+
for (const preview of existing) {
|
|
96
|
+
await amo(`/addons/addon/${encodeURIComponent(config.geckoId)}/previews/${preview.id}/`, amoToken(issuer, secret), { method: "DELETE" });
|
|
97
|
+
removed.push(preview.id);
|
|
98
|
+
}
|
|
99
|
+
return { unchanged: !1, uploaded, removed };
|
|
100
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from './app-store-submission';
|
|
|
13
13
|
export * from './chrome-web-store';
|
|
14
14
|
export * from './config';
|
|
15
15
|
export * from './firefox-addons';
|
|
16
|
+
export * from './firefox-previews';
|
|
16
17
|
export * from './manifest';
|
|
17
18
|
export * from './package';
|
|
18
19
|
export * from './safari';
|
package/dist/index.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export declare interface FirefoxAddonsConfig {
|
|
|
67
67
|
supportEmail?: string
|
|
68
68
|
requiresPayment?: boolean
|
|
69
69
|
artifactsDir?: string
|
|
70
|
+
screenshots?: string[]
|
|
71
|
+
screenshotCaptions?: string[]
|
|
70
72
|
}
|
|
71
73
|
export declare interface SafariAppStoreReviewContact {
|
|
72
74
|
firstName: string
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/browser-extension",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.247",
|
|
5
5
|
"description": "Build MV3 browser extensions (Chrome, Firefox, Safari) the Stacks way — manifest, content/background scripts, DNR rules, packaging, Safari container app, all config-driven.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|