pack-crx 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2024 Ant_Throw_Pology
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # pack-crx
2
+
3
+ A(nother) Chrome Extension packager in ESM and TypeScript.
4
+
5
+ Use of the file system is opt-in, so you can use only `Uint8Array`s if you want/need to.
6
+
7
+ Fun fact: If you create two extensions from the same private key, they will have the same ID. **Do not do this. Chrome will (probably) treat them as the same extension - one as an update to another.**
8
+
9
+ Credit to [thom4parisot/crx](https://github.com/thom4parisot/crx) for some parts of the code.
10
+
11
+ ## Example
12
+
13
+ ```ts
14
+ import pack, { generateUpdateXML } from "pack-crx";
15
+ import { serve, type ServeOptions } from "bun";
16
+ import { constants, writeFile } from "node:fs/promises";
17
+
18
+ const {
19
+ crx,
20
+ id: crxId,
21
+ manifest,
22
+ rsa
23
+ } = await pack({
24
+ contents: "./extension", // path to extension root
25
+ privateKey: "./key.pem", // path to key (if it doesn't exist, that's fine)
26
+ crx: null,
27
+ id: null
28
+ });
29
+
30
+ // Write the CRX
31
+ await writeFile("./extension.crx", crx);
32
+
33
+ // Write the private key unless it already exists
34
+ try {
35
+ await writeFile("./key.pem", rsa.exportKey("pkcs8-private-pem"), {flag: constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY});
36
+ } catch (e) {}
37
+
38
+ let updateXML: string;
39
+
40
+ // Serve the extension (optional)
41
+ const server = serve({
42
+ fetch(request, server) {
43
+ const url = new URL(request.url);
44
+ if (url.pathname.startsWith("/updates.xml")) {
45
+ updateXML ??= generateUpdateXML(crxId, server.url.toString() + "extension.crx", manifest.version);
46
+ return new Response(updateXML, {headers: {"Content-Type": "application/xml"}});
47
+ } else if (url.pathname.startsWith("/extension.crx")) {
48
+ return new Response(crx, {headers: {"Content-Type": "application/x-chrome-extension"}});
49
+ }
50
+ return new Response(undefined, {status: 404, statusText: "Not Found"});
51
+ },
52
+ port: 3000,
53
+ hostname: "localhost"
54
+ } as ServeOptions);
55
+
56
+ console.log(`Server opened at ${server.url}`);
57
+ ```
58
+
59
+ ## API
60
+
61
+ **IMPORTANT:** All keys passed to this package should be in **pkcs8-der** format. This is also the format in which they will be returned.
62
+
63
+ ### pack
64
+
65
+ The easiest way to use this package is through the default export, `pack`.
66
+
67
+ `pack` takes an object of input parameters, **all of which are optional:**
68
+
69
+ <!-- It is highly recommended to turn off line wrapping while editing this table -->
70
+
71
+ | Name | Type (also can be `undefined`) | Description | Auto-generation notes |
72
+ |--------------------|----------------------------------|-------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
73
+ | `contents` | `Uint8Array` or `string` | The ZIP archive of the contents of the extension, or a path to the folder containing the extension. | Cannot be auto-generated |
74
+ | `privateKey` | `Uint8Array`, `string` or `null` | The private key for the extension, or a path to it. | If loading from a string, the contents should be in pkcs8-pem if the path ends in `.pem`. |
75
+ | `keySize` | `number` | The size of key to generate, if needed. | No dependencies, defaults to 4096 |
76
+ | `publicKey` | `Uint8Array`, `string` or `null` | The public key for the extension, or a path to it. | Requires `privateKey`. If loading from a string, the contents should be in pkcs8-pem if the path ends in `.pem`. |
77
+ | `rsa` | `NodeRSA` | The instance of NodeRSA to use. | Automatically created along with `privateKey`. |
78
+ | `id` | `string` or `null` | The extension's ID. | Requires `publicKey`. |
79
+ | `crx` | `Uint8Array` or `null` | The outputted CRX file. | Requires `contents`, `privateKey`, and `publicKey`. |
80
+ | `crxVersion` | `number` | The CRX format version to use. Defaults to 3. | Defaults to 3. |
81
+ | `crxUrl` | `string` | The URL to where the CRX file (not the updates XML) will be hosted. | Cannot be auto-generated. |
82
+ | `updateXML` | `string` or `null` | The [updates XML file](https://developer.chrome.com/docs/extensions/how-to/distribute/host-on-linux). | Requires `id`, `extVersion`, and `minChromeVersion`. |
83
+ | `extVersion` | `string` or `null` | The extension's version. | Requires `manifest`. |
84
+ | `minChromeVersion` | `string` or `null` | The minimum Chrome version the extension requires. | Requires `manifest`. (but can also be derived from `crxVersion`) |
85
+ | `manifest` | `ChromeManifest` or `null` | The extension's [manifest](https://developer.chrome.com/docs/extensions/reference/manifest). | Requires `contents` to be a path string. |
86
+
87
+ If `null` is given for a property, then the function will generate a value for it based on the other properties.
88
+
89
+ If a property requires another but that property is not requested (with `null`), then it is generated and given anyways.
90
+
91
+ 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.
92
+
93
+ `pack` always returns a `Promise`, even if all of the operations inside are synchronous.
94
+
95
+ 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.
96
+
97
+ ### packCrx3
98
+
99
+ ```ts
100
+ function packCrx3(privateKey: Uint8Array, publicKey: Uint8Array, contents: Uint8Array): Uint8Array
101
+ ```
102
+
103
+ Pack a CRX3 extension.
104
+
105
+ (param) `privateKey` (`Uint8Array`) - The extension's private key. \
106
+ (param) `publicKey` (`Uint8Array`) - The extension's public key. \
107
+ (param) `contents` (`Uint8Array`) - 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.
108
+
109
+ (returns) `Uint8Array` - The contents of the packaged extension.
110
+
111
+ ### packCrx2 (deprecated)
112
+
113
+ ```ts
114
+ function packCrx2(privateKey: Uint8Array, publicKey: Uint8Array, contents: Uint8Array): Uint8Array
115
+ ```
116
+
117
+ Pack a CRX2 extension. Chrome stopped supporting these entirely in version 73.0.3683, which released in October of 2017.
118
+
119
+ (param) `privateKey` (`Uint8Array`) - The extension's private key. \
120
+ (param) `publicKey` (`Uint8Array`) - The extension's public key. \
121
+ (param) `contents` (`Uint8Array`) - 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.
122
+
123
+ (returns) `Uint8Array` - The contents of the packaged extension.
124
+
125
+ ### generateCrxId
126
+
127
+ ```ts
128
+ function generateCrxId(publicKey: Uint8Array): string
129
+ ```
130
+
131
+ Generate an extension's ID (32 characters, a-p) from its public key.
132
+
133
+ (param) `publicKey` (`Uint8Array`) - The public key of the extension.
134
+
135
+ (returns) `string` - The generated extension ID.
136
+
137
+ ### packContents
138
+
139
+ ```ts
140
+ function packContents(where: string): Promise<{contents: Uint8Array, manifest: ChromeManifest}>
141
+ ```
142
+
143
+ Load a directory from the filesystem into a ZIP archive, using the node:fs API.
144
+
145
+ (param) `where` (`string`) - The path to the directory.
146
+
147
+ (returns) `object`
148
+ * `contents` (`Uint8Array`) - The ZIP-encoded data.
149
+ * `manifest` (`ChromeManifest`) - The manifest for the extension, parsed as JSON.
150
+
151
+ ### generateUpdateXML
152
+
153
+ ```ts
154
+ function generateUpdateXML(crxId: string, url: string, version: string, minChromeVersion?: string): string
155
+ ```
156
+
157
+ Generate the updates XML file for [serving an extension yourself](https://developer.chrome.com/docs/extensions/how-to/distribute/host-on-linux).
158
+
159
+ (param) `crxId` (`string`) - The extension's ID. \
160
+ (param) `url` (`string`) - The URL where the extension's CRX file will be hosted. \
161
+ (param) `version` (`string`) - The extension's version. \
162
+ (param) `minChromeVersion` (`string`, optional) - The minimum Chrome version that the extension can be installed on.
163
+
164
+ (returns) `string` - The updates XML text
165
+
166
+ ### unpack
167
+
168
+ ```ts
169
+ function unpack(crx: Uint8Array): Uint8Array
170
+ ```
171
+
172
+ Unpack a CRX file and extract its contents as ZIP data.
173
+
174
+ (param) `crx` (`Uint8Array`) - The CRX to be unpacked.
175
+
176
+ (returns) `object`
177
+ - `archive` (`Uint8Array`) - The ZIP data.
178
+ - `crxVersion` (`3`) - The CRX format version.
179
+ - `header` (`CrxFileHeader`) - The header for the CRX file, for signatures and things.
180
+ OR
181
+ - `archive` (`Uint8Array`) - The ZIP data.
182
+ - `crxVersion` (`2`) - The CRX format version.
183
+ - `key` (`Uint8Array`) - The extension's public key.
184
+ - `sign` (`Uint8Array`) - The signature over the contents of the extension.
185
+
186
+ ### Key utilities
187
+
188
+ The following functions are self-explanatory:
189
+
190
+ ```ts
191
+ function generatePrivateKey(bits?: number): Uint8Array
192
+ function generatePublicKey(privateKey: Uint8Array): Uint8Array
193
+ function convertToPem(key: Uint8Array, type: "private" | "public"): string
194
+ function convertFromPem(key: string, type: "private" | "public"): Uint8Array
195
+ ```
196
+
197
+ However, they all create new NodeRSA instances which are immediately discarded, so it is recommended to make your own RSA instance (or use the one from `pack`) and its methods to export/import keys.
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "pack-crx",
3
+ "module": "src/index.ts",
4
+ "type": "module",
5
+ "version": "1.0.0",
6
+ "devDependencies": {
7
+ "@types/bun": "latest",
8
+ "@types/node-rsa": "^1.1.4"
9
+ },
10
+ "dependencies": {
11
+ "jszip": "^3.10.1",
12
+ "node-rsa": "^1.1.1",
13
+ "pbf": "^4.0.1"
14
+ },
15
+ "types": "src/index.d.ts"
16
+ }
package/src/crx3.pb.js ADDED
@@ -0,0 +1,47 @@
1
+ // CrxFileHeader ========================================
2
+
3
+ export var CrxFileHeader = {};
4
+
5
+ CrxFileHeader.read = function (pbf, end) {
6
+ return pbf.readFields(CrxFileHeader._readField, {sha256_with_rsa: [], sha256_with_ecdsa: [], signed_header_data: null}, end);
7
+ };
8
+ CrxFileHeader._readField = function (tag, obj, pbf) {
9
+ if (tag === 2) obj.sha256_with_rsa.push(AsymmetricKeyProof.read(pbf, pbf.readVarint() + pbf.pos));
10
+ else if (tag === 3) obj.sha256_with_ecdsa.push(AsymmetricKeyProof.read(pbf, pbf.readVarint() + pbf.pos));
11
+ else if (tag === 10000) obj.signed_header_data = pbf.readBytes();
12
+ };
13
+ CrxFileHeader.write = function (obj, pbf) {
14
+ if (obj.sha256_with_rsa) for (var i = 0; i < obj.sha256_with_rsa.length; i++) pbf.writeMessage(2, AsymmetricKeyProof.write, obj.sha256_with_rsa[i]);
15
+ if (obj.sha256_with_ecdsa) for (i = 0; i < obj.sha256_with_ecdsa.length; i++) pbf.writeMessage(3, AsymmetricKeyProof.write, obj.sha256_with_ecdsa[i]);
16
+ if (obj.signed_header_data) pbf.writeBytesField(10000, obj.signed_header_data);
17
+ };
18
+
19
+ // AsymmetricKeyProof ========================================
20
+
21
+ export var AsymmetricKeyProof = {};
22
+
23
+ AsymmetricKeyProof.read = function (pbf, end) {
24
+ return pbf.readFields(AsymmetricKeyProof._readField, {public_key: null, signature: null}, end);
25
+ };
26
+ AsymmetricKeyProof._readField = function (tag, obj, pbf) {
27
+ if (tag === 1) obj.public_key = pbf.readBytes();
28
+ else if (tag === 2) obj.signature = pbf.readBytes();
29
+ };
30
+ AsymmetricKeyProof.write = function (obj, pbf) {
31
+ if (obj.public_key) pbf.writeBytesField(1, obj.public_key);
32
+ if (obj.signature) pbf.writeBytesField(2, obj.signature);
33
+ };
34
+
35
+ // SignedData ========================================
36
+
37
+ export var SignedData = {};
38
+
39
+ SignedData.read = function (pbf, end) {
40
+ return pbf.readFields(SignedData._readField, {crx_id: null}, end);
41
+ };
42
+ SignedData._readField = function (tag, obj, pbf) {
43
+ if (tag === 1) obj.crx_id = pbf.readBytes();
44
+ };
45
+ SignedData.write = function (obj, pbf) {
46
+ if (obj.crx_id) pbf.writeBytesField(1, obj.crx_id);
47
+ };
package/src/index.d.ts ADDED
@@ -0,0 +1,320 @@
1
+ import type NodeRSA from "node-rsa";
2
+
3
+ interface ChromeBaseManifest {
4
+ // Required keys
5
+ manifest_version: number;
6
+ name: string;
7
+ version: string;
8
+
9
+ // Required by Chrome Web Store
10
+ description?: string;
11
+ icons?: {[x: `${number}`]: string};
12
+
13
+ // Optional
14
+ author?: string;
15
+ background?: {
16
+ service_worker?: string;
17
+ type?: "module";
18
+ };
19
+ chrome_settings_overrides?: {
20
+ alternate_urls?: string[];
21
+ encoding?: string;
22
+ favicon_url?: string;
23
+ homepage?: string;
24
+ image_url?: string;
25
+ image_url_post_params?: string;
26
+ is_default?: boolean;
27
+ keyword?: string;
28
+ name?: string;
29
+ prepopulated_id?: number;
30
+ search_provider?: object;
31
+ search_url?: string;
32
+ search_url_post_params?: string;
33
+ startup_pages?: string[];
34
+ suggest_url?: string;
35
+ suggest_url_post_params?: string;
36
+ };
37
+ chrome_url_overrides?: {
38
+ bookmarks?: string;
39
+ history?: string;
40
+ newtab?: string;
41
+ };
42
+ commands?: {
43
+ [x: string]: {
44
+ description: string;
45
+ suggested_key?: string;
46
+ }
47
+ };
48
+ content_scripts?: {
49
+ matches: string[];
50
+ css?: string[];
51
+ js?: string[];
52
+ run_at?: "document_start" | "document_end" | "document_idle";
53
+ match_about_blank?: boolean;
54
+ match_origin_as_fallback?: boolean;
55
+ world?: "ISOLATED" | "MAIN";
56
+ }[];
57
+ content_security_policy?: {
58
+ extension_pages?: string;
59
+ sandbox?: string;
60
+ };
61
+ cross_origin_embedder_policy?: string;
62
+ cross_origin_opener_policy?: string;
63
+ declarative_net_request?: {
64
+ rule_resources?: {
65
+ id: string;
66
+ enabled: boolean;
67
+ path: string;
68
+ }[];
69
+ };
70
+ default_locale?: string;
71
+ devtools_page?: string;
72
+ export?: {
73
+ allowlist?: string[];
74
+ };
75
+ externally_connectable?: {
76
+ ids?: string[];
77
+ matches?: string[];
78
+ accepts_tls_channel_id?: boolean;
79
+ };
80
+ homepage_url?: string;
81
+ host_permissions?: string[];
82
+ import?: {
83
+ id: string;
84
+ minimum_version?: string;
85
+ }[];
86
+ incognito?: "spanning" | "split" | "not_allowed";
87
+ key?: string;
88
+ minimum_chrome_version?: string;
89
+ oauth2?: {
90
+ client_id: string;
91
+ scopes: string[];
92
+ };
93
+ omnibox?: {
94
+ keyword?: string;
95
+ };
96
+ optional_host_permissions?: string[];
97
+ optional_permissions?: string[];
98
+ options_page?: string;
99
+ options_ui?: {
100
+ page: string;
101
+ open_in_tab?: boolean;
102
+ };
103
+ permissions?: string[];
104
+ requirements?: {
105
+ [x: string]: {features: string[]};
106
+ };
107
+ sandbox?: {
108
+ pages: string[];
109
+ };
110
+ short_name?: string;
111
+ side_panel?: string;
112
+ storage?: {
113
+ managed_schema?: string;
114
+ };
115
+ tts_engine?: {
116
+ voices?: {
117
+ voice_name: string;
118
+ lang?: string;
119
+ event_types?: ("start" | "word" | "sentence" | "marker" | "end" | "error")[];
120
+ }[];
121
+ };
122
+ update_url?: string;
123
+ version_name?: string;
124
+ web_accessible_resources?: ({
125
+ resources: string[];
126
+ } & ({
127
+ matches: string[];
128
+ } | {
129
+ extension_ids: string[];
130
+ }))[];
131
+
132
+ // ChromeOS
133
+ file_browser_handlers?: {
134
+ id: string;
135
+ default_title: string;
136
+ file_filters: string[];
137
+ }[];
138
+ file_handlers?: {
139
+ action: string;
140
+ name: string;
141
+ accept: {
142
+ [x: string]: string[];
143
+ };
144
+ launch_type?: "single-client" | "multiple-clients";
145
+ }[];
146
+ file_system_provider_capabilities?: {
147
+ configurable?: boolean;
148
+ multiple_mounts?: boolean;
149
+ watchable?: boolean;
150
+ source: "file" | "device" | "network";
151
+ };
152
+ input_components?: {
153
+ name: string;
154
+ id?: string;
155
+ language?: string | string[];
156
+ layouts?: string | string[];
157
+ input_view?: string;
158
+ options_page?: string;
159
+ }[];
160
+ }
161
+
162
+ export type ChromeMV2Manifest = ChromeBaseManifest & {
163
+ manifest_version: 2;
164
+ browser_action?: {
165
+ default_icon?: {[x: `${number}`]: string};
166
+ default_title?: string;
167
+ default_popup?: string;
168
+ };
169
+ page_action?: {
170
+ default_icon?: {[x: `${number}`]: string};
171
+ default_title?: string;
172
+ default_popup?: string;
173
+ };
174
+ };
175
+
176
+ export type ChromeMV3Manifest = ChromeBaseManifest & {
177
+ manifest_version: 3;
178
+ action?: {
179
+ default_icon?: {[x: `${number}`]: string};
180
+ default_title?: string;
181
+ default_popup?: string;
182
+ };
183
+ };
184
+
185
+ export type ChromeManifest = ChromeMV2Manifest | ChromeMV3Manifest;
186
+
187
+ export interface CrxFileHeader {
188
+ sha256_with_rsa?: AsymmetricKeyProof[];
189
+ sha256_with_ecdsa?: AsymmetricKeyProof[];
190
+ signed_header_data?: Uint8Array;
191
+ }
192
+
193
+ export interface AsymmetricKeyProof {
194
+ public_key?: Uint8Array;
195
+ signature?: Uint8Array;
196
+ }
197
+
198
+ export function packCrx2(privateKey: Uint8Array, publicKey: Uint8Array, contents: Uint8Array, rsa?: NodeRSA): Uint8Array;
199
+ export function packCrx3(privateKey: Uint8Array, publicKey: Uint8Array, contents: Uint8Array, rsa?: NodeRSA): Uint8Array;
200
+ export function generateCrxId(publicKey: Uint8Array): string;
201
+ export function packContents(where: string): Promise<{
202
+ /** The ZIP-encoded data. */
203
+ contents: Uint8Array,
204
+ /** The manifest for the extension, parsed as JSON. */
205
+ manifest: ChromeManifest
206
+ }>;
207
+ export function generateUpdateXML(crxId: string, url: string, version: string, minChromeVersion?: string): string;
208
+ export function generatePrivateKey(bits?: number): Uint8Array;
209
+ export function generatePublicKey(privateKey: Uint8Array): Uint8Array;
210
+ export function convertToPem(key: Uint8Array, type: "private" | "public"): string;
211
+ export function convertFromPem(key: string, type: "private" | "public"): Uint8Array;
212
+ export function unpack(crx: Uint8Array): {
213
+ /** The ZIP data. */
214
+ archive: Uint8Array,
215
+ /** The CRX format version. */
216
+ crxVersion: 2,
217
+ /** The extension's public key. */
218
+ key: Uint8Array,
219
+ /** The signature over the contents of the extension. */
220
+ sign: Uint8Array
221
+ } | {
222
+ /** The ZIP data. */
223
+ archive: Uint8Array,
224
+ /** The CRX format version. */
225
+ crxVersion: 3,
226
+ /** The header for the CRX file, for signatures and things. */
227
+ header: CrxFileHeader
228
+ };
229
+ export interface PackInput {
230
+ /** The ZIP archive of the contents of the extension, or a path to the folder containing the extension. */
231
+ contents?: Uint8Array | string;
232
+ /** The private key for the extension, or a path to it. */
233
+ privateKey?: Uint8Array | string | null;
234
+ /** The size of key to generate, if needed. */
235
+ keySize?: number;
236
+ /** The public key for the extension, or a path to it. */
237
+ publicKey?: Uint8Array | string | null;
238
+ /** The instance of NodeRSA to use. */
239
+ rsa?: NodeRSA;
240
+ /** The extension's ID. */
241
+ id?: string | null;
242
+ /** The outputted CRX file. */
243
+ crx?: Uint8Array | null;
244
+ /** The CRX format version to use. Defaults to 3. */
245
+ crxVersion?: number;
246
+ /** The URL to where the CRX file (not the updates XML) will be hosted. */
247
+ crxUrl?: string;
248
+ /** The [updates XML file](https://developer.chrome.com/docs/extensions/how-to/distribute/host-on-linux). */
249
+ updateXML?: string | null;
250
+ /** The extension's version. */
251
+ extVersion?: string | null;
252
+ /** The minimum Chrome version the extension requires. */
253
+ minChromeVersion?: string | null;
254
+ /** The extension's [manifest](https://developer.chrome.com/docs/extensions/reference/manifest). */
255
+ manifest?: ChromeManifest | null;
256
+ }
257
+ 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};
258
+
259
+ export type TransformPack<I extends PackInput> =
260
+ I["publicKey"] extends string ?
261
+ TransformPack<SetKeys<I, {
262
+ publicKey: Uint8Array | undefined;
263
+ rsa: NodeRSA;
264
+ }>>
265
+ : I["privateKey"] extends string ?
266
+ TransformPack<SetKeys<I, {
267
+ privateKey: Uint8Array | undefined;
268
+ }>>
269
+ : I["updateXML"] extends null ?
270
+ undefined extends I["crxUrl"] ? never : TransformPack<SetKeys<I, {
271
+ updateXML: string;
272
+ id: undefined extends I["id"] ? null : I["id"];
273
+ extVersion: undefined extends I["extVersion"] ? null : I["extVersion"];
274
+ minChromeVersion: undefined extends I["minChromeVersion"] ? null : I["minChromeVersion"];
275
+ }>>
276
+ : I["extVersion"] extends null ?
277
+ TransformPack<SetKeys<I, {
278
+ extVersion: string;
279
+ manifest: undefined extends I["manifest"] ? null : I["manifest"];
280
+ }>>
281
+ : I["minChromeVersion"] extends null ?
282
+ TransformPack<SetKeys<I, {
283
+ minChromeVersion: string | undefined;
284
+ manifest: undefined extends I["manifest"] ? null : I["manifest"];
285
+ }>>
286
+ : I["manifest"] extends null ?
287
+ undefined extends I["contents"] ? never
288
+ : Uint8Array extends I["contents"] ? never
289
+ : TransformPack<SetKeys<I, {
290
+ manifest: ChromeManifest;
291
+ }>>
292
+ : I["crx"] extends null ?
293
+ undefined extends I["contents"] ? never : TransformPack<SetKeys<I, {
294
+ crx: Uint8Array;
295
+ privateKey: undefined extends I["privateKey"] ? null : I["privateKey"];
296
+ publicKey: undefined extends I["publicKey"] ? null : I["publicKey"];
297
+ }>>
298
+ : I["id"] extends null ?
299
+ TransformPack<SetKeys<I, {
300
+ id: string;
301
+ publicKey: undefined extends I["publicKey"] ? null : I["publicKey"];
302
+ }>>
303
+ : I["publicKey"] extends null ?
304
+ TransformPack<SetKeys<I, {
305
+ publicKey: Uint8Array;
306
+ privateKey: undefined extends I["privateKey"] ? null : I["privateKey"];
307
+ }>>
308
+ : I["privateKey"] extends null ?
309
+ TransformPack<SetKeys<I, {
310
+ privateKey: Uint8Array;
311
+ rsa: NodeRSA;
312
+ }>>
313
+ : I["contents"] extends string ?
314
+ TransformPack<SetKeys<I, {
315
+ contents: Uint8Array;
316
+ manifest: ChromeManifest;
317
+ }>>
318
+ : I;
319
+ export function pack<I extends PackInput>(options: I): Promise<TransformPack<I>>;
320
+ export default pack;
package/src/index.ts ADDED
@@ -0,0 +1,660 @@
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["publicKey"] extends string ?
508
+ TransformPack<SetKeys<I, {
509
+ publicKey: Uint8Array | undefined;
510
+ rsa: RSA;
511
+ }>>
512
+ : I["privateKey"] extends string ?
513
+ TransformPack<SetKeys<I, {
514
+ privateKey: 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 ADDED
@@ -0,0 +1,27 @@
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
+ }