├── .editorconfig
├── .eslintrc.js
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── release.yml
├── .gitignore
├── .prettierrc
├── .releaserc
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── docs
└── img
│ ├── hero.jpg
│ └── wope.png
├── package-lock.json
├── package.json
├── src
├── api-config.json
├── api-config.schema.json
├── auth.js
├── common.js
├── cors.js
├── enums
│ ├── http-method.js
│ └── integration-type.js
├── index.js
├── integrations
│ ├── auth0.js
│ └── supabase-auth.js
├── mapping.js
├── path-ops.js
├── powered-by.js
├── requests.js
├── responses.js
├── services
│ └── endpoint1.js
├── types
│ ├── error_types.js
│ └── serverless_api_gateway_context.js
└── utils
│ ├── config.js
│ └── logger.js
├── worker-configuration.d.ts
└── wrangler.toml
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = tab
6 | tab_width = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.yml]
13 | indent_style = space
14 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | tsconfigRootDir : __dirname,
6 | sourceType: 'module',
7 | },
8 | plugins: ['@typescript-eslint/eslint-plugin'],
9 | extends: [
10 | 'plugin:@typescript-eslint/recommended',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | ignorePatterns: ['.eslintrc.js'],
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | },
25 | };
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: irensaltali
4 | patreon: irensaltali
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "npm" # or "yarn" if you are using Yarn
4 | directory: "/" # Location of package manifests
5 | schedule:
6 | interval: "weekly"
7 | commit-message:
8 | prefix: "fix" # You can customize the commit message prefix
9 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | defaults:
12 | run:
13 | working-directory: ./
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - name: Setup Node.js
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: '21'
21 |
22 | - name: Install Dependencies
23 | run: npm install
24 |
25 | - name: Release
26 | env:
27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28 | run: npx semantic-release
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 |
3 | logs
4 | _.log
5 | npm-debug.log_
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 |
13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14 |
15 | # Runtime data
16 |
17 | pids
18 | _.pid
19 | _.seed
20 | \*.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 |
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 |
28 | coverage
29 | \*.lcov
30 |
31 | # nyc test coverage
32 |
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 |
37 | .grunt
38 |
39 | # Bower dependency directory (https://bower.io/)
40 |
41 | bower_components
42 |
43 | # node-waf configuration
44 |
45 | .lock-wscript
46 |
47 | # Compiled binary addons (https://nodejs.org/api/addons.html)
48 |
49 | build/Release
50 |
51 | # Dependency directories
52 |
53 | node_modules/
54 | jspm_packages/
55 |
56 | # Snowpack dependency directory (https://snowpack.dev/)
57 |
58 | web_modules/
59 |
60 | # TypeScript cache
61 |
62 | \*.tsbuildinfo
63 |
64 | # Optional npm cache directory
65 |
66 | .npm
67 |
68 | # Optional eslint cache
69 |
70 | .eslintcache
71 |
72 | # Optional stylelint cache
73 |
74 | .stylelintcache
75 |
76 | # Microbundle cache
77 |
78 | .rpt2_cache/
79 | .rts2_cache_cjs/
80 | .rts2_cache_es/
81 | .rts2_cache_umd/
82 |
83 | # Optional REPL history
84 |
85 | .node_repl_history
86 |
87 | # Output of 'npm pack'
88 |
89 | \*.tgz
90 |
91 | # Yarn Integrity file
92 |
93 | .yarn-integrity
94 |
95 | # dotenv environment variable files
96 |
97 | .env
98 | .env.development.local
99 | .env.test.local
100 | .env.production.local
101 | .env.local
102 |
103 | # parcel-bundler cache (https://parceljs.org/)
104 |
105 | .cache
106 | .parcel-cache
107 |
108 | # Next.js build output
109 |
110 | .next
111 | out
112 |
113 | # Nuxt.js build / generate output
114 |
115 | .nuxt
116 | dist
117 |
118 | # Gatsby files
119 |
120 | .cache/
121 |
122 | # Comment in the public line in if your project uses Gatsby and not Next.js
123 |
124 | # https://nextjs.org/blog/next-9-1#public-directory-support
125 |
126 | # public
127 |
128 | # vuepress build output
129 |
130 | .vuepress/dist
131 |
132 | # vuepress v2.x temp and cache directory
133 |
134 | .temp
135 | .cache
136 |
137 | # Docusaurus cache and generated files
138 |
139 | .docusaurus
140 |
141 | # Serverless directories
142 |
143 | .serverless/
144 |
145 | # FuseBox cache
146 |
147 | .fusebox/
148 |
149 | # DynamoDB Local files
150 |
151 | .dynamodb/
152 |
153 | # TernJS port file
154 |
155 | .tern-port
156 |
157 | # Stores VSCode versions used for testing VSCode extensions
158 |
159 | .vscode-test
160 |
161 | # yarn v2
162 |
163 | .yarn/cache
164 | .yarn/unplugged
165 | .yarn/build-state.yml
166 | .yarn/install-state.gz
167 | .pnp.\*
168 |
169 | # wrangler project
170 |
171 | .dev.vars
172 | .wrangler/
173 |
174 | .DS_Store
175 |
176 |
177 | # Exclude dev files
178 |
179 | .dev/
180 | .env
181 | .env.development.local
182 | .env.test.local
183 | .env.production.local
184 | .env.local
185 |
186 | # Ignore services_by_link folder
187 | services_by_link/
188 |
189 | # Cloudflare Workers sensitive configuration files
190 | wrangler.auth.toml
191 | *.auth.toml
192 |
193 | # API configuration files with sensitive data
194 | src/api-config.json
195 | src/api-config.api.json
196 |
197 | # Test and development configuration files
198 | **/test-*.sh
199 | **/setup-*.sh
200 |
201 | # Remove examples folder from tracking (if it exists)
202 | examples/
203 | scripts/
204 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 140,
3 | "singleQuote": true,
4 | "semi": true,
5 | "useTabs": true
6 | }
7 |
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": [
3 | {
4 | "name": "main",
5 | "prerelease": false
6 | }
7 | ],
8 | "plugins": [
9 | "@semantic-release/commit-analyzer",
10 | "@semantic-release/release-notes-generator",
11 | [
12 | "@semantic-release/github",
13 | {
14 | "assets": [
15 | {
16 | "path": "src/*"
17 | },
18 | {
19 | "path": "*.js"
20 | },
21 | {
22 | "path": "package*"
23 | },
24 | {
25 | "path": "wrangler.toml"
26 | }
27 | ],
28 | "addReleases": "bottom",
29 | "successComment": false
30 | }
31 | ]
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Serverless API Gateway
2 |
3 | Serverless API Gateway is an open project, and you can contribute to it in many ways. You can help with ideas, code, or documentation. We appreciate any efforts that help us to make the project better.
4 |
5 | Thank you!
6 |
7 | ## Legal Info
8 |
9 | When you open your first pull-request to ClickHouse repo, a bot will invite you to accept ClickHouse Individual CLA (Contributor License Agreement). It is a simple few click process. For subsequent pull-requests the bot will check if you have already signed it and won't bother you again.
10 |
11 | ## How to Contribute
12 |
13 | Your contributions are what make the Serverless API Gateway an even better API management solution! If you have suggestions for new features, notice a bug, or want to improve the code, please take the following steps:
14 |
15 | 1. Fork the repository.
16 | 2. Implement your changes on a new branch.
17 | 3. Submit a pull request with a clear description of your improvements.
18 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # Elastic License 2.0
2 |
3 | ## Acceptance
4 | By using the software, you agree to all of the terms and conditions below.
5 |
6 | ## Copyright License
7 | The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below.
8 |
9 | ## Limitations
10 | You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software.
11 |
12 | You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key.
13 |
14 | You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
15 |
16 | ## Patents
17 | The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
18 |
19 | ## Notices
20 | You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms.
21 |
22 | If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software.
23 |
24 | ## No Other Rights
25 | These terms do not imply any licenses other than those expressly granted in these terms.
26 |
27 | ## Termination
28 | If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently.
29 |
30 | ## No Liability
31 | **As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.**
32 |
33 | ## Definitions
34 | The **licensor** is the entity offering these terms, and the software is the software the licensor makes available under these terms, including any portion of it.
35 |
36 | **you** refers to the individual or entity agreeing to these terms.
37 |
38 | **your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
39 |
40 | **your licenses** are all the licenses granted to you for the software under these terms.
41 |
42 | **use** means anything you do with the software requiring one of your licenses.
43 |
44 | **trademark** means trademarks, service marks, and similar rights.
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 | Serverless API Gateway
5 |
6 |
7 |
8 | [
](https://api.gitsponsors.com/api/badge/link?p=zGF0mvB4EVuuLcG20aJcGcbYYMtR20/RG8/n8Uq6Aq3cPgUcE5M+BDSf9G8ly/DDBPVi8ecJ3NT+GIuj2+h8+/ta2Nth49SJAnE96sTYsk70BhaeMMMpoHNu8R9yc8hodGpE5mlSInPC/uEAJIdwEQ==)
9 |
10 | []()
11 |
12 | Welcome to the Serverless API Gateway, an innovative tool designed to streamline your API management tasks using the powerful capabilities of Cloudflare Workers.
13 |
14 | ## Features
15 |
16 | - **JS Workers**: Write serverless JavaScript workers that intercept and modify your API requests and responses on the fly.
17 | - **Routing (Path and Method)**: Simplify your API architecture with flexible path and method-based routing for directing traffic to the appropriate endpoints.
18 | - **CORS (Basic)**: Manage cross-origin resource sharing settings with ease, ensuring your APIs can securely handle requests from different origins.
19 | - **Auth (JWT)**: Secure your APIs by implementing JSON Web Token (JWT) based authentication to validate and manage user access efficiently.
20 | - **Service Binding**: Bind your API to a service as Workers, so you can use the Workers capabilities within your API.
21 | - **Value Mapping**: Map values from sources to destinations, allowing you to easily transform your data.
22 |
23 | ## Motivation
24 |
25 | APIs are pivotal in the landscape of modern applications, but they bring forth a unique set of challenges regarding security, routing, and overall management. The Serverless API Gateway emerged from the need to address these issues in a reliable, manageable, and cost-effective way. Built upon Cloudflare's serverless infrastructure, this project provides developers with a lightweight yet robust toolkit that adapts to the unpredictability of internet scale and traffic. Our mission is to empower developers to securely and efficiently manage their APIs without the overhead of managing infrastructure.
26 |
27 |
28 | For detailed setup and usage instructions, please see the [Serverless API Gateway documentation](https://docs.serverlessapigateway.com).
29 |
30 | ## Contributing
31 |
32 | If you want to contribute to the project, you can start by checking the [contributing guidelines](CONTRIBUTING.md).
33 |
34 |
35 | ## Acknowledgments
36 |
37 | A shoutout to the contributors, community members, and the maintainers of Cloudflare Workers for their support and inspiration in making this project a reality.
38 |
39 | The Serverless API Gateway is not just another API tool; it's created by developers, for developers, with the vision of making API management a breeze. Let's build together.
40 |
41 |
42 | ## Support
43 |
44 | I'm always happy to help with any questions or concerns you may have. Feel free to reach out to me from on [Twitter](https://twitter.com/irensaltali) or [LinkedIn](https://www.linkedin.com/in/irensaltali/).
45 |
46 | If you need a more extensive support you can always book on [Superpeer](https://superpeer.com/irensaltali/-/serverless-api-gateway)
47 |
48 | # Companies that use Serverless API Gateway
49 |
50 |
51 |
52 |
53 |
54 | Let us know if you are using Serverless API Gateway and we can add your company here.
55 |
56 | # Contributors
57 |
58 |
59 |
60 |
61 |
62 |
63 | # Feeback Survey
64 |
65 | Please take a few minutes to fill out the [feedback survey](https://r39ra55b0sl.typeform.com/to/ex8HMyTH) to help us improve the Serverless API Gateway.
66 |
--------------------------------------------------------------------------------
/docs/img/hero.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irensaltali/serverlessapigateway/f9595fc63f478021a4d47586e73d9a79e17b3037/docs/img/hero.jpg
--------------------------------------------------------------------------------
/docs/img/wope.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irensaltali/serverlessapigateway/f9595fc63f478021a4d47586e73d9a79e17b3037/docs/img/wope.png
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "serverlessapigateway",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "dependencies": {
8 | "@supabase/supabase-js": "^2.78.0",
9 | "jose": "^6.1.0",
10 | "jsonwebtoken": "^9.0.2",
11 | "wrangler": "^4.38.0"
12 | }
13 | },
14 | "node_modules/@cloudflare/kv-asset-handler": {
15 | "version": "0.4.0",
16 | "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz",
17 | "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==",
18 | "license": "MIT OR Apache-2.0",
19 | "dependencies": {
20 | "mime": "^3.0.0"
21 | },
22 | "engines": {
23 | "node": ">=18.0.0"
24 | }
25 | },
26 | "node_modules/@cloudflare/unenv-preset": {
27 | "version": "2.7.4",
28 | "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.7.4.tgz",
29 | "integrity": "sha512-KIjbu/Dt50zseJIoOOK5y4eYpSojD9+xxkePYVK1Rg9k/p/st4YyMtz1Clju/zrenJHrOH+AAcjNArOPMwH4Bw==",
30 | "license": "MIT OR Apache-2.0",
31 | "peerDependencies": {
32 | "unenv": "2.0.0-rc.21",
33 | "workerd": "^1.20250912.0"
34 | },
35 | "peerDependenciesMeta": {
36 | "workerd": {
37 | "optional": true
38 | }
39 | }
40 | },
41 | "node_modules/@cloudflare/workerd-darwin-64": {
42 | "version": "1.20250917.0",
43 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250917.0.tgz",
44 | "integrity": "sha512-0kL/kFnKUSycoo7b3PgM0nRyZ+1MGQAKaXtE6a2+SAeUkZ2FLnuFWmASi0s4rlWGsf/rlTw4AwXROePir9dUcQ==",
45 | "cpu": [
46 | "x64"
47 | ],
48 | "license": "Apache-2.0",
49 | "optional": true,
50 | "os": [
51 | "darwin"
52 | ],
53 | "engines": {
54 | "node": ">=16"
55 | }
56 | },
57 | "node_modules/@cloudflare/workerd-darwin-arm64": {
58 | "version": "1.20250917.0",
59 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250917.0.tgz",
60 | "integrity": "sha512-3/N1QmEJsC8Byxt1SGgVp5o0r+eKjuUEMbIL2yzLk/jrMdErPXy/DGf/tXZoACU68a/gMEbbT1itkYrm85iQHg==",
61 | "cpu": [
62 | "arm64"
63 | ],
64 | "license": "Apache-2.0",
65 | "optional": true,
66 | "os": [
67 | "darwin"
68 | ],
69 | "engines": {
70 | "node": ">=16"
71 | }
72 | },
73 | "node_modules/@cloudflare/workerd-linux-64": {
74 | "version": "1.20250917.0",
75 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250917.0.tgz",
76 | "integrity": "sha512-E7sEow7CErbWY3olMmlbj6iss9r7Xb2uMyc+MKzYC9/J6yFlJd/dNHvjey9QIdxzbkC9qGe90a+KxQrjs+fspA==",
77 | "cpu": [
78 | "x64"
79 | ],
80 | "license": "Apache-2.0",
81 | "optional": true,
82 | "os": [
83 | "linux"
84 | ],
85 | "engines": {
86 | "node": ">=16"
87 | }
88 | },
89 | "node_modules/@cloudflare/workerd-linux-arm64": {
90 | "version": "1.20250917.0",
91 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250917.0.tgz",
92 | "integrity": "sha512-roOnRjxut2FUxo6HA9spbfs32naXAsnSQqsgku3iq6BYKv1QqGiFoY5bReK72N5uxmhxo7+RiTo8ZEkxA/vMIQ==",
93 | "cpu": [
94 | "arm64"
95 | ],
96 | "license": "Apache-2.0",
97 | "optional": true,
98 | "os": [
99 | "linux"
100 | ],
101 | "engines": {
102 | "node": ">=16"
103 | }
104 | },
105 | "node_modules/@cloudflare/workerd-windows-64": {
106 | "version": "1.20250917.0",
107 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250917.0.tgz",
108 | "integrity": "sha512-gslh6Ou9+kshHjR1BJX47OsbPw3/cZCvGDompvaW/URCgr7aMzljbgmBb7p0uhwGy1qCXcIt31St6pd3IEcLng==",
109 | "cpu": [
110 | "x64"
111 | ],
112 | "license": "Apache-2.0",
113 | "optional": true,
114 | "os": [
115 | "win32"
116 | ],
117 | "engines": {
118 | "node": ">=16"
119 | }
120 | },
121 | "node_modules/@cspotcode/source-map-support": {
122 | "version": "0.8.1",
123 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
124 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
125 | "license": "MIT",
126 | "dependencies": {
127 | "@jridgewell/trace-mapping": "0.3.9"
128 | },
129 | "engines": {
130 | "node": ">=12"
131 | }
132 | },
133 | "node_modules/@emnapi/runtime": {
134 | "version": "1.5.0",
135 | "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
136 | "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
137 | "license": "MIT",
138 | "optional": true,
139 | "dependencies": {
140 | "tslib": "^2.4.0"
141 | }
142 | },
143 | "node_modules/@esbuild/aix-ppc64": {
144 | "version": "0.25.4",
145 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
146 | "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
147 | "cpu": [
148 | "ppc64"
149 | ],
150 | "license": "MIT",
151 | "optional": true,
152 | "os": [
153 | "aix"
154 | ],
155 | "engines": {
156 | "node": ">=18"
157 | }
158 | },
159 | "node_modules/@esbuild/android-arm": {
160 | "version": "0.25.4",
161 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
162 | "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
163 | "cpu": [
164 | "arm"
165 | ],
166 | "license": "MIT",
167 | "optional": true,
168 | "os": [
169 | "android"
170 | ],
171 | "engines": {
172 | "node": ">=18"
173 | }
174 | },
175 | "node_modules/@esbuild/android-arm64": {
176 | "version": "0.25.4",
177 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
178 | "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
179 | "cpu": [
180 | "arm64"
181 | ],
182 | "license": "MIT",
183 | "optional": true,
184 | "os": [
185 | "android"
186 | ],
187 | "engines": {
188 | "node": ">=18"
189 | }
190 | },
191 | "node_modules/@esbuild/android-x64": {
192 | "version": "0.25.4",
193 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
194 | "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
195 | "cpu": [
196 | "x64"
197 | ],
198 | "license": "MIT",
199 | "optional": true,
200 | "os": [
201 | "android"
202 | ],
203 | "engines": {
204 | "node": ">=18"
205 | }
206 | },
207 | "node_modules/@esbuild/darwin-arm64": {
208 | "version": "0.25.4",
209 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
210 | "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
211 | "cpu": [
212 | "arm64"
213 | ],
214 | "license": "MIT",
215 | "optional": true,
216 | "os": [
217 | "darwin"
218 | ],
219 | "engines": {
220 | "node": ">=18"
221 | }
222 | },
223 | "node_modules/@esbuild/darwin-x64": {
224 | "version": "0.25.4",
225 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
226 | "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
227 | "cpu": [
228 | "x64"
229 | ],
230 | "license": "MIT",
231 | "optional": true,
232 | "os": [
233 | "darwin"
234 | ],
235 | "engines": {
236 | "node": ">=18"
237 | }
238 | },
239 | "node_modules/@esbuild/freebsd-arm64": {
240 | "version": "0.25.4",
241 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
242 | "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
243 | "cpu": [
244 | "arm64"
245 | ],
246 | "license": "MIT",
247 | "optional": true,
248 | "os": [
249 | "freebsd"
250 | ],
251 | "engines": {
252 | "node": ">=18"
253 | }
254 | },
255 | "node_modules/@esbuild/freebsd-x64": {
256 | "version": "0.25.4",
257 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
258 | "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
259 | "cpu": [
260 | "x64"
261 | ],
262 | "license": "MIT",
263 | "optional": true,
264 | "os": [
265 | "freebsd"
266 | ],
267 | "engines": {
268 | "node": ">=18"
269 | }
270 | },
271 | "node_modules/@esbuild/linux-arm": {
272 | "version": "0.25.4",
273 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
274 | "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
275 | "cpu": [
276 | "arm"
277 | ],
278 | "license": "MIT",
279 | "optional": true,
280 | "os": [
281 | "linux"
282 | ],
283 | "engines": {
284 | "node": ">=18"
285 | }
286 | },
287 | "node_modules/@esbuild/linux-arm64": {
288 | "version": "0.25.4",
289 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
290 | "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
291 | "cpu": [
292 | "arm64"
293 | ],
294 | "license": "MIT",
295 | "optional": true,
296 | "os": [
297 | "linux"
298 | ],
299 | "engines": {
300 | "node": ">=18"
301 | }
302 | },
303 | "node_modules/@esbuild/linux-ia32": {
304 | "version": "0.25.4",
305 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
306 | "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
307 | "cpu": [
308 | "ia32"
309 | ],
310 | "license": "MIT",
311 | "optional": true,
312 | "os": [
313 | "linux"
314 | ],
315 | "engines": {
316 | "node": ">=18"
317 | }
318 | },
319 | "node_modules/@esbuild/linux-loong64": {
320 | "version": "0.25.4",
321 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
322 | "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
323 | "cpu": [
324 | "loong64"
325 | ],
326 | "license": "MIT",
327 | "optional": true,
328 | "os": [
329 | "linux"
330 | ],
331 | "engines": {
332 | "node": ">=18"
333 | }
334 | },
335 | "node_modules/@esbuild/linux-mips64el": {
336 | "version": "0.25.4",
337 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
338 | "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
339 | "cpu": [
340 | "mips64el"
341 | ],
342 | "license": "MIT",
343 | "optional": true,
344 | "os": [
345 | "linux"
346 | ],
347 | "engines": {
348 | "node": ">=18"
349 | }
350 | },
351 | "node_modules/@esbuild/linux-ppc64": {
352 | "version": "0.25.4",
353 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
354 | "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
355 | "cpu": [
356 | "ppc64"
357 | ],
358 | "license": "MIT",
359 | "optional": true,
360 | "os": [
361 | "linux"
362 | ],
363 | "engines": {
364 | "node": ">=18"
365 | }
366 | },
367 | "node_modules/@esbuild/linux-riscv64": {
368 | "version": "0.25.4",
369 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
370 | "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
371 | "cpu": [
372 | "riscv64"
373 | ],
374 | "license": "MIT",
375 | "optional": true,
376 | "os": [
377 | "linux"
378 | ],
379 | "engines": {
380 | "node": ">=18"
381 | }
382 | },
383 | "node_modules/@esbuild/linux-s390x": {
384 | "version": "0.25.4",
385 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
386 | "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
387 | "cpu": [
388 | "s390x"
389 | ],
390 | "license": "MIT",
391 | "optional": true,
392 | "os": [
393 | "linux"
394 | ],
395 | "engines": {
396 | "node": ">=18"
397 | }
398 | },
399 | "node_modules/@esbuild/linux-x64": {
400 | "version": "0.25.4",
401 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
402 | "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
403 | "cpu": [
404 | "x64"
405 | ],
406 | "license": "MIT",
407 | "optional": true,
408 | "os": [
409 | "linux"
410 | ],
411 | "engines": {
412 | "node": ">=18"
413 | }
414 | },
415 | "node_modules/@esbuild/netbsd-arm64": {
416 | "version": "0.25.4",
417 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
418 | "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
419 | "cpu": [
420 | "arm64"
421 | ],
422 | "license": "MIT",
423 | "optional": true,
424 | "os": [
425 | "netbsd"
426 | ],
427 | "engines": {
428 | "node": ">=18"
429 | }
430 | },
431 | "node_modules/@esbuild/netbsd-x64": {
432 | "version": "0.25.4",
433 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
434 | "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
435 | "cpu": [
436 | "x64"
437 | ],
438 | "license": "MIT",
439 | "optional": true,
440 | "os": [
441 | "netbsd"
442 | ],
443 | "engines": {
444 | "node": ">=18"
445 | }
446 | },
447 | "node_modules/@esbuild/openbsd-arm64": {
448 | "version": "0.25.4",
449 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
450 | "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
451 | "cpu": [
452 | "arm64"
453 | ],
454 | "license": "MIT",
455 | "optional": true,
456 | "os": [
457 | "openbsd"
458 | ],
459 | "engines": {
460 | "node": ">=18"
461 | }
462 | },
463 | "node_modules/@esbuild/openbsd-x64": {
464 | "version": "0.25.4",
465 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
466 | "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
467 | "cpu": [
468 | "x64"
469 | ],
470 | "license": "MIT",
471 | "optional": true,
472 | "os": [
473 | "openbsd"
474 | ],
475 | "engines": {
476 | "node": ">=18"
477 | }
478 | },
479 | "node_modules/@esbuild/sunos-x64": {
480 | "version": "0.25.4",
481 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
482 | "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
483 | "cpu": [
484 | "x64"
485 | ],
486 | "license": "MIT",
487 | "optional": true,
488 | "os": [
489 | "sunos"
490 | ],
491 | "engines": {
492 | "node": ">=18"
493 | }
494 | },
495 | "node_modules/@esbuild/win32-arm64": {
496 | "version": "0.25.4",
497 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
498 | "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
499 | "cpu": [
500 | "arm64"
501 | ],
502 | "license": "MIT",
503 | "optional": true,
504 | "os": [
505 | "win32"
506 | ],
507 | "engines": {
508 | "node": ">=18"
509 | }
510 | },
511 | "node_modules/@esbuild/win32-ia32": {
512 | "version": "0.25.4",
513 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
514 | "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
515 | "cpu": [
516 | "ia32"
517 | ],
518 | "license": "MIT",
519 | "optional": true,
520 | "os": [
521 | "win32"
522 | ],
523 | "engines": {
524 | "node": ">=18"
525 | }
526 | },
527 | "node_modules/@esbuild/win32-x64": {
528 | "version": "0.25.4",
529 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
530 | "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
531 | "cpu": [
532 | "x64"
533 | ],
534 | "license": "MIT",
535 | "optional": true,
536 | "os": [
537 | "win32"
538 | ],
539 | "engines": {
540 | "node": ">=18"
541 | }
542 | },
543 | "node_modules/@img/sharp-darwin-arm64": {
544 | "version": "0.33.5",
545 | "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
546 | "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
547 | "cpu": [
548 | "arm64"
549 | ],
550 | "license": "Apache-2.0",
551 | "optional": true,
552 | "os": [
553 | "darwin"
554 | ],
555 | "engines": {
556 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
557 | },
558 | "funding": {
559 | "url": "https://opencollective.com/libvips"
560 | },
561 | "optionalDependencies": {
562 | "@img/sharp-libvips-darwin-arm64": "1.0.4"
563 | }
564 | },
565 | "node_modules/@img/sharp-darwin-x64": {
566 | "version": "0.33.5",
567 | "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
568 | "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
569 | "cpu": [
570 | "x64"
571 | ],
572 | "license": "Apache-2.0",
573 | "optional": true,
574 | "os": [
575 | "darwin"
576 | ],
577 | "engines": {
578 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
579 | },
580 | "funding": {
581 | "url": "https://opencollective.com/libvips"
582 | },
583 | "optionalDependencies": {
584 | "@img/sharp-libvips-darwin-x64": "1.0.4"
585 | }
586 | },
587 | "node_modules/@img/sharp-libvips-darwin-arm64": {
588 | "version": "1.0.4",
589 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
590 | "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
591 | "cpu": [
592 | "arm64"
593 | ],
594 | "license": "LGPL-3.0-or-later",
595 | "optional": true,
596 | "os": [
597 | "darwin"
598 | ],
599 | "funding": {
600 | "url": "https://opencollective.com/libvips"
601 | }
602 | },
603 | "node_modules/@img/sharp-libvips-darwin-x64": {
604 | "version": "1.0.4",
605 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
606 | "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
607 | "cpu": [
608 | "x64"
609 | ],
610 | "license": "LGPL-3.0-or-later",
611 | "optional": true,
612 | "os": [
613 | "darwin"
614 | ],
615 | "funding": {
616 | "url": "https://opencollective.com/libvips"
617 | }
618 | },
619 | "node_modules/@img/sharp-libvips-linux-arm": {
620 | "version": "1.0.5",
621 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
622 | "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
623 | "cpu": [
624 | "arm"
625 | ],
626 | "license": "LGPL-3.0-or-later",
627 | "optional": true,
628 | "os": [
629 | "linux"
630 | ],
631 | "funding": {
632 | "url": "https://opencollective.com/libvips"
633 | }
634 | },
635 | "node_modules/@img/sharp-libvips-linux-arm64": {
636 | "version": "1.0.4",
637 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
638 | "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
639 | "cpu": [
640 | "arm64"
641 | ],
642 | "license": "LGPL-3.0-or-later",
643 | "optional": true,
644 | "os": [
645 | "linux"
646 | ],
647 | "funding": {
648 | "url": "https://opencollective.com/libvips"
649 | }
650 | },
651 | "node_modules/@img/sharp-libvips-linux-s390x": {
652 | "version": "1.0.4",
653 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
654 | "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
655 | "cpu": [
656 | "s390x"
657 | ],
658 | "license": "LGPL-3.0-or-later",
659 | "optional": true,
660 | "os": [
661 | "linux"
662 | ],
663 | "funding": {
664 | "url": "https://opencollective.com/libvips"
665 | }
666 | },
667 | "node_modules/@img/sharp-libvips-linux-x64": {
668 | "version": "1.0.4",
669 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
670 | "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
671 | "cpu": [
672 | "x64"
673 | ],
674 | "license": "LGPL-3.0-or-later",
675 | "optional": true,
676 | "os": [
677 | "linux"
678 | ],
679 | "funding": {
680 | "url": "https://opencollective.com/libvips"
681 | }
682 | },
683 | "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
684 | "version": "1.0.4",
685 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
686 | "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
687 | "cpu": [
688 | "arm64"
689 | ],
690 | "license": "LGPL-3.0-or-later",
691 | "optional": true,
692 | "os": [
693 | "linux"
694 | ],
695 | "funding": {
696 | "url": "https://opencollective.com/libvips"
697 | }
698 | },
699 | "node_modules/@img/sharp-libvips-linuxmusl-x64": {
700 | "version": "1.0.4",
701 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
702 | "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
703 | "cpu": [
704 | "x64"
705 | ],
706 | "license": "LGPL-3.0-or-later",
707 | "optional": true,
708 | "os": [
709 | "linux"
710 | ],
711 | "funding": {
712 | "url": "https://opencollective.com/libvips"
713 | }
714 | },
715 | "node_modules/@img/sharp-linux-arm": {
716 | "version": "0.33.5",
717 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
718 | "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
719 | "cpu": [
720 | "arm"
721 | ],
722 | "license": "Apache-2.0",
723 | "optional": true,
724 | "os": [
725 | "linux"
726 | ],
727 | "engines": {
728 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
729 | },
730 | "funding": {
731 | "url": "https://opencollective.com/libvips"
732 | },
733 | "optionalDependencies": {
734 | "@img/sharp-libvips-linux-arm": "1.0.5"
735 | }
736 | },
737 | "node_modules/@img/sharp-linux-arm64": {
738 | "version": "0.33.5",
739 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
740 | "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
741 | "cpu": [
742 | "arm64"
743 | ],
744 | "license": "Apache-2.0",
745 | "optional": true,
746 | "os": [
747 | "linux"
748 | ],
749 | "engines": {
750 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
751 | },
752 | "funding": {
753 | "url": "https://opencollective.com/libvips"
754 | },
755 | "optionalDependencies": {
756 | "@img/sharp-libvips-linux-arm64": "1.0.4"
757 | }
758 | },
759 | "node_modules/@img/sharp-linux-s390x": {
760 | "version": "0.33.5",
761 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
762 | "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
763 | "cpu": [
764 | "s390x"
765 | ],
766 | "license": "Apache-2.0",
767 | "optional": true,
768 | "os": [
769 | "linux"
770 | ],
771 | "engines": {
772 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
773 | },
774 | "funding": {
775 | "url": "https://opencollective.com/libvips"
776 | },
777 | "optionalDependencies": {
778 | "@img/sharp-libvips-linux-s390x": "1.0.4"
779 | }
780 | },
781 | "node_modules/@img/sharp-linux-x64": {
782 | "version": "0.33.5",
783 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
784 | "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
785 | "cpu": [
786 | "x64"
787 | ],
788 | "license": "Apache-2.0",
789 | "optional": true,
790 | "os": [
791 | "linux"
792 | ],
793 | "engines": {
794 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
795 | },
796 | "funding": {
797 | "url": "https://opencollective.com/libvips"
798 | },
799 | "optionalDependencies": {
800 | "@img/sharp-libvips-linux-x64": "1.0.4"
801 | }
802 | },
803 | "node_modules/@img/sharp-linuxmusl-arm64": {
804 | "version": "0.33.5",
805 | "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
806 | "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
807 | "cpu": [
808 | "arm64"
809 | ],
810 | "license": "Apache-2.0",
811 | "optional": true,
812 | "os": [
813 | "linux"
814 | ],
815 | "engines": {
816 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
817 | },
818 | "funding": {
819 | "url": "https://opencollective.com/libvips"
820 | },
821 | "optionalDependencies": {
822 | "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
823 | }
824 | },
825 | "node_modules/@img/sharp-linuxmusl-x64": {
826 | "version": "0.33.5",
827 | "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
828 | "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
829 | "cpu": [
830 | "x64"
831 | ],
832 | "license": "Apache-2.0",
833 | "optional": true,
834 | "os": [
835 | "linux"
836 | ],
837 | "engines": {
838 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
839 | },
840 | "funding": {
841 | "url": "https://opencollective.com/libvips"
842 | },
843 | "optionalDependencies": {
844 | "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
845 | }
846 | },
847 | "node_modules/@img/sharp-wasm32": {
848 | "version": "0.33.5",
849 | "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
850 | "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
851 | "cpu": [
852 | "wasm32"
853 | ],
854 | "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
855 | "optional": true,
856 | "dependencies": {
857 | "@emnapi/runtime": "^1.2.0"
858 | },
859 | "engines": {
860 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
861 | },
862 | "funding": {
863 | "url": "https://opencollective.com/libvips"
864 | }
865 | },
866 | "node_modules/@img/sharp-win32-ia32": {
867 | "version": "0.33.5",
868 | "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
869 | "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
870 | "cpu": [
871 | "ia32"
872 | ],
873 | "license": "Apache-2.0 AND LGPL-3.0-or-later",
874 | "optional": true,
875 | "os": [
876 | "win32"
877 | ],
878 | "engines": {
879 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
880 | },
881 | "funding": {
882 | "url": "https://opencollective.com/libvips"
883 | }
884 | },
885 | "node_modules/@img/sharp-win32-x64": {
886 | "version": "0.33.5",
887 | "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
888 | "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
889 | "cpu": [
890 | "x64"
891 | ],
892 | "license": "Apache-2.0 AND LGPL-3.0-or-later",
893 | "optional": true,
894 | "os": [
895 | "win32"
896 | ],
897 | "engines": {
898 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
899 | },
900 | "funding": {
901 | "url": "https://opencollective.com/libvips"
902 | }
903 | },
904 | "node_modules/@jridgewell/resolve-uri": {
905 | "version": "3.1.2",
906 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
907 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
908 | "license": "MIT",
909 | "engines": {
910 | "node": ">=6.0.0"
911 | }
912 | },
913 | "node_modules/@jridgewell/sourcemap-codec": {
914 | "version": "1.5.5",
915 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
916 | "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
917 | "license": "MIT"
918 | },
919 | "node_modules/@jridgewell/trace-mapping": {
920 | "version": "0.3.9",
921 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
922 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
923 | "license": "MIT",
924 | "dependencies": {
925 | "@jridgewell/resolve-uri": "^3.0.3",
926 | "@jridgewell/sourcemap-codec": "^1.4.10"
927 | }
928 | },
929 | "node_modules/@poppinss/colors": {
930 | "version": "4.1.5",
931 | "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.5.tgz",
932 | "integrity": "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==",
933 | "license": "MIT",
934 | "dependencies": {
935 | "kleur": "^4.1.5"
936 | }
937 | },
938 | "node_modules/@poppinss/dumper": {
939 | "version": "0.6.4",
940 | "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.4.tgz",
941 | "integrity": "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ==",
942 | "license": "MIT",
943 | "dependencies": {
944 | "@poppinss/colors": "^4.1.5",
945 | "@sindresorhus/is": "^7.0.2",
946 | "supports-color": "^10.0.0"
947 | }
948 | },
949 | "node_modules/@poppinss/exception": {
950 | "version": "1.2.2",
951 | "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.2.tgz",
952 | "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==",
953 | "license": "MIT"
954 | },
955 | "node_modules/@sindresorhus/is": {
956 | "version": "7.1.0",
957 | "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.0.tgz",
958 | "integrity": "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA==",
959 | "license": "MIT",
960 | "engines": {
961 | "node": ">=18"
962 | },
963 | "funding": {
964 | "url": "https://github.com/sindresorhus/is?sponsor=1"
965 | }
966 | },
967 | "node_modules/@speed-highlight/core": {
968 | "version": "1.2.7",
969 | "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.7.tgz",
970 | "integrity": "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==",
971 | "license": "CC0-1.0"
972 | },
973 | "node_modules/@supabase/auth-js": {
974 | "version": "2.78.0",
975 | "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.78.0.tgz",
976 | "integrity": "sha512-cXDtu1U0LeZj/xfnFoV7yCze37TcbNo8FCxy1FpqhMbB9u9QxxDSW6pA5gm/07Ei7m260Lof4CZx67Cu6DPeig==",
977 | "license": "MIT",
978 | "dependencies": {
979 | "@supabase/node-fetch": "2.6.15",
980 | "tslib": "2.8.1"
981 | }
982 | },
983 | "node_modules/@supabase/functions-js": {
984 | "version": "2.78.0",
985 | "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.78.0.tgz",
986 | "integrity": "sha512-t1jOvArBsOINyqaRee1xJ3gryXLvkBzqnKfi6q3YRzzhJbGS6eXz0pXR5fqmJeB01fLC+1njpf3YhMszdPEF7g==",
987 | "license": "MIT",
988 | "dependencies": {
989 | "@supabase/node-fetch": "2.6.15",
990 | "tslib": "2.8.1"
991 | }
992 | },
993 | "node_modules/@supabase/node-fetch": {
994 | "version": "2.6.15",
995 | "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
996 | "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
997 | "license": "MIT",
998 | "dependencies": {
999 | "whatwg-url": "^5.0.0"
1000 | },
1001 | "engines": {
1002 | "node": "4.x || >=6.0.0"
1003 | }
1004 | },
1005 | "node_modules/@supabase/postgrest-js": {
1006 | "version": "2.78.0",
1007 | "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.78.0.tgz",
1008 | "integrity": "sha512-AwhpYlSvJ+PSnPmIK8sHj7NGDyDENYfQGKrMtpVIEzQA2ApUjgpUGxzXWN4Z0wEtLQsvv7g4y9HVad9Hzo1TNA==",
1009 | "license": "MIT",
1010 | "dependencies": {
1011 | "@supabase/node-fetch": "2.6.15",
1012 | "tslib": "2.8.1"
1013 | }
1014 | },
1015 | "node_modules/@supabase/realtime-js": {
1016 | "version": "2.78.0",
1017 | "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.78.0.tgz",
1018 | "integrity": "sha512-rCs1zmLe7of7hj4s7G9z8rTqzWuNVtmwDr3FiCRCJFawEoa+RQO1xpZGbdeuVvVmKDyVN6b542Okci+117y/LQ==",
1019 | "license": "MIT",
1020 | "dependencies": {
1021 | "@supabase/node-fetch": "2.6.15",
1022 | "@types/phoenix": "^1.6.6",
1023 | "@types/ws": "^8.18.1",
1024 | "tslib": "2.8.1",
1025 | "ws": "^8.18.2"
1026 | }
1027 | },
1028 | "node_modules/@supabase/realtime-js/node_modules/ws": {
1029 | "version": "8.18.3",
1030 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
1031 | "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
1032 | "license": "MIT",
1033 | "engines": {
1034 | "node": ">=10.0.0"
1035 | },
1036 | "peerDependencies": {
1037 | "bufferutil": "^4.0.1",
1038 | "utf-8-validate": ">=5.0.2"
1039 | },
1040 | "peerDependenciesMeta": {
1041 | "bufferutil": {
1042 | "optional": true
1043 | },
1044 | "utf-8-validate": {
1045 | "optional": true
1046 | }
1047 | }
1048 | },
1049 | "node_modules/@supabase/storage-js": {
1050 | "version": "2.78.0",
1051 | "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.78.0.tgz",
1052 | "integrity": "sha512-n17P0JbjHOlxqJpkaGFOn97i3EusEKPEbWOpuk1r4t00Wg06B8Z4GUiq0O0n1vUpjiMgJUkLIMuBVp+bEgunzQ==",
1053 | "license": "MIT",
1054 | "dependencies": {
1055 | "@supabase/node-fetch": "2.6.15",
1056 | "tslib": "2.8.1"
1057 | }
1058 | },
1059 | "node_modules/@supabase/supabase-js": {
1060 | "version": "2.78.0",
1061 | "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.78.0.tgz",
1062 | "integrity": "sha512-xYMRNBFmKp2m1gMuwcp/gr/HlfZKqjye1Ib8kJe29XJNsgwsfO/f8skxnWiscFKTlkOKLuBexNgl5L8dzGt6vA==",
1063 | "license": "MIT",
1064 | "dependencies": {
1065 | "@supabase/auth-js": "2.78.0",
1066 | "@supabase/functions-js": "2.78.0",
1067 | "@supabase/node-fetch": "2.6.15",
1068 | "@supabase/postgrest-js": "2.78.0",
1069 | "@supabase/realtime-js": "2.78.0",
1070 | "@supabase/storage-js": "2.78.0"
1071 | }
1072 | },
1073 | "node_modules/@types/node": {
1074 | "version": "24.10.0",
1075 | "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz",
1076 | "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
1077 | "license": "MIT",
1078 | "dependencies": {
1079 | "undici-types": "~7.16.0"
1080 | }
1081 | },
1082 | "node_modules/@types/phoenix": {
1083 | "version": "1.6.6",
1084 | "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
1085 | "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
1086 | "license": "MIT"
1087 | },
1088 | "node_modules/@types/ws": {
1089 | "version": "8.18.1",
1090 | "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
1091 | "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
1092 | "license": "MIT",
1093 | "dependencies": {
1094 | "@types/node": "*"
1095 | }
1096 | },
1097 | "node_modules/acorn": {
1098 | "version": "8.14.0",
1099 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
1100 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
1101 | "license": "MIT",
1102 | "bin": {
1103 | "acorn": "bin/acorn"
1104 | },
1105 | "engines": {
1106 | "node": ">=0.4.0"
1107 | }
1108 | },
1109 | "node_modules/acorn-walk": {
1110 | "version": "8.3.2",
1111 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
1112 | "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
1113 | "license": "MIT",
1114 | "engines": {
1115 | "node": ">=0.4.0"
1116 | }
1117 | },
1118 | "node_modules/blake3-wasm": {
1119 | "version": "2.1.5",
1120 | "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
1121 | "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
1122 | "license": "MIT"
1123 | },
1124 | "node_modules/buffer-equal-constant-time": {
1125 | "version": "1.0.1",
1126 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
1127 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
1128 | "license": "BSD-3-Clause"
1129 | },
1130 | "node_modules/color": {
1131 | "version": "4.2.3",
1132 | "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
1133 | "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
1134 | "license": "MIT",
1135 | "dependencies": {
1136 | "color-convert": "^2.0.1",
1137 | "color-string": "^1.9.0"
1138 | },
1139 | "engines": {
1140 | "node": ">=12.5.0"
1141 | }
1142 | },
1143 | "node_modules/color-convert": {
1144 | "version": "2.0.1",
1145 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1146 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1147 | "license": "MIT",
1148 | "dependencies": {
1149 | "color-name": "~1.1.4"
1150 | },
1151 | "engines": {
1152 | "node": ">=7.0.0"
1153 | }
1154 | },
1155 | "node_modules/color-name": {
1156 | "version": "1.1.4",
1157 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1158 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1159 | "license": "MIT"
1160 | },
1161 | "node_modules/color-string": {
1162 | "version": "1.9.1",
1163 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
1164 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
1165 | "license": "MIT",
1166 | "dependencies": {
1167 | "color-name": "^1.0.0",
1168 | "simple-swizzle": "^0.2.2"
1169 | }
1170 | },
1171 | "node_modules/cookie": {
1172 | "version": "1.0.2",
1173 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
1174 | "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
1175 | "license": "MIT",
1176 | "engines": {
1177 | "node": ">=18"
1178 | }
1179 | },
1180 | "node_modules/defu": {
1181 | "version": "6.1.4",
1182 | "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
1183 | "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
1184 | "license": "MIT"
1185 | },
1186 | "node_modules/detect-libc": {
1187 | "version": "2.1.0",
1188 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz",
1189 | "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==",
1190 | "license": "Apache-2.0",
1191 | "engines": {
1192 | "node": ">=8"
1193 | }
1194 | },
1195 | "node_modules/ecdsa-sig-formatter": {
1196 | "version": "1.0.11",
1197 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
1198 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
1199 | "license": "Apache-2.0",
1200 | "dependencies": {
1201 | "safe-buffer": "^5.0.1"
1202 | }
1203 | },
1204 | "node_modules/error-stack-parser-es": {
1205 | "version": "1.0.5",
1206 | "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
1207 | "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
1208 | "license": "MIT",
1209 | "funding": {
1210 | "url": "https://github.com/sponsors/antfu"
1211 | }
1212 | },
1213 | "node_modules/esbuild": {
1214 | "version": "0.25.4",
1215 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
1216 | "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
1217 | "hasInstallScript": true,
1218 | "license": "MIT",
1219 | "bin": {
1220 | "esbuild": "bin/esbuild"
1221 | },
1222 | "engines": {
1223 | "node": ">=18"
1224 | },
1225 | "optionalDependencies": {
1226 | "@esbuild/aix-ppc64": "0.25.4",
1227 | "@esbuild/android-arm": "0.25.4",
1228 | "@esbuild/android-arm64": "0.25.4",
1229 | "@esbuild/android-x64": "0.25.4",
1230 | "@esbuild/darwin-arm64": "0.25.4",
1231 | "@esbuild/darwin-x64": "0.25.4",
1232 | "@esbuild/freebsd-arm64": "0.25.4",
1233 | "@esbuild/freebsd-x64": "0.25.4",
1234 | "@esbuild/linux-arm": "0.25.4",
1235 | "@esbuild/linux-arm64": "0.25.4",
1236 | "@esbuild/linux-ia32": "0.25.4",
1237 | "@esbuild/linux-loong64": "0.25.4",
1238 | "@esbuild/linux-mips64el": "0.25.4",
1239 | "@esbuild/linux-ppc64": "0.25.4",
1240 | "@esbuild/linux-riscv64": "0.25.4",
1241 | "@esbuild/linux-s390x": "0.25.4",
1242 | "@esbuild/linux-x64": "0.25.4",
1243 | "@esbuild/netbsd-arm64": "0.25.4",
1244 | "@esbuild/netbsd-x64": "0.25.4",
1245 | "@esbuild/openbsd-arm64": "0.25.4",
1246 | "@esbuild/openbsd-x64": "0.25.4",
1247 | "@esbuild/sunos-x64": "0.25.4",
1248 | "@esbuild/win32-arm64": "0.25.4",
1249 | "@esbuild/win32-ia32": "0.25.4",
1250 | "@esbuild/win32-x64": "0.25.4"
1251 | }
1252 | },
1253 | "node_modules/exit-hook": {
1254 | "version": "2.2.1",
1255 | "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
1256 | "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==",
1257 | "license": "MIT",
1258 | "engines": {
1259 | "node": ">=6"
1260 | },
1261 | "funding": {
1262 | "url": "https://github.com/sponsors/sindresorhus"
1263 | }
1264 | },
1265 | "node_modules/exsolve": {
1266 | "version": "1.0.7",
1267 | "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
1268 | "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
1269 | "license": "MIT"
1270 | },
1271 | "node_modules/fsevents": {
1272 | "version": "2.3.3",
1273 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1274 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1275 | "hasInstallScript": true,
1276 | "optional": true,
1277 | "os": [
1278 | "darwin"
1279 | ],
1280 | "engines": {
1281 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1282 | }
1283 | },
1284 | "node_modules/glob-to-regexp": {
1285 | "version": "0.4.1",
1286 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
1287 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
1288 | "license": "BSD-2-Clause"
1289 | },
1290 | "node_modules/is-arrayish": {
1291 | "version": "0.3.4",
1292 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
1293 | "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
1294 | "license": "MIT"
1295 | },
1296 | "node_modules/jose": {
1297 | "version": "6.1.0",
1298 | "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz",
1299 | "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==",
1300 | "license": "MIT",
1301 | "funding": {
1302 | "url": "https://github.com/sponsors/panva"
1303 | }
1304 | },
1305 | "node_modules/jsonwebtoken": {
1306 | "version": "9.0.2",
1307 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
1308 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
1309 | "license": "MIT",
1310 | "dependencies": {
1311 | "jws": "^3.2.2",
1312 | "lodash.includes": "^4.3.0",
1313 | "lodash.isboolean": "^3.0.3",
1314 | "lodash.isinteger": "^4.0.4",
1315 | "lodash.isnumber": "^3.0.3",
1316 | "lodash.isplainobject": "^4.0.6",
1317 | "lodash.isstring": "^4.0.1",
1318 | "lodash.once": "^4.0.0",
1319 | "ms": "^2.1.1",
1320 | "semver": "^7.5.4"
1321 | },
1322 | "engines": {
1323 | "node": ">=12",
1324 | "npm": ">=6"
1325 | }
1326 | },
1327 | "node_modules/jwa": {
1328 | "version": "1.4.2",
1329 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
1330 | "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
1331 | "license": "MIT",
1332 | "dependencies": {
1333 | "buffer-equal-constant-time": "^1.0.1",
1334 | "ecdsa-sig-formatter": "1.0.11",
1335 | "safe-buffer": "^5.0.1"
1336 | }
1337 | },
1338 | "node_modules/jws": {
1339 | "version": "3.2.2",
1340 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
1341 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
1342 | "license": "MIT",
1343 | "dependencies": {
1344 | "jwa": "^1.4.1",
1345 | "safe-buffer": "^5.0.1"
1346 | }
1347 | },
1348 | "node_modules/kleur": {
1349 | "version": "4.1.5",
1350 | "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
1351 | "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
1352 | "license": "MIT",
1353 | "engines": {
1354 | "node": ">=6"
1355 | }
1356 | },
1357 | "node_modules/lodash.includes": {
1358 | "version": "4.3.0",
1359 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
1360 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
1361 | "license": "MIT"
1362 | },
1363 | "node_modules/lodash.isboolean": {
1364 | "version": "3.0.3",
1365 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
1366 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
1367 | "license": "MIT"
1368 | },
1369 | "node_modules/lodash.isinteger": {
1370 | "version": "4.0.4",
1371 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
1372 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
1373 | "license": "MIT"
1374 | },
1375 | "node_modules/lodash.isnumber": {
1376 | "version": "3.0.3",
1377 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
1378 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
1379 | "license": "MIT"
1380 | },
1381 | "node_modules/lodash.isplainobject": {
1382 | "version": "4.0.6",
1383 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
1384 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
1385 | "license": "MIT"
1386 | },
1387 | "node_modules/lodash.isstring": {
1388 | "version": "4.0.1",
1389 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
1390 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
1391 | "license": "MIT"
1392 | },
1393 | "node_modules/lodash.once": {
1394 | "version": "4.1.1",
1395 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
1396 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
1397 | "license": "MIT"
1398 | },
1399 | "node_modules/mime": {
1400 | "version": "3.0.0",
1401 | "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
1402 | "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
1403 | "license": "MIT",
1404 | "bin": {
1405 | "mime": "cli.js"
1406 | },
1407 | "engines": {
1408 | "node": ">=10.0.0"
1409 | }
1410 | },
1411 | "node_modules/miniflare": {
1412 | "version": "4.20250917.0",
1413 | "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250917.0.tgz",
1414 | "integrity": "sha512-A7kYEc/Y6ohiiTji4W/qGJj3aJNc/9IMj/6wLy2phD/iMjcoY8t35654gR5mHbMx0AgUolDdr3HOsHB0cYBf+Q==",
1415 | "license": "MIT",
1416 | "dependencies": {
1417 | "@cspotcode/source-map-support": "0.8.1",
1418 | "acorn": "8.14.0",
1419 | "acorn-walk": "8.3.2",
1420 | "exit-hook": "2.2.1",
1421 | "glob-to-regexp": "0.4.1",
1422 | "sharp": "^0.33.5",
1423 | "stoppable": "1.1.0",
1424 | "undici": "7.14.0",
1425 | "workerd": "1.20250917.0",
1426 | "ws": "8.18.0",
1427 | "youch": "4.1.0-beta.10",
1428 | "zod": "3.22.3"
1429 | },
1430 | "bin": {
1431 | "miniflare": "bootstrap.js"
1432 | },
1433 | "engines": {
1434 | "node": ">=18.0.0"
1435 | }
1436 | },
1437 | "node_modules/ms": {
1438 | "version": "2.1.3",
1439 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1440 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1441 | "license": "MIT"
1442 | },
1443 | "node_modules/ohash": {
1444 | "version": "2.0.11",
1445 | "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
1446 | "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
1447 | "license": "MIT"
1448 | },
1449 | "node_modules/path-to-regexp": {
1450 | "version": "6.3.0",
1451 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
1452 | "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
1453 | "license": "MIT"
1454 | },
1455 | "node_modules/pathe": {
1456 | "version": "2.0.3",
1457 | "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
1458 | "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
1459 | "license": "MIT"
1460 | },
1461 | "node_modules/safe-buffer": {
1462 | "version": "5.2.1",
1463 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1464 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1465 | "funding": [
1466 | {
1467 | "type": "github",
1468 | "url": "https://github.com/sponsors/feross"
1469 | },
1470 | {
1471 | "type": "patreon",
1472 | "url": "https://www.patreon.com/feross"
1473 | },
1474 | {
1475 | "type": "consulting",
1476 | "url": "https://feross.org/support"
1477 | }
1478 | ],
1479 | "license": "MIT"
1480 | },
1481 | "node_modules/semver": {
1482 | "version": "7.7.2",
1483 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
1484 | "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
1485 | "bin": {
1486 | "semver": "bin/semver.js"
1487 | },
1488 | "engines": {
1489 | "node": ">=10"
1490 | }
1491 | },
1492 | "node_modules/sharp": {
1493 | "version": "0.33.5",
1494 | "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
1495 | "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
1496 | "hasInstallScript": true,
1497 | "license": "Apache-2.0",
1498 | "dependencies": {
1499 | "color": "^4.2.3",
1500 | "detect-libc": "^2.0.3",
1501 | "semver": "^7.6.3"
1502 | },
1503 | "engines": {
1504 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1505 | },
1506 | "funding": {
1507 | "url": "https://opencollective.com/libvips"
1508 | },
1509 | "optionalDependencies": {
1510 | "@img/sharp-darwin-arm64": "0.33.5",
1511 | "@img/sharp-darwin-x64": "0.33.5",
1512 | "@img/sharp-libvips-darwin-arm64": "1.0.4",
1513 | "@img/sharp-libvips-darwin-x64": "1.0.4",
1514 | "@img/sharp-libvips-linux-arm": "1.0.5",
1515 | "@img/sharp-libvips-linux-arm64": "1.0.4",
1516 | "@img/sharp-libvips-linux-s390x": "1.0.4",
1517 | "@img/sharp-libvips-linux-x64": "1.0.4",
1518 | "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
1519 | "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
1520 | "@img/sharp-linux-arm": "0.33.5",
1521 | "@img/sharp-linux-arm64": "0.33.5",
1522 | "@img/sharp-linux-s390x": "0.33.5",
1523 | "@img/sharp-linux-x64": "0.33.5",
1524 | "@img/sharp-linuxmusl-arm64": "0.33.5",
1525 | "@img/sharp-linuxmusl-x64": "0.33.5",
1526 | "@img/sharp-wasm32": "0.33.5",
1527 | "@img/sharp-win32-ia32": "0.33.5",
1528 | "@img/sharp-win32-x64": "0.33.5"
1529 | }
1530 | },
1531 | "node_modules/simple-swizzle": {
1532 | "version": "0.2.4",
1533 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
1534 | "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
1535 | "license": "MIT",
1536 | "dependencies": {
1537 | "is-arrayish": "^0.3.1"
1538 | }
1539 | },
1540 | "node_modules/stoppable": {
1541 | "version": "1.1.0",
1542 | "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
1543 | "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
1544 | "license": "MIT",
1545 | "engines": {
1546 | "node": ">=4",
1547 | "npm": ">=6"
1548 | }
1549 | },
1550 | "node_modules/supports-color": {
1551 | "version": "10.2.2",
1552 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
1553 | "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
1554 | "license": "MIT",
1555 | "engines": {
1556 | "node": ">=18"
1557 | },
1558 | "funding": {
1559 | "url": "https://github.com/chalk/supports-color?sponsor=1"
1560 | }
1561 | },
1562 | "node_modules/tr46": {
1563 | "version": "0.0.3",
1564 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
1565 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
1566 | "license": "MIT"
1567 | },
1568 | "node_modules/tslib": {
1569 | "version": "2.8.1",
1570 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1571 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1572 | "license": "0BSD"
1573 | },
1574 | "node_modules/ufo": {
1575 | "version": "1.6.1",
1576 | "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
1577 | "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
1578 | "license": "MIT"
1579 | },
1580 | "node_modules/undici": {
1581 | "version": "7.14.0",
1582 | "resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz",
1583 | "integrity": "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ==",
1584 | "license": "MIT",
1585 | "engines": {
1586 | "node": ">=20.18.1"
1587 | }
1588 | },
1589 | "node_modules/undici-types": {
1590 | "version": "7.16.0",
1591 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
1592 | "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
1593 | "license": "MIT"
1594 | },
1595 | "node_modules/unenv": {
1596 | "version": "2.0.0-rc.21",
1597 | "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.21.tgz",
1598 | "integrity": "sha512-Wj7/AMtE9MRnAXa6Su3Lk0LNCfqDYgfwVjwRFVum9U7wsto1imuHqk4kTm7Jni+5A0Hn7dttL6O/zjvUvoo+8A==",
1599 | "license": "MIT",
1600 | "dependencies": {
1601 | "defu": "^6.1.4",
1602 | "exsolve": "^1.0.7",
1603 | "ohash": "^2.0.11",
1604 | "pathe": "^2.0.3",
1605 | "ufo": "^1.6.1"
1606 | }
1607 | },
1608 | "node_modules/webidl-conversions": {
1609 | "version": "3.0.1",
1610 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
1611 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
1612 | "license": "BSD-2-Clause"
1613 | },
1614 | "node_modules/whatwg-url": {
1615 | "version": "5.0.0",
1616 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
1617 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
1618 | "license": "MIT",
1619 | "dependencies": {
1620 | "tr46": "~0.0.3",
1621 | "webidl-conversions": "^3.0.0"
1622 | }
1623 | },
1624 | "node_modules/workerd": {
1625 | "version": "1.20250917.0",
1626 | "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250917.0.tgz",
1627 | "integrity": "sha512-0D+wWaccyYQb2Zx2DZDC77YDn9kOpkpGMCgyKgIHilghut5hBQ/adUIEseS4iuIZxBPeFSn6zFtICP0SxZ3z0g==",
1628 | "hasInstallScript": true,
1629 | "license": "Apache-2.0",
1630 | "bin": {
1631 | "workerd": "bin/workerd"
1632 | },
1633 | "engines": {
1634 | "node": ">=16"
1635 | },
1636 | "optionalDependencies": {
1637 | "@cloudflare/workerd-darwin-64": "1.20250917.0",
1638 | "@cloudflare/workerd-darwin-arm64": "1.20250917.0",
1639 | "@cloudflare/workerd-linux-64": "1.20250917.0",
1640 | "@cloudflare/workerd-linux-arm64": "1.20250917.0",
1641 | "@cloudflare/workerd-windows-64": "1.20250917.0"
1642 | }
1643 | },
1644 | "node_modules/wrangler": {
1645 | "version": "4.38.0",
1646 | "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.38.0.tgz",
1647 | "integrity": "sha512-ITL4VZ4KWs8LMDEttDTrAKLktwtv1NxHBd5QIqHOczvcjnAQr+GQoE6XYQws+w8jlOjDV7KyvbFqAdyRh5om3g==",
1648 | "license": "MIT OR Apache-2.0",
1649 | "dependencies": {
1650 | "@cloudflare/kv-asset-handler": "0.4.0",
1651 | "@cloudflare/unenv-preset": "2.7.4",
1652 | "blake3-wasm": "2.1.5",
1653 | "esbuild": "0.25.4",
1654 | "miniflare": "4.20250917.0",
1655 | "path-to-regexp": "6.3.0",
1656 | "unenv": "2.0.0-rc.21",
1657 | "workerd": "1.20250917.0"
1658 | },
1659 | "bin": {
1660 | "wrangler": "bin/wrangler.js",
1661 | "wrangler2": "bin/wrangler.js"
1662 | },
1663 | "engines": {
1664 | "node": ">=18.0.0"
1665 | },
1666 | "optionalDependencies": {
1667 | "fsevents": "~2.3.2"
1668 | },
1669 | "peerDependencies": {
1670 | "@cloudflare/workers-types": "^4.20250917.0"
1671 | },
1672 | "peerDependenciesMeta": {
1673 | "@cloudflare/workers-types": {
1674 | "optional": true
1675 | }
1676 | }
1677 | },
1678 | "node_modules/ws": {
1679 | "version": "8.18.0",
1680 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
1681 | "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
1682 | "license": "MIT",
1683 | "engines": {
1684 | "node": ">=10.0.0"
1685 | },
1686 | "peerDependencies": {
1687 | "bufferutil": "^4.0.1",
1688 | "utf-8-validate": ">=5.0.2"
1689 | },
1690 | "peerDependenciesMeta": {
1691 | "bufferutil": {
1692 | "optional": true
1693 | },
1694 | "utf-8-validate": {
1695 | "optional": true
1696 | }
1697 | }
1698 | },
1699 | "node_modules/youch": {
1700 | "version": "4.1.0-beta.10",
1701 | "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
1702 | "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",
1703 | "license": "MIT",
1704 | "dependencies": {
1705 | "@poppinss/colors": "^4.1.5",
1706 | "@poppinss/dumper": "^0.6.4",
1707 | "@speed-highlight/core": "^1.2.7",
1708 | "cookie": "^1.0.2",
1709 | "youch-core": "^0.3.3"
1710 | }
1711 | },
1712 | "node_modules/youch-core": {
1713 | "version": "0.3.3",
1714 | "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",
1715 | "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",
1716 | "license": "MIT",
1717 | "dependencies": {
1718 | "@poppinss/exception": "^1.2.2",
1719 | "error-stack-parser-es": "^1.0.5"
1720 | }
1721 | },
1722 | "node_modules/zod": {
1723 | "version": "3.22.3",
1724 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
1725 | "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
1726 | "license": "MIT",
1727 | "funding": {
1728 | "url": "https://github.com/sponsors/colinhacks"
1729 | }
1730 | }
1731 | }
1732 | }
1733 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "jose": "^6.1.0",
4 | "jsonwebtoken": "^9.0.2",
5 | "wrangler": "^4.38.0",
6 | "@supabase/supabase-js": "^2.78.0"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/api-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./api-config.schema.json",
3 | "title": "API Gateway Config",
4 | "description": "Configuration for the Serverless API Gateway",
5 | "servers": [
6 | {
7 | "alias": "serverlessapigateway-api",
8 | "url": "https://74ec-2a02-e0-665f-2400-4803-52e0-7bcf-8789.ngrok-free.app"
9 | },
10 | {
11 | "alias": "serverlessapigateway-api-sub",
12 | "url": "https://4e05-2a02-e0-665f-2400-e945-4e3-409c-d532.ngrok-free.app/sub"
13 | }
14 | ],
15 | "services": [
16 | {
17 | "alias": "endpoint1",
18 | "entrypoint": "./services/endpoint1"
19 | },
20 | {
21 | "alias": "endpoint2",
22 | "entrypoint": "services/endpoint2"
23 | },
24 | {
25 | "alias": "endpoint3",
26 | "entrypoint": "./endpoint3"
27 | }
28 | ],
29 | "cors": {
30 | "allow_origins": ["https://api1.serverlessapigateway.com", "http://api1.serverlessapigateway.com", "https://api2.serverlessapigateway.com"],
31 | "allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
32 | "allow_headers": ["*"],
33 | "expose_headers": ["*"],
34 | "allow_credentials": true,
35 | "max_age": 3600
36 | },
37 | "authorizer": {
38 | "type": "auth0",
39 | "domain": "$env.AUTH0_DOMAIN",
40 | "client_id": "$env.AUTH0_CLIENT_ID",
41 | "client_secret": "$secret.AUTH0_CLIENT_SECRET",
42 | "redirect_uri": "https://api-test.xx.com/api/v1/auth0/callback",
43 | "callback_uri": "https://api-test.xx.com/api/v1/auth0/callback-redirect",
44 | "jwks": "$secret.AUTH0_JWKS",
45 | "jwks_uri": "https://xx.us.auth0.com/.well-known/jwks.json",
46 | "scope": "openid profile email"
47 | },
48 | "variables": {
49 | "global_variable": "global_variable_value"
50 | },
51 | "paths": [
52 | {
53 | "method": "GET",
54 | "path": "/api/v1/mapping",
55 | "integration": {
56 | "type": "http_proxy",
57 | "server": "serverlessapigateway-api"
58 | },
59 | "auth": true,
60 | "mapping": {
61 | "headers": {
62 | "x-jwt-sub": "$request.jwt.sub",
63 | "x-jwt-aud": "$request.jwt.aud",
64 | "x-jwt-iss": "$request.jwt.iss",
65 | "x-jwt-name": "$request.jwt.name",
66 | "x-jwt-email": "$request.jwt.email",
67 | "x-config-api-key": "$config.api_key",
68 | "x-config-database-url": "$config.database-url",
69 | "x-config-nested-config-key": "$config.nested.config.key",
70 | "x-query-userId": "$request.query.userId",
71 | "x-query-redirect_uri": "$request.query.redirect_uri",
72 | "x-global-variable": "$config.global_variable"
73 | },
74 | "query": {
75 | "jwt-sub": "$request.jwt.sub",
76 | "jwt-aud": "$request.jwt.aud",
77 | "jwt-iss": "$request.jwt.iss",
78 | "jwt-name": "$request.jwt.name",
79 | "jwt-email": "$request.jwt.email",
80 | "config-api-key": "$config.api_key",
81 | "config-database-url": "$config.database-url",
82 | "config-nested-config-key": "$config.nested.config.key"
83 | }
84 | },
85 | "variables": {
86 | "api_key": "API_KEY_VALUE",
87 | "database-url": "sqlite://db.sqlite",
88 | "nested.config.key": "nested config value",
89 | "global_variable": "this-not-global-variable"
90 | }
91 | },
92 | {
93 | "method": "GET",
94 | "path": "/api/v1/auth",
95 | "response": {
96 | "status": "this is authenticated GET method"
97 | },
98 | "auth": true
99 | },
100 | {
101 | "method": "GET",
102 | "path": "/api/v1/no-auth",
103 | "response": {
104 | "status": "this is un-authenticated GET method"
105 | },
106 | "auth": false
107 | },
108 | {
109 | "method": "GET",
110 | "path": "/api/v1/proxy",
111 | "integration": {
112 | "type": "http_proxy",
113 | "server": "serverlessapigateway-api"
114 | }
115 | },
116 | {
117 | "method": "GET",
118 | "path": "/api/v1/proxy/{parameter}",
119 | "integration": {
120 | "type": "http_proxy",
121 | "server": "serverlessapigateway-api"
122 | }
123 | },
124 | {
125 | "method": "ANY",
126 | "path": "/api/v1/proxy/{.+}",
127 | "integration": {
128 | "type": "http_proxy",
129 | "server": "serverlessapigateway-api"
130 | }
131 | },
132 | {
133 | "method": "ANY",
134 | "path": "/{.+}",
135 | "integration": {
136 | "type": "http_proxy",
137 | "server": "serverlessapigateway-api"
138 | }
139 | },
140 | {
141 | "method": "GET",
142 | "path": "/api/v1/proxy/sub",
143 | "integration": {
144 | "type": "http_proxy",
145 | "server": "serverlessapigateway-api-sub"
146 | }
147 | },
148 | {
149 | "method": "GET",
150 | "path": "/api/v1/method",
151 | "response": {
152 | "status": "this is GET method"
153 | }
154 | },
155 | {
156 | "method": "POST",
157 | "path": "/api/v1/method",
158 | "response": {
159 | "status": "this is POST method"
160 | }
161 | },
162 | {
163 | "method": "ANY",
164 | "path": "/api/v1/method",
165 | "response": {
166 | "status": "this is ANY method"
167 | }
168 | },
169 | {
170 | "method": "OPTIONS",
171 | "path": "/api/v1/method",
172 | "response": {
173 | "status": "this is OPTIONS method"
174 | }
175 | },
176 | {
177 | "method": "POST",
178 | "path": "/api/v1/proxy",
179 | "integration": {
180 | "type": "http_proxy",
181 | "server": "serverlessapigateway-api"
182 | }
183 | },
184 | {
185 | "method": "GET",
186 | "path": "/api/v1/health",
187 | "response": {
188 | "status": "ok"
189 | }
190 | },
191 | {
192 | "method": "GET",
193 | "path": "/api/v1/health/ready",
194 | "response": {
195 | "status": "ready ok"
196 | }
197 | },
198 | {
199 | "method": "GET",
200 | "path": "/api/v1/health/live",
201 | "response": {
202 | "status": "live ok"
203 | }
204 | },
205 | {
206 | "method": "GET",
207 | "path": "/api/v1/health/string",
208 | "response": "string ok"
209 | },
210 | {
211 | "method": "POST",
212 | "path": "/api/v1/health",
213 | "response": {
214 | "status": "ok"
215 | }
216 | },
217 | {
218 | "method": "ANY",
219 | "path": "/api/v1/health/any",
220 | "response": {
221 | "status": "ok"
222 | }
223 | },
224 | {
225 | "method": "ANY",
226 | "path": "/api/v1/env",
227 | "response": {
228 | "status": "$env.VAR_TEST_RESPONSE_TEXT"
229 | }
230 | },
231 | {
232 | "method": "ANY",
233 | "path": "/api/v1/secret",
234 | "response": {
235 | "status": "$secrets.VAR_TEST_RESPONSE_TEXT_SECRET"
236 | }
237 | },
238 | {
239 | "method": "GET",
240 | "path": "/api/v1/endpoint1",
241 | "integration": {
242 | "type": "service",
243 | "binding": "endpoint1"
244 | }
245 | },
246 | {
247 | "method": "GET",
248 | "path": "/api/v1/endpoint2",
249 | "integration": {
250 | "type": "service",
251 | "binding": "endpoint2"
252 | }
253 | },
254 | {
255 | "method": "GET",
256 | "path": "/api/v1/endpoint3",
257 | "integration": {
258 | "type": "service",
259 | "binding": "endpoint3"
260 | }
261 | },
262 | {
263 | "method": "GET",
264 | "path": "/api/v1/auth0/callback",
265 | "integration": {
266 | "type": "auth0_callback"
267 | }
268 | },
269 | {
270 | "method": "GET",
271 | "path": "/api/v1/auth0/profile",
272 | "integration": {
273 | "type": "auth0_userinfo"
274 | },
275 | "auth": true
276 | },
277 | {
278 | "method": "GET",
279 | "path": "/api/v1/auth0/callback-redirect",
280 | "integration": {
281 | "type": "auth0_callback_redirect"
282 | },
283 | "auth": false
284 | }
285 | ]
286 | }
287 |
--------------------------------------------------------------------------------
/src/api-config.schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json-schema.org/draft/2020-12/schema",
3 | "$id": "https://serverlessapigateway.com/api-config.schema.json",
4 | "title": "API Gateway Config",
5 | "description": "Configuration for the Serverless API Gateway",
6 | "type": "object",
7 | "properties": {
8 | "servers": {
9 | "type": "array",
10 | "items": {
11 | "type": "object",
12 | "properties": {
13 | "alias": {
14 | "type": "string"
15 | },
16 | "url": {
17 | "type": "string",
18 | "format": "idn-hostname"
19 | }
20 | },
21 | "required": [
22 | "alias",
23 | "url"
24 | ]
25 | }
26 | },
27 | "services": {
28 | "type": "array",
29 | "items": {
30 | "type": "object",
31 | "properties": {
32 | "alias": {
33 | "type": "string"
34 | },
35 | "entrypoint": {
36 | "type": "string"
37 | }
38 | },
39 | "required": [
40 | "alias",
41 | "entrypoint"
42 | ]
43 | }
44 | },
45 | "servicesBindings": {
46 | "type": "array",
47 | "items": {
48 | "type": "object",
49 | "properties": {
50 | "alias": {
51 | "type": "string"
52 | },
53 | "binding": {
54 | "type": "string"
55 | }
56 | },
57 | "required": [
58 | "alias",
59 | "binding"
60 | ]
61 | }
62 | },
63 | "cors": {
64 | "type": "object",
65 | "properties": {
66 | "allow_origins": {
67 | "type": "array",
68 | "uniqueItems": true,
69 | "items": {
70 | "anyOf": [
71 | {
72 | "type": "string",
73 | "format": "idn-hostname"
74 | },
75 | {
76 | "const": "*"
77 | }
78 | ]
79 | }
80 | },
81 | "allow_methods": {
82 | "type": "array",
83 | "uniqueItems": true,
84 | "items": {
85 | "type": "string",
86 | "enum": [
87 | "GET",
88 | "POST",
89 | "PUT",
90 | "DELETE",
91 | "OPTIONS",
92 | "PATCH",
93 | "*"
94 | ]
95 | }
96 | },
97 | "allow_headers": {
98 | "type": "array",
99 | "items": {
100 | "type": "string"
101 | }
102 | },
103 | "expose_headers": {
104 | "type": "array",
105 | "items": {
106 | "type": "string"
107 | }
108 | },
109 | "allow_credentials": {
110 | "type": "boolean"
111 | },
112 | "max_age": {
113 | "type": "integer"
114 | }
115 | },
116 | "required": [
117 | "allow_origins",
118 | "allow_methods",
119 | "allow_headers",
120 | "expose_headers",
121 | "allow_credentials",
122 | "max_age"
123 | ]
124 | },
125 | "authorizer": {
126 | "oneOf": [
127 | {
128 | "type": "object",
129 | "properties": {
130 | "type": {
131 | "const": "jwt"
132 | },
133 | "secret": {
134 | "type": "string"
135 | },
136 | "algorithm": {
137 | "const": "HS256"
138 | },
139 | "audience": {
140 | "type": "string"
141 | },
142 | "issuer": {
143 | "type": "string"
144 | }
145 | },
146 | "required": [
147 | "type",
148 | "secret",
149 | "algorithm",
150 | "audience",
151 | "issuer"
152 | ]
153 | },
154 | {
155 | "type": "object",
156 | "properties": {
157 | "type": {
158 | "const": "auth0"
159 | },
160 | "domain": {
161 | "type": "string"
162 | },
163 | "client_id": {
164 | "type": "string"
165 | },
166 | "client_secret": {
167 | "type": "string"
168 | },
169 | "redirect_uri": {
170 | "type": "string"
171 | },
172 | "callback_uri": {
173 | "type": "string"
174 | },
175 | "jwks": {
176 | "type": "string"
177 | },
178 | "jwks_uri": {
179 | "type": "string"
180 | },
181 | "scope": {
182 | "type": "string"
183 | }
184 | },
185 | "required": [
186 | "type",
187 | "domain",
188 | "client_id",
189 | "client_secret",
190 | "redirect_uri",
191 | "callback_uri",
192 | "scope"
193 | ],
194 | "anyOf": [
195 | {
196 | "required": [
197 | "jwks"
198 | ]
199 | },
200 | {
201 | "required": [
202 | "jwks_uri"
203 | ]
204 | }
205 | ]
206 | },
207 | {
208 | "type": "object",
209 | "properties": {
210 | "type": {
211 | "const": "supabase"
212 | },
213 | "jwt_secret": {
214 | "type": "string"
215 | },
216 | "issuer": {
217 | "type": "string"
218 | },
219 | "audience": {
220 | "type": "string"
221 | }
222 | },
223 | "required": [
224 | "type",
225 | "jwt_secret",
226 | "issuer",
227 | "audience"
228 | ]
229 | }
230 | ]
231 | },
232 | "paths": {
233 | "type": "array",
234 | "items": {
235 | "type": "object",
236 | "properties": {
237 | "method": {
238 | "enum": [
239 | "GET",
240 | "POST",
241 | "PUT",
242 | "DELETE",
243 | "OPTIONS",
244 | "PATCH",
245 | "ANY"
246 | ]
247 | },
248 | "path": {
249 | "type": "string",
250 | "format": "pathStart"
251 | },
252 | "integration": {
253 | "type": "object",
254 | "properties": {
255 | "type": {
256 | "enum": [
257 | "http",
258 | "http_proxy",
259 | "service",
260 | "service_binding",
261 | "auth0_callback",
262 | "auth0_userinfo",
263 | "auth0_callback_redirect",
264 | "auth0_refresh",
265 | "supabase_passwordless_auth",
266 | "supabase_passwordless_verify",
267 | "supabase_passwordless_auth_alt"
268 | ]
269 | },
270 | "server": {
271 | "type": "string",
272 | "$ref": "#/properties/servers/items/properties/alias"
273 | },
274 | "service": {
275 | "type": "string"
276 | }
277 | },
278 | "required": [
279 | "type"
280 | ]
281 | },
282 | "auth": {
283 | "type": [
284 | "boolean",
285 | "null"
286 | ]
287 | },
288 | "mapping": {
289 | "type": "object",
290 | "properties": {
291 | "headers": {
292 | "type": "object"
293 | },
294 | "query": {
295 | "type": "object"
296 | }
297 | }
298 | },
299 | "variables": {
300 | "type": "object"
301 | },
302 | "response": {
303 | "type": [
304 | "object",
305 | "string"
306 | ],
307 | "properties": {
308 | "status": {
309 | "type": "string"
310 | }
311 | }
312 | }
313 | },
314 | "required": [
315 | "method",
316 | "path"
317 | ]
318 | }
319 | },
320 | "variables": {
321 | "type": "object"
322 | },
323 | "required": [
324 | "paths"
325 | ]
326 | }
327 | }
328 |
--------------------------------------------------------------------------------
/src/auth.js:
--------------------------------------------------------------------------------
1 | import {jwtVerify, errors } from 'jose';
2 | import { AuthError } from "./types/error_types";
3 |
4 | async function jwtAuth(request, apiConfig) {
5 | const secret = new TextEncoder().encode(apiConfig.authorizer?.secret);
6 | const authHeader = request.headers.get('Authorization');
7 | if (!authHeader || !authHeader.startsWith('Bearer ')) {
8 | throw new AuthError('No token provided or token format is invalid.', 'AUTH_ERROR', 401);
9 | }
10 | const jwt = authHeader.split(' ')[1];
11 |
12 | try {
13 | const { payload, protectedHeader } = await jwtVerify(jwt, secret, {
14 | issuer: apiConfig.authorizer?.issuer,
15 | audience: apiConfig.authorizer?.audience,
16 | });
17 |
18 | return payload;
19 | } catch (error) {
20 | if (error instanceof errors.JOSEAlgNotAllowed) {
21 | throw new AuthError('Algorithm not allowed', error.code, 401);
22 | } else if (error instanceof errors.JWEDecryptionFailed) {
23 | throw new AuthError('Decryption failed', error.code, 401);
24 | } else if (error instanceof errors.JWEInvalid) {
25 | throw new AuthError('Invalid JWE', error.code, 401);
26 | } else if (error instanceof errors.JWTExpired) {
27 | throw new AuthError('Token has expired.', error.code, 401);
28 | } else if (error instanceof errors.JWTClaimValidationFailed) {
29 | throw new AuthError('JWT claim validation failed', error.code, 401);
30 | } else if (error instanceof errors.JWTInvalid) {
31 | throw new AuthError('Invalid JWT', error.code, 401);
32 | } else if (error instanceof errors.JWKSNoMatchingKey) {
33 | throw new AuthError('No matching key found in JWKS.', error.code, 401);
34 | } else if (error instanceof errors.JWKSInvalid) {
35 | throw new AuthError('Invalid JWKS', error.code, 401);
36 | } else if (error instanceof errors.JWKSMultipleMatchingKeys) {
37 | throw new AuthError('Multiple matching keys found in JWKS.', error.code, 401);
38 | } else if (error instanceof errors.JWKSNoMatchingKey) {
39 | throw new AuthError('No matching key in JWKS.', error.code, 401);
40 | } else if (error instanceof errors.JWSInvalid) {
41 | throw new AuthError('Invalid JWS', error.code, 401);
42 | } else if (error instanceof errors.JWSSignatureVerificationFailed) {
43 | throw new AuthError('Signature verification failed', error.code, 401);
44 | } else if (error instanceof Error) {
45 | throw new AuthError('JWT verification failed', 'AUTH_ERROR', 401);
46 | }
47 | // Fallback in case error is not an instance of Error
48 | throw new AuthError('JWT verification failed due to an unexpected error.', 'AUTH_ERROR', 401);
49 | }
50 | }
51 |
52 | export { jwtAuth, AuthError };
53 |
--------------------------------------------------------------------------------
/src/common.js:
--------------------------------------------------------------------------------
1 | function safeStringify(obj) {
2 | return JSON.stringify(obj, (key, value) => {
3 | // Check if the value is a function, undefined, symbol, or a Promise
4 | if (typeof value === 'function' ||
5 | typeof value === 'undefined' ||
6 | typeof value === 'symbol' ||
7 | (typeof value === 'object' && value !== null && typeof value.then === 'function')) {
8 | return undefined; // Exclude these values
9 | }
10 | return value; // Include all other values
11 | });
12 | }
13 |
14 | function generateJsonResponse(input) {
15 | if (input instanceof Response) {
16 | return input;
17 | }
18 |
19 | if (input === null) {
20 | return new Response(null, {
21 | headers: {
22 | 'Content-Type': 'application/json',
23 | },
24 | status: 200,
25 | });
26 | }
27 |
28 | if (typeof input === 'string') {
29 | return new Response(input, {
30 | headers: {
31 | 'Content-Type': 'application/json',
32 | },
33 | status: 200,
34 | });
35 | }
36 |
37 | if (typeof input === 'object') {
38 | const { statusCode, error, message, ...data } = input;
39 | const responseBody = {
40 | status: error ? 'error' : 'success',
41 | message: message || (error ? 'An error occurred.' : 'Operation completed successfully.'),
42 | data: error ? undefined : data,
43 | error: error || undefined,
44 | };
45 |
46 | return new Response(JSON.stringify(responseBody), {
47 | headers: {
48 | 'Content-Type': 'application/json',
49 | },
50 | status: statusCode !== undefined ? statusCode : (error ? 500 : 200),
51 | });
52 | }
53 |
54 | // Default response for unsupported input types
55 | return new Response(null, {
56 | headers: {
57 | 'Content-Type': 'application/json',
58 | },
59 | status: 400,
60 | });
61 | }
62 |
63 | export { safeStringify, generateJsonResponse };
64 |
--------------------------------------------------------------------------------
/src/cors.js:
--------------------------------------------------------------------------------
1 | function setCorsHeaders(request, response, corsConfig) {
2 | const origin = request.headers.get('Origin');
3 | console.log('Origin:', origin);
4 |
5 | const matchingOrigin = corsConfig.allow_origins.find((allowedOrigin) => {
6 | if (allowedOrigin === origin) {
7 | return true;
8 | }
9 | if (allowedOrigin === '*') {
10 | return true;
11 | }
12 | // Handle wildcard patterns like "https://*.example.com"
13 | if (allowedOrigin.includes('*')) {
14 | const pattern = allowedOrigin.replace(/\*/g, '.*');
15 | const regex = new RegExp(`^${pattern}$`);
16 | return regex.test(origin);
17 | }
18 | return false;
19 | });
20 |
21 | console.log('Matching Origin:', matchingOrigin);
22 |
23 | const headers = new Headers(response.headers);
24 | headers.set('Access-Control-Allow-Origin', matchingOrigin || corsConfig.allow_origins[0]);
25 | headers.set('Access-Control-Allow-Methods', corsConfig.allow_methods.join(','));
26 | headers.set('Access-Control-Allow-Headers', corsConfig.allow_headers.join(','));
27 | headers.set('Access-Control-Expose-Headers', corsConfig.expose_headers.join(','));
28 | headers.set('Access-Control-Allow-Credentials', corsConfig.allow_credentials.toString());
29 | headers.set('Access-Control-Max-Age', corsConfig.max_age.toString());
30 |
31 | const newResponse = new Response(response.body, {
32 | status: response.status,
33 | statusText: response.statusText,
34 | headers: headers,
35 | });
36 | return newResponse;
37 | }
38 |
39 | export { setCorsHeaders };
40 |
--------------------------------------------------------------------------------
/src/enums/http-method.js:
--------------------------------------------------------------------------------
1 | const HttpMethod = {
2 | GET : 'GET',
3 | POST : 'POST',
4 | PUT : 'PUT',
5 | DELETE : 'DELETE',
6 | OPTIONS : 'OPTIONS',
7 | PATCH : 'PATCH',
8 | ALL : '*',
9 | }
10 |
11 | const RequestMethod = {
12 | GET : 'GET',
13 | POST : 'POST',
14 | PUT : 'PUT',
15 | DELETE : 'DELETE',
16 | OPTIONS : 'OPTIONS',
17 | PATCH : 'PATCH',
18 | ANY : 'ANY',
19 | }
20 |
21 | export { HttpMethod, RequestMethod };
--------------------------------------------------------------------------------
/src/enums/integration-type.js:
--------------------------------------------------------------------------------
1 | export const IntegrationTypeEnum = {
2 | HTTP : 'http',
3 | HTTP_PROXY : 'http_proxy',
4 | SERVICE : 'service',
5 | SERVICE_BINDING : 'service_binding',
6 | AUTH0CALLBACK : 'auth0_callback',
7 | AUTH0USERINFO : 'auth0_userinfo',
8 | AUTH0CALLBACKREDIRECT : 'auth0_callback_redirect',
9 | AUTH0REFRESH: 'auth0_refresh',
10 | SUPABASEPASSWORDLESSAUTH: 'supabase_passwordless_auth',
11 | SUPABASEPASSWORDLESSVERIFY: 'supabase_passwordless_verify',
12 | SUPABASEPASSWORDLESSAUTHALT: 'supabase_passwordless_auth_alt',
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { safeStringify, generateJsonResponse } from "./common";
2 | import { logger, LOG_LEVELS } from './utils/logger';
3 | import { getApiConfig } from './utils/config';
4 | const { jwtAuth } = await import('./auth');
5 | const responses = await import('./responses');
6 | const { ValueMapper } = await import('./mapping');
7 | const { setCorsHeaders } = await import('./cors');
8 | const { PathOperator } = await import('./path-ops');
9 | const { AuthError, SAGError } = await import('./types/error_types');
10 | const { setPoweredByHeader } = await import('./powered-by');
11 | const { createProxiedRequest } = await import('./requests');
12 | const { IntegrationTypeEnum } = await import('./enums/integration-type');
13 | const { ServerlessAPIGatewayContext } = await import('./types/serverless_api_gateway_context');
14 | const { auth0CallbackHandler, validateIdToken, getProfile, redirectToLogin, refreshToken } = await import('./integrations/auth0');
15 | const { supabaseEmailOTP, supabasePhoneOTP, supabaseVerifyOTP, supabaseJwtVerify, supabaseEmailOTPAlternative } = await import('./integrations/supabase-auth');
16 |
17 | export default {
18 | async fetch(request, env, ctx) {
19 | logger.info('Received new request', { method: request.method, url: request.url });
20 | logger.debug('Env', env);
21 | const sagContext = new ServerlessAPIGatewayContext();
22 | try {
23 | logger.debug('Loading API configuration');
24 | sagContext.apiConfig = await getApiConfig(env);
25 | sagContext.requestUrl = new URL(request.url);
26 |
27 | // Handle CORS preflight (OPTIONS) requests directly
28 | if (sagContext.apiConfig.cors && request.method === 'OPTIONS') {
29 | logger.debug('Handling CORS preflight request');
30 | const matchedItem = sagContext.apiConfig.paths.find((item) => {
31 | const matchResult = PathOperator.match(item.path, sagContext.requestUrl.pathname, request.method, item.method);
32 | return item.method === 'OPTIONS' && matchResult.matchedCount > 0 && matchResult.methodMatches;
33 | });
34 | if (!matchedItem) {
35 | logger.debug('No specific OPTIONS handler found, using default CORS response');
36 | return setPoweredByHeader(setCorsHeaders(request, new Response(null, { status: 204 }), sagContext.apiConfig.cors));
37 | }
38 | }
39 |
40 | // Adjusted filtering based on the updated pathsMatch return value
41 | logger.debug('Matching request path against configured paths');
42 | const matchedPaths = sagContext.apiConfig.paths
43 | .map((config) => ({ config, matchResult: PathOperator.match(config.path, sagContext.requestUrl.pathname, request.method, config.method) }))
44 | .filter((item) => item.matchResult.matchedCount > 0 && item.matchResult.methodMatches);
45 |
46 | // Sorting with priority: exact matches > parameterized matches > wildcard matches
47 | const matchedPath = matchedPaths.sort((a, b) => {
48 | // Prioritize exact matches
49 | if (a.matchResult.isExact !== b.matchResult.isExact) {
50 | return a.matchResult.isExact ? -1 : 1;
51 | }
52 | // Among exact or parameterized matches, prioritize those with more matched segments
53 | if (a.matchResult.matchedCount !== b.matchResult.matchedCount) {
54 | return b.matchResult.matchedCount - a.matchResult.matchedCount;
55 | }
56 | // If both are parameterized, prioritize non-wildcard over wildcard
57 | if (a.matchResult.isWildcard !== b.matchResult.isWildcard) {
58 | return a.matchResult.isWildcard ? 1 : -1;
59 | }
60 | // Prioritize exact method matches over "ANY"
61 | if (a.config.method !== b.config.method) {
62 | if (a.config.method === request.method) return -1;
63 | if (b.config.method === request.method) return 1;
64 | }
65 | return 0; // Equal priority
66 | })[0];
67 |
68 | sagContext.matchedPath = matchedPath;
69 |
70 | if (matchedPath) {
71 | logger.info('Found matching path configuration', {
72 | path: matchedPath.config.path,
73 | method: matchedPath.config.method,
74 | integration: matchedPath.config.integration?.type
75 | });
76 |
77 | // Check if the matched path requires authorization
78 | if (sagContext.apiConfig.authorizer && matchedPath.config.auth && sagContext.apiConfig.authorizer.type == 'jwt') {
79 | logger.debug('Validating JWT token');
80 | try {
81 | sagContext.jwtPayload = await jwtAuth(request, sagContext.apiConfig);
82 | logger.debug('JWT validation successful');
83 | } catch (error) {
84 | logger.error('JWT validation failed', error);
85 | if (error instanceof AuthError) {
86 | return setPoweredByHeader(
87 | setCorsHeaders(
88 | request,
89 | new Response(safeStringify({ error: error.message, code: error.code }), {
90 | status: error.statusCode,
91 | headers: { 'Content-Type': 'application/json' },
92 | }),
93 | sagContext.apiConfig.cors
94 | ),
95 | );
96 | } else if (error instanceof GenericError) {
97 | return setPoweredByHeader(setCorsHeaders(request, error.toApiResponse(), sagContext.apiConfig.cors));
98 | } else {
99 | return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors));
100 | }
101 | }
102 | } else if (sagContext.apiConfig.authorizer && matchedPath.config.auth && sagContext.apiConfig.authorizer.type == 'auth0') {
103 | logger.debug('Validating Auth0 token');
104 | try {
105 | sagContext.jwtPayload = await validateIdToken(request, null, sagContext.apiConfig.authorizer);
106 | logger.debug('Auth0 token validation successful');
107 | } catch (error) {
108 | logger.error('Auth0 token validation failed', error);
109 | if (error instanceof AuthError) {
110 | return setPoweredByHeader(
111 | setCorsHeaders(
112 | request,
113 | new Response(safeStringify({ error: error.message, code: error.code }), {
114 | status: error.statusCode,
115 | headers: { 'Content-Type': 'application/json' },
116 | }),
117 | sagContext.apiConfig.cors
118 | ),
119 | );
120 | } else if (error instanceof GenericError) {
121 | return setPoweredByHeader(setCorsHeaders(request, error.toApiResponse(), sagContext.apiConfig.cors));
122 | } else {
123 | return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors));
124 | }
125 | }
126 | } else if (sagContext.apiConfig.authorizer && matchedPath.config.auth && sagContext.apiConfig.authorizer.type == 'supabase') {
127 | logger.debug('Validating Supabase token');
128 | try {
129 | sagContext.jwtPayload = await supabaseJwtVerify(request, sagContext.apiConfig.authorizer);
130 | logger.debug('Supabase token validation successful');
131 | } catch (error) {
132 | logger.error('Supabase token validation failed', error);
133 | if (error instanceof AuthError) {
134 | return setPoweredByHeader(
135 | setCorsHeaders(
136 | request,
137 | new Response(safeStringify({ error: error.message, code: error.code }), {
138 | status: error.statusCode,
139 | headers: { 'Content-Type': 'application/json' },
140 | }),
141 | sagContext.apiConfig.cors
142 | ),
143 | );
144 | } else if (error instanceof GenericError) {
145 | return setPoweredByHeader(setCorsHeaders(request, error.toApiResponse(), sagContext.apiConfig.cors));
146 | } else {
147 | return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext.apiConfig.cors));
148 | }
149 | }
150 | }
151 |
152 | // Preprocess logic
153 | if (matchedPath.config.integration && matchedPath.config.pre_process) {
154 | logger.debug('Executing pre-process logic');
155 | const service =
156 | sagContext.apiConfig.serviceBindings &&
157 | sagContext.apiConfig.serviceBindings.find((serviceBinding) => serviceBinding.alias === matchedPath.config.pre_process.binding);
158 |
159 | if (service) {
160 | const [body1, body2] = request.body.tee();
161 | let response = await env[service.binding][matchedPath.config.pre_process.function](new Request(request, { body: body1 }), safeStringify(env), safeStringify(sagContext));
162 | if (response !== true) {
163 | logger.debug('Pre-process returned non-true response, returning early');
164 | return setPoweredByHeader(setCorsHeaders(request, response, sagContext.apiConfig.cors));
165 | }
166 | request = new Request(request, { body: body2 });
167 | }
168 | }
169 |
170 | if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.HTTP_PROXY) {
171 | logger.debug('Processing HTTP proxy integration');
172 | const server =
173 | sagContext.apiConfig.servers &&
174 | sagContext.apiConfig.servers.find((server) => server.alias === matchedPath.config.integration.server);
175 | if (server) {
176 | let modifiedRequest = createProxiedRequest(request, server, matchedPath.config);
177 | if (matchedPath.config.mapping) {
178 | logger.debug('Applying request mapping');
179 | modifiedRequest = await ValueMapper.modify({
180 | request: modifiedRequest,
181 | mappingConfig: matchedPath.config.mapping,
182 | jwtPayload: sagContext.jwtPayload,
183 | configVariables: matchedPath.config.variables,
184 | globalVariables: sagContext.apiConfig.variables,
185 | });
186 | }
187 | logger.debug('Forwarding request to target server');
188 | return fetch(modifiedRequest).then((response) => setPoweredByHeader(setCorsHeaders(request, response, sagContext.apiConfig.cors)));
189 | }
190 | } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SERVICE) {
191 | logger.debug('Processing service integration');
192 | const service =
193 | sagContext.apiConfig.services &&
194 | sagContext.apiConfig.services.find((service) => service.alias === matchedPath.config.integration.binding);
195 |
196 | if (service) {
197 | const module = await import(`${service.entrypoint}.js`);
198 | const Service = module.default;
199 | const serviceInstance = new Service();
200 | const response = await serviceInstance.fetch(request, env, ctx);
201 | return setPoweredByHeader(setCorsHeaders(request, generateJsonResponse(response), sagContext.apiConfig.cors));
202 | }
203 | } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SERVICE_BINDING) {
204 | logger.debug('Processing service binding integration');
205 | const service =
206 | sagContext.apiConfig.serviceBindings &&
207 | sagContext.apiConfig.serviceBindings.find((serviceBinding) => serviceBinding.alias === matchedPath.config.integration.binding);
208 |
209 | if (service) {
210 | const response = await env[service.binding][matchedPath.config.integration.function](request, safeStringify(env), safeStringify(sagContext));
211 | return setPoweredByHeader(setCorsHeaders(request, generateJsonResponse(response), sagContext.apiConfig.cors));
212 | }
213 | } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0CALLBACK) {
214 | logger.debug('Processing Auth0 callback');
215 | const urlParams = new URLSearchParams(sagContext.requestUrl.search);
216 | const code = urlParams.get('code');
217 |
218 | const jwt = await auth0CallbackHandler(code, sagContext.apiConfig.authorizer);
219 | sagContext.jwtPayload = await validateIdToken(null, jwt.id_token, sagContext.apiConfig.authorizer);
220 |
221 | // Post-process logic
222 | if (matchedPath.config.integration.post_process) {
223 | logger.debug('Executing post-process logic');
224 | const postProcessConfig = matchedPath.config.integration.post_process;
225 | if (postProcessConfig.type === 'service_binding') {
226 | const postProcessService = sagContext.apiConfig.serviceBindings.find(
227 | (serviceBinding) => serviceBinding.alias === postProcessConfig.binding
228 | );
229 | if (postProcessService) {
230 | await env[postProcessService.binding][postProcessConfig.function](request, safeStringify(env), safeStringify(sagContext));
231 | }
232 | }
233 | }
234 |
235 | return setPoweredByHeader(setCorsHeaders(
236 | request,
237 | new Response(safeStringify(jwt), {
238 | status: 200,
239 | headers: { 'Content-Type': 'application/json' },
240 | }),
241 | sagContext.apiConfig.cors
242 | ));
243 | } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0USERINFO) {
244 | logger.debug('Processing Auth0 userinfo request');
245 | const urlParams = new URLSearchParams(sagContext.requestUrl.search);
246 | const accessToken = urlParams.get('access_token');
247 |
248 | return getProfile(accessToken, sagContext.apiConfig.authorizer);
249 | } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0CALLBACKREDIRECT) {
250 | logger.debug('Processing Auth0 callback redirect');
251 | const urlParams = new URLSearchParams(sagContext.requestUrl.search);
252 | return redirectToLogin({ state: urlParams.get('state') }, sagContext.apiConfig.authorizer);
253 | } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.AUTH0REFRESH) {
254 | logger.debug('Processing Auth0 token refresh');
255 | return this.refreshTokenLogic(request, env, sagContext);
256 | } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SUPABASEPASSWORDLESSAUTH) {
257 | logger.debug('Processing Supabase passwordless auth');
258 | const requestBody = await request.json();
259 | const email = requestBody.email;
260 | const phone = requestBody.phone;
261 |
262 | if (email) {
263 | const response = await supabaseEmailOTP(env, email)
264 | return setPoweredByHeader(setCorsHeaders(request, response, sagContext.apiConfig.cors));
265 | } else if (phone) {
266 | const response = await supabasePhoneOTP(env, phone)
267 | return setPoweredByHeader(setCorsHeaders(request, response, sagContext.apiConfig.cors));
268 | } else {
269 | logger.warn('Missing email or phone in Supabase passwordless auth request');
270 | return setPoweredByHeader(setCorsHeaders(new Response(safeStringify({ error: 'Missing email or phone', code: 'missing_email_or_phone' }), {
271 | status: 400,
272 | headers: { 'Content-Type': 'application/json' },
273 | })));
274 | }
275 | } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SUPABASEPASSWORDLESSVERIFY) {
276 | logger.debug('Processing Supabase passwordless verify');
277 | const requestBody = await request.json();
278 | const token = requestBody.token;
279 | const email = requestBody.email;
280 | const phone = requestBody.phone;
281 |
282 | if (!token || (!email && !phone)) {
283 | logger.warn('Missing token, email, or phone in Supabase passwordless verify request');
284 | return new Response(safeStringify({ error: 'Missing token, email, or phone', code: 'missing_token_or_contact' }), {
285 | status: 400,
286 | headers: { 'Content-Type': 'application/json' },
287 | });
288 | }
289 |
290 | const response = await supabaseVerifyOTP(env, email, phone, token);
291 | return setPoweredByHeader(setCorsHeaders(request,
292 | new Response(safeStringify(response), { status: 200, headers: { 'Content-Type': 'application/json' }, }),
293 | sagContext.apiConfig.cors
294 | ));
295 | } else if (matchedPath.config.integration && matchedPath.config.integration.type == IntegrationTypeEnum.SUPABASEPASSWORDLESSAUTHALT) {
296 | logger.debug('Processing Supabase passwordless auth (alternative method)');
297 | const requestBody = await request.json();
298 | const email = requestBody.email;
299 |
300 | if (email) {
301 | const response = await supabaseEmailOTPAlternative(env, email)
302 | return setPoweredByHeader(setCorsHeaders(request, response, sagContext.apiConfig.cors));
303 | } else {
304 | logger.warn('Missing email in Supabase alternative auth request');
305 | return setPoweredByHeader(setCorsHeaders(new Response(safeStringify({
306 | error: 'Missing email - alternative method only supports email',
307 | code: 'missing_email'
308 | }), {
309 | status: 400,
310 | headers: { 'Content-Type': 'application/json' },
311 | })));
312 | }
313 | } else {
314 | logger.debug('Returning static response');
315 | return setPoweredByHeader(
316 | setCorsHeaders(
317 | request,
318 | new Response(safeStringify(matchedPath.config.response), { headers: { 'Content-Type': 'application/json' } }),
319 | sagContext.apiConfig.cors
320 | ),
321 | );
322 | }
323 | }
324 |
325 | logger.warn('No matching path found for request');
326 | return setPoweredByHeader(setCorsHeaders(request, responses.noMatchResponse(), sagContext.apiConfig.cors));
327 | } catch (error) {
328 | logger.error('Error processing request', error);
329 | if (error instanceof AuthError || error instanceof SAGError) {
330 | return setPoweredByHeader(setCorsHeaders(request, error.toApiResponse(), sagContext?.apiConfig?.cors));
331 | } else {
332 | return setPoweredByHeader(setCorsHeaders(request, responses.internalServerErrorResponse(), sagContext?.apiConfig?.cors));
333 | }
334 | }
335 | },
336 |
337 | async refreshTokenLogic(request, env, sagContext) {
338 | logger.debug('Processing token refresh request');
339 | const urlParams = new URLSearchParams(sagContext.requestUrl.search);
340 | const refreshTokenParam = urlParams.get('refresh_token');
341 |
342 | if (!refreshTokenParam) {
343 | logger.warn('Missing refresh token in request');
344 | return setPoweredByHeader(setCorsHeaders(request,
345 | new Response(
346 | safeStringify({ error: 'Missing refresh token', code: 'missing_refresh_token' }),
347 | { status: 400, headers: { 'Content-Type': 'application/json' } }
348 | ),
349 | sagContext.apiConfig.cors));
350 | }
351 |
352 | try {
353 | sagContext.jwtPayload = await validateIdToken(request, null, sagContext.apiConfig.authorizer);
354 | logger.debug('Token is still valid');
355 | return setPoweredByHeader(setCorsHeaders(request,
356 | new Response(
357 | safeStringify({ message: 'Token is still valid', code: 'token_still_valid' }),
358 | { status: 200, headers: { 'Content-Type': 'application/json' } }
359 | ),
360 | sagContext.apiConfig.cors));
361 |
362 | } catch (error) {
363 | if (error instanceof AuthError && error.code === 'ERR_JWT_EXPIRED') {
364 | logger.debug('Token expired, attempting refresh');
365 | try {
366 | const newTokens = await refreshToken(refreshTokenParam, sagContext.apiConfig.authorizer);
367 | logger.debug('Token refresh successful');
368 | return setPoweredByHeader(setCorsHeaders(
369 | request,
370 | new Response(safeStringify(newTokens), { status: 200, headers: { 'Content-Type': 'application/json' }, }),
371 | sagContext.apiConfig.cors
372 | ));
373 | } catch (refreshError) {
374 | logger.error('Token refresh failed', refreshError);
375 | return setPoweredByHeader(setCorsHeaders(
376 | request,
377 | new Response(safeStringify({ error: refreshError.message, code: refreshError.code }), {
378 | status: refreshError.statusCode || 500, headers: { 'Content-Type': 'application/json' },
379 | }),
380 | sagContext.apiConfig.cors
381 | ));
382 | }
383 | } else if (error instanceof SAGError) {
384 | logger.error('SAG error during token refresh', error);
385 | return setPoweredByHeader(setCorsHeaders(request, error.toApiResponse(), sagContext.apiConfig.cors));
386 | } else {
387 | logger.error('Unexpected error during token refresh', error);
388 | return setPoweredByHeader(
389 | setCorsHeaders(
390 | request, new Response(safeStringify({ error: error.message, code: error.code }), {
391 | status: error.statusCode || 500, headers: { 'Content-Type': 'application/json' },
392 | }),
393 | sagContext.apiConfig.cors
394 | ));
395 | }
396 | }
397 | }
398 | };
399 |
--------------------------------------------------------------------------------
/src/integrations/auth0.js:
--------------------------------------------------------------------------------
1 | import { jwtVerify, createLocalJWKSet, createRemoteJWKSet, errors } from 'jose';
2 | import { AuthError, SAGError } from "../types/error_types";
3 |
4 | async function auth0CallbackHandler(code, authorizer) {
5 | const { domain, client_id, client_secret, redirect_uri } = authorizer;
6 |
7 | const tokenUrl = `https://${domain}/oauth/token`;
8 |
9 | const body = new URLSearchParams({
10 | grant_type: 'authorization_code',
11 | client_id,
12 | client_secret,
13 | code,
14 | redirect_uri
15 | });
16 |
17 | try {
18 | const response = await fetch(tokenUrl, {
19 | method: 'POST',
20 | headers: {
21 | 'Content-Type': 'application/x-www-form-urlencoded'
22 | },
23 | body: body.toString()
24 | });
25 |
26 | if (!response.ok) {
27 | const errorData = await response.json();
28 | throw new SAGError(`Failed to fetch token: ${JSON.stringify(errorData)}`, nil, 500, nil);
29 | }
30 |
31 | const jwt = await response.json();
32 | return jwt;
33 | } catch (error) {
34 | throw new SAGError('Internal Server Error', nil, 500, error.message);
35 | }
36 | }
37 |
38 | async function validateIdToken(request, jwt, authorizer) {
39 | const { domain, jwks, jwks_uri } = authorizer;
40 | if (!jwt) {
41 | const authHeader = request.headers.get('Authorization');
42 | if (!authHeader || !authHeader.startsWith('Bearer ')) {
43 | throw new AuthError('No token provided or token format is invalid.', 'AUTH_ERROR', 401);
44 | }
45 | jwt = authHeader.split(' ')[1];
46 | }
47 |
48 | try {
49 | // Create a JWK Set from the JWKS endpoint or the JWKS data
50 | let jwksSet;
51 | if (jwks) {
52 | const jwksData = JSON.parse(jwks);
53 | jwksSet = createLocalJWKSet(jwksData);
54 | }
55 | else if (jwks_uri) {
56 | jwksSet = createRemoteJWKSet(new URL(jwks_uri));
57 | }
58 |
59 | const { payload, protectedHeader } = await jwtVerify(jwt, jwksSet, {
60 | issuer: `https://${domain}/`,
61 | });
62 | return payload;
63 | } catch (error) {
64 | // Handle token validation errors
65 | if (error instanceof errors.JOSEAlgNotAllowed) {
66 | throw new AuthError('Algorithm not allowed', error.code, 401);
67 | } else if (error instanceof errors.JWEDecryptionFailed) {
68 | throw new AuthError('Decryption failed', error.code, 401);
69 | } else if (error instanceof errors.JWEInvalid) {
70 | throw new AuthError('Invalid JWE', error.code, 401);
71 | } else if (error instanceof errors.JWTExpired) {
72 | throw new AuthError('Token has expired.', error.code, 401);
73 | } else if (error instanceof errors.JWTClaimValidationFailed) {
74 | throw new AuthError('JWT claim validation failed', error.code, 401);
75 | } else if (error instanceof errors.JWTInvalid) {
76 | throw new AuthError('Invalid JWT', error.code, 401);
77 | } else if (error instanceof errors.JWKSNoMatchingKey) {
78 | throw new AuthError('No matching key found in JWKS.', error.code, 401);
79 | } else if (error instanceof errors.JWKSInvalid) {
80 | throw new AuthError('Invalid JWKS', error.code, 401);
81 | } else if (error instanceof errors.JWKSMultipleMatchingKeys) {
82 | throw new AuthError('Multiple matching keys found in JWKS.', error.code, 401);
83 | } else if (error instanceof errors.JWKSNoMatchingKey) {
84 | throw new AuthError('No matching key in JWKS.', error.code, 401);
85 | } else if (error instanceof errors.JWSInvalid) {
86 | throw new AuthError('Invalid JWS', error.code, 401);
87 | } else if (error instanceof errors.JWSSignatureVerificationFailed) {
88 | throw new AuthError('Signature verification failed', error.code, 401);
89 | } else if (error instanceof Error) {
90 | throw new AuthError('JWT verification failed', 'AUTH_ERROR', 401);
91 | }
92 | // Fallback in case error is not an instance of Error
93 | throw new AuthError('JWT verification failed due to an unexpected error.', 'AUTH_ERROR', 401);
94 | }
95 | }
96 |
97 | async function getProfile(accessToken, authorizer) {
98 | const { domain } = authorizer;
99 |
100 | const userinfourl = `https://${domain}/userinfo`;
101 |
102 | try {
103 | const response = await fetch(userinfourl, {
104 | method: 'GET',
105 | headers: {
106 | 'Authorization': `Bearer ${accessToken}`
107 | }
108 | });
109 |
110 | if (!response.ok) {
111 | const errorData = await response.json();
112 | throw new SAGError('Failed to fetch token', response.status, response.status, JSON.stringify(errorData));
113 | }
114 |
115 | const data = await response.json();
116 | return new Response(JSON.stringify(data), {
117 | status: 200,
118 | headers: { 'Content-Type': 'application/json' }
119 | });
120 | } catch (error) {
121 | throw new SAGError('Internal Server Error', nil, 500, error.message);
122 | }
123 | }
124 |
125 | async function redirectToLogin(params, authorizer) {
126 | const { domain, client_id, redirect_uri, scope } = authorizer;
127 | const loginUrl = `https://${domain}/authorize?response_type=code&client_id=${client_id}&redirect_uri=${redirect_uri}&scope=${scope}&state=${params.state}`;
128 | return Response.redirect(loginUrl, 302);
129 | }
130 |
131 | async function refreshToken(refreshToken, authorizer) {
132 | const { domain, client_id, client_secret } = authorizer;
133 |
134 | const tokenUrl = `https://${domain}/oauth/token`;
135 |
136 | const body = new URLSearchParams({
137 | grant_type: 'refresh_token',
138 | client_id,
139 | client_secret,
140 | refresh_token: refreshToken
141 | });
142 |
143 | try {
144 | const response = await fetch(tokenUrl, {
145 | method: 'POST',
146 | headers: {
147 | 'Content-Type': 'application/x-www-form-urlencoded'
148 | },
149 | body: body.toString()
150 | });
151 |
152 | if (!response.ok) {
153 | const errorData = await response.json();
154 | throw new SAGError(`Failed to fetch token: ${JSON.stringify(errorData)}`, response.status, response.status, nil);
155 | }
156 |
157 | const jwt = await response.json();
158 | return jwt;
159 | } catch (error) {
160 | throw new SAGError('Internal Server Error', nil, 500, error.message);
161 | }
162 | }
163 |
164 | export { auth0CallbackHandler, validateIdToken, getProfile, redirectToLogin, refreshToken };
165 |
--------------------------------------------------------------------------------
/src/integrations/supabase-auth.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import { createClient } from '@supabase/supabase-js';
3 | import { AuthError } from "../types/error_types";
4 |
5 | async function supabaseEmailOTP(env, email, shouldCreateUser = true) {
6 | const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
7 |
8 | // Try multiple approaches to force OTP instead of magic link
9 | try {
10 | // Approach 1: Use signInWithOtp with explicit configuration
11 | const { data, error } = await supabase.auth.signInWithOtp({
12 | email,
13 | options: {
14 | shouldCreateUser,
15 | // Explicitly disable redirect to force OTP
16 | emailRedirectTo: undefined,
17 | data: {}
18 | },
19 | });
20 |
21 | if (error) {
22 | throw new AuthError(`Supabase OTP Error: ${error.message}`);
23 | }
24 |
25 | return new Response(JSON.stringify({
26 | message: 'Email OTP sent successfully',
27 | note: 'Check your email for a 6-digit verification code (not a link)',
28 | debug: 'If you received a magic link, check your Supabase project Auth settings'
29 | }), { headers: { 'Content-Type': 'application/json' } });
30 |
31 | } catch (otpError) {
32 | // Approach 2: Try using auth admin API if regular OTP fails
33 | try {
34 | const adminSupabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
35 |
36 | const { data, error } = await adminSupabase.auth.admin.generateLink({
37 | type: 'magiclink',
38 | email: email,
39 | options: {
40 | redirectTo: 'otp://verify' // Custom scheme to indicate OTP
41 | }
42 | });
43 |
44 | if (error) {
45 | throw new AuthError(`Admin API Error: ${error.message}`);
46 | }
47 |
48 | return new Response(JSON.stringify({
49 | message: 'Email OTP request sent via admin API',
50 | note: 'Check your email for verification code'
51 | }), { headers: { 'Content-Type': 'application/json' } });
52 |
53 | } catch (adminError) {
54 | throw new AuthError(`Failed to send OTP: ${otpError.message}. Admin fallback failed: ${adminError.message}`);
55 | }
56 | }
57 | }
58 |
59 | async function supabasePhoneOTP(env, phone, shouldCreateUser = true) {
60 | const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
61 |
62 | const { data, error } = await supabase.auth.signInWithOtp({
63 | phone,
64 | options: {
65 | shouldCreateUser
66 | },
67 | })
68 |
69 | if (error) {
70 | throw new AuthError(`Phone OTP Error: ${error.message}`);
71 | }
72 |
73 | return new Response(JSON.stringify({
74 | message: 'SMS OTP sent successfully',
75 | note: 'Check your phone for a 6-digit verification code'
76 | }), { headers: { 'Content-Type': 'application/json' } });
77 | }
78 |
79 | async function supabaseVerifyOTP(env, email, phone, token) {
80 | const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
81 |
82 | const { data, error } = await supabase.auth.verifyOtp({
83 | [email ? 'email' : 'phone']: email || phone,
84 | token,
85 | type: email ? 'email' : 'sms',
86 | });
87 |
88 | if (error) {
89 | throw new AuthError(`OTP Verification Error: ${error.message}`);
90 | }
91 |
92 | console.log('OTP Verification successful:', data);
93 | return data.session;
94 | }
95 |
96 | // Alternative function that tries to use a different approach for email OTP
97 | async function supabaseEmailOTPAlternative(env, email) {
98 | const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY);
99 |
100 | try {
101 | // Try using the auth.signUp method which might behave differently
102 | const { data, error } = await supabase.auth.signUp({
103 | email,
104 | password: Math.random().toString(36), // Random password, we won't use it
105 | options: {
106 | emailRedirectTo: undefined, // No redirect
107 | data: {
108 | otp_only: true // Custom flag
109 | }
110 | }
111 | });
112 |
113 | if (error && !error.message.includes('already registered')) {
114 | throw new AuthError(error.message);
115 | }
116 |
117 | return new Response(JSON.stringify({
118 | message: 'Alternative OTP method attempted',
119 | note: 'Check your email for verification code'
120 | }), { headers: { 'Content-Type': 'application/json' } });
121 |
122 | } catch (altError) {
123 | throw new AuthError(`Alternative OTP method failed: ${altError.message}`);
124 | }
125 | }
126 |
127 | // Generic helper to decode base64-url with automatic padding
128 | function decodeBase64Url(str) {
129 | try {
130 | // Add padding if required
131 | const padded = str + '='.repeat((4 - (str.length % 4)) % 4);
132 | const b64 = padded.replace(/-/g, '+').replace(/_/g, '/');
133 | return Buffer.from(b64, 'base64').toString('utf8');
134 | } catch (e) {
135 | return null;
136 | }
137 | }
138 |
139 | function decodeJWTPayload(token) {
140 | try {
141 | const parts = token.split('.');
142 | if (parts.length !== 3) return null;
143 | return JSON.parse(decodeBase64Url(parts[1]));
144 | } catch (e) {
145 | return null;
146 | }
147 | }
148 |
149 | function decodeJWTHeader(token) {
150 | try {
151 | const parts = token.split('.');
152 | if (parts.length !== 3) return null;
153 | return JSON.parse(decodeBase64Url(parts[0]));
154 | } catch (e) {
155 | return null;
156 | }
157 | }
158 |
159 | async function supabaseJwtVerify(request, authorizer) {
160 | const authHeader = request.headers.get('Authorization');
161 | if (!authHeader || !authHeader.startsWith('Bearer ')) {
162 | throw new AuthError('No token provided or token format is invalid.', 'AUTH_ERROR', 401);
163 | }
164 | const jwtToken = authHeader.split(' ')[1];
165 |
166 | // Log the raw incoming JWT token (for debugging – consider redacting in production)
167 | console.log('Incoming JWT token:', jwtToken);
168 | console.log('JWT length:', jwtToken.length);
169 |
170 | // Debug: Decode token to see what we're working with
171 | const payload = decodeJWTPayload(jwtToken);
172 | const header = decodeJWTHeader(jwtToken);
173 |
174 | console.log('JWT Debug Info:');
175 | console.log('Header:', header);
176 | console.log('Header Algorithm:', header?.alg);
177 | console.log('Payload issuer:', payload?.iss);
178 | console.log('Payload audience:', payload?.aud);
179 | console.log('Expected issuer:', authorizer.issuer);
180 | console.log('Expected audience:', authorizer.audience);
181 |
182 | // Log whether we have access to the JWT secret
183 | console.log('Has JWT secret:', !!authorizer.jwt_secret);
184 | if (authorizer.jwt_secret) {
185 | console.log('JWT secret (first 10 chars):', authorizer.jwt_secret.substring(0, 10), '...');
186 | }
187 |
188 | // Check if we have the JWT secret
189 | if (!authorizer.jwt_secret) {
190 | throw new AuthError('JWT secret not configured. Please set SUPABASE_JWT_SECRET in your environment.', 'MISSING_JWT_SECRET', 500);
191 | }
192 |
193 | try {
194 | const verifyOptions = {
195 | algorithms: ['HS256'], // Supabase defaults to HS256; change if needed
196 | issuer: authorizer.issuer,
197 | audience: authorizer.audience,
198 | };
199 |
200 | // Debug: Log verification options
201 | console.log('JWT Verification Options:', verifyOptions);
202 |
203 | const verifiedPayload = jwt.verify(jwtToken, authorizer.jwt_secret, verifyOptions);
204 |
205 | console.log('JWT Verification successful:', verifiedPayload);
206 | return verifiedPayload;
207 |
208 | } catch (error) {
209 | console.error('JWT Verification Error:', error);
210 |
211 | if (error.name === 'TokenExpiredError') {
212 | const expiry = payload?.exp ? new Date(payload.exp * 1000).toISOString() : 'unknown';
213 | throw new AuthError(`Token has expired at ${expiry}`, 'JWT_EXPIRED', 401);
214 | } else if (error.name === 'JsonWebTokenError') {
215 | throw new AuthError(`JWT verification failed: ${error.message}`, 'JWT_INVALID', 401);
216 | } else if (error.name === 'NotBeforeError') {
217 | throw new AuthError('Token not active yet (nbf)', 'JWT_NOT_ACTIVE', 401);
218 | } else {
219 | throw new AuthError('JWT verification failed due to an unexpected error.', 'AUTH_ERROR', 401);
220 | }
221 | }
222 | }
223 |
224 | export { supabaseEmailOTP, supabasePhoneOTP, supabaseVerifyOTP, supabaseJwtVerify, supabaseEmailOTPAlternative };
225 |
--------------------------------------------------------------------------------
/src/mapping.js:
--------------------------------------------------------------------------------
1 | export class ValueMapper {
2 | static async modify(incoming) {
3 | let newRequest = incoming.request.clone();
4 | const url = new URL(newRequest.url);
5 | const searchParams = new URLSearchParams(url.searchParams);
6 |
7 | // Apply mappings to headers
8 | if (incoming.mappingConfig.headers) {
9 | const newHeaders = new Headers(newRequest.headers);
10 |
11 | for (const [key, value] of Object.entries(incoming.mappingConfig.headers)) {
12 | const resolvedValue = this.resolveValue(
13 | String(value),
14 | incoming.request,
15 | incoming.jwtPayload,
16 | incoming.configVariables,
17 | incoming.globalVariables,
18 | );
19 | if (resolvedValue !== null) {
20 | newHeaders.set(key, resolvedValue);
21 | }
22 | }
23 |
24 | newRequest = new Request(newRequest, { headers: newHeaders });
25 | }
26 |
27 | // Apply mappings to query parameters
28 | if (incoming.mappingConfig.query) {
29 | for (const [key, value] of Object.entries(incoming.mappingConfig.query)) {
30 | const resolvedValue = this.resolveValue(
31 | String(value),
32 | incoming.request,
33 | incoming.jwtPayload,
34 | incoming.configVariables,
35 | incoming.globalVariables,
36 | );
37 | if (resolvedValue !== null) {
38 | searchParams.set(key, resolvedValue);
39 | }
40 | }
41 |
42 | url.search = searchParams.toString();
43 | newRequest = new Request(url.toString(), newRequest);
44 | }
45 |
46 | return newRequest;
47 | }
48 | static resolveValue(
49 | template,
50 | request,
51 | jwtPayload,
52 | configVariables,
53 | globalVariables,
54 | ) {
55 | try {
56 | const templateMatcher = /\$(request\.header|request\.jwt|config|request\.query)\.([a-zA-Z0-9-_.]+)/g;
57 | const match = templateMatcher.exec(template);
58 |
59 | if (match) {
60 | switch (match[1]) {
61 | case 'request.header':
62 | return request && request.headers && request.headers.hasOwnProperty(match[2]) ? request.headers.get(match[2]) : null;
63 | case 'request.jwt':
64 | return jwtPayload && jwtPayload.hasOwnProperty(match[2]) ? jwtPayload[match[2]] : null;
65 | case 'config':
66 | return configVariables && configVariables.hasOwnProperty(match[2])
67 | ? configVariables[match[2]]
68 | : globalVariables && globalVariables.hasOwnProperty(match[2])
69 | ? globalVariables[match[2]]
70 | : null;
71 | case 'request.query':
72 | const url = new URL(request.url);
73 | return url.searchParams.get(match[2]) || null;
74 | default:
75 | return null;
76 | }
77 | }
78 | } catch (error) {
79 | console.error(error);
80 | }
81 |
82 | return null;
83 | }
84 |
85 | static async replaceEnvAndSecrets(config, env) {
86 | // Helper function to recursively traverse the object
87 | function traverse(obj) {
88 | for (const key in obj) {
89 | if (typeof obj[key] === 'object' && obj[key] !== null) {
90 | // Recursively call traverse for nested objects
91 | traverse(obj[key]);
92 | } else if (typeof obj[key] === 'string') {
93 | // Replace environment variables
94 | if (obj[key].startsWith('$env.')) {
95 | const varName = obj[key].substring(5); // Get the variable name
96 | if (env[varName] === null) {
97 | console.error(`Error: Environment variable ${varName} is null.`);
98 | obj[key] = ''; // Replace with empty string
99 | } else {
100 | obj[key] = env[varName] !== undefined ? env[varName] : ''; // Replace or set to empty string
101 | }
102 | }
103 | // Replace secrets
104 | else if (obj[key].startsWith('$secrets.')) {
105 | const secretName = obj[key].substring(9); // Get the secret name
106 | if (env[secretName] === null) {
107 | console.error(`Error: Secret ${secretName} is null.`);
108 | obj[key] = ''; // Replace with empty string
109 | } else {
110 | obj[key] = env[secretName] !== undefined ? env[secretName] : ''; // Replace or set to empty string
111 | }
112 | }
113 | }
114 | }
115 | }
116 |
117 | // Start traversing the config object
118 | traverse(config);
119 | return config; // Return the modified config
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/path-ops.js:
--------------------------------------------------------------------------------
1 | export class PathOperator {
2 | // Function to check if a segment is a path parameter
3 | static isParam(segment) {
4 | return segment.startsWith('{') && segment.endsWith('}');
5 | }
6 |
7 | // Function to check if the segment is a wildcard
8 | static isWildcard(segment) {
9 | return segment === '{.+}';
10 | }
11 |
12 | // Function to match the paths and return the count of matched segments
13 | static match(configPath, requestPath, requestMethod, configMethod) {
14 | const configSegments = configPath.split('/');
15 | const requestSegments = requestPath.split('/');
16 | let matchedSegments = 0;
17 | let isExact = true;
18 | let isWildcardUsed = false;
19 | // Method match check
20 | const methodMatches = requestMethod === configMethod || configMethod === 'ANY';
21 |
22 | if (!methodMatches) {
23 | return { matchedCount: 0, isExact: false, isWildcard: false, methodMatches: methodMatches };
24 | }
25 |
26 | if (!this.isWildcard(configSegments[configSegments.length - 1]) && requestSegments.length !== configSegments.length) {
27 | return { matchedCount: 0, isExact: false, isWildcard: false, methodMatches: methodMatches };
28 | }
29 |
30 | if (this.isWildcard(configSegments[configSegments.length - 1]) && requestSegments.length < configSegments.length - 1) {
31 | return { matchedCount: 0, isExact: false, isWildcard: false, methodMatches: methodMatches };
32 | }
33 |
34 | const params = {}; // Initialize an empty object to store parameters
35 |
36 | for (let i = 0; i < Math.max(configSegments.length, requestSegments.length); i++) {
37 | if (i < configSegments.length && this.isWildcard(configSegments[i])) {
38 | isWildcardUsed = true;
39 | matchedSegments = Math.min(configSegments.length, requestSegments.length); // Wildcard matches all corresponding segments
40 | break;
41 | }
42 |
43 | if (i >= configSegments.length || i >= requestSegments.length) {
44 | isExact = false;
45 | break; // Reached the end of one of the paths
46 | }
47 |
48 | if (this.isParam(configSegments[i])) {
49 | isExact = false; // Found a parameterized segment, so it's not an exact match
50 | const paramName = configSegments[i].slice(1, -1); // Extract the parameter name without the first and last character
51 | params[paramName] = requestSegments[i]; // Store the parameter value
52 | matchedSegments++;
53 | } else if (configSegments[i] === requestSegments[i]) {
54 | matchedSegments++; // Exact match for this segment
55 | } else {
56 | return { matchedCount: 0, isExact: false, isWildcard: false, methodMatches: methodMatches }; // Mismatch found
57 | }
58 | }
59 |
60 | return {
61 | matchedCount: matchedSegments,
62 | isExact: isExact && matchedSegments === configSegments.length && matchedSegments === requestSegments.length,
63 | isWildcard: isWildcardUsed,
64 | methodMatches: methodMatches, // Include method match status
65 | params: params, // Include the parameters in the result
66 | };
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/powered-by.js:
--------------------------------------------------------------------------------
1 | function setPoweredByHeader(response) {
2 | const headers = new Headers(response.headers);
3 | headers.set('X-Powered-By', 'github.com/irensaltali/serverlessapigateway');
4 |
5 | return new Response(response.body, {
6 | status: response.status,
7 | statusText: response.statusText,
8 | headers: headers,
9 | });
10 | }
11 |
12 | export { setPoweredByHeader };
13 |
--------------------------------------------------------------------------------
/src/requests.js:
--------------------------------------------------------------------------------
1 | // Function to create a new request based on the matched path and server
2 | function createProxiedRequest(request, server, matchedPath) {
3 | const requestUrl = new URL(request.url);
4 | let newPath = '';
5 |
6 | if (matchedPath.integration && matchedPath.integration.type === 'http_proxy') {
7 | // For 'http_proxy', use the original path without the matching part
8 | const matchedPathPart = matchedPath.path.replace('{.+}', '');
9 | newPath = requestUrl.pathname.replace(matchedPathPart, '/');
10 | }
11 |
12 | // Create the new request with the updated URL
13 | const newRequest = new Request(server.url + newPath + requestUrl.search, request);
14 | return newRequest;
15 | }
16 |
17 | export { createProxiedRequest };
18 |
--------------------------------------------------------------------------------
/src/responses.js:
--------------------------------------------------------------------------------
1 | export const badRequestResponse = () =>
2 | new Response(JSON.stringify({ message: 'Bad request' }), { headers: { 'Content-Type': 'application/json' }, status: 400 });
3 | export const noMatchResponse = () =>
4 | new Response(JSON.stringify({ message: 'No match found.' }), { headers: { 'Content-Type': 'application/json' }, status: 404 });
5 | export const unauthorizedResponse = () =>
6 | new Response(JSON.stringify({ message: 'Unauthorized' }), { headers: { 'Content-Type': 'application/json' }, status: 401 });
7 | export const forbiddenResponse = () =>
8 | new Response(JSON.stringify({ message: 'Forbidden' }), { headers: { 'Content-Type': 'application/json' }, status: 403 });
9 | export const notFoundResponse = () =>
10 | new Response(JSON.stringify({ message: 'Not found' }), { headers: { 'Content-Type': 'application/json' }, status: 404 });
11 | export const internalServerErrorResponse = () =>
12 | new Response(JSON.stringify({ message: 'Internal server error' }), { headers: { 'Content-Type': 'application/json' }, status: 500 });
13 | export const configIsMissingResponse = () =>
14 | new Response(JSON.stringify({ message: 'API configuration is missing' }), { headers: { 'Content-Type': 'application/json' }, status: 501 });
15 |
--------------------------------------------------------------------------------
/src/services/endpoint1.js:
--------------------------------------------------------------------------------
1 | export default class Service {
2 | async fetch(request, env, ctx) {
3 | return new Response("Hello from Worker 1!", {
4 | headers: { "content-type": "text/plain" },
5 | });
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/src/types/error_types.js:
--------------------------------------------------------------------------------
1 | class AuthError extends Error {
2 | constructor(message, code, statusCode) {
3 | super(message);
4 | this.name = 'AuthError';
5 | this.code = code;
6 | this.statusCode = statusCode;
7 | }
8 |
9 | toApiResponse() {
10 | return new Response(JSON.stringify({ error: this.message, code: this.code }), {
11 | status: this.statusCode,
12 | headers: { 'Content-Type': 'application/json' },
13 | });
14 | }
15 | }
16 |
17 | class SAGError extends Error {
18 | constructor(message, code, statusCode, logMessage) {
19 | super(message);
20 | this.name = 'SAGError';
21 | this.code = code;
22 | this.statusCode = statusCode;
23 | this.logMessage = logMessage;
24 | Error.captureStackTrace(this, this.constructor);
25 | }
26 |
27 | toApiResponse() {
28 | console.error(this.logMessage || this.message);
29 | return new Response(JSON.stringify({ error: this.message, code: this.code }), {
30 | status: this.statusCode,
31 | headers: { 'Content-Type': 'application/json' },
32 | });
33 | }
34 | }
35 |
36 | export { AuthError, SAGError };
37 |
--------------------------------------------------------------------------------
/src/types/serverless_api_gateway_context.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {Object} ServerlessAPIGatewayContext
3 | * @property {string} apiConfig - The API Gateway configuration.
4 | * @property {URL} requestUrl - The request URL.
5 | * @property {JWTPayload} jwtPayload - The JWT payload.
6 | */
7 | export class ServerlessAPIGatewayContext {
8 | /**
9 | * @param {string} [apiConfig]
10 | * @param {URL} [requestUrl]
11 | * @param {JWTPayload} [jwtPayload]
12 | */
13 | constructor(apiConfig = null, requestUrl = null, jwtPayload = null) {
14 | this.apiConfig = apiConfig;
15 | this.requestUrl = requestUrl;
16 | this.jwtPayload = jwtPayload;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/utils/config.js:
--------------------------------------------------------------------------------
1 | import { logger } from './logger';
2 | import { ValueMapper } from '../mapping';
3 | import { configIsMissingResponse } from '../responses';
4 | import { setPoweredByHeader } from '../powered-by';
5 |
6 | export async function getApiConfig(env) {
7 | let apiConfig;
8 | try {
9 | logger.debug('Loading API configuration');
10 | if (typeof env.CONFIG === 'undefined' || await env.CONFIG.get("api-config.json") === null) {
11 | apiConfig = await import('../api-config.json');
12 | logger.debug('Loaded API configuration from local file');
13 | } else {
14 | apiConfig = JSON.parse(await env.CONFIG.get("api-config.json"));
15 | logger.debug('Loaded API configuration from KV store');
16 | }
17 | } catch (e) {
18 | logger.error('Error loading API configuration', e);
19 | return setPoweredByHeader(request, configIsMissingResponse());
20 | }
21 |
22 | // Replace environment variables and secrets in the API configuration
23 | logger.debug('Replacing environment variables and secrets in API configuration');
24 | apiConfig = await ValueMapper.replaceEnvAndSecrets(apiConfig, env);
25 |
26 | return apiConfig;
27 | }
28 |
--------------------------------------------------------------------------------
/src/utils/logger.js:
--------------------------------------------------------------------------------
1 | const LOG_LEVELS = {
2 | DEBUG: 0,
3 | INFO: 1,
4 | WARN: 2,
5 | ERROR: 3
6 | };
7 |
8 | class Logger {
9 | constructor(debugMode = false) {
10 | this.debugMode = debugMode;
11 | }
12 |
13 | setDebugMode(enabled) {
14 | this.debugMode = enabled;
15 | }
16 |
17 | debug(message, ...args) {
18 | if (this.debugMode) {
19 | console.debug(`[DEBUG] ${message}`, ...args);
20 | }
21 | }
22 |
23 | info(message, ...args) {
24 | console.info(`[INFO] ${message}`, ...args);
25 | }
26 |
27 | warn(message, ...args) {
28 | console.warn(`[WARN] ${message}`, ...args);
29 | }
30 |
31 | error(message, ...args) {
32 | console.error(`[ERROR] ${message}`, ...args);
33 | }
34 |
35 | log(level, message, ...args) {
36 | switch (level) {
37 | case LOG_LEVELS.DEBUG:
38 | this.debug(message, ...args);
39 | break;
40 | case LOG_LEVELS.INFO:
41 | this.info(message, ...args);
42 | break;
43 | case LOG_LEVELS.WARN:
44 | this.warn(message, ...args);
45 | break;
46 | case LOG_LEVELS.ERROR:
47 | this.error(message, ...args);
48 | break;
49 | default:
50 | this.info(message, ...args);
51 | }
52 | }
53 | }
54 |
55 | export const logger = new Logger(process.env.NODE_ENV === 'development');
56 | export { LOG_LEVELS };
57 |
--------------------------------------------------------------------------------
/worker-configuration.d.ts:
--------------------------------------------------------------------------------
1 | interface Env {
2 | // Example binding to KV. Learn more at https://developers.cloudflare.com/workers/runtime-apis/kv/
3 | // MY_KV_NAMESPACE: KVNamespace;
4 | //
5 | // Example binding to Durable Object. Learn more at https://developers.cloudflare.com/workers/runtime-apis/durable-objects/
6 | // MY_DURABLE_OBJECT: DurableObjectNamespace;
7 | //
8 | // Example binding to R2. Learn more at https://developers.cloudflare.com/workers/runtime-apis/r2/
9 | // MY_BUCKET: R2Bucket;
10 | //
11 | // Example binding to a Service. Learn more at https://developers.cloudflare.com/workers/runtime-apis/service-bindings/
12 | // MY_SERVICE: Fetcher;
13 | //
14 | // Example binding to a Queue. Learn more at https://developers.cloudflare.com/queues/javascript-apis/
15 | // MY_QUEUE: Queue;
16 | }
17 |
--------------------------------------------------------------------------------
/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "serverlessapigateway"
2 | main = "src/index.js"
3 | compatibility_date = "2025-07-04"
4 | compatibility_flags = ["nodejs_compat"]
5 | send_metrics = true
6 | minify = true
7 | workers_dev = true
8 |
9 | find_additional_modules = true
10 | rules = [
11 | { type = "ESModule", globs = ["services/*.js"]}
12 | ]
13 |
14 | services = [
15 | { binding = "USER_SERVICE", service = "user_service" }
16 | ]
17 |
18 | [observability]
19 | enabled = true
20 | head_sampling_rate = 0.3
21 |
22 | [vars]
23 | VAR_TEST_RESPONSE_TEXT = "This respose from environment"
24 | VAR_TEST_RESPONSE_TEXT_SECRET = "This respose from secrets"
25 |
26 | # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
27 | # Note: Use secrets to store sensitive data.
28 | # Docs: https://developers.cloudflare.com/workers/platform/environment-variables
29 | # [vars]
30 | # MY_VARIABLE = "production_value"
31 |
32 | # Bind a KV Namespace. Use KV as persistent storage for small key-value pairs.
33 | # Docs: https://developers.cloudflare.com/workers/runtime-apis/kv
34 | # [[kv_namespaces]]
35 | # binding = "MY_KV_NAMESPACE"
36 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
37 |
38 | # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files.
39 | # Docs: https://developers.cloudflare.com/r2/api/workers/workers-api-usage/
40 | # [[r2_buckets]]
41 | # binding = "MY_BUCKET"
42 | # bucket_name = "my-bucket"
43 |
44 | # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer.
45 | # Docs: https://developers.cloudflare.com/queues/get-started
46 | # [[queues.producers]]
47 | # binding = "MY_QUEUE"
48 | # queue = "my-queue"
49 |
50 | # Bind a Queue consumer. Queue Consumers can retrieve tasks scheduled by Producers to act on them.
51 | # Docs: https://developers.cloudflare.com/queues/get-started
52 | # [[queues.consumers]]
53 | # queue = "my-queue"
54 |
55 | # Bind another Worker service. Use this binding to call another Worker without network overhead.
56 | # Docs: https://developers.cloudflare.com/workers/platform/services
57 | # [[services]]
58 | # binding = "MY_SERVICE"
59 | # service = "my-service"
60 |
61 | # Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.
62 | # Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps.
63 | # Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects
64 | # [[durable_objects.bindings]]
65 | # name = "MY_DURABLE_OBJECT"
66 | # class_name = "MyDurableObject"
67 |
68 | # Durable Object migrations.
69 | # Docs: https://developers.cloudflare.com/workers/learning/using-durable-objects#configure-durable-object-classes-with-migrations
70 | # [[migrations]]
71 | # tag = "v1"
72 | # new_classes = ["MyDurableObject"]
73 |
--------------------------------------------------------------------------------