├── .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 | --------------------------------------------------------------------------------