├── .gitignore ├── config └── default.example.json ├── src ├── server │ ├── types.ts │ ├── constants.ts │ ├── middlewares.ts │ ├── logger.ts │ └── server.ts └── web │ ├── service-worker.ts │ └── index.ts ├── tsconfig.server.json ├── tsconfig.web.json ├── package.json ├── login.html ├── index.html ├── README.md ├── css └── common.css ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | data.json 5 | config/default.json 6 | -------------------------------------------------------------------------------- /config/default.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "gcmKey": "", 3 | "subject": "mailto:", 4 | "vapid": { 5 | "public": "", 6 | "private": "" 7 | } 8 | } -------------------------------------------------------------------------------- /src/server/types.ts: -------------------------------------------------------------------------------- 1 | import { PushSubscription } from 'web-push'; 2 | 3 | export type UserSubscription = { 4 | userId: string; 5 | subscription: PushSubscription; 6 | }; 7 | 8 | export type Store = { 9 | data: UserSubscription[]; 10 | }; 11 | 12 | export type PushMessage = { 13 | title: string; 14 | body: string; 15 | }; 16 | -------------------------------------------------------------------------------- /src/server/constants.ts: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | 3 | export const DATA_PATH = 'data.json'; 4 | 5 | export const GCM_KEY = config.get('gcmKey'); 6 | export const SUBJECT = config.get('subject'); 7 | export const VAPID_PUBLIC = config.get('vapid.public'); 8 | export const VAPID_PRIVATE = config.get('vapid.private'); 9 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", /* Specify what module code is generated. */ 5 | "outDir": "./dist/server", /* Specify an output folder for all emitted files. */ 6 | }, 7 | "include": ["src/server/server.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | /* Language and Environment */ 5 | "target": "es2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 6 | "lib": ["ES2015", "Dom", "WebWorker"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 7 | "outDir": "./dist/web" /* Specify an output folder for all emitted files. */ 8 | }, 9 | "include": ["src/web/index.ts", "src/web/service-worker.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-push", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build:web": "tsc --project tsconfig.web.json", 6 | "build:server": "tsc --project tsconfig.server.json", 7 | "build": "yarn build:web && yarn build:server", 8 | "start": "node dist/server/server.js" 9 | }, 10 | "repository": "https://github.com/leegeunhyeok/web-push.git", 11 | "author": "leegeunhyeok ", 12 | "devDependencies": { 13 | "@types/config": "^0.0.41", 14 | "@types/express": "^4.17.13", 15 | "@types/web-push": "^3.3.2", 16 | "typescript": "^4.7.3" 17 | }, 18 | "dependencies": { 19 | "colors": "^1.4.0", 20 | "config": "^3.3.7", 21 | "express": "^4.18.1", 22 | "web-push": "^3.5.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/server/middlewares.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { logger } from './logger'; 3 | 4 | export const responseLogger = (_req: Request, res: Response, next: NextFunction) => { 5 | const { originalUrl: url, method } = res.req; 6 | 7 | const afterResponse = () => { 8 | res.removeListener('finish', afterResponse); 9 | res.removeListener('close', afterResponse); 10 | 11 | let statusCode = res.statusCode.toString(); 12 | let loggerByStatusCode = logger.info; 13 | 14 | switch (statusCode.charAt(0)) { 15 | case '2': 16 | statusCode = statusCode.green; 17 | break; 18 | 19 | case '3': 20 | statusCode = statusCode.yellow; 21 | break; 22 | 23 | case '4': 24 | case '5': 25 | statusCode = statusCode.red; 26 | loggerByStatusCode = logger.error; 27 | break; 28 | } 29 | 30 | loggerByStatusCode(`${method} ${statusCode} ${url}`); 31 | }; 32 | 33 | res.on('finish', afterResponse); 34 | res.on('close', afterResponse); 35 | 36 | next(); 37 | }; 38 | -------------------------------------------------------------------------------- /src/web/service-worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | type PushMessage = { 4 | title: string; 5 | body: string; 6 | } 7 | 8 | function log (...args: any[]) { 9 | console.log('service-worker:', ...args); 10 | } 11 | 12 | // https://github.com/microsoft/TypeScript/issues/14877 13 | ((self: ServiceWorkerGlobalScope) => { 14 | 15 | self.addEventListener('install', (event: ExtendableEvent) => { 16 | log('install', { event }); 17 | event.waitUntil(self.skipWaiting()); 18 | }); 19 | 20 | self.addEventListener('activate', (event: ExtendableEvent) => { 21 | log('activate', { event }); 22 | }); 23 | 24 | self.addEventListener('push', (event: PushEvent) => { 25 | log('push', { event }); 26 | 27 | const message = event.data?.json() as PushMessage; 28 | event.waitUntil( 29 | self.registration.showNotification(message.title, { 30 | body: message.body, 31 | }) 32 | ); 33 | }); 34 | 35 | self.addEventListener('notificationclick', (event: NotificationEvent) => { 36 | log('notificationclick', { event }); 37 | self.clients.openWindow('https://github.com/leegeunhyeok/web-push'); 38 | }); 39 | 40 | })(self as unknown as ServiceWorkerGlobalScope); 41 | -------------------------------------------------------------------------------- /login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Web Push - login 7 | 8 | 9 | 10 | 11 |
12 |

Web Push

13 | 14 | 15 | 16 |
17 |
18 |
19 |

👋 Login

20 |
21 | 22 | 23 |
24 |
25 |
26 | 27 | 40 | -------------------------------------------------------------------------------- /src/server/logger.ts: -------------------------------------------------------------------------------- 1 | import colors from 'colors'; 2 | 3 | interface Logger { 4 | (...messages: any[]): void; 5 | } 6 | 7 | const LEVEL_COLORS = { 8 | info: colors.green, 9 | debug: colors.blue, 10 | success: colors.green, 11 | warning: colors.yellow, 12 | danger: colors.red, 13 | error: colors.red, 14 | critical: colors.magenta 15 | } as const; 16 | 17 | const getTimestamp = () => { 18 | const date = new Date(); 19 | 20 | const yyyy = date.getFullYear(); 21 | const MM = date.getMonth().toString().padStart(2, '0'); 22 | const dd = date.getDate().toString().padStart(2, '0'); 23 | 24 | const hh = date.getHours().toString().padStart(2, '0'); 25 | const mm = date.getMinutes().toString().padStart(2, '0'); 26 | const ss = date.getSeconds().toString().padStart(2, '0'); 27 | const aaa = date.getMilliseconds().toString().padStart(3, '0'); 28 | 29 | return `${yyyy}-${MM}-${dd} ${hh}:${mm}:${ss}.${aaa}`; 30 | }; 31 | 32 | const printLog = (level: keyof typeof LEVEL_COLORS, ...messages: any[]) => { 33 | console.log( 34 | `[${getTimestamp()}]`.gray, 35 | `${LEVEL_COLORS[level](level.toUpperCase())}`, 36 | '-', 37 | ...messages 38 | ); 39 | }; 40 | 41 | export const logger: Record = { 42 | info: (...messages) => printLog('info', ...messages), 43 | debug: (...messages) => printLog('debug', ...messages), 44 | success: (...messages) => printLog('success', ...messages), 45 | warning: (...messages) => printLog('warning', ...messages), 46 | danger: (...messages) => printLog('danger', ...messages), 47 | error: (...messages) => printLog('error', ...messages), 48 | critical: (...messages) => printLog('critical', ...messages), 49 | }; 50 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Web Push 7 | 8 | 9 | 10 | 11 |
12 |

Web Push

13 | 14 | 15 | 16 |
17 |
18 |
19 |

👀 Status

20 |
21 | Current User ID:

22 |
23 |
24 | ServiceWorker registered:

25 |
26 | Push Support:

27 |
28 | Notification permission:

29 |
30 |
31 |

32 |         
33 |
34 |
35 |

👉 Subscribe

36 |
37 | 38 | 39 |
40 |
41 |
42 |

🚀 Send Push Notification

43 |
44 | 45 |
46 |
47 | 48 | 49 |
50 |
51 |

-

52 |
53 |
54 |
55 |

🔥 Logout

56 |
57 | 58 |
59 |
60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![top](https://user-images.githubusercontent.com/26512984/173287314-ba3b8d9b-6303-4e20-93de-cc82f2bfce3a.png) 4 | 5 |

Web Push | Getting Started

6 | 7 | [Show article](https://geundung.dev/114) 8 | 9 |
10 | 11 | ## Project 12 | 13 | ``` 14 | . 15 | ├── README.md 16 | ├── config 17 | │   └── default.example.json 18 | ├── css 19 | │   └── common.css 20 | ├── index.html 21 | ├── login.html 22 | ├── package.json 23 | ├── src 24 | │   ├── 💻 server 25 | │   │   ├── constants.ts 26 | │   │   ├── logger.ts 27 | │   │   ├── middlewares.ts 28 | │   │   ├── server.ts 29 | │   │   └── types.ts 30 | │   └── 🌍 web 31 | │   ├── index.ts 32 | │   └── service-worker.ts 33 | ├── tsconfig.json 34 | ├── tsconfig.server.json 35 | ├── tsconfig.web.json 36 | └── yarn.lock 37 | ``` 38 | 39 | - Server 40 | - [src/server/server.ts](https://github.com/leegeunhyeok/web-push/blob/main/src/server/server.ts): server entry code 41 | - Web page 42 | - [src/web/index.ts](https://github.com/leegeunhyeok/web-push/blob/main/src/web/index.ts): page script 43 | - [src/web/service-worker.ts](https://github.com/leegeunhyeok/web-push/blob/main/src/web/service-worker.ts): service worker script 44 | 45 | ## Setup 46 | 47 | - Pre-requirements 48 | - >= Node 16 49 | - yarn 50 | ```bash 51 | npm i -g yarn 52 | ``` 53 | - create vapid key pairs 54 | ```bash 55 | yarn generate-vapid-keys 56 | ``` 57 | - create `config/default.json` (check config/default.example.json) 58 | ```json 59 | { 60 | "gcmKey": "GCM API Key HERE", 61 | "subject": "mailto:your@domain.com", 62 | "vapid": { 63 | "public": "PUBLIC KEY HERE", 64 | "private": "PRIVATE KEY HERE" 65 | } 66 | } 67 | ``` 68 | - How to get GCM API key? 69 | - Visit [Google Firebase Console](https://console.firebase.google.com) and create project 70 | - Open Project > Project settings > Cloud Messaging 71 | - Cloud Messaging API > `Server Key` 72 | - setup project 73 | ```bash 74 | # Install dependencies 75 | yarn 76 | 77 | # Build scripts (TS -> JS) 78 | yarn build 79 | 80 | # Start demo server 81 | yarn start 82 | ``` 83 | 84 | Done! visit [http://localhost:8080](http://localhost:8080) 85 | 86 | ## Preview 87 | 88 | ![main](https://user-images.githubusercontent.com/26512984/173250652-cdc843de-8c1c-4220-838c-40815189af26.png) 89 | 90 | ![notification](https://user-images.githubusercontent.com/26512984/173250678-a88651b4-87c5-4f62-b8d2-bdf625e0ac5b.png) 91 | -------------------------------------------------------------------------------- /src/server/server.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import express from 'express'; 4 | import webpush from 'web-push'; 5 | import { logger } from './logger'; 6 | import { responseLogger } from './middlewares'; 7 | import { 8 | DATA_PATH, 9 | GCM_KEY, 10 | SUBJECT, 11 | VAPID_PUBLIC, 12 | VAPID_PRIVATE, 13 | } from './constants'; 14 | import { Store, PushMessage } from './types'; 15 | 16 | webpush.setGCMAPIKey(GCM_KEY); 17 | webpush.setVapidDetails( 18 | SUBJECT, 19 | VAPID_PUBLIC, 20 | VAPID_PRIVATE 21 | ); 22 | 23 | const store: Store = { data: [] }; 24 | 25 | const app = express(); 26 | 27 | app.use('/', express.static(path.join(__dirname, '../../'))); // project root 28 | app.use('/', express.static(path.join(__dirname, '../web'))); // project root/dist/web 29 | app.use(responseLogger) 30 | app.use(express.json()); 31 | 32 | app.get('/vapid-public-key', (_req, res) => { 33 | res.send(VAPID_PUBLIC); 34 | }); 35 | 36 | app.post('/subscription', (req, res) => { 37 | const { userId, subscription } = req.body ?? {}; 38 | 39 | // replace to new subscription if userId is already exist 40 | const index = store.data.findIndex((data) => data.userId === userId); 41 | if (~index) store.data[index].subscription = subscription; 42 | 43 | store.data.push({ userId, subscription }); 44 | const data = JSON.stringify(store.data); 45 | 46 | fs.writeFile(DATA_PATH, data, 'utf-8', (error) => { 47 | if (error) { 48 | logger.error('POST /subscription', { error }); 49 | res.status(500).end(); 50 | } else { 51 | res.status(201).end(); 52 | } 53 | }); 54 | }); 55 | 56 | app.delete('/subscription', (req, res) => { 57 | const { userId } = req.body ?? {}; 58 | 59 | // remove target user data 60 | const index = store.data.findIndex((data) => data.userId === userId); 61 | if (~index) { 62 | store.data.splice(index, 1); 63 | } 64 | 65 | const data = JSON.stringify(store.data); 66 | 67 | fs.writeFile(DATA_PATH, data, 'utf-8', (error) => { 68 | if (error) { 69 | logger.error('DELETE /subscription', { error }); 70 | res.status(500).end(); 71 | } else { 72 | res.status(200).end(); 73 | } 74 | }); 75 | }); 76 | 77 | app.post('/send-push-notification', (req, res) => { 78 | const { targetId: targetUserId, message } = req.body ?? {}; 79 | logger.info(`Send push notification to '${targetUserId}' with '${message}'`); 80 | const targetUser = store 81 | .data 82 | .reverse() 83 | .find(({ userId }) => userId === targetUserId); 84 | 85 | if (targetUser) { 86 | const messageData: PushMessage = { 87 | title: 'Web Push | Getting Started', 88 | body: message || '(Empty message)', 89 | }; 90 | 91 | webpush 92 | .sendNotification(targetUser.subscription, JSON.stringify(messageData)) 93 | .then((pushServiceRes) => res.status(pushServiceRes.statusCode).end()) 94 | .catch((error) => { 95 | logger.error('POST /send-push', { error }); 96 | res.status(error?.statusCode ?? 500).end(); 97 | }); 98 | } else { 99 | res.status(404).end(); 100 | } 101 | }); 102 | 103 | new Promise((resolve) => { 104 | fs.access(DATA_PATH, fs.constants.F_OK, (error) => { 105 | // create data file if not exist 106 | error && fs.writeFileSync(DATA_PATH, JSON.stringify([]), 'utf-8'); 107 | resolve(); 108 | }); 109 | }).then(() => { 110 | fs.readFile(DATA_PATH, (error, data) => { 111 | if (error) { 112 | logger.error('Cannot load data.json', { error }); 113 | } else { 114 | store.data = JSON.parse(data.toString()); 115 | } 116 | app.listen(8080, () => logger.info('Server started')); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /css/common.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Pretendard, -apple-system, BlinkMacSystemFont, 3 | system-ui, Roboto, 'Helvetica Neue', 'Segoe UI', 4 | 'Apple SD Gothic Neo', 'Noto Sans KR', 5 | 'Malgun Gothic', 'Apple Color Emoji', 6 | 'Segoe UI Emoji', 'Segoe UI Symbol', sans-serif; 7 | } 8 | 9 | html, body { 10 | width: 100%; 11 | height: 100%; 12 | padding: 0; 13 | margin: 0; 14 | } 15 | 16 | body { font-size: 16px; } 17 | 18 | header { 19 | display: flex; 20 | align-items: center; 21 | justify-content: space-between; 22 | width: 100%; 23 | height: 70px; 24 | padding: .5rem 1rem; 25 | background-color: #fff; 26 | font-weight: bold; 27 | box-sizing: border-box; 28 | box-shadow: 0px 1px 4px #00000033; 29 | } 30 | 31 | header > h1 { 32 | margin: 0; 33 | color: #000; 34 | font-size: 2rem; 35 | } 36 | 37 | main { 38 | width: 100%; 39 | max-width: 700px; 40 | padding: 0 1rem; 41 | margin: auto; 42 | margin-top: 1rem; 43 | box-sizing: border-box; 44 | } 45 | 46 | main > section { 47 | padding-bottom: 1rem; 48 | margin-bottom: 1rem; 49 | border-bottom: 1px solid #eee; 50 | } 51 | 52 | main > section > h2 { 53 | margin: 1rem 0; 54 | color: #000; 55 | } 56 | 57 | main > section > .item { 58 | margin: .5rem 0; 59 | overflow: auto; 60 | } 61 | 62 | main > section p:last-child, 63 | main > section > .item:last-child { 64 | margin-bottom: 0; 65 | } 66 | main > section:last-child { border-bottom: none; } 67 | 68 | input { 69 | width: 100%; 70 | padding: .5rem; 71 | margin: 0; 72 | border: 1px solid #eee; 73 | border-radius: 3px; 74 | background-color: #eee; 75 | color: #000; 76 | font-size: 1rem; 77 | box-sizing: border-box; 78 | overflow: hidden; 79 | outline: none; 80 | -webkit-appearance: none; 81 | -webkit-user-select: initial !important; 82 | user-select: initial !important; 83 | -webkit-transition: background-color .3s; 84 | transition: background-color .3s; 85 | } 86 | input:focus, 87 | input:active { 88 | background-color: #ddd; 89 | } 90 | input::placeholder, 91 | input::-webkit-input-placeholder { 92 | color: #555; 93 | } 94 | 95 | button { 96 | padding: .5rem 1rem; 97 | margin: 0; 98 | border: 1px solid #000; 99 | border-radius: 3px; 100 | background-color: #000; 101 | color: #fff; 102 | font-size: 1rem; 103 | cursor: pointer; 104 | outline: none; 105 | -webkit-transition: background-color .3s; 106 | transition: background-color .3s; 107 | } 108 | button:hover { background-color: #333; } 109 | button:active { background-color: #555; } 110 | button:disabled { 111 | background-color: #aaa; 112 | border: 1px solid #aaa; 113 | cursor: not-allowed; 114 | } 115 | 116 | pre { 117 | display: inline-block; 118 | padding: .5rem; 119 | min-width: 100%; 120 | margin: 0; 121 | border-radius: 3px; 122 | background-color: #eee; 123 | box-sizing: border-box; 124 | } 125 | 126 | p.inline { display: inline; } 127 | p.secondary { color: #555; } 128 | 129 | div.group { 130 | display: flex; 131 | align-items: stretch; 132 | } 133 | div.group > * { width: auto; } 134 | div.group > *.fill { flex: 1; } 135 | div.group > *:first-child { border-radius: 3px 0 0 3px; } 136 | div.group > *:last-child { border-radius: 0 3px 3px 0; } 137 | 138 | .t { color: dodgerblue; } 139 | .f { color: tomato; } 140 | 141 | span.github { 142 | display: inline-block; 143 | width: 24px; 144 | height: 24px; 145 | background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMGMtNi42MjYgMC0xMiA1LjM3My0xMiAxMiAwIDUuMzAyIDMuNDM4IDkuOCA4LjIwNyAxMS4zODcuNTk5LjExMS43OTMtLjI2MS43OTMtLjU3N3YtMi4yMzRjLTMuMzM4LjcyNi00LjAzMy0xLjQxNi00LjAzMy0xLjQxNi0uNTQ2LTEuMzg3LTEuMzMzLTEuNzU2LTEuMzMzLTEuNzU2LTEuMDg5LS43NDUuMDgzLS43MjkuMDgzLS43MjkgMS4yMDUuMDg0IDEuODM5IDEuMjM3IDEuODM5IDEuMjM3IDEuMDcgMS44MzQgMi44MDcgMS4zMDQgMy40OTIuOTk3LjEwNy0uNzc1LjQxOC0xLjMwNS43NjItMS42MDQtMi42NjUtLjMwNS01LjQ2Ny0xLjMzNC01LjQ2Ny01LjkzMSAwLTEuMzExLjQ2OS0yLjM4MSAxLjIzNi0zLjIyMS0uMTI0LS4zMDMtLjUzNS0xLjUyNC4xMTctMy4xNzYgMCAwIDEuMDA4LS4zMjIgMy4zMDEgMS4yMy45NTctLjI2NiAxLjk4My0uMzk5IDMuMDAzLS40MDQgMS4wMi4wMDUgMi4wNDcuMTM4IDMuMDA2LjQwNCAyLjI5MS0xLjU1MiAzLjI5Ny0xLjIzIDMuMjk3LTEuMjMuNjUzIDEuNjUzLjI0MiAyLjg3NC4xMTggMy4xNzYuNzcuODQgMS4yMzUgMS45MTEgMS4yMzUgMy4yMjEgMCA0LjYwOS0yLjgwNyA1LjYyNC01LjQ3OSA1LjkyMS40My4zNzIuODIzIDEuMTAyLjgyMyAyLjIyMnYzLjI5M2MwIC4zMTkuMTkyLjY5NC44MDEuNTc2IDQuNzY1LTEuNTg5IDguMTk5LTYuMDg2IDguMTk5LTExLjM4NiAwLTYuNjI3LTUuMzczLTEyLTEyLTEyeiIvPjwvc3ZnPg=='); 146 | } 147 | -------------------------------------------------------------------------------- /src/web/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | type Store = { 4 | pushSupport: boolean; 5 | serviceWorkerRegistration: ServiceWorkerRegistration | null; 6 | pushSubscription: PushSubscription | null; 7 | } 8 | 9 | type Elements = { 10 | currentUserId: HTMLParagraphElement | null; 11 | notificationPermission: HTMLParagraphElement | null; 12 | pushSupport: HTMLParagraphElement | null; 13 | registration: HTMLParagraphElement | null; 14 | subscription: HTMLPreElement | null; 15 | sendStatus: HTMLParagraphElement | null; 16 | message: HTMLInputElement | null; 17 | targetUserId: HTMLInputElement | null; 18 | } 19 | 20 | const userId = localStorage.getItem('userId'); 21 | const elements: Elements = { 22 | // Status text 23 | currentUserId: null, 24 | registration: null, 25 | pushSupport: null, 26 | notificationPermission: null, 27 | subscription: null, 28 | sendStatus: null, 29 | // Inputs 30 | message: null, 31 | targetUserId: null, 32 | }; 33 | 34 | const store: Store = { 35 | pushSupport: false, 36 | serviceWorkerRegistration: null, 37 | pushSubscription: null, 38 | }; 39 | 40 | if (!userId) location.href = '/login.html'; 41 | 42 | async function registerServiceWorker () { 43 | if (!('serviceWorker' in navigator)) return; 44 | 45 | let registration = await navigator.serviceWorker.getRegistration(); 46 | if (!registration) { 47 | registration = await navigator.serviceWorker.register('/service-worker.js'); 48 | } 49 | 50 | store.serviceWorkerRegistration = registration ?? null; 51 | store.pushSupport = !!registration?.pushManager; 52 | store.pushSubscription = await registration?.pushManager?.getSubscription(); 53 | 54 | updateStatus(); 55 | } 56 | 57 | async function postSubscription (subscription?: PushSubscription) { 58 | console.log('postSubscription', { subscription }); 59 | 60 | if (!subscription) { 61 | showAlert('postSubscription - subscription cannot be empty'); 62 | return; 63 | } 64 | 65 | const response = await fetch('/subscription', { 66 | method: 'POST', 67 | headers: { 68 | 'Content-Type': 'application/json', 69 | }, 70 | body: JSON.stringify({ userId, subscription }), 71 | }); 72 | 73 | console.log('postSubscription', { response }); 74 | } 75 | 76 | async function deleteSubscription () { 77 | const response = await fetch('/subscription', { 78 | method: 'DELETE', 79 | headers: { 80 | 'Content-Type': 'application/json', 81 | }, 82 | body: JSON.stringify({ userId }), 83 | }); 84 | 85 | console.log('deleteSubscription', { response }); 86 | } 87 | 88 | async function subscribe () { 89 | if (store.pushSubscription) { 90 | showAlert('subscribe - already subscribed'); 91 | return; 92 | } 93 | 94 | try { 95 | const response = await fetch('/vapid-public-key'); 96 | const vapidPublicKey = await response.text(); 97 | console.log('subscribe', { vapidPublicKey }); 98 | 99 | const registration = store.serviceWorkerRegistration; 100 | 101 | if (!registration) { 102 | showAlert('subscribe - service worker is not registered'); 103 | return; 104 | } 105 | 106 | const subscription = await registration.pushManager.subscribe({ 107 | applicationServerKey: vapidPublicKey, 108 | userVisibleOnly: true, 109 | }); 110 | store.pushSubscription = subscription; 111 | await postSubscription(subscription); 112 | } catch (error) { 113 | console.error('subscribe', { error }); 114 | } finally { 115 | updateStatus(); 116 | } 117 | } 118 | 119 | async function unsubscribe () { 120 | const subscription = store.pushSubscription; 121 | 122 | if (!subscription) { 123 | showAlert('unsubscribe - push subscription not exist'); 124 | return; 125 | } 126 | 127 | try { 128 | const unsubscribed = await subscription.unsubscribe(); 129 | store.pushSubscription = null; 130 | console.log('unsubscribe', { unsubscribed }); 131 | await deleteSubscription(); 132 | } catch (error) { 133 | console.error('unsubscribe', { error }); 134 | } finally { 135 | updateStatus(); 136 | } 137 | } 138 | 139 | async function sendPushNotification () { 140 | const targetId = elements.targetUserId?.value; 141 | const message = elements.message?.value ?? ''; 142 | console.log('sendPushNotification', { targetId, message }); 143 | 144 | if (!targetId) { 145 | showAlert('Target userId cannot be empty'); 146 | return; 147 | } 148 | 149 | const response = await fetch('/send-push-notification', { 150 | method: 'POST', 151 | headers: { 152 | 'Content-Type': 'application/json', 153 | }, 154 | body: JSON.stringify({ targetId, message }), 155 | }); 156 | 157 | console.log('sendPushNotification', { response }); 158 | setText(elements.sendStatus, `(${response.status}) ${response.statusText} / ${new Date()}`); 159 | } 160 | 161 | function setText (element: HTMLElement | null, value: string | boolean) { 162 | if (!element) return; 163 | element.textContent = value.toString(); 164 | element.classList.remove('t'); 165 | element.classList.remove('f'); 166 | typeof value === 'boolean' && element.classList.add(value ? 't' : 'f'); 167 | } 168 | 169 | async function updateStatus () { 170 | setText(elements.currentUserId, userId as string); 171 | setText(elements.registration, !!store.serviceWorkerRegistration); 172 | setText(elements.pushSupport, store.pushSupport); 173 | setText(elements.notificationPermission, Notification.permission); 174 | setText(elements.subscription, JSON.stringify(store.pushSubscription, null, 2)); 175 | } 176 | 177 | function showAlert(message: string) { 178 | console.warn(message); 179 | alert(message); 180 | } 181 | 182 | function logout () { 183 | localStorage.removeItem('userId'); 184 | location.href = '/login.html'; 185 | } 186 | 187 | window.onload = () => { 188 | elements.currentUserId = document.getElementById('current_user_id') as HTMLParagraphElement; 189 | elements.registration = document.getElementById('registration_status') as HTMLParagraphElement; 190 | elements.pushSupport = document.getElementById('push_support_status') as HTMLParagraphElement; 191 | elements.notificationPermission = document.getElementById('notification_permission_status') as HTMLParagraphElement; 192 | elements.subscription = document.getElementById('subscription') as HTMLPreElement; 193 | elements.sendStatus = document.getElementById('send_status') as HTMLParagraphElement; 194 | elements.message = document.getElementById('message') as HTMLInputElement; 195 | elements.targetUserId = document.getElementById('target_user_id') as HTMLInputElement; 196 | updateStatus(); 197 | } 198 | 199 | registerServiceWorker(); 200 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | 12 | /* Language and Environment */ 13 | "target": "es2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 14 | "lib": [ 15 | "webworker", 16 | "es6", 17 | "scripthost" 18 | ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 19 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 20 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 25 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 29 | 30 | /* Modules */ 31 | // "module": "None", /* Specify what module code is generated. */ 32 | // "rootDir": "./", /* Specify the root folder within your source files. */ 33 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 34 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 35 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 36 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 37 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 38 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 39 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 40 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 41 | // "resolveJsonModule": true, /* Enable importing .json files. */ 42 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 43 | 44 | /* JavaScript Support */ 45 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 46 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 47 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 48 | 49 | /* Emit */ 50 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 51 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 52 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 53 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 54 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 55 | // "removeComments": true, /* Disable emitting comments. */ 56 | // "noEmit": true, /* Disable emitting files from a compilation. */ 57 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 58 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 59 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 60 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 63 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 64 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 65 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 66 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 67 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 68 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 69 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 70 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 71 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 72 | 73 | /* Interop Constraints */ 74 | "isolatedModules": false, /* Ensure that each file can be safely transpiled without relying on other imports. */ 75 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 76 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 77 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 78 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 79 | 80 | /* Type Checking */ 81 | "strict": true, /* Enable all strict type-checking options. */ 82 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 83 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 84 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 85 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 86 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 87 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 88 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 89 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 90 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 91 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 92 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 93 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 94 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 95 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 96 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 97 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 98 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 99 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 100 | 101 | /* Completeness */ 102 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 103 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 104 | }, 105 | "exclude": ["node_modules"] 106 | } -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/body-parser@*": 6 | version "1.19.2" 7 | resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" 8 | integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== 9 | dependencies: 10 | "@types/connect" "*" 11 | "@types/node" "*" 12 | 13 | "@types/config@^0.0.41": 14 | version "0.0.41" 15 | resolved "https://registry.yarnpkg.com/@types/config/-/config-0.0.41.tgz#c8237ab09730380801f3643beaefa077ca5f3c28" 16 | integrity sha512-HjXUmIld0gwvyG8MU/17QtLzOyuMX4jbGuijmS9sWsob5xxgZ/hY9cbRCaHIHqTQ3HMLhwS3F8uXq3Bt9zgzHA== 17 | 18 | "@types/connect@*": 19 | version "3.4.35" 20 | resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" 21 | integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== 22 | dependencies: 23 | "@types/node" "*" 24 | 25 | "@types/express-serve-static-core@^4.17.18": 26 | version "4.17.28" 27 | resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" 28 | integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== 29 | dependencies: 30 | "@types/node" "*" 31 | "@types/qs" "*" 32 | "@types/range-parser" "*" 33 | 34 | "@types/express@^4.17.13": 35 | version "4.17.13" 36 | resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" 37 | integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== 38 | dependencies: 39 | "@types/body-parser" "*" 40 | "@types/express-serve-static-core" "^4.17.18" 41 | "@types/qs" "*" 42 | "@types/serve-static" "*" 43 | 44 | "@types/mime@^1": 45 | version "1.3.2" 46 | resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" 47 | integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== 48 | 49 | "@types/node@*": 50 | version "17.0.42" 51 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.42.tgz#d7e8f22700efc94d125103075c074396b5f41f9b" 52 | integrity sha512-Q5BPGyGKcvQgAMbsr7qEGN/kIPN6zZecYYABeTDBizOsau+2NMdSVTar9UQw21A2+JyA2KRNDYaYrPB0Rpk2oQ== 53 | 54 | "@types/qs@*": 55 | version "6.9.7" 56 | resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" 57 | integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== 58 | 59 | "@types/range-parser@*": 60 | version "1.2.4" 61 | resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" 62 | integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== 63 | 64 | "@types/serve-static@*": 65 | version "1.13.10" 66 | resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" 67 | integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== 68 | dependencies: 69 | "@types/mime" "^1" 70 | "@types/node" "*" 71 | 72 | "@types/web-push@^3.3.2": 73 | version "3.3.2" 74 | resolved "https://registry.yarnpkg.com/@types/web-push/-/web-push-3.3.2.tgz#8c32434147c0396415862e86405c9edc9c50fc15" 75 | integrity sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw== 76 | dependencies: 77 | "@types/node" "*" 78 | 79 | accepts@~1.3.8: 80 | version "1.3.8" 81 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 82 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 83 | dependencies: 84 | mime-types "~2.1.34" 85 | negotiator "0.6.3" 86 | 87 | agent-base@6: 88 | version "6.0.2" 89 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" 90 | integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== 91 | dependencies: 92 | debug "4" 93 | 94 | array-flatten@1.1.1: 95 | version "1.1.1" 96 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 97 | integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== 98 | 99 | asn1.js@^5.3.0: 100 | version "5.4.1" 101 | resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" 102 | integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== 103 | dependencies: 104 | bn.js "^4.0.0" 105 | inherits "^2.0.1" 106 | minimalistic-assert "^1.0.0" 107 | safer-buffer "^2.1.0" 108 | 109 | bn.js@^4.0.0: 110 | version "4.12.0" 111 | resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" 112 | integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== 113 | 114 | body-parser@1.20.0: 115 | version "1.20.0" 116 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" 117 | integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== 118 | dependencies: 119 | bytes "3.1.2" 120 | content-type "~1.0.4" 121 | debug "2.6.9" 122 | depd "2.0.0" 123 | destroy "1.2.0" 124 | http-errors "2.0.0" 125 | iconv-lite "0.4.24" 126 | on-finished "2.4.1" 127 | qs "6.10.3" 128 | raw-body "2.5.1" 129 | type-is "~1.6.18" 130 | unpipe "1.0.0" 131 | 132 | buffer-equal-constant-time@1.0.1: 133 | version "1.0.1" 134 | resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" 135 | integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== 136 | 137 | bytes@3.1.2: 138 | version "3.1.2" 139 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 140 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 141 | 142 | call-bind@^1.0.0: 143 | version "1.0.2" 144 | resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" 145 | integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== 146 | dependencies: 147 | function-bind "^1.1.1" 148 | get-intrinsic "^1.0.2" 149 | 150 | colors@^1.4.0: 151 | version "1.4.0" 152 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" 153 | integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== 154 | 155 | config@^3.3.7: 156 | version "3.3.7" 157 | resolved "https://registry.yarnpkg.com/config/-/config-3.3.7.tgz#4310410dc2bf4e0effdca21a12a4035860a24ee4" 158 | integrity sha512-mX/n7GKDYZMqvvkY6e6oBY49W8wxdmQt+ho/5lhwFDXqQW9gI+Ahp8EKp8VAbISPnmf2+Bv5uZK7lKXZ6pf1aA== 159 | dependencies: 160 | json5 "^2.1.1" 161 | 162 | content-disposition@0.5.4: 163 | version "0.5.4" 164 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 165 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 166 | dependencies: 167 | safe-buffer "5.2.1" 168 | 169 | content-type@~1.0.4: 170 | version "1.0.4" 171 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 172 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 173 | 174 | cookie-signature@1.0.6: 175 | version "1.0.6" 176 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 177 | integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== 178 | 179 | cookie@0.5.0: 180 | version "0.5.0" 181 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" 182 | integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 183 | 184 | debug@2.6.9: 185 | version "2.6.9" 186 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 187 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 188 | dependencies: 189 | ms "2.0.0" 190 | 191 | debug@4: 192 | version "4.3.4" 193 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 194 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 195 | dependencies: 196 | ms "2.1.2" 197 | 198 | depd@2.0.0: 199 | version "2.0.0" 200 | resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 201 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 202 | 203 | destroy@1.2.0: 204 | version "1.2.0" 205 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 206 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 207 | 208 | ecdsa-sig-formatter@1.0.11: 209 | version "1.0.11" 210 | resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" 211 | integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== 212 | dependencies: 213 | safe-buffer "^5.0.1" 214 | 215 | ee-first@1.1.1: 216 | version "1.1.1" 217 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 218 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 219 | 220 | encodeurl@~1.0.2: 221 | version "1.0.2" 222 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 223 | integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 224 | 225 | escape-html@~1.0.3: 226 | version "1.0.3" 227 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 228 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 229 | 230 | etag@~1.8.1: 231 | version "1.8.1" 232 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 233 | integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== 234 | 235 | express@^4.18.1: 236 | version "4.18.1" 237 | resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" 238 | integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== 239 | dependencies: 240 | accepts "~1.3.8" 241 | array-flatten "1.1.1" 242 | body-parser "1.20.0" 243 | content-disposition "0.5.4" 244 | content-type "~1.0.4" 245 | cookie "0.5.0" 246 | cookie-signature "1.0.6" 247 | debug "2.6.9" 248 | depd "2.0.0" 249 | encodeurl "~1.0.2" 250 | escape-html "~1.0.3" 251 | etag "~1.8.1" 252 | finalhandler "1.2.0" 253 | fresh "0.5.2" 254 | http-errors "2.0.0" 255 | merge-descriptors "1.0.1" 256 | methods "~1.1.2" 257 | on-finished "2.4.1" 258 | parseurl "~1.3.3" 259 | path-to-regexp "0.1.7" 260 | proxy-addr "~2.0.7" 261 | qs "6.10.3" 262 | range-parser "~1.2.1" 263 | safe-buffer "5.2.1" 264 | send "0.18.0" 265 | serve-static "1.15.0" 266 | setprototypeof "1.2.0" 267 | statuses "2.0.1" 268 | type-is "~1.6.18" 269 | utils-merge "1.0.1" 270 | vary "~1.1.2" 271 | 272 | finalhandler@1.2.0: 273 | version "1.2.0" 274 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" 275 | integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== 276 | dependencies: 277 | debug "2.6.9" 278 | encodeurl "~1.0.2" 279 | escape-html "~1.0.3" 280 | on-finished "2.4.1" 281 | parseurl "~1.3.3" 282 | statuses "2.0.1" 283 | unpipe "~1.0.0" 284 | 285 | forwarded@0.2.0: 286 | version "0.2.0" 287 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 288 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 289 | 290 | fresh@0.5.2: 291 | version "0.5.2" 292 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 293 | integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== 294 | 295 | function-bind@^1.1.1: 296 | version "1.1.1" 297 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 298 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 299 | 300 | get-intrinsic@^1.0.2: 301 | version "1.1.2" 302 | resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" 303 | integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== 304 | dependencies: 305 | function-bind "^1.1.1" 306 | has "^1.0.3" 307 | has-symbols "^1.0.3" 308 | 309 | has-symbols@^1.0.3: 310 | version "1.0.3" 311 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 312 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 313 | 314 | has@^1.0.3: 315 | version "1.0.3" 316 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 317 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 318 | dependencies: 319 | function-bind "^1.1.1" 320 | 321 | http-errors@2.0.0: 322 | version "2.0.0" 323 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 324 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 325 | dependencies: 326 | depd "2.0.0" 327 | inherits "2.0.4" 328 | setprototypeof "1.2.0" 329 | statuses "2.0.1" 330 | toidentifier "1.0.1" 331 | 332 | http_ece@1.1.0: 333 | version "1.1.0" 334 | resolved "https://registry.yarnpkg.com/http_ece/-/http_ece-1.1.0.tgz#74780c6eb32d8ddfe9e36a83abcd81fe0cd4fb75" 335 | integrity sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA== 336 | dependencies: 337 | urlsafe-base64 "~1.0.0" 338 | 339 | https-proxy-agent@^5.0.0: 340 | version "5.0.1" 341 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" 342 | integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== 343 | dependencies: 344 | agent-base "6" 345 | debug "4" 346 | 347 | iconv-lite@0.4.24: 348 | version "0.4.24" 349 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 350 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 351 | dependencies: 352 | safer-buffer ">= 2.1.2 < 3" 353 | 354 | inherits@2.0.4, inherits@^2.0.1: 355 | version "2.0.4" 356 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 357 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 358 | 359 | ipaddr.js@1.9.1: 360 | version "1.9.1" 361 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 362 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 363 | 364 | json5@^2.1.1: 365 | version "2.2.1" 366 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" 367 | integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== 368 | 369 | jwa@^2.0.0: 370 | version "2.0.0" 371 | resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" 372 | integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== 373 | dependencies: 374 | buffer-equal-constant-time "1.0.1" 375 | ecdsa-sig-formatter "1.0.11" 376 | safe-buffer "^5.0.1" 377 | 378 | jws@^4.0.0: 379 | version "4.0.0" 380 | resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" 381 | integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== 382 | dependencies: 383 | jwa "^2.0.0" 384 | safe-buffer "^5.0.1" 385 | 386 | media-typer@0.3.0: 387 | version "0.3.0" 388 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 389 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== 390 | 391 | merge-descriptors@1.0.1: 392 | version "1.0.1" 393 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 394 | integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== 395 | 396 | methods@~1.1.2: 397 | version "1.1.2" 398 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 399 | integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== 400 | 401 | mime-db@1.52.0: 402 | version "1.52.0" 403 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 404 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 405 | 406 | mime-types@~2.1.24, mime-types@~2.1.34: 407 | version "2.1.35" 408 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 409 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 410 | dependencies: 411 | mime-db "1.52.0" 412 | 413 | mime@1.6.0: 414 | version "1.6.0" 415 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 416 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 417 | 418 | minimalistic-assert@^1.0.0: 419 | version "1.0.1" 420 | resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" 421 | integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== 422 | 423 | minimist@^1.2.5: 424 | version "1.2.6" 425 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" 426 | integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== 427 | 428 | ms@2.0.0: 429 | version "2.0.0" 430 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 431 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== 432 | 433 | ms@2.1.2: 434 | version "2.1.2" 435 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 436 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 437 | 438 | ms@2.1.3: 439 | version "2.1.3" 440 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 441 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 442 | 443 | negotiator@0.6.3: 444 | version "0.6.3" 445 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 446 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 447 | 448 | object-inspect@^1.9.0: 449 | version "1.12.2" 450 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" 451 | integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== 452 | 453 | on-finished@2.4.1: 454 | version "2.4.1" 455 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 456 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 457 | dependencies: 458 | ee-first "1.1.1" 459 | 460 | parseurl@~1.3.3: 461 | version "1.3.3" 462 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 463 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 464 | 465 | path-to-regexp@0.1.7: 466 | version "0.1.7" 467 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 468 | integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== 469 | 470 | proxy-addr@~2.0.7: 471 | version "2.0.7" 472 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 473 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 474 | dependencies: 475 | forwarded "0.2.0" 476 | ipaddr.js "1.9.1" 477 | 478 | qs@6.10.3: 479 | version "6.10.3" 480 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" 481 | integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== 482 | dependencies: 483 | side-channel "^1.0.4" 484 | 485 | range-parser@~1.2.1: 486 | version "1.2.1" 487 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 488 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 489 | 490 | raw-body@2.5.1: 491 | version "2.5.1" 492 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" 493 | integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== 494 | dependencies: 495 | bytes "3.1.2" 496 | http-errors "2.0.0" 497 | iconv-lite "0.4.24" 498 | unpipe "1.0.0" 499 | 500 | safe-buffer@5.2.1, safe-buffer@^5.0.1: 501 | version "5.2.1" 502 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 503 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 504 | 505 | "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: 506 | version "2.1.2" 507 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 508 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 509 | 510 | send@0.18.0: 511 | version "0.18.0" 512 | resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" 513 | integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== 514 | dependencies: 515 | debug "2.6.9" 516 | depd "2.0.0" 517 | destroy "1.2.0" 518 | encodeurl "~1.0.2" 519 | escape-html "~1.0.3" 520 | etag "~1.8.1" 521 | fresh "0.5.2" 522 | http-errors "2.0.0" 523 | mime "1.6.0" 524 | ms "2.1.3" 525 | on-finished "2.4.1" 526 | range-parser "~1.2.1" 527 | statuses "2.0.1" 528 | 529 | serve-static@1.15.0: 530 | version "1.15.0" 531 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" 532 | integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== 533 | dependencies: 534 | encodeurl "~1.0.2" 535 | escape-html "~1.0.3" 536 | parseurl "~1.3.3" 537 | send "0.18.0" 538 | 539 | setprototypeof@1.2.0: 540 | version "1.2.0" 541 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 542 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 543 | 544 | side-channel@^1.0.4: 545 | version "1.0.4" 546 | resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" 547 | integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== 548 | dependencies: 549 | call-bind "^1.0.0" 550 | get-intrinsic "^1.0.2" 551 | object-inspect "^1.9.0" 552 | 553 | statuses@2.0.1: 554 | version "2.0.1" 555 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 556 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 557 | 558 | toidentifier@1.0.1: 559 | version "1.0.1" 560 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 561 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 562 | 563 | type-is@~1.6.18: 564 | version "1.6.18" 565 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 566 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 567 | dependencies: 568 | media-typer "0.3.0" 569 | mime-types "~2.1.24" 570 | 571 | typescript@^4.7.3: 572 | version "4.7.3" 573 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d" 574 | integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA== 575 | 576 | unpipe@1.0.0, unpipe@~1.0.0: 577 | version "1.0.0" 578 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 579 | integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 580 | 581 | urlsafe-base64@^1.0.0, urlsafe-base64@~1.0.0: 582 | version "1.0.0" 583 | resolved "https://registry.yarnpkg.com/urlsafe-base64/-/urlsafe-base64-1.0.0.tgz#23f89069a6c62f46cf3a1d3b00169cefb90be0c6" 584 | integrity sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA== 585 | 586 | utils-merge@1.0.1: 587 | version "1.0.1" 588 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 589 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== 590 | 591 | vary@~1.1.2: 592 | version "1.1.2" 593 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 594 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 595 | 596 | web-push@^3.5.0: 597 | version "3.5.0" 598 | resolved "https://registry.yarnpkg.com/web-push/-/web-push-3.5.0.tgz#4576533746052eda3bd50414b54a1b0a21eeaeae" 599 | integrity sha512-JC0V9hzKTqlDYJ+LTZUXtW7B175qwwaqzbbMSWDxHWxZvd3xY0C2rcotMGDavub2nAAFw+sXTsqR65/KY2A5AQ== 600 | dependencies: 601 | asn1.js "^5.3.0" 602 | http_ece "1.1.0" 603 | https-proxy-agent "^5.0.0" 604 | jws "^4.0.0" 605 | minimist "^1.2.5" 606 | urlsafe-base64 "^1.0.0" 607 | --------------------------------------------------------------------------------