feat: add initial API

This commit is contained in:
Morten Olsen
2025-10-16 20:54:31 +02:00
parent 5cf0a3612a
commit 11828da073
17 changed files with 647 additions and 89 deletions

View File

@@ -29,7 +29,6 @@ docker-compose up -d
- **WebSocket MQTT**: `ws://localhost:8883/ws` - **WebSocket MQTT**: `ws://localhost:8883/ws`
- **HTTP API**: `http://localhost:8883/api` - **HTTP API**: `http://localhost:8883/api`
3. Connect with an MQTT client: 3. Connect with an MQTT client:
> [!IMPORTANT] > [!IMPORTANT]
@@ -48,24 +47,25 @@ const client = mqtt.connect('ws://localhost:8883/ws')
Backbone can be configured using environment variables: Backbone can be configured using environment variables:
| Variable | Description | Default | | Variable | Description | Default |
|----------|-------------|---------| | -------------------- | ---------------------------------------- | ----------- |
| `ADMIN_TOKEN` | Admin token for API requests | `undefined` | | `ADMIN_TOKEN` | Admin token for API requests | `undefined` |
| `TOKEN_SECRET` | JWT signing secret for authentication | `undefined` | | `JWT_SECRET` | JWT signing secret for authentication | `undefined` |
| `K8S_ENABLED` | Enable Kubernetes operator mode | `false` | | `K8S_ENABLED` | Enable Kubernetes operator mode | `false` |
| `HTTP_ENABLED` | Enable HTTP/WebSocket server | `true` | | `WS_ENABLED` | Enable WebSocket MQTT server | `false` |
| `HTTP_PORT` | HTTP server port | `8883` | | `API_ENABLED` | Enable HTTP API | `false` |
| `TCP_ENABLED` | Enable TCP MQTT server | `true` | | `HTTP_PORT` | HTTP server port | `8883` |
| `TCP_PORT` | TCP server port | `1883` | | `TCP_ENABLED` | Enable TCP MQTT server | `false` |
| `OIDC_ENABLED` | OIDC discovery URL | `undefined` | | `TCP_PORT` | TCP server port | `1883` |
| `OIDC_DISCOVERY` | OIDC discovery URL | `undefined` | | `OIDC_ENABLED` | OIDC discovery URL | `false` |
| `OIDC_CLIENT_ID` | OIDC client ID | `undefined` | | `OIDC_DISCOVERY` | OIDC discovery URL | `undefined` |
| `OIDC_CLIENT_SECRET` | OIDC client secret | `undefined` | | `OIDC_CLIENT_ID` | OIDC client ID | `undefined` |
| `OIDC_CLIENT_SECRET` | OIDC client secret | `undefined` | | `OIDC_CLIENT_SECRET` | OIDC client secret | `undefined` |
| `OIDC_GROUP_FIELD` | JWT field for reading groups | `groups` | | `OIDC_CLIENT_SECRET` | OIDC client secret | `undefined` |
| `OIDC_ADMIN_GROUP` | JWT group for admins | `undefined` | | `OIDC_GROUP_FIELD` | JWT field for reading groups | `groups` |
| `OIDC_WRITER_GROUP` | JWT group with publish access to queue | `undefined` | | `OIDC_ADMIN_GROUP` | JWT group for admins | `undefined` |
| `OIDC_READER_GROUP` | JWT group with read-only access to queue | `undefined` | | `OIDC_WRITER_GROUP` | JWT group with publish access to queue | `undefined` |
| `OIDC_READER_GROUP` | JWT group with read-only access to queue | `undefined` |
### Example Configuration ### Example Configuration
@@ -130,9 +130,9 @@ Access control is defined using statement-based policies similar to AWS IAM:
```yaml ```yaml
statements: statements:
- effect: allow # or "deny" - effect: allow # or "deny"
resources: ['*'] # MQTT topic patterns resources: ['*'] # MQTT topic patterns
actions: ['*'] # MQTT actions actions: ['*'] # MQTT actions
``` ```
#### MQTT Actions #### MQTT Actions
@@ -159,6 +159,7 @@ statements:
### HTTP API ### HTTP API
The HTTP API provides management endpoints for: The HTTP API provides management endpoints for:
- Client management - Client management
- Topic configuration - Topic configuration
- Broker statistics - Broker statistics

View File

