├── .eslintignore
├── .eslintrc.cjs
├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── packages
├── rollup-plugin-webbundle
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ └── index.ts
│ └── test
│ │ ├── fixtures
│ │ ├── index.js
│ │ └── static
│ │ │ └── index.html
│ │ ├── snapshots
│ │ ├── test.js.md
│ │ └── test.js.snap
│ │ └── test.js
├── shared
│ ├── iwa-headers.ts
│ ├── package.json
│ ├── test
│ │ └── test.js
│ ├── tsconfig.json
│ ├── types.ts
│ ├── utils.ts
│ └── wbn-types.ts
└── webbundle-webpack-plugin
│ ├── README.md
│ ├── index.cjs
│ ├── package.json
│ ├── src
│ └── index.ts
│ └── test
│ ├── fixtures
│ ├── app.js
│ └── static
│ │ └── index.html
│ └── test.js
└── tsconfig.json
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | lib/
3 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | module.exports = {
18 | env: {
19 | es2021: true,
20 | node: true,
21 | },
22 | plugins: ['@typescript-eslint', 'header'],
23 | extends: [
24 | 'eslint:recommended',
25 | 'plugin:@typescript-eslint/recommended',
26 | 'prettier',
27 | ],
28 | overrides: [],
29 | parser: '@typescript-eslint/parser',
30 | parserOptions: {
31 | ecmaVersion: 'latest',
32 | sourceType: 'module',
33 | },
34 | rules: {
35 | 'header/header': [
36 | 2,
37 | 'block',
38 | [
39 | // The exclamation mark marks this header as a license header, which
40 | // some tools, like bundlers, treat differently to normal headers.
41 | '!',
42 | {
43 | pattern: ' \\* Copyright \\d{4} Google LLC',
44 | template: ` * Copyright ${new Date().getFullYear()} Google LLC`,
45 | },
46 | ' *',
47 | ' * Licensed under the Apache License, Version 2.0 (the "License");',
48 | ' * you may not use this file except in compliance with the License.',
49 | ' * You may obtain a copy of the License at',
50 | ' *',
51 | ' * http://www.apache.org/licenses/LICENSE-2.0',
52 | ' *',
53 | ' * Unless required by applicable law or agreed to in writing, software',
54 | ' * distributed under the License is distributed on an "AS IS" BASIS,',
55 | ' * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.',
56 | ' * See the License for the specific language governing permissions and',
57 | ' * limitations under the License.',
58 | ' ',
59 | ],
60 | ],
61 | },
62 | };
63 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | defaults:
9 | run:
10 | working-directory: ./
11 |
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | test-case: [
16 | # Lowest supported versions are 1.21.0 for Rollup and 4.0.1 for Webpack.
17 | { node: 14.x, rollup: 1.21.0, webpack: 4.0.1 },
18 | { node: 14.x, rollup: ^1.21.0, webpack: ^4.0.1 },
19 | { node: 14.x, rollup: ^2.0.0, webpack: 5.0.x },
20 | { node: 14.x, rollup: ^3.0.0, webpack: ^5.0.0 },
21 |
22 | { node: 16.x, rollup: 1.21.x, webpack: 4.1.x },
23 | { node: 16.x, rollup: ^1.21.0, webpack: ^4.0.1 },
24 | { node: 16.x, rollup: ^2.0.0, webpack: 5.0.x },
25 | { node: 16.x, rollup: ^3.0.0, webpack: ^5.0.0 },
26 |
27 | # Node 18 only works with Webpack version ^5.61.x.
28 | { node: 18.x, rollup: 1.21.x, webpack: 5.61.x },
29 | { node: 18.x, rollup: ^1.21.0, webpack: ^5.0.0 },
30 | { node: 18.x, rollup: ^2.0.0, webpack: 5.61.x },
31 | { node: 18.x, rollup: ^3.0.0, webpack: ^5.0.0 },
32 | ]
33 |
34 | steps:
35 | - uses: actions/checkout@v3
36 |
37 | - name: Use Node.js ${{ matrix.test-case.node }}
38 | uses: actions/setup-node@v3
39 | with:
40 | node-version: ${{ matrix.test-case.node }}
41 | cache: 'npm'
42 |
43 | # Underlying wbn-sign requires to use npm v8+
44 | - run: npm install -g npm@8
45 |
46 | - name: Install deps
47 | run: npm ci
48 |
49 | - name: Uninstall old versions of Webpack
50 | run: npm uninstall -D @types/webpack webpack
51 | working-directory: ./packages/webbundle-webpack-plugin
52 |
53 | - name: Uninstall old versions of Rollup
54 | run: npm uninstall -D rollup
55 | working-directory: ./packages/rollup-plugin-webbundle
56 |
57 | - name: Install Rollup v${{ matrix.test-case.rollup }}
58 | run: npm install --save-dev rollup@${{ matrix.test-case.rollup }}
59 | working-directory: ./packages/rollup-plugin-webbundle
60 |
61 | - name: Install Webpack v${{ matrix.test-case.webpack }}
62 | run:
63 | npm install --save-dev webpack@${{ matrix.test-case.webpack }}
64 | @types/webpack
65 | working-directory: ./packages/webbundle-webpack-plugin
66 |
67 | - run: npm run build
68 | - run: npm run test
69 |
70 | lint:
71 | runs-on: ubuntu-latest
72 | steps:
73 | - uses: actions/checkout@v3
74 | - name: Use Node.js 18.x
75 | uses: actions/setup-node@v3
76 | with:
77 | node-version: 18.x
78 | cache: 'npm'
79 | - run: npm ci
80 | - run: npm run lint
81 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.tgz
3 | lib/
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | tsconfig.json
2 | .github
3 | test/
4 | src/
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | lib/
3 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | All Google open source projects are covered by our
4 | [community guidelines](https://opensource.google/conduct/) which define the kind
5 | of respectful behavior we expect of all participants.
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution;
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult
22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows
28 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/).
29 |
30 | ## Auto-formatting code
31 |
32 | The Github Actions workflow enforces linting code with Prettier according to the
33 | Prettier configs specified in the package.json.
34 |
35 | To lint your code locally before committing, one can run `npm run lint`.
36 |
37 | To enable running Prettier on save with VSCode, one can install the Prettier
38 | extension and then in VScode's settings have the following entries:
39 |
40 | ```json
41 | "editor.formatOnSave": true,
42 | "[javascript]": {
43 | "editor.defaultFormatter": "esbenp.prettier-vscode"
44 | }
45 | ```
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Web Bundle plugins
2 |
3 | Contains the code for Webpack and Rollup plugins to generate web bundles.
4 |
5 | See
6 | [packages/rollup-plugin-webbundle/README.md](./packages/rollup-plugin-webbundle/README.md)
7 | and
8 | [packages/webbundle-webpack-plugin/README.md](./packages/webbundle-webpack-plugin/README.md)
9 | for more details on how to use the plugins.
10 |
11 | ## Discuss & Help
12 |
13 | For discussions related to this repository's content, the Web Bundle plugins for
14 | webpack and rollup, please use
15 | [GitHub Issues](https://github.com/GoogleChromeLabs/webbundle-plugins/issues).
16 |
17 | If you'd like to discuss the Web Packaging proposal itself, consider opening an
18 | issue in its incubation repository at https://github.com/WICG/webpackage.
19 |
20 | For discussions related to Isolated Web Apps in general, or Chromium-specific
21 | implementation and development questions, please use the
22 | [iwa-dev@chromium.org](https://groups.google.com/a/chromium.org/g/iwa-dev)
23 | mailing list.
24 |
25 | If you'd like to discuss the Isolated Web Apps proposal, which builds on top of
26 | Web Bundles, consider opening an issue in the incubation repository at
27 | https://github.com/WICG/isolated-web-apps.
28 |
29 | ## License
30 |
31 | Licensed under the Apache-2.0 license.
32 |
33 | ## Contributing
34 |
35 | See [CONTRIBUTING.md](CONTRIBUTING.md) file.
36 |
37 | ## Disclaimer
38 |
39 | This is not an officially supported Google product.
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webbundle-plugins",
3 | "scripts": {
4 | "test": "npm run build && ava",
5 | "build": "npm run build --workspaces",
6 | "pack": "npm pack --workspace=rollup-plugin-webbundle --workspace=webbundle-webpack-plugin",
7 | "lint": "npm run lint:prettier && npm run lint:eslint",
8 | "lint:eslint": "eslint --ext .js,.ts,.cjs,.mjs .",
9 | "lint:prettier": "prettier --check {**/*,*}.{cjs,js,ts,md,json} --config ./package.json --no-error-on-unmatched-pattern",
10 | "format": "npm run format:prettier && npm run format:eslint",
11 | "format:eslint": "eslint --ext .js,.ts,.cjs,.mjs --fix .",
12 | "format:prettier": "prettier --write {**/*,*}.{cjs,js,ts,md,json} --config ./package.json --no-error-on-unmatched-pattern",
13 | "update-snapshots": "ava --update-snapshots"
14 | },
15 | "type": "module",
16 | "workspaces": [
17 | "packages/rollup-plugin-webbundle",
18 | "packages/shared",
19 | "packages/webbundle-webpack-plugin"
20 | ],
21 | "private": true,
22 | "license": "Apache-2.0",
23 | "devDependencies": {
24 | "@types/mime": "^3.0.1",
25 | "@types/node": "^18.16.0",
26 | "@typescript-eslint/eslint-plugin": "^5.59.1",
27 | "@typescript-eslint/parser": "^5.59.1",
28 | "ava": "^4.3.3",
29 | "esbuild": "^0.17.15",
30 | "eslint": "^8.37.0",
31 | "eslint-config-prettier": "^8.8.0",
32 | "eslint-plugin-header": "^3.1.1",
33 | "prettier": "2.8.0",
34 | "typescript": "^5.0.4"
35 | },
36 | "engines": {
37 | "node": ">= 16.0.0"
38 | },
39 | "prettier": {
40 | "tabWidth": 2,
41 | "semi": true,
42 | "singleQuote": true,
43 | "printWidth": 80,
44 | "proseWrap": "always"
45 | },
46 | "ava": {
47 | "workerThreads": false,
48 | "files": [
49 | "!**/fixtures/**"
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-webbundle/README.md:
--------------------------------------------------------------------------------
1 | # rollup-plugin-webbundle
2 |
3 | A Rollup plugin which generates
4 | [Web Bundles](https://wpack-wg.github.io/bundled-responses/draft-ietf-wpack-bundled-responses.html)
5 | output. Currently the spec is still a draft, so this package is also in alpha
6 | until the spec stabilizes.
7 |
8 | ## Requirements
9 |
10 | This plugin requires Node v14.0.0+ and Rollup v1.21.0+.
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```bash
17 | npm install rollup-plugin-webbundle --save-dev
18 | ```
19 |
20 | ## Usage
21 |
22 | ### General Web Bundle
23 |
24 | This example assumes your application entry point is `src/index.js` and static
25 | files (including `index.html`) are located in `static` directory.
26 |
27 | ```js
28 | /* rollup.config.mjs */
29 | import webbundle from 'rollup-plugin-webbundle';
30 |
31 | export default {
32 | input: 'src/index.js',
33 | output: {
34 | dir: 'dist',
35 | format: 'esm',
36 | },
37 | plugins: [
38 | webbundle({
39 | baseURL: 'https://example.com/',
40 | static: { dir: 'static' },
41 | }),
42 | ],
43 | };
44 | ```
45 |
46 | A WBN file `dist/out.wbn` should be written.
47 |
48 | ### [Isolated Web App](https://github.com/WICG/isolated-web-apps/blob/main/README.md) (Signed Web Bundle)
49 |
50 | This example assumes your application entry point is `src/index.js`, static
51 | files (including `index.html`) are located in `static` directory and you have a
52 | `.env` file in the root directory with `ED25519KEY` defined in it. The example
53 | also requires installing `dotenv` npm package as a dev dependency.
54 |
55 | It is also required to have a
56 | [Web App Manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) at
57 | `/.well-known/manifest.webmanifest`, which can be placed e.g. in the `static`
58 | directory.
59 |
60 | Also as in the below example, `baseURL` must be of format
61 | `isolated-app://${WEB_BUNDLE_ID}` for Isolated Web Apps. It can easily be
62 | generated from the private key with `WebBundleId` helper class from `wbn-sign`
63 | package. See
64 | [Scheme explainer](https://github.com/WICG/isolated-web-apps/blob/main/Scheme.md)
65 | for more details. Also note that providing `headerOverride` is optional.
66 |
67 | ```js
68 | /* rollup.config.mjs */
69 | import webbundle from 'rollup-plugin-webbundle';
70 | import * as wbnSign from 'wbn-sign';
71 | import dotenv from 'dotenv';
72 | dotenv.config({ path: './.env' });
73 |
74 | export default async () => {
75 | const key = wbnSign.parsePemKey(
76 | process.env.ENC_ED25519KEY,
77 | await wbnSign.readPassphrase()
78 | );
79 |
80 | return {
81 | input: 'src/index.js',
82 | output: { dir: 'dist', format: 'esm' },
83 | plugins: [
84 | webbundle({
85 | baseURL: new wbnSign.WebBundleId(
86 | key
87 | ).serializeWithIsolatedWebAppOrigin(),
88 | static: { dir: 'public' },
89 | output: 'signed.swbn',
90 | integrityBlockSign: {
91 | strategy: new wbnSign.NodeCryptoSigningStrategy(key),
92 | },
93 | headerOverride: {
94 | 'cross-origin-embedder-policy': 'require-corp',
95 | 'cross-origin-opener-policy': 'same-origin',
96 | 'cross-origin-resource-policy': 'same-origin',
97 | 'content-security-policy':
98 | "base-uri 'none'; default-src 'self'; object-src 'none'; frame-src 'self' https: blob: data:; connect-src 'self' https: wss:; script-src 'self' 'wasm-unsafe-eval'; img-src 'self' https: blob: data:; media-src 'self' https: blob: data:; font-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; require-trusted-types-for 'script';",
99 | },
100 | }),
101 | ],
102 | };
103 | };
104 | ```
105 |
106 | A signed web bundle (containing an
107 | [Integrity Block](https://github.com/WICG/webpackage/blob/main/explainers/integrity-signature.md))
108 | should be written to `dist/signed.swbn`.
109 |
110 | ## Options
111 |
112 | ### `baseURL`
113 |
114 | Type: `string`
115 |
116 | Default: `''`
117 |
118 | Specifies the URL prefix prepended to the file names in the bundle. Non-empty
119 | baseURL must end with `/`.
120 |
121 | ### `formatVersion`
122 |
123 | Type: `string`
124 |
125 | Default: `b2`
126 |
127 | Specifies WebBundle format version.
128 |
129 | ### `primaryURL`
130 |
131 | Type: `string`
132 |
133 | Default: baseURL
134 |
135 | Specifies the bundle's main resource URL. If omitted, the value of the `baseURL`
136 | option is used.
137 |
138 | ### `static`
139 |
140 | Type: `{ dir: String, baseURL?: string }`
141 |
142 | If specified, files and subdirectories under `dir` will be added to the bundle.
143 | `baseURL` can be omitted and defaults to `Options.baseURL`.
144 |
145 | ### `output`
146 |
147 | Type: `string`
148 |
149 | Default: `out.wbn`
150 |
151 | Specifies the file name of the Web Bundle to emit.
152 |
153 | ### `integrityBlockSign`
154 |
155 | Type:
156 |
157 | - `{ key: KeyObject, isIwa?: boolean }`
158 | - `{ strategy: ISigningStrategy, isIwa?: boolean }`
159 | - `{ strategies: Array, webBundleId: string, isIwa?: boolean }`
160 |
161 | Object specifying the signing options with
162 | [Integrity Block](https://github.com/WICG/webpackage/blob/main/explainers/integrity-signature.md).
163 |
164 | ### `integrityBlockSign.key`
165 |
166 | Type: `KeyObject`
167 |
168 | An unencrypted ed25519 private key can be generated with:
169 |
170 | ```bash
171 | openssl genpkey -algorithm Ed25519 -out ed25519key.pem
172 | ```
173 |
174 | For better security, one should prefer using passphrase-encrypted ed25519
175 | private keys. To encrypt an unencrypted private key, run:
176 |
177 | ```bash
178 | # encrypt the key (will ask for a passphrase, make sure to use a strong one)
179 | openssl pkcs8 -in ed25519key.pem -topk8 -out encrypted_ed25519key.pem
180 |
181 | # delete the unencrypted key
182 | rm ed25519key.pem
183 | ```
184 |
185 | It can be parsed with an imported helper function `parsePemKey(...)` from
186 | `wbn-sign` npm package. For an encrypted private key there's also an async
187 | helper function (`readPassphrase()`) to prompt the user for the passphrase the
188 | key was encrypted with.
189 |
190 | ```js
191 | // For an unencrypted ed25519 key.
192 | const key = wbnSign.parsePemKey(process.env.ED25519KEY);
193 |
194 | // For an encrypted ed25519 key.
195 | const key = wbnSign.parsePemKey(
196 | process.env.ENC_ED25519KEY,
197 | await wbnSign.readPassphrase()
198 | );
199 | ```
200 |
201 | Note that in order for the key to be parsed correctly, it must contain the
202 | `BEGIN` and `END` headers and line breaks (`\n`). Below an example `.env` file:
203 |
204 | ```bash
205 | ED25519KEY="-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIB8nP5PpWU7HiILHSfh5PYzb5GAcIfHZ+bw6tcd/LZXh\n-----END PRIVATE KEY-----"
206 | ```
207 |
208 | ### `integrityBlockSign.strategy`
209 |
210 | Type: `ISigningStrategy`
211 |
212 | Example web bundle plugin options using a signing strategy:
213 |
214 | ```js
215 | const pluginOptionsWithPredefinedSigningStrategy = {
216 | // ...other plugin options here...
217 | integrityBlockSign: {
218 | strategy: new wbnSign.NodeCryptoSigningStrategy(privateKey),
219 | },
220 | };
221 |
222 | const pluginOptionsWithCustomSigningStrategy = {
223 | // ...other plugin options here...
224 | integrityBlockSign: {
225 | strategy: new (class /* implements ISigningStrategy */ {
226 | async sign(data) {
227 | /** E.g. connect to one's external signing service that signs the
228 | * payload. */
229 | }
230 | async getPublicKey() {
231 | /** E.g. connect to one's external signing service that returns the
232 | * public key. */
233 | }
234 | })(),
235 | },
236 | };
237 | ```
238 |
239 | ### `integrityBlockSign.strategies`
240 |
241 | Type: `Array`
242 |
243 | Use this overload to sign a bundle with multiple keys. Note that `webBundleId`
244 | must always be specified when using `strategies`.
245 |
246 | ```
247 | const pluginOptionsWithMultipleStrategiesAndWebBundleId = {
248 | // ...other plugin options here...
249 | integrityBlockSign: {
250 | strategies: [
251 | new NodeCryptoSigningStrategy(privateKey1),
252 | new NodeCryptoSigningStrategy(privateKey2)
253 | ],
254 | webBundleId: "some-random-id"
255 | },
256 | };
257 | ```
258 |
259 | ### `integrityBlockSign.webBundleId`
260 |
261 | Type: `string`
262 |
263 | Allows specifying a custom id for this signed web bundle to decouple it from the
264 | signing keys. Must be used together with `strategies`.
265 |
266 | ### `integrityBlockSign.isIwa` (optional)
267 |
268 | Type: `boolean`
269 |
270 | Default: `true`
271 |
272 | If `undefined` or `true`, enforces certain
273 | [Isolated Web App](https://github.com/WICG/isolated-web-apps) -related checks
274 | for the headers. Also adds default IWA headers if completely missing. If set to
275 | `false`, skips validation checks and doesn't tamper with the headers.
276 |
277 | ### `headerOverride` (optional)
278 |
279 | Type: `{ [key: string]: string; }` |
280 | `(filepath: string) => { [key: string]: string; };`
281 |
282 | Object of strings specifying overridden headers or a function returning the same
283 | kind of object.
284 |
285 | ## Discuss & Help
286 |
287 | For discussions related to this repository's content, the Web Bundle plugins for
288 | webpack and rollup, please use
289 | [GitHub Issues](https://github.com/GoogleChromeLabs/webbundle-plugins/issues).
290 |
291 | If you'd like to discuss the Web Packaging proposal itself, consider opening an
292 | issue in its incubation repository at https://github.com/WICG/webpackage.
293 |
294 | For discussions related to Isolated Web Apps in general, or Chromium-specific
295 | implementation and development questions, please use the
296 | [iwa-dev@chromium.org](https://groups.google.com/a/chromium.org/g/iwa-dev)
297 | mailing list.
298 |
299 | If you'd like to discuss the Isolated Web Apps proposal, which builds on top of
300 | Web Bundles, consider opening an issue in the incubation repository at
301 | https://github.com/WICG/isolated-web-apps.
302 |
303 | ## Release Notes
304 |
305 | ### v0.2.0
306 |
307 | - Add support for the v2 integrity block format. Now web-bundle-id is no longer
308 | presumed to be a derivative of the first public key in the stack, but rather
309 | acts as a separate entry in the integrity block attributes, and multiple
310 | independent signatures are allowed to facilitate key rotation.
311 |
312 | ### v0.1.4
313 |
314 | - Add support for ECDSA P-256 SHA-256 signing algorithm
315 | - Bumping underlying wbn-sign version to v0.1.3.
316 |
317 | ### v0.1.3
318 |
319 | - Updates to style-src and wss CSP values.
320 | - Bumping underlying wbn-sign version to v0.1.1.
321 |
322 | ### v0.1.2
323 |
324 | - Add support for `integrityBlockSign.strategy` plugin option which can be used
325 | to pass one of the predefined strategies or one's own implementation class for
326 | ISigningStrategy. One can also use the old `integrityBlockSign.key` option,
327 | which defaults to the predefined `NodeCryptoSigningStrategy` strategy.
328 | - Refactor plugin to be in TypeScript.
329 | - Combine the Webpack and Rollup web bundle plugins to live in the same
330 | repository and share some duplicated code. Taking advantage of
331 | [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces).
332 |
333 | ### v0.1.1
334 |
335 | - Add support for overriding headers with an optional `headerOverride` plugin
336 | option.
337 |
338 | ### v0.1.0
339 |
340 | - BREAKING CHANGE: Change type of integrityBlockSign.key to be KeyObject instead
341 | of string.
342 | - Upgrade to support Rollup 3.
343 |
344 | ### v0.0.4
345 |
346 | - Support for signing web bundles with
347 | [integrity block](https://github.com/WICG/webpackage/blob/main/explainers/integrity-signature.md)
348 | added.
349 |
350 | ## License
351 |
352 | Licensed under the Apache-2.0 license.
353 |
354 | ## Contributing
355 |
356 | See [CONTRIBUTING.md](../../CONTRIBUTING.md) file.
357 |
358 | ## Disclaimer
359 |
360 | This is not an officially supported Google product.
361 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-webbundle/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rollup-plugin-webbundle",
3 | "version": "0.2.0",
4 | "description": "Rollup plugin to generate WebBundle output.",
5 | "keywords": [
6 | "rollup-plugin",
7 | "web-bundle",
8 | "isolated-web-app"
9 | ],
10 | "main": "lib/index.js",
11 | "scripts": {
12 | "prepack": "npm run build && cp ../../LICENSE ./LICENSE",
13 | "postpack": "rm -f ./LICENSE",
14 | "build": "rm -rf lib && esbuild --bundle --packages=external --format=esm --outfile=lib/index.js src/index.ts --platform=node --legal-comments=inline --sourcemap --keep-names"
15 | },
16 | "type": "module",
17 | "author": "Kunihiko Sakamoto ",
18 | "contributors": [
19 | "Sonja Laurila (https://github.com/sonkkeli)",
20 | "Christian Flach (https://github.com/cmfcmf)",
21 | "Andrew Rayskiy (https://github.com/GreenGrape)"
22 | ],
23 | "license": "Apache-2.0",
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/GoogleChromeLabs/webbundle-plugins.git",
27 | "directory": "packages/rollup-plugin-webbundle"
28 | },
29 | "peerDependencies": {
30 | "rollup": ">=1.21.0 <4.0.0"
31 | },
32 | "dependencies": {
33 | "mime": "^2.4.4",
34 | "wbn": "0.0.9",
35 | "wbn-sign": "0.2.0",
36 | "zod": "^3.21.4"
37 | },
38 | "devDependencies": {
39 | "rollup": "^3.29.5"
40 | },
41 | "engines": {
42 | "node": ">= 16.0.0"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-webbundle/src/index.ts:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { BundleBuilder } from 'wbn';
18 | import { Plugin, OutputOptions } from 'rollup';
19 | import {
20 | addAsset,
21 | addFilesRecursively,
22 | getSignedWebBundle,
23 | } from '../../shared/utils';
24 | import {
25 | getValidatedOptionsWithDefaults,
26 | PluginOptions,
27 | } from '../../shared/types';
28 |
29 | const consoleLogColor = { green: '\x1b[32m', reset: '\x1b[0m' };
30 | function infoLogger(text: string): void {
31 | console.log(`${consoleLogColor.green}${text}${consoleLogColor.reset}\n`);
32 | }
33 |
34 | // TODO(sonkkeli): Probably this depends on the Rollup version. Figure out how
35 | // this should be refactored.
36 | // https://rollupjs.org/plugin-development/#build-hooks
37 | type EnforcedPlugin = Plugin & { enforce: 'post' | 'pre' | null };
38 |
39 | export default function wbnOutputPlugin(
40 | rawOpts: PluginOptions
41 | ): EnforcedPlugin {
42 | return {
43 | name: 'wbn-output-plugin',
44 | enforce: 'post',
45 |
46 | async generateBundle(_: OutputOptions, bundle): Promise {
47 | const opts = await getValidatedOptionsWithDefaults(rawOpts);
48 |
49 | const builder = new BundleBuilder(opts.formatVersion);
50 | if ('primaryURL' in opts && opts.primaryURL) {
51 | builder.setPrimaryURL(opts.primaryURL);
52 | }
53 |
54 | if (opts.static) {
55 | addFilesRecursively(
56 | builder,
57 | opts.static.baseURL ?? opts.baseURL,
58 | opts.static.dir,
59 | opts
60 | );
61 | }
62 |
63 | for (const name of Object.keys(bundle)) {
64 | const asset = bundle[name];
65 | const content = asset.type === 'asset' ? asset.source : asset.code;
66 | addAsset(
67 | builder,
68 | opts.baseURL,
69 | asset.fileName, // This contains the relative path to the base dir already.
70 | content,
71 | opts
72 | );
73 | delete bundle[name];
74 | }
75 |
76 | let webBundle = builder.createBundle();
77 | if ('integrityBlockSign' in opts) {
78 | webBundle = await getSignedWebBundle(webBundle, opts, infoLogger);
79 | }
80 |
81 | this.emitFile({
82 | fileName: opts.output,
83 | type: 'asset',
84 | source: Buffer.from(
85 | webBundle,
86 | webBundle.byteOffset,
87 | webBundle.byteLength
88 | ),
89 | });
90 | },
91 | };
92 | }
93 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-webbundle/test/fixtures/index.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | export default 42;
18 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-webbundle/test/fixtures/static/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-webbundle/test/snapshots/test.js.md:
--------------------------------------------------------------------------------
1 | # Snapshot report for `packages/rollup-plugin-webbundle/test/test.js`
2 |
3 | The actual snapshot is saved in `test.js.snap`.
4 |
5 | Generated by [AVA](https://avajs.dev).
6 |
7 | ## simple
8 |
9 | > Snapshot 1
10 |
11 | {
12 | exchanges: {
13 | 'https://wbn.example.com/index.js': {
14 | body: `/*!␊
15 | * Copyright 2020 Google LLC␊
16 | *␊
17 | * Licensed under the Apache License, Version 2.0 (the "License");␊
18 | * you may not use this file except in compliance with the License.␊
19 | * You may obtain a copy of the License at␊
20 | *␊
21 | * http://www.apache.org/licenses/LICENSE-2.0␊
22 | *␊
23 | * Unless required by applicable law or agreed to in writing, software␊
24 | * distributed under the License is distributed on an "AS IS" BASIS,␊
25 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.␊
26 | * See the License for the specific language governing permissions and␊
27 | * limitations under the License.␊
28 | */␊
29 | ␊
30 | var index = 42;␊
31 | ␊
32 | export { index as default };␊
33 | `,
34 | headers: {
35 | 'content-type': 'application/javascript',
36 | },
37 | status: 200,
38 | },
39 | },
40 | primaryURL: 'https://wbn.example.com/index.js',
41 | version: 'b2',
42 | }
43 |
44 | ## asset
45 |
46 | > Snapshot 1
47 |
48 | {
49 | exchanges: {
50 | 'https://wbn.example.com/assets/hello.txt': {
51 | body: 'Hello',
52 | headers: {
53 | 'content-type': 'text/plain',
54 | },
55 | status: 200,
56 | },
57 | 'https://wbn.example.com/index.js': {
58 | body: `/*!␊
59 | * Copyright 2020 Google LLC␊
60 | *␊
61 | * Licensed under the Apache License, Version 2.0 (the "License");␊
62 | * you may not use this file except in compliance with the License.␊
63 | * You may obtain a copy of the License at␊
64 | *␊
65 | * http://www.apache.org/licenses/LICENSE-2.0␊
66 | *␊
67 | * Unless required by applicable law or agreed to in writing, software␊
68 | * distributed under the License is distributed on an "AS IS" BASIS,␊
69 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.␊
70 | * See the License for the specific language governing permissions and␊
71 | * limitations under the License.␊
72 | */␊
73 | ␊
74 | var index = 42;␊
75 | ␊
76 | export { index as default };␊
77 | `,
78 | headers: {
79 | 'content-type': 'application/javascript',
80 | },
81 | status: 200,
82 | },
83 | },
84 | primaryURL: 'https://wbn.example.com/assets/hello.txt',
85 | version: 'b1',
86 | }
87 |
88 | ## static
89 |
90 | > Snapshot 1
91 |
92 | {
93 | exchanges: {
94 | 'https://wbn.example.com/': {
95 | body: '',
96 | headers: {
97 | 'content-type': 'text/html',
98 | },
99 | status: 200,
100 | },
101 | 'https://wbn.example.com/index.html': {
102 | body: '',
103 | headers: {
104 | location: './',
105 | },
106 | status: 301,
107 | },
108 | 'https://wbn.example.com/index.js': {
109 | body: `/*!␊
110 | * Copyright 2020 Google LLC␊
111 | *␊
112 | * Licensed under the Apache License, Version 2.0 (the "License");␊
113 | * you may not use this file except in compliance with the License.␊
114 | * You may obtain a copy of the License at␊
115 | *␊
116 | * http://www.apache.org/licenses/LICENSE-2.0␊
117 | *␊
118 | * Unless required by applicable law or agreed to in writing, software␊
119 | * distributed under the License is distributed on an "AS IS" BASIS,␊
120 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.␊
121 | * See the License for the specific language governing permissions and␊
122 | * limitations under the License.␊
123 | */␊
124 | ␊
125 | var index = 42;␊
126 | ␊
127 | export { index as default };␊
128 | `,
129 | headers: {
130 | 'content-type': 'application/javascript',
131 | },
132 | status: 200,
133 | },
134 | },
135 | primaryURL: 'https://wbn.example.com/',
136 | version: 'b2',
137 | }
138 |
139 | ## relative
140 |
141 | > Snapshot 1
142 |
143 | {
144 | exchanges: {
145 | '/': {
146 | body: '',
147 | headers: {
148 | 'content-type': 'text/html',
149 | },
150 | status: 200,
151 | },
152 | '/index.html': {
153 | body: '',
154 | headers: {
155 | location: './',
156 | },
157 | status: 301,
158 | },
159 | '/index.js': {
160 | body: `/*!␊
161 | * Copyright 2020 Google LLC␊
162 | *␊
163 | * Licensed under the Apache License, Version 2.0 (the "License");␊
164 | * you may not use this file except in compliance with the License.␊
165 | * You may obtain a copy of the License at␊
166 | *␊
167 | * http://www.apache.org/licenses/LICENSE-2.0␊
168 | *␊
169 | * Unless required by applicable law or agreed to in writing, software␊
170 | * distributed under the License is distributed on an "AS IS" BASIS,␊
171 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.␊
172 | * See the License for the specific language governing permissions and␊
173 | * limitations under the License.␊
174 | */␊
175 | ␊
176 | var index = 42;␊
177 | ␊
178 | export { index as default };␊
179 | `,
180 | headers: {
181 | 'content-type': 'application/javascript',
182 | },
183 | status: 200,
184 | },
185 | },
186 | primaryURL: null,
187 | version: 'b2',
188 | }
189 |
--------------------------------------------------------------------------------
/packages/rollup-plugin-webbundle/test/snapshots/test.js.snap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/webbundle-plugins/fa2b3a0bda6a6e3db7570fb45dd8a9a2f303267d/packages/rollup-plugin-webbundle/test/snapshots/test.js.snap
--------------------------------------------------------------------------------
/packages/rollup-plugin-webbundle/test/test.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import test from 'ava';
18 | import * as path from 'path';
19 | import * as rollup from 'rollup';
20 | import crypto from 'crypto';
21 | import url from 'url';
22 | import * as wbn from 'wbn';
23 | import * as wbnSign from 'wbn-sign';
24 | import webbundle from '../lib/index.js';
25 | import {
26 | coep,
27 | coop,
28 | corp,
29 | csp,
30 | iwaHeaderDefaults,
31 | } from '../../shared/lib/iwa-headers.js';
32 |
33 | const TEST_ED25519_PRIVATE_KEY = wbnSign.parsePemKey(
34 | '-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIB8nP5PpWU7HiILHSfh5PYzb5GAcIfHZ+bw6tcd/LZXh\n-----END PRIVATE KEY-----'
35 | );
36 | const TEST_IWA_BASE_URL =
37 | 'isolated-app://4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/';
38 |
39 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
40 | process.chdir(__dirname);
41 |
42 | function parseWebBundle(buf) {
43 | const bundle = new wbn.Bundle(buf);
44 | const exchanges = {};
45 | for (const url of bundle.urls) {
46 | const resp = bundle.getResponse(url);
47 | let body = new TextDecoder('utf-8').decode(resp.body);
48 |
49 | // Our test snapshots are generated with Rollup 2, but Rollup 1 uses
50 | // different syntax for default export.
51 | if (rollup.VERSION.startsWith('1.')) {
52 | body = body.replace(
53 | 'export default index',
54 | 'export { index as default }'
55 | );
56 | }
57 |
58 | exchanges[url] = {
59 | status: resp.status,
60 | headers: resp.headers,
61 | body: body,
62 | };
63 | }
64 | return {
65 | version: bundle.version,
66 | primaryURL: bundle.primaryURL,
67 | exchanges,
68 | };
69 | }
70 |
71 | test('simple', async (t) => {
72 | const bundle = await rollup.rollup({
73 | input: 'fixtures/index.js',
74 | plugins: [
75 | webbundle({
76 | baseURL: 'https://wbn.example.com/',
77 | primaryURL: 'https://wbn.example.com/index.js',
78 | output: 'out.wbn',
79 | }),
80 | ],
81 | });
82 | const { output } = await bundle.generate({ format: 'esm' });
83 | const keys = Object.keys(output);
84 | t.is(keys.length, 1);
85 | if (output[keys[0]].type) t.is(output[keys[0]].type, 'asset');
86 | else t.true(output[keys[0]].isAsset);
87 | t.is(output[keys[0]].fileName, 'out.wbn');
88 |
89 | t.snapshot(parseWebBundle(output[keys[0]].source));
90 | });
91 |
92 | test('asset', async (t) => {
93 | const bundle = await rollup.rollup({
94 | input: 'fixtures/index.js',
95 | plugins: [
96 | {
97 | name: 'add-asset',
98 | generateBundle() {
99 | this.emitFile({
100 | type: 'asset',
101 | name: 'hello.txt',
102 | source: 'Hello',
103 | });
104 | },
105 | },
106 | webbundle({
107 | formatVersion: 'b1',
108 | baseURL: 'https://wbn.example.com/',
109 | primaryURL: 'https://wbn.example.com/assets/hello.txt',
110 | output: 'out.wbn',
111 | }),
112 | ],
113 | });
114 | const { output } = await bundle.generate({
115 | format: 'esm',
116 | assetFileNames: 'assets/[name][extname]',
117 | });
118 | const keys = Object.keys(output);
119 | t.is(keys.length, 1);
120 | if (output[keys[0]].type) t.is(output[keys[0]].type, 'asset');
121 | else t.true(output[keys[0]].isAsset);
122 | t.is(output[keys[0]].fileName, 'out.wbn');
123 |
124 | t.snapshot(parseWebBundle(output[keys[0]].source));
125 | });
126 |
127 | test('static', async (t) => {
128 | const bundle = await rollup.rollup({
129 | input: 'fixtures/index.js',
130 | plugins: [
131 | webbundle({
132 | baseURL: 'https://wbn.example.com/',
133 | primaryURL: 'https://wbn.example.com/',
134 | output: 'out.wbn',
135 | static: { dir: 'fixtures/static' },
136 | }),
137 | ],
138 | });
139 | const { output } = await bundle.generate({ format: 'esm' });
140 | const keys = Object.keys(output);
141 | t.is(keys.length, 1);
142 | if (output[keys[0]].type) t.is(output[keys[0]].type, 'asset');
143 | else t.true(output[keys[0]].isAsset);
144 | t.is(output[keys[0]].fileName, 'out.wbn');
145 |
146 | t.snapshot(parseWebBundle(output[keys[0]].source));
147 | });
148 |
149 | test('relative', async (t) => {
150 | const bundle = await rollup.rollup({
151 | input: 'fixtures/index.js',
152 | plugins: [
153 | webbundle({
154 | baseURL: '/',
155 | output: 'out.wbn',
156 | static: { dir: 'fixtures/static' },
157 | }),
158 | ],
159 | });
160 | const { output } = await bundle.generate({ format: 'esm' });
161 | const keys = Object.keys(output);
162 | t.is(keys.length, 1);
163 | if (output[keys[0]].type) t.is(output[keys[0]].type, 'asset');
164 | else t.true(output[keys[0]].isAsset);
165 | t.is(output[keys[0]].fileName, 'out.wbn');
166 |
167 | t.snapshot(parseWebBundle(output[keys[0]].source));
168 | });
169 |
170 | test('integrityBlockSign', async (t) => {
171 | const outputFileName = 'out.swbn';
172 |
173 | const testCases = [
174 | // With default signer.
175 | {
176 | key: TEST_ED25519_PRIVATE_KEY,
177 | },
178 | // With signer option specified.
179 | {
180 | strategy: new wbnSign.NodeCryptoSigningStrategy(TEST_ED25519_PRIVATE_KEY),
181 | },
182 | ];
183 |
184 | for (const testCase of testCases) {
185 | const bundle = await rollup.rollup({
186 | input: 'fixtures/index.js',
187 | plugins: [
188 | webbundle({
189 | baseURL: TEST_IWA_BASE_URL,
190 | output: outputFileName,
191 | integrityBlockSign: testCase,
192 | }),
193 | ],
194 | });
195 | const { output } = await bundle.generate({ format: 'esm' });
196 | const keys = Object.keys(output);
197 | t.is(keys.length, 1);
198 | t.is(output[keys[0]].fileName, outputFileName);
199 |
200 | const swbnFile = output[keys[0]].source;
201 | const wbnLength = Number(Buffer.from(swbnFile.slice(-8)).readBigUint64BE());
202 | t.truthy(wbnLength < swbnFile.length);
203 |
204 | const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(
205 | /*is_v2=*/ true,
206 | swbnFile.slice(-wbnLength),
207 | new wbnSign.WebBundleId(TEST_ED25519_PRIVATE_KEY).serialize(),
208 | [new wbnSign.NodeCryptoSigningStrategy(TEST_ED25519_PRIVATE_KEY)]
209 | ).sign();
210 |
211 | t.deepEqual(swbnFile, Buffer.from(signedWebBundle));
212 | }
213 | });
214 |
215 | test('headerOverride - IWA with good headers', async (t) => {
216 | const headersTestCases = [
217 | // These are added manually as they expect more than just `iwaHeaderDefaults`.
218 | {
219 | headerOverride: {
220 | ...iwaHeaderDefaults,
221 | 'X-Csrf-Token': 'hello-world',
222 | },
223 | expectedHeaders: {
224 | ...iwaHeaderDefaults,
225 | 'x-csrf-token': 'hello-world',
226 | },
227 | },
228 | {
229 | headerOverride: () => {
230 | return {
231 | ...iwaHeaderDefaults,
232 | 'X-Csrf-Token': 'hello-world',
233 | };
234 | },
235 | expectedHeaders: {
236 | ...iwaHeaderDefaults,
237 | 'x-csrf-token': 'hello-world',
238 | },
239 | },
240 | ];
241 |
242 | const headersThatDefaultToIWADefaults = [
243 | { ...coop, ...corp, ...csp },
244 | { ...coep, ...corp, ...csp },
245 | { ...coep, ...coop, ...csp },
246 | { ...coep, ...coop, ...corp },
247 | iwaHeaderDefaults,
248 | {},
249 | undefined,
250 | {
251 | ...iwaHeaderDefaults,
252 | 'Cross-Origin-Embedder-Policy': 'require-corp',
253 | },
254 | ];
255 |
256 | for (const headers of headersThatDefaultToIWADefaults) {
257 | // Both functions and objects are ok so let's test with both.
258 | headersTestCases.push({
259 | headerOverride: headers,
260 | expectedHeaders: iwaHeaderDefaults,
261 | });
262 |
263 | // Not supported as typeof function because that's forced to return `Headers` map.
264 | if (headers === undefined) continue;
265 | headersTestCases.push({
266 | headerOverride: () => headers,
267 | expectedHeaders: iwaHeaderDefaults,
268 | });
269 | }
270 |
271 | const outputFileName = 'out.swbn';
272 | for (const headersTestCase of headersTestCases) {
273 | for (const isIwaTestCase of [undefined, true]) {
274 | const bundle = await rollup.rollup({
275 | input: 'fixtures/index.js',
276 | plugins: [
277 | webbundle({
278 | baseURL: TEST_IWA_BASE_URL,
279 | output: outputFileName,
280 | integrityBlockSign: {
281 | key: TEST_ED25519_PRIVATE_KEY,
282 | isIwa: isIwaTestCase,
283 | },
284 | headerOverride: headersTestCase.headerOverride,
285 | }),
286 | ],
287 | });
288 | const { output } = await bundle.generate({ format: 'esm' });
289 | const keys = Object.keys(output);
290 | t.is(keys.length, 1);
291 | t.is(output[keys[0]].fileName, outputFileName);
292 |
293 | const swbnFile = output[keys[0]].source;
294 | const wbnLength = Number(
295 | Buffer.from(swbnFile.slice(-8)).readBigUint64BE()
296 | );
297 | t.truthy(wbnLength < swbnFile.length);
298 |
299 | const usignedBundle = new wbn.Bundle(swbnFile.slice(-wbnLength));
300 | for (const url of usignedBundle.urls) {
301 | for (const [headerName, headerValue] of Object.entries(
302 | iwaHeaderDefaults
303 | )) {
304 | t.is(usignedBundle.getResponse(url).headers[headerName], headerValue);
305 | }
306 | }
307 | }
308 | }
309 | });
310 |
311 | test("headerOverride - non-IWA doesn't enforce IWA headers", async (t) => {
312 | // Irrelevant what this would contain.
313 | const randomNonIwaHeaders = { 'x-csrf-token': 'hello-world' };
314 |
315 | const headersTestCases = [
316 | {
317 | // Type `object` is ok.
318 | headerOverride: randomNonIwaHeaders,
319 | expectedHeaders: randomNonIwaHeaders,
320 | },
321 | {
322 | // Same but camel case, which gets lower-cased.
323 | headerOverride: { 'X-Csrf-Token': 'hello-world' },
324 | expectedHeaders: randomNonIwaHeaders,
325 | },
326 | {
327 | // Type `function` is ok.
328 | headerOverride: () => randomNonIwaHeaders,
329 | expectedHeaders: randomNonIwaHeaders,
330 | },
331 | {
332 | // When `integrityBlockSign.isIwa` is false and `headerOverride` is
333 | // `undefined`, nothing unusual gets added.
334 | headerOverride: undefined,
335 | expectedHeaders: {},
336 | },
337 | ];
338 |
339 | const outputFileName = 'out.swbn';
340 | for (const headersTestCase of headersTestCases) {
341 | const bundle = await rollup.rollup({
342 | input: 'fixtures/index.js',
343 | plugins: [
344 | webbundle({
345 | baseURL: TEST_IWA_BASE_URL,
346 | output: outputFileName,
347 | integrityBlockSign: {
348 | key: TEST_ED25519_PRIVATE_KEY,
349 | isIwa: false,
350 | },
351 | headerOverride: headersTestCase.headerOverride,
352 | }),
353 | ],
354 | });
355 |
356 | const { output } = await bundle.generate({ format: 'esm' });
357 | const keys = Object.keys(output);
358 | t.is(keys.length, 1);
359 | t.is(output[keys[0]].fileName, outputFileName);
360 | const swbnFile = output[keys[0]].source;
361 |
362 | const wbnLength = Number(Buffer.from(swbnFile.slice(-8)).readBigUint64BE());
363 | t.truthy(wbnLength < swbnFile.length);
364 |
365 | const usignedBundle = new wbn.Bundle(swbnFile.slice(-wbnLength));
366 | for (const url of usignedBundle.urls) {
367 | // Added the expected headers.
368 | for (const [headerName, headerValue] of Object.entries(
369 | headersTestCase.expectedHeaders
370 | )) {
371 | t.is(usignedBundle.getResponse(url).headers[headerName], headerValue);
372 | }
373 | // Did not add any IWA headers automatically.
374 | for (const headerName of Object.keys(iwaHeaderDefaults)) {
375 | t.is(usignedBundle.getResponse(url).headers[headerName], undefined);
376 | }
377 | }
378 | }
379 | });
380 |
381 | test("integrityBlockSign with undefined baseURL doesn't fail", async (t) => {
382 | const outputFileName = 'out.swbn';
383 |
384 | const bundle = await rollup.rollup({
385 | input: 'fixtures/index.js',
386 | plugins: [
387 | webbundle({
388 | output: outputFileName,
389 | integrityBlockSign: {
390 | strategy: new wbnSign.NodeCryptoSigningStrategy(
391 | TEST_ED25519_PRIVATE_KEY
392 | ),
393 | },
394 | }),
395 | ],
396 | });
397 | const { output } = await bundle.generate({ format: 'esm' });
398 | const keys = Object.keys(output);
399 | t.is(keys.length, 1);
400 | t.is(output[keys[0]].fileName, outputFileName);
401 | });
402 |
403 | // To make sure actually async things work.
404 | test('integrityBlockSign with sleeping CustomSigningStrategy', async (t) => {
405 | function sleep(ms) {
406 | return new Promise((resolve) => {
407 | setTimeout(resolve, ms);
408 | });
409 | }
410 |
411 | class CustomSigningStrategy {
412 | async sign(data) {
413 | await sleep(500);
414 | return crypto.sign(
415 | /*algorithm=*/ undefined,
416 | data,
417 | TEST_ED25519_PRIVATE_KEY
418 | );
419 | }
420 |
421 | async getPublicKey() {
422 | await sleep(500);
423 | return crypto.createPublicKey(TEST_ED25519_PRIVATE_KEY);
424 | }
425 | }
426 |
427 | const outputFileName = 'out.swbn';
428 |
429 | const bundle = await rollup.rollup({
430 | input: 'fixtures/index.js',
431 | plugins: [
432 | webbundle({
433 | output: outputFileName,
434 | integrityBlockSign: {
435 | strategy: new CustomSigningStrategy(),
436 | },
437 | }),
438 | ],
439 | });
440 | const { output } = await bundle.generate({ format: 'esm' });
441 | const keys = Object.keys(output);
442 | t.is(keys.length, 1);
443 | t.is(output[keys[0]].fileName, outputFileName);
444 | });
445 |
446 | test('integrityBlockSign with multiple strategies', async (t) => {
447 | // ECDSA P-256 SHA-256 signatures are not deterministic, so this test uses
448 | // only Ed25519 ones for generating bundles side-by-side.
449 | const keyPairs = [
450 | crypto.generateKeyPairSync('ed25519'),
451 | crypto.generateKeyPairSync('ed25519'),
452 | crypto.generateKeyPairSync('ed25519'),
453 | ];
454 |
455 | const webBundleId = new wbnSign.WebBundleId(keyPairs[0].privateKey);
456 | const strategies = keyPairs.map(
457 | (keyPair) => new wbnSign.NodeCryptoSigningStrategy(keyPair.privateKey)
458 | );
459 |
460 | const outputFileName = 'out.swbn';
461 | const bundle = await rollup.rollup({
462 | input: 'fixtures/index.js',
463 | plugins: [
464 | webbundle({
465 | baseURL: webBundleId.serializeWithIsolatedWebAppOrigin(),
466 | output: outputFileName,
467 | integrityBlockSign: {
468 | webBundleId: webBundleId.serialize(),
469 | strategies,
470 | },
471 | }),
472 | ],
473 | });
474 | const { output } = await bundle.generate({ format: 'esm' });
475 | const keys = Object.keys(output);
476 | t.is(keys.length, 1);
477 | t.is(output[keys[0]].fileName, outputFileName);
478 |
479 | const swbnFile = output[keys[0]].source;
480 | const wbnLength = Number(Buffer.from(swbnFile.slice(-8)).readBigUint64BE());
481 | t.truthy(wbnLength < swbnFile.length);
482 |
483 | const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(
484 | /*is_v2=*/ true,
485 | swbnFile.slice(-wbnLength),
486 | webBundleId.serialize(),
487 | strategies
488 | ).sign();
489 |
490 | t.deepEqual(swbnFile, Buffer.from(signedWebBundle));
491 | });
492 |
--------------------------------------------------------------------------------
/packages/shared/iwa-headers.ts:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { Headers } from './types';
18 |
19 | export const coep: Headers = Object.freeze({
20 | 'cross-origin-embedder-policy': 'require-corp',
21 | });
22 | export const coop: Headers = Object.freeze({
23 | 'cross-origin-opener-policy': 'same-origin',
24 | });
25 | export const corp: Headers = Object.freeze({
26 | 'cross-origin-resource-policy': 'same-origin',
27 | });
28 |
29 | export const CSP_HEADER_NAME = 'content-security-policy';
30 | export const csp: Headers = Object.freeze({
31 | [CSP_HEADER_NAME]:
32 | "base-uri 'none'; default-src 'self'; object-src 'none'; frame-src 'self' https: blob: data:; connect-src 'self' https: wss:; script-src 'self' 'wasm-unsafe-eval'; img-src 'self' https: blob: data:; media-src 'self' https: blob: data:; font-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; require-trusted-types-for 'script'; frame-ancestors 'self';",
33 | });
34 |
35 | // These headers must have these exact values for Isolated Web Apps, whereas the
36 | // CSP header can also be more strict.
37 | const invariableIwaHeaders: Headers = Object.freeze({
38 | ...coep,
39 | ...coop,
40 | ...corp,
41 | });
42 |
43 | export const iwaHeaderDefaults: Headers = Object.freeze({
44 | ...csp,
45 | ...invariableIwaHeaders,
46 | });
47 |
48 | function headerNamesToLowerCase(headers: Headers): Headers {
49 | const lowerCaseHeaders: Headers = {};
50 | for (const [headerName, headerValue] of Object.entries(headers)) {
51 | lowerCaseHeaders[headerName.toLowerCase()] = headerValue;
52 | }
53 | return lowerCaseHeaders;
54 | }
55 |
56 | const ifNotIwaMsg =
57 | "If you are bundling a non-IWA, set `integrityBlockSign: { isIwa: false }` in the plugin's configuration.";
58 |
59 | // Checks if the IWA headers are strict enough or adds in case missing.
60 | export function checkAndAddIwaHeaders(headers: Headers) {
61 | const lowerCaseHeaders = headerNamesToLowerCase(headers);
62 |
63 | // Add missing IWA headers.
64 | for (const [iwaHeaderName, iwaHeaderValue] of Object.entries(
65 | iwaHeaderDefaults
66 | )) {
67 | if (!lowerCaseHeaders[iwaHeaderName]) {
68 | console.log(
69 | `For Isolated Web Apps, ${iwaHeaderName} header was automatically set to ${iwaHeaderValue}. ${ifNotIwaMsg}`
70 | );
71 | headers[iwaHeaderName] = iwaHeaderValue;
72 | }
73 | }
74 |
75 | // Check strictness of IWA headers (apart from special case `Content-Security-Policy`).
76 | for (const [iwaHeaderName, iwaHeaderValue] of Object.entries(
77 | invariableIwaHeaders
78 | )) {
79 | if (
80 | lowerCaseHeaders[iwaHeaderName] &&
81 | lowerCaseHeaders[iwaHeaderName].toLowerCase() !== iwaHeaderValue
82 | ) {
83 | throw new Error(
84 | `For Isolated Web Apps ${iwaHeaderName} should be ${iwaHeaderValue}. Now it is ${headers[iwaHeaderName]}. ${ifNotIwaMsg}`
85 | );
86 | }
87 | }
88 |
89 | // TODO: Parse and check strictness of `Content-Security-Policy`.
90 | }
91 |
--------------------------------------------------------------------------------
/packages/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shared",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "build": "rm -rf lib && tsc"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/packages/shared/test/test.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import test from 'ava';
18 | import { getValidatedOptionsWithDefaults } from '../lib/types.js';
19 | import * as wbnSign from 'wbn-sign';
20 |
21 | const TEST_ED25519_PRIVATE_KEY = wbnSign.parsePemKey(
22 | '-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIB8nP5PpWU7HiILHSfh5PYzb5GAcIfHZ+bw6tcd/LZXh\n-----END PRIVATE KEY-----'
23 | );
24 | const TEST_IWA_BASE_URL =
25 | 'isolated-app://4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/';
26 |
27 | test('headerOverride - IWA with bad headers', async (t) => {
28 | const badHeadersTestCase = [
29 | { 'cross-origin-embedder-policy': 'unsafe-none' },
30 | { 'cross-origin-opener-policy': 'unsafe-none' },
31 | { 'cross-origin-resource-policy': 'cross-origin' },
32 | ];
33 |
34 | for (const badHeaders of badHeadersTestCase) {
35 | for (const isIwaTestCase of [undefined, true]) {
36 | await t.throwsAsync(
37 | async () => {
38 | await getValidatedOptionsWithDefaults({
39 | baseURL: TEST_IWA_BASE_URL,
40 | output: 'example.swbn',
41 | integrityBlockSign: {
42 | key: TEST_ED25519_PRIVATE_KEY,
43 | isIwa: isIwaTestCase,
44 | },
45 | headerOverride: badHeaders,
46 | });
47 | },
48 | { instanceOf: Error }
49 | );
50 | }
51 | }
52 | });
53 |
--------------------------------------------------------------------------------
/packages/shared/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "lib",
5 | "noEmit": false
6 | },
7 | "include": ["**/*"],
8 | "exclude": ["test/**"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/shared/types.ts:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import { KeyObject } from 'crypto';
18 | import { URL } from 'url';
19 | import * as z from 'zod';
20 | // TODO(sonkkeli: b/282899095): This should get fixed whenever we use a more
21 | // modern test framework like Jest.
22 | import { checkAndAddIwaHeaders, iwaHeaderDefaults } from './iwa-headers.js';
23 | import {
24 | NodeCryptoSigningStrategy,
25 | ISigningStrategy,
26 | WebBundleId,
27 | } from 'wbn-sign';
28 |
29 | const headersSchema = z.record(z.string());
30 |
31 | export type Headers = z.infer;
32 |
33 | const baseOptionsSchema = z.strictObject({
34 | static: z
35 | .strictObject({
36 | dir: z.string(),
37 | baseURL: z.string().optional(),
38 | })
39 | .optional(),
40 | baseURL: z.string().default(''),
41 | output: z.string().default('out.wbn'),
42 | formatVersion: z.enum(['b1', 'b2']).default('b2'),
43 | headerOverride: z
44 | .union([z.function().returns(headersSchema), headersSchema])
45 | .optional(),
46 | });
47 |
48 | const nonSigningSchema = baseOptionsSchema.extend({
49 | primaryURL: z.string().optional(),
50 | });
51 |
52 | const baseIntegrityBlockSignSchema = z.strictObject({
53 | isIwa: z.boolean().default(true),
54 | });
55 |
56 | const keyBasedIntegrityBlockSignSchema = baseIntegrityBlockSignSchema
57 | .extend({
58 | // Unfortunately we cannot use `KeyObject` directly within `instanceof()`,
59 | // because its constructor is private.
60 | key: z
61 | .instanceof(Object)
62 | .refine((key): key is KeyObject => key instanceof KeyObject, {
63 | message: `Key must be an instance of "KeyObject"`,
64 | }),
65 | })
66 |
67 | // Use the default NodeCryptoSigningStrategy strategy instead of key.
68 | .transform((ibSignOpts) => {
69 | return {
70 | isIwa: ibSignOpts.isIwa,
71 | webBundleId: new WebBundleId(ibSignOpts.key).serialize(),
72 | strategies: [new NodeCryptoSigningStrategy(ibSignOpts.key)],
73 | };
74 | });
75 |
76 | const strategyBasedIntegrityBlockSignSchema = baseIntegrityBlockSignSchema
77 | .extend({
78 | strategy: z.instanceof(Object).refine(
79 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
80 | (strategy: Record): strategy is ISigningStrategy => {
81 | return ['getPublicKey', 'sign'].every(
82 | (func) => func in strategy && typeof strategy[func] === 'function'
83 | );
84 | },
85 | { message: `Strategy must implement "ISigningStrategy"` }
86 | ),
87 | })
88 | .transform(async (ibSignOpts) => {
89 | return {
90 | isIwa: ibSignOpts.isIwa,
91 | webBundleId: new WebBundleId(
92 | await ibSignOpts.strategy.getPublicKey()
93 | ).serialize(),
94 | strategies: [ibSignOpts.strategy],
95 | };
96 | });
97 |
98 | const strategiesBasedIntegrityBlockSignSchemaWithWebBundleId =
99 | baseIntegrityBlockSignSchema.extend({
100 | strategies: z
101 | .array(
102 | z.instanceof(Object).refine(
103 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
104 | (strategy: Record): strategy is ISigningStrategy => {
105 | return ['getPublicKey', 'sign'].every(
106 | (func) => func in strategy && typeof strategy[func] === 'function'
107 | );
108 | },
109 | { message: `Strategy must implement "ISigningStrategy"` }
110 | )
111 | )
112 | .min(1),
113 | webBundleId: z.string().min(1),
114 | });
115 |
116 | const signingSchema = baseOptionsSchema
117 | .extend({
118 | integrityBlockSign: z.union([
119 | keyBasedIntegrityBlockSignSchema,
120 | strategyBasedIntegrityBlockSignSchema,
121 | strategiesBasedIntegrityBlockSignSchemaWithWebBundleId,
122 | ]),
123 | })
124 |
125 | // Check that `baseURL` is either not set, or set to the expected origin based
126 | // on the private key.
127 | .superRefine((opts, ctx) => {
128 | const webBundleId = opts.integrityBlockSign.webBundleId;
129 | if (opts.baseURL !== '' && new URL(opts.baseURL).host !== webBundleId) {
130 | ctx.addIssue({
131 | code: z.ZodIssueCode.custom,
132 | message: `The hostname of the provided "baseURL" option (${opts.baseURL}) does not match the expected host (${webBundleId}) derived from the public key.`,
133 | });
134 | }
135 | })
136 |
137 | // Set and validate the `headerOverride` option.
138 | .transform((opts, ctx) => {
139 | if (!opts.integrityBlockSign.isIwa) {
140 | return opts;
141 | }
142 |
143 | if (opts.headerOverride === undefined) {
144 | console.info(
145 | `Setting the empty headerOverrides to IWA defaults. To bundle a non-IWA, set \`integrityBlockSign { isIwa: false }\` in your plugin configs. Defaults are set to:\n ${JSON.stringify(
146 | iwaHeaderDefaults
147 | )}`
148 | );
149 | opts.headerOverride = iwaHeaderDefaults;
150 | } else if (typeof opts.headerOverride === 'object') {
151 | try {
152 | checkAndAddIwaHeaders(opts.headerOverride);
153 | } catch (err) {
154 | ctx.addIssue({
155 | code: z.ZodIssueCode.custom,
156 | message: String(err),
157 | });
158 | }
159 | }
160 | return opts;
161 | });
162 |
163 | export const optionsSchema = z.union([nonSigningSchema, signingSchema]);
164 |
165 | export const getValidatedOptionsWithDefaults = optionsSchema.parseAsync;
166 |
167 | export type PluginOptions = z.input;
168 |
169 | export type ValidPluginOptions = z.infer;
170 |
171 | export type ValidIbSignPluginOptions = z.infer;
172 |
--------------------------------------------------------------------------------
/packages/shared/utils.ts:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import * as fs from 'fs';
18 | import * as path from 'path';
19 | import mime from 'mime';
20 | import { combineHeadersForUrl, BundleBuilder } from 'wbn';
21 | import { IntegrityBlockSigner } from 'wbn-sign';
22 | import { checkAndAddIwaHeaders } from './iwa-headers';
23 | import { ValidIbSignPluginOptions, ValidPluginOptions } from './types';
24 |
25 | // If the file name is 'index.html', create an entry for both baseURL/dir/ and
26 | // baseURL/dir/index.html which redirects to the aforementioned. Otherwise just
27 | // for the asset itself. This matches the behavior of gen-bundle.
28 | export function addAsset(
29 | builder: BundleBuilder,
30 | baseURL: string,
31 | relativeAssetPath: string, // Asset's path relative to app's base dir. E.g. sub-dir/helloworld.js
32 | assetContentBuffer: Uint8Array | string,
33 | pluginOptions: ValidPluginOptions
34 | ) {
35 | const parsedAssetPath = path.parse(relativeAssetPath);
36 | const isIndexHtmlFile = parsedAssetPath.base === 'index.html';
37 |
38 | // For object type, the IWA headers have already been check in constructor.
39 | const shouldCheckIwaHeaders =
40 | typeof pluginOptions.headerOverride === 'function' &&
41 | 'integrityBlockSign' in pluginOptions &&
42 | pluginOptions.integrityBlockSign.isIwa;
43 |
44 | if (isIndexHtmlFile) {
45 | const combinedIndexHeaders = combineHeadersForUrl(
46 | { Location: './' },
47 | pluginOptions.headerOverride,
48 | baseURL + relativeAssetPath
49 | );
50 | if (shouldCheckIwaHeaders) checkAndAddIwaHeaders(combinedIndexHeaders);
51 |
52 | builder.addExchange(
53 | baseURL + relativeAssetPath,
54 | 301,
55 | combinedIndexHeaders,
56 | '' // Empty content.
57 | );
58 | }
59 |
60 | const baseURLWithAssetPath =
61 | baseURL + (isIndexHtmlFile ? parsedAssetPath.dir : relativeAssetPath);
62 | const combinedHeaders = combineHeadersForUrl(
63 | {
64 | 'Content-Type':
65 | mime.getType(relativeAssetPath) || 'application/octet-stream',
66 | },
67 | pluginOptions.headerOverride,
68 | baseURLWithAssetPath
69 | );
70 | if (shouldCheckIwaHeaders) checkAndAddIwaHeaders(combinedHeaders);
71 |
72 | builder.addExchange(
73 | baseURLWithAssetPath,
74 | 200,
75 | combinedHeaders,
76 | assetContentBuffer
77 | );
78 | }
79 |
80 | export function addFilesRecursively(
81 | builder: BundleBuilder,
82 | baseURL: string,
83 | dir: string,
84 | pluginOptions: ValidPluginOptions,
85 | recPath = ''
86 | ) {
87 | const files = fs.readdirSync(dir);
88 | files.sort(); // Sort entries for reproducibility.
89 |
90 | for (const fileName of files) {
91 | const filePath = path.join(dir, fileName);
92 |
93 | if (fs.statSync(filePath).isDirectory()) {
94 | addFilesRecursively(
95 | builder,
96 | baseURL,
97 | filePath,
98 | pluginOptions,
99 | recPath + fileName + '/'
100 | );
101 | } else {
102 | const fileContent = fs.readFileSync(filePath);
103 | // `fileName` contains the directory as this is done recursively for every
104 | // directory so it gets added to the baseURL.
105 | addAsset(
106 | builder,
107 | baseURL,
108 | recPath + fileName,
109 | fileContent,
110 | pluginOptions
111 | );
112 | }
113 | }
114 | }
115 |
116 | export async function getSignedWebBundle(
117 | webBundle: Uint8Array,
118 | opts: ValidIbSignPluginOptions,
119 | infoLogger: (str: string) => void
120 | ): Promise {
121 | const { signedWebBundle } = await new IntegrityBlockSigner(
122 | /*is_v2=*/ true,
123 | webBundle,
124 | opts.integrityBlockSign.webBundleId,
125 | opts.integrityBlockSign.strategies
126 | ).sign();
127 |
128 | infoLogger(opts.integrityBlockSign.webBundleId);
129 | return signedWebBundle;
130 | }
131 |
--------------------------------------------------------------------------------
/packages/shared/wbn-types.ts:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // These must match with the types in `wbn`.
18 | // github.com/WICG/webpackage/blob/main/js/bundle/src/constants.ts
19 | const B1 = 'b1';
20 | const B2 = 'b2';
21 | const APPROVED_VERSIONS = [B1, B2] as const;
22 | export type FormatVersion = typeof APPROVED_VERSIONS[number];
23 |
--------------------------------------------------------------------------------
/packages/webbundle-webpack-plugin/README.md:
--------------------------------------------------------------------------------
1 | # webbundle-webpack-plugin
2 |
3 | A Webpack plugin which generates
4 | [Web Bundles](https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html)
5 | output. Currently the spec is still a draft, so this package is also in alpha
6 | until the spec stabilizes.
7 |
8 | ## Requirements
9 |
10 | This plugin requires Node v14.0.0+ and Webpack v4.0.1+.
11 |
12 | ## Install
13 |
14 | Using npm:
15 |
16 | ```bash
17 | npm install webbundle-webpack-plugin --save-dev
18 | ```
19 |
20 | ## Usage
21 |
22 | ### General Web Bundle
23 |
24 | This example assumes your application entry point is `src/index.js` and static
25 | files (including `index.html`) are located in `static` directory.
26 |
27 | ```js
28 | /* webpack.config.js */
29 | const path = require('path');
30 | const WebBundlePlugin = require('webbundle-webpack-plugin');
31 |
32 | module.exports = {
33 | entry: './src/index.js',
34 | output: {
35 | path: path.resolve(__dirname, 'dist'),
36 | filename: 'app.js',
37 | },
38 | plugins: [
39 | new WebBundlePlugin({
40 | baseURL: 'https://example.com/',
41 | static: { dir: path.resolve(__dirname, 'static') },
42 | output: 'example.wbn',
43 | }),
44 | ],
45 | };
46 | ```
47 |
48 | A WBN file `dist/example.wbn` should be written.
49 |
50 | ### [Isolated Web App](https://github.com/WICG/isolated-web-apps/blob/main/README.md) (Signed Web Bundle)
51 |
52 | This example assumes your application entry point is `src/index.js`, static
53 | files (including `index.html`) are located in `static` directory and you have a
54 | `.env` file in the root directory with `ED25519KEY` defined in it. The example
55 | also requires installing `dotenv` npm package as a dev dependency.
56 |
57 | It is also required to have a
58 | [Web App Manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) at
59 | `/.well-known/manifest.webmanifest`, which can be placed e.g. in the `static`
60 | directory.
61 |
62 | Also as in the below example, `baseURL` must be of format
63 | `isolated-app://${WEB_BUNDLE_ID}` for Isolated Web Apps. It can easily be
64 | generated from the private key with `WebBundleId` helper class from `wbn-sign`
65 | package. See
66 | [Scheme explainer](https://github.com/WICG/isolated-web-apps/blob/main/Scheme.md)
67 | for more details. Also note that providing `headerOverride` is optional.
68 |
69 | ```js
70 | /* webpack.config.js */
71 | const path = require('path');
72 | const WebBundlePlugin = require('webbundle-webpack-plugin');
73 | const {
74 | NodeCryptoSigningStrategy,
75 | parsePemKey,
76 | readPassphrase,
77 | WebBundleId,
78 | } = require('wbn-sign');
79 | require('dotenv').config({ path: './.env' });
80 |
81 | module.exports = async () => {
82 | const key = parsePemKey(process.env.ENC_ED25519KEY, await readPassphrase());
83 |
84 | return {
85 | entry: './src/index.js',
86 | output: { path: path.resolve(__dirname, 'dist'), filename: 'app.js' },
87 | plugins: [
88 | new WebBundlePlugin({
89 | baseURL: new WebBundleId(key).serializeWithIsolatedWebAppOrigin(),
90 | static: { dir: path.resolve(__dirname, 'static') },
91 | output: 'signed.swbn',
92 | integrityBlockSign: {
93 | strategy: new NodeCryptoSigningStrategy(key),
94 | },
95 | headerOverride: {
96 | 'cross-origin-embedder-policy': 'require-corp',
97 | 'cross-origin-opener-policy': 'same-origin',
98 | 'cross-origin-resource-policy': 'same-origin',
99 | 'content-security-policy':
100 | "base-uri 'none'; default-src 'self'; object-src 'none'; frame-src 'self' https: blob: data:; connect-src 'self' https: wss:; script-src 'self' 'wasm-unsafe-eval'; img-src 'self' https: blob: data:; media-src 'self' https: blob: data:; font-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; require-trusted-types-for 'script';",
101 | },
102 | }),
103 | ],
104 | };
105 | };
106 | ```
107 |
108 | A signed web bundle (containing an
109 | [Integrity Block](https://github.com/WICG/webpackage/blob/main/explainers/integrity-signature.md))
110 | should be written to `dist/signed.swbn`.
111 |
112 | ## Options
113 |
114 | ### `baseURL`
115 |
116 | Type: `string`
117 |
118 | Default: `''`
119 |
120 | Specifies the URL prefix prepended to the file names in the bundle. Non-empty
121 | baseURL must end with `/`.
122 |
123 | ### `primaryURL`
124 |
125 | Type: `string`
126 |
127 | Default: baseURL
128 |
129 | Specifies the bundle's main resource URL.
130 |
131 | ### `static`
132 |
133 | Type: `{ dir: String, baseURL?: string }`
134 |
135 | If specified, files and subdirectories under `dir` will be added to the bundle.
136 | The `baseURL` field can be omitted and defaults to `Options.baseURL`.
137 |
138 | ### `output`
139 |
140 | Type: `string`
141 |
142 | Default: `out.wbn`
143 |
144 | Specifies the file name of the Web Bundle to emit.
145 |
146 | ### `formatVersion`
147 |
148 | Type: `string`
149 |
150 | Default: `b2`
151 |
152 | Specifies WebBundle format version.
153 |
154 | - version `b2` follows
155 | [the latest version of the Web Bundles spec](https://datatracker.ietf.org/doc/html/draft-yasskin-wpack-bundled-exchanges-04)
156 | (default).
157 | - version `b1` follows
158 | [the previous version of the Web Bundles spec](https://datatracker.ietf.org/doc/html/draft-yasskin-wpack-bundled-exchanges-03).
159 |
160 | ### `integrityBlockSign`
161 |
162 | Type:
163 |
164 | - `{ key: KeyObject, isIwa?: boolean }`
165 | - `{ strategy: ISigningStrategy, isIwa?: boolean }`
166 | - `{ strategies: Array, webBundleId: string, isIwa?: boolean }`
167 |
168 | Object specifying the signing options with
169 | [Integrity Block](https://github.com/WICG/webpackage/blob/main/explainers/integrity-signature.md).
170 |
171 | ### `integrityBlockSign.key`
172 |
173 | Note: Either this or `integrityBlockSign.strategy` is required when
174 | `integrityBlockSign` is in place.
175 |
176 | Type: `KeyObject`
177 |
178 | An unencrypted ed25519 private key can be generated with:
179 |
180 | ```bash
181 | openssl genpkey -algorithm Ed25519 -out ed25519key.pem
182 | ```
183 |
184 | For better security, one should prefer using passphrase-encrypted ed25519
185 | private keys. To encrypt an unencrypted private key, run:
186 |
187 | ```bash
188 | # encrypt the key (will ask for a passphrase, make sure to use a strong one)
189 | openssl pkcs8 -in ed25519key.pem -topk8 -out encrypted_ed25519key.pem
190 |
191 | # delete the unencrypted key
192 | rm ed25519key.pem
193 | ```
194 |
195 | It can be parsed with an imported helper function `parsePemKey(...)` from
196 | `wbn-sign` npm package. For an encrypted private key there's also an async
197 | helper function (`readPassphrase()`) to prompt the user for the passphrase the
198 | key was encrypted with.
199 |
200 | ```js
201 | // For an unencrypted ed25519 key.
202 | const key = parsePemKey(process.env.ED25519KEY);
203 |
204 | // For an encrypted ed25519 key.
205 | const key = parsePemKey(process.env.ENC_ED25519KEY, await readPassphrase());
206 | ```
207 |
208 | Note that in order for the key to be parsed correctly, it must contain the
209 | `BEGIN` and `END` headers and line breaks (`\n`). Below an example `.env` file:
210 |
211 | ```bash
212 | ED25519KEY="-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIB8nP5PpWU7HiILHSfh5PYzb5GAcIfHZ+bw6tcd/LZXh\n-----END PRIVATE KEY-----"
213 | ```
214 |
215 | ### `integrityBlockSign.strategy`
216 |
217 | Note: Either this or `integrityBlockSign.key` is required when
218 | `integrityBlockSign` is in place.
219 |
220 | Type: `ISigningStrategy`
221 |
222 | Example web bundle plugin options using a signing strategy:
223 |
224 | ```js
225 | const pluginOptionsWithPredefinedSigningStrategy = {
226 | // ...other plugin options here...
227 | integrityBlockSign: {
228 | strategy: new NodeCryptoSigningStrategy(privateKey),
229 | },
230 | };
231 |
232 | const pluginOptionsWithCustomSigningStrategy = {
233 | // ...other plugin options here...
234 | integrityBlockSign: {
235 | strategy: new (class /* implements ISigningStrategy */ {
236 | async sign(data) {
237 | /** E.g. connect to one's external signing service that signs the
238 | * payload. */
239 | }
240 | async getPublicKey() {
241 | /** E.g. connect to one's external signing service that returns the
242 | * public key. */
243 | }
244 | })(),
245 | },
246 | };
247 | ```
248 |
249 | ### `integrityBlockSign.strategies`
250 |
251 | Type: `Array`
252 |
253 | Use this overload to sign a bundle with multiple keys. Note that `webBundleId`
254 | must always be specified when using `strategies`.
255 |
256 | ```
257 | const pluginOptionsWithMultipleStrategiesAndWebBundleId = {
258 | // ...other plugin options here...
259 | integrityBlockSign: {
260 | strategies: [
261 | new NodeCryptoSigningStrategy(privateKey1),
262 | new NodeCryptoSigningStrategy(privateKey2)
263 | ],
264 | webBundleId: "some-random-id"
265 | },
266 | };
267 | ```
268 |
269 | ### `integrityBlockSign.webBundleId`
270 |
271 | Type: `string`
272 |
273 | Allows specifying a custom id for this signed web bundle to decouple it from the
274 | signing keys. Must be used together with `strategies`.
275 |
276 | ### `integrityBlockSign.isIwa` (optional)
277 |
278 | Type: `boolean`
279 |
280 | Default: `true`
281 |
282 | If `undefined` or `true`, enforces certain
283 | [Isolated Web App](https://github.com/WICG/isolated-web-apps) -related checks
284 | for the headers. Also adds default IWA headers if completely missing. If set to
285 | `false`, skips validation checks and doesn't tamper with the headers.
286 |
287 | ### `headerOverride` (optional)
288 |
289 | Type: `{ [key: string]: string; }` |
290 | `(filepath: string) => { [key: string]: string; };`
291 |
292 | Object of strings specifying overridden headers or a function returning the same
293 | kind of object.
294 |
295 | ## Discuss & Help
296 |
297 | For discussions related to this repository's content, the Web Bundle plugins for
298 | webpack and rollup, please use
299 | [GitHub Issues](https://github.com/GoogleChromeLabs/webbundle-plugins/issues).
300 |
301 | If you'd like to discuss the Web Packaging proposal itself, consider opening an
302 | issue in its incubation repository at https://github.com/WICG/webpackage.
303 |
304 | For discussions related to Isolated Web Apps in general, or Chromium-specific
305 | implementation and development questions, please use the
306 | [iwa-dev@chromium.org](https://groups.google.com/a/chromium.org/g/iwa-dev)
307 | mailing list.
308 |
309 | If you'd like to discuss the Isolated Web Apps proposal, which builds on top of
310 | Web Bundles, consider opening an issue in the incubation repository at
311 | https://github.com/WICG/isolated-web-apps.
312 |
313 | ## Release Notes
314 |
315 | ### v0.2.0
316 |
317 | - Add support for the v2 integrity block format. Now web-bundle-id is no longer
318 | presumed to be a derivative of the first public key in the stack, but rather
319 | acts as a separate entry in the integrity block attributes, and multiple
320 | independent signatures are allowed to facilitate key rotation.
321 |
322 | ### v0.1.5
323 |
324 | - Add support for ECDSA P-256 SHA-256 signing algorithm
325 | - Bumping underlying wbn-sign version to v0.1.3.
326 |
327 | ### v0.1.4
328 |
329 | - Updates to style-src and wss CSP values.
330 | - Bumping underlying wbn-sign version to v0.1.1.
331 |
332 | ### v0.1.3
333 |
334 | - BUG: Async `integrityBlockSign.strategy` was not working correctly. Now with
335 | `tapPromise` instead of `tap` this is fixed.
336 | \[[#59](https://github.com/GoogleChromeLabs/webbundle-plugins/pull/59/)\]
337 |
338 | ### v0.1.2
339 |
340 | - Add support for `integrityBlockSign.strategy` plugin option which can be used
341 | to pass one of the predefined strategies or one's own implementation class for
342 | ISigningStrategy. One can also use the old `integrityBlockSign.key` option,
343 | which defaults to the predefined `NodeCryptoSigningStrategy` strategy.
344 | - Refactor plugin to be in TypeScript.
345 | - Combine the Webpack and Rollup web bundle plugins to live in the same
346 | repository and share some duplicated code. Taking advantage of
347 | [npm workspaces](https://docs.npmjs.com/cli/v7/using-npm/workspaces).
348 |
349 | ### v0.1.1
350 |
351 | - Add support for overriding headers with an optional `headerOverride` plugin
352 | option.
353 |
354 | ### v0.1.0
355 |
356 | - BREAKING CHANGE: Change type of integrityBlockSign.key to be KeyObject instead
357 | of string.
358 |
359 | ### v0.0.4
360 |
361 | - Support for signing web bundles with
362 | [integrity block](https://github.com/WICG/webpackage/blob/main/explainers/integrity-signature.md)
363 | added.
364 |
365 | ## License
366 |
367 | Licensed under the Apache-2.0 license.
368 |
369 | ## Contributing
370 |
371 | See [CONTRIBUTING.md](../../CONTRIBUTING.md) file.
372 |
373 | ## Disclaimer
374 |
375 | This is not an officially supported Google product.
376 |
--------------------------------------------------------------------------------
/packages/webbundle-webpack-plugin/index.cjs:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2023 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // This is just a CommonJS wrapper for the default export to not to introduce a
18 | // breaking change on how WebBundlePlugin is imported.
19 | // For context, see:
20 | // https://github.com/evanw/esbuild/issues/532#issuecomment-1019392638
21 | // TODO: Get rid of this together with the next other breaking change.
22 |
23 | // eslint-disable-next-line @typescript-eslint/no-var-requires
24 | const { WebBundlePlugin } = require('./lib/index.cjs');
25 | module.exports = WebBundlePlugin;
26 |
--------------------------------------------------------------------------------
/packages/webbundle-webpack-plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webbundle-webpack-plugin",
3 | "version": "0.2.0",
4 | "description": "Webpack plugin to generate WebBundle output.",
5 | "keywords": [
6 | "webpack",
7 | "plugin",
8 | "web-bundle",
9 | "isolated-web-app"
10 | ],
11 | "type": "module",
12 | "main": "index.cjs",
13 | "scripts": {
14 | "prepack": "npm run build && cp ../../LICENSE ./LICENSE",
15 | "postpack": "rm -f ./LICENSE",
16 | "build": "rm -rf lib && esbuild --bundle --packages=external --format=cjs --outfile=lib/index.cjs src/index.ts --platform=node --legal-comments=inline --sourcemap --keep-names"
17 | },
18 | "author": "Kunihiko Sakamoto ",
19 | "contributors": [
20 | "Sonja Laurila (https://github.com/sonkkeli)",
21 | "Christian Flach (https://github.com/cmfcmf)",
22 | "Andrew Rayskiy (https://github.com/GreenGrape)"
23 | ],
24 | "license": "Apache-2.0",
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/GoogleChromeLabs/webbundle-plugins.git",
28 | "directory": "packages/webbundle-webpack-plugin"
29 | },
30 | "peerDependencies": {
31 | "webpack": ">=4.0.1 <6.0.0"
32 | },
33 | "dependencies": {
34 | "mime": "^2.4.4",
35 | "wbn": "0.0.9",
36 | "wbn-sign": "0.2.0",
37 | "zod": "^3.21.4"
38 | },
39 | "devDependencies": {
40 | "esbuild": "^0.17.15",
41 | "memory-fs": "^0.5.0",
42 | "webpack": "^5.94.0",
43 | "@types/webpack": "*"
44 | },
45 | "engines": {
46 | "node": ">= 16.0.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/packages/webbundle-webpack-plugin/src/index.ts:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import webpack, { Compiler, Compilation, WebpackPluginInstance } from 'webpack';
18 | import { BundleBuilder } from 'wbn';
19 | import {
20 | addAsset,
21 | addFilesRecursively,
22 | getSignedWebBundle,
23 | } from '../../shared/utils';
24 | import {
25 | getValidatedOptionsWithDefaults,
26 | PluginOptions,
27 | } from '../../shared/types';
28 |
29 | const PLUGIN_NAME = 'webbundle-webpack-plugin';
30 |
31 | // Returns if the semantic version number of Webpack is 4.
32 | function isWebpackMajorV4(): boolean {
33 | return webpack.version.startsWith('4.');
34 | }
35 |
36 | export class WebBundlePlugin implements WebpackPluginInstance {
37 | constructor(private rawOpts: PluginOptions) {}
38 |
39 | process = async (compilation: Compilation) => {
40 | const opts = await getValidatedOptionsWithDefaults(this.rawOpts);
41 |
42 | const builder = new BundleBuilder(opts.formatVersion);
43 | if ('primaryURL' in opts && opts.primaryURL) {
44 | builder.setPrimaryURL(opts.primaryURL);
45 | }
46 | if (opts.static) {
47 | addFilesRecursively(
48 | builder,
49 | opts.static.baseURL ?? opts.baseURL,
50 | opts.static.dir,
51 | opts
52 | );
53 | }
54 |
55 | for (const [assetName, assetValue] of Object.entries(compilation.assets)) {
56 | const assetRawSource = assetValue.source();
57 | const assetBuffer = Buffer.isBuffer(assetRawSource)
58 | ? assetRawSource
59 | : Buffer.from(assetRawSource);
60 |
61 | addAsset(
62 | builder,
63 | opts.baseURL,
64 | assetName, // This contains the relative path to the base dir already.
65 | assetBuffer,
66 | opts
67 | );
68 | }
69 |
70 | // TODO: Logger is supported v4.37+. Remove once Webpack versions below that
71 | // are no longer supported.
72 | const infoLogger =
73 | typeof compilation.getLogger === 'function'
74 | ? (str: string) => compilation.getLogger(PLUGIN_NAME).info(str)
75 | : (str: string) => console.log(str);
76 |
77 | let webBundle = builder.createBundle();
78 | if ('integrityBlockSign' in opts) {
79 | webBundle = await getSignedWebBundle(webBundle, opts, infoLogger);
80 | }
81 |
82 | if (isWebpackMajorV4()) {
83 | // @ts-expect-error Missing properties don't exist on webpack v4.
84 | compilation.assets[opts.output] = {
85 | source: () => Buffer.from(webBundle),
86 | size: () => webBundle.length,
87 | };
88 | } else {
89 | compilation.assets[opts.output] = new webpack.sources.RawSource(
90 | Buffer.from(webBundle),
91 | /*convertToString=*/ false
92 | );
93 | }
94 | };
95 |
96 | apply = (compiler: Compiler) => {
97 | if (isWebpackMajorV4()) {
98 | compiler.hooks.emit.tapPromise(
99 | this.constructor.name,
100 | (compilation: Compilation) => this.process(compilation)
101 | );
102 | } else {
103 | compiler.hooks.thisCompilation.tap(
104 | this.constructor.name,
105 | (compilation: Compilation) => {
106 | compilation.hooks.processAssets.tapPromise(
107 | {
108 | name: this.constructor.name,
109 | stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER,
110 | },
111 | () => this.process(compilation)
112 | );
113 | }
114 | );
115 | }
116 | };
117 | }
118 |
--------------------------------------------------------------------------------
/packages/webbundle-webpack-plugin/test/fixtures/app.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | console.log('Hello, world!');
18 |
--------------------------------------------------------------------------------
/packages/webbundle-webpack-plugin/test/fixtures/static/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/webbundle-webpack-plugin/test/test.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import test from 'ava';
18 | import crypto from 'crypto';
19 | import webpack from 'webpack';
20 | import MemoryFS from 'memory-fs';
21 | import url from 'url';
22 | import fs from 'fs';
23 | import * as path from 'path';
24 | import * as wbn from 'wbn';
25 | import * as wbnSign from 'wbn-sign';
26 | import { WebBundlePlugin } from '../lib/index.cjs';
27 | import {
28 | coep,
29 | coop,
30 | corp,
31 | csp,
32 | iwaHeaderDefaults,
33 | } from '../../shared/lib/iwa-headers.js';
34 |
35 | const __filename = url.fileURLToPath(import.meta.url);
36 | const __dirname = path.dirname(__filename);
37 |
38 | const TEST_ED25519_PRIVATE_KEY = wbnSign.parsePemKey(
39 | '-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIB8nP5PpWU7HiILHSfh5PYzb5GAcIfHZ+bw6tcd/LZXh\n-----END PRIVATE KEY-----'
40 | );
41 | const TEST_IWA_BASE_URL =
42 | 'isolated-app://4tkrnsmftl4ggvvdkfth3piainqragus2qbhf7rlz2a3wo3rh4wqaaic/';
43 |
44 | function run(options) {
45 | return new Promise((resolve, reject) => {
46 | const compiler = webpack({
47 | mode: 'development',
48 | devtool: false,
49 | entry: path.join(__dirname, 'fixtures', 'app'),
50 | bail: true,
51 | output: {
52 | path: '/out',
53 | filename: 'main.js',
54 | },
55 | plugins: [new WebBundlePlugin(options)],
56 | });
57 | const memfs = new MemoryFS();
58 | compiler.outputFileSystem = memfs;
59 | compiler.run((err, stats) => {
60 | err ? reject(err) : resolve({ stats, memfs });
61 | });
62 | });
63 | }
64 |
65 | test('basic', async (t) => {
66 | const primaryURL = 'https://example.com/main.js';
67 | const { memfs } = await run({
68 | baseURL: 'https://example.com/',
69 | primaryURL,
70 | output: 'example.wbn',
71 | });
72 | t.deepEqual(memfs.readdirSync('/out').sort(), ['example.wbn', 'main.js']);
73 | const js = memfs.readFileSync('/out/main.js');
74 | const bundle = new wbn.Bundle(memfs.readFileSync('/out/example.wbn'));
75 | t.is(bundle.primaryURL, primaryURL);
76 | t.deepEqual(bundle.urls, [primaryURL]);
77 | const resp = bundle.getResponse(primaryURL);
78 | t.is(new TextDecoder('utf-8').decode(resp.body), js.toString());
79 | });
80 |
81 | test('static', async (t) => {
82 | const primaryURL = 'https://example.com/';
83 | const { memfs } = await run({
84 | baseURL: 'https://example.com/',
85 | primaryURL,
86 | static: { dir: path.join(__dirname, 'fixtures', 'static') },
87 | output: 'example.wbn',
88 | });
89 | t.deepEqual(memfs.readdirSync('/out').sort(), ['example.wbn', 'main.js']);
90 | const html = fs.readFileSync(
91 | path.join(__dirname, 'fixtures', 'static', 'index.html')
92 | );
93 | const bundle = new wbn.Bundle(memfs.readFileSync('/out/example.wbn'));
94 | t.is(bundle.primaryURL, primaryURL);
95 | t.deepEqual(bundle.urls.sort(), [
96 | primaryURL,
97 | 'https://example.com/index.html',
98 | 'https://example.com/main.js',
99 | ]);
100 | const resp = bundle.getResponse(primaryURL);
101 | t.is(new TextDecoder('utf-8').decode(resp.body), html.toString());
102 | });
103 |
104 | test('relative', async (t) => {
105 | const { memfs } = await run({
106 | static: { dir: path.join(__dirname, 'fixtures', 'static') },
107 | output: 'example.wbn',
108 | });
109 | t.deepEqual(memfs.readdirSync('/out').sort(), ['example.wbn', 'main.js']);
110 | const html = fs.readFileSync(
111 | path.join(__dirname, 'fixtures', 'static', 'index.html')
112 | );
113 | const js = memfs.readFileSync('/out/main.js');
114 | const bundle = new wbn.Bundle(memfs.readFileSync('/out/example.wbn'));
115 | t.deepEqual(bundle.urls.sort(), ['', 'index.html', 'main.js']);
116 | let resp = bundle.getResponse('');
117 | t.is(new TextDecoder('utf-8').decode(resp.body), html.toString());
118 | resp = bundle.getResponse('main.js');
119 | t.is(new TextDecoder('utf-8').decode(resp.body), js.toString());
120 | });
121 |
122 | test('integrityBlockSign', async (t) => {
123 | const testCases = [
124 | // With default signer.
125 | {
126 | key: TEST_ED25519_PRIVATE_KEY,
127 | },
128 | // With signer option specified.
129 | {
130 | strategy: new wbnSign.NodeCryptoSigningStrategy(TEST_ED25519_PRIVATE_KEY),
131 | },
132 | ];
133 | for (const testCase of testCases) {
134 | const { memfs } = await run({
135 | baseURL: TEST_IWA_BASE_URL,
136 | output: 'example.swbn',
137 | integrityBlockSign: testCase,
138 | });
139 | t.deepEqual(memfs.readdirSync('/out').sort(), ['example.swbn', 'main.js']);
140 |
141 | const swbnFile = memfs.readFileSync('/out/example.swbn');
142 | const wbnLength = Number(Buffer.from(swbnFile.slice(-8)).readBigUint64BE());
143 | t.truthy(wbnLength < swbnFile.length);
144 |
145 | const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(
146 | /*is_v2=*/ true,
147 | swbnFile.slice(-wbnLength),
148 | new wbnSign.WebBundleId(TEST_ED25519_PRIVATE_KEY).serialize(),
149 | [new wbnSign.NodeCryptoSigningStrategy(TEST_ED25519_PRIVATE_KEY)]
150 | ).sign();
151 |
152 | t.deepEqual(swbnFile, Buffer.from(signedWebBundle));
153 | }
154 | });
155 |
156 | test('headerOverride - IWA with good headers', async (t) => {
157 | const headersTestCases = [
158 | // These are added manually as they expect more than just `iwaHeaderDefaults`.
159 | {
160 | headerOverride: { ...iwaHeaderDefaults, 'X-Csrf-Token': 'hello-world' },
161 | expectedHeaders: { ...iwaHeaderDefaults, 'x-csrf-token': 'hello-world' },
162 | },
163 | {
164 | headerOverride: () => {
165 | return { ...iwaHeaderDefaults, 'X-Csrf-Token': 'hello-world' };
166 | },
167 | expectedHeaders: { ...iwaHeaderDefaults, 'x-csrf-token': 'hello-world' },
168 | },
169 | ];
170 |
171 | const headersThatDefaultToIWADefaults = [
172 | { ...coop, ...corp, ...csp },
173 | { ...coep, ...corp, ...csp },
174 | { ...coep, ...coop, ...csp },
175 | { ...coep, ...coop, ...corp },
176 | iwaHeaderDefaults,
177 | {},
178 | undefined,
179 | {
180 | ...iwaHeaderDefaults,
181 | 'Cross-Origin-Embedder-Policy': 'require-corp',
182 | },
183 | ];
184 |
185 | for (const headers of headersThatDefaultToIWADefaults) {
186 | // Both functions and objects are ok so let's test with both.
187 | headersTestCases.push({
188 | headerOverride: headers,
189 | expectedHeaders: iwaHeaderDefaults,
190 | });
191 |
192 | // Not supported as typeof function because that's forced to return `Headers` map.
193 | if (headers === undefined) continue;
194 | headersTestCases.push({
195 | headerOverride: () => headers,
196 | expectedHeaders: iwaHeaderDefaults,
197 | });
198 | }
199 |
200 | for (const headersTestCase of headersTestCases) {
201 | for (const isIwaTestCase of [undefined, true]) {
202 | const { memfs } = await run({
203 | baseURL: TEST_IWA_BASE_URL,
204 | output: 'example.swbn',
205 | integrityBlockSign: {
206 | key: TEST_ED25519_PRIVATE_KEY,
207 | isIwa: isIwaTestCase,
208 | },
209 | headerOverride: headersTestCase.headerOverride,
210 | });
211 | t.deepEqual(memfs.readdirSync('/out').sort(), [
212 | 'example.swbn',
213 | 'main.js',
214 | ]);
215 |
216 | const swbnFile = memfs.readFileSync('/out/example.swbn');
217 | const wbnLength = Number(
218 | Buffer.from(swbnFile.slice(-8)).readBigUint64BE()
219 | );
220 | t.truthy(wbnLength < swbnFile.length);
221 |
222 | const usignedBundle = new wbn.Bundle(swbnFile.slice(-wbnLength));
223 | for (const url of usignedBundle.urls) {
224 | for (const [headerName, headerValue] of Object.entries(
225 | iwaHeaderDefaults
226 | )) {
227 | t.is(usignedBundle.getResponse(url).headers[headerName], headerValue);
228 | }
229 | }
230 | }
231 | }
232 | });
233 |
234 | test("headerOverride - non-IWA doesn't enforce IWA headers", async (t) => {
235 | // Irrelevant what this would contain.
236 | const randomNonIwaHeaders = { 'x-csrf-token': 'hello-world' };
237 |
238 | const headersTestCases = [
239 | {
240 | // Type `object` is ok.
241 | headerOverride: randomNonIwaHeaders,
242 | expectedHeaders: randomNonIwaHeaders,
243 | },
244 | {
245 | // Same but camel case, which gets lower-cased.
246 | headerOverride: { 'X-Csrf-Token': 'hello-world' },
247 | expectedHeaders: randomNonIwaHeaders,
248 | },
249 | {
250 | // Type `function` is ok.
251 | headerOverride: () => randomNonIwaHeaders,
252 | expectedHeaders: randomNonIwaHeaders,
253 | },
254 | {
255 | // When `integrityBlockSign.isIwa` is false and `headerOverride` is
256 | // `undefined`, nothing unusual gets added.
257 | headerOverride: undefined,
258 | expectedHeaders: {},
259 | },
260 | ];
261 |
262 | for (const headersTestCase of headersTestCases) {
263 | const { memfs } = await run({
264 | baseURL: TEST_IWA_BASE_URL,
265 | output: 'example.swbn',
266 | integrityBlockSign: {
267 | key: TEST_ED25519_PRIVATE_KEY,
268 | isIwa: false,
269 | },
270 | headerOverride: headersTestCase.headerOverride,
271 | });
272 | t.deepEqual(memfs.readdirSync('/out').sort(), ['example.swbn', 'main.js']);
273 |
274 | const swbnFile = memfs.readFileSync('/out/example.swbn');
275 | const wbnLength = Number(Buffer.from(swbnFile.slice(-8)).readBigUint64BE());
276 | t.truthy(wbnLength < swbnFile.length);
277 |
278 | const usignedBundle = new wbn.Bundle(swbnFile.slice(-wbnLength));
279 | for (const url of usignedBundle.urls) {
280 | // Added the expected headers.
281 | for (const [headerName, headerValue] of Object.entries(
282 | headersTestCase.expectedHeaders
283 | )) {
284 | t.is(usignedBundle.getResponse(url).headers[headerName], headerValue);
285 | }
286 | // Did not add any IWA headers automatically.
287 | for (const headerName of Object.keys(iwaHeaderDefaults)) {
288 | t.is(usignedBundle.getResponse(url).headers[headerName], undefined);
289 | }
290 | }
291 | }
292 | });
293 |
294 | test("integrityBlockSign with undefined baseURL doesn't fail", async (t) => {
295 | const { memfs } = await run({
296 | output: 'example.swbn',
297 | integrityBlockSign: {
298 | strategy: new wbnSign.NodeCryptoSigningStrategy(TEST_ED25519_PRIVATE_KEY),
299 | },
300 | });
301 | t.deepEqual(memfs.readdirSync('/out').sort(), ['example.swbn', 'main.js']);
302 | });
303 |
304 | // The webpack plugin had a bug that it didn't work for actually async code so
305 | // this test will prevent that from occurring again.
306 | test('integrityBlockSign with sleeping CustomSigningStrategy', async (t) => {
307 | function sleep(ms) {
308 | return new Promise((resolve) => {
309 | setTimeout(resolve, ms);
310 | });
311 | }
312 |
313 | class CustomSigningStrategy {
314 | async sign(data) {
315 | await sleep(500);
316 | return crypto.sign(
317 | /*algorithm=*/ undefined,
318 | data,
319 | TEST_ED25519_PRIVATE_KEY
320 | );
321 | }
322 |
323 | async getPublicKey() {
324 | await sleep(500);
325 | return crypto.createPublicKey(TEST_ED25519_PRIVATE_KEY);
326 | }
327 | }
328 |
329 | const { memfs } = await run({
330 | output: 'async.swbn',
331 | integrityBlockSign: {
332 | strategy: new CustomSigningStrategy(),
333 | },
334 | });
335 | t.deepEqual(memfs.readdirSync('/out').sort(), ['async.swbn', 'main.js']);
336 | });
337 |
338 | test('integrityBlockSign with multiple strategies', async (t) => {
339 | // ECDSA P-256 SHA-256 signatures are not deterministic, so this test uses
340 | // only Ed25519 ones for generating bundles side-by-side.
341 | const keyPairs = [
342 | crypto.generateKeyPairSync('ed25519'),
343 | crypto.generateKeyPairSync('ed25519'),
344 | crypto.generateKeyPairSync('ed25519'),
345 | ];
346 |
347 | const webBundleId = new wbnSign.WebBundleId(keyPairs[0].privateKey);
348 | const strategies = keyPairs.map(
349 | (keyPair) => new wbnSign.NodeCryptoSigningStrategy(keyPair.privateKey)
350 | );
351 |
352 | const { memfs } = await run({
353 | baseURL: webBundleId.serializeWithIsolatedWebAppOrigin(),
354 | output: 'example.swbn',
355 | integrityBlockSign: {
356 | webBundleId: webBundleId.serialize(),
357 | strategies,
358 | },
359 | });
360 |
361 | t.deepEqual(memfs.readdirSync('/out').sort(), ['example.swbn', 'main.js']);
362 |
363 | const swbnFile = memfs.readFileSync('/out/example.swbn');
364 | const wbnLength = Number(Buffer.from(swbnFile.slice(-8)).readBigUint64BE());
365 | t.truthy(wbnLength < swbnFile.length);
366 |
367 | const { signedWebBundle } = await new wbnSign.IntegrityBlockSigner(
368 | /*is_v2=*/ true,
369 | swbnFile.slice(-wbnLength),
370 | webBundleId.serialize(),
371 | strategies
372 | ).sign();
373 |
374 | t.deepEqual(swbnFile, Buffer.from(signedWebBundle));
375 | });
376 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es2020"],
4 | "module": "es2020",
5 | "target": "es2020",
6 | "moduleResolution": "node",
7 | "esModuleInterop": true,
8 | "isolatedModules": true,
9 | "strict": true,
10 | "declaration": true,
11 | "newLine": "lf",
12 | "noImplicitReturns": true,
13 | "skipLibCheck": true,
14 | "allowJs": true,
15 | "noEmit": true
16 | },
17 | "include": ["packages/**/*"],
18 | "exclude": ["**/node_modules", "**/lib"]
19 | }
20 |
--------------------------------------------------------------------------------