├── .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 |    
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 | 
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 | [](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 | [](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 | [](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 |    
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 | 
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 | [](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 | [](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 | [](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 |
--------------------------------------------------------------------------------