@@ -2,7 +2,7 @@
"type": "module", "type": "module",
"main": "dist/exports.js", "main": "dist/exports.js",
"scripts": { "scripts": {
"dev": "node --no-warnings --watch src/start.ts", "dev": "node --no-warnings --watch src/dev.ts",
"test:lint": "eslint", "test:lint": "eslint",
"build": "tsc --build", "build": "tsc --build",
"test:unit": "vitest --run --passWithNoTests", "test:unit": "vitest --run --passWithNoTests",
@@ -41,13 +41,18 @@
"#root/*": "./src/*" "#root/*": "./src/*"
}, },
"dependencies": { "dependencies": {
"@fastify/sensible": "^6.0.3",
"@fastify/swagger": "^9.5.2",
"@fastify/websocket": "^11.2.0", "@fastify/websocket": "^11.2.0",
"@kubernetes/client-node": "^1.4.0", "@kubernetes/client-node": "^1.4.0",
"@scalar/fastify-api-reference": "^1.38.1",
"aedes": "^0.51.3", "aedes": "^0.51.3",
"aedes-packet": "^3.0.0",
"aedes-persistence": "^10.2.2", "aedes-persistence": "^10.2.2",
"ajv": "^8.17.1", "ajv": "^8.17.1",
"better-sqlite3": "^12.4.1", "better-sqlite3": "^12.4.1",
"fastify": "^5.6.1", "fastify": "^5.6.1",
"fastify-type-provider-zod": "^6.0.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"knex": "^3.1.0", "knex": "^3.1.0",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",

321
pnpm-lock.yaml generated
View File

@@ -8,15 +8,27 @@ importers:
.: .:
dependencies: dependencies:
'@fastify/sensible':
specifier: ^6.0.3
version: 6.0.3
'@fastify/swagger':
specifier: ^9.5.2
version: 9.5.2
'@fastify/websocket': '@fastify/websocket':
specifier: ^11.2.0 specifier: ^11.2.0
version: 11.2.0 version: 11.2.0
'@kubernetes/client-node': '@kubernetes/client-node':
specifier: ^1.4.0 specifier: ^1.4.0
version: 1.4.0 version: 1.4.0
'@scalar/fastify-api-reference':
specifier: ^1.38.1
version: 1.38.1
aedes: aedes:
specifier: ^0.51.3 specifier: ^0.51.3
version: 0.51.3 version: 0.51.3
aedes-packet:
specifier: ^3.0.0
version: 3.0.0
aedes-persistence: aedes-persistence:
specifier: ^10.2.2 specifier: ^10.2.2
version: 10.2.2 version: 10.2.2
@@ -29,6 +41,9 @@ importers:
fastify: fastify:
specifier: ^5.6.1 specifier: ^5.6.1
version: 5.6.1 version: 5.6.1
fastify-type-provider-zod:
specifier: ^6.0.0
version: 6.0.0(@fastify/swagger@9.5.2)(fastify@5.6.1)(openapi-types@12.1.3)(zod@4.1.12)
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.2 version: 9.0.2
@@ -71,7 +86,7 @@ importers:
version: 8.18.1 version: 8.18.1
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: 3.2.4 specifier: 3.2.4
version: 3.2.4(vitest@3.2.4(@types/node@24.7.2)) version: 3.2.4(vitest@3.2.4(@types/node@24.7.2)(yaml@2.8.1))
eslint: eslint:
specifier: 9.37.0 specifier: 9.37.0
version: 9.37.0 version: 9.37.0
@@ -101,7 +116,7 @@ importers:
version: 8.46.1(eslint@9.37.0)(typescript@5.9.3) version: 8.46.1(eslint@9.37.0)(typescript@5.9.3)
vitest: vitest:
specifier: 3.2.4 specifier: 3.2.4
version: 3.2.4(@types/node@24.7.2) version: 3.2.4(@types/node@24.7.2)(yaml@2.8.1)
packages: packages:
@@ -350,6 +365,12 @@ packages:
'@fastify/proxy-addr@5.1.0': '@fastify/proxy-addr@5.1.0':
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
'@fastify/sensible@6.0.3':
resolution: {integrity: sha512-Iyn8698hp/e5+v8SNBBruTa7UfrMEP52R16dc9jMpqSyEcPsvWFQo+R6WwHCUnJiLIsuci2ZoEZ7ilrSSCPIVg==}
'@fastify/swagger@9.5.2':
resolution: {integrity: sha512-8e8w/LItg/cF6IR/hYKtnt+E0QImees5o3YWJsTLxaIk+tzNUEc6Z2Ursi4oOHWwUlKjUCnV6yh5z5ZdxvlsWA==}
'@fastify/websocket@11.2.0': '@fastify/websocket@11.2.0':
resolution: {integrity: sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==} resolution: {integrity: sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==}
@@ -409,6 +430,10 @@ packages:
'@kubernetes/client-node@1.4.0': '@kubernetes/client-node@1.4.0':
resolution: {integrity: sha512-Zge3YvF7DJi264dU1b3wb/GmzR99JhUpqTvp+VGHfwZT+g7EOOYNScDJNZwXy9cszyIGPIs0VHr+kk8e95qqrA==} resolution: {integrity: sha512-Zge3YvF7DJi264dU1b3wb/GmzR99JhUpqTvp+VGHfwZT+g7EOOYNScDJNZwXy9cszyIGPIs0VHr+kk8e95qqrA==}
'@lukeed/ms@2.0.2':
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
engines: {node: '>=8'}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -683,6 +708,38 @@ packages:
'@rtsao/scc@1.1.0': '@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
'@scalar/core@0.3.20':
resolution: {integrity: sha512-bIlrePx41pSvjDcaJPa9YVVhbSm0N9SKQm2Fzl489S0bUVToyXIQtMFVR4i+BmXGjOcATm/66ELW4vdXRjHoRA==}
engines: {node: '>=20'}
'@scalar/fastify-api-reference@1.38.1':
resolution: {integrity: sha512-olRjyMn45gTB1tYCXjjQMhAeku8Z46rlPNYYAg9BU+dcfcm1miuxT0cYhB2U9pI5rZtoh2j305M7FG7ShlY2hQ==}
engines: {node: '>=20'}
'@scalar/helpers@0.0.12':
resolution: {integrity: sha512-4NDmHShyi1hrVRsJCdRZT/FIpy+/5PFbVbQLRYX/pjpu5cYqHBj9s6n5RI6gGDXEBHAIFi63g9FC6Isgr66l1Q==}
engines: {node: '>=20'}
'@scalar/json-magic@0.6.1':
resolution: {integrity: sha512-HJMPY5dUU3EXVS4EkjAFXo+uCrby/YFu/gljKDQnhYWRy5zQ0sJWrOEDcHS8nLoJRCIRD5tiVpCxnUnSb6OoAQ==}
engines: {node: '>=20'}
'@scalar/openapi-parser@0.22.3':
resolution: {integrity: sha512-5Znbx9HVJb7EV9EJXJrUj+cs116QIBwX/hxkyaiLaaDL2w5S+z1rjtV+d0Jv7382FCtzAtfv/9llVuxZYPVqXA==}
engines: {node: '>=20'}
'@scalar/openapi-types@0.5.0':
resolution: {integrity: sha512-HJBcLa+/mPP+3TCcQngj/iW5UqynRosOQdEETXjmdy6Ngw8wBjwIcT6C86J5jufJ6sI8++HYnt+e7pAvp5FO6A==}
engines: {node: '>=20'}
'@scalar/openapi-upgrader@0.1.3':
resolution: {integrity: sha512-iROhcgy3vge6zsviPtoTLHale0nYt1PLhuMmJweQwJ0U23ZYyYhV5xgHtAd0OBEXuqT6rjYbJFvKOJZmJOwpNQ==}
engines: {node: '>=20'}
'@scalar/types@0.3.2':
resolution: {integrity: sha512-+X10CCvG57nAqYbTGteiSzRFQcMYm7DLfCRMeEfiWQ9Bq2ladat17XsMSvkvwcfpOSlsoepWf3P5dErERUSOQQ==}
engines: {node: '>=20'}
'@types/braces@3.0.5': '@types/braces@3.0.5':
resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==}
@@ -870,6 +927,14 @@ packages:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
ajv-draft-04@1.0.0:
resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
peerDependencies:
ajv: ^8.5.0
peerDependenciesMeta:
ajv:
optional: true
ajv-formats@3.0.1: ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies: peerDependencies:
@@ -1253,6 +1318,10 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dequal@2.0.3: dequal@2.0.3:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1506,9 +1575,20 @@ packages:
resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
fastify-plugin@4.5.1:
resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==}
fastify-plugin@5.1.0: fastify-plugin@5.1.0:
resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==}
fastify-type-provider-zod@6.0.0:
resolution: {integrity: sha512-Bz+Qll2XuvvueHz0yhcr67V/43q1VecSyIqZm+P8OL8KZHznUXECZXkuwQePR5b6fWY/kzhhadmgNs9dB/Nifg==}
peerDependencies:
'@fastify/swagger': '>=9.5.1'
fastify: ^5.0.0
openapi-types: ^12.1.3
zod: '>=4.1.5'
fastify@5.6.1: fastify@5.6.1:
resolution: {integrity: sha512-WjjlOciBF0K8pDUPZoGPhqhKrQJ02I8DKaDIfO51EL0kbSMwQFl85cRwhOvmSDWoukNOdTo27gLN549pLCcH7Q==} resolution: {integrity: sha512-WjjlOciBF0K8pDUPZoGPhqhKrQJ02I8DKaDIfO51EL0kbSMwQFl85cRwhOvmSDWoukNOdTo27gLN549pLCcH7Q==}
@@ -1568,6 +1648,10 @@ packages:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
fs-constants@1.0.0: fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@@ -1623,6 +1707,9 @@ packages:
github-from-package@0.0.0: github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
glob-parent@5.1.2: glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
@@ -1693,6 +1780,10 @@ packages:
html-escaper@2.0.2: html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
human-signals@2.1.0: human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'} engines: {node: '>=10.17.0'}
@@ -1930,6 +2021,10 @@ packages:
json-schema-ref-resolver@3.0.0: json-schema-ref-resolver@3.0.0:
resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==}
json-schema-resolver@3.0.0:
resolution: {integrity: sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==}
engines: {node: '>=20'}
json-schema-traverse@0.4.1: json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -1956,6 +2051,10 @@ packages:
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
hasBin: true hasBin: true
jsonpointer@5.0.1:
resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==}
engines: {node: '>=0.10.0'}
jsonwebtoken@9.0.2: jsonwebtoken@9.0.2:
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
engines: {node: '>=12', npm: '>=6'} engines: {node: '>=12', npm: '>=6'}
@@ -1997,6 +2096,10 @@ packages:
tedious: tedious:
optional: true optional: true
leven@4.1.0:
resolution: {integrity: sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
levn@0.4.1: levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -2070,6 +2173,10 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
mem@8.1.1: mem@8.1.1:
resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==} resolution: {integrity: sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2148,6 +2255,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
nanoid@5.1.5:
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
engines: {node: ^18 || >=20}
hasBin: true
napi-build-utils@2.0.0: napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
@@ -2224,6 +2336,9 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'} engines: {node: '>=6'}
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
openid-client@6.8.1: openid-client@6.8.1:
resolution: {integrity: sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==} resolution: {integrity: sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==}
@@ -2598,6 +2713,9 @@ packages:
resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
shebang-command@2.0.0: shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2681,6 +2799,10 @@ packages:
stacktracey@2.1.8: stacktracey@2.1.8:
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
std-env@3.10.0: std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
@@ -2771,6 +2893,10 @@ packages:
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
tagged-tag@1.0.0:
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
engines: {node: '>=20'}
tar-fs@2.1.4: tar-fs@2.1.4:
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
@@ -2835,6 +2961,10 @@ packages:
resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
engines: {node: '>=12'} engines: {node: '>=12'}
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
tr46@0.0.3: tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
@@ -2865,6 +2995,14 @@ packages:
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
engines: {node: '>=8'} engines: {node: '>=8'}
type-fest@5.0.0:
resolution: {integrity: sha512-GeJop7+u7BYlQ6yQCAY1nBQiRSHR+6OdCEtd8Bwp9a3NK3+fWAVjOaPKJDteB9f6cIJ0wt4IfnScjLG450EpXA==}
engines: {node: '>=20'}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
typed-array-buffer@1.0.3: typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2924,6 +3062,10 @@ packages:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true hasBin: true
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
vite-node@3.2.4: vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -3101,10 +3243,23 @@ packages:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
yaml@2.8.0:
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
engines: {node: '>= 14.6'}
hasBin: true
yaml@2.8.1:
resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
engines: {node: '>= 14.6'}
hasBin: true
yocto-queue@0.1.0: yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
zod@4.1.11:
resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==}
zod@4.1.12: zod@4.1.12:
resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
@@ -3285,6 +3440,26 @@ snapshots:
'@fastify/forwarded': 3.0.1 '@fastify/forwarded': 3.0.1
ipaddr.js: 2.2.0 ipaddr.js: 2.2.0
'@fastify/sensible@6.0.3':
dependencies:
'@lukeed/ms': 2.0.2
dequal: 2.0.3
fastify-plugin: 5.1.0
forwarded: 0.2.0
http-errors: 2.0.0
type-is: 1.6.18
vary: 1.1.2
'@fastify/swagger@9.5.2':
dependencies:
fastify-plugin: 5.1.0
json-schema-resolver: 3.0.0
openapi-types: 12.1.3
rfdc: 1.4.1
yaml: 2.8.1
transitivePeerDependencies:
- supports-color
'@fastify/websocket@11.2.0': '@fastify/websocket@11.2.0':
dependencies: dependencies:
duplexify: 4.1.3 duplexify: 4.1.3
@@ -3367,6 +3542,8 @@ snapshots:
- supports-color - supports-color
- utf-8-validate - utf-8-validate
'@lukeed/ms@2.0.2': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -3686,6 +3863,52 @@ snapshots:
'@rtsao/scc@1.1.0': {} '@rtsao/scc@1.1.0': {}
'@scalar/core@0.3.20':
dependencies:
'@scalar/types': 0.3.2
'@scalar/fastify-api-reference@1.38.1':
dependencies:
'@scalar/core': 0.3.20
'@scalar/openapi-parser': 0.22.3
'@scalar/openapi-types': 0.5.0
fastify-plugin: 4.5.1
github-slugger: 2.0.0
'@scalar/helpers@0.0.12': {}
'@scalar/json-magic@0.6.1':
dependencies:
'@scalar/helpers': 0.0.12
yaml: 2.8.0
'@scalar/openapi-parser@0.22.3':
dependencies:
'@scalar/json-magic': 0.6.1
'@scalar/openapi-types': 0.5.0
'@scalar/openapi-upgrader': 0.1.3
ajv: 8.17.1
ajv-draft-04: 1.0.0(ajv@8.17.1)
ajv-formats: 3.0.1(ajv@8.17.1)
jsonpointer: 5.0.1
leven: 4.1.0
yaml: 2.8.0
'@scalar/openapi-types@0.5.0':
dependencies:
zod: 4.1.11
'@scalar/openapi-upgrader@0.1.3':
dependencies:
'@scalar/openapi-types': 0.5.0
'@scalar/types@0.3.2':
dependencies:
'@scalar/openapi-types': 0.5.0
nanoid: 5.1.5
type-fest: 5.0.0
zod: 4.1.11
'@types/braces@3.0.5': {} '@types/braces@3.0.5': {}
'@types/chai@5.2.2': '@types/chai@5.2.2':
@@ -3831,7 +4054,7 @@ snapshots:
'@typescript-eslint/types': 8.46.1 '@typescript-eslint/types': 8.46.1
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 4.2.1
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.7.2))': '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.7.2)(yaml@2.8.1))':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
@@ -3846,7 +4069,7 @@ snapshots:
std-env: 3.10.0 std-env: 3.10.0
test-exclude: 7.0.1 test-exclude: 7.0.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vitest: 3.2.4(@types/node@24.7.2) vitest: 3.2.4(@types/node@24.7.2)(yaml@2.8.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -3858,13 +4081,13 @@ snapshots:
chai: 5.3.3 chai: 5.3.3
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@7.1.10(@types/node@24.7.2))': '@vitest/mocker@3.2.4(vite@7.1.10(@types/node@24.7.2)(yaml@2.8.1))':
dependencies: dependencies:
'@vitest/spy': 3.2.4 '@vitest/spy': 3.2.4
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.19 magic-string: 0.30.19
optionalDependencies: optionalDependencies:
vite: 7.1.10(@types/node@24.7.2) vite: 7.1.10(@types/node@24.7.2)(yaml@2.8.1)
'@vitest/pretty-format@3.2.4': '@vitest/pretty-format@3.2.4':
dependencies: dependencies:
@@ -3947,6 +4170,10 @@ snapshots:
agent-base@7.1.4: {} agent-base@7.1.4: {}
ajv-draft-04@1.0.0(ajv@8.17.1):
optionalDependencies:
ajv: 8.17.1
ajv-formats@3.0.1(ajv@8.17.1): ajv-formats@3.0.1(ajv@8.17.1):
optionalDependencies: optionalDependencies:
ajv: 8.17.1 ajv: 8.17.1
@@ -4353,6 +4580,8 @@ snapshots:
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
depd@2.0.0: {}
dequal@2.0.3: {} dequal@2.0.3: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
@@ -4719,8 +4948,18 @@ snapshots:
dependencies: dependencies:
reusify: 1.1.0 reusify: 1.1.0
fastify-plugin@4.5.1: {}
fastify-plugin@5.1.0: {} fastify-plugin@5.1.0: {}
fastify-type-provider-zod@6.0.0(@fastify/swagger@9.5.2)(fastify@5.6.1)(openapi-types@12.1.3)(zod@4.1.12):
dependencies:
'@fastify/error': 4.2.0
'@fastify/swagger': 9.5.2
fastify: 5.6.1
openapi-types: 12.1.3
zod: 4.1.12
fastify@5.6.1: fastify@5.6.1:
dependencies: dependencies:
'@fastify/ajv-compiler': 4.0.3 '@fastify/ajv-compiler': 4.0.3
@@ -4799,6 +5038,8 @@ snapshots:
hasown: 2.0.2 hasown: 2.0.2
mime-types: 2.1.35 mime-types: 2.1.35
forwarded@0.2.0: {}
fs-constants@1.0.0: {} fs-constants@1.0.0: {}
fsevents@2.3.3: fsevents@2.3.3:
@@ -4858,6 +5099,8 @@ snapshots:
github-from-package@0.0.0: {} github-from-package@0.0.0: {}
github-slugger@2.0.0: {}
glob-parent@5.1.2: glob-parent@5.1.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -4918,6 +5161,14 @@ snapshots:
html-escaper@2.0.2: {} html-escaper@2.0.2: {}
http-errors@2.0.0:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.1
toidentifier: 1.0.1
human-signals@2.1.0: {} human-signals@2.1.0: {}
hyperid@3.3.0: hyperid@3.3.0:
@@ -5138,6 +5389,14 @@ snapshots:
dependencies: dependencies:
dequal: 2.0.3 dequal: 2.0.3
json-schema-resolver@3.0.0:
dependencies:
debug: 4.4.3
fast-uri: 3.1.0
rfdc: 1.4.1
transitivePeerDependencies:
- supports-color
json-schema-traverse@0.4.1: {} json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {} json-schema-traverse@1.0.0: {}
@@ -5158,6 +5417,8 @@ snapshots:
'@jsep-plugin/regex': 1.0.4(jsep@1.4.0) '@jsep-plugin/regex': 1.0.4(jsep@1.4.0)
jsep: 1.4.0 jsep: 1.4.0
jsonpointer@5.0.1: {}
jsonwebtoken@9.0.2: jsonwebtoken@9.0.2:
dependencies: dependencies:
jws: 3.2.2 jws: 3.2.2
@@ -5208,6 +5469,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
leven@4.1.0: {}
levn@0.4.1: levn@0.4.1:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1
@@ -5276,6 +5539,8 @@ snapshots:
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
media-typer@0.3.0: {}
mem@8.1.1: mem@8.1.1:
dependencies: dependencies:
map-age-cleaner: 0.1.3 map-age-cleaner: 0.1.3
@@ -5366,6 +5631,8 @@ snapshots:
nanoid@3.3.11: {} nanoid@3.3.11: {}
nanoid@5.1.5: {}
napi-build-utils@2.0.0: {} napi-build-utils@2.0.0: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
@@ -5446,6 +5713,8 @@ snapshots:
dependencies: dependencies:
mimic-fn: 2.1.0 mimic-fn: 2.1.0
openapi-types@12.1.3: {}
openid-client@6.8.1: openid-client@6.8.1:
dependencies: dependencies:
jose: 6.1.0 jose: 6.1.0
@@ -5843,6 +6112,8 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
es-object-atoms: 1.1.1 es-object-atoms: 1.1.1
setprototypeof@1.2.0: {}
shebang-command@2.0.0: shebang-command@2.0.0:
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0
@@ -5933,6 +6204,8 @@ snapshots:
as-table: 1.0.55 as-table: 1.0.55
get-source: 2.0.12 get-source: 2.0.12
statuses@2.0.1: {}
std-env@3.10.0: {} std-env@3.10.0: {}
stop-iteration-iterator@1.1.0: stop-iteration-iterator@1.1.0:
@@ -6031,6 +6304,8 @@ snapshots:
dependencies: dependencies:
'@pkgr/core': 0.2.9 '@pkgr/core': 0.2.9
tagged-tag@1.0.0: {}
tar-fs@2.1.4: tar-fs@2.1.4:
dependencies: dependencies:
chownr: 1.1.4 chownr: 1.1.4
@@ -6112,6 +6387,8 @@ snapshots:
toad-cache@3.7.0: {} toad-cache@3.7.0: {}
toidentifier@1.0.1: {}
tr46@0.0.3: {} tr46@0.0.3: {}
ts-api-utils@2.1.0(typescript@5.9.3): ts-api-utils@2.1.0(typescript@5.9.3):
@@ -6139,6 +6416,15 @@ snapshots:
type-fest@0.6.0: {} type-fest@0.6.0: {}
type-fest@5.0.0:
dependencies:
tagged-tag: 1.0.0
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
mime-types: 2.1.35
typed-array-buffer@1.0.3: typed-array-buffer@1.0.3:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
@@ -6212,13 +6498,15 @@ snapshots:
uuid@8.3.2: {} uuid@8.3.2: {}
vite-node@3.2.4(@types/node@24.7.2): vary@1.1.2: {}
vite-node@3.2.4(@types/node@24.7.2)(yaml@2.8.1):
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14
debug: 4.4.3 debug: 4.4.3
es-module-lexer: 1.7.0 es-module-lexer: 1.7.0
pathe: 2.0.3 pathe: 2.0.3
vite: 7.1.10(@types/node@24.7.2) vite: 7.1.10(@types/node@24.7.2)(yaml@2.8.1)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- jiti - jiti
@@ -6233,7 +6521,7 @@ snapshots:
- tsx - tsx
- yaml - yaml
vite@7.1.10(@types/node@24.7.2): vite@7.1.10(@types/node@24.7.2)(yaml@2.8.1):
dependencies: dependencies:
esbuild: 0.25.10 esbuild: 0.25.10
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@@ -6244,12 +6532,13 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/node': 24.7.2 '@types/node': 24.7.2
fsevents: 2.3.3 fsevents: 2.3.3
yaml: 2.8.1
vitest@3.2.4(@types/node@24.7.2): vitest@3.2.4(@types/node@24.7.2)(yaml@2.8.1):
dependencies: dependencies:
'@types/chai': 5.2.2 '@types/chai': 5.2.2
'@vitest/expect': 3.2.4 '@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.1.10(@types/node@24.7.2)) '@vitest/mocker': 3.2.4(vite@7.1.10(@types/node@24.7.2)(yaml@2.8.1))
'@vitest/pretty-format': 3.2.4 '@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4 '@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4 '@vitest/snapshot': 3.2.4
@@ -6267,8 +6556,8 @@ snapshots:
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinypool: 1.1.1 tinypool: 1.1.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vite: 7.1.10(@types/node@24.7.2) vite: 7.1.10(@types/node@24.7.2)(yaml@2.8.1)
vite-node: 3.2.4(@types/node@24.7.2) vite-node: 3.2.4(@types/node@24.7.2)(yaml@2.8.1)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 24.7.2 '@types/node': 24.7.2
@@ -6431,6 +6720,12 @@ snapshots:
xtend@4.0.2: {} xtend@4.0.2: {}
yaml@2.8.0: {}
yaml@2.8.1: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zod@4.1.11: {}
zod@4.1.12: {} zod@4.1.12: {}

