refact: use service container

This commit is contained in:
Morten Olsen
2025-10-16 12:13:22 +02:00
parent 8e594d59fd
commit 1f4b9cab04
12 changed files with 657 additions and 143 deletions

View File

@@ -45,9 +45,12 @@
"aedes": "^0.51.3",
"aedes-persistence": "^10.2.2",
"ajv": "^8.17.1",
"better-sqlite3": "^12.4.1",
"fastify": "^5.6.1",
"jsonwebtoken": "^9.0.2",
"knex": "^3.1.0",
"micromatch": "^4.0.8",
"pg": "^8.16.3",
"ws": "^8.18.3",
"zod": "^4.1.12"
}

408
pnpm-lock.yaml generated
View File

@@ -23,15 +23,24 @@ importers:
ajv:
specifier: ^8.17.1
version: 8.17.1
better-sqlite3:
specifier: ^12.4.1
version: 12.4.1
fastify:
specifier: ^5.6.1
version: 5.6.1
jsonwebtoken:
specifier: ^9.0.2
version: 9.0.2
knex:
specifier: ^3.1.0
version: 3.1.0(better-sqlite3@12.4.1)(pg@8.16.3)
micromatch:
specifier: ^4.0.8
version: 4.0.8
pg:
specifier: ^8.16.3
version: 8.16.3
ws:
specifier: ^8.18.3
version: 8.18.3
@@ -1018,6 +1027,13 @@ packages:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
engines: {node: '>=4'}
better-sqlite3@12.4.1:
resolution: {integrity: sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==}
engines: {node: 20.x || 22.x || 23.x || 24.x}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@@ -1108,6 +1124,9 @@ packages:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'}
chownr@1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
cli-boxes@2.2.1:
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
engines: {node: '>=6'}
@@ -1127,10 +1146,17 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
colorette@2.0.19:
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@10.0.1:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
commist@3.2.0:
resolution: {integrity: sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==}
@@ -1179,6 +1205,15 @@ packages:
supports-color:
optional: true
debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -1188,10 +1223,18 @@ packages:
supports-color:
optional: true
decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -1283,6 +1326,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
escalade@3.2.0:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -1363,6 +1410,10 @@ packages:
jiti:
optional: true
esm@3.2.25:
resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==}
engines: {node: '>=6'}
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1401,6 +1452,10 @@ packages:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
expand-template@2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
engines: {node: '>=6'}
expect-type@1.2.2:
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
engines: {node: '>=12.0.0'}
@@ -1479,6 +1534,9 @@ packages:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -1510,6 +1568,9 @@ packages:
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
engines: {node: '>= 6'}
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -1533,6 +1594,10 @@ packages:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-package-type@0.1.0:
resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
engines: {node: '>=8.0.0'}
get-port@7.1.0:
resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==}
engines: {node: '>=16'}
@@ -1552,6 +1617,12 @@ packages:
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
engines: {node: '>= 0.4'}
getopts@2.3.0:
resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==}
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -1665,6 +1736,10 @@ packages:
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
engines: {node: '>= 0.4'}
interpret@2.2.0:
resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==}
engines: {node: '>= 0.10'}
ip-address@10.0.1:
resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==}
engines: {node: '>= 12'}
@@ -1894,6 +1969,34 @@ packages:
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
knex@3.1.0:
resolution: {integrity: sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==}
engines: {node: '>=16'}
hasBin: true
peerDependencies:
better-sqlite3: '*'
mysql: '*'
mysql2: '*'
pg: '*'
pg-native: '*'
sqlite3: '*'
tedious: '*'
peerDependenciesMeta:
better-sqlite3:
optional: true
mysql:
optional: true
mysql2:
optional: true
pg:
optional: true
pg-native:
optional: true
sqlite3:
optional: true
tedious:
optional: true
levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
@@ -1936,6 +2039,9 @@ packages:
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
@@ -1995,6 +2101,10 @@ packages:
resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==}
engines: {node: '>=8'}
mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -2009,6 +2119,9 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mkdirp-classic@0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
mqemitter@6.0.2:
resolution: {integrity: sha512-8RGlznQx/Nb1xC3xKUFXHWov7pn7JdH++YVwlr6SLT6k3ft1h+ImGqZdVudbdKruFckIq9wheq9s4hgCivJDow==}
engines: {node: '>=16'}
@@ -2024,6 +2137,9 @@ packages:
engines: {node: '>=16.0.0'}
hasBin: true
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -2032,6 +2148,9 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@@ -2040,6 +2159,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
node-abi@3.78.0:
resolution: {integrity: sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==}
engines: {node: '>=10'}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
@@ -2180,6 +2303,43 @@ packages:
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
engines: {node: '>= 14.16'}
pg-cloudflare@1.2.7:
resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==}
pg-connection-string@2.6.2:
resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==}
pg-connection-string@2.9.1:
resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==}
pg-int8@1.0.1:
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
engines: {node: '>=4.0.0'}
pg-pool@3.10.1:
resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==}
peerDependencies:
pg: '>=8.0'
pg-protocol@1.10.3:
resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
pg-types@2.2.0:
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
engines: {node: '>=4'}
pg@8.16.3:
resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==}
engines: {node: '>= 16.0.0'}
peerDependencies:
pg-native: '>=3.0.1'
peerDependenciesMeta:
pg-native:
optional: true
pgpass@1.0.5:
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2209,6 +2369,27 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
postgres-array@2.0.0:
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
engines: {node: '>=4'}
postgres-bytea@1.0.0:
resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
engines: {node: '>=0.10.0'}
postgres-date@1.0.7:
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
engines: {node: '>=0.10.0'}
postgres-interval@1.2.0:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
hasBin: true
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -2274,6 +2455,10 @@ packages:
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
engines: {node: '>=8'}
rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
read-ini-file@4.0.0:
resolution: {integrity: sha512-zz4qv/sKETv7nAkATqSJ9YMbKD8NXRPuA8d17VdYCuNYrVstB1S6UAMU6aytf5vRa9MESbZN7jLZdcmrOxz4gg==}
engines: {node: '>=14.6'}
@@ -2298,6 +2483,10 @@ packages:
resolution: {integrity: sha512-wnWtnywepjg/eHIgWR97R7UuM5i+qHLA195qdN9UPKvcMqfn60+67S8sPPW3vDlSEfYHoFkKU8IvpCNty3zQvQ==}
engines: {node: '>=10'}
rechoir@0.8.0:
resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==}
engines: {node: '>= 10.13.0'}
reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'}
@@ -2314,6 +2503,10 @@ packages:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
resolve-from@5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'}
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
@@ -2439,6 +2632,12 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
simple-concat@1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
simple-get@4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
slow-redact@0.3.2:
resolution: {integrity: sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw==}
@@ -2549,6 +2748,10 @@ packages:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
strip-json-comments@2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
engines: {node: '>=0.10.0'}
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@@ -2568,12 +2771,23 @@ packages:
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
engines: {node: ^14.18.0 || >=16.0.0}
tar-fs@2.1.4:
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
tar-fs@3.1.1:
resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
tar-stream@3.1.7:
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
tarn@3.0.2:
resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==}
engines: {node: '>=8.0.0'}
test-exclude@7.0.1:
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
engines: {node: '>=18'}
@@ -2587,6 +2801,10 @@ packages:
through2@4.0.2:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
tildify@2.0.0:
resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==}
engines: {node: '>=8'}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -2632,6 +2850,9 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -3897,6 +4118,15 @@ snapshots:
dependencies:
is-windows: 1.0.2
better-sqlite3@12.4.1:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
bl@4.1.0:
dependencies:
buffer: 5.7.1
@@ -4012,6 +4242,8 @@ snapshots:
check-error@2.1.1: {}
chownr@1.1.4: {}
cli-boxes@2.2.1: {}
cli-columns@4.0.0:
@@ -4027,10 +4259,14 @@ snapshots:
color-name@1.1.4: {}
colorette@2.0.19: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@10.0.1: {}
commist@3.2.0: {}
concat-map@0.0.1: {}
@@ -4081,12 +4317,22 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.3.4:
dependencies:
ms: 2.1.2
debug@4.4.3:
dependencies:
ms: 2.1.3
decompress-response@6.0.0:
dependencies:
mimic-response: 3.1.0
deep-eql@5.0.2: {}
deep-extend@0.6.0: {}
deep-is@0.1.4: {}
defaults@1.0.4:
@@ -4259,6 +4505,8 @@ snapshots:
'@esbuild/win32-ia32': 0.25.10
'@esbuild/win32-x64': 0.25.10
escalade@3.2.0: {}
escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.8(eslint@9.37.0):
@@ -4370,6 +4618,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
esm@3.2.25: {}
espree@10.4.0:
dependencies:
acorn: 8.15.0
@@ -4414,6 +4664,8 @@ snapshots:
signal-exit: 3.0.7
strip-final-newline: 2.0.0
expand-template@2.0.3: {}
expect-type@1.2.2: {}
fast-decode-uri-component@1.0.1: {}
@@ -4506,6 +4758,8 @@ snapshots:
dependencies:
flat-cache: 4.0.1
file-uri-to-path@1.0.0: {}
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -4545,6 +4799,8 @@ snapshots:
hasown: 2.0.2
mime-types: 2.1.35
fs-constants@1.0.0: {}
fsevents@2.3.3:
optional: true
@@ -4576,6 +4832,8 @@ snapshots:
hasown: 2.0.2
math-intrinsics: 1.1.0
get-package-type@0.1.0: {}
get-port@7.1.0: {}
get-proto@1.0.1:
@@ -4596,6 +4854,10 @@ snapshots:
es-errors: 1.3.0
get-intrinsic: 1.3.0
getopts@2.3.0: {}
github-from-package@0.0.0: {}
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -4691,6 +4953,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.1.0
interpret@2.2.0: {}
ip-address@10.0.1: {}
ipaddr.js@2.2.0: {}
@@ -4922,6 +5186,28 @@ snapshots:
dependencies:
json-buffer: 3.0.1
knex@3.1.0(better-sqlite3@12.4.1)(pg@8.16.3):
dependencies:
colorette: 2.0.19
commander: 10.0.1
debug: 4.3.4
escalade: 3.2.0
esm: 3.2.25
get-package-type: 0.1.0
getopts: 2.3.0
interpret: 2.2.0
lodash: 4.17.21
pg-connection-string: 2.6.2
rechoir: 0.8.0
resolve-from: 5.0.0
tarn: 3.0.2
tildify: 2.0.0
optionalDependencies:
better-sqlite3: 12.4.1
pg: 8.16.3
transitivePeerDependencies:
- supports-color
levn@0.4.1:
dependencies:
prelude-ls: 1.2.1
@@ -4962,6 +5248,8 @@ snapshots:
lodash.once@4.1.1: {}
lodash@4.17.21: {}
loupe@3.2.1: {}
lru-cache@10.4.3: {}
@@ -5012,6 +5300,8 @@ snapshots:
mimic-fn@3.1.0: {}
mimic-response@3.1.0: {}
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -5024,6 +5314,8 @@ snapshots:
minipass@7.1.2: {}
mkdirp-classic@0.5.3: {}
mqemitter@6.0.2:
dependencies:
fastparallel: 2.4.1
@@ -5068,10 +5360,14 @@ snapshots:
- supports-color
- utf-8-validate
ms@2.1.2: {}
ms@2.1.3: {}
nanoid@3.3.11: {}
napi-build-utils@2.0.0: {}
natural-compare@1.4.0: {}
ndjson@2.0.0:
@@ -5082,6 +5378,10 @@ snapshots:
split2: 3.2.2
through2: 4.0.2
node-abi@3.78.0:
dependencies:
semver: 7.7.3
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
@@ -5220,6 +5520,43 @@ snapshots:
pathval@2.0.1: {}
pg-cloudflare@1.2.7:
optional: true
pg-connection-string@2.6.2: {}
pg-connection-string@2.9.1: {}
pg-int8@1.0.1: {}
pg-pool@3.10.1(pg@8.16.3):
dependencies:
pg: 8.16.3
pg-protocol@1.10.3: {}
pg-types@2.2.0:
dependencies:
pg-int8: 1.0.1
postgres-array: 2.0.0
postgres-bytea: 1.0.0
postgres-date: 1.0.7
postgres-interval: 1.2.0
pg@8.16.3:
dependencies:
pg-connection-string: 2.9.1
pg-pool: 3.10.1(pg@8.16.3)
pg-protocol: 1.10.3
pg-types: 2.2.0
pgpass: 1.0.5
optionalDependencies:
pg-cloudflare: 1.2.7
pgpass@1.0.5:
dependencies:
split2: 4.2.0
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -5254,6 +5591,31 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postgres-array@2.0.0: {}
postgres-bytea@1.0.0: {}
postgres-date@1.0.7: {}
postgres-interval@1.2.0:
dependencies:
xtend: 4.0.2
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.1.2
expand-template: 2.0.3
github-from-package: 0.0.0
minimist: 1.2.8
mkdirp-classic: 0.5.3
napi-build-utils: 2.0.0
node-abi: 3.78.0
pump: 3.0.3
rc: 1.2.8
simple-get: 4.0.1
tar-fs: 2.1.4
tunnel-agent: 0.6.0
prelude-ls@1.2.1: {}
prettier-linter-helpers@1.0.0:
@@ -5297,6 +5659,13 @@ snapshots:
quick-lru@4.0.1: {}
rc@1.2.8:
dependencies:
deep-extend: 0.6.0
ini: 1.3.8
minimist: 1.2.8
strip-json-comments: 2.0.1
read-ini-file@4.0.0:
dependencies:
ini: 3.0.1
@@ -5325,6 +5694,10 @@ snapshots:
realpath-missing@1.1.0: {}
rechoir@0.8.0:
dependencies:
resolve: 1.22.10
reflect.getprototypeof@1.0.10:
dependencies:
call-bind: 1.0.8
@@ -5349,6 +5722,8 @@ snapshots:
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
@@ -5508,6 +5883,14 @@ snapshots:
signal-exit@4.1.0: {}
simple-concat@1.0.1: {}
simple-get@4.0.1:
dependencies:
decompress-response: 6.0.0
once: 1.4.0
simple-concat: 1.0.1
slow-redact@0.3.2: {}
smart-buffer@4.2.0: {}
@@ -5630,6 +6013,8 @@ snapshots:
strip-final-newline@2.0.0: {}
strip-json-comments@2.0.1: {}
strip-json-comments@3.1.1: {}
strip-literal@3.1.0:
@@ -5646,6 +6031,13 @@ snapshots:
dependencies:
'@pkgr/core': 0.2.9
tar-fs@2.1.4:
dependencies:
chownr: 1.1.4
mkdirp-classic: 0.5.3
pump: 3.0.3
tar-stream: 2.2.0
tar-fs@3.1.1:
dependencies:
pump: 3.0.3
@@ -5658,6 +6050,14 @@ snapshots:
- bare-buffer
- react-native-b4a
tar-stream@2.2.0:
dependencies:
bl: 4.1.0
end-of-stream: 1.4.5
fs-constants: 1.0.0
inherits: 2.0.4
readable-stream: 3.6.2
tar-stream@3.1.7:
dependencies:
b4a: 1.7.3
@@ -5667,6 +6067,8 @@ snapshots:
- bare-abort-controller
- react-native-b4a
tarn@3.0.2: {}
test-exclude@7.0.1:
dependencies:
'@istanbuljs/schema': 0.1.3
@@ -5687,6 +6089,8 @@ snapshots:
dependencies:
readable-stream: 3.6.2
tildify@2.0.0: {}
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
@@ -5723,6 +6127,10 @@ snapshots:
tslib@2.8.1: {}
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1

