├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── DOCS.md ├── LICENSE ├── README.md ├── lerna.json ├── package.json ├── packages ├── flyyer-lite │ ├── LICENSE │ ├── package.json │ ├── src │ │ ├── common.ts │ │ ├── ext.ts │ │ ├── flyyer.ts │ │ ├── index.ts │ │ ├── invariant.ts │ │ ├── meta.ts │ │ ├── paths.ts │ │ ├── query.ts │ │ ├── render.ts │ │ ├── utils.ts │ │ ├── v.ts │ │ └── variables.ts │ ├── test │ │ ├── flyyer.test.ts │ │ ├── query.test.ts │ │ └── render.test.ts │ └── tsconfig.json └── flyyer │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ ├── compare.ts │ ├── crypto │ │ ├── LICENSE │ │ ├── core.js │ │ ├── enc-base64.js │ │ └── sha256.js │ ├── flyyer-render-signed.ts │ ├── flyyer-signed.ts │ ├── index.ts │ └── jwt.ts │ ├── test │ ├── flyyer-render.test.ts │ └── flyyer.test.ts │ └── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | !.eslintrc.js 2 | 3 | .vscode 4 | .rpt2_cache 5 | dist 6 | public 7 | node_modules 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "@flyyer/eslint-config", 4 | "@flyyer/eslint-config/typescript", 5 | "@flyyer/eslint-config/jest", 6 | "@flyyer/eslint-config/prettier", 7 | ], 8 | rules: { 9 | "@typescript-eslint/no-shadow": "off", 10 | "@typescript-eslint/no-empty-function": "off", 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['12.x', '14.x', '16.x'] 11 | os: [ubuntu-latest, macOS-latest] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Use Node ${{ matrix.node }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node }} 20 | 21 | - name: modules cache 22 | uses: actions/cache@v2 23 | with: 24 | path: "**/node_modules" 25 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 26 | 27 | - name: Bootstrap 28 | - run: yarn bootstrap 29 | 30 | - name: Lint 31 | run: yarn lint 32 | 33 | - name: Build 34 | run: yarn build 35 | 36 | - name: Test 37 | run: yarn test -- -- --ci --coverage --maxWorkers=2 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | # Docs 2 | 3 | ## Publishing 4 | 5 | Create a new [Lerna version](https://github.com/lerna/lerna/tree/main/commands/version#readme): 6 | 7 | ```bash 8 | yarn run lerna version 9 | ``` 10 | 11 | Publish packages: 12 | 13 | ```bash 14 | yarn run lerna publish from-git --yes 15 | ``` 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flyyer.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flyyer-js 2 | 3 | ![npm-version](https://badgen.net/npm/v/@flyyer/flyyer) ![downloads](https://badgen.net/npm/dt/@flyyer/flyyer) ![size](https://badgen.net/bundlephobia/minzip/@flyyer/flyyer) ![tree-shake](https://badgen.net/bundlephobia/tree-shaking/@flyyer/flyyer) 4 | 5 | Format URLs to generate social media images using Flyyer.io. 6 | 7 | **To create templates with React.js or Vue.js use [create-flyyer-app](https://github.com/useflyyer/create-flyyer-app) 👈** 8 | 9 | ![Flyyer live image](https://github.com/useflyyer/create-flyyer-app/blob/master/.github/assets/website-to-preview.png?raw=true&v=1) 10 | 11 | **This module is agnostic to any JS framework and has only one dependency: [qs](https://github.com/ljharb/qs).** 12 | 13 | ## Index 14 | 15 | - [Get started (5 minutes)](#get-started-5-minutes) 16 | - [Advanced usage](#advanced-usage) 17 | - [Flyyer Render](#flyyer-render) 18 | - [Development](#development) 19 | - [Test](#test) 20 | - [FAQ](#faq) 21 | 22 | ## Get started (5 minutes) 23 | 24 | ### 1. Install module 25 | 26 | This module supports Node.js, Browser and can be bundled with any tool such as Rollup, Webpack, etc and includes Typescript definitions. 27 | 28 | ```sh 29 | yarn add @flyyer/flyyer 30 | 31 | # or with npm: 32 | npm install --save @flyyer/flyyer 33 | ``` 34 | 35 | ### 2. Flyyer.io smart image link 36 | 37 | > Haven't registered your website yet? Go to [Flyyer.io](https://flyyer.io/get-started?ref=flyyer-js) and import your website to create a project (e.g. `website-com`). 38 | 39 | For each of your routes, create an instance. 40 | 41 | ```tsx 42 | import { Flyyer } from "@flyyer/flyyer"; 43 | 44 | const flyyer = new Flyyer({ 45 | // Your project slug 46 | project: "website-com", 47 | // Relative path 48 | path: `/path/to/product`, 49 | // Optional: preserve and re-use your default or current image. 50 | // default: "/images/default-image.png", 51 | }); 52 | 53 | console.log(flyyer.href()); 54 | // > https://cdn.flyyer.io/v2/website-com/_/__v=1618281823/path/to/product 55 | ``` 56 | 57 | #### 2.1 Next.js 58 | 59 | Remember to dynamically get the current path for each page. If you are using [Next.js](https://nextjs.org/) you should probably do this: 60 | 61 | ```tsx 62 | import { useRouter } from 'next/router' 63 | 64 | function SEO() { 65 | const router = useRouter(); 66 | const flyyer = new Flyyer({ 67 | project: "my-project", 68 | path: router.asPath, 69 | // default: product["image"], 70 | }); 71 | // ... 72 | } 73 | ``` 74 | 75 | Check our official Next.js documentation [here](https://docs.flyyer.io/guides/javascript/nextjs?ref=flyyer-js); 76 | 77 | ### 3. Setup `` meta tags 78 | 79 | You'll get the best results doing this: 80 | 81 | ```tsx 82 | 83 | 84 | 85 | ``` 86 | 87 | ### 4. Manage rules 88 | 89 | [Login at Flyyer.io](https://flyyer.io/dashboard/_/projects/_/manage?ref=flyyer-js), select your project and go to Manage rules. Then create a rule like the following: 90 | 91 | [![Flyyer basic rule example](https://github.com/useflyyer/create-flyyer-app/blob/master/.github/assets/rule-example.png?raw=true&v=1)](https://flyyer.io/dashboard/) 92 | 93 | Voilà! **To create templates with React.js or Vue.js use [create-flyyer-app](https://github.com/useflyyer/create-flyyer-app) 👈** 94 | 95 | ## Advanced usage 96 | 97 | Advanced features include: 98 | 99 | - Custom variables: additional information for your preview that is not present in your website. [Note: if you need customization you should take a look at [Flyyer Render](#flyyer-render)] 100 | - Custom metadata: set custom width, height, resolution, and more (see example). 101 | - Signed URLs. 102 | 103 | Here you have a detailed full example for project `website-com` and path `/path/to/product`. 104 | 105 | ```tsx 106 | import { Flyyer } from "@flyyer/flyyer"; 107 | 108 | const flyyer = new Flyyer({ 109 | // Project slug, find it in your dashboard https://flyyer.io/dashboard/. 110 | project: "website-com", 111 | // The current path of your website (by default it's `/`). 112 | path: "/path/to/product", 113 | 114 | // [Optional] Keep and re-use your current image. 115 | default: product["image"], 116 | 117 | // [Optional] In case you want to provide information that is not present in your page set it here. 118 | variables: { 119 | title: "Product name", 120 | img: "https://flyyer.io/img/marketplace/flyyer-banner.png", 121 | }, 122 | // [Optional] Additional variables. 123 | meta: { 124 | id: "jeans-123", // stats identifier (e.g. product SKU), defaults to `path`. 125 | width: 1080, // force width (pixels). 126 | height: 1080, // force height (pixels). 127 | v: null, // cache-burst, to circumvent platforms' cache, default to a timestamp, null to disable. 128 | }, 129 | }); 130 | ``` 131 | 132 | > Read more about integration guides here: https://docs.flyyer.io/guides 133 | 134 | ## Flyyer Lite 135 | 136 | If you are not using Signed URLs you can opt-in for `@flyyer/flyyer-lite` which is a lightweight version because it doesn't include crypto functions. 137 | 138 | ```sh 139 | yarn add @flyyer/flyyer-lite 140 | ``` 141 | 142 | Usage is the same: 143 | 144 | ```tsx 145 | import { Flyyer } from "@flyyer/flyyer-lite"; 146 | // ... 147 | ``` 148 | 149 | ## FlyyerRender 150 | 151 | * Flyyer uses the [rules defined on your dashboard](https://flyyer.io/dashboard/_/projects) to decide how to handle every image. It analyse your website to render a content-rich image. Let's say _"Flyyer renders images based on the content of this route"_. 152 | 153 | * FlyyerRender instead requires you to explicitly declare template and variables for the images to render, **giving you more control for customization**. Let's say _"FlyyerRender renders an image using this template and these explicit variables"_. 154 | 155 | ```tsx 156 | import { FlyyerRender } from "@flyyer/flyyer"; 157 | const flyyer = new FlyyerRender({ 158 | tenant: "flyyer", 159 | deck: "default", 160 | template: "main", 161 | variables: { title: "try changing this" }, 162 | }); 163 | const url = flyyer.href() 164 | // https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=try+changing+this 165 | ``` 166 | 167 | [![Resultant flyyer live image](https://github.com/useflyyer/create-flyyer-app/blob/master/.github/assets/result-1.png?raw=true&v=1)](https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=try+changing+this) 168 | 169 | After installing this module you can format URLs. Here is an example with React.js, but note this can be used with any framework: 170 | 171 | ```tsx 172 | import React from "react"; 173 | import { FlyyerRender } from "@flyyer/flyyer"; 174 | 175 | function Head() { 176 | const flyyer = new FlyyerRender({ 177 | tenant: "tenant", 178 | deck: "deck", 179 | template: "template", 180 | variables: { 181 | title: "Hello world!", 182 | image: "https://yoursite.com/image/products/1.png", 183 | }, 184 | }); 185 | const url = flyyer.href(); 186 | 187 | return ( 188 | 189 | 190 | 191 | 192 | 193 | ); 194 | } 195 | ``` 196 | 197 | Variables can be complex arrays and objects. 198 | 199 | ```js 200 | const flyyer = new FlyyerRender({ 201 | // ... 202 | variables: { 203 | items: [ 204 | { text: "Oranges", count: 12 }, 205 | { text: "Apples", count: 14 }, 206 | ], 207 | }, 208 | meta { 209 | id: "slug-or-id", // To identify the resource in our analytics report 210 | } 211 | }); 212 | ``` 213 | 214 | **IMPORTANT: variables must be serializable.** 215 | 216 | You can **set image dimensions**, note if your are planing to use this as `` you should disable cache-bursting. 217 | 218 | ```tsx 219 | const flyyer = new FlyyerRender({ 220 | tenant: "tenant", 221 | deck: "default", 222 | template: "main", 223 | variables: { 224 | title: "Awesome 😃", 225 | description: "Optional description", 226 | }, 227 | meta: { 228 | v: null, // prevent cache-bursting in browsers 229 | width: 1080, // in pixels 230 | height: 1920, // in pixels 231 | } 232 | }); 233 | 234 | 235 | ``` 236 | 237 | [![Resultant flyyer live image](https://github.com/useflyyer/create-flyyer-app/blob/master/.github/assets/result-2.png?raw=true&v=1)](https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=awesome!+%F0%9F%98%83&description=Optional+description&_w=1080&_h=1920) 238 | 239 | **To create templates with React.js or Vue.js use [create-flyyer-app](https://github.com/useflyyer/create-flyyer-app) 👈** 240 | 241 | --- 242 | 243 | ## Development 244 | 245 | Prepare the local environment: 246 | 247 | ```sh 248 | yarn install 249 | ``` 250 | 251 | To decode an URL for debugging purposes: 252 | 253 | ```js 254 | console.log(decodeURI(url)); 255 | // > https://cdn.flyyer.io/r/v2/tenant/deck/template.jpeg?title=Hello+world!&__v=123 256 | ``` 257 | 258 | Helpers to compare instances (ignores `__v` param and performs a shallow compare of `variables`). 259 | 260 | ```tsx 261 | import { 262 | isEqualFlyyer, 263 | isEqualFlyyerRender, 264 | isEqualFlyyerMeta, 265 | } from "@flyyer/flyyer"; 266 | import { dequal } from "dequal/lite"; 267 | 268 | const boolean = isEqualFlyyer(fio1, fio2, dequal); 269 | ``` 270 | 271 | ## Test 272 | 273 | To run tests: 274 | 275 | ```sh 276 | yarn test 277 | ``` 278 | 279 | ## FAQ 280 | 281 | ### What is the difference between Flyyer and FlyyerRender? 282 | 283 | * Flyyer uses the [rules defined on your dashboard](https://flyyer.io/dashboard/_/projects) to decide how to handle every image. It analyse your website to render a content-rich image. Let's say _"Flyyer renders images based on the content of this route"_. 284 | 285 | * FlyyerRender instead requires you to explicitly declare template and variables for the images to render, **giving you more control for customization**. Let's say _"FlyyerRender renders an image using this template and these explicit variables"_. 286 | 287 | ### Is it compatible with Nextjs, React, Vue, Express and other frameworks? 288 | 289 | This is framework-agnostic, you can use this library on any framework on any platform. 290 | 291 | ### How to configure Flyyer rules? 292 | 293 | Visit your [project rules and settings](https://flyyer.io/dashboard/_/projects) on the Flyyer Dashboard. 294 | 295 | ### What is the `__v=` thing? 296 | 297 | Most social networks caches images, we use this variable to invalidate their caches but we ignore it on our system to prevent unnecessary renders. **We strongly recommend it and its generated by default.** 298 | 299 | Pass `meta: { v: null }` to disabled it (not recommended). 300 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.4.1", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": [ 6 | "packages/*" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "license": "MIT", 5 | "author": { 6 | "name": "Patricio Lopez Juri", 7 | "email": "patricio@flyyer.io" 8 | }, 9 | "homepage": "https://github.com/useflyyer/flyyer-js", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/useflyyer/flyyer-js.git" 13 | }, 14 | "workspaces": [ 15 | "packages/*" 16 | ], 17 | "scripts": { 18 | "bootstrap": "lerna bootstrap", 19 | "build": "lerna run build", 20 | "test": "lerna run test", 21 | "lint": "eslint '*/**/*.{js,ts,tsx}'" 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "lint-staged" 26 | } 27 | }, 28 | "lint-staged": { 29 | "*.{js,ts,tsx}": [ 30 | "eslint --fix" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "@flyyer/eslint-config": "^3.0.6", 35 | "eslint": "^8.19.0", 36 | "husky": "^4.3.8", 37 | "lerna": "^5.1.8", 38 | "lint-staged": "^13.0.3", 39 | "prettier": "^2.7.1", 40 | "typescript": "^4.7.4" 41 | }, 42 | "version": "0.0.0" 43 | } 44 | -------------------------------------------------------------------------------- /packages/flyyer-lite/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flyyer.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/flyyer-lite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flyyer/flyyer-lite", 3 | "version": "3.4.1", 4 | "description": "Flyyer.io helper classes and methods to generate smart URL to render images.", 5 | "keywords": [ 6 | "flyyer", 7 | "typescript", 8 | "seo", 9 | "og:image", 10 | "og-image", 11 | "react" 12 | ], 13 | "author": "Patricio López Juri ", 14 | "module": "dist/flyyer-lite.esm.js", 15 | "license": "MIT", 16 | "main": "dist/index.js", 17 | "typings": "dist/index.d.ts", 18 | "sideEffects": false, 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/useflyyer/flyyer-js.git" 25 | }, 26 | "files": [ 27 | "dist", 28 | "src" 29 | ], 30 | "engines": { 31 | "node": ">=12" 32 | }, 33 | "scripts": { 34 | "start": "tsdx watch", 35 | "build": "tsdx build", 36 | "test": "tsdx test", 37 | "lint": "eslint '*/**/*.{js,ts,tsx}'", 38 | "prepare": "tsdx build" 39 | }, 40 | "dependencies": { 41 | "@types/qs": "^6.5.3", 42 | "qs": "^6.5.1 < 6.10" 43 | }, 44 | "devDependencies": { 45 | "@flyyer/eslint-config": "^3.0.6", 46 | "eslint": "^8.19.0", 47 | "eslint-plugin-jest": "^26.6.0", 48 | "prettier": "^2.7.1", 49 | "tsdx": "^0.14.1", 50 | "tslib": "^2.4.0", 51 | "typescript": "^4.7.4" 52 | }, 53 | "gitHead": "8d9241700c1836306991ee49fbea295087eb8d08" 54 | } 55 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/common.ts: -------------------------------------------------------------------------------- 1 | import type { FlyyerExtension } from "./ext"; 2 | import type { FlyyerMetaVariables } from "./meta"; 3 | import type { FlyyerVariables } from "./variables"; 4 | 5 | export interface FlyyerCommonParams { 6 | /** 7 | * Optional. Leave empty `""` or as `_` to always grab the latest version. 8 | */ 9 | version?: string | number | undefined | null; 10 | 11 | /** 12 | * Supported extensions are: `"jpeg" | "jpg" | "png" | "webp"` 13 | */ 14 | extension?: FlyyerExtension | undefined | null; 15 | 16 | /** 17 | * JS serializable variables. 18 | * @example 19 | * const flyyerRender = new FlyyerRender({ variables: { title: "Hello world", image: "https://example.com/logo.png" } }) 20 | * console.log(flyyerRender.href()) 21 | * // https://cdn.flyyer.io/r/v2/TENANT/DECK/TEMPLATE.jpeg?title=Hello+world&image=https%3A%2F%2Fexample.com%2Flogo.png 22 | * @example 23 | * const flyyer = new Flyyer({ variables: { title: "Hello world", image: "https://example.com/logo.png" } }) 24 | * console.log(flyyer.href()) 25 | * // https://cdn.flyyer.io/v2/flyyer-com/_/title=Hello+world&image=https%3A%2F%2Fexample.com%2Flogo.png/ 26 | */ 27 | variables?: T | undefined | null; 28 | 29 | /** 30 | * Meta variables usually have values assigned by Flyyer depending on how and where images are rendered. 31 | * 32 | * You can force these values here. 33 | * @example 34 | * const meta: FlyyerMetaVariables ={ 35 | * width: 1080, // in pixels 36 | * height: 1080, // in pixels 37 | * v: null, // disable cache-burst 38 | * id: "my-id", // analytics id 39 | * } 40 | * const flyyerRender = new FlyyerRender({ meta }); 41 | * const flyyer = new Flyyer({ meta }); 42 | * @example 43 | * `https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?__v=disabled` 44 | * `https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?_w=1080&_h=1080` 45 | * `https://cdn.flyyer.io/v2/flyyer-com/_/_/?__v=disabled` 46 | * `https://cdn.flyyer.io/v2/flyyer-com/_/_w=1080&_h=1080/marketplace` 47 | */ 48 | meta?: FlyyerMetaVariables | undefined | null; 49 | 50 | secret?: string | undefined | undefined | null; 51 | strategy?: "JWT" | "HMAC" | undefined | null; 52 | } 53 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/ext.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Supported extensions 3 | * @example 4 | */ 5 | export type FlyyerExtension = "jpeg" | "jpg" | "png" | "webp"; 6 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/flyyer.ts: -------------------------------------------------------------------------------- 1 | import type { IStringifyOptions } from "qs"; 2 | 3 | import type { FlyyerCommonParams } from "./common"; 4 | import type { FlyyerExtension } from "./ext"; 5 | import { invariant } from "./invariant"; 6 | import type { FlyyerMetaVariables } from "./meta"; 7 | import { FlyyerPath, normalizePath } from "./paths"; 8 | import { toQuery } from "./query"; 9 | import { CDN, isUndefined } from "./utils"; 10 | import { __V } from "./v"; 11 | import type { FlyyerVariables } from "./variables"; 12 | 13 | /** 14 | * This class helps you creating URLs that will render Flyyer images. 15 | * 16 | * Required is: `project`. 17 | * 18 | * Set default variables by using the `variables` object. 19 | * 20 | * Example: https://cdn.flyyer.io/v2/flyyer-com/_/_/marketplace/simple-fade 21 | * @example 22 | * const flyyer = new Flyyer({ 23 | * project: "flyyer-com", 24 | * route: "simple-fade", 25 | * }); 26 | * console.log(flyyer.href()) 27 | * // https://cdn.flyyer.io/v2/flyyer-com/_/_/marketplace/templates/simple-fade 28 | */ 29 | export interface FlyyerParams extends FlyyerCommonParams { 30 | /** 31 | * Your project's `slug`. Lowercase and no spaces. 32 | * Visit https://flyyer.io/dashboard to get this value for your project 33 | */ 34 | project: string; 35 | 36 | /** 37 | * Preferred way of declaring the default social image to use along with Flyyer. It will be available as `{{ page.image }}` and `{{ flyyer.default }}`. **Use an absolute URL**, but relative URLs are also supported. 38 | * 39 | * Alternatively you can set a custom meta-tag but it was worst performance: ``. 40 | * 41 | * Values defined here takes precedence over `flyyer:default` meta-tag. 42 | */ 43 | default?: string | undefined | null; 44 | 45 | /** 46 | * Current page path we will use in conjunction with the base URL defined in the Flyyer's Dashboard of your project. 47 | * @example 48 | * const flyyer = new Flyyer({ path: "/" }); 49 | * @example 50 | * const flyyer = new Flyyer({ path: "/about" }); 51 | * @example 52 | * const flyyer = new Flyyer({ path: "/products/1" }); 53 | * @example 54 | * const flyyer = new Flyyer({ path: ["products", "1"] }); 55 | * @example 56 | * const flyyer = new Flyyer({ path: "/page?id=1" }); 57 | */ 58 | path?: FlyyerPath; 59 | } 60 | export class Flyyer implements FlyyerParams { 61 | public project: string; 62 | public default?: string | undefined | null; 63 | public path?: FlyyerPath; 64 | public extension?: FlyyerExtension | undefined | null; 65 | public variables: T; 66 | public meta: FlyyerMetaVariables; 67 | public secret?: string | undefined | null; 68 | public strategy?: "JWT" | "HMAC" | undefined | null; 69 | 70 | public constructor(args: FlyyerParams) { 71 | invariant(args, "Flyyer constructor must not be empty. Expected object with 'project' property."); 72 | 73 | this.project = args.project; 74 | this.path = args.path; 75 | this.default = args.default; 76 | this.extension = args.extension; 77 | this.variables = args.variables || ({} as T); 78 | this.meta = args.meta || {}; 79 | this.secret = args.secret; 80 | this.strategy = args.strategy; 81 | } 82 | 83 | /** 84 | * Override this method to implement signatures. Must be synchronous (no `Promise` allowed). 85 | */ 86 | public sign( 87 | project: FlyyerParams["project"], 88 | path: string, // normalized 89 | params: string, 90 | strategy: FlyyerParams["strategy"], 91 | secret: FlyyerParams["secret"], 92 | ): string | undefined | void {} 93 | 94 | public params(extra?: any, options?: IStringifyOptions): string { 95 | const meta = this.meta; 96 | const defaults = { 97 | __v: __V(meta.v), 98 | __id: meta.id, 99 | _w: meta.width, 100 | _h: meta.height, 101 | _res: meta.resolution, 102 | _ua: meta.agent, 103 | _def: this.default, 104 | _ext: this.extension, 105 | }; 106 | return toQuery(Object.assign(defaults, this.variables, extra), options); 107 | } 108 | 109 | /** 110 | * Generate final URL you can use in your og:images. 111 | * @example 112 | * 113 | * @example 114 | * const flyyer = new Flyyer({ meta: { v: null } }); 115 | * 116 | */ 117 | public href(): string { 118 | const project = this.project; 119 | invariant(!isUndefined(project), "Missing 'project' property"); 120 | 121 | const path = normalizePath(this.path); 122 | 123 | const strategy = this.strategy; 124 | const secret = this.secret; 125 | const signature = this.sign(project, path, this.params({ __v: undefined }), strategy, secret); 126 | 127 | const params = this.params() || "_"; 128 | if (strategy && strategy.toUpperCase() === "JWT") { 129 | const __v = __V(this.meta.v); 130 | const query = toQuery({ __v }, { addQueryPrefix: true }); 131 | return `${CDN}/v2/${project}/jwt-${signature}${query}`; 132 | } else { 133 | return `${CDN}/v2/${project}/${signature || "_"}/${params}/${path}`; 134 | } 135 | } 136 | 137 | /** 138 | * Alias of `.href()` 139 | */ 140 | public toString() { 141 | return this.href(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/index.ts: -------------------------------------------------------------------------------- 1 | export { __V } from "./v"; 2 | export { toQuery } from "./query"; 3 | export { FlyyerCommonParams } from "./common"; 4 | export { FlyyerMetaVariables } from "./meta"; 5 | export { FlyyerVariables } from "./variables"; 6 | export { FlyyerExtension } from "./ext"; 7 | export { invariant } from "./invariant"; 8 | export { normalizePath, FlyyerPath } from "./paths"; 9 | export { Flyyer, FlyyerParams } from "./flyyer"; 10 | export { FlyyerRender, FlyyerRenderParams } from "./render"; 11 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/invariant.ts: -------------------------------------------------------------------------------- 1 | const isProduction: boolean = process.env.NODE_ENV === "production"; 2 | const base = "Flyyer invariant failed"; 3 | 4 | export function invariant(condition: any, message?: string): asserts condition { 5 | if (condition) return; 6 | if (isProduction) { 7 | throw new Error(base); 8 | } 9 | throw new Error(`${base}: ${message || ""}`); 10 | } 11 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/meta.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Meta variables usually have values assigned by Flyyer depending on how and where images are rendered. 3 | * 4 | * You can force these values here. 5 | * @example 6 | * const meta: FlyyerMetaVariables ={ 7 | * width: 1080, // in pixels 8 | * height: 1080, // in pixels 9 | * v: null, // disable cache-burst 10 | * id: "my-id", // analytics id 11 | * } 12 | * const flyyerRender = new FlyyerRender({ meta }); 13 | * const flyyer = new Flyyer({ meta }); 14 | * @example 15 | * `https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?__v=disabled` 16 | * `https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?_w=1080&_h=1080` 17 | * `https://cdn.flyyer.io/v2/flyyer-com/_/_/?__v=disabled` 18 | * `https://cdn.flyyer.io/v2/flyyer-com/_/_w=1080&_h=1080/marketplace` 19 | */ 20 | export interface FlyyerMetaVariables { 21 | /** 22 | * Force crawler user agent. 23 | * Converted to `_ua=` on `flyyer.href()` if set. 24 | * 25 | * Full list at https://docs.flyyer.io/docs/features/agent-detection 26 | * @example 27 | * "whatsapp" // _ua=whatsapp 28 | * "facebook" // _ua=facebook 29 | * "twitter" // _ua=twitter 30 | * "instagram" // _ua=instagram 31 | */ 32 | agent?: string | undefined | null; 33 | /* 34 | * Force language instead of using the viewer's locale. 35 | * This is useful when you have your website with international routes (eg: `example.com/de` or `de.example.com`) 36 | * Converted to `_loc=` on `flyyer.href()` if set. 37 | */ 38 | locale?: string | undefined | null; 39 | /** 40 | * Pixels (integer value). 41 | * Converted to `_w=` on `flyyer.href()` if set. 42 | * @example 43 | * 1200 // _w=1200 // default value most of the time 44 | * 1080 // _w=1080 45 | * @example 46 | * `https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?_w=1080&_h=1080` 47 | * `https://cdn.flyyer.io/v2/flyyer-com/_/_w=1080&_h=1080/jobs` 48 | */ 49 | width?: string | number | undefined | null; 50 | /** 51 | * Pixels (integer value). 52 | * Converted to `_h=` on `flyyer.href()` if set. 53 | * @example 54 | * 1200 // _h=630 // default value most of the time 55 | * 1080 // _h=1080 56 | * 1080 // _h=1920 57 | * @example 58 | * `https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?_w=1080&_h=1080` 59 | * `https://cdn.flyyer.io/v2/flyyer-com/_/_w=1080&_h=1080/jobs` 60 | */ 61 | height?: string | number | undefined | null; 62 | /** 63 | * Range from [0.0, 1.0] 64 | * Converted to `_res=` on `flyyer.href()` if set. 65 | */ 66 | resolution?: string | number | undefined | null; 67 | /** 68 | * To identify your links on the analytics report 69 | * Converted to `__id=` on `flyyer.href()` if set. 70 | */ 71 | id?: string | number | undefined | null; 72 | /** 73 | * Cache invalidator, set to `null` or empty string `""` to disable it. 74 | * Converted to `__v=` on `flyyer.href()` if set. 75 | * 76 | * **If you are using Flyyer inside your website for to render images we recommend disabling it to use browser's cache.** 77 | * @example 78 | * const flyyer = new FlyyerRender({ meta: { v: null } }); // disabled 79 | * @example 80 | * __v=null // disabled 81 | * __v="1" // constant 82 | * undefined|__ // `__v=123123` a timestamp will be used. 83 | * @example 84 | * `https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=Hello&__v=123123` // by default is a timestamp 85 | * `https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=Hello&__v=` // disabled to use browser's cache 86 | */ 87 | v?: string | number | null | undefined; 88 | } 89 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/paths.ts: -------------------------------------------------------------------------------- 1 | export type FlyyerPath = string | number | null | undefined | (string | number | null | undefined)[]; 2 | 3 | /** 4 | * Convert path or array of path parts to a string. 5 | */ 6 | export function normalizePath(path?: FlyyerPath) { 7 | return [] 8 | .concat(path as any) // force array 9 | .filter((part: any) => part || part === 0) // filter falsy values 10 | .map((part: string) => String(part).replace(/^\/+/, "").replace(/\/+$/, "")) // remove leading and trailing slashes 11 | .join("/"); // compose URL 12 | } 13 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/query.ts: -------------------------------------------------------------------------------- 1 | import type { IStringifyOptions } from "qs"; 2 | // @ts-expect-error Type 3 | import stringify from "qs/lib/stringify"; 4 | 5 | /** 6 | * Internally used to convert an object to querystring. 7 | */ 8 | export function toQuery(variables: any, options?: IStringifyOptions) { 9 | return stringify(variables, Object.assign({ addQueryPrefix: false, format: "RFC1738" }, options)); 10 | } 11 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/render.ts: -------------------------------------------------------------------------------- 1 | import type { IStringifyOptions } from "qs"; 2 | 3 | import type { FlyyerCommonParams } from "./common"; 4 | import type { FlyyerExtension } from "./ext"; 5 | import { invariant } from "./invariant"; 6 | import type { FlyyerMetaVariables } from "./meta"; 7 | import { toQuery } from "./query"; 8 | import { CDN, isUndefined } from "./utils"; 9 | import { __V } from "./v"; 10 | import type { FlyyerVariables } from "./variables"; 11 | 12 | /** 13 | * These are the parameters required (some are optional) to create an URL that will render Flyyer images. 14 | * 15 | * Required is: `tenant`, `deck`, `template`. 16 | * 17 | * Set and override the variables of the template by using the `variables` object. 18 | * 19 | * Example: https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=Thanks+for+reading+this 20 | * @example 21 | * const flyyer = new FlyyerRender({ 22 | * tenant: "flyyer", 23 | * deck: "default", 24 | * template: "main", 25 | * variables: { title: "Thanks for reading this" }, 26 | * }); 27 | * console.log(flyyer.href()) 28 | * // https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=Thanks+for+reading+this 29 | */ 30 | export interface FlyyerRenderParams extends FlyyerCommonParams { 31 | /** 32 | * Your tenant's `slug`. Lowercase and no spaces. 33 | * Visit https://flyyer.io/dashboard to get this value for your project 34 | */ 35 | tenant: string; 36 | 37 | /** 38 | * Your deck's `slug`. Lowercase and no spaces. 39 | * Visit https://flyyer.io/dashboard to get this value for your project 40 | */ 41 | deck: string; 42 | 43 | /** 44 | * Your template's `slug`. Lowercase and no spaces. 45 | * Visit https://flyyer.io/dashboard to get this value for your project 46 | */ 47 | template: string; 48 | } 49 | /** 50 | * This class helps you creating URLs that will render Flyyer images. 51 | * 52 | * Required is: `tenant`, `deck`, `template`. 53 | * 54 | * Set and override the variables of the template by using the `variables` object. 55 | * 56 | * Example: https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=Thanks+for+reading+this 57 | * @example 58 | * const flyyer = new FlyyerRender({ 59 | * tenant: "flyyer", 60 | * deck: "default", 61 | * template: "main", 62 | * variables: { title: "Thanks for reading this" }, 63 | * }); 64 | * console.log(flyyer.href()) 65 | * // https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=Thanks+for+reading+this 66 | */ 67 | export class FlyyerRender implements FlyyerRenderParams { 68 | public tenant: string; 69 | public deck: string; 70 | public template: string; 71 | public version?: string | number | undefined | null; 72 | public extension?: FlyyerExtension | undefined | null; 73 | public variables: T; 74 | public meta: FlyyerMetaVariables; 75 | public secret?: string | undefined | null; 76 | public strategy?: "JWT" | "HMAC" | undefined | null; 77 | 78 | public constructor(args: FlyyerRenderParams) { 79 | invariant(args, "FlyyerRender constructor must not be empty"); 80 | 81 | this.tenant = args.tenant; 82 | this.deck = args.deck; 83 | this.template = args.template; 84 | this.version = args.version; 85 | this.extension = args.extension; 86 | this.variables = args.variables || ({} as T); 87 | this.meta = args.meta || {}; 88 | this.secret = args.secret; 89 | this.strategy = args.strategy; 90 | } 91 | 92 | /** 93 | * Override this method to implement signatures. Must be synchronous (no `Promise` allowed). 94 | */ 95 | public sign( 96 | deck: FlyyerRenderParams["deck"], 97 | template: FlyyerRenderParams["template"], 98 | version: FlyyerRenderParams["version"], 99 | extension: FlyyerRenderParams["extension"], 100 | variables: FlyyerRenderParams["variables"], 101 | meta: NonNullable["meta"]>, 102 | strategy: FlyyerRenderParams["strategy"], 103 | secret: FlyyerRenderParams["secret"], 104 | ): string | undefined | void {} 105 | 106 | public querystring(extra?: any, options?: IStringifyOptions): string { 107 | const meta = this.meta; 108 | const defaults = { 109 | __v: __V(meta.v), 110 | __id: meta.id, 111 | _w: meta.width, 112 | _h: meta.height, 113 | _res: meta.resolution, 114 | _ua: meta.agent, 115 | _loc: meta.locale, 116 | }; 117 | return toQuery(Object.assign(defaults, this.variables, extra), options); 118 | } 119 | 120 | /** 121 | * Generate final URL you can use in your og:images. 122 | * @example 123 | * 124 | * @example 125 | * const flyyer = new FlyyerRender({ meta: { v: null } }); 126 | * 127 | */ 128 | public href(): string { 129 | const { tenant, deck, template, strategy, secret, version, extension, variables, meta } = this; 130 | invariant(!isUndefined(tenant), "Missing 'tenant' property"); 131 | invariant(!isUndefined(deck), "Missing 'deck' property"); 132 | invariant(!isUndefined(template), "Missing 'template' property"); 133 | 134 | const signature = this.sign(deck, template, version, extension, variables, meta, strategy, secret); 135 | 136 | if (strategy && strategy.toUpperCase() === "JWT") { 137 | const __v = __V(meta.v); 138 | const query = toQuery({ __jwt: signature, __v }, { addQueryPrefix: true }); 139 | return `${CDN}/r/v2/${tenant}${query}`; 140 | } else { 141 | const query = this.querystring({ __hmac: signature }, { addQueryPrefix: true }); 142 | const base = `${CDN}/r/v2/${tenant}/${deck}/${template}`; 143 | if (version && extension) { 144 | return `${base}.${version}.${extension}${query}`; 145 | } else if (version) { 146 | return `${base}.${version}${query}`; 147 | } else if (extension) { 148 | return `${base}.${extension}${query}`; 149 | } else { 150 | return `${base}${query}`; 151 | } 152 | } 153 | } 154 | 155 | /** 156 | * Alias of `.href()` 157 | */ 158 | public toString() { 159 | return this.href(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const CDN = "https://cdn.flyyer.io" as const; 2 | 3 | export function isUndefined(value: any): boolean { 4 | return typeof value === "undefined"; 5 | } 6 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/v.ts: -------------------------------------------------------------------------------- 1 | import { FlyyerMetaVariables } from "./meta"; 2 | import { isUndefined } from "./utils"; 3 | 4 | /** 5 | * Set `__v` variable for cache invalidation 6 | */ 7 | export function __V(v?: FlyyerMetaVariables["v"]) { 8 | // return isUndefined(v) ? (new Date().getTime() / 1000).toFixed(0) : v; 9 | if (isUndefined(v)) { 10 | return (new Date().getTime() / 1000).toFixed(0); 11 | } 12 | if (v === null) { 13 | return undefined; // gets removed from querystring 14 | } 15 | return v; // keep wanted constant value 16 | } 17 | -------------------------------------------------------------------------------- /packages/flyyer-lite/src/variables.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Any possible object. **Remember: keys and values must be serializable.** 3 | */ 4 | export type FlyyerVariables = 5 | | { 6 | [key: string]: any; 7 | } 8 | | Record; 9 | -------------------------------------------------------------------------------- /packages/flyyer-lite/test/flyyer.test.ts: -------------------------------------------------------------------------------- 1 | import { Flyyer } from "../src/flyyer"; 2 | 3 | describe("Flyyer", () => { 4 | it("Flyyer is instantiable", () => { 5 | const flyyer = new Flyyer({ project: "" }); 6 | expect(flyyer).toBeInstanceOf(Flyyer); 7 | }); 8 | 9 | it("raises error if missing arguments", () => { 10 | const executer = (args?: any) => new Flyyer(args).href(); 11 | 12 | expect(() => executer()).toThrow("Flyyer constructor must not be empty"); 13 | expect(() => executer({ project: "" })).not.toThrow(); 14 | }); 15 | 16 | it("without path fallbacks to root", () => { 17 | const flyyer = new Flyyer({ project: "project" }); 18 | expect(flyyer.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/$/); 19 | }); 20 | 21 | it("handles single path", () => { 22 | const flyyer = new Flyyer({ project: "project", path: "about" }); 23 | expect(flyyer.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/about$/); 24 | }); 25 | 26 | it("handles multiple paths", () => { 27 | const options = [ 28 | ["dashboard", "company"], 29 | ["/dashboard", "company"], 30 | ["/dashboard", "/company"], 31 | ["dashboard", false, "/company"], 32 | ["/dashboard/", null, "/company/"], 33 | ["dashboard///", null, undefined, false, "////company/"], 34 | ]; 35 | for (const path of options) { 36 | const flyyer = new Flyyer({ project: "project", path: path as any }); 37 | expect(flyyer.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/dashboard\/company$/); 38 | } 39 | }); 40 | 41 | it("handle numbers in path", () => { 42 | const flyyer1 = new Flyyer({ project: "project", path: ["products", 1] }); 43 | expect(flyyer1.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products\/1$/); 44 | const flyyer0 = new Flyyer({ project: "project", path: ["products", 0] }); 45 | expect(flyyer0.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products\/0$/); 46 | const flyyerInf = new Flyyer({ project: "project", path: ["products", Infinity] }); 47 | expect(flyyerInf.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products\/Infinity$/); 48 | // Ignores falsy values 49 | const flyyerNaN = new Flyyer({ project: "project", path: ["products", NaN] }); 50 | expect(flyyerNaN.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products$/); 51 | }); 52 | 53 | it("handle booleans in path", () => { 54 | const flyyerTrue = new Flyyer({ project: "project", path: ["products", true as any] }); 55 | expect(flyyerTrue.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products\/true$/); 56 | // Ignores falsy values 57 | const flyyerFalse = new Flyyer({ project: "project", path: ["products", false as any] }); 58 | expect(flyyerFalse.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products$/); 59 | }); 60 | 61 | it("handle variables such as `title`", () => { 62 | const flyyer = new Flyyer({ project: "project", path: "about", variables: { title: "hello world" } }); 63 | expect(flyyer.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)&title=hello\+world\/about$/); 64 | }); 65 | 66 | it("can disable __v cache-bursting param", () => { 67 | const flyyer0 = new Flyyer({ project: "project", meta: { v: undefined } }); 68 | expect(flyyer0.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/$/); 69 | const flyyer1 = new Flyyer({ project: "project", meta: { v: null } }); 70 | expect(flyyer1.href()).toEqual("https://cdn.flyyer.io/v2/project/_/_/"); 71 | const flyyer2 = new Flyyer({ project: "project", meta: { v: "" } }); 72 | expect(flyyer2.href()).toEqual("https://cdn.flyyer.io/v2/project/_/__v=/"); 73 | const flyyer3 = new Flyyer({ project: "project", meta: { v: false as any } }); 74 | expect(flyyer3.href()).toEqual("https://cdn.flyyer.io/v2/project/_/__v=false/"); 75 | }); 76 | 77 | it("sets 'default' image as '_def' param", () => { 78 | const flyyer0 = new Flyyer({ project: "project", path: "path", default: "/static/product/1.png" }); 79 | expect(flyyer0.href()).toMatch( 80 | /^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)&_def=%2Fstatic%2Fproduct%2F1.png\/path$/, 81 | ); 82 | const flyyer1 = new Flyyer({ project: "project", path: "path", default: "https://www.flyyer.io/logo.png" }); 83 | expect(flyyer1.href()).toMatch( 84 | /^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)&_def=https%3A%2F%2Fwww.flyyer.io%2Flogo.png\/path$/, 85 | ); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/flyyer-lite/test/query.test.ts: -------------------------------------------------------------------------------- 1 | import { toQuery } from "../src/query"; 2 | 3 | describe("toQuery", () => { 4 | it("stringifies object of primitives", () => { 5 | // @ts-expect-error will complain about duplicate identifier `b` 6 | const object = { a: "hello", b: 100, c: false, d: null, e: undefined, b: 999 }; // eslint-disable-line no-dupe-keys 7 | const str = toQuery(object); 8 | expect(str).toEqual(`a=hello&b=999&c=false&d=`); 9 | }); 10 | 11 | it("stringifies a complex object", () => { 12 | const object = { 13 | a: { aa: "bar", ab: "foo" }, 14 | b: [{ c: "foo" }, { c: "bar" }], 15 | }; 16 | const str = toQuery(object); 17 | expect(str).not.toEqual(decodeURI(str)); 18 | expect(decodeURI(str)).toEqual(`a[aa]=bar&a[ab]=foo&b[0][c]=foo&b[1][c]=bar`); 19 | }); 20 | 21 | it("encodes special characters", () => { 22 | const object = { title: "Ñandú" }; 23 | const str = toQuery(object); 24 | expect(str).toEqual(`title=%C3%91and%C3%BA`); 25 | expect(decodeURIComponent("%C3%91")).toEqual("Ñ"); 26 | expect(decodeURI(str)).toEqual(`title=Ñandú`); 27 | }); 28 | 29 | it("encodes Date", () => { 30 | const now = new Date(); 31 | const object = { timestamp: now }; 32 | const str = toQuery(object); 33 | expect(str).toEqual(`timestamp=${encodeURIComponent(now.toISOString())}`); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/flyyer-lite/test/render.test.ts: -------------------------------------------------------------------------------- 1 | import { FlyyerRender } from "../src/render"; 2 | 3 | describe("FlyyerRender", () => { 4 | it("FlyyerRender is instantiable", () => { 5 | const flyyer = new FlyyerRender({ tenant: "", deck: "", template: "" }); 6 | expect(flyyer).toBeInstanceOf(FlyyerRender); 7 | }); 8 | 9 | it("raises error if missing arguments", () => { 10 | const executer = (args?: any) => new FlyyerRender(args).href(); 11 | 12 | expect(() => executer()).toThrow("FlyyerRender constructor must not be empty"); 13 | expect(() => executer({ tenant: "" })).toThrow("Missing 'deck' property"); 14 | expect(() => executer({ tenant: "", deck: "", template: "" })).not.toThrow(); 15 | }); 16 | 17 | const DEFAULTS = { 18 | tenant: "tenant", 19 | deck: "deck", 20 | template: "template", 21 | }; 22 | 23 | it("no queryparams no '?'", () => { 24 | const flyyer = new FlyyerRender({ 25 | extension: "jpeg", 26 | ...DEFAULTS, 27 | meta: { v: null }, 28 | }); 29 | expect(flyyer.href()).toEqual("https://cdn.flyyer.io/r/v2/tenant/deck/template.jpeg"); 30 | }); 31 | 32 | it("encodes url", () => { 33 | const flyyer = new FlyyerRender({ 34 | extension: "jpeg", 35 | ...DEFAULTS, 36 | variables: { 37 | title: "Hello world!", 38 | description: "", 39 | }, 40 | }); 41 | const href = flyyer.href(); 42 | expect(href).toMatch( 43 | /^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\/deck\/template\.jpeg\?__v=(\d+)&title=Hello\+world%21&description=$/, 44 | ); 45 | }); 46 | 47 | it("encodes url and skips undefined values", () => { 48 | const flyyer = new FlyyerRender({ 49 | ...DEFAULTS, 50 | extension: "jpeg", 51 | variables: { 52 | title: "title", 53 | description: undefined, 54 | }, 55 | }); 56 | const href = flyyer.href(); 57 | expect(href).toMatch(/^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\/deck\/template\.jpeg\?__v=(\d+)&title=title$/); 58 | }); 59 | 60 | it("encodes url and convert null values to empty string", () => { 61 | const flyyer = new FlyyerRender({ 62 | ...DEFAULTS, 63 | extension: "jpeg", 64 | variables: { 65 | title: "title", 66 | description: null, 67 | }, 68 | }); 69 | const href = flyyer.href(); 70 | expect(href).toMatch( 71 | /^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\/deck\/template\.jpeg\?__v=(\d+)&title=title&description=$/, 72 | ); 73 | }); 74 | 75 | it("encodes url with meta values", () => { 76 | const flyyer = new FlyyerRender({ 77 | ...DEFAULTS, 78 | variables: { 79 | title: "title", 80 | }, 81 | meta: { 82 | agent: "whatsapp", 83 | locale: "es-CL", 84 | height: 100, 85 | width: "200", 86 | id: "dev forgot to encode", 87 | v: null, 88 | }, 89 | extension: "png", 90 | }); 91 | const href = flyyer.href(); 92 | expect(href).toMatch( 93 | /^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\/deck\/template\.png\?__id=dev\+forgot\+to\+encode&_w=200&_h=100&_ua=whatsapp&_loc=es-CL&title=title$/, 94 | ); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /packages/flyyer-lite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": false, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/flyyer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flyyer.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/flyyer/README.md: -------------------------------------------------------------------------------- 1 | # flyyer-js 2 | 3 | ![npm-version](https://badgen.net/npm/v/@flyyer/flyyer) ![downloads](https://badgen.net/npm/dt/@flyyer/flyyer) ![size](https://badgen.net/bundlephobia/minzip/@flyyer/flyyer) ![tree-shake](https://badgen.net/bundlephobia/tree-shaking/@flyyer/flyyer) 4 | 5 | Format URLs to generate social media images using Flyyer.io. 6 | 7 | **To create templates with React.js or Vue.js use [create-flyyer-app](https://github.com/useflyyer/create-flyyer-app) 👈** 8 | 9 | ![Flyyer live image](https://github.com/useflyyer/create-flyyer-app/blob/master/.github/assets/website-to-preview.png?raw=true&v=1) 10 | 11 | **This module is agnostic to any JS framework and has only one dependency: [qs](https://github.com/ljharb/qs).** 12 | 13 | ## Index 14 | 15 | - [Get started (5 minutes)](#get-started-5-minutes) 16 | - [Advanced usage](#advanced-usage) 17 | - [Flyyer Render](#flyyer-render) 18 | - [Development](#development) 19 | - [Test](#test) 20 | - [FAQ](#faq) 21 | 22 | ## Get started (5 minutes) 23 | 24 | ### 1. Install module 25 | 26 | This module supports Node.js, Browser and can be bundled with any tool such as Rollup, Webpack, etc and includes Typescript definitions. 27 | 28 | ```sh 29 | yarn add @flyyer/flyyer 30 | 31 | # or with npm: 32 | npm install --save @flyyer/flyyer 33 | ``` 34 | 35 | ### 2. Flyyer.io smart image link 36 | 37 | > Haven't registered your website yet? Go to [Flyyer.io](https://flyyer.io/get-started?ref=flyyer-js) and import your website to create a project (e.g. `website-com`). 38 | 39 | For each of your routes, create an instance. 40 | 41 | ```tsx 42 | import { Flyyer } from "@flyyer/flyyer"; 43 | 44 | const flyyer = new Flyyer({ 45 | // Your project slug 46 | project: "website-com", 47 | // Relative path 48 | path: `/path/to/product`, 49 | }); 50 | 51 | console.log(flyyer.href()); 52 | // > https://cdn.flyyer.io/v2/website-com/_/__v=1618281823/path/to/product 53 | ``` 54 | 55 | #### 2.1 Next.js 56 | 57 | Remember to dynamically get the current path for each page. If you are using [Next.js](https://nextjs.org/) you should probably do this: 58 | 59 | ```tsx 60 | import { useRouter } from 'next/router' 61 | 62 | function SEO() { 63 | const router = useRouter(); 64 | const flyyer = new Flyyer({ 65 | project: "my-project", 66 | path: router.asPath, 67 | }); 68 | // ... 69 | } 70 | ``` 71 | 72 | Check our official Next.js documentation [here](https://docs.flyyer.io/guides/javascript/nextjs?ref=flyyer-js); 73 | 74 | ### 3. Setup `` meta tags 75 | 76 | You'll get the best results doing this: 77 | 78 | ```tsx 79 | 80 | 81 | 82 | ``` 83 | 84 | ### 4. Manage rules 85 | 86 | [Login at Flyyer.io](https://flyyer.io/dashboard/_/projects/_/manage?ref=flyyer-js), select your project and go to Manage rules. Then create a rule like the following: 87 | 88 | [![Flyyer basic rule example](https://github.com/useflyyer/create-flyyer-app/blob/master/.github/assets/rule-example.png?raw=true&v=1)](https://flyyer.io/dashboard/) 89 | 90 | Voilà! **To create templates with React.js or Vue.js use [create-flyyer-app](https://github.com/useflyyer/create-flyyer-app) 👈** 91 | 92 | ## Advanced usage 93 | 94 | Advanced features include: 95 | 96 | - Custom variables: additional information for your preview that is not present in your website. [Note: if you need customization you should take a look at [Flyyer Render](#flyyer-render)] 97 | - Custom metadata: set custom width, height, resolution, and more (see example). 98 | - Signed URLs. 99 | 100 | Here you have a detailed full example for project `website-com` and path `/path/to/product`. 101 | 102 | ```tsx 103 | import { Flyyer } from "@flyyer/flyyer"; 104 | 105 | const flyyer = new Flyyer({ 106 | // Project slug, find it in your dashboard https://flyyer.io/dashboard/. 107 | project: "website-com", 108 | // The current path of your website (by default it's `/`). 109 | path: "/path/to/product", 110 | 111 | // [Optional] In case you want to provide information that is not present in your page set it here. 112 | variables: { 113 | title: "Product name", 114 | img: "https://flyyer.io/img/marketplace/flyyer-banner.png", 115 | }, 116 | // [Optional] Additional variables. 117 | meta: { 118 | id: "jeans-123", // stats identifier (e.g. product SKU), defaults to `path`. 119 | width: 1080, // force width (pixels). 120 | height: 1080, // force height (pixels). 121 | v: null, // cache-burst, to circumvent platforms' cache, default to a timestamp, null to disable. 122 | }, 123 | }); 124 | ``` 125 | 126 | > Read more about integration guides here: https://docs.flyyer.io/guides 127 | 128 | ## Flyyer Lite 129 | 130 | If you are not using Signed URLs you can opt-in for `@flyyer/flyyer-lite` which is a lightweight version because it doesn't include crypto functions. 131 | 132 | ```sh 133 | yarn add @flyyer/flyyer-lite 134 | ``` 135 | 136 | Usage is the same: 137 | 138 | ```tsx 139 | import { Flyyer } from "@flyyer/flyyer-lite"; 140 | // ... 141 | ``` 142 | 143 | ## FlyyerRender 144 | 145 | * Flyyer uses the [rules defined on your dashboard](https://flyyer.io/dashboard/_/projects) to decide how to handle every image. It analyse your website to render a content-rich image. Let's say _"Flyyer renders images based on the content of this route"_. 146 | 147 | * FlyyerRender instead requires you to explicitly declare template and variables for the images to render, **giving you more control for customization**. Let's say _"FlyyerRender renders an image using this template and these explicit variables"_. 148 | 149 | ```tsx 150 | import { FlyyerRender } from "@flyyer/flyyer"; 151 | const flyyer = new FlyyerRender({ 152 | tenant: "flyyer", 153 | deck: "default", 154 | template: "main", 155 | variables: { title: "try changing this" }, 156 | }); 157 | const url = flyyer.href() 158 | // https://cdn.flyyer.io/v2/flyyer/default/main.jpeg?title=try+changing+this 159 | ``` 160 | 161 | [![Resultant flyyer live image](https://github.com/useflyyer/create-flyyer-app/blob/master/.github/assets/result-1.png?raw=true&v=1)](https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=try+changing+this) 162 | 163 | After installing this module you can format URLs. Here is an example with React.js, but note this can be used with any framework: 164 | 165 | ```tsx 166 | import React from "react"; 167 | import { FlyyerRender } from "@flyyer/flyyer"; 168 | 169 | function Head() { 170 | const flyyer = new FlyyerRender({ 171 | tenant: "tenant", 172 | deck: "deck", 173 | template: "template", 174 | variables: { 175 | title: "Hello world!", 176 | image: "https://yoursite.com/image/products/1.png", 177 | }, 178 | }); 179 | const url = flyyer.href(); 180 | 181 | return ( 182 | 183 | 184 | 185 | 186 | 187 | ); 188 | } 189 | ``` 190 | 191 | Variables can be complex arrays and objects. 192 | 193 | ```js 194 | const flyyer = new FlyyerRender({ 195 | // ... 196 | variables: { 197 | items: [ 198 | { text: "Oranges", count: 12 }, 199 | { text: "Apples", count: 14 }, 200 | ], 201 | }, 202 | meta { 203 | id: "slug-or-id", // To identify the resource in our analytics report 204 | } 205 | }); 206 | ``` 207 | 208 | **IMPORTANT: variables must be serializable.** 209 | 210 | You can **set image dimensions**, note if your are planing to use this as `` you should disable cache-bursting. 211 | 212 | ```tsx 213 | const flyyer = new FlyyerRender({ 214 | tenant: "tenant", 215 | deck: "default", 216 | template: "main", 217 | variables: { 218 | title: "Awesome 😃", 219 | description: "Optional description", 220 | }, 221 | meta: { 222 | v: null, // prevent cache-bursting in browsers 223 | width: 1080, // in pixels 224 | height: 1920, // in pixels 225 | } 226 | }); 227 | 228 | 229 | ``` 230 | 231 | [![Resultant flyyer live image](https://github.com/useflyyer/create-flyyer-app/blob/master/.github/assets/result-2.png?raw=true&v=1)](https://cdn.flyyer.io/r/v2/flyyer/default/main.jpeg?title=awesome!+%F0%9F%98%83&description=Optional+description&_w=1080&_h=1920) 232 | 233 | **To create templates with React.js or Vue.js use [create-flyyer-app](https://github.com/useflyyer/create-flyyer-app) 👈** 234 | 235 | --- 236 | 237 | ## Development 238 | 239 | Prepare the local environment: 240 | 241 | ```sh 242 | yarn install 243 | ``` 244 | 245 | To decode an URL for debugging purposes: 246 | 247 | ```js 248 | console.log(decodeURI(url)); 249 | // > https://cdn.flyyer.io/v2/tenant/deck/template.jpeg?title=Hello+world!&__v=123 250 | ``` 251 | 252 | Helpers to compare instances (ignores `__v` param and performs a shallow compare of `variables`). 253 | 254 | ```tsx 255 | import { 256 | isEqualFlyyer, 257 | isEqualFlyyerRender, 258 | isEqualFlyyerMeta, 259 | } from "@flyyer/flyyer"; 260 | 261 | const boolean = isEqualFlyyer(fio1, fio2); 262 | ``` 263 | 264 | ## Test 265 | 266 | To run tests: 267 | 268 | ```sh 269 | yarn test 270 | ``` 271 | 272 | ## FAQ 273 | 274 | ### What is the difference between Flyyer and FlyyerRender? 275 | 276 | * Flyyer uses the [rules defined on your dashboard](https://flyyer.io/dashboard/_/projects) to decide how to handle every image. It analyse your website to render a content-rich image. Let's say _"Flyyer renders images based on the content of this route"_. 277 | 278 | * FlyyerRender instead requires you to explicitly declare template and variables for the images to render, **giving you more control for customization**. Let's say _"FlyyerRender renders an image using this template and these explicit variables"_. 279 | 280 | ### Is it compatible with Nextjs, React, Vue, Express and other frameworks? 281 | 282 | This is framework-agnostic, you can use this library on any framework on any platform. 283 | 284 | ### How to configure Flyyer rules? 285 | 286 | Visit your [project rules and settings](https://flyyer.io/dashboard/_/projects) on the Flyyer Dashboard. 287 | 288 | ### What is the `__v=` thing? 289 | 290 | Most social networks caches images, we use this variable to invalidate their caches but we ignore it on our system to prevent unnecessary renders. **We strongly recommend it and its generated by default.** 291 | 292 | Pass `meta: { v: null }` to disabled it (not recommended). 293 | -------------------------------------------------------------------------------- /packages/flyyer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flyyer/flyyer", 3 | "version": "3.4.1", 4 | "description": "Flyyer.io helper classes and methods to generate smart URL to render images.", 5 | "keywords": [ 6 | "flyyer", 7 | "typescript", 8 | "seo", 9 | "og:image", 10 | "og-image", 11 | "react" 12 | ], 13 | "author": "Patricio López Juri ", 14 | "module": "dist/flyyer.esm.js", 15 | "license": "MIT", 16 | "main": "dist/index.js", 17 | "typings": "dist/index.d.ts", 18 | "sideEffects": false, 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/useflyyer/flyyer-js.git" 25 | }, 26 | "files": [ 27 | "dist", 28 | "src" 29 | ], 30 | "engines": { 31 | "node": ">=12" 32 | }, 33 | "scripts": { 34 | "start": "tsdx watch", 35 | "build": "tsdx build", 36 | "test": "tsdx test", 37 | "lint": "eslint '*/**/*.{js,ts,tsx}'", 38 | "prepare": "tsdx build" 39 | }, 40 | "dependencies": { 41 | "@flyyer/flyyer-lite": "^3.4.1" 42 | }, 43 | "devDependencies": { 44 | "@flyyer/eslint-config": "^3.0.6", 45 | "dequal": "^2.0.3", 46 | "eslint": "^8.19.0", 47 | "eslint-plugin-jest": "^26.6.0", 48 | "prettier": "^2.7.1", 49 | "tsdx": "^0.14.1", 50 | "tslib": "^2.4.0", 51 | "typescript": "^4.7.4" 52 | }, 53 | "gitHead": "8d9241700c1836306991ee49fbea295087eb8d08" 54 | } 55 | -------------------------------------------------------------------------------- /packages/flyyer/src/compare.ts: -------------------------------------------------------------------------------- 1 | import { Flyyer, FlyyerMetaVariables, FlyyerRender, FlyyerVariables, normalizePath } from "@flyyer/flyyer-lite"; 2 | 3 | /** 4 | * Compare two `FlyyerMetaVariables` object. Ignores `__v` param. 5 | */ 6 | export function isEqualFlyyerMeta(ameta: FlyyerMetaVariables, bmeta: FlyyerMetaVariables): boolean { 7 | const metas = ["width", "height", "agent", "id", "locale", "resolution"] as const; 8 | for (const meta of metas) { 9 | if (ameta[meta] !== bmeta[meta]) { 10 | return false; 11 | } 12 | } 13 | return true; 14 | } 15 | 16 | /** 17 | * Compare two Flyyer instances. Ignores `__v` param. Required a variable compare function as third argument. 18 | * @example 19 | * import { dequal } from "dequal/lite"; 20 | * const isEqual = isEqualFlyyer(f1, f2, dequal); 21 | */ 22 | export function isEqualFlyyer( 23 | a: Flyyer, 24 | b: Flyyer, 25 | variablesCompareFn: (a: FlyyerVariables, b: FlyyerVariables) => boolean, 26 | ): boolean { 27 | if (a === b) return true; 28 | if ( 29 | a.project !== b.project || 30 | a.default !== b.default || 31 | a.extension !== b.extension || 32 | a.strategy !== b.strategy || 33 | a.secret !== b.secret || 34 | normalizePath(a.path) !== normalizePath(b.path) 35 | ) { 36 | return false; 37 | } 38 | return isEqualFlyyerMeta(a.meta, b.meta) && variablesCompareFn(a.variables, b.variables); 39 | } 40 | 41 | /** 42 | * Compare two FlyyerRender instances. Ignores `__v` param. 43 | */ 44 | export function isEqualFlyyerRender( 45 | a: FlyyerRender, 46 | b: FlyyerRender, 47 | variablesCompareFn: (a: FlyyerVariables, b: FlyyerVariables) => boolean, 48 | ): boolean { 49 | if (a === b) return true; 50 | const attrs = ["tenant", "deck", "template", "version", "extension", "strategy", "secret"] as const; 51 | for (const attr of attrs) { 52 | if (a[attr] !== b[attr]) { 53 | return false; 54 | } 55 | } 56 | return isEqualFlyyerMeta(a.meta, b.meta) && variablesCompareFn(a.variables, b.variables); 57 | } 58 | -------------------------------------------------------------------------------- /packages/flyyer/src/crypto/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 LIN Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | [The MIT License (MIT)](http://opensource.org/licenses/MIT) 24 | 25 | Copyright (c) 2009-2013 Jeff Mott 26 | Copyright (c) 2013-2016 Evan Vosberg 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy 29 | of this software and associated documentation files (the "Software"), to deal 30 | in the Software without restriction, including without limitation the rights 31 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | copies of the Software, and to permit persons to whom the Software is 33 | furnished to do so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in 36 | all copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 44 | THE SOFTWARE. 45 | 46 | (c) 2009-2013 by Jeff Mott. All rights reserved. 47 | 48 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 49 | 50 | Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer. 51 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation or other materials provided with the distribution. 52 | Neither the name CryptoJS nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 53 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS," AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 54 | -------------------------------------------------------------------------------- /packages/flyyer/src/crypto/core.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | /** 4 | * Base class for inheritance. 5 | */ 6 | export class Base { 7 | /** 8 | * Extends this object and runs the init method. 9 | * Arguments to create() will be passed to init(). 10 | * 11 | * @return {Object} The new object. 12 | * 13 | * @static 14 | * 15 | * @example 16 | * 17 | * var instance = MyType.create(); 18 | */ 19 | static create(...args) { 20 | return new this(...args); 21 | } 22 | 23 | /** 24 | * Copies properties into this object. 25 | * 26 | * @param {Object} properties The properties to mix in. 27 | * 28 | * @example 29 | * 30 | * MyType.mixIn({ 31 | * field: 'value' 32 | * }); 33 | */ 34 | mixIn(properties) { 35 | return Object.assign(this, properties); 36 | } 37 | 38 | /** 39 | * Creates a copy of this object. 40 | * 41 | * @return {Object} The clone. 42 | * 43 | * @example 44 | * 45 | * var clone = instance.clone(); 46 | */ 47 | clone() { 48 | const clone = new this.constructor(); 49 | Object.assign(clone, this); 50 | return clone; 51 | } 52 | } 53 | 54 | /** 55 | * An array of 32-bit words. 56 | * 57 | * @property {Array} words The array of 32-bit words. 58 | * @property {number} sigBytes The number of significant bytes in this word array. 59 | */ 60 | export class WordArray extends Base { 61 | /** 62 | * Initializes a newly created word array. 63 | * 64 | * @param {Array} words (Optional) An array of 32-bit words. 65 | * @param {number} sigBytes (Optional) The number of significant bytes in the words. 66 | * 67 | * @example 68 | * 69 | * var wordArray = CryptoJS.lib.WordArray.create(); 70 | * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]); 71 | * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6); 72 | */ 73 | constructor(words = [], sigBytes = words.length * 4) { 74 | super(); 75 | 76 | let typedArray = words; 77 | // Convert buffers to uint8 78 | if (typedArray instanceof ArrayBuffer) { 79 | typedArray = new Uint8Array(typedArray); 80 | } 81 | 82 | // Convert other array views to uint8 83 | if ( 84 | typedArray instanceof Int8Array || 85 | typedArray instanceof Uint8ClampedArray || 86 | typedArray instanceof Int16Array || 87 | typedArray instanceof Uint16Array || 88 | typedArray instanceof Int32Array || 89 | typedArray instanceof Uint32Array || 90 | typedArray instanceof Float32Array || 91 | typedArray instanceof Float64Array 92 | ) { 93 | typedArray = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength); 94 | } 95 | 96 | // Handle Uint8Array 97 | if (typedArray instanceof Uint8Array) { 98 | // Shortcut 99 | const typedArrayByteLength = typedArray.byteLength; 100 | 101 | // Extract bytes 102 | const _words = []; 103 | for (let i = 0; i < typedArrayByteLength; i += 1) { 104 | _words[i >>> 2] |= typedArray[i] << (24 - (i % 4) * 8); 105 | } 106 | 107 | // Initialize this word array 108 | this.words = _words; 109 | this.sigBytes = typedArrayByteLength; 110 | } else { 111 | // Else call normal init 112 | this.words = words; 113 | this.sigBytes = sigBytes; 114 | } 115 | } 116 | 117 | /** 118 | * Creates a word array filled with random bytes. 119 | * 120 | * @param {number} nBytes The number of random bytes to generate. 121 | * 122 | * @return {WordArray} The random word array. 123 | * 124 | * @static 125 | * 126 | * @example 127 | * 128 | * var wordArray = CryptoJS.lib.WordArray.random(16); 129 | */ 130 | static random(nBytes) { 131 | const words = []; 132 | 133 | const r = (m_w) => { 134 | let _m_w = m_w; 135 | let _m_z = 0x3ade68b1; 136 | const mask = 0xffffffff; 137 | 138 | return () => { 139 | _m_z = (0x9069 * (_m_z & 0xffff) + (_m_z >> 0x10)) & mask; 140 | _m_w = (0x4650 * (_m_w & 0xffff) + (_m_w >> 0x10)) & mask; 141 | let result = ((_m_z << 0x10) + _m_w) & mask; 142 | result /= 0x100000000; 143 | result += 0.5; 144 | return result * (Math.random() > 0.5 ? 1 : -1); 145 | }; 146 | }; 147 | 148 | for (let i = 0, rcache; i < nBytes; i += 4) { 149 | const _r = r((rcache || Math.random()) * 0x100000000); 150 | 151 | rcache = _r() * 0x3ade67b7; 152 | words.push((_r() * 0x100000000) | 0); 153 | } 154 | 155 | return new WordArray(words, nBytes); 156 | } 157 | 158 | /** 159 | * Converts this word array to a string. 160 | * 161 | * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex 162 | * 163 | * @return {string} The stringified word array. 164 | * 165 | * @example 166 | * 167 | * var string = wordArray + ''; 168 | * var string = wordArray.toString(); 169 | * var string = wordArray.toString(CryptoJS.enc.Utf8); 170 | */ 171 | toString(encoder = Hex) { 172 | return encoder.stringify(this); 173 | } 174 | 175 | /** 176 | * Concatenates a word array to this word array. 177 | * 178 | * @param {WordArray} wordArray The word array to append. 179 | * 180 | * @return {WordArray} This word array. 181 | * 182 | * @example 183 | * 184 | * wordArray1.concat(wordArray2); 185 | */ 186 | concat(wordArray) { 187 | // Shortcuts 188 | const thisWords = this.words; 189 | const thatWords = wordArray.words; 190 | const thisSigBytes = this.sigBytes; 191 | const thatSigBytes = wordArray.sigBytes; 192 | 193 | // Clamp excess bits 194 | this.clamp(); 195 | 196 | // Concat 197 | if (thisSigBytes % 4) { 198 | // Copy one byte at a time 199 | for (let i = 0; i < thatSigBytes; i += 1) { 200 | const thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; 201 | thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); 202 | } 203 | } else { 204 | // Copy one word at a time 205 | for (let i = 0; i < thatSigBytes; i += 4) { 206 | thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2]; 207 | } 208 | } 209 | this.sigBytes += thatSigBytes; 210 | 211 | // Chainable 212 | return this; 213 | } 214 | 215 | /** 216 | * Removes insignificant bits. 217 | * 218 | * @example 219 | * 220 | * wordArray.clamp(); 221 | */ 222 | clamp() { 223 | // Shortcuts 224 | const { words, sigBytes } = this; 225 | 226 | // Clamp 227 | words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); 228 | words.length = Math.ceil(sigBytes / 4); 229 | } 230 | 231 | /** 232 | * Creates a copy of this word array. 233 | * 234 | * @return {WordArray} The clone. 235 | * 236 | * @example 237 | * 238 | * var clone = wordArray.clone(); 239 | */ 240 | clone() { 241 | const clone = super.clone.call(this); 242 | clone.words = this.words.slice(0); 243 | 244 | return clone; 245 | } 246 | } 247 | 248 | /** 249 | * Hex encoding strategy. 250 | */ 251 | export const Hex = { 252 | /** 253 | * Converts a word array to a hex string. 254 | * 255 | * @param {WordArray} wordArray The word array. 256 | * 257 | * @return {string} The hex string. 258 | * 259 | * @static 260 | * 261 | * @example 262 | * 263 | * var hexString = CryptoJS.enc.Hex.stringify(wordArray); 264 | */ 265 | stringify(wordArray) { 266 | // Shortcuts 267 | const { words, sigBytes } = wordArray; 268 | 269 | // Convert 270 | const hexChars = []; 271 | for (let i = 0; i < sigBytes; i += 1) { 272 | const bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; 273 | hexChars.push((bite >>> 4).toString(16)); 274 | hexChars.push((bite & 0x0f).toString(16)); 275 | } 276 | 277 | return hexChars.join(""); 278 | }, 279 | 280 | /** 281 | * Converts a hex string to a word array. 282 | * 283 | * @param {string} hexStr The hex string. 284 | * 285 | * @return {WordArray} The word array. 286 | * 287 | * @static 288 | * 289 | * @example 290 | * 291 | * var wordArray = CryptoJS.enc.Hex.parse(hexString); 292 | */ 293 | parse(hexStr) { 294 | // Shortcut 295 | const hexStrLength = hexStr.length; 296 | 297 | // Convert 298 | const words = []; 299 | for (let i = 0; i < hexStrLength; i += 2) { 300 | words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4); 301 | } 302 | 303 | return new WordArray(words, hexStrLength / 2); 304 | }, 305 | }; 306 | 307 | /** 308 | * Latin1 encoding strategy. 309 | */ 310 | export const Latin1 = { 311 | /** 312 | * Converts a word array to a Latin1 string. 313 | * 314 | * @param {WordArray} wordArray The word array. 315 | * 316 | * @return {string} The Latin1 string. 317 | * 318 | * @static 319 | * 320 | * @example 321 | * 322 | * var latin1String = CryptoJS.enc.Latin1.stringify(wordArray); 323 | */ 324 | stringify(wordArray) { 325 | // Shortcuts 326 | const { words, sigBytes } = wordArray; 327 | 328 | // Convert 329 | const latin1Chars = []; 330 | for (let i = 0; i < sigBytes; i += 1) { 331 | const bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; 332 | latin1Chars.push(String.fromCharCode(bite)); 333 | } 334 | 335 | return latin1Chars.join(""); 336 | }, 337 | 338 | /** 339 | * Converts a Latin1 string to a word array. 340 | * 341 | * @param {string} latin1Str The Latin1 string. 342 | * 343 | * @return {WordArray} The word array. 344 | * 345 | * @static 346 | * 347 | * @example 348 | * 349 | * var wordArray = CryptoJS.enc.Latin1.parse(latin1String); 350 | */ 351 | parse(latin1Str) { 352 | // Shortcut 353 | const latin1StrLength = latin1Str.length; 354 | 355 | // Convert 356 | const words = []; 357 | for (let i = 0; i < latin1StrLength; i += 1) { 358 | words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8); 359 | } 360 | 361 | return new WordArray(words, latin1StrLength); 362 | }, 363 | }; 364 | 365 | /** 366 | * UTF-8 encoding strategy. 367 | */ 368 | export const Utf8 = { 369 | /** 370 | * Converts a word array to a UTF-8 string. 371 | * 372 | * @param {WordArray} wordArray The word array. 373 | * 374 | * @return {string} The UTF-8 string. 375 | * 376 | * @static 377 | * 378 | * @example 379 | * 380 | * var utf8String = CryptoJS.enc.Utf8.stringify(wordArray); 381 | */ 382 | stringify(wordArray) { 383 | try { 384 | return decodeURIComponent(escape(Latin1.stringify(wordArray))); 385 | } catch (e) { 386 | throw new Error("Malformed UTF-8 data"); 387 | } 388 | }, 389 | 390 | /** 391 | * Converts a UTF-8 string to a word array. 392 | * 393 | * @param {string} utf8Str The UTF-8 string. 394 | * 395 | * @return {WordArray} The word array. 396 | * 397 | * @static 398 | * 399 | * @example 400 | * 401 | * var wordArray = CryptoJS.enc.Utf8.parse(utf8String); 402 | */ 403 | parse(utf8Str) { 404 | return Latin1.parse(unescape(encodeURIComponent(utf8Str))); 405 | }, 406 | }; 407 | 408 | /** 409 | * Abstract buffered block algorithm template. 410 | * 411 | * The property blockSize must be implemented in a concrete subtype. 412 | * 413 | * @property {number} _minBufferSize 414 | * 415 | * The number of blocks that should be kept unprocessed in the buffer. Default: 0 416 | */ 417 | export class BufferedBlockAlgorithm extends Base { 418 | constructor() { 419 | super(); 420 | this._minBufferSize = 0; 421 | } 422 | 423 | /** 424 | * Resets this block algorithm's data buffer to its initial state. 425 | * 426 | * @example 427 | * 428 | * bufferedBlockAlgorithm.reset(); 429 | */ 430 | reset() { 431 | // Initial values 432 | this._data = new WordArray(); 433 | this._nDataBytes = 0; 434 | } 435 | 436 | /** 437 | * Adds new data to this block algorithm's buffer. 438 | * 439 | * @param {WordArray|string} data 440 | * 441 | * The data to append. Strings are converted to a WordArray using UTF-8. 442 | * 443 | * @example 444 | * 445 | * bufferedBlockAlgorithm._append('data'); 446 | * bufferedBlockAlgorithm._append(wordArray); 447 | */ 448 | _append(data) { 449 | let m_data = data; 450 | 451 | // Convert string to WordArray, else assume WordArray already 452 | if (typeof m_data === "string") { 453 | m_data = Utf8.parse(m_data); 454 | } 455 | 456 | // Append 457 | this._data.concat(m_data); 458 | this._nDataBytes += m_data.sigBytes; 459 | } 460 | 461 | /** 462 | * Processes available data blocks. 463 | * 464 | * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. 465 | * 466 | * @param {boolean} doFlush Whether all blocks and partial blocks should be processed. 467 | * 468 | * @return {WordArray} The processed data. 469 | * 470 | * @example 471 | * 472 | * var processedData = bufferedBlockAlgorithm._process(); 473 | * var processedData = bufferedBlockAlgorithm._process(!!'flush'); 474 | */ 475 | _process(doFlush) { 476 | let processedWords; 477 | 478 | // Shortcuts 479 | const { _data: data, blockSize } = this; 480 | const dataWords = data.words; 481 | const dataSigBytes = data.sigBytes; 482 | const blockSizeBytes = blockSize * 4; 483 | 484 | // Count blocks ready 485 | let nBlocksReady = dataSigBytes / blockSizeBytes; 486 | if (doFlush) { 487 | // Round up to include partial blocks 488 | nBlocksReady = Math.ceil(nBlocksReady); 489 | } else { 490 | // Round down to include only full blocks, 491 | // less the number of blocks that must remain in the buffer 492 | nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0); 493 | } 494 | 495 | // Count words ready 496 | const nWordsReady = nBlocksReady * blockSize; 497 | 498 | // Count bytes ready 499 | const nBytesReady = Math.min(nWordsReady * 4, dataSigBytes); 500 | 501 | // Process blocks 502 | if (nWordsReady) { 503 | for (let offset = 0; offset < nWordsReady; offset += blockSize) { 504 | // Perform concrete-algorithm logic 505 | this._doProcessBlock(dataWords, offset); 506 | } 507 | 508 | // Remove processed words 509 | processedWords = dataWords.splice(0, nWordsReady); 510 | data.sigBytes -= nBytesReady; 511 | } 512 | 513 | // Return processed words 514 | return new WordArray(processedWords, nBytesReady); 515 | } 516 | 517 | /** 518 | * Creates a copy of this object. 519 | * 520 | * @return {Object} The clone. 521 | * 522 | * @example 523 | * 524 | * var clone = bufferedBlockAlgorithm.clone(); 525 | */ 526 | clone() { 527 | const clone = super.clone.call(this); 528 | clone._data = this._data.clone(); 529 | 530 | return clone; 531 | } 532 | } 533 | 534 | /** 535 | * Abstract hasher template. 536 | * 537 | * @property {number} blockSize 538 | * 539 | * The number of 32-bit words this hasher operates on. Default: 16 (512 bits) 540 | */ 541 | export class Hasher extends BufferedBlockAlgorithm { 542 | constructor(cfg) { 543 | super(); 544 | 545 | this.blockSize = 512 / 32; 546 | 547 | /** 548 | * Configuration options. 549 | */ 550 | this.cfg = Object.assign(new Base(), cfg); 551 | 552 | // Set initial values 553 | this.reset(); 554 | } 555 | 556 | /** 557 | * Creates a shortcut function to a hasher's object interface. 558 | * 559 | * @param {Hasher} SubHasher The hasher to create a helper for. 560 | * 561 | * @return {Function} The shortcut function. 562 | * 563 | * @static 564 | * 565 | * @example 566 | * 567 | * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); 568 | */ 569 | static _createHelper(SubHasher) { 570 | return (message, cfg) => new SubHasher(cfg).finalize(message); 571 | } 572 | 573 | /** 574 | * Creates a shortcut function to the HMAC's object interface. 575 | * 576 | * @param {Hasher} SubHasher The hasher to use in this HMAC helper. 577 | * 578 | * @return {Function} The shortcut function. 579 | * 580 | * @static 581 | * 582 | * @example 583 | * 584 | * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); 585 | */ 586 | static _createHmacHelper(SubHasher) { 587 | return (message, key) => new HMAC(SubHasher, key).finalize(message); 588 | } 589 | 590 | /** 591 | * Resets this hasher to its initial state. 592 | * 593 | * @example 594 | * 595 | * hasher.reset(); 596 | */ 597 | reset() { 598 | // Reset data buffer 599 | super.reset.call(this); 600 | 601 | // Perform concrete-hasher logic 602 | this._doReset(); 603 | } 604 | 605 | /** 606 | * Updates this hasher with a message. 607 | * 608 | * @param {WordArray|string} messageUpdate The message to append. 609 | * 610 | * @return {Hasher} This hasher. 611 | * 612 | * @example 613 | * 614 | * hasher.update('message'); 615 | * hasher.update(wordArray); 616 | */ 617 | update(messageUpdate) { 618 | // Append 619 | this._append(messageUpdate); 620 | 621 | // Update the hash 622 | this._process(); 623 | 624 | // Chainable 625 | return this; 626 | } 627 | 628 | /** 629 | * Finalizes the hash computation. 630 | * Note that the finalize operation is effectively a destructive, read-once operation. 631 | * 632 | * @param {WordArray|string} messageUpdate (Optional) A final message update. 633 | * 634 | * @return {WordArray} The hash. 635 | * 636 | * @example 637 | * 638 | * var hash = hasher.finalize(); 639 | * var hash = hasher.finalize('message'); 640 | * var hash = hasher.finalize(wordArray); 641 | */ 642 | finalize(messageUpdate) { 643 | // Final message update 644 | if (messageUpdate) { 645 | this._append(messageUpdate); 646 | } 647 | 648 | // Perform concrete-hasher logic 649 | const hash = this._doFinalize(); 650 | 651 | return hash; 652 | } 653 | } 654 | 655 | /** 656 | * HMAC algorithm. 657 | */ 658 | export class HMAC extends Base { 659 | /** 660 | * Initializes a newly created HMAC. 661 | * 662 | * @param {Hasher} SubHasher The hash algorithm to use. 663 | * @param {WordArray|string} key The secret key. 664 | * 665 | * @example 666 | * 667 | * var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key); 668 | */ 669 | constructor(SubHasher, key) { 670 | super(); 671 | 672 | const hasher = new SubHasher(); 673 | this._hasher = hasher; 674 | 675 | // Convert string to WordArray, else assume WordArray already 676 | let _key = key; 677 | if (typeof _key === "string") { 678 | _key = Utf8.parse(_key); 679 | } 680 | 681 | // Shortcuts 682 | const hasherBlockSize = hasher.blockSize; 683 | const hasherBlockSizeBytes = hasherBlockSize * 4; 684 | 685 | // Allow arbitrary length keys 686 | if (_key.sigBytes > hasherBlockSizeBytes) { 687 | _key = hasher.finalize(key); 688 | } 689 | 690 | // Clamp excess bits 691 | _key.clamp(); 692 | 693 | // Clone key for inner and outer pads 694 | const oKey = _key.clone(); 695 | this._oKey = oKey; 696 | const iKey = _key.clone(); 697 | this._iKey = iKey; 698 | 699 | // Shortcuts 700 | const oKeyWords = oKey.words; 701 | const iKeyWords = iKey.words; 702 | 703 | // XOR keys with pad constants 704 | for (let i = 0; i < hasherBlockSize; i += 1) { 705 | oKeyWords[i] ^= 0x5c5c5c5c; 706 | iKeyWords[i] ^= 0x36363636; 707 | } 708 | oKey.sigBytes = hasherBlockSizeBytes; 709 | iKey.sigBytes = hasherBlockSizeBytes; 710 | 711 | // Set initial values 712 | this.reset(); 713 | } 714 | 715 | /** 716 | * Resets this HMAC to its initial state. 717 | * 718 | * @example 719 | * 720 | * hmacHasher.reset(); 721 | */ 722 | reset() { 723 | // Shortcut 724 | const hasher = this._hasher; 725 | 726 | // Reset 727 | hasher.reset(); 728 | hasher.update(this._iKey); 729 | } 730 | 731 | /** 732 | * Updates this HMAC with a message. 733 | * 734 | * @param {WordArray|string} messageUpdate The message to append. 735 | * 736 | * @return {HMAC} This HMAC instance. 737 | * 738 | * @example 739 | * 740 | * hmacHasher.update('message'); 741 | * hmacHasher.update(wordArray); 742 | */ 743 | update(messageUpdate) { 744 | this._hasher.update(messageUpdate); 745 | 746 | // Chainable 747 | return this; 748 | } 749 | 750 | /** 751 | * Finalizes the HMAC computation. 752 | * Note that the finalize operation is effectively a destructive, read-once operation. 753 | * 754 | * @param {WordArray|string} messageUpdate (Optional) A final message update. 755 | * 756 | * @return {WordArray} The HMAC. 757 | * 758 | * @example 759 | * 760 | * var hmac = hmacHasher.finalize(); 761 | * var hmac = hmacHasher.finalize('message'); 762 | * var hmac = hmacHasher.finalize(wordArray); 763 | */ 764 | finalize(messageUpdate) { 765 | // Shortcut 766 | const hasher = this._hasher; 767 | 768 | // Compute HMAC 769 | const innerHash = hasher.finalize(messageUpdate); 770 | hasher.reset(); 771 | const hmac = hasher.finalize(this._oKey.clone().concat(innerHash)); 772 | 773 | return hmac; 774 | } 775 | } 776 | -------------------------------------------------------------------------------- /packages/flyyer/src/crypto/enc-base64.js: -------------------------------------------------------------------------------- 1 | import { WordArray } from "./core"; 2 | 3 | const parseLoop = (base64Str, base64StrLength, reverseMap) => { 4 | const words = []; 5 | let nBytes = 0; 6 | for (let i = 0; i < base64StrLength; i += 1) { 7 | if (i % 4) { 8 | const bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2); 9 | const bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2); 10 | const bitsCombined = bits1 | bits2; 11 | words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8); 12 | nBytes += 1; 13 | } 14 | } 15 | return WordArray.create(words, nBytes); 16 | }; 17 | 18 | /** 19 | * Base64 encoding strategy. 20 | */ 21 | export const Base64 = { 22 | /** 23 | * Converts a word array to a Base64 string. 24 | * 25 | * @param {WordArray} wordArray The word array. 26 | * 27 | * @return {string} The Base64 string. 28 | * 29 | * @static 30 | * 31 | * @example 32 | * 33 | * const base64String = CryptoJS.enc.Base64.stringify(wordArray); 34 | */ 35 | stringify(wordArray) { 36 | // Shortcuts 37 | const { words, sigBytes } = wordArray; 38 | const map = this._map; 39 | 40 | // Clamp excess bits 41 | wordArray.clamp(); 42 | 43 | // Convert 44 | const base64Chars = []; 45 | for (let i = 0; i < sigBytes; i += 3) { 46 | const byte1 = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; 47 | const byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff; 48 | const byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff; 49 | 50 | const triplet = (byte1 << 16) | (byte2 << 8) | byte3; 51 | 52 | for (let j = 0; j < 4 && i + j * 0.75 < sigBytes; j += 1) { 53 | base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f)); 54 | } 55 | } 56 | 57 | // Add padding 58 | const paddingChar = map.charAt(64); 59 | if (paddingChar) { 60 | while (base64Chars.length % 4) { 61 | base64Chars.push(paddingChar); 62 | } 63 | } 64 | 65 | return base64Chars.join(""); 66 | }, 67 | 68 | /** 69 | * Converts a Base64 string to a word array. 70 | * 71 | * @param {string} base64Str The Base64 string. 72 | * 73 | * @return {WordArray} The word array. 74 | * 75 | * @static 76 | * 77 | * @example 78 | * 79 | * const wordArray = CryptoJS.enc.Base64.parse(base64String); 80 | */ 81 | parse(base64Str) { 82 | // Shortcuts 83 | let base64StrLength = base64Str.length; 84 | const map = this._map; 85 | let reverseMap = this._reverseMap; 86 | 87 | if (!reverseMap) { 88 | this._reverseMap = []; 89 | reverseMap = this._reverseMap; 90 | for (let j = 0; j < map.length; j += 1) { 91 | reverseMap[map.charCodeAt(j)] = j; 92 | } 93 | } 94 | 95 | // Ignore padding 96 | const paddingChar = map.charAt(64); 97 | if (paddingChar) { 98 | const paddingIndex = base64Str.indexOf(paddingChar); 99 | if (paddingIndex !== -1) { 100 | base64StrLength = paddingIndex; 101 | } 102 | } 103 | 104 | // Convert 105 | return parseLoop(base64Str, base64StrLength, reverseMap); 106 | }, 107 | 108 | _map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", 109 | }; 110 | -------------------------------------------------------------------------------- /packages/flyyer/src/crypto/sha256.js: -------------------------------------------------------------------------------- 1 | import { WordArray, Hasher } from "./core.js"; 2 | 3 | // Initialization and round constants tables 4 | const H = []; 5 | const K = []; 6 | 7 | // Compute constants 8 | const isPrime = (n) => { 9 | const sqrtN = Math.sqrt(n); 10 | for (let factor = 2; factor <= sqrtN; factor += 1) { 11 | if (!(n % factor)) { 12 | return false; 13 | } 14 | } 15 | 16 | return true; 17 | }; 18 | 19 | const getFractionalBits = (n) => ((n - (n | 0)) * 0x100000000) | 0; 20 | 21 | let n = 2; 22 | let nPrime = 0; 23 | while (nPrime < 64) { 24 | if (isPrime(n)) { 25 | if (nPrime < 8) { 26 | H[nPrime] = getFractionalBits(n ** (1 / 2)); 27 | } 28 | K[nPrime] = getFractionalBits(n ** (1 / 3)); 29 | 30 | nPrime += 1; 31 | } 32 | 33 | n += 1; 34 | } 35 | 36 | // Reusable object 37 | const W = []; 38 | 39 | /** 40 | * SHA-256 hash algorithm. 41 | */ 42 | export class SHA256Algo extends Hasher { 43 | _doReset() { 44 | this._hash = new WordArray(H.slice(0)); 45 | } 46 | 47 | _doProcessBlock(M, offset) { 48 | // Shortcut 49 | const _H = this._hash.words; 50 | 51 | // Working variables 52 | let a = _H[0]; 53 | let b = _H[1]; 54 | let c = _H[2]; 55 | let d = _H[3]; 56 | let e = _H[4]; 57 | let f = _H[5]; 58 | let g = _H[6]; 59 | let h = _H[7]; 60 | 61 | // Computation 62 | for (let i = 0; i < 64; i += 1) { 63 | if (i < 16) { 64 | W[i] = M[offset + i] | 0; 65 | } else { 66 | const gamma0x = W[i - 15]; 67 | const gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ ((gamma0x << 14) | (gamma0x >>> 18)) ^ (gamma0x >>> 3); 68 | 69 | const gamma1x = W[i - 2]; 70 | const gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ ((gamma1x << 13) | (gamma1x >>> 19)) ^ (gamma1x >>> 10); 71 | 72 | W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]; 73 | } 74 | 75 | const ch = (e & f) ^ (~e & g); 76 | const maj = (a & b) ^ (a & c) ^ (b & c); 77 | 78 | const sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22)); 79 | const sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25)); 80 | 81 | const t1 = h + sigma1 + ch + K[i] + W[i]; 82 | const t2 = sigma0 + maj; 83 | 84 | h = g; 85 | g = f; 86 | f = e; 87 | e = (d + t1) | 0; 88 | d = c; 89 | c = b; 90 | b = a; 91 | a = (t1 + t2) | 0; 92 | } 93 | 94 | // Intermediate hash value 95 | _H[0] = (_H[0] + a) | 0; 96 | _H[1] = (_H[1] + b) | 0; 97 | _H[2] = (_H[2] + c) | 0; 98 | _H[3] = (_H[3] + d) | 0; 99 | _H[4] = (_H[4] + e) | 0; 100 | _H[5] = (_H[5] + f) | 0; 101 | _H[6] = (_H[6] + g) | 0; 102 | _H[7] = (_H[7] + h) | 0; 103 | } 104 | 105 | _doFinalize() { 106 | // Shortcuts 107 | const data = this._data; 108 | const dataWords = data.words; 109 | 110 | const nBitsTotal = this._nDataBytes * 8; 111 | const nBitsLeft = data.sigBytes * 8; 112 | 113 | // Add padding 114 | dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - (nBitsLeft % 32)); 115 | dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000); 116 | dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal; 117 | data.sigBytes = dataWords.length * 4; 118 | 119 | // Hash final blocks 120 | this._process(); 121 | 122 | // Return final computed hash 123 | return this._hash; 124 | } 125 | 126 | clone() { 127 | const clone = super.clone.call(this); 128 | clone._hash = this._hash.clone(); 129 | 130 | return clone; 131 | } 132 | } 133 | 134 | /** 135 | * Shortcut function to the hasher's object interface. 136 | * 137 | * @param {WordArray|string} message The message to hash. 138 | * 139 | * @return {WordArray} The hash. 140 | * 141 | * @static 142 | * 143 | * @example 144 | * 145 | * var hash = CryptoJS.SHA256('message'); 146 | * var hash = CryptoJS.SHA256(wordArray); 147 | */ 148 | export const SHA256 = Hasher._createHelper(SHA256Algo); 149 | 150 | /** 151 | * Shortcut function to the HMAC's object interface. 152 | * 153 | * @param {WordArray|string} message The message to hash. 154 | * @param {WordArray|string} key The secret key. 155 | * 156 | * @return {WordArray} The HMAC. 157 | * 158 | * @static 159 | * 160 | * @example 161 | * 162 | * var hmac = CryptoJS.HmacSHA256(message, key); 163 | */ 164 | export const HmacSHA256 = Hasher._createHmacHelper(SHA256Algo); 165 | -------------------------------------------------------------------------------- /packages/flyyer/src/flyyer-render-signed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FlyyerRender as FlyyerBase, 3 | FlyyerRenderParams, 4 | FlyyerVariables, 5 | toQuery, 6 | invariant, 7 | } from "@flyyer/flyyer-lite"; 8 | 9 | import { CREATE_JWT_TOKEN, SIGN_JWT_TOKEN, SIGN_HMAC_DATA } from "./jwt"; 10 | 11 | export class FlyyerRender extends FlyyerBase { 12 | public static signHMAC(data: string, secret: string): string { 13 | const LENGTH = 16; // We only compare the first 16 chars. 14 | return SIGN_HMAC_DATA(data, secret).slice(0, LENGTH); 15 | } 16 | public static signJWT(data: Record, secret: string): string { 17 | const token = CREATE_JWT_TOKEN(data); 18 | return SIGN_JWT_TOKEN(token, secret); 19 | } 20 | 21 | public static sign( 22 | deck: FlyyerRenderParams["deck"], 23 | template: FlyyerRenderParams["template"], 24 | version: FlyyerRenderParams["version"], 25 | extension: FlyyerRenderParams["extension"], 26 | variables: FlyyerRenderParams["variables"], 27 | meta: NonNullable["meta"]>, 28 | strategy: FlyyerRenderParams["strategy"], 29 | secret: FlyyerRenderParams["secret"], 30 | ): string | undefined { 31 | if (!strategy && !secret) return undefined; 32 | invariant( 33 | secret, 34 | "Missing `secret`. You can find it in your deck settings: https://flyyer.io/dashboard/_/library/_/latest/manage", 35 | ); 36 | invariant(strategy, "Missing `strategy`. Valid options are `HMAC` or `JWT`."); 37 | 38 | const normalized = strategy.toUpperCase(); 39 | if (normalized === "HMAC") { 40 | const defaults = { 41 | __id: meta.id, 42 | _w: meta.width, 43 | _h: meta.height, 44 | _res: meta.resolution, 45 | _ua: meta.agent, 46 | _loc: meta.locale, 47 | }; 48 | const data = [deck, template, version || "", extension || "", toQuery(Object.assign(defaults, variables))]; 49 | return FlyyerRender.signHMAC(data.join("#"), secret); 50 | } 51 | if (normalized === "JWT") { 52 | const data = { 53 | d: deck, 54 | t: template, 55 | v: version, 56 | e: extension, 57 | // jwt defaults 58 | i: meta.id, 59 | w: meta.width, 60 | h: meta.height, 61 | r: meta.resolution, 62 | u: meta.agent, 63 | l: meta.locale, 64 | var: variables, 65 | }; 66 | return FlyyerRender.signJWT(data, secret); 67 | } 68 | invariant(false, "Invalid `strategy`. Valid options are `HMAC` or `JWT`."); 69 | } 70 | 71 | public sign( 72 | deck: FlyyerRenderParams["deck"], 73 | template: FlyyerRenderParams["template"], 74 | version: FlyyerRenderParams["version"], 75 | extension: FlyyerRenderParams["extension"], 76 | variables: FlyyerRenderParams["variables"], 77 | meta: NonNullable["meta"]>, 78 | strategy: FlyyerRenderParams["strategy"], 79 | secret: FlyyerRenderParams["secret"], 80 | ): string | undefined { 81 | return FlyyerRender.sign(deck, template, version, extension, variables, meta, strategy, secret); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/flyyer/src/flyyer-signed.ts: -------------------------------------------------------------------------------- 1 | import { Flyyer as FlyyerBase, FlyyerParams, FlyyerVariables, invariant } from "@flyyer/flyyer-lite"; 2 | 3 | import { CREATE_JWT_TOKEN, SIGN_JWT_TOKEN, SIGN_HMAC_DATA } from "./jwt"; 4 | 5 | export class Flyyer extends FlyyerBase { 6 | public static signHMAC(data: string, secret: string): string { 7 | const LENGTH = 16; // We only compare the first 16 chars. 8 | return SIGN_HMAC_DATA(data, secret).slice(0, LENGTH); 9 | } 10 | public static signJWT(data: Record, secret: string): string { 11 | const token = CREATE_JWT_TOKEN(data); 12 | return SIGN_JWT_TOKEN(token, secret); 13 | } 14 | public static sign( 15 | project: FlyyerParams["project"], 16 | path: string, // normalized 17 | params: string, 18 | strategy: FlyyerParams["strategy"], 19 | secret: FlyyerParams["secret"], 20 | ): string | undefined { 21 | if (!strategy && !secret) return "_"; 22 | invariant( 23 | secret, 24 | "Missing `secret`. You can find it in your project in Advanced settings: https://flyyer.io/dashboard/_/projects/_/advanced", 25 | ); 26 | invariant(strategy, "Missing `strategy`. Valid options are `HMAC` or `JWT`."); 27 | 28 | const str = strategy.toUpperCase(); 29 | if (str === "HMAC") { 30 | const data = `${project}/${path}${params}`; 31 | return Flyyer.signHMAC(data, secret); 32 | } 33 | if (str === "JWT") { 34 | return Flyyer.signJWT({ path, params }, secret); 35 | } 36 | invariant(false, "Invalid `strategy`. Valid options are `HMAC` or `JWT`."); 37 | } 38 | public sign( 39 | project: FlyyerParams["project"], 40 | path: string, // normalized 41 | params: string, 42 | strategy: FlyyerParams["strategy"], 43 | secret: FlyyerParams["secret"], 44 | ): string | undefined { 45 | return Flyyer.sign(project, path, params, strategy, secret); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/flyyer/src/index.ts: -------------------------------------------------------------------------------- 1 | export { __V } from "@flyyer/flyyer-lite"; 2 | export { toQuery } from "@flyyer/flyyer-lite"; 3 | export { FlyyerCommonParams } from "@flyyer/flyyer-lite"; 4 | export { FlyyerMetaVariables } from "@flyyer/flyyer-lite"; 5 | export { FlyyerVariables } from "@flyyer/flyyer-lite"; 6 | export { FlyyerExtension } from "@flyyer/flyyer-lite"; 7 | export { normalizePath, FlyyerPath } from "@flyyer/flyyer-lite"; 8 | export { FlyyerParams } from "@flyyer/flyyer-lite"; 9 | export { FlyyerRenderParams } from "@flyyer/flyyer-lite"; 10 | 11 | export { CREATE_JWT_TOKEN, SIGN_JWT_TOKEN, SIGN_HMAC_DATA, BASE64_URL } from "./jwt"; 12 | export { Flyyer } from "./flyyer-signed"; 13 | export { FlyyerRender } from "./flyyer-render-signed"; 14 | export { isEqualFlyyer, isEqualFlyyerMeta, isEqualFlyyerRender } from "./compare"; 15 | -------------------------------------------------------------------------------- /packages/flyyer/src/jwt.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-ignore */ 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | 4 | // @ts-ignore 5 | import { Utf8 } from "./crypto/core"; 6 | // @ts-ignore 7 | import { Base64 } from "./crypto/enc-base64"; 8 | // @ts-ignore 9 | import { HmacSHA256 } from "./crypto/sha256"; 10 | 11 | export function BASE64_URL(source: any): string { 12 | // Encode in classical base64 13 | let encodedSource = Base64.stringify(source); 14 | // Remove padding equal characters 15 | encodedSource = encodedSource.replace(/=+$/, ""); 16 | // Replace characters according to base64url specifications 17 | encodedSource = encodedSource.replace(/\+/g, "-"); 18 | encodedSource = encodedSource.replace(/\//g, "_"); 19 | return encodedSource; 20 | } 21 | 22 | export function CREATE_JWT_TOKEN(data: any): string { 23 | // https://www.jonathan-petitcolas.com/2014/11/27/creating-json-web-token-in-javascript.html 24 | const header = { 25 | alg: "HS256", 26 | typ: "JWT", 27 | }; 28 | 29 | const stringifiedHeader = Utf8.parse(JSON.stringify(header)); 30 | const encodedHeader = BASE64_URL(stringifiedHeader); 31 | const stringifiedData = Utf8.parse(JSON.stringify(data)); 32 | const encodedData = BASE64_URL(stringifiedData); 33 | 34 | const token = encodedHeader + "." + encodedData; 35 | return token; 36 | } 37 | 38 | export function SIGN_JWT_TOKEN(token: string, secret: string): string { 39 | const sha = HmacSHA256(token, secret); 40 | const signature = BASE64_URL(sha); 41 | const signed = token + "." + signature; 42 | return signed; 43 | } 44 | 45 | export function SIGN_HMAC_DATA(data: string, secret: string): string { 46 | return HmacSHA256(data, secret).toString(); 47 | } 48 | -------------------------------------------------------------------------------- /packages/flyyer/test/flyyer-render.test.ts: -------------------------------------------------------------------------------- 1 | import { dequal } from "dequal/lite"; 2 | 3 | import { FlyyerRender, isEqualFlyyerRender } from ".."; 4 | 5 | describe("FlyyerRender", () => { 6 | it("FlyyerRender is instantiable", () => { 7 | const flyyer = new FlyyerRender({ tenant: "", deck: "", template: "" }); 8 | expect(flyyer).toBeInstanceOf(FlyyerRender); 9 | }); 10 | 11 | it("raises error if missing arguments", () => { 12 | const executer = (args?: any) => new FlyyerRender(args).href(); 13 | 14 | expect(() => executer()).toThrow("FlyyerRender constructor must not be empty"); 15 | expect(() => executer({ tenant: undefined })).toThrow("Missing 'tenant' property"); 16 | }); 17 | 18 | const DEFAULTS = { 19 | tenant: "tenant", 20 | deck: "deck", 21 | template: "template", 22 | }; 23 | 24 | it("no queryparams no '?'", () => { 25 | const flyyer = new FlyyerRender({ 26 | ...DEFAULTS, 27 | meta: { v: null }, 28 | }); 29 | expect(flyyer.href()).toEqual("https://cdn.flyyer.io/r/v2/tenant/deck/template"); 30 | }); 31 | 32 | it("encodes url", () => { 33 | const flyyer = new FlyyerRender({ 34 | ...DEFAULTS, 35 | extension: "jpeg", 36 | variables: { 37 | title: "Hello world!", 38 | description: "", 39 | }, 40 | }); 41 | const href = flyyer.href(); 42 | expect(href).toMatch( 43 | /^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\/deck\/template\.jpeg\?__v=(\d+)&title=Hello\+world%21&description=$/, 44 | ); 45 | }); 46 | 47 | it("encodes url and skips undefined values", () => { 48 | const flyyer = new FlyyerRender({ 49 | ...DEFAULTS, 50 | extension: "jpeg", 51 | variables: { 52 | title: "title", 53 | description: undefined, 54 | }, 55 | }); 56 | const href = flyyer.href(); 57 | expect(href).toMatch(/^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\/deck\/template\.jpeg\?__v=(\d+)&title=title$/); 58 | }); 59 | 60 | it("encodes url and convert null values to empty string", () => { 61 | const flyyer = new FlyyerRender({ 62 | ...DEFAULTS, 63 | extension: "jpeg", 64 | variables: { 65 | title: "title", 66 | description: null, 67 | }, 68 | }); 69 | const href = flyyer.href(); 70 | expect(href).toMatch( 71 | /^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\/deck\/template\.jpeg\?__v=(\d+)&title=title&description=$/, 72 | ); 73 | }); 74 | 75 | it("encodes url with meta values", () => { 76 | const flyyer = new FlyyerRender({ 77 | ...DEFAULTS, 78 | variables: { 79 | title: "title", 80 | }, 81 | meta: { 82 | agent: "whatsapp", 83 | locale: "es-CL", 84 | height: 100, 85 | width: "200", 86 | id: "dev forgot to encode", 87 | v: null, 88 | }, 89 | extension: "png", 90 | }); 91 | const href = flyyer.href(); 92 | expect(href).toMatch( 93 | /^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\/deck\/template\.png\?__id=dev\+forgot\+to\+encode&_w=200&_h=100&_ua=whatsapp&_loc=es-CL&title=title$/, 94 | ); 95 | }); 96 | 97 | it("compares two instances", async () => { 98 | const flyyer0 = new FlyyerRender({ 99 | ...DEFAULTS, 100 | variables: { title: "Hello" }, 101 | meta: { v: "anything" }, 102 | }); 103 | const flyyer1 = new FlyyerRender({ ...DEFAULTS, variables: { title: "Hello" } }); 104 | const flyyer2 = new FlyyerRender({ ...DEFAULTS, variables: { title: "Bye" } }); 105 | expect(flyyer0.href()).not.toEqual(flyyer1.href()); // different __v 106 | expect(isEqualFlyyerRender(flyyer0, flyyer0, dequal)).toEqual(true); 107 | expect(isEqualFlyyerRender(flyyer0, flyyer1, dequal)).toEqual(true); 108 | expect(isEqualFlyyerRender(flyyer0, flyyer2, dequal)).toEqual(false); 109 | }); 110 | 111 | it("encodes url with hmac", () => { 112 | const flyyer = new FlyyerRender({ 113 | ...DEFAULTS, 114 | variables: { 115 | title: "Hello world!", 116 | }, 117 | extension: "jpeg", 118 | secret: "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx", 119 | strategy: "HMAC", 120 | }); 121 | const href = flyyer.href(); 122 | expect(href).toMatch( 123 | /^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\/deck\/template.jpeg\?__v=\d+&title=Hello\+world%21&__hmac=6b631ae8c4ca2977$/, 124 | ); 125 | }); 126 | 127 | it("encode url with jwt default values", () => { 128 | const flyyer = new FlyyerRender({ 129 | ...DEFAULTS, 130 | variables: { 131 | title: "title", 132 | }, 133 | version: 4, 134 | extension: "jpeg", 135 | secret: "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx", 136 | strategy: "JWT", 137 | }); 138 | const href = flyyer.href(); 139 | expect(href).toMatch( 140 | /^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\?__jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkIjoiZGVjayIsInQiOiJ0ZW1wbGF0ZSIsInYiOjQsImUiOiJqcGVnIiwidmFyIjp7InRpdGxlIjoidGl0bGUifX0.d_25VySY-Lfhwm3L2C-tNFwD7Zrh3cx_svyDEfkP2FE&__v=\d+$/, 141 | ); 142 | }); 143 | 144 | it("encode url with jwt with meta", () => { 145 | const flyyer = new FlyyerRender({ 146 | ...DEFAULTS, 147 | variables: { 148 | title: "title", 149 | }, 150 | meta: { 151 | agent: "whatsapp", 152 | locale: "es-CL", 153 | height: 100, 154 | width: "200", 155 | id: "dev forgot to encode", 156 | v: null, 157 | }, 158 | version: 4, 159 | extension: "jpeg", 160 | secret: "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx", 161 | strategy: "JWT", 162 | }); 163 | const href = flyyer.href(); 164 | expect(href).toMatch( 165 | /^https:\/\/cdn.flyyer.io\/r\/v2\/tenant\?__jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkIjoiZGVjayIsInQiOiJ0ZW1wbGF0ZSIsInYiOjQsImUiOiJqcGVnIiwiaSI6ImRldiBmb3Jnb3QgdG8gZW5jb2RlIiwidyI6IjIwMCIsImgiOjEwMCwidSI6IndoYXRzYXBwIiwibCI6ImVzLUNMIiwidmFyIjp7InRpdGxlIjoidGl0bGUifX0.WFfRe-mniiftEwp-J3lFkV3ME0iXHUyg5QHwEgMKW88$/, 166 | ); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /packages/flyyer/test/flyyer.test.ts: -------------------------------------------------------------------------------- 1 | import { dequal } from "dequal/lite"; 2 | 3 | import { Flyyer, isEqualFlyyer } from "../dist/index"; 4 | 5 | describe("Flyyer", () => { 6 | it("Flyyer is instantiable", () => { 7 | const flyyer = new Flyyer({ project: "" }); 8 | expect(flyyer).toBeInstanceOf(Flyyer); 9 | }); 10 | 11 | it("raises error if missing arguments", () => { 12 | const executer = (args?: any) => new Flyyer(args).href(); 13 | 14 | expect(() => executer()).toThrow("Flyyer constructor must not be empty"); 15 | expect(() => executer({ project: "" })).not.toThrow(); 16 | }); 17 | 18 | it("without path fallbacks to root", () => { 19 | const flyyer = new Flyyer({ project: "project" }); 20 | expect(flyyer.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/$/); 21 | }); 22 | 23 | it("handles single path", () => { 24 | const flyyer = new Flyyer({ project: "project", path: "about" }); 25 | expect(flyyer.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/about$/); 26 | }); 27 | 28 | it("handles multiple paths", () => { 29 | const options = [ 30 | ["dashboard", "company"], 31 | ["/dashboard", "company"], 32 | ["/dashboard", "/company"], 33 | ["dashboard", false, "/company"], 34 | ["/dashboard/", null, "/company/"], 35 | ["dashboard///", null, undefined, false, "////company/"], 36 | ]; 37 | for (const path of options) { 38 | const flyyer = new Flyyer({ project: "project", path: path as any }); 39 | expect(flyyer.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/dashboard\/company$/); 40 | } 41 | }); 42 | 43 | it("handle numbers in path", () => { 44 | const flyyer1 = new Flyyer({ project: "project", path: ["products", 1] }); 45 | expect(flyyer1.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products\/1$/); 46 | const flyyer0 = new Flyyer({ project: "project", path: ["products", 0] }); 47 | expect(flyyer0.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products\/0$/); 48 | const flyyerInf = new Flyyer({ project: "project", path: ["products", Infinity] }); 49 | expect(flyyerInf.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products\/Infinity$/); 50 | // Ignores falsy values 51 | const flyyerNaN = new Flyyer({ project: "project", path: ["products", NaN] }); 52 | expect(flyyerNaN.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products$/); 53 | }); 54 | 55 | it("handle booleans in path", () => { 56 | const flyyerTrue = new Flyyer({ project: "project", path: ["products", true as any] }); 57 | expect(flyyerTrue.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products\/true$/); 58 | // Ignores falsy values 59 | const flyyerFalse = new Flyyer({ project: "project", path: ["products", false as any] }); 60 | expect(flyyerFalse.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/products$/); 61 | }); 62 | 63 | it("handle variables such as `title`", () => { 64 | const flyyer = new Flyyer({ project: "project", path: "about", variables: { title: "hello world" } }); 65 | expect(flyyer.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)&title=hello\+world\/about$/); 66 | }); 67 | 68 | it("can disable __v cache-bursting param", () => { 69 | const flyyer0 = new Flyyer({ project: "project", meta: { v: undefined } }); 70 | expect(flyyer0.href()).toMatch(/^https:\/\/cdn.flyyer.io\/v2\/project\/_\/__v=(\d+)\/$/); 71 | const flyyer1 = new Flyyer({ project: "project", meta: { v: null } }); 72 | expect(flyyer1.href()).toEqual("https://cdn.flyyer.io/v2/project/_/_/"); 73 | const flyyer2 = new Flyyer({ project: "project", meta: { v: "" } }); 74 | expect(flyyer2.href()).toEqual("https://cdn.flyyer.io/v2/project/_/__v=/"); 75 | const flyyer3 = new Flyyer({ project: "project", meta: { v: false as any } }); 76 | expect(flyyer3.href()).toEqual("https://cdn.flyyer.io/v2/project/_/__v=false/"); 77 | }); 78 | 79 | it("encodes url with hmac signature", () => { 80 | const flyyer1 = new Flyyer({ 81 | project: "project", 82 | path: "/products/1", 83 | secret: "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx", 84 | strategy: "HMAC", 85 | meta: { width: 1080 }, 86 | }); 87 | const flyyer2 = new Flyyer({ ...flyyer1, meta: { ...flyyer1.meta, v: 123 } }); 88 | 89 | // if __v changes the signature remains the same 90 | const regex = /^https:\/\/cdn.flyyer.io\/v2\/project\/c5bd759442845a20\/__v=(\d+)&_w=1080\/products\/1$/; 91 | expect(flyyer1.href()).toMatch(regex); 92 | expect(flyyer2.href()).toMatch(regex); 93 | }); 94 | 95 | it("encodes url with JWT signature", () => { 96 | const flyyer1 = new Flyyer({ 97 | project: "project", 98 | path: "/products/1", 99 | secret: "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx", 100 | strategy: "JWT", 101 | meta: { v: null }, 102 | }); 103 | expect(flyyer1.href()).toEqual( 104 | "https://cdn.flyyer.io/v2/project/jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXRoIjoicHJvZHVjdHMvMSIsInBhcmFtcyI6IiJ9.KMAG3_NQkfou6rkBc3gYunVilfqNnFdVzKd2IrRmUz4", 105 | ); 106 | }); 107 | 108 | it("sets 'default' image as '_def' param in JWT", () => { 109 | const flyyer0 = new Flyyer({ 110 | project: "project", 111 | path: "path", 112 | default: "/static/product/1.png", 113 | secret: "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx", 114 | strategy: "JWT", 115 | meta: { v: null }, 116 | }); 117 | expect(flyyer0.href()).toEqual( 118 | "https://cdn.flyyer.io/v2/project/jwt-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXRoIjoicGF0aCIsInBhcmFtcyI6Il9kZWY9JTJGc3RhdGljJTJGcHJvZHVjdCUyRjEucG5nIn0.W-aMsd4jakMYftprBmOCFxdR67xMPKbdvLDgPLFv0Ws", 119 | ); 120 | }); 121 | it("sets 'default' image as '_def' param in HMAC", () => { 122 | const flyyer0 = new Flyyer({ 123 | project: "project", 124 | path: "path", 125 | default: "/static/product/1.png", 126 | secret: "sg1j0HVy9bsMihJqa8Qwu8ZYgCYHG0tx", 127 | strategy: "HMAC", 128 | meta: { v: null }, 129 | }); 130 | expect(flyyer0.href()).toEqual( 131 | "https://cdn.flyyer.io/v2/project/89312d0aaddd60fe/_def=%2Fstatic%2Fproduct%2F1.png/path", 132 | ); 133 | }); 134 | 135 | it("compares two instances", async () => { 136 | const flyyer0 = new Flyyer({ 137 | project: "project", 138 | path: "products/1", 139 | variables: { title: "Hello" }, 140 | meta: { v: "anything" }, 141 | }); 142 | const flyyer1 = new Flyyer({ project: "project", path: ["/products", "1"], variables: { title: "Hello" } }); 143 | const flyyer2 = new Flyyer({ project: "project", path: ["/products", "1"], variables: { title: "Bye" } }); 144 | expect(flyyer0.href()).not.toEqual(flyyer1.href()); // different __v 145 | expect(isEqualFlyyer(flyyer0, flyyer0, dequal)).toEqual(true); 146 | expect(isEqualFlyyer(flyyer0, flyyer1, dequal)).toEqual(true); 147 | expect(isEqualFlyyer(flyyer0, flyyer2, dequal)).toEqual(false); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /packages/flyyer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": false, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | "allowJs": true, 35 | } 36 | } 37 | --------------------------------------------------------------------------------