View File

@@ -1,8 +1,34 @@
import { type FastifyPluginAsync } from 'fastify'; import { type FastifyPluginAsync } from 'fastify';
import { manageEndpoints } from './endpoints/endpoints.manage.ts';
import { authPlugin } from './plugins/plugins.auth.ts';
import { messageEndpoints } from './endpoints/endpoints.message.ts';
import { z } from 'zod';
const api: FastifyPluginAsync = async (fastify) => { const api: FastifyPluginAsync = async (fastify) => {
fastify.get('/healthz', () => { fastify.route({
return { status: 'ok' }; method: 'get',
url: '/health',
schema: {
operationId: 'health.get',
summary: 'Get health status',
tags: ['system'],
response: {
200: z.object({
status: z.literal('ok'),
}),
},
},
handler: () => {
return { status: 'ok' };
},
});
await authPlugin(fastify, {});
await fastify.register(manageEndpoints, {
prefix: '/manage',
});
await fastify.register(messageEndpoints, {
prefix: '/message',
}); });
}; };

View File

@@ -0,0 +1,45 @@
import { JwtAuth } from '#root/auth/auth.jwt.ts';
import { statementSchema } from '#root/auth/auth.schemas.ts';
import { Config } from '#root/config/config.ts';
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
import { z } from 'zod';
const manageEndpoints: FastifyPluginAsyncZod = async (fastify) => {
const config = fastify.services.get(Config);
if (config.jwtSecret) {
fastify.route({
method: 'post',
url: '/jwt',
schema: {
operationId: 'manage.jwt.post',
summary: 'Generate a JWT',
tags: ['manage'],
body: z.object({
exp: z.number().optional(),
statements: z.array(statementSchema),
}),
response: {
200: z.object({
jwt: z.string(),
}),
},
},
handler: async (req, reply) => {
if (
!req.session.validate({
action: 'mgmt:generate-jwt',
resource: 'mgmt/',
})
) {
throw reply.unauthorized('not allowed');
}
const jwtAuth = fastify.services.get(JwtAuth);
const jwt = jwtAuth.generate(req.body);
reply.send({ jwt });
},
});
}
};
export { manageEndpoints };

