pack-crx 1.0.2 → 1.1.1
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 +1 -1
- package/README.md +65 -38
- package/build/crx3.pb.d.ts +15 -0
- package/build/index.d.ts +354 -0
- package/build/index.js +9 -0
- package/build/index.js.map +11 -0
- package/package.json +21 -6
- package/bun.lockb +0 -0
- package/src/crx3.pb.js +0 -47
- package/src/index.d.ts +0 -320
- package/src/index.ts +0 -660
- package/tsconfig.json +0 -27
package/src/index.ts
DELETED
|
@@ -1,660 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import Pbf from "pbf";
|
|
3
|
-
import * as crx3 from "./crx3.pb";
|
|
4
|
-
import JSZip from "jszip";
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import { join, relative, resolve } from "node:path";
|
|
7
|
-
import { Buffer } from "node:buffer";
|
|
8
|
-
import RSA from "node-rsa";
|
|
9
|
-
|
|
10
|
-
interface ChromeBaseManifest {
|
|
11
|
-
// Required keys
|
|
12
|
-
manifest_version: number;
|
|
13
|
-
name: string;
|
|
14
|
-
version: string;
|
|
15
|
-
|
|
16
|
-
// Required by Chrome Web Store
|
|
17
|
-
description?: string;
|
|
18
|
-
icons?: {[x: `${number}`]: string};
|
|
19
|
-
|
|
20
|
-
// Optional
|
|
21
|
-
author?: string;
|
|
22
|
-
background?: {
|
|
23
|
-
service_worker?: string;
|
|
24
|
-
type?: "module";
|
|
25
|
-
};
|
|
26
|
-
chrome_settings_overrides?: {
|
|
27
|
-
alternate_urls?: string[];
|
|
28
|
-
encoding?: string;
|
|
29
|
-
favicon_url?: string;
|
|
30
|
-
homepage?: string;
|
|
31
|
-
image_url?: string;
|
|
32
|
-
image_url_post_params?: string;
|
|
33
|
-
is_default?: boolean;
|
|
34
|
-
keyword?: string;
|
|
35
|
-
name?: string;
|
|
36
|
-
prepopulated_id?: number;
|
|
37
|
-
search_provider?: object;
|
|
38
|
-
search_url?: string;
|
|
39
|
-
search_url_post_params?: string;
|
|
40
|
-
startup_pages?: string[];
|
|
41
|
-
suggest_url?: string;
|
|
42
|
-
suggest_url_post_params?: string;
|
|
43
|
-
};
|
|
44
|
-
chrome_url_overrides?: {
|
|
45
|
-
bookmarks?: string;
|
|
46
|
-
history?: string;
|
|
47
|
-
newtab?: string;
|
|
48
|
-
};
|
|
49
|
-
commands?: {
|
|
50
|
-
[x: string]: {
|
|
51
|
-
description: string;
|
|
52
|
-
suggested_key?: string;
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
content_scripts?: {
|
|
56
|
-
matches: string[];
|
|
57
|
-
css?: string[];
|
|
58
|
-
js?: string[];
|
|
59
|
-
run_at?: "document_start" | "document_end" | "document_idle";
|
|
60
|
-
match_about_blank?: boolean;
|
|
61
|
-
match_origin_as_fallback?: boolean;
|
|
62
|
-
world?: "ISOLATED" | "MAIN";
|
|
63
|
-
}[];
|
|
64
|
-
content_security_policy?: {
|
|
65
|
-
extension_pages?: string;
|
|
66
|
-
sandbox?: string;
|
|
67
|
-
};
|
|
68
|
-
cross_origin_embedder_policy?: string;
|
|
69
|
-
cross_origin_opener_policy?: string;
|
|
70
|
-
declarative_net_request?: {
|
|
71
|
-
rule_resources?: {
|
|
72
|
-
id: string;
|
|
73
|
-
enabled: boolean;
|
|
74
|
-
path: string;
|
|
75
|
-
}[];
|
|
76
|
-
};
|
|
77
|
-
default_locale?: string;
|
|
78
|
-
devtools_page?: string;
|
|
79
|
-
export?: {
|
|
80
|
-
allowlist?: string[];
|
|
81
|
-
};
|
|
82
|
-
externally_connectable?: {
|
|
83
|
-
ids?: string[];
|
|
84
|
-
matches?: string[];
|
|
85
|
-
accepts_tls_channel_id?: boolean;
|
|
86
|
-
};
|
|
87
|
-
homepage_url?: string;
|
|
88
|
-
host_permissions?: string[];
|
|
89
|
-
import?: {
|
|
90
|
-
id: string;
|
|
91
|
-
minimum_version?: string;
|
|
92
|
-
}[];
|
|
93
|
-
incognito?: "spanning" | "split" | "not_allowed";
|
|
94
|
-
key?: string;
|
|
95
|
-
minimum_chrome_version?: string;
|
|
96
|
-
oauth2?: {
|
|
97
|
-
client_id: string;
|
|
98
|
-
scopes: string[];
|
|
99
|
-
};
|
|
100
|
-
omnibox?: {
|
|
101
|
-
keyword?: string;
|
|
102
|
-
};
|
|
103
|
-
optional_host_permissions?: string[];
|
|
104
|
-
optional_permissions?: string[];
|
|
105
|
-
options_page?: string;
|
|
106
|
-
options_ui?: {
|
|
107
|
-
page: string;
|
|
108
|
-
open_in_tab?: boolean;
|
|
109
|
-
};
|
|
110
|
-
permissions?: string[];
|
|
111
|
-
requirements?: {
|
|
112
|
-
[x: string]: {features: string[]};
|
|
113
|
-
};
|
|
114
|
-
sandbox?: {
|
|
115
|
-
pages: string[];
|
|
116
|
-
};
|
|
117
|
-
short_name?: string;
|
|
118
|
-
side_panel?: string;
|
|
119
|
-
storage?: {
|
|
120
|
-
managed_schema?: string;
|
|
121
|
-
};
|
|
122
|
-
tts_engine?: {
|
|
123
|
-
voices?: {
|
|
124
|
-
voice_name: string;
|
|
125
|
-
lang?: string;
|
|
126
|
-
event_types?: ("start" | "word" | "sentence" | "marker" | "end" | "error")[];
|
|
127
|
-
}[];
|
|
128
|
-
};
|
|
129
|
-
update_url?: string;
|
|
130
|
-
version_name?: string;
|
|
131
|
-
web_accessible_resources?: ({
|
|
132
|
-
resources: string[];
|
|
133
|
-
} & ({
|
|
134
|
-
matches: string[];
|
|
135
|
-
} | {
|
|
136
|
-
extension_ids: string[];
|
|
137
|
-
}))[];
|
|
138
|
-
|
|
139
|
-
// ChromeOS
|
|
140
|
-
file_browser_handlers?: {
|
|
141
|
-
id: string;
|
|
142
|
-
default_title: string;
|
|
143
|
-
file_filters: string[];
|
|
144
|
-
}[];
|
|
145
|
-
file_handlers?: {
|
|
146
|
-
action: string;
|
|
147
|
-
name: string;
|
|
148
|
-
accept: {
|
|
149
|
-
[x: string]: string[];
|
|
150
|
-
};
|
|
151
|
-
launch_type?: "single-client" | "multiple-clients";
|
|
152
|
-
}[];
|
|
153
|
-
file_system_provider_capabilities?: {
|
|
154
|
-
configurable?: boolean;
|
|
155
|
-
multiple_mounts?: boolean;
|
|
156
|
-
watchable?: boolean;
|
|
157
|
-
source: "file" | "device" | "network";
|
|
158
|
-
};
|
|
159
|
-
input_components?: {
|
|
160
|
-
name: string;
|
|
161
|
-
id?: string;
|
|
162
|
-
language?: string | string[];
|
|
163
|
-
layouts?: string | string[];
|
|
164
|
-
input_view?: string;
|
|
165
|
-
options_page?: string;
|
|
166
|
-
}[];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export type ChromeMV2Manifest = ChromeBaseManifest & {
|
|
170
|
-
manifest_version: 2;
|
|
171
|
-
browser_action?: {
|
|
172
|
-
default_icon?: {[x: `${number}`]: string};
|
|
173
|
-
default_title?: string;
|
|
174
|
-
default_popup?: string;
|
|
175
|
-
};
|
|
176
|
-
page_action?: {
|
|
177
|
-
default_icon?: {[x: `${number}`]: string};
|
|
178
|
-
default_title?: string;
|
|
179
|
-
default_popup?: string;
|
|
180
|
-
};
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
export type ChromeMV3Manifest = ChromeBaseManifest & {
|
|
184
|
-
manifest_version: 3;
|
|
185
|
-
action?: {
|
|
186
|
-
default_icon?: {[x: `${number}`]: string};
|
|
187
|
-
default_title?: string;
|
|
188
|
-
default_popup?: string;
|
|
189
|
-
};
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
export type ChromeManifest = ChromeMV2Manifest | ChromeMV3Manifest;
|
|
193
|
-
|
|
194
|
-
export interface CrxFileHeader {
|
|
195
|
-
sha256_with_rsa?: AsymmetricKeyProof[];
|
|
196
|
-
sha256_with_ecdsa?: AsymmetricKeyProof[];
|
|
197
|
-
signed_header_data?: Uint8Array;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export interface AsymmetricKeyProof {
|
|
201
|
-
public_key?: Uint8Array;
|
|
202
|
-
signature?: Uint8Array;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* CRX IDs are 16 bytes long
|
|
207
|
-
* @constant
|
|
208
|
-
*/
|
|
209
|
-
const CRX_ID_SIZE = 16;
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* CRX3 uses 32bit numbers in various places,
|
|
213
|
-
* so let's prepare size constant for that.
|
|
214
|
-
* @constant
|
|
215
|
-
*/
|
|
216
|
-
const SIZE_BYTES = 4;
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Used for file format.
|
|
220
|
-
* @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx3.proto}
|
|
221
|
-
* @constant
|
|
222
|
-
*/
|
|
223
|
-
const kSignature = Uint8Array.from("Cr24", ch => ch.charCodeAt(0));
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Used for file format.
|
|
227
|
-
* @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx3.proto}
|
|
228
|
-
* @constant
|
|
229
|
-
*/
|
|
230
|
-
const kVersion = Uint8Array.from([3, 0, 0, 0]);
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Used for generating package signatures.
|
|
234
|
-
* @see {@link https://github.com/chromium/chromium/blob/master/components/crx_file/crx3.proto}
|
|
235
|
-
* @constant
|
|
236
|
-
*/
|
|
237
|
-
const kSignatureContext = Uint8Array.from("CRX3 SignedData\x00", ch => ch.charCodeAt(0));
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Pack a CRX2 extension. Chrome stopped supporting these entirely in version 73.0.3683, which released in October of 2017.
|
|
241
|
-
*
|
|
242
|
-
* @param privateKey The extension's private key.
|
|
243
|
-
* @param publicKey The extension's public key.
|
|
244
|
-
* @param contents The zipped contents of the extension. This should contain a `manifest.json` file directly inside it, but we don't validate that in this function.
|
|
245
|
-
*
|
|
246
|
-
* @returns The contents of the packaged extension.
|
|
247
|
-
*
|
|
248
|
-
* @deprecated
|
|
249
|
-
*/
|
|
250
|
-
export function packCrx2(privateKey: Uint8Array, publicKey: Uint8Array, contents: Uint8Array, rsa?: RSA): Uint8Array {
|
|
251
|
-
rsa ??= new RSA(Buffer.from(privateKey), "pkcs8-private-der");
|
|
252
|
-
rsa.setOptions({signingScheme: "pkcs1-sha1"});
|
|
253
|
-
const signature = rsa.sign(contents);
|
|
254
|
-
const length = 16 /* magic + version + key length + sign length */ + publicKey.length + signature.length;
|
|
255
|
-
const result = new Uint8Array(length);
|
|
256
|
-
result.set(kSignature, 0);
|
|
257
|
-
const dv = new DataView(result.buffer);
|
|
258
|
-
dv.setUint32(4, 2, true);
|
|
259
|
-
dv.setUint32(8, publicKey.length, true);
|
|
260
|
-
dv.setUint32(12, signature.length, true);
|
|
261
|
-
result.set(publicKey, 16);
|
|
262
|
-
result.set(signature, 16 + publicKey.length);
|
|
263
|
-
result.set(contents, length);
|
|
264
|
-
return result;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Pack a CRX3 extension.
|
|
269
|
-
*
|
|
270
|
-
* @param privateKey The extension's private key.
|
|
271
|
-
* @param publicKey The extension's public key.
|
|
272
|
-
* @param contents The zipped contents of the extension. This should contain a `manifest.json` file directly inside it, but we don't validate that in this function.
|
|
273
|
-
*
|
|
274
|
-
* @returns The contents of the packaged extension.
|
|
275
|
-
*/
|
|
276
|
-
export function packCrx3(privateKey: Uint8Array, publicKey: Uint8Array, contents: Uint8Array, rsa?: RSA): Uint8Array {
|
|
277
|
-
let pb = new Pbf();
|
|
278
|
-
crx3.SignedData.write({
|
|
279
|
-
crx_id: generateBinaryCrxId(publicKey)
|
|
280
|
-
}, pb);
|
|
281
|
-
const signedHeaderData = pb.finish();
|
|
282
|
-
|
|
283
|
-
const signature = generateCrx3Signature(privateKey, signedHeaderData, contents, rsa);
|
|
284
|
-
|
|
285
|
-
pb = new Pbf();
|
|
286
|
-
crx3.CrxFileHeader.write({
|
|
287
|
-
sha256_with_rsa: [{
|
|
288
|
-
public_key: publicKey satisfies Uint8Array,
|
|
289
|
-
signature
|
|
290
|
-
}],
|
|
291
|
-
signed_header_data: signedHeaderData
|
|
292
|
-
} as CrxFileHeader, pb);
|
|
293
|
-
const header = pb.finish();
|
|
294
|
-
|
|
295
|
-
const size =
|
|
296
|
-
kSignature.length + // Magic constant
|
|
297
|
-
kVersion.length + // Version number
|
|
298
|
-
SIZE_BYTES + // Header size
|
|
299
|
-
header.length +
|
|
300
|
-
contents.length;
|
|
301
|
-
|
|
302
|
-
const result = new Uint8Array(size);
|
|
303
|
-
|
|
304
|
-
let index = 0;
|
|
305
|
-
result.set(kSignature, index);
|
|
306
|
-
result.set(kVersion, index += kSignature.length);
|
|
307
|
-
new DataView(result.buffer).setUint32(index += kVersion.length, header.length, true);
|
|
308
|
-
result.set(header, index += SIZE_BYTES);
|
|
309
|
-
result.set(contents, index += header.length);
|
|
310
|
-
|
|
311
|
-
return result;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function generateBinaryCrxId(publicKey: Uint8Array): Uint8Array {
|
|
315
|
-
var hash = createHash("sha256");
|
|
316
|
-
hash.update(publicKey);
|
|
317
|
-
return Uint8Array.from(hash.digest()).slice(0, CRX_ID_SIZE);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function generateCrx3Signature(privateKey: Uint8Array, signedHeaderData: Uint8Array, contents: Uint8Array, rsa?: RSA): Uint8Array {
|
|
321
|
-
rsa ??= new RSA(Buffer.from(privateKey), "pkcs8-private-der");
|
|
322
|
-
rsa.setOptions({signingScheme: "pkcs1-sha256"});
|
|
323
|
-
|
|
324
|
-
// Size of signed_header_data
|
|
325
|
-
const sizeOctets = new DataView(new ArrayBuffer(SIZE_BYTES));
|
|
326
|
-
sizeOctets.setUint32(0, signedHeaderData.length, true);
|
|
327
|
-
|
|
328
|
-
const toSign = Buffer.concat([
|
|
329
|
-
kSignatureContext,
|
|
330
|
-
new Uint8Array(sizeOctets.buffer),
|
|
331
|
-
signedHeaderData,
|
|
332
|
-
contents
|
|
333
|
-
]);
|
|
334
|
-
|
|
335
|
-
return Uint8Array.from(rsa.sign(toSign));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Generate an extension's ID (32 characters, a-p) from its public key.
|
|
340
|
-
*
|
|
341
|
-
* @param publicKey The public key of the extension.
|
|
342
|
-
*
|
|
343
|
-
* @returns The generated extension ID.
|
|
344
|
-
*/
|
|
345
|
-
export function generateCrxId(publicKey: Uint8Array): string {
|
|
346
|
-
return createHash("sha256")
|
|
347
|
-
.update(publicKey)
|
|
348
|
-
.digest()
|
|
349
|
-
.toString("hex")
|
|
350
|
-
.split("")
|
|
351
|
-
.map(x => (parseInt(x, 16) + 0x0a).toString(26))
|
|
352
|
-
.join("")
|
|
353
|
-
.slice(0, 32);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Load a directory from the filesystem into a ZIP archive, using the node:fs API.
|
|
358
|
-
*
|
|
359
|
-
* @param where The path to the directory.
|
|
360
|
-
*/
|
|
361
|
-
export async function packContents(where: string): Promise<{
|
|
362
|
-
/** The ZIP-encoded data. */
|
|
363
|
-
contents: Uint8Array,
|
|
364
|
-
/** The manifest for the extension, parsed as JSON. */
|
|
365
|
-
manifest: ChromeManifest
|
|
366
|
-
}> {
|
|
367
|
-
const zip = new JSZip();
|
|
368
|
-
let manifest: Promise<Uint8Array> | undefined;
|
|
369
|
-
async function f(loc: string) {
|
|
370
|
-
for (const entry of await fs.readdir(loc, {withFileTypes: true})) {
|
|
371
|
-
const fp = join(loc, entry.name);
|
|
372
|
-
if (entry.isDirectory()) {
|
|
373
|
-
await f(fp);
|
|
374
|
-
} else {
|
|
375
|
-
const contents = fs.readFile(fp).then(buf => Uint8Array.from(buf));
|
|
376
|
-
const rp = relative(where, fp);
|
|
377
|
-
if (rp == "manifest.json") manifest ??= contents;
|
|
378
|
-
zip.file(rp, contents);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
await f(resolve(process.cwd(), where));
|
|
383
|
-
if (manifest == undefined) throw new Error("Manifest file not found");
|
|
384
|
-
return {
|
|
385
|
-
contents: await zip.generateAsync({
|
|
386
|
-
compression: "DEFLATE",
|
|
387
|
-
type: "uint8array"
|
|
388
|
-
}),
|
|
389
|
-
manifest: JSON.parse(new TextDecoder().decode(await manifest))
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Generate the updates XML file for [serving an extension yourself](https://developer.chrome.com/docs/extensions/how-to/distribute/host-on-linux).
|
|
395
|
-
*
|
|
396
|
-
* @param crxId The extension's ID.
|
|
397
|
-
* @param url The URL where the extension's CRX file will be hosted.
|
|
398
|
-
* @param version The extension's version.
|
|
399
|
-
* @param minChromeVersion The minimum Chrome version that the extension can be installed on.
|
|
400
|
-
*
|
|
401
|
-
* @returns The updates XML text
|
|
402
|
-
*/
|
|
403
|
-
export function generateUpdateXML(crxId: string, url: string, version: string, minChromeVersion?: string): string {
|
|
404
|
-
return `<?xml version='1.0' encoding='UTF-8'?>
|
|
405
|
-
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
|
406
|
-
<app appid='${crxId}'>
|
|
407
|
-
<updatecheck codebase='${url}' version='${version}'${minChromeVersion ? ` prodversionmin='${minChromeVersion}'` : ""} />
|
|
408
|
-
</app>
|
|
409
|
-
</gupdate>`;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
export function generatePrivateKey(bits = 4096): Uint8Array {
|
|
413
|
-
return Uint8Array.from(new RSA({b: bits}).exportKey("pkcs8-private-der"));
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
export function generatePublicKey(privateKey: Uint8Array): Uint8Array {
|
|
417
|
-
return Uint8Array.from(new RSA(Buffer.from(privateKey), "pkcs8-private-der").exportKey("pkcs8-public-der"));
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export function convertToPem(key: Uint8Array, type: "private" | "public"): string {
|
|
421
|
-
return new RSA(Buffer.from(key), `pkcs8-${type}-der`).exportKey(`pkcs8-${type}-pem`);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
export function convertFromPem(key: string, type: "private" | "public"): Uint8Array {
|
|
425
|
-
return Uint8Array.from(new RSA(key, `pkcs8-${type}-pem`).exportKey(`pkcs8-${type}-der`));
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Unpack a CRX file and extract its contents as ZIP data.
|
|
430
|
-
*
|
|
431
|
-
* @param crx The CRX to be unpacked.
|
|
432
|
-
*/
|
|
433
|
-
export function unpack(crx: Uint8Array): {
|
|
434
|
-
/** The ZIP data. */
|
|
435
|
-
archive: Uint8Array,
|
|
436
|
-
/** The CRX format version. */
|
|
437
|
-
crxVersion: 2,
|
|
438
|
-
/** The extension's public key. */
|
|
439
|
-
key: Uint8Array,
|
|
440
|
-
/** The signature over the contents of the extension. */
|
|
441
|
-
sign: Uint8Array
|
|
442
|
-
} | {
|
|
443
|
-
/** The ZIP data. */
|
|
444
|
-
archive: Uint8Array,
|
|
445
|
-
/** The CRX format version. */
|
|
446
|
-
crxVersion: 3,
|
|
447
|
-
/** The header for the CRX file, for signatures and things. */
|
|
448
|
-
header: CrxFileHeader
|
|
449
|
-
} {
|
|
450
|
-
const abuf = crx.buffer;
|
|
451
|
-
const dv = new DataView(abuf);
|
|
452
|
-
if (kSignature.every((v, i) => dv.getUint8(i) == v)) {
|
|
453
|
-
const crxVersion = dv.getUint32(4, true);
|
|
454
|
-
if (crxVersion == 2) {
|
|
455
|
-
const keyLength = dv.getUint32(8, true);
|
|
456
|
-
const signLength = dv.getUint32(12, true);
|
|
457
|
-
return {
|
|
458
|
-
archive: crx.slice(16 + keyLength + signLength),
|
|
459
|
-
crxVersion: 2,
|
|
460
|
-
key: crx.slice(16, 16 + keyLength),
|
|
461
|
-
sign: crx.slice(16 + keyLength, 16 + keyLength + signLength)
|
|
462
|
-
};
|
|
463
|
-
} else if (crxVersion == 3) {
|
|
464
|
-
const headerLength = dv.getUint32(8, true);
|
|
465
|
-
const archive = crx.slice(12 + headerLength);
|
|
466
|
-
const header = crx.slice(12, 12 + headerLength);
|
|
467
|
-
const pb = new Pbf(header);
|
|
468
|
-
const decodedHeader = crx3.CrxFileHeader.read(pb);
|
|
469
|
-
return {archive, crxVersion: 3, header: decodedHeader};
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
throw new Error("The file given is not a valid CRX file");
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
export interface PackInput {
|
|
476
|
-
/** The ZIP archive of the contents of the extension, or a path to the folder containing the extension. */
|
|
477
|
-
contents?: Uint8Array | string;
|
|
478
|
-
/** The private key for the extension, or a path to it. */
|
|
479
|
-
privateKey?: Uint8Array | string | null;
|
|
480
|
-
/** The size of key to generate, if needed. */
|
|
481
|
-
keySize?: number;
|
|
482
|
-
/** The public key for the extension, or a path to it. */
|
|
483
|
-
publicKey?: Uint8Array | string | null;
|
|
484
|
-
/** The instance of NodeRSA to use. */
|
|
485
|
-
rsa?: RSA;
|
|
486
|
-
/** The extension's ID. */
|
|
487
|
-
id?: string | null;
|
|
488
|
-
/** The outputted CRX file. */
|
|
489
|
-
crx?: Uint8Array | null;
|
|
490
|
-
/** The CRX format version to use. Defaults to 3. */
|
|
491
|
-
crxVersion?: number;
|
|
492
|
-
/** The URL to where the CRX file (not the updates XML) will be hosted. */
|
|
493
|
-
crxUrl?: string;
|
|
494
|
-
/** The [updates XML file](https://developer.chrome.com/docs/extensions/how-to/distribute/host-on-linux). */
|
|
495
|
-
updateXML?: string | null;
|
|
496
|
-
/** The extension's version. */
|
|
497
|
-
extVersion?: string | null;
|
|
498
|
-
/** The minimum Chrome version the extension requires. */
|
|
499
|
-
minChromeVersion?: string | null;
|
|
500
|
-
/** The extension's [manifest](https://developer.chrome.com/docs/extensions/reference/manifest). */
|
|
501
|
-
manifest?: ChromeManifest | null;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
type SetKeys<A extends object, B extends object> = {[x in keyof A | keyof B]: x extends keyof B ? unknown extends B[x] ? x extends keyof A ? A[x] : never : B[x] : x extends keyof A ? A[x] : never};
|
|
505
|
-
|
|
506
|
-
export type TransformPack<I extends PackInput> =
|
|
507
|
-
I["privateKey"] extends string ?
|
|
508
|
-
TransformPack<SetKeys<I, {
|
|
509
|
-
privateKey: Uint8Array | undefined;
|
|
510
|
-
rsa: RSA;
|
|
511
|
-
}>>
|
|
512
|
-
: I["publicKey"] extends string ?
|
|
513
|
-
TransformPack<SetKeys<I, {
|
|
514
|
-
publicKey: Uint8Array | undefined;
|
|
515
|
-
}>>
|
|
516
|
-
: I["updateXML"] extends null ?
|
|
517
|
-
undefined extends I["crxUrl"] ? never : TransformPack<SetKeys<I, {
|
|
518
|
-
updateXML: string;
|
|
519
|
-
id: undefined extends I["id"] ? null : I["id"];
|
|
520
|
-
extVersion: undefined extends I["extVersion"] ? null : I["extVersion"];
|
|
521
|
-
minChromeVersion: undefined extends I["minChromeVersion"] ? null : I["minChromeVersion"];
|
|
522
|
-
}>>
|
|
523
|
-
: I["extVersion"] extends null ?
|
|
524
|
-
TransformPack<SetKeys<I, {
|
|
525
|
-
extVersion: string;
|
|
526
|
-
manifest: undefined extends I["manifest"] ? null : I["manifest"];
|
|
527
|
-
}>>
|
|
528
|
-
: I["minChromeVersion"] extends null ?
|
|
529
|
-
TransformPack<SetKeys<I, {
|
|
530
|
-
minChromeVersion: string | undefined;
|
|
531
|
-
manifest: undefined extends I["manifest"] ? null : I["manifest"];
|
|
532
|
-
}>>
|
|
533
|
-
: I["manifest"] extends null ?
|
|
534
|
-
undefined extends I["contents"] ? never
|
|
535
|
-
: Uint8Array extends I["contents"] ? never
|
|
536
|
-
: TransformPack<SetKeys<I, {
|
|
537
|
-
manifest: ChromeManifest;
|
|
538
|
-
}>>
|
|
539
|
-
: I["crx"] extends null ?
|
|
540
|
-
undefined extends I["contents"] ? never : TransformPack<SetKeys<I, {
|
|
541
|
-
crx: Uint8Array;
|
|
542
|
-
privateKey: undefined extends I["privateKey"] ? null : I["privateKey"];
|
|
543
|
-
publicKey: undefined extends I["publicKey"] ? null : I["publicKey"];
|
|
544
|
-
}>>
|
|
545
|
-
: I["id"] extends null ?
|
|
546
|
-
TransformPack<SetKeys<I, {
|
|
547
|
-
id: string;
|
|
548
|
-
publicKey: undefined extends I["publicKey"] ? null : I["publicKey"];
|
|
549
|
-
}>>
|
|
550
|
-
: I["publicKey"] extends null ?
|
|
551
|
-
TransformPack<SetKeys<I, {
|
|
552
|
-
publicKey: Uint8Array;
|
|
553
|
-
privateKey: undefined extends I["privateKey"] ? null : I["privateKey"];
|
|
554
|
-
}>>
|
|
555
|
-
: I["privateKey"] extends null ?
|
|
556
|
-
TransformPack<SetKeys<I, {
|
|
557
|
-
privateKey: Uint8Array;
|
|
558
|
-
rsa: RSA;
|
|
559
|
-
}>>
|
|
560
|
-
: I["contents"] extends string ?
|
|
561
|
-
TransformPack<SetKeys<I, {
|
|
562
|
-
contents: Uint8Array;
|
|
563
|
-
manifest: ChromeManifest;
|
|
564
|
-
}>>
|
|
565
|
-
: I;
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Use the entirety of this API in one function.
|
|
569
|
-
*
|
|
570
|
-
* @param options An object containing input parameters.
|
|
571
|
-
*
|
|
572
|
-
* If `null` is given for a property, then the function will generate a value for it based on the other properties.
|
|
573
|
-
*
|
|
574
|
-
* If a property requires another but that property is not requested (with `null`), then it is generated and given anyways.
|
|
575
|
-
*
|
|
576
|
-
* If `privateKey` or `publicKey` are strings, `pack` will load the file at each path as a key. If the extension is `.pem`, they are loaded as pkcs8-pem and converted to pkcs8-der.
|
|
577
|
-
*
|
|
578
|
-
* `pack` always returns a `Promise`, even if all of the operations inside are synchronous.
|
|
579
|
-
*
|
|
580
|
-
* However, the return result is the same object as the input - just with the properties modified - so if you *really* want synchronous operations, you can keep a reference to the input object, call the function, and access the synchronous results from that object. Just make sure you don't set `privateKey` or `manifest` to `null` or `contents` to a string, otherwise some of your values might not arrive synchronously.
|
|
581
|
-
*/
|
|
582
|
-
export async function pack<I extends PackInput>(options: I): Promise<TransformPack<I>> {
|
|
583
|
-
if (typeof options.privateKey == "string") {
|
|
584
|
-
try {
|
|
585
|
-
const isPem = options.privateKey.endsWith(".pem");
|
|
586
|
-
const contents = await fs.readFile(options.privateKey);
|
|
587
|
-
if (isPem) options.privateKey = Uint8Array.from((options.rsa ??= new RSA(contents, "pkcs8-private-pem")).exportKey("pkcs8-private-der"));
|
|
588
|
-
else options.privateKey = Uint8Array.from(contents);
|
|
589
|
-
} catch (e) {
|
|
590
|
-
if (!`${e}`.includes("ENOENT")) throw e;
|
|
591
|
-
options.privateKey = undefined;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
if (typeof options.publicKey == "string") {
|
|
595
|
-
try {
|
|
596
|
-
const isPem = options.publicKey.endsWith(".pem");
|
|
597
|
-
const contents = await fs.readFile(options.publicKey);
|
|
598
|
-
if (isPem) options.publicKey = Uint8Array.from(new RSA(contents, "pkcs8-public-pem").exportKey("pkcs8-public-der"));
|
|
599
|
-
else options.publicKey = Uint8Array.from(contents);
|
|
600
|
-
} catch (e) {
|
|
601
|
-
if (!`${e}`.includes("ENOENT")) throw e;
|
|
602
|
-
options.publicKey = undefined;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
if (options.updateXML === null) {
|
|
606
|
-
if (options.crxUrl === undefined) throw new Error("crxUrl must be defined to generate updateXML");
|
|
607
|
-
if (options.id === undefined) options.id = null;
|
|
608
|
-
if (options.extVersion === undefined) options.extVersion = null;
|
|
609
|
-
if (options.minChromeVersion === undefined) options.minChromeVersion = null;
|
|
610
|
-
}
|
|
611
|
-
if (options.extVersion === null) {
|
|
612
|
-
if (options.manifest === undefined) options.manifest = null;
|
|
613
|
-
}
|
|
614
|
-
if (options.minChromeVersion === null) {
|
|
615
|
-
if (options.manifest === undefined) options.manifest = null;
|
|
616
|
-
}
|
|
617
|
-
if (options.crx === null) {
|
|
618
|
-
if (options.contents === undefined) throw new Error("contents must be defined to generate crx");
|
|
619
|
-
if (options.privateKey === undefined) options.privateKey = null;
|
|
620
|
-
if (options.publicKey === undefined) options.publicKey = null;
|
|
621
|
-
}
|
|
622
|
-
if (options.id === null) {
|
|
623
|
-
if (options.publicKey === undefined) options.publicKey = null;
|
|
624
|
-
}
|
|
625
|
-
if (options.publicKey === null) {
|
|
626
|
-
if (options.privateKey === undefined) options.privateKey = null;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (options.privateKey === null) {
|
|
630
|
-
options.rsa ??= new RSA({b: options.keySize || 4096});
|
|
631
|
-
options.privateKey = Uint8Array.from(options.rsa.exportKey("pkcs8-private-der"));
|
|
632
|
-
options.publicKey = Uint8Array.from(options.rsa.exportKey("pkcs8-public-der"));
|
|
633
|
-
}
|
|
634
|
-
if (options.publicKey === null) {
|
|
635
|
-
options.publicKey = Uint8Array.from((options.rsa ??= new RSA(Buffer.from(options.privateKey!), "pkcs8-private-der")).exportKey("pkcs8-public-der"));
|
|
636
|
-
}
|
|
637
|
-
if (typeof options.contents == "string") {
|
|
638
|
-
({contents: options.contents, manifest: options.manifest} = await packContents(options.contents));
|
|
639
|
-
}
|
|
640
|
-
if (options.crx === null) {
|
|
641
|
-
if (options.crxVersion == 3 || options.crxVersion == undefined) options.crx = packCrx3(options.privateKey!, options.publicKey!, options.contents!, options.rsa);
|
|
642
|
-
else if (options.crxVersion == 2) options.crx = packCrx2(options.privateKey!, options.publicKey!, options.contents!, options.rsa);
|
|
643
|
-
}
|
|
644
|
-
if (options.id === null) {
|
|
645
|
-
options.id = generateCrxId(options.publicKey!);
|
|
646
|
-
}
|
|
647
|
-
if (options.extVersion === null) {
|
|
648
|
-
if (!options.manifest) throw new Error("manifest must be defined to generate extVersion");
|
|
649
|
-
options.extVersion = options.manifest.version;
|
|
650
|
-
}
|
|
651
|
-
if (options.minChromeVersion === null) {
|
|
652
|
-
options.minChromeVersion = options.manifest?.minimum_chrome_version || (options.crxVersion == 3 || options.crxVersion == undefined ? "73.0.3683" : undefined);
|
|
653
|
-
}
|
|
654
|
-
if (options.updateXML === null) {
|
|
655
|
-
options.updateXML = generateUpdateXML(options.id!, options.crxUrl!, options.extVersion!, options.minChromeVersion);
|
|
656
|
-
}
|
|
657
|
-
return options as any;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
export default pack;
|
package/tsconfig.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
// Enable latest features
|
|
4
|
-
"lib": ["ESNext"],
|
|
5
|
-
"target": "ESNext",
|
|
6
|
-
"module": "ESNext",
|
|
7
|
-
"moduleDetection": "force",
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
|
-
"allowJs": true,
|
|
10
|
-
|
|
11
|
-
// Bundler mode
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"allowImportingTsExtensions": true,
|
|
14
|
-
"verbatimModuleSyntax": true,
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
|
|
17
|
-
// Best practices
|
|
18
|
-
"strict": true,
|
|
19
|
-
"skipLibCheck": true,
|
|
20
|
-
"noFallthroughCasesInSwitch": true,
|
|
21
|
-
|
|
22
|
-
// Some stricter flags (disabled by default)
|
|
23
|
-
"noUnusedLocals": false,
|
|
24
|
-
"noUnusedParameters": false,
|
|
25
|
-
"noPropertyAccessFromIndexSignature": false
|
|
26
|
-
}
|
|
27
|
-
}
|