├── .env.template ├── .envrc ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── README.md ├── app.js ├── config.js ├── index.js ├── package.json ├── pnpm-lock.yaml ├── src ├── api │ ├── auth.js │ ├── data.js │ └── index.js ├── middleware │ └── logger.js ├── persistance │ ├── mongo │ │ ├── mongo-persistance.js │ │ └── mongo-schema.js │ ├── mysql │ │ └── mysql-persistance.js │ ├── persister-factories.js │ └── postgres │ │ └── postgres-persistance.js └── utils │ └── generate-key.js └── tsconfig.json /.env.template: -------------------------------------------------------------------------------- 1 | POWERSYNC_PRIVATE_KEY= 2 | POWERSYNC_PUBLIC_KEY= 3 | POWERSYNC_URL= 4 | PORT= 5 | JWT_ISSUER= 6 | # Either 'mongodb', 'mysql' or 'postgres'. This defaults to Postgres 7 | DATABASE_TYPE= 8 | DATABASE_URI= 9 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | layout node 2 | use node 3 | [ -f .env ] && dotenv -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .idea 4 | error.log 5 | combined.log 6 | *.tsbuildinfo 7 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.9.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore all node_modules 2 | **/node_modules/** 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "printWidth": 120, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node.js 20 Docker image as base 2 | FROM node:20 3 | 4 | ENV DATABASE_URI= 5 | # Either 'mongodb' or 'postgres'. This defaults to Postgres 6 | ENV DATABASE_TYPE= 7 | ENV POWERSYNC_PRIVATE_KEY= 8 | ENV POWERSYNC_PUBLIC_KEY= 9 | ENV POWERSYNC_URL= 10 | ENV PORT= 11 | ENV JWT_ISSUER= 12 | 13 | # Set the working directory inside the container 14 | 15 | RUN npm install -g pnpm@9 16 | 17 | WORKDIR /app 18 | 19 | # Copy the package.json and package-lock.json files to the container 20 | COPY package*.json ./ 21 | COPY pnpm-lock*.yaml ./ 22 | 23 | # Install dependencies 24 | RUN pnpm install --frozen-lockfile 25 | 26 | # Copy the rest of the demo launcher code to the container 27 | COPY / ./ 28 | 29 | # Command to run the application 30 | CMD ["pnpm", "start"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerSync Node.js Backend: Todo List Demo 2 | 3 | ## Overview 4 | 5 | This repo contains a demo Node.js server application which has HTTP endpoints to authorize a [PowerSync](https://www.powersync.com/) enabled application to sync data between a client device and a PostgreSQL or MongoDB database. 6 | 7 | The endpoints are as follows: 8 | 9 | 1. GET `/api/auth/token` 10 | 11 | - PowerSync uses this endpoint to retrieve a JWT access token which is used for authentication. 12 | 13 | 2. GET `/api/auth/keys` 14 | 15 | - PowerSync uses this endpoint to validate the JWT returned from the endpoint above. 16 | 17 | 3. PUT `/api/data` 18 | 19 | - PowerSync uses this endpoint to sync upsert events that occurred on the client application. 20 | 21 | 4. PATCH `/api/data` 22 | 23 | - PowerSync uses this endpoint to sync update events that occurred on the client application. 24 | 25 | 5. DELETE `/api/data` 26 | 27 | - PowerSync uses this endpoint to sync delete events that occurred on the client application. 28 | 29 | ## Packages 30 | 31 | [node-postgres](https://github.com/brianc/node-postgres) is used to interact with the Postgres database when a client performs requests to the `/api/data` endpoint. 32 | 33 | [mongodb](https://www.npmjs.com/package/mongodb) is used to interact with the MongoDB database when a client performs requests to the `/api/data` endpoint. 34 | 35 | [mysql2](https://www.npmjs.com/package/mysql2) is used to interact with the MySQL database when a client performs requests to the `/api/data` endpoint. 36 | 37 | [jose](https://github.com/panva/jose) is used to sign the JWT which PowerSync uses for authorization. 38 | 39 | ## Requirements 40 | 41 | This app needs a Postgres instance that's hosted. For a free version for testing/demo purposes, visit [Supabase](https://supabase.com/). 42 | 43 | ## Running the app 44 | 45 | 1. Clone the repository 46 | 2. Follow the steps outlined in [PowerSync Custom Authentication Example](https://github.com/journeyapps/powersync-jwks-example) → [Generate a key-pair](https://github.com/journeyapps/powersync-jwks-example#1-generate-a-key-pair) to get the keys you need for this app. This is an easy way to get started with this demo app. You can use your own public/private keys as well. Note: This backend will generate a temporary key pair for development purposes if the keys are not present in the `.env` file. This should not be used in production. 47 | 3. Create a new `.env` file in the root project directory and add the variables as defined in the `.env` file: 48 | 49 | ```shell 50 | cp .env.template .env 51 | ``` 52 | 53 | 4. Install dependancies 54 | 55 | ```shell 56 | nvm use 57 | ``` 58 | 59 | ```shell 60 | pnpm install 61 | ``` 62 | 63 | ## Start App 64 | 65 | 1. Run the following to start the application 66 | 67 | ```shell 68 | pnpm start 69 | ``` 70 | 71 | This will start the app on `http://127.0.0.1:PORT`, where PORT is what you specify in your `.env` file. 72 | 73 | 2. Test if the app is working by opening `http://127.0.0.1:PORT/api/auth/token/` in the browser 74 | 75 | 3. You should get a JSON object as the response to that request 76 | 77 | ## Connecting the app with PowerSync 78 | 79 | This process is only designed for demo/testing purposes, and is not intended for production use. You won't be using ngrok to host your application and database. 80 | 81 | 1. Download and install [ngrok](https://ngrok.com/) 82 | 2. Run the ngrok command to create a HTTPS tunnel to your local application 83 | 84 | ```shell 85 | ngrok http 8000 86 | ``` 87 | 88 | This should create the tunnel and a new HTTPS URL should be availible e.g. 89 | 90 | ```shell 91 | ngrok by @inconshreveable (Ctrl+C to quit) 92 | 93 | Session Status online 94 | Account Michael Barnes (Plan: Free) 95 | Update update available (version 2.3.41, Ctrl-U to update) 96 | Version 2.3.40 97 | Region United States (us) 98 | Web Interface http://127.0.0.1:4040 99 | Forwarding http://your_id.ngrok-free.app -> http://localhost:8000 100 | Forwarding https://your_id.ngrok-free.app -> http://localhost:8000 101 | 102 | Connections ttl opn rt1 rt5 p50 p90 103 | 1957 0 0.04 0.03 0.01 89.93 104 | ``` 105 | 106 | 3. Open the [PowerSync Dashboard](https://powersync.journeyapps.com/) and paste the `Forwarding` URL starting with HTTPS into the Credentials tab of your PowerSync instance e.g. 107 | 108 | ``` 109 | JWKS URI 110 | https://your_id.ngrok-free.app/api/auth/keys/ 111 | ``` 112 | 113 | Pay special attention to the URL, it should include the `/api/auth/keys/` path as this is used by the PowerSync server to validate tokens. 114 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import { apiRouter } from './src/api/index.js'; 4 | import logRequest from './src/middleware/logger.js'; 5 | import config from './config.js'; 6 | 7 | const app = express(); 8 | 9 | app.use(bodyParser.json()); 10 | app.use(logRequest); 11 | 12 | app.use((req, res, next) => { 13 | // Website you wish to allow to connect 14 | res.setHeader('Access-Control-Allow-Origin', '*'); 15 | 16 | // Request methods you wish to allow 17 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); 18 | 19 | // Request headers you wish to allow 20 | res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); 21 | 22 | // Pass to next layer of middleware 23 | next(); 24 | }); 25 | try { 26 | app.get('/', (req, res) => { 27 | res.status(200).send({ 28 | message: 'powersync-nodejs-backend-todolist-demo' 29 | }); 30 | }); 31 | app.use('/api', apiRouter); 32 | } catch (err) { 33 | console.log('Unexpected error', err); 34 | } 35 | 36 | export default app; 37 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | const config = { 4 | port: process.env.PORT ? parseInt(process.env.PORT) : 6060, 5 | database: { 6 | type: process.env.DATABASE_TYPE || 'postgres', 7 | uri: process.env.DATABASE_URI 8 | }, 9 | powersync: { 10 | url: process.env.POWERSYNC_URL, 11 | publicKey: process.env.POWERSYNC_PUBLIC_KEY, 12 | privateKey: process.env.POWERSYNC_PRIVATE_KEY, 13 | jwtIssuer: process.env.JWT_ISSUER 14 | } 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import app from './app.js'; 2 | import config from './config.js'; 3 | 4 | const PORT = process.env.PORT || config.port; 5 | 6 | app.listen(PORT, () => { 7 | console.log(`Server is running @ http://127.0.0.1:${PORT}`); 8 | }); 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "powersync-nodejs-backend-todolist-demo", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "author": "Mike Barnes ", 7 | "scripts": { 8 | "start": "node index.js", 9 | "format": "prettier --write .", 10 | "check": "tsc -b" 11 | }, 12 | "dependencies": { 13 | "body-parser": "^1.20.2", 14 | "dotenv": "^16.3.1", 15 | "express": "^4.18.2", 16 | "jose": "^5.3.0", 17 | "mongodb": "^6.9.0", 18 | "mysql2": "^3.11.3", 19 | "pg": "^8.11.3", 20 | "winston": "^3.11.0" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^22.7.5", 24 | "prettier": "^3.2.4", 25 | "typescript": "^5.6.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | body-parser: 12 | specifier: ^1.20.2 13 | version: 1.20.3 14 | dotenv: 15 | specifier: ^16.3.1 16 | version: 16.4.5 17 | express: 18 | specifier: ^4.18.2 19 | version: 4.21.0 20 | jose: 21 | specifier: ^5.3.0 22 | version: 5.9.2 23 | mongodb: 24 | specifier: ^6.9.0 25 | version: 6.9.0 26 | mysql2: 27 | specifier: ^3.11.3 28 | version: 3.11.3 29 | pg: 30 | specifier: ^8.11.3 31 | version: 8.13.0 32 | winston: 33 | specifier: ^3.11.0 34 | version: 3.14.2 35 | devDependencies: 36 | '@types/node': 37 | specifier: ^22.7.5 38 | version: 22.7.5 39 | prettier: 40 | specifier: ^3.2.4 41 | version: 3.3.3 42 | typescript: 43 | specifier: ^5.6.2 44 | version: 5.6.2 45 | 46 | packages: 47 | 48 | '@colors/colors@1.6.0': 49 | resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} 50 | engines: {node: '>=0.1.90'} 51 | 52 | '@dabh/diagnostics@2.0.3': 53 | resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} 54 | 55 | '@mongodb-js/saslprep@1.1.9': 56 | resolution: {integrity: sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==} 57 | 58 | '@types/node@22.7.5': 59 | resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} 60 | 61 | '@types/triple-beam@1.3.5': 62 | resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} 63 | 64 | '@types/webidl-conversions@7.0.3': 65 | resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} 66 | 67 | '@types/whatwg-url@11.0.5': 68 | resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} 69 | 70 | accepts@1.3.8: 71 | resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} 72 | engines: {node: '>= 0.6'} 73 | 74 | array-flatten@1.1.1: 75 | resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} 76 | 77 | async@3.2.6: 78 | resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} 79 | 80 | aws-ssl-profiles@1.1.2: 81 | resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} 82 | engines: {node: '>= 6.0.0'} 83 | 84 | body-parser@1.20.3: 85 | resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} 86 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 87 | 88 | bson@6.8.0: 89 | resolution: {integrity: sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==} 90 | engines: {node: '>=16.20.1'} 91 | 92 | bytes@3.1.2: 93 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 94 | engines: {node: '>= 0.8'} 95 | 96 | call-bind@1.0.7: 97 | resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} 98 | engines: {node: '>= 0.4'} 99 | 100 | color-convert@1.9.3: 101 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 102 | 103 | color-name@1.1.3: 104 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 105 | 106 | color-name@1.1.4: 107 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 108 | 109 | color-string@1.9.1: 110 | resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} 111 | 112 | color@3.2.1: 113 | resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} 114 | 115 | colorspace@1.1.4: 116 | resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} 117 | 118 | content-disposition@0.5.4: 119 | resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} 120 | engines: {node: '>= 0.6'} 121 | 122 | content-type@1.0.5: 123 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 124 | engines: {node: '>= 0.6'} 125 | 126 | cookie-signature@1.0.6: 127 | resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} 128 | 129 | cookie@0.6.0: 130 | resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} 131 | engines: {node: '>= 0.6'} 132 | 133 | debug@2.6.9: 134 | resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} 135 | peerDependencies: 136 | supports-color: '*' 137 | peerDependenciesMeta: 138 | supports-color: 139 | optional: true 140 | 141 | define-data-property@1.1.4: 142 | resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} 143 | engines: {node: '>= 0.4'} 144 | 145 | denque@2.1.0: 146 | resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} 147 | engines: {node: '>=0.10'} 148 | 149 | depd@2.0.0: 150 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 151 | engines: {node: '>= 0.8'} 152 | 153 | destroy@1.2.0: 154 | resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} 155 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 156 | 157 | dotenv@16.4.5: 158 | resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} 159 | engines: {node: '>=12'} 160 | 161 | ee-first@1.1.1: 162 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 163 | 164 | enabled@2.0.0: 165 | resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} 166 | 167 | encodeurl@1.0.2: 168 | resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} 169 | engines: {node: '>= 0.8'} 170 | 171 | encodeurl@2.0.0: 172 | resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} 173 | engines: {node: '>= 0.8'} 174 | 175 | es-define-property@1.0.0: 176 | resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} 177 | engines: {node: '>= 0.4'} 178 | 179 | es-errors@1.3.0: 180 | resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 181 | engines: {node: '>= 0.4'} 182 | 183 | escape-html@1.0.3: 184 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 185 | 186 | etag@1.8.1: 187 | resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 188 | engines: {node: '>= 0.6'} 189 | 190 | express@4.21.0: 191 | resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==} 192 | engines: {node: '>= 0.10.0'} 193 | 194 | fecha@4.2.3: 195 | resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} 196 | 197 | finalhandler@1.3.1: 198 | resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} 199 | engines: {node: '>= 0.8'} 200 | 201 | fn.name@1.1.0: 202 | resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} 203 | 204 | forwarded@0.2.0: 205 | resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 206 | engines: {node: '>= 0.6'} 207 | 208 | fresh@0.5.2: 209 | resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} 210 | engines: {node: '>= 0.6'} 211 | 212 | function-bind@1.1.2: 213 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 214 | 215 | generate-function@2.3.1: 216 | resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} 217 | 218 | get-intrinsic@1.2.4: 219 | resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} 220 | engines: {node: '>= 0.4'} 221 | 222 | gopd@1.0.1: 223 | resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} 224 | 225 | has-property-descriptors@1.0.2: 226 | resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} 227 | 228 | has-proto@1.0.3: 229 | resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} 230 | engines: {node: '>= 0.4'} 231 | 232 | has-symbols@1.0.3: 233 | resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} 234 | engines: {node: '>= 0.4'} 235 | 236 | hasown@2.0.2: 237 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 238 | engines: {node: '>= 0.4'} 239 | 240 | http-errors@2.0.0: 241 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 242 | engines: {node: '>= 0.8'} 243 | 244 | iconv-lite@0.4.24: 245 | resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} 246 | engines: {node: '>=0.10.0'} 247 | 248 | iconv-lite@0.6.3: 249 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 250 | engines: {node: '>=0.10.0'} 251 | 252 | inherits@2.0.4: 253 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 254 | 255 | ipaddr.js@1.9.1: 256 | resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 257 | engines: {node: '>= 0.10'} 258 | 259 | is-arrayish@0.3.2: 260 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 261 | 262 | is-property@1.0.2: 263 | resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} 264 | 265 | is-stream@2.0.1: 266 | resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} 267 | engines: {node: '>=8'} 268 | 269 | jose@5.9.2: 270 | resolution: {integrity: sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ==} 271 | 272 | kuler@2.0.0: 273 | resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} 274 | 275 | logform@2.6.1: 276 | resolution: {integrity: sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==} 277 | engines: {node: '>= 12.0.0'} 278 | 279 | long@5.2.3: 280 | resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} 281 | 282 | lru-cache@7.18.3: 283 | resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} 284 | engines: {node: '>=12'} 285 | 286 | lru.min@1.1.1: 287 | resolution: {integrity: sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==} 288 | engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} 289 | 290 | media-typer@0.3.0: 291 | resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} 292 | engines: {node: '>= 0.6'} 293 | 294 | memory-pager@1.5.0: 295 | resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} 296 | 297 | merge-descriptors@1.0.3: 298 | resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} 299 | 300 | methods@1.1.2: 301 | resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} 302 | engines: {node: '>= 0.6'} 303 | 304 | mime-db@1.52.0: 305 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 306 | engines: {node: '>= 0.6'} 307 | 308 | mime-types@2.1.35: 309 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 310 | engines: {node: '>= 0.6'} 311 | 312 | mime@1.6.0: 313 | resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} 314 | engines: {node: '>=4'} 315 | hasBin: true 316 | 317 | mongodb-connection-string-url@3.0.1: 318 | resolution: {integrity: sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==} 319 | 320 | mongodb@6.9.0: 321 | resolution: {integrity: sha512-UMopBVx1LmEUbW/QE0Hw18u583PEDVQmUmVzzBRH0o/xtE9DBRA5ZYLOjpLIa03i8FXjzvQECJcqoMvCXftTUA==} 322 | engines: {node: '>=16.20.1'} 323 | peerDependencies: 324 | '@aws-sdk/credential-providers': ^3.188.0 325 | '@mongodb-js/zstd': ^1.1.0 326 | gcp-metadata: ^5.2.0 327 | kerberos: ^2.0.1 328 | mongodb-client-encryption: '>=6.0.0 <7' 329 | snappy: ^7.2.2 330 | socks: ^2.7.1 331 | peerDependenciesMeta: 332 | '@aws-sdk/credential-providers': 333 | optional: true 334 | '@mongodb-js/zstd': 335 | optional: true 336 | gcp-metadata: 337 | optional: true 338 | kerberos: 339 | optional: true 340 | mongodb-client-encryption: 341 | optional: true 342 | snappy: 343 | optional: true 344 | socks: 345 | optional: true 346 | 347 | ms@2.0.0: 348 | resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} 349 | 350 | ms@2.1.3: 351 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 352 | 353 | mysql2@3.11.3: 354 | resolution: {integrity: sha512-Qpu2ADfbKzyLdwC/5d4W7+5Yz7yBzCU05YWt5npWzACST37wJsB23wgOSo00qi043urkiRwXtEvJc9UnuLX/MQ==} 355 | engines: {node: '>= 8.0'} 356 | 357 | named-placeholders@1.1.3: 358 | resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} 359 | engines: {node: '>=12.0.0'} 360 | 361 | negotiator@0.6.3: 362 | resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} 363 | engines: {node: '>= 0.6'} 364 | 365 | object-inspect@1.13.2: 366 | resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} 367 | engines: {node: '>= 0.4'} 368 | 369 | on-finished@2.4.1: 370 | resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 371 | engines: {node: '>= 0.8'} 372 | 373 | one-time@1.0.0: 374 | resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} 375 | 376 | parseurl@1.3.3: 377 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 378 | engines: {node: '>= 0.8'} 379 | 380 | path-to-regexp@0.1.10: 381 | resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} 382 | 383 | pg-cloudflare@1.1.1: 384 | resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} 385 | 386 | pg-connection-string@2.7.0: 387 | resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} 388 | 389 | pg-int8@1.0.1: 390 | resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} 391 | engines: {node: '>=4.0.0'} 392 | 393 | pg-pool@3.7.0: 394 | resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==} 395 | peerDependencies: 396 | pg: '>=8.0' 397 | 398 | pg-protocol@1.7.0: 399 | resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} 400 | 401 | pg-types@2.2.0: 402 | resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} 403 | engines: {node: '>=4'} 404 | 405 | pg@8.13.0: 406 | resolution: {integrity: sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==} 407 | engines: {node: '>= 8.0.0'} 408 | peerDependencies: 409 | pg-native: '>=3.0.1' 410 | peerDependenciesMeta: 411 | pg-native: 412 | optional: true 413 | 414 | pgpass@1.0.5: 415 | resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} 416 | 417 | postgres-array@2.0.0: 418 | resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} 419 | engines: {node: '>=4'} 420 | 421 | postgres-bytea@1.0.0: 422 | resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} 423 | engines: {node: '>=0.10.0'} 424 | 425 | postgres-date@1.0.7: 426 | resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} 427 | engines: {node: '>=0.10.0'} 428 | 429 | postgres-interval@1.2.0: 430 | resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} 431 | engines: {node: '>=0.10.0'} 432 | 433 | prettier@3.3.3: 434 | resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} 435 | engines: {node: '>=14'} 436 | hasBin: true 437 | 438 | proxy-addr@2.0.7: 439 | resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 440 | engines: {node: '>= 0.10'} 441 | 442 | punycode@2.3.1: 443 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 444 | engines: {node: '>=6'} 445 | 446 | qs@6.13.0: 447 | resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} 448 | engines: {node: '>=0.6'} 449 | 450 | range-parser@1.2.1: 451 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 452 | engines: {node: '>= 0.6'} 453 | 454 | raw-body@2.5.2: 455 | resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} 456 | engines: {node: '>= 0.8'} 457 | 458 | readable-stream@3.6.2: 459 | resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} 460 | engines: {node: '>= 6'} 461 | 462 | safe-buffer@5.2.1: 463 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 464 | 465 | safe-stable-stringify@2.5.0: 466 | resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} 467 | engines: {node: '>=10'} 468 | 469 | safer-buffer@2.1.2: 470 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 471 | 472 | send@0.19.0: 473 | resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} 474 | engines: {node: '>= 0.8.0'} 475 | 476 | seq-queue@0.0.5: 477 | resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} 478 | 479 | serve-static@1.16.2: 480 | resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} 481 | engines: {node: '>= 0.8.0'} 482 | 483 | set-function-length@1.2.2: 484 | resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} 485 | engines: {node: '>= 0.4'} 486 | 487 | setprototypeof@1.2.0: 488 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 489 | 490 | side-channel@1.0.6: 491 | resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} 492 | engines: {node: '>= 0.4'} 493 | 494 | simple-swizzle@0.2.2: 495 | resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} 496 | 497 | sparse-bitfield@3.0.3: 498 | resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} 499 | 500 | split2@4.2.0: 501 | resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} 502 | engines: {node: '>= 10.x'} 503 | 504 | sqlstring@2.3.3: 505 | resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} 506 | engines: {node: '>= 0.6'} 507 | 508 | stack-trace@0.0.10: 509 | resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} 510 | 511 | statuses@2.0.1: 512 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 513 | engines: {node: '>= 0.8'} 514 | 515 | string_decoder@1.3.0: 516 | resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 517 | 518 | text-hex@1.0.0: 519 | resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} 520 | 521 | toidentifier@1.0.1: 522 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 523 | engines: {node: '>=0.6'} 524 | 525 | tr46@4.1.1: 526 | resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} 527 | engines: {node: '>=14'} 528 | 529 | triple-beam@1.4.1: 530 | resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} 531 | engines: {node: '>= 14.0.0'} 532 | 533 | type-is@1.6.18: 534 | resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} 535 | engines: {node: '>= 0.6'} 536 | 537 | typescript@5.6.2: 538 | resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} 539 | engines: {node: '>=14.17'} 540 | hasBin: true 541 | 542 | undici-types@6.19.8: 543 | resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 544 | 545 | unpipe@1.0.0: 546 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 547 | engines: {node: '>= 0.8'} 548 | 549 | util-deprecate@1.0.2: 550 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 551 | 552 | utils-merge@1.0.1: 553 | resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} 554 | engines: {node: '>= 0.4.0'} 555 | 556 | vary@1.1.2: 557 | resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 558 | engines: {node: '>= 0.8'} 559 | 560 | webidl-conversions@7.0.0: 561 | resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} 562 | engines: {node: '>=12'} 563 | 564 | whatwg-url@13.0.0: 565 | resolution: {integrity: sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==} 566 | engines: {node: '>=16'} 567 | 568 | winston-transport@4.7.1: 569 | resolution: {integrity: sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==} 570 | engines: {node: '>= 12.0.0'} 571 | 572 | winston@3.14.2: 573 | resolution: {integrity: sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==} 574 | engines: {node: '>= 12.0.0'} 575 | 576 | xtend@4.0.2: 577 | resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} 578 | engines: {node: '>=0.4'} 579 | 580 | snapshots: 581 | 582 | '@colors/colors@1.6.0': {} 583 | 584 | '@dabh/diagnostics@2.0.3': 585 | dependencies: 586 | colorspace: 1.1.4 587 | enabled: 2.0.0 588 | kuler: 2.0.0 589 | 590 | '@mongodb-js/saslprep@1.1.9': 591 | dependencies: 592 | sparse-bitfield: 3.0.3 593 | 594 | '@types/node@22.7.5': 595 | dependencies: 596 | undici-types: 6.19.8 597 | 598 | '@types/triple-beam@1.3.5': {} 599 | 600 | '@types/webidl-conversions@7.0.3': {} 601 | 602 | '@types/whatwg-url@11.0.5': 603 | dependencies: 604 | '@types/webidl-conversions': 7.0.3 605 | 606 | accepts@1.3.8: 607 | dependencies: 608 | mime-types: 2.1.35 609 | negotiator: 0.6.3 610 | 611 | array-flatten@1.1.1: {} 612 | 613 | async@3.2.6: {} 614 | 615 | aws-ssl-profiles@1.1.2: {} 616 | 617 | body-parser@1.20.3: 618 | dependencies: 619 | bytes: 3.1.2 620 | content-type: 1.0.5 621 | debug: 2.6.9 622 | depd: 2.0.0 623 | destroy: 1.2.0 624 | http-errors: 2.0.0 625 | iconv-lite: 0.4.24 626 | on-finished: 2.4.1 627 | qs: 6.13.0 628 | raw-body: 2.5.2 629 | type-is: 1.6.18 630 | unpipe: 1.0.0 631 | transitivePeerDependencies: 632 | - supports-color 633 | 634 | bson@6.8.0: {} 635 | 636 | bytes@3.1.2: {} 637 | 638 | call-bind@1.0.7: 639 | dependencies: 640 | es-define-property: 1.0.0 641 | es-errors: 1.3.0 642 | function-bind: 1.1.2 643 | get-intrinsic: 1.2.4 644 | set-function-length: 1.2.2 645 | 646 | color-convert@1.9.3: 647 | dependencies: 648 | color-name: 1.1.3 649 | 650 | color-name@1.1.3: {} 651 | 652 | color-name@1.1.4: {} 653 | 654 | color-string@1.9.1: 655 | dependencies: 656 | color-name: 1.1.4 657 | simple-swizzle: 0.2.2 658 | 659 | color@3.2.1: 660 | dependencies: 661 | color-convert: 1.9.3 662 | color-string: 1.9.1 663 | 664 | colorspace@1.1.4: 665 | dependencies: 666 | color: 3.2.1 667 | text-hex: 1.0.0 668 | 669 | content-disposition@0.5.4: 670 | dependencies: 671 | safe-buffer: 5.2.1 672 | 673 | content-type@1.0.5: {} 674 | 675 | cookie-signature@1.0.6: {} 676 | 677 | cookie@0.6.0: {} 678 | 679 | debug@2.6.9: 680 | dependencies: 681 | ms: 2.0.0 682 | 683 | define-data-property@1.1.4: 684 | dependencies: 685 | es-define-property: 1.0.0 686 | es-errors: 1.3.0 687 | gopd: 1.0.1 688 | 689 | denque@2.1.0: {} 690 | 691 | depd@2.0.0: {} 692 | 693 | destroy@1.2.0: {} 694 | 695 | dotenv@16.4.5: {} 696 | 697 | ee-first@1.1.1: {} 698 | 699 | enabled@2.0.0: {} 700 | 701 | encodeurl@1.0.2: {} 702 | 703 | encodeurl@2.0.0: {} 704 | 705 | es-define-property@1.0.0: 706 | dependencies: 707 | get-intrinsic: 1.2.4 708 | 709 | es-errors@1.3.0: {} 710 | 711 | escape-html@1.0.3: {} 712 | 713 | etag@1.8.1: {} 714 | 715 | express@4.21.0: 716 | dependencies: 717 | accepts: 1.3.8 718 | array-flatten: 1.1.1 719 | body-parser: 1.20.3 720 | content-disposition: 0.5.4 721 | content-type: 1.0.5 722 | cookie: 0.6.0 723 | cookie-signature: 1.0.6 724 | debug: 2.6.9 725 | depd: 2.0.0 726 | encodeurl: 2.0.0 727 | escape-html: 1.0.3 728 | etag: 1.8.1 729 | finalhandler: 1.3.1 730 | fresh: 0.5.2 731 | http-errors: 2.0.0 732 | merge-descriptors: 1.0.3 733 | methods: 1.1.2 734 | on-finished: 2.4.1 735 | parseurl: 1.3.3 736 | path-to-regexp: 0.1.10 737 | proxy-addr: 2.0.7 738 | qs: 6.13.0 739 | range-parser: 1.2.1 740 | safe-buffer: 5.2.1 741 | send: 0.19.0 742 | serve-static: 1.16.2 743 | setprototypeof: 1.2.0 744 | statuses: 2.0.1 745 | type-is: 1.6.18 746 | utils-merge: 1.0.1 747 | vary: 1.1.2 748 | transitivePeerDependencies: 749 | - supports-color 750 | 751 | fecha@4.2.3: {} 752 | 753 | finalhandler@1.3.1: 754 | dependencies: 755 | debug: 2.6.9 756 | encodeurl: 2.0.0 757 | escape-html: 1.0.3 758 | on-finished: 2.4.1 759 | parseurl: 1.3.3 760 | statuses: 2.0.1 761 | unpipe: 1.0.0 762 | transitivePeerDependencies: 763 | - supports-color 764 | 765 | fn.name@1.1.0: {} 766 | 767 | forwarded@0.2.0: {} 768 | 769 | fresh@0.5.2: {} 770 | 771 | function-bind@1.1.2: {} 772 | 773 | generate-function@2.3.1: 774 | dependencies: 775 | is-property: 1.0.2 776 | 777 | get-intrinsic@1.2.4: 778 | dependencies: 779 | es-errors: 1.3.0 780 | function-bind: 1.1.2 781 | has-proto: 1.0.3 782 | has-symbols: 1.0.3 783 | hasown: 2.0.2 784 | 785 | gopd@1.0.1: 786 | dependencies: 787 | get-intrinsic: 1.2.4 788 | 789 | has-property-descriptors@1.0.2: 790 | dependencies: 791 | es-define-property: 1.0.0 792 | 793 | has-proto@1.0.3: {} 794 | 795 | has-symbols@1.0.3: {} 796 | 797 | hasown@2.0.2: 798 | dependencies: 799 | function-bind: 1.1.2 800 | 801 | http-errors@2.0.0: 802 | dependencies: 803 | depd: 2.0.0 804 | inherits: 2.0.4 805 | setprototypeof: 1.2.0 806 | statuses: 2.0.1 807 | toidentifier: 1.0.1 808 | 809 | iconv-lite@0.4.24: 810 | dependencies: 811 | safer-buffer: 2.1.2 812 | 813 | iconv-lite@0.6.3: 814 | dependencies: 815 | safer-buffer: 2.1.2 816 | 817 | inherits@2.0.4: {} 818 | 819 | ipaddr.js@1.9.1: {} 820 | 821 | is-arrayish@0.3.2: {} 822 | 823 | is-property@1.0.2: {} 824 | 825 | is-stream@2.0.1: {} 826 | 827 | jose@5.9.2: {} 828 | 829 | kuler@2.0.0: {} 830 | 831 | logform@2.6.1: 832 | dependencies: 833 | '@colors/colors': 1.6.0 834 | '@types/triple-beam': 1.3.5 835 | fecha: 4.2.3 836 | ms: 2.1.3 837 | safe-stable-stringify: 2.5.0 838 | triple-beam: 1.4.1 839 | 840 | long@5.2.3: {} 841 | 842 | lru-cache@7.18.3: {} 843 | 844 | lru.min@1.1.1: {} 845 | 846 | media-typer@0.3.0: {} 847 | 848 | memory-pager@1.5.0: {} 849 | 850 | merge-descriptors@1.0.3: {} 851 | 852 | methods@1.1.2: {} 853 | 854 | mime-db@1.52.0: {} 855 | 856 | mime-types@2.1.35: 857 | dependencies: 858 | mime-db: 1.52.0 859 | 860 | mime@1.6.0: {} 861 | 862 | mongodb-connection-string-url@3.0.1: 863 | dependencies: 864 | '@types/whatwg-url': 11.0.5 865 | whatwg-url: 13.0.0 866 | 867 | mongodb@6.9.0: 868 | dependencies: 869 | '@mongodb-js/saslprep': 1.1.9 870 | bson: 6.8.0 871 | mongodb-connection-string-url: 3.0.1 872 | 873 | ms@2.0.0: {} 874 | 875 | ms@2.1.3: {} 876 | 877 | mysql2@3.11.3: 878 | dependencies: 879 | aws-ssl-profiles: 1.1.2 880 | denque: 2.1.0 881 | generate-function: 2.3.1 882 | iconv-lite: 0.6.3 883 | long: 5.2.3 884 | lru.min: 1.1.1 885 | named-placeholders: 1.1.3 886 | seq-queue: 0.0.5 887 | sqlstring: 2.3.3 888 | 889 | named-placeholders@1.1.3: 890 | dependencies: 891 | lru-cache: 7.18.3 892 | 893 | negotiator@0.6.3: {} 894 | 895 | object-inspect@1.13.2: {} 896 | 897 | on-finished@2.4.1: 898 | dependencies: 899 | ee-first: 1.1.1 900 | 901 | one-time@1.0.0: 902 | dependencies: 903 | fn.name: 1.1.0 904 | 905 | parseurl@1.3.3: {} 906 | 907 | path-to-regexp@0.1.10: {} 908 | 909 | pg-cloudflare@1.1.1: 910 | optional: true 911 | 912 | pg-connection-string@2.7.0: {} 913 | 914 | pg-int8@1.0.1: {} 915 | 916 | pg-pool@3.7.0(pg@8.13.0): 917 | dependencies: 918 | pg: 8.13.0 919 | 920 | pg-protocol@1.7.0: {} 921 | 922 | pg-types@2.2.0: 923 | dependencies: 924 | pg-int8: 1.0.1 925 | postgres-array: 2.0.0 926 | postgres-bytea: 1.0.0 927 | postgres-date: 1.0.7 928 | postgres-interval: 1.2.0 929 | 930 | pg@8.13.0: 931 | dependencies: 932 | pg-connection-string: 2.7.0 933 | pg-pool: 3.7.0(pg@8.13.0) 934 | pg-protocol: 1.7.0 935 | pg-types: 2.2.0 936 | pgpass: 1.0.5 937 | optionalDependencies: 938 | pg-cloudflare: 1.1.1 939 | 940 | pgpass@1.0.5: 941 | dependencies: 942 | split2: 4.2.0 943 | 944 | postgres-array@2.0.0: {} 945 | 946 | postgres-bytea@1.0.0: {} 947 | 948 | postgres-date@1.0.7: {} 949 | 950 | postgres-interval@1.2.0: 951 | dependencies: 952 | xtend: 4.0.2 953 | 954 | prettier@3.3.3: {} 955 | 956 | proxy-addr@2.0.7: 957 | dependencies: 958 | forwarded: 0.2.0 959 | ipaddr.js: 1.9.1 960 | 961 | punycode@2.3.1: {} 962 | 963 | qs@6.13.0: 964 | dependencies: 965 | side-channel: 1.0.6 966 | 967 | range-parser@1.2.1: {} 968 | 969 | raw-body@2.5.2: 970 | dependencies: 971 | bytes: 3.1.2 972 | http-errors: 2.0.0 973 | iconv-lite: 0.4.24 974 | unpipe: 1.0.0 975 | 976 | readable-stream@3.6.2: 977 | dependencies: 978 | inherits: 2.0.4 979 | string_decoder: 1.3.0 980 | util-deprecate: 1.0.2 981 | 982 | safe-buffer@5.2.1: {} 983 | 984 | safe-stable-stringify@2.5.0: {} 985 | 986 | safer-buffer@2.1.2: {} 987 | 988 | send@0.19.0: 989 | dependencies: 990 | debug: 2.6.9 991 | depd: 2.0.0 992 | destroy: 1.2.0 993 | encodeurl: 1.0.2 994 | escape-html: 1.0.3 995 | etag: 1.8.1 996 | fresh: 0.5.2 997 | http-errors: 2.0.0 998 | mime: 1.6.0 999 | ms: 2.1.3 1000 | on-finished: 2.4.1 1001 | range-parser: 1.2.1 1002 | statuses: 2.0.1 1003 | transitivePeerDependencies: 1004 | - supports-color 1005 | 1006 | seq-queue@0.0.5: {} 1007 | 1008 | serve-static@1.16.2: 1009 | dependencies: 1010 | encodeurl: 2.0.0 1011 | escape-html: 1.0.3 1012 | parseurl: 1.3.3 1013 | send: 0.19.0 1014 | transitivePeerDependencies: 1015 | - supports-color 1016 | 1017 | set-function-length@1.2.2: 1018 | dependencies: 1019 | define-data-property: 1.1.4 1020 | es-errors: 1.3.0 1021 | function-bind: 1.1.2 1022 | get-intrinsic: 1.2.4 1023 | gopd: 1.0.1 1024 | has-property-descriptors: 1.0.2 1025 | 1026 | setprototypeof@1.2.0: {} 1027 | 1028 | side-channel@1.0.6: 1029 | dependencies: 1030 | call-bind: 1.0.7 1031 | es-errors: 1.3.0 1032 | get-intrinsic: 1.2.4 1033 | object-inspect: 1.13.2 1034 | 1035 | simple-swizzle@0.2.2: 1036 | dependencies: 1037 | is-arrayish: 0.3.2 1038 | 1039 | sparse-bitfield@3.0.3: 1040 | dependencies: 1041 | memory-pager: 1.5.0 1042 | 1043 | split2@4.2.0: {} 1044 | 1045 | sqlstring@2.3.3: {} 1046 | 1047 | stack-trace@0.0.10: {} 1048 | 1049 | statuses@2.0.1: {} 1050 | 1051 | string_decoder@1.3.0: 1052 | dependencies: 1053 | safe-buffer: 5.2.1 1054 | 1055 | text-hex@1.0.0: {} 1056 | 1057 | toidentifier@1.0.1: {} 1058 | 1059 | tr46@4.1.1: 1060 | dependencies: 1061 | punycode: 2.3.1 1062 | 1063 | triple-beam@1.4.1: {} 1064 | 1065 | type-is@1.6.18: 1066 | dependencies: 1067 | media-typer: 0.3.0 1068 | mime-types: 2.1.35 1069 | 1070 | typescript@5.6.2: {} 1071 | 1072 | undici-types@6.19.8: {} 1073 | 1074 | unpipe@1.0.0: {} 1075 | 1076 | util-deprecate@1.0.2: {} 1077 | 1078 | utils-merge@1.0.1: {} 1079 | 1080 | vary@1.1.2: {} 1081 | 1082 | webidl-conversions@7.0.0: {} 1083 | 1084 | whatwg-url@13.0.0: 1085 | dependencies: 1086 | tr46: 4.1.1 1087 | webidl-conversions: 7.0.0 1088 | 1089 | winston-transport@4.7.1: 1090 | dependencies: 1091 | logform: 2.6.1 1092 | readable-stream: 3.6.2 1093 | triple-beam: 1.4.1 1094 | 1095 | winston@3.14.2: 1096 | dependencies: 1097 | '@colors/colors': 1.6.0 1098 | '@dabh/diagnostics': 2.0.3 1099 | async: 3.2.6 1100 | is-stream: 2.0.1 1101 | logform: 2.6.1 1102 | one-time: 1.0.0 1103 | readable-stream: 3.6.2 1104 | safe-stable-stringify: 2.5.0 1105 | stack-trace: 0.0.10 1106 | triple-beam: 1.4.1 1107 | winston-transport: 4.7.1 1108 | 1109 | xtend@4.0.2: {} 1110 | -------------------------------------------------------------------------------- /src/api/auth.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { SignJWT, importJWK } from 'jose'; 3 | import config from '../../config.js'; 4 | import { generateKeyPair } from '../utils/generate-key.js'; 5 | const router = express.Router(); 6 | 7 | /** 8 | * Imported Jose keys 9 | */ 10 | const keys = { 11 | privateKey: null, 12 | publicKey: null 13 | }; 14 | 15 | /** 16 | * Generates a key pair if none is available on the ENV 17 | */ 18 | async function ensureKeys() { 19 | // Keys are loaded already 20 | if (keys.privateKey) { 21 | return; 22 | } 23 | 24 | const { powersync } = config; 25 | const base64Keys = { 26 | private: powersync.privateKey, 27 | public: powersync.publicKey 28 | }; 29 | 30 | if (!base64Keys.private) { 31 | // Key is not present in ENV 32 | console.warn( 33 | `Private key has not been supplied in process.env.POWERSYNC_PRIVATE_KEY. A temporary key pair will be generated.` 34 | ); 35 | const generated = await generateKeyPair(); 36 | base64Keys.private = generated.privateBase64; 37 | base64Keys.public = generated.publicBase64; 38 | } 39 | 40 | const decodedPrivateKey = Buffer.from(base64Keys.private, 'base64'); 41 | const powerSyncPrivateKey = JSON.parse(new TextDecoder().decode(decodedPrivateKey)); 42 | keys.privateKey = { 43 | alg: powerSyncPrivateKey.alg, 44 | kid: powerSyncPrivateKey.kid, 45 | key: await importJWK(powerSyncPrivateKey) 46 | }; 47 | 48 | const decodedPublicKey = Buffer.from(base64Keys.public, 'base64'); 49 | keys.publicKey = JSON.parse(new TextDecoder().decode(decodedPublicKey)); 50 | } 51 | 52 | /** 53 | * Get the JWT token that PowerSync will use to authenticate the user 54 | */ 55 | router.get('/token', async (req, res) => { 56 | await ensureKeys(); 57 | const powerSyncKey = keys.privateKey; 58 | 59 | const { user_id = 'UserID ' } = req.query; 60 | 61 | const token = await new SignJWT({}) 62 | .setProtectedHeader({ 63 | alg: powerSyncKey.alg, 64 | kid: powerSyncKey.kid 65 | }) 66 | .setSubject(user_id) 67 | .setIssuedAt() 68 | .setIssuer(config.powersync.jwtIssuer) 69 | .setAudience(config.powersync.url) 70 | .setExpirationTime('5m') 71 | .sign(powerSyncKey.key); 72 | res.send({ 73 | token: token, 74 | powersync_url: config.powersync.url 75 | }); 76 | }); 77 | 78 | /** 79 | * This is the JWKS endpoint PowerSync uses to handle authentication 80 | */ 81 | router.get('/keys', async (req, res) => { 82 | await ensureKeys(); 83 | const powerSyncPublicKey = keys.publicKey; 84 | res.send({ 85 | keys: [powerSyncPublicKey] 86 | }); 87 | }); 88 | 89 | export { router as authRouter }; 90 | -------------------------------------------------------------------------------- /src/api/data.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import config from '../../config.js'; 3 | import { factories } from '../persistance/persister-factories.js'; 4 | 5 | const router = express.Router(); 6 | 7 | const persistenceFactory = factories[config.database.type]; 8 | 9 | const { updateBatch, createCheckpoint } = await persistenceFactory(config.database.uri); 10 | 11 | /** 12 | * Handle a batch of events. 13 | */ 14 | router.post('/', async (req, res) => { 15 | if (!req.body) { 16 | res.status(400).send({ 17 | message: 'Invalid body provided' 18 | }); 19 | return; 20 | } 21 | 22 | try { 23 | await updateBatch(req.body.batch); 24 | 25 | res.status(200).send({ 26 | message: `Batch completed` 27 | }); 28 | } catch (e) { 29 | console.error('Request failed', e.stack); 30 | res.status(400).send({ 31 | message: `Request failed: ${e.message}` 32 | }); 33 | } 34 | }); 35 | 36 | /** 37 | * Handle all PUT events sent to the server by the client PowerSync application 38 | */ 39 | router.put('/', async (req, res) => { 40 | if (!req.body) { 41 | res.status(400).send({ 42 | message: 'Invalid body provided' 43 | }); 44 | return; 45 | } 46 | 47 | try { 48 | await updateBatch([{ op: 'PUT', table: req.body.table, data: req.body.data }]); 49 | 50 | res.status(200).send({ 51 | message: `PUT completed for ${req.body.table} ${req.body.data.id}` 52 | }); 53 | } catch (e) { 54 | console.error(e.stack ?? e.message); 55 | res.status(400).send({ 56 | message: `Request failed: ${e.message}` 57 | }); 58 | } 59 | }); 60 | 61 | router.put('/checkpoint', async (req, res) => { 62 | if (!req.body) { 63 | res.status(400).send({ 64 | message: 'Invalid body provided' 65 | }); 66 | return; 67 | } 68 | const { user_id = 'UserID', client_id = '1' } = req.body; 69 | 70 | const checkpoint = await createCheckpoint(user_id, client_id); 71 | 72 | res.status(200).send({ 73 | checkpoint 74 | }); 75 | }); 76 | 77 | /** 78 | * Handle all PATCH events sent to the server by the client PowerSync application 79 | */ 80 | router.patch('/', async (req, res) => { 81 | if (!req.body) { 82 | res.status(400).send({ 83 | message: 'Invalid body provided' 84 | }); 85 | return; 86 | } 87 | 88 | try { 89 | await updateBatch([{ op: 'PATCH', table: req.body.table, data: req.body.data }]); 90 | 91 | res.status(200).send({ 92 | message: `PATCH completed for ${req.body.table}` 93 | }); 94 | } catch (e) { 95 | console.error(e.stack ?? e.message); 96 | res.status(400).send({ 97 | message: `Request failed: ${e.message}` 98 | }); 99 | } 100 | }); 101 | 102 | /** 103 | * Handle all DELETE events sent to the server by the client PowerSync application 104 | */ 105 | router.delete('/', async (req, res) => { 106 | if (!req.body) { 107 | res.status(400).send({ 108 | message: 'Invalid body provided' 109 | }); 110 | return; 111 | } 112 | 113 | const table = req.body.table; 114 | const data = req.body.data; 115 | 116 | if (!table || !data?.id) { 117 | res.status(400).send({ 118 | message: 'Invalid body provided, expected table and data' 119 | }); 120 | return; 121 | } 122 | 123 | await updateBatch([{ op: 'DELETE', table: table, data: data }]); 124 | 125 | res.status(200).send({ 126 | message: `DELETE completed for ${table} ${data.id}` 127 | }); 128 | }); 129 | 130 | export { router as dataRouter }; 131 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { authRouter } from './auth.js'; 3 | import { dataRouter } from './data.js'; 4 | 5 | const router = express.Router(); 6 | 7 | router.use('/auth', authRouter); 8 | router.use('/data', dataRouter); 9 | 10 | export { router as apiRouter }; 11 | -------------------------------------------------------------------------------- /src/middleware/logger.js: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | 3 | const logger = winston.createLogger({ 4 | level: 'info', 5 | format: winston.format.combine( 6 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 7 | winston.format.printf(({ level, message, timestamp }) => { 8 | return `${timestamp} ${level}: ${message}`; 9 | }) 10 | ), 11 | transports: [ 12 | new winston.transports.Console(), // Log to console 13 | new winston.transports.File({ filename: 'error.log', level: 'error' }), 14 | new winston.transports.File({ filename: 'combined.log' }) 15 | ] 16 | }); 17 | 18 | const logRequest = (req, res, next) => { 19 | logger.info(`${req.method} ${req.url}`); 20 | next(); 21 | }; 22 | 23 | export default logRequest; 24 | -------------------------------------------------------------------------------- /src/persistance/mongo/mongo-persistance.js: -------------------------------------------------------------------------------- 1 | import * as mongo from 'mongodb'; 2 | import { applySchema, schema } from './mongo-schema.js'; 3 | 4 | /** 5 | * Creates a MongoDB batch persister. This is used by the 6 | * `data` api routes. 7 | * @param {string} uri MongoDB connection URI 8 | */ 9 | export const createMongoPersister = async (uri) => { 10 | console.debug('Using MongoDB Persister'); 11 | 12 | const client = new mongo.MongoClient(uri); 13 | const db = client.db(); 14 | await client.connect(); 15 | 16 | /** 17 | * @type {import('../persister-factories.js').Persister} 18 | */ 19 | const persister = { 20 | createCheckpoint: async (user_id, client_id) => { 21 | const doc = await db.collection('checkpoints').findOneAndUpdate( 22 | { 23 | user_id, 24 | client_id 25 | }, 26 | { 27 | $inc: { 28 | checkpoint: 1n 29 | } 30 | }, 31 | { upsert: true, returnDocument: 'after' } 32 | ); 33 | return doc.checkpoint; 34 | }, 35 | updateBatch: async (batch) => { 36 | // TODO: Use batches & transactions. 37 | // TODO: Do type conversion. This currently persists data from the client as is, 38 | // only using strings or numbers for all data. 39 | for (const op of batch) { 40 | const tableSchema = schema[op.table]; 41 | if (tableSchema == null) { 42 | console.warn(`Ignoring update to unknown table ${op.table}`); 43 | continue; 44 | } 45 | const collection = db.collection(op.table); 46 | if (op.op == 'PUT') { 47 | const data = op.data; 48 | const id = op.id ?? data.id; 49 | const doc = { _id: id, ...data }; 50 | delete doc.id; 51 | 52 | const converted = applySchema(tableSchema, doc); 53 | await collection.insertOne(converted); 54 | } else if (op.op == 'PATCH') { 55 | const data = op.data; 56 | const id = op.id ?? data.id; 57 | const doc = { ...data }; 58 | delete doc.id; 59 | 60 | const converted = applySchema(tableSchema, doc); 61 | await collection.updateOne({ _id: id }, { $set: converted }); 62 | } else if (op.op == 'DELETE') { 63 | const id = op.id ?? op.data?.id; 64 | if (id != null) { 65 | await collection.deleteOne({ _id: id }); 66 | } 67 | } 68 | } 69 | } 70 | }; 71 | 72 | return persister; 73 | }; 74 | -------------------------------------------------------------------------------- /src/persistance/mongo/mongo-schema.js: -------------------------------------------------------------------------------- 1 | export const types = { 2 | date: (v) => new Date(v), 3 | boolean: (v) => !!v, 4 | string: (v) => String(v), 5 | number: (v) => Number(v) 6 | }; 7 | 8 | export const schema = { 9 | lists: { 10 | _id: types.string, 11 | created_at: types.date, 12 | name: types.string, 13 | owner_id: types.string 14 | }, 15 | todos: { 16 | _id: types.string, 17 | completed: types.boolean, 18 | created_at: types.date, 19 | created_by: types.string, 20 | description: types.string, 21 | list_id: types.string, 22 | completed_at: types.date, 23 | completed_by: types.string 24 | } 25 | }; 26 | 27 | /** 28 | * A basic function to convert data according to a schema specified above. 29 | * 30 | * A production application should probably use a purpose-built library for this, 31 | * and use MongoDB Schema Validation to enforce the types in the database. 32 | */ 33 | export function applySchema(tableSchema, data) { 34 | const converted = Object.entries(tableSchema) 35 | .map(([key, converter]) => { 36 | const rawValue = data[key]; 37 | if (typeof rawValue == 'undefined') { 38 | return null; 39 | } else if (rawValue == null) { 40 | return [key, null]; 41 | } else { 42 | return [key, converter(rawValue)]; 43 | } 44 | }) 45 | .filter((v) => v != null); 46 | return Object.fromEntries(converted); 47 | } 48 | -------------------------------------------------------------------------------- /src/persistance/mysql/mysql-persistance.js: -------------------------------------------------------------------------------- 1 | import mysql from 'mysql2/promise'; 2 | 3 | function escapeIdentifier(identifier) { 4 | return `\`${identifier.replace(/`/g, '``').replace(/\./g, '`.`')}\``; 5 | } 6 | 7 | /** 8 | * Creates a MySQL batch persister. This is used by the 9 | * `data` API routes. 10 | * @param {string} uri MySQL connection URI 11 | */ 12 | export const createMySQLPersister = (uri) => { 13 | console.debug('Using MySQL Persister'); 14 | 15 | const pool = mysql.createPool(uri); 16 | /** 17 | * @type {import('../persister-factories.js').Persister} 18 | */ 19 | const persister = { 20 | async createCheckpoint(user_id, client_id) { 21 | const connection = await pool.getConnection(); 22 | try { 23 | await connection.beginTransaction(); 24 | await connection.query( 25 | ` 26 | INSERT INTO checkpoints 27 | (user_id, client_id, checkpoint) 28 | VALUES (?, ?, 1) 29 | ON DUPLICATE KEY UPDATE 30 | checkpoint = checkpoint + 1; 31 | `, 32 | [user_id, client_id] 33 | ); 34 | const [rows] = await connection.query( 35 | ` 36 | SELECT checkpoint FROM checkpoints WHERE user_id = ? AND client_id = ?; 37 | `, 38 | [user_id, client_id] 39 | ); 40 | 41 | await connection.commit(); 42 | /** 43 | * @type {bigint} 44 | */ 45 | const checkpoint = rows[0].checkpoint; 46 | return checkpoint; 47 | } catch (ex) { 48 | await connection.rollback(); 49 | } finally { 50 | connection.release(); 51 | } 52 | }, 53 | /** 54 | * @type {import('../persister-factories.js').BatchPersister} 55 | */ 56 | updateBatch: async (batch) => { 57 | const connection = await pool.getConnection(); 58 | try { 59 | await connection.beginTransaction(); 60 | 61 | for (let op of batch) { 62 | const table = escapeIdentifier(op.table); 63 | if (op.op === 'PUT') { 64 | const data = op.data; 65 | const with_id = { ...data, id: op.id ?? op.data.id }; 66 | 67 | const columnsEscaped = Object.keys(with_id).map(escapeIdentifier); 68 | const columnsJoined = columnsEscaped.join(', '); 69 | 70 | let updateClauses = []; 71 | 72 | for (let key of Object.keys(data)) { 73 | if (key === 'id') continue; 74 | updateClauses.push(`${escapeIdentifier(key)} = VALUES(${escapeIdentifier(key)})`); 75 | } 76 | 77 | const updateClause = updateClauses.length > 0 ? `ON DUPLICATE KEY UPDATE ${updateClauses.join(', ')}` : ``; 78 | 79 | const statement = ` 80 | INSERT INTO ${table} (${columnsJoined}) 81 | VALUES (${Object.keys(with_id) 82 | .map(() => '?') 83 | .join(', ')}) 84 | ${updateClause}`; 85 | 86 | await connection.execute(statement, Object.values(with_id)); 87 | } else if (op.op === 'PATCH') { 88 | const data = op.data; 89 | const with_id = { ...data, id: op.id ?? data.id }; 90 | 91 | let updateClauses = []; 92 | 93 | for (let key of Object.keys(data)) { 94 | if (key === 'id') continue; 95 | updateClauses.push(`${escapeIdentifier(key)} = ?`); 96 | } 97 | 98 | const statement = ` 99 | UPDATE ${table} 100 | SET ${updateClauses.join(', ')} 101 | WHERE id = ?`; 102 | 103 | const values = [...Object.values(data), with_id.id]; 104 | await connection.execute(statement, values); 105 | } else if (op.op === 'DELETE') { 106 | const id = op.id ?? op.data?.id; 107 | const statement = `DELETE FROM ${table} WHERE id = ?`; 108 | await connection.execute(statement, [id]); 109 | } 110 | } 111 | await connection.commit(); 112 | } catch (e) { 113 | await connection.rollback(); 114 | throw e; 115 | } finally { 116 | connection.release(); 117 | } 118 | } 119 | }; 120 | return persister; 121 | }; 122 | -------------------------------------------------------------------------------- /src/persistance/persister-factories.js: -------------------------------------------------------------------------------- 1 | import { createMongoPersister } from './mongo/mongo-persistance.js'; 2 | import { createMySQLPersister } from './mysql/mysql-persistance.js'; 3 | import { createPostgresPersister } from './postgres/postgres-persistance.js'; 4 | 5 | /** 6 | * Apply a batch of PUT, PATCH and/or DELETE updates. 7 | * 8 | * @typedef {Object} DeleteOp 9 | * @prop {"DELETE"} op - op type 10 | * @prop {string} table - table name 11 | * @prop {string=} id - record id 12 | * @prop {Object=} data - record data, including id (alternative to direct id) 13 | * 14 | * @typedef {Object} PutOp 15 | * @prop {"PUT"} op - op type 16 | * @prop {string} table - table name 17 | * @prop {string=} id - record id 18 | * @prop {Object} data - record data 19 | * 20 | * @typedef {Object} PatchOp 21 | * @prop {"PATCH"} op - op type 22 | * @prop {string} table - table name 23 | * @prop {string=} id - record id 24 | * @prop {Object} data - record data 25 | * 26 | * @callback BatchPersister 27 | * @param {(DeleteOp | PutOp | PatchOp)[]} batch 28 | * @returns {Promise} 29 | * 30 | * @callback CreateCheckpoint 31 | * @param {string} user_id 32 | * @param {string} client_id 33 | * @returns {Promise} checkpoint 34 | * 35 | * @typedef {Object} Persister 36 | * @prop {BatchPersister} updateBatch 37 | * @prop {CreateCheckpoint} createCheckpoint 38 | 39 | * @callback PersisterFactory 40 | * @param {string} URI - 41 | * @returns {Promise | Persister} 42 | */ 43 | 44 | /** 45 | * @type {Record} 46 | */ 47 | export const factories = { 48 | mongodb: createMongoPersister, 49 | postgres: createPostgresPersister, 50 | mysql: createMySQLPersister 51 | }; 52 | -------------------------------------------------------------------------------- /src/persistance/postgres/postgres-persistance.js: -------------------------------------------------------------------------------- 1 | import { URL } from 'url'; 2 | 3 | import PG from 'pg'; 4 | 5 | const { Pool } = PG; 6 | 7 | function escapeIdentifier(identifier) { 8 | return `"${identifier.replace(/"/g, '""').replace(/\./g, '"."')}"`; 9 | } 10 | 11 | /** 12 | * Creates a Postgres batch persister. This is used by the 13 | * `data` api routes. 14 | * @param {string} uri Postgres connection URI 15 | */ 16 | export const createPostgresPersister = (uri) => { 17 | console.debug('Using Postgres Persister'); 18 | 19 | const url = new URL(uri); 20 | 21 | const pool = new Pool({ 22 | host: url.hostname, 23 | database: url.pathname.split('/')[1], 24 | user: url.username, 25 | password: url.password, 26 | port: url.port 27 | }); 28 | 29 | pool.on('error', (err, client) => { 30 | console.error('Pool connection failure to postgres:', err, client); 31 | }); 32 | 33 | /** 34 | * @type {import('../persister-factories.js').Persister} 35 | */ 36 | const persister = { 37 | updateBatch: async (batch) => { 38 | const client = await pool.connect(); 39 | try { 40 | await client.query('BEGIN'); 41 | 42 | for (let op of batch) { 43 | const table = escapeIdentifier(op.table); 44 | if (op.op == 'PUT') { 45 | const data = op.data; 46 | const with_id = { ...data, id: op.id ?? op.data.id }; 47 | 48 | const columnsEscaped = Object.keys(with_id).map(escapeIdentifier); 49 | const columnsJoined = columnsEscaped.join(', '); 50 | 51 | let updateClauses = []; 52 | 53 | for (let key of Object.keys(data)) { 54 | if (key == 'id') { 55 | continue; 56 | } 57 | updateClauses.push(`${escapeIdentifier(key)} = EXCLUDED.${escapeIdentifier(key)}`); 58 | } 59 | 60 | const updateClause = updateClauses.length > 0 ? `DO UPDATE SET ${updateClauses.join(', ')}` : `DO NOTHING`; 61 | 62 | const statement = ` 63 | WITH data_row AS ( 64 | SELECT (json_populate_record(null::${table}, $1::json)).* 65 | ) 66 | INSERT INTO ${table} (${columnsJoined}) 67 | SELECT ${columnsJoined} FROM data_row 68 | ON CONFLICT(id) ${updateClause}`; 69 | 70 | await client.query(statement, [JSON.stringify(with_id)]); 71 | } else if (op.op == 'PATCH') { 72 | const data = op.data; 73 | const with_id = { ...data, id: op.id ?? data.id }; 74 | 75 | let updateClauses = []; 76 | 77 | for (let key of Object.keys(data)) { 78 | if (key == 'id') { 79 | continue; 80 | } 81 | updateClauses.push(`${escapeIdentifier(key)} = data_row.${escapeIdentifier(key)}`); 82 | } 83 | 84 | const statement = ` 85 | WITH data_row AS ( 86 | SELECT (json_populate_record(null::${table}, $1::json)).* 87 | ) 88 | UPDATE ${table} 89 | SET ${updateClauses.join(', ')} 90 | FROM data_row 91 | WHERE ${table}.id = data_row.id`; 92 | await client.query(statement, [JSON.stringify(with_id)]); 93 | } else if (op.op == 'DELETE') { 94 | const id = op.id ?? op.data?.id; 95 | const statement = ` 96 | WITH data_row AS ( 97 | SELECT (json_populate_record(null::${table}, $1::json)).* 98 | ) 99 | DELETE FROM ${table} 100 | USING data_row 101 | WHERE ${table}.id = data_row.id`; 102 | await client.query(statement, [JSON.stringify({ id: id })]); 103 | } 104 | } 105 | await client.query('COMMIT'); 106 | } catch (e) { 107 | await client.query('ROLLBACK'); 108 | throw e; 109 | } finally { 110 | client.release(); 111 | } 112 | }, 113 | async createCheckpoint(user_id, client_id) { 114 | const response = await pool.query( 115 | ` 116 | INSERT INTO checkpoints(user_id, client_id, checkpoint) 117 | VALUES 118 | ($1, $2, '1') 119 | ON 120 | CONFLICT (user_id, client_id) 121 | DO 122 | UPDATE SET checkpoint = checkpoints.checkpoint + 1 123 | RETURNING checkpoint; 124 | `, 125 | [user_id, client_id] 126 | ); 127 | /** 128 | * @type {bigint} 129 | */ 130 | const checkpoint = response.rows[0].checkpoint; 131 | return checkpoint; 132 | } 133 | }; 134 | return persister; 135 | }; 136 | -------------------------------------------------------------------------------- /src/utils/generate-key.js: -------------------------------------------------------------------------------- 1 | import * as jose from 'jose'; 2 | import * as crypto from 'crypto'; 3 | 4 | export async function generateKeyPair() { 5 | const alg = 'RS256'; 6 | const kid = `powersync-${crypto.randomBytes(5).toString('hex')}`; 7 | 8 | const { publicKey, privateKey } = await jose.generateKeyPair(alg, { 9 | extractable: true 10 | }); 11 | 12 | const privateJwk = { 13 | ...(await jose.exportJWK(privateKey)), 14 | alg, 15 | kid 16 | }; 17 | const publicJwk = { 18 | ...(await jose.exportJWK(publicKey)), 19 | alg, 20 | kid 21 | }; 22 | 23 | const privateBase64 = Buffer.from(JSON.stringify(privateJwk)).toString('base64'); 24 | const publicBase64 = Buffer.from(JSON.stringify(publicJwk)).toString('base64'); 25 | 26 | return { 27 | privateBase64, 28 | publicBase64 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "noEmit": true, 6 | "module": "Node16" 7 | }, 8 | "include": ["src/**/*.js"] 9 | } 10 | --------------------------------------------------------------------------------