View File

@@ -0,0 +1,62 @@
import { Config } from '#root/config/config.ts';
import { MqttServer } from '#root/server/server.ts';
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
import { z } from 'zod';
const messageEndpoints: FastifyPluginAsyncZod = async (fastify) => {
const config = fastify.services.get(Config);
if (config.jwtSecret) {
fastify.route({
method: 'post',
url: '',
schema: {
summary: 'Post a message to the bus',
operationId: 'message.post',
tags: ['message'],
body: z.object({
topic: z.string(),
dup: z.boolean(),
qos: z.union([z.literal(0), z.literal(1), z.literal(2)]),
retain: z.boolean(),
payload: z.string(),
}),
response: {
200: z.object({
success: z.literal(true),
}),
},
},
handler: async (req, reply) => {
if (
!req.session.validate({
action: 'mqtt:publish',
resource: 'mgmt:',
})
) {
throw reply.unauthorized('not allowed');
}
const server = fastify.services.get(MqttServer);
await new Promise<void>((resolve, reject) => {
server.bus.publish(
{
...req.body,
cmd: 'publish',
payload: Buffer.from(req.body.payload, 'base64'),
},
(err) => {
if (err) {
return reject(err);
}
resolve();
},
);
});
reply.send({ success: true });
},
});
}
};
export { messageEndpoints };