40
src/backbone.ts Normal file
View File

@@ -0,0 +1,40 @@
import { AccessHandler } from './access/access.handler.ts';
import { K8sService } from './k8s/k8s.ts';
import { MqttServer } from './server/server.ts';
import { TopicsHandler } from './topics/topics.handler.ts';
import { Services } from './utils/services.ts';
class Backbone {
#services: Services;
constructor(services = new Services()) {
this.#services = services;
}
public get services() {
return this.#services;
}
public get server() {
return this.#services.get(MqttServer);
}
public get accessHandler() {
return this.#services.get(AccessHandler);
}
public get topicsHandler() {
return this.#services.get(TopicsHandler);
}
public get k8s() {
return this.#services.get(K8sService);
}
public setupK8sOperator = async () => {
await this.k8s.setup();
this.accessHandler.register('k8s', this.k8s.clients);
};
}
export { Backbone };

View File

@@ -1,33 +1,31 @@
import { KubernetesObjectApi, PatchStrategy, type KubeConfig, type KubernetesObject } from '@kubernetes/client-node';
import { KubernetesObjectApi, type KubernetesObject } from '@kubernetes/client-node';
import type { K8sResources } from './k8s.resources.ts';
import { K8sResources } from './k8s.resources.ts';
import type { K8sBackboneClient } from './k8s.schemas.ts';
import type { AccessProvider } from '#root/access/access.provider.ts';
import type { Statement } from '#root/access/access.schemas.ts';
type K8sClientsOptions = {
config: KubeConfig;
resources: K8sResources;
};
import type { Services } from '#root/utils/services.ts';
import { K8sConfig } from './k8s.config.ts';
type K8sClient = {
statements: Statement[];
};
class K8sClients implements AccessProvider {
#options: K8sClientsOptions;
#services: Services;
#clients: Map<string, K8sClient>;
constructor(options: K8sClientsOptions) {
constructor(services: Services) {
this.#services = services;
this.#clients = new Map();
this.#options = options;
const { clients } = options.resources;
const { clients } = services.get(K8sResources);
clients.on('updated', this.#handleClientAdded);
}
#handleClientAdded = async (manifest: KubernetesObject & { spec: K8sBackboneClient }) => {
const { resources, config } = this.#options;
const resources = this.#services.get(K8sResources);
const { config } = this.#services.get(K8sConfig);
const secretName = `${manifest.metadata?.name}-secret`;
const secret = resources.secrets.manifests.find(
(m) => m.metadata?.namespace === manifest.metadata?.namespace && m.metadata?.name === secretName,

15
src/k8s/k8s.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import { KubeConfig } from '@kubernetes/client-node';
class K8sConfig {
#config?: KubeConfig;
public get config() {
if (!this.#config) {
this.#config = new KubeConfig();
this.#config.loadFromDefault();
}
return this.#config;
}
}
export { K8sConfig };

View File

@@ -1,71 +1,80 @@
import { type KubeConfig, ApiException, ApiextensionsV1Api } from '@kubernetes/client-node';
import type { Services } from '#root/utils/services.ts';
import { ApiException, ApiextensionsV1Api } from '@kubernetes/client-node';
import { z, type ZodType } from 'zod';
import { K8sConfig } from './k8s.config.ts';
type CreateCrdOptions = {
config: KubeConfig;
kind: string;
apiVersion: string;
plural?: string;
scope: 'Cluster' | 'Namespaced';
spec: ZodType;
};
const createCrd = async (options: CreateCrdOptions) => {
const { config, ...definition } = options;
const plural = definition.plural ?? definition.kind.toLowerCase() + 's';
const [version, group] = definition.apiVersion.split('/').toReversed();
const manifest = {
apiVersion: 'apiextensions.k8s.io/v1',
kind: 'CustomResourceDefinition',
metadata: {
name: `${plural}.${group}`,
},
spec: {
group: group,
names: {
kind: definition.kind,
plural: plural,
singular: definition.kind.toLowerCase(),
class K8sCrds {
#services: Services;
constructor(services: Services) {
this.#services = services;
}
install = async (definition: CreateCrdOptions) => {
const { config } = this.#services.get(K8sConfig);
const plural = definition.plural ?? definition.kind.toLowerCase() + 's';
const [version, group] = definition.apiVersion.split('/').toReversed();
const manifest = {
apiVersion: 'apiextensions.k8s.io/v1',
kind: 'CustomResourceDefinition',
metadata: {
name: `${plural}.${group}`,
},
scope: definition.scope,
versions: [
{
name: version,
served: true,
storage: true,
schema: {
openAPIV3Schema: {
type: 'object',
properties: {
spec: {
...z.toJSONSchema(definition.spec, { io: 'input' }),
$schema: undefined,
additionalProperties: undefined,
} as ExplicitAny,
spec: {
group: group,
names: {
kind: definition.kind,
plural: plural,
singular: definition.kind.toLowerCase(),
},
scope: definition.scope,
versions: [
{
name: version,
served: true,
storage: true,
schema: {
openAPIV3Schema: {
type: 'object',
properties: {
spec: {
...z.toJSONSchema(definition.spec, { io: 'input' }),
$schema: undefined,
additionalProperties: undefined,
} as ExplicitAny,
},
},
},
subresources: {
status: {},
},
},
subresources: {
status: {},
},
},
],
},
};
const extensionsApi = config.makeApiClient(ApiextensionsV1Api);
try {
await extensionsApi.createCustomResourceDefinition({
body: manifest,
});
} catch (error) {
if (error instanceof ApiException && error.code === 409) {
await extensionsApi.patchCustomResourceDefinition({
name: manifest.metadata.name,
body: [{ op: 'replace', path: '/spec', value: manifest.spec }],
],
},
};
const extensionsApi = config.makeApiClient(ApiextensionsV1Api);
try {
await extensionsApi.createCustomResourceDefinition({
body: manifest,
});
} else {
throw error;
} catch (error) {
if (error instanceof ApiException && error.code === 409) {
await extensionsApi.patchCustomResourceDefinition({
name: manifest.metadata.name,
body: [{ op: 'replace', path: '/spec', value: manifest.spec }],
});
} else {
throw error;
}
}
}
};
};
}
export { createCrd };
export { K8sCrds };

View File

@@ -1,48 +1,60 @@
import { KubeConfig, V1Secret, type KubernetesObject } from '@kubernetes/client-node';
import { V1Secret, type KubernetesObject } from '@kubernetes/client-node';
import { K8sWatcher } from './k8s.watcher.ts';
import type { K8sBackboneClient, K8sBackboneTopic } from './k8s.schemas.ts';
import type { Services } from '#root/utils/services.ts';
import { K8sConfig } from './k8s.config.ts';
class K8sResources {
#secrets: K8sWatcher<V1Secret>;
#clients: K8sWatcher<KubernetesObject & { spec: K8sBackboneClient }>;
#topics: K8sWatcher<KubernetesObject & { spec: K8sBackboneTopic }>;
#services: Services;
#secrets?: K8sWatcher<V1Secret>;
#clients?: K8sWatcher<KubernetesObject & { spec: K8sBackboneClient }>;
#topics?: K8sWatcher<KubernetesObject & { spec: K8sBackboneTopic }>;
constructor(config: KubeConfig) {
config.loadFromDefault();
this.#secrets = new K8sWatcher({
config,
apiVersion: 'v1',
kind: 'Secret',
});
this.#clients = new K8sWatcher({
config,
apiVersion: 'backbone.mortenolsen.pro/v1',
kind: 'Client',
});
this.#topics = new K8sWatcher({
config,
apiVersion: 'backbone.mortenolsen.pro/v1',
kind: 'Topic',
});
constructor(services: Services) {
this.#services = services;
}
public get secrets() {
if (!this.#secrets) {
const { config } = this.#services.get(K8sConfig);
this.#secrets = new K8sWatcher({
config,
apiVersion: 'v1',
kind: 'Secret',
});
}
return this.#secrets;
}
public get clients() {
if (!this.#clients) {
const { config } = this.#services.get(K8sConfig);
this.#clients = new K8sWatcher({
config,
apiVersion: 'backbone.mortenolsen.pro/v1',
kind: 'Client',
});
}
return this.#clients;
}
public get topics() {
return this.#clients;
if (!this.#topics) {
const { config } = this.#services.get(K8sConfig);
this.#topics = new K8sWatcher({
config,
apiVersion: 'backbone.mortenolsen.pro/v1',
kind: 'Topic',
});
}
return this.#topics;
}
public start = async () => {
await this.#secrets.start();
await this.#clients.start();
await this.#topics.start();
await this.secrets.start();
await this.clients.start();
await this.topics.start();
};
}

View File

@@ -1,51 +1,43 @@
import { KubeConfig } from '@kubernetes/client-node';
import { K8sResources } from './k8s.resources.ts';
import { createCrd } from './k8s.crd.ts';
import { K8sCrds } from './k8s.crd.ts';
import { k8sBackboneClientSchema, k8sBackboneTopicSchema } from './k8s.schemas.ts';
import { K8sClients } from './k8s.clients.ts';
import { API_VERSION } from '#root/utils/consts.ts';
import type { Services } from '#root/utils/services.ts';
class K8sService {
#config: KubeConfig;
#resources: K8sResources;
#clients: K8sClients;
#services: Services;
constructor() {
this.#config = new KubeConfig();
this.#config.loadFromDefault();
this.#resources = new K8sResources(this.#config);
this.#clients = new K8sClients({
config: this.#config,
resources: this.resources,
});
constructor(services: Services) {
this.#services = services;
}
public get resources() {
return this.#resources;
return this.#services.get(K8sResources);
}
public get clients() {
return this.#clients;
return this.#services.get(K8sClients);
}
public setup = async () => {
await createCrd({
config: this.#config,
const crds = this.#services.get(K8sCrds);
await crds.install({
apiVersion: API_VERSION,
kind: 'Client',
scope: 'Namespaced',
spec: k8sBackboneClientSchema,
});
await createCrd({
config: this.#config,
await crds.install({
apiVersion: API_VERSION,
kind: 'Topic',
scope: 'Namespaced',
spec: k8sBackboneTopicSchema,
});
await this.#resources.start();
await this.resources.start();
};
}

View File

@@ -16,8 +16,9 @@ import { createWebSocketStream } from 'ws';
import { Session } from '../access/access.session.ts';
import { api } from '../api/api.ts';
import type { AccessHandler } from '#root/access/access.handler.ts';
import type { TopicsHandler } from '#root/topics/topics.handler.ts';
import { AccessHandler } from '#root/access/access.handler.ts';
import { TopicsHandler } from '#root/topics/topics.handler.ts';
import type { Services } from '#root/utils/services.ts';
type Aedes = ReturnType<typeof aedes.createBroker>;
@@ -30,23 +31,18 @@ declare module 'aedes' {
const packetMetaSymbol = Symbol('packetMeta');
type MqttServerOptions = {
accessHandler: AccessHandler;
topicsHandler: TopicsHandler;
};
class AuthError extends Error {
public readonly returnCode = 4;
}
class MqttServer {
#options: MqttServerOptions;
#services: Services;
#server: Aedes;
#http?: Promise<FastifyInstance>;
#tcp?: tcp.Server;
constructor(options: MqttServerOptions) {
this.#options = options;
constructor(services: Services) {
this.#services = services;
this.#server = aedes.createBroker({
authenticate: this.#authenticate,
authorizePublish: this.#authorizePublish,
@@ -61,7 +57,7 @@ class MqttServer {
if (!username || !password) {
throw new Error('unauthorized');
}
const { accessHandler } = this.#options;
const accessHandler = this.#services.get(AccessHandler);
const auth = await accessHandler.validate(username, password.toString('utf8'));
client.session = new Session(auth);
callback(null, true);
@@ -71,7 +67,7 @@ class MqttServer {
};
#authorizePublish: AuthorizePublishHandler = (client, packet, callback) => {
const { topicsHandler } = this.#options;
const topicsHandler = this.#services.get(TopicsHandler);
(packet as ExplicitAny)[packetMetaSymbol] = {
foo: 'bar',
};

View File

@@ -1,21 +1,11 @@
import { AccessHandler } from './access/access.handler.ts';
import { K8sService } from './k8s/k8s.ts';
import { MqttServer } from './server/server.ts';
import { TopicsHandler } from './topics/topics.handler.ts';
import { Backbone } from './backbone.ts';
const accessHandler = new AccessHandler();
const topicsHandler = new TopicsHandler();
const backbone = new Backbone();
await backbone.setupK8sOperator();
const k8s = new K8sService();
await k8s.setup();
accessHandler.register('k8s', k8s.clients);
const server = new MqttServer({
accessHandler,
topicsHandler,
});
const tcp = server.getTcpServer();
const tcp = backbone.server.getTcpServer();
tcp.listen(1883);
const http = await server.getHttpServer();
const http = await backbone.server.getHttpServer();
http.listen({ port: 8883 });
console.log('started');

51
src/utils/services.ts Normal file
View File

@@ -0,0 +1,51 @@
const destroy = Symbol('destroy');
const instanceKey = Symbol('instances');
type ServiceDependency<T> = new (services: Services) => T & {
[destroy]?: () => Promise<void> | void;
};
class Services {
[instanceKey]: Map<ServiceDependency<unknown>, unknown>;
constructor() {
this[instanceKey] = new Map();
}
public get = <T>(service: ServiceDependency<T>) => {
if (!this[instanceKey].has(service)) {
this[instanceKey].set(service, new service(this));
}
const instance = this[instanceKey].get(service);
if (!instance) {
throw new Error('Could not generate instance');
}
return instance as T;
};
public set = <T>(service: ServiceDependency<T>, instance: Partial<T>) => {
this[instanceKey].set(service, instance);
};
public clone = () => {
const services = new Services();
services[instanceKey] = Object.fromEntries(this[instanceKey].entries());
};
public destroy = async () => {
await Promise.all(
this[instanceKey].values().map(async (instance) => {
if (
typeof instance === 'object' &&
instance &&
destroy in instance &&
typeof instance[destroy] === 'function'
) {
await instance[destroy]();
}
}),
);
};
}
export { Services, destroy };

View File

@@ -8,6 +8,7 @@ import { MqttServer } from '#root/server/server.ts';
import type { TopicDefinition } from '#root/topics/topcis.schemas.ts';
import { TopicsHandler } from '#root/topics/topics.handler.ts';
import { TopicsStore } from '#root/topics/topics.store.ts';
import { Backbone } from '#root/backbone.ts';
type CreateSocketOptions = {
port: number;
@@ -30,17 +31,16 @@ type WorldOptions = {
const createWorld = async (options: WorldOptions) => {
const { topics = [] } = options;
const backbone = new Backbone();
const secret = 'test';
const accessHandler = new AccessHandler();
const accessTokens = new AccessTokens({
secret,
});
accessHandler.register('token', accessTokens);
const topicsHandler = new TopicsHandler();
backbone.accessHandler.register('token', accessTokens);
const topicsStore = new TopicsStore();
topicsStore.register(...topics);
topicsHandler.register(topicsStore);
const server = new MqttServer({ topicsHandler, accessHandler });
backbone.topicsHandler.register(topicsStore);
const server = backbone.server;
const fastify = await server.getHttpServer();
const port = await getPort();
await fastify.listen({ port });