├── .npmignore
├── .gitignore
├── test
├── fixtures
│ ├── static
│ │ └── index.html
│ └── index.js
├── snapshots
│ ├── test.js.snap
│ └── test.js.md
└── test.js
├── .github
└── workflows
│ └── build.yml
├── package.json
├── CONTRIBUTING.md
├── lib
├── iwa-headers.js
└── index.js
├── README.md
└── LICENSE
/.npmignore:
--------------------------------------------------------------------------------
1 | test
2 | .github
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | rollup-plugin-webbundle-*.tgz
--------------------------------------------------------------------------------
/test/fixtures/static/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/snapshots/test.js.snap:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/rollup-plugin-webbundle/HEAD/test/snapshots/test.js.snap
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
9 | strategy:
10 | matrix:
11 | node-version: [14.x, 16.x]
12 | rollup-version: [1.21.x, ^1.0.0, ^2.0.0, ^3.0.0]
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - run: npm install rollup@${{ matrix.rollup-version }}
21 | - run: npm run build --if-present
22 | - run: npm test
23 | env:
24 | CI: true
25 |
26 | prettier:
27 | runs-on: ubuntu-latest
28 | steps:
29 | - uses: actions/checkout@v2
30 | - uses: creyD/prettier_action@v4.3
31 | with:
32 | prettier_options: --write **/*.{js,ts}
33 | only_changed: True
34 | dry: True
35 | prettier_version: 2.7.1
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rollup-plugin-webbundle",
3 | "version": "0.1.1",
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 | "type": "module",
12 | "scripts": {
13 | "test": "ava",
14 | "update-snapshots":"ava --update-snapshots",
15 | "lint": "npx prettier --write **/*.{js,md} --config ./package.json"
16 | },
17 | "author": "Kunihiko Sakamoto ",
18 | "license": "Apache-2.0",
19 | "repository": {
20 | "type": "git",
21 | "url": "https://github.com/GoogleChromeLabs/rollup-plugin-webbundle.git"
22 | },
23 | "peerDependencies": {
24 | "rollup": ">=1.21.0 <4.0.0"
25 | },
26 | "dependencies": {
27 | "mime": "^2.4.4",
28 | "wbn": "0.0.9",
29 | "wbn-sign": "0.0.1"
30 | },
31 | "devDependencies": {
32 | "ava": "^4.3.1",
33 | "prettier": "2.8.0",
34 | "rollup": "^2.76.0"
35 | },
36 | "engines": {
37 | "node": ">= 14.0.0"
38 | },
39 | "ava": {
40 | "workerThreads": false,
41 | "files": [
42 | "!**/fixtures/**"
43 | ]
44 | },
45 | "prettier": {
46 | "tabWidth": 2,
47 | "semi": true,
48 | "singleQuote": true,
49 | "printWidth": 80,
50 | "proseWrap": "always"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/iwa-headers.js:
--------------------------------------------------------------------------------
1 | export const coep = Object.freeze({
2 | 'cross-origin-embedder-policy': 'require-corp',
3 | });
4 | export const coop = Object.freeze({
5 | 'cross-origin-opener-policy': 'same-origin',
6 | });
7 | export const corp = Object.freeze({
8 | 'cross-origin-resource-policy': 'same-origin',
9 | });
10 |
11 | export const CSP_HEADER_NAME = 'content-security-policy';
12 | export const csp = Object.freeze({
13 | [CSP_HEADER_NAME]:
14 | "base-uri 'none'; default-src 'self'; object-src 'none'; frame-src 'self' https:; connect-src 'self' https:; script-src 'self' 'wasm-unsafe-eval'; img-src 'self' https: blob: data:; media-src 'self' https: blob: data:; font-src 'self' blob: data:; require-trusted-types-for 'script'; frame-ancestors 'self';",
15 | });
16 |
17 | // These headers must have these exact values for Isolated Web Apps, whereas the
18 | // CSP header can also be more strict.
19 | export const invariableIwaHeaders = Object.freeze({
20 | ...coep,
21 | ...coop,
22 | ...corp,
23 | });
24 |
25 | export const iwaHeaderDefaults = Object.freeze({
26 | ...csp,
27 | ...invariableIwaHeaders,
28 | });
29 |
30 | export function headerNamesToLowerCase(headers) {
31 | const lowerCaseHeaders = {};
32 | for (const [headerName, headerValue] of Object.entries(headers)) {
33 | lowerCaseHeaders[headerName.toLowerCase()] = headerValue;
34 | }
35 | return lowerCaseHeaders;
36 | }
37 |
38 | const ifNotIwaMsg =
39 | "If you are bundling a non-IWA, set `integrityBlockSign: { isIwa: false }` in the plugin's configuration.";
40 |
41 | // Checks if the IWA headers are strict enough or adds in case missing.
42 | export function checkAndAddIwaHeaders(headers) {
43 | const lowerCaseHeaders = headerNamesToLowerCase(headers);
44 |
45 | // Add missing IWA headers.
46 | for (const [iwaHeaderName, iwaHeaderValue] of Object.entries(
47 | iwaHeaderDefaults
48 | )) {
49 | if (!lowerCaseHeaders[iwaHeaderName]) {
50 | console.log(
51 | `For Isolated Web Apps, ${iwaHeaderName} header was automatically set to ${iwaHeaderValue}. ${ifNotIwaMsg}`
52 | );
53 | headers[iwaHeaderName] = iwaHeaderValue;
54 | }
55 | }
56 |
57 | // Check strictness of IWA headers (apart from special case `Content-Security-Policy`).
58 | for (const [iwaHeaderName, iwaHeaderValue] of Object.entries(
59 | invariableIwaHeaders
60 | )) {
61 | if (
62 | lowerCaseHeaders[iwaHeaderName] &&
63 | lowerCaseHeaders[iwaHeaderName].toLowerCase() !== iwaHeaderValue
64 | ) {
65 | throw new Error(
66 | `For Isolated Web Apps ${iwaHeaderName} should be ${iwaHeaderValue}. Now it is ${headers[iwaHeaderName]}. ${ifNotIwaMsg}`
67 | );
68 | }
69 | }
70 |
71 | // TODO: Parse and check strictness of `Content-Security-Policy`.
72 | }
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🚨 This repository is archived 🗄️
2 |
3 | The plugin has been moved to
4 | [GoogleChromeLabs/webbundle-plugins/packages/rollup-plugin-webbundle](https://github.com/GoogleChromeLabs/webbundle-plugins/tree/main/packages/rollup-plugin-webbundle).
5 |
6 | # rollup-plugin-webbundle
7 |
8 | A Rollup plugin which generates
9 | [Web Bundles](https://wicg.github.io/webpackage/draft-yasskin-wpack-bundled-exchanges.html)
10 | output. Currently the spec is still a draft, so this package is also in alpha
11 | until the spec stabilizes.
12 |
13 | ## Requirements
14 |
15 | This plugin requires Node v14.0.0+ and Rollup v1.21.0+.
16 |
17 | ## Install
18 |
19 | Using npm:
20 |
21 | ```console
22 | npm install rollup-plugin-webbundle --save-dev
23 | ```
24 |
25 | ## Usage
26 |
27 | ### General Web Bundle
28 |
29 | This example assumes your application entry point is `src/index.js` and static
30 | files (including `index.html`) are located in `static` directory.
31 |
32 | ```js
33 | /* rollup.config.mjs */
34 | import webbundle from 'rollup-plugin-webbundle';
35 |
36 | export default {
37 | input: 'src/index.js',
38 | output: {
39 | dir: 'dist',
40 | format: 'esm',
41 | },
42 | plugins: [
43 | webbundle({
44 | baseURL: 'https://example.com/',
45 | static: { dir: 'static' },
46 | }),
47 | ],
48 | };
49 | ```
50 |
51 | A WBN file `dist/out.wbn` should be written.
52 |
53 | ### [Isolated Web App](https://github.com/WICG/isolated-web-apps/blob/main/README.md) (Signed Web Bundle)
54 |
55 | This example assumes your application entry point is `src/index.js`, static
56 | files (including `index.html`) are located in `static` directory and you have a
57 | `.env` file in the root directory with `ED25519KEY` defined in it. The example
58 | also requires installing `dotenv` npm package as a dev dependency.
59 |
60 | It is also required to have a
61 | [Web App Manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) at
62 | `/manifest.webmanifest`, which can be placed e.g. in the `static` directory.
63 |
64 | Also as in the below example, `baseURL` must be of format
65 | `isolated-app://${WEB_BUNDLE_ID}` for Isolated Web Apps. It can easily be
66 | generated from the private key with `WebBundleId` helper class from `wbn-sign`
67 | package. See
68 | [Scheme explainer](https://github.com/WICG/isolated-web-apps/blob/main/Scheme.md)
69 | for more details.
70 |
71 | ```js
72 | /* rollup.config.mjs */
73 | import webbundle from 'rollup-plugin-webbundle';
74 | import * as wbnSign from 'wbn-sign';
75 | import dotenv from 'dotenv';
76 | dotenv.config({ path: './.env' });
77 |
78 | const key = wbnSign.parsePemKey(process.env.ED25519KEY);
79 |
80 | export default {
81 | input: 'src/index.js',
82 | output: {
83 | dir: 'dist',
84 | format: 'esm',
85 | },
86 | plugins: [
87 | webbundle({
88 | baseURL: new wbnSign.WebBundleId(key).serializeWithIsolatedWebAppOrigin(),
89 | static: { dir: 'public' },
90 | output: 'signed.swbn',
91 | integrityBlockSign: { key },
92 | }),
93 | ],
94 | };
95 | ```
96 |
97 | A signed web bundle (containing an
98 | [Integrity Block](https://github.com/WICG/webpackage/blob/main/explainers/integrity-signature.md))
99 | should be written to `dist/signed.swbn`.
100 |
101 | ## Options
102 |
103 | ### `baseURL`
104 |
105 | Type: `string`
106 | Default: `''`
107 |
108 | Specifies the URL prefix prepended to the file names in the bundle. Non-empty
109 | baseURL must end with `/`.
110 |
111 | ### `formatVersion`
112 |
113 | Type: `string`
114 | Default: `b2`
115 |
116 | Specifies WebBundle format version.
117 |
118 | ### `primaryURL`
119 |
120 | Type: `string`
121 | Default: baseURL
122 |
123 | Specifies the bundle's main resource URL. If omitted, the value of the `baseURL`
124 | option is used.
125 |
126 | ### `static`
127 |
128 | Type: `{ dir: String, baseURL?: string }`
129 |
130 | If specified, files and subdirectories under `dir` will be added to the bundle.
131 | `baseURL` can be omitted and defaults to `Options.baseURL`.
132 |
133 | ### `output`
134 |
135 | Type: `string`
136 | Default: `out.wbn`
137 |
138 | Specifies the file name of the Web Bundle to emit.
139 |
140 | ### `integrityBlockSign`
141 |
142 | Type: `{ key: KeyObject }`
143 |
144 | Object specifying the signing options with
145 | [Integrity Block](https://github.com/WICG/webpackage/blob/main/explainers/integrity-signature.md).
146 |
147 | ### `integrityBlockSign.key` (required if `integrityBlockSign` is in place)
148 |
149 | Type: `KeyObject`
150 |
151 | A parsed Ed25519 private key, which can be generated with:
152 |
153 | ```bash
154 | openssl genpkey -algorithm Ed25519 -out ed25519key.pem
155 | ```
156 |
157 | And parsed with `wbnSign.parsePemKey(process.env.ED25519KEY)` helper function.
158 |
159 | Note than in order for it to be parsed correctly, it must contain the `BEGIN`
160 | and `END` texts and line breaks (`\n`). Below an example `.env` file:
161 |
162 | ```bash
163 | ED25519KEY="-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIB8nP5PpWU7HiILHSfh5PYzb5GAcIfHZ+bw6tcd/LZXh\n-----END PRIVATE KEY-----"
164 | ```
165 |
166 | ### `integrityBlockSign.isIwa`
167 |
168 | Type: `boolean`
169 |
170 | If `undefined` or `true`, enforces certain
171 | [Isolated Web App](https://github.com/WICG/isolated-web-apps) -related checks
172 | for the headers. Also adds default IWA headers if completely missing. If set to
173 | `false`, skips validation checks and doesn't tamper with the headers.
174 |
175 | ### `headerOverride`
176 |
177 | Type: `{ [key: string]: string; }` |
178 | `(filepath: string) => { [key: string]: string; };`
179 |
180 | Object of strings specifying overridden headers or a function returning the same
181 | kind of object.
182 |
183 | ## License
184 |
185 | Licensed under the Apache-2.0 license.
186 |
187 | ## Contributing
188 |
189 | See [CONTRIBUTING.md](CONTRIBUTING.md) file.
190 |
191 | ## Disclaimer
192 |
193 | This is not an officially supported Google product.
194 |
195 | ## Release Notes
196 |
197 | ### v0.1.1
198 |
199 | - Add support for overriding headers.
200 |
201 | ### v0.1.0
202 |
203 | - BREAKING CHANGE: Change type of integrityBlockSign.key to be KeyObject instead
204 | of string.
205 | - Upgrade to support Rollup 3.
206 |
207 | ### v0.0.4
208 |
209 | - Support for signing web bundles with
210 | [integrity block](https://github.com/WICG/webpackage/blob/main/explainers/integrity-signature.md)
211 | added.
212 |
--------------------------------------------------------------------------------
/test/snapshots/test.js.md:
--------------------------------------------------------------------------------
1 | # Snapshot report for `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 |
--------------------------------------------------------------------------------
/lib/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 | import * as fs from 'fs';
18 | import mime from 'mime';
19 | import * as path from 'path';
20 | import { BundleBuilder, combineHeadersForUrl } from 'wbn';
21 | import { IntegrityBlockSigner, WebBundleId } from 'wbn-sign';
22 | import { iwaHeaderDefaults, checkAndAddIwaHeaders } from './iwa-headers.js';
23 |
24 | const defaults = {
25 | output: 'out.wbn',
26 | baseURL: '',
27 | };
28 |
29 | // If the file name is 'index.html', create an entry for both baseURL/dir/
30 | // and baseURL/dir/index.html which redirects to the aforementioned. Otherwise
31 | // just for the asset itself. This matches the behavior of gen-bundle.
32 | function addAsset(
33 | builder,
34 | baseURL,
35 | relativeAssetPath, // Asset's path relative to app's base dir. E.g. sub-dir/helloworld.js
36 | assetContentBuffer,
37 | pluginOptions
38 | ) {
39 | const parsedAssetPath = path.parse(relativeAssetPath);
40 | const isIndexHtmlFile = parsedAssetPath.base === 'index.html';
41 |
42 | // For object type, the IWA headers have already been check in constructor.
43 | const shouldCheckIwaHeaders =
44 | typeof pluginOptions.headerOverride === 'function' &&
45 | pluginOptions.integrityBlockSign &&
46 | pluginOptions.integrityBlockSign.isIwa;
47 |
48 | if (isIndexHtmlFile) {
49 | const combinedIndexHeaders = combineHeadersForUrl(
50 | { Location: './' },
51 | pluginOptions.headerOverride,
52 | baseURL + relativeAssetPath
53 | );
54 | if (shouldCheckIwaHeaders) checkAndAddIwaHeaders(combinedIndexHeaders);
55 |
56 | builder.addExchange(
57 | baseURL + relativeAssetPath,
58 | 301,
59 | combinedIndexHeaders,
60 | '' // Empty content.
61 | );
62 | }
63 |
64 | const baseURLWithAssetPath =
65 | baseURL + (isIndexHtmlFile ? parsedAssetPath.dir : relativeAssetPath);
66 | const combinedHeaders = combineHeadersForUrl(
67 | {
68 | 'Content-Type':
69 | mime.getType(relativeAssetPath) || 'application/octet-stream',
70 | },
71 | pluginOptions.headerOverride,
72 | baseURLWithAssetPath
73 | );
74 | if (shouldCheckIwaHeaders) checkAndAddIwaHeaders(combinedHeaders);
75 |
76 | builder.addExchange(
77 | baseURLWithAssetPath,
78 | 200,
79 | combinedHeaders,
80 | assetContentBuffer
81 | );
82 | }
83 |
84 | function addFilesRecursively(
85 | builder,
86 | baseURL,
87 | dir,
88 | pluginOptions,
89 | recPath = ''
90 | ) {
91 | if (baseURL !== '' && !baseURL.endsWith('/')) {
92 | throw new Error("Non-empty baseURL must end with '/'.");
93 | }
94 | const files = fs.readdirSync(dir);
95 | files.sort(); // Sort entries for reproducibility.
96 | for (const fileName of files) {
97 | const filePath = path.join(dir, fileName);
98 | if (fs.statSync(filePath).isDirectory()) {
99 | addFilesRecursively(
100 | builder,
101 | baseURL,
102 | filePath,
103 | pluginOptions,
104 | recPath + fileName + '/'
105 | );
106 | } else {
107 | const fileContent = fs.readFileSync(filePath);
108 | // `fileName` contains the directory as this is done recursively for every
109 | // directory so it gets added to the baseURL.
110 | addAsset(
111 | builder,
112 | baseURL,
113 | recPath + fileName,
114 | fileContent,
115 | pluginOptions
116 | );
117 | }
118 | }
119 | }
120 |
121 | function maybeSignWebBundle(webBundle, opts) {
122 | if (!opts.integrityBlockSign) {
123 | return webBundle;
124 | }
125 |
126 | const { signedWebBundle } = new IntegrityBlockSigner(webBundle, {
127 | key: opts.integrityBlockSign.key,
128 | }).sign();
129 |
130 | const consoleLogColor = { green: '\x1b[32m', reset: '\x1b[0m' };
131 | console.log(
132 | `${consoleLogColor.green}${new WebBundleId(opts.integrityBlockSign.key)}${
133 | consoleLogColor.reset
134 | }\n`
135 | );
136 | return signedWebBundle;
137 | }
138 |
139 | function maybeSetIwaDefaults(opts) {
140 | // Note that `undefined` is ignored on purpose.
141 | if (opts.integrityBlockSign.isIwa === false) {
142 | return;
143 | }
144 |
145 | // `isIwa` is defaulting to `true` if not provided as currently there is no
146 | // other use case for integrityBlockSign outside of IWAs.
147 | opts.integrityBlockSign.isIwa = true;
148 |
149 | if (opts.headerOverride === undefined) {
150 | console.info(
151 | `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(
152 | iwaHeaderDefaults
153 | )}`
154 | );
155 | opts.headerOverride = iwaHeaderDefaults;
156 | }
157 | }
158 |
159 | function validateIntegrityBlockOptions(opts) {
160 | maybeSetIwaDefaults(opts);
161 |
162 | if (opts.primaryURL !== undefined) {
163 | throw new Error('Primary URL is not supported for Isolated Web Apps.');
164 | }
165 |
166 | if (opts.baseURL !== '') {
167 | const expectedOrigin = new WebBundleId(
168 | opts.integrityBlockSign.key
169 | ).serializeWithIsolatedWebAppOrigin();
170 |
171 | if (opts.baseURL !== expectedOrigin) {
172 | throw new Error(
173 | `The provided "baseURL" option (${opts.baseURL}) does not match the expected base URL (${expectedOrigin}), which is derived from the provided private key`
174 | );
175 | }
176 | }
177 |
178 | if (
179 | opts.integrityBlockSign.isIwa === true &&
180 | typeof opts.headerOverride === 'object'
181 | ) {
182 | checkAndAddIwaHeaders(opts.headerOverride);
183 | }
184 | }
185 |
186 | function validateOptions(opts) {
187 | if (opts.baseURL !== '' && !opts.baseURL.endsWith('/')) {
188 | throw new Error('Non-empty baseURL must end with "/".');
189 | }
190 | if (opts.integrityBlockSign) {
191 | validateIntegrityBlockOptions(opts);
192 | }
193 | }
194 |
195 | export default function wbnOutputPlugin(opts) {
196 | opts = Object.assign({}, defaults, opts);
197 | validateOptions(opts);
198 |
199 | return {
200 | name: 'wbn-output-plugin',
201 | enforce: 'post',
202 |
203 | async generateBundle(_, bundle) {
204 | const builder = new BundleBuilder(opts.formatVersion);
205 | if (opts.primaryURL) {
206 | builder.setPrimaryURL(opts.primaryURL);
207 | }
208 | if (opts.static) {
209 | addFilesRecursively(
210 | builder,
211 | opts.static.baseURL || opts.baseURL,
212 | opts.static.dir,
213 | opts
214 | );
215 | }
216 |
217 | for (let name of Object.keys(bundle)) {
218 | const asset = bundle[name];
219 | const content = asset.type === 'asset' ? asset.source : asset.code;
220 | addAsset(
221 | builder,
222 | opts.baseURL,
223 | asset.fileName, // This contains the relative path to the base dir already.
224 | content,
225 | opts
226 | );
227 | delete bundle[name];
228 | }
229 |
230 | const webBundle = maybeSignWebBundle(builder.createBundle(), opts);
231 |
232 | this.emitFile({
233 | fileName: opts.output,
234 | type: 'asset',
235 | source: Buffer.from(
236 | webBundle,
237 | webBundle.byteOffset,
238 | webBundle.byteLength
239 | ),
240 | });
241 | },
242 | };
243 | }
244 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 url from 'url';
21 | import * as wbn from 'wbn';
22 | import * as wbnSign from 'wbn-sign';
23 |
24 | import webbundle from '../lib/index.js';
25 | import {
26 | coep,
27 | coop,
28 | corp,
29 | csp,
30 | iwaHeaderDefaults,
31 | } from '../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 bundle = await rollup.rollup({
174 | input: 'fixtures/index.js',
175 | plugins: [
176 | webbundle({
177 | baseURL: TEST_IWA_BASE_URL,
178 | output: outputFileName,
179 | integrityBlockSign: {
180 | key: TEST_ED25519_PRIVATE_KEY,
181 | },
182 | }),
183 | ],
184 | });
185 | const { output } = await bundle.generate({ format: 'esm' });
186 | const keys = Object.keys(output);
187 | t.is(keys.length, 1);
188 | t.is(output[keys[0]].fileName, outputFileName);
189 |
190 | const swbnFile = output[keys[0]].source;
191 | const wbnLength = Number(Buffer.from(swbnFile.slice(-8)).readBigUint64BE());
192 | t.truthy(wbnLength < swbnFile.length);
193 | const { signedWebBundle } = new wbnSign.IntegrityBlockSigner(
194 | swbnFile.slice(-wbnLength),
195 | { key: TEST_ED25519_PRIVATE_KEY }
196 | ).sign();
197 |
198 | t.deepEqual(swbnFile, Buffer.from(signedWebBundle));
199 | });
200 |
201 | test('headerOverride - IWA with good headers', async (t) => {
202 | const headersTestCases = [
203 | // These are added manually as they expect more than just `iwaHeaderDefaults`.
204 | {
205 | headerOverride: {
206 | ...iwaHeaderDefaults,
207 | 'X-Csrf-Token': 'hello-world',
208 | },
209 | expectedHeaders: {
210 | ...iwaHeaderDefaults,
211 | 'x-csrf-token': 'hello-world',
212 | },
213 | },
214 | {
215 | headerOverride: () => {
216 | return {
217 | ...iwaHeaderDefaults,
218 | 'X-Csrf-Token': 'hello-world',
219 | };
220 | },
221 | expectedHeaders: {
222 | ...iwaHeaderDefaults,
223 | 'x-csrf-token': 'hello-world',
224 | },
225 | },
226 | ];
227 |
228 | const headersThatDefaultToIWADefaults = [
229 | { ...coop, ...corp, ...csp },
230 | { ...coep, ...corp, ...csp },
231 | { ...coep, ...coop, ...csp },
232 | { ...coep, ...coop, ...corp },
233 | iwaHeaderDefaults,
234 | {},
235 | undefined,
236 | {
237 | ...iwaHeaderDefaults,
238 | 'Cross-Origin-Embedder-Policy': 'require-corp',
239 | },
240 | ];
241 |
242 | for (const headers of headersThatDefaultToIWADefaults) {
243 | // Both functions and objects are ok so let's test with both.
244 | headersTestCases.push({
245 | headerOverride: headers,
246 | expectedHeaders: iwaHeaderDefaults,
247 | });
248 |
249 | // Not supported as typeof function because that's forced to return `Headers` map.
250 | if (headers === undefined) continue;
251 | headersTestCases.push({
252 | headerOverride: () => headers,
253 | expectedHeaders: iwaHeaderDefaults,
254 | });
255 | }
256 |
257 | const outputFileName = 'out.swbn';
258 | for (const headersTestCase of headersTestCases) {
259 | for (const isIwaTestCase of [undefined, true]) {
260 | const bundle = await rollup.rollup({
261 | input: 'fixtures/index.js',
262 | plugins: [
263 | webbundle({
264 | baseURL: TEST_IWA_BASE_URL,
265 | output: outputFileName,
266 | integrityBlockSign: {
267 | key: TEST_ED25519_PRIVATE_KEY,
268 | isIwa: isIwaTestCase,
269 | },
270 | headerOverride: headersTestCase.headerOverride,
271 | }),
272 | ],
273 | });
274 | const { output } = await bundle.generate({ format: 'esm' });
275 | const keys = Object.keys(output);
276 | t.is(keys.length, 1);
277 | t.is(output[keys[0]].fileName, outputFileName);
278 |
279 | const swbnFile = output[keys[0]].source;
280 | const wbnLength = Number(
281 | Buffer.from(swbnFile.slice(-8)).readBigUint64BE()
282 | );
283 | t.truthy(wbnLength < swbnFile.length);
284 |
285 | const usignedBundle = new wbn.Bundle(swbnFile.slice(-wbnLength));
286 | for (const url of usignedBundle.urls) {
287 | for (const [headerName, headerValue] of Object.entries(
288 | iwaHeaderDefaults
289 | )) {
290 | t.is(usignedBundle.getResponse(url).headers[headerName], headerValue);
291 | }
292 | }
293 | }
294 | }
295 | });
296 |
297 | test('headerOverride - IWA with bad headers', async (t) => {
298 | const badHeadersTestCase = [
299 | { 'cross-origin-embedder-policy': 'unsafe-none' },
300 | { 'cross-origin-opener-policy': 'unsafe-none' },
301 | { 'cross-origin-resource-policy': 'cross-origin' },
302 | ];
303 |
304 | for (const badHeaders of badHeadersTestCase) {
305 | for (const isIwaTestCase of [undefined, true]) {
306 | await t.throwsAsync(
307 | async () => {
308 | await rollup.rollup({
309 | input: 'fixtures/index.js',
310 | plugins: [
311 | webbundle({
312 | baseURL: TEST_IWA_BASE_URL,
313 | output: 'example.swbn',
314 | integrityBlockSign: {
315 | key: TEST_ED25519_PRIVATE_KEY,
316 | isIwa: isIwaTestCase,
317 | },
318 | headerOverride: badHeaders,
319 | }),
320 | ],
321 | });
322 | },
323 | { instanceOf: Error }
324 | );
325 | }
326 | }
327 | });
328 |
329 | test("headerOverride - non-IWA doesn't enforce IWA headers", async (t) => {
330 | // Irrelevant what this would contain.
331 | const randomNonIwaHeaders = { 'x-csrf-token': 'hello-world' };
332 |
333 | const headersTestCases = [
334 | {
335 | // Type `object` is ok.
336 | headerOverride: randomNonIwaHeaders,
337 | expectedHeaders: randomNonIwaHeaders,
338 | },
339 | {
340 | // Same but camel case, which gets lower-cased.
341 | headerOverride: { 'X-Csrf-Token': 'hello-world' },
342 | expectedHeaders: randomNonIwaHeaders,
343 | },
344 | {
345 | // Type `function` is ok.
346 | headerOverride: () => randomNonIwaHeaders,
347 | expectedHeaders: randomNonIwaHeaders,
348 | },
349 | {
350 | // When `integrityBlockSign.isIwa` is false and `headerOverride` is
351 | // `undefined`, nothing unusual gets added.
352 | headerOverride: undefined,
353 | expectedHeaders: {},
354 | },
355 | ];
356 |
357 | const outputFileName = 'out.swbn';
358 | for (const headersTestCase of headersTestCases) {
359 | const bundle = await rollup.rollup({
360 | input: 'fixtures/index.js',
361 | plugins: [
362 | webbundle({
363 | baseURL: TEST_IWA_BASE_URL,
364 | output: outputFileName,
365 | integrityBlockSign: {
366 | key: TEST_ED25519_PRIVATE_KEY,
367 | isIwa: false,
368 | },
369 | headerOverride: headersTestCase.headerOverride,
370 | }),
371 | ],
372 | });
373 |
374 | const { output } = await bundle.generate({ format: 'esm' });
375 | const keys = Object.keys(output);
376 | t.is(keys.length, 1);
377 | t.is(output[keys[0]].fileName, outputFileName);
378 | const swbnFile = output[keys[0]].source;
379 |
380 | const wbnLength = Number(Buffer.from(swbnFile.slice(-8)).readBigUint64BE());
381 | t.truthy(wbnLength < swbnFile.length);
382 |
383 | const usignedBundle = new wbn.Bundle(swbnFile.slice(-wbnLength));
384 | for (const url of usignedBundle.urls) {
385 | // Added the expected headers.
386 | for (const [headerName, headerValue] of Object.entries(
387 | headersTestCase.expectedHeaders
388 | )) {
389 | t.is(usignedBundle.getResponse(url).headers[headerName], headerValue);
390 | }
391 | // Did not add any IWA headers automatically.
392 | for (const headerName of Object.keys(iwaHeaderDefaults)) {
393 | t.is(usignedBundle.getResponse(url).headers[headerName], undefined);
394 | }
395 | }
396 | }
397 | });
398 |
--------------------------------------------------------------------------------