14
src/api/extensions.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
import type { Session } from '#root/services/sessions/sessions.session.ts';
import type { Services } from '#root/utils/services.ts';
import 'fastify';
declare module 'fastify' {
// eslint-disable-next-line
export interface FastifyInstance {
services: Services;
}
// eslint-disable-next-line
export interface FastifyRequest {
session: Session;
}
}

View File

@@ -0,0 +1,27 @@
import { SessionProvider } from '#root/services/sessions/sessions.provider.ts';
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
const authPlugin: FastifyPluginAsyncZod = async (fastify) => {
fastify.addHook('onRequest', async (req, reply) => {
const authProvider = req.headers['x-auth-provider'];
if (!authProvider || Array.isArray(authProvider)) {
throw reply.unauthorized('missing x-auth-provider header');
}
const authorization = req.headers.authorization;
if (!authorization) {
throw reply.unauthorized('missing authorization header');
}
const [type, token] = authorization.split(' ');
if (type.toLowerCase() !== 'bearer') {
throw reply.unauthorized('only bearer tokens are allowed');
}
if (!token) {
throw reply.unauthorized('missing token');
}
const sessionProvider = fastify.services.get(SessionProvider);
const session = await sessionProvider.get(authProvider, token);
req.session = session;
});
};
export { authPlugin };

View File

@@ -1,15 +1,7 @@
import type { Services } from '#root/utils/services.ts'; import type { Services } from '#root/utils/services.ts';
import { Config } from '#root/config/config.ts'; import { Config } from '#root/config/config.ts';
import type { Statement } from './auth.schemas.ts';
import type { AuthProvider } from './auth.provider.ts'; import type { AuthProvider } from './auth.provider.ts';
import { ADMIN_STATEMENTS } from './auth.consts.ts';
const adminStatements: Statement[] = [
{
effect: 'allow',
resources: ['**'],
actions: ['**'],
},
];
class AdminAuth implements AuthProvider { class AdminAuth implements AuthProvider {
#services: Services; #services: Services;
@@ -24,7 +16,7 @@ class AdminAuth implements AuthProvider {
throw new Error('Invalid admin token'); throw new Error('Invalid admin token');
} }
return { return {
statements: adminStatements, statements: ADMIN_STATEMENTS,
}; };
}; };
} }

25
src/auth/auth.consts.ts Normal file
View File

@@ -0,0 +1,25 @@
import type { Statement } from './auth.schemas.ts';
const ADMIN_STATEMENTS: Statement[] = [
{
effect: 'allow',
resources: ['**'],
actions: ['**'],
},
];
const WRITER_STATEMENTS: Statement[] = [
{
effect: 'allow',
resources: ['**'],
actions: ['mqtt:**'],
},
];
const READER_STATEMENTS: Statement[] = [
{
effect: 'allow',
resources: ['**'],
actions: ['mqtt:read', 'mqtt:subscribe'],
},
];
export { ADMIN_STATEMENTS, WRITER_STATEMENTS, READER_STATEMENTS };

View File

@@ -8,6 +8,7 @@ import type { Services } from '#root/utils/services.ts';
import { Config } from '#root/config/config.ts'; import { Config } from '#root/config/config.ts';
const tokenBodySchema = z.object({ const tokenBodySchema = z.object({
exp: z.number().optional(),
statements: z.array(statementSchema), statements: z.array(statementSchema),
}); });
@@ -32,11 +33,11 @@ class JwtAuth implements AuthProvider {
public getAccess = async (token: string) => { public getAccess = async (token: string) => {
const config = this.#services.get(Config); const config = this.#services.get(Config);
const { jwtSecret: tokenSecret } = config; const { jwtSecret } = config;
if (!tokenSecret) { if (!jwtSecret) {
throw new Error('Token secret does not exist'); throw new Error('Token secret does not exist');
} }
const data = jwt.verify(token, tokenSecret); const data = jwt.verify(token, jwtSecret);
const parsed = tokenBodySchema.parse(data); const parsed = tokenBodySchema.parse(data);
return parsed; return parsed;
}; };

View File

@@ -5,28 +5,7 @@ import type { AuthProvider } from './auth.provider.ts';
import type { Services } from '#root/utils/services.ts'; import type { Services } from '#root/utils/services.ts';
import { Config } from '#root/config/config.ts'; import { Config } from '#root/config/config.ts';
import { ADMIN_STATEMENTS, READER_STATEMENTS, WRITER_STATEMENTS } from './auth.consts.ts';
const adminStatements: Statement[] = [
{
effect: 'allow',
resources: ['**'],
actions: ['**'],
},
];
const writerStatements: Statement[] = [
{
effect: 'allow',
resources: ['**'],
actions: ['mqtt:**'],
},
];
const readerStatements: Statement[] = [
{
effect: 'allow',
resources: ['**'],
actions: ['mqtt:read', 'mqtt:subscribe'],
},
];
class OidcAuth implements AuthProvider { class OidcAuth implements AuthProvider {
#services: Services; #services: Services;
@@ -49,13 +28,13 @@ class OidcAuth implements AuthProvider {
const groups = data[config.oidc.groupField]; const groups = data[config.oidc.groupField];
if (Array.isArray(groups)) { if (Array.isArray(groups)) {
if (config.oidc.groups.admin && groups.includes(config.oidc.groups.admin)) { if (config.oidc.groups.admin && groups.includes(config.oidc.groups.admin)) {
statements = adminStatements; statements = ADMIN_STATEMENTS;
} }
if (config.oidc.groups.writer && groups.includes(config.oidc.groups.writer)) { if (config.oidc.groups.writer && groups.includes(config.oidc.groups.writer)) {
statements = writerStatements; statements = WRITER_STATEMENTS;
} }
if (config.oidc.groups.reader && groups.includes(config.oidc.groups.reader)) { if (config.oidc.groups.reader && groups.includes(config.oidc.groups.reader)) {
statements = readerStatements; statements = READER_STATEMENTS;
} }
} }
return { return {

View File

@@ -45,8 +45,7 @@ class Backbone {
await this.k8s.setup(); await this.k8s.setup();
this.sessionProvider.register('k8s', this.#services.get(K8sAuth)); this.sessionProvider.register('k8s', this.#services.get(K8sAuth));
} }
if (this.config.http.enabled) { if (this.config.ws.enabled || this.config.api.enabled) {
console.log('starting http');
const http = await this.server.getHttpServer(); const http = await this.server.getHttpServer();
http.listen({ port: this.config.http.port, host: '0.0.0.0' }); http.listen({ port: this.config.http.port, host: '0.0.0.0' });
} }

View File

@@ -1,6 +1,6 @@
class Config { class Config {
public get jwtSecret() { public get jwtSecret() {
return process.env.TOKEN_SECRET; return process.env.JWT_SECRET;
} }
public get adminToken() { public get adminToken() {
@@ -38,14 +38,26 @@ class Config {
} }
public get http() { public get http() {
const enabled = (process.env.HTTP_ENABLED = 'true');
const port = process.env.HTTP_PORT ? parseInt(process.env.HTTP_PORT) : 8883; const port = process.env.HTTP_PORT ? parseInt(process.env.HTTP_PORT) : 8883;
return { return {
enabled,
port, port,
}; };
} }
public get api() {
const enabled = process.env.API_ENABLED === 'true';
return {
enabled,
};
}
public get ws() {
const enabled = process.env.WS_ENABLED === 'true';
return {
enabled,
};
}
public get tcp() { public get tcp() {
const enabled = (process.env.TCP_ENABLED = 'true'); const enabled = (process.env.TCP_ENABLED = 'true');
const port = process.env.TCP_PORT ? parseInt(process.env.TCP_PORT) : 1883; const port = process.env.TCP_PORT ? parseInt(process.env.TCP_PORT) : 1883;

9
src/dev.ts Normal file
View File

@@ -0,0 +1,9 @@
import { Backbone } from './backbone.ts';
process.env.JWT_SECRET = 'test';
process.env.ADMIN_TOKEN = 'admin';
process.env.API_ENABLED = 'true';
const backbone = new Backbone();
await backbone.start();
console.log('started');

View File

@@ -1,6 +1,15 @@
import tcp from 'node:net'; import tcp from 'node:net';
import type { IncomingMessage } from 'node:http'; import type { IncomingMessage } from 'node:http';
import swagger from '@fastify/swagger';
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
import {
jsonSchemaTransform,
createJsonSchemaTransform,
serializerCompiler,
validatorCompiler,
} from 'fastify-type-provider-zod';
import scalar from '@scalar/fastify-api-reference';
import { import {
type AuthenticateHandler, type AuthenticateHandler,
type AuthorizeForwardHandler, type AuthorizeForwardHandler,
@@ -19,6 +28,8 @@ import { TopicsHandler } from '#root/topics/topics.handler.ts';
import type { Services } from '#root/utils/services.ts'; import type { Services } from '#root/utils/services.ts';
import { Session } from '#root/services/sessions/sessions.session.ts'; import { Session } from '#root/services/sessions/sessions.session.ts';
import { SessionProvider } from '#root/services/sessions/sessions.provider.ts'; import { SessionProvider } from '#root/services/sessions/sessions.provider.ts';
import fastifySensible from '@fastify/sensible';
import { Config } from '#root/config/config.ts';
type Aedes = ReturnType<typeof aedes.createBroker>; type Aedes = ReturnType<typeof aedes.createBroker>;
@@ -52,6 +63,10 @@ class MqttServer {
}); });
} }
public get bus() {
return this.#server;
}
#authenticate: AuthenticateHandler = async (client, username, password, callback) => { #authenticate: AuthenticateHandler = async (client, username, password, callback) => {
try { try {
if (!username || !password) { if (!username || !password) {
@@ -112,14 +127,51 @@ class MqttServer {
#setupHttpServer = async () => { #setupHttpServer = async () => {
const http = fastify({}); const http = fastify({});
await http.register(fastifyWebSocket); const config = this.#services.get(Config);
http.get('/ws', { websocket: true }, (socket, req) => { if (config.api.enabled) {
const stream = createWebSocketStream(socket); http.decorate('services', this.#services);
this.#server.handle(stream, req as unknown as IncomingMessage); http.setValidatorCompiler(validatorCompiler);
}); http.setSerializerCompiler(serializerCompiler);
await http.register(api, { await http.register(fastifyWebSocket);
prefix: '/api', await http.register(fastifySensible);
}); await http.register(swagger, {
openapi: {
info: {
title: 'Backbone',
version: '1.0.0',
},
components: {
securitySchemes: {
authProviderHeader: {
type: 'apiKey',
name: 'X-Auth-Provider',
in: 'header',
},
bearerAuth: {
type: 'http',
scheme: 'bearer',
},
},
},
security: [{ bearerAuth: [], authProviderHeader: [] }],
},
transform: jsonSchemaTransform,
});
await http.register(scalar, {
routePrefix: '/docs',
});
await http.register(api, {
prefix: '/api',
});
}
if (config.ws.enabled) {
http.get('/ws', { websocket: true }, (socket, req) => {
const stream = createWebSocketStream(socket);
this.#server.handle(stream, req as unknown as IncomingMessage);
});
}
await http.ready();
http.swagger();
return http; return http;
}; };

View File

@@ -1,4 +1,5 @@
import type { AuthProvider } from '#root/auth/auth.provider.ts'; import type { AuthProvider } from '#root/auth/auth.provider.ts';
import { Session } from './sessions.session.ts';
class SessionProvider { class SessionProvider {
#handlers: Map<string, AuthProvider>; #handlers: Map<string, AuthProvider>;
@@ -7,6 +8,10 @@ class SessionProvider {
this.#handlers = new Map(); this.#handlers = new Map();
} }
public get providers() {
return Array.from(this.#handlers.keys());
}
public register = (name: string, provider: AuthProvider) => { public register = (name: string, provider: AuthProvider) => {
this.#handlers.set(name, provider); this.#handlers.set(name, provider);
}; };
@@ -18,6 +23,15 @@ class SessionProvider {
} }
return handler.getAccess(token); return handler.getAccess(token);
}; };
public get = async (provider: string, token: string) => {
const handler = this.#handlers.get(provider);
if (!handler) {
throw new Error('Provider not available');
}
const access = await handler.getAccess(token);
return new Session(access);
};
} }
export { SessionProvider }; export { SessionProvider };