fix: PXE boot debugging — bisect root cause, syslog logging, serial console #3

Merged
michal merged 31 commits from wip/ks-debugging into main 2026-03-29 00:50:05 +00:00
17 changed files with 1162 additions and 34 deletions
Showing only changes of commit 44f1ebb843 - Show all commits

434
bastion/pnpm-lock.yaml generated
View File

@@ -13,16 +13,16 @@ importers:
version: 22.19.15 version: 22.19.15
'@typescript-eslint/eslint-plugin': '@typescript-eslint/eslint-plugin':
specifier: ^8.57.1 specifier: ^8.57.1
version: 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3) version: 8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': '@typescript-eslint/parser':
specifier: ^8.57.1 specifier: ^8.57.1
version: 8.57.1(eslint@10.0.3)(typescript@5.9.3) version: 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
eslint: eslint:
specifier: ^10.0.3 specifier: ^10.0.3
version: 10.0.3 version: 10.0.3(jiti@2.6.1)
eslint-config-prettier: eslint-config-prettier:
specifier: ^10.1.8 specifier: ^10.1.8
version: 10.1.8(eslint@10.0.3) version: 10.1.8(eslint@10.0.3(jiti@2.6.1))
rimraf: rimraf:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.1.3 version: 6.1.3
@@ -34,7 +34,7 @@ importers:
version: 5.9.3 version: 5.9.3
vitest: vitest:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.2.4(@types/node@22.19.15)(tsx@4.21.0) version: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)
src/bastion: src/bastion:
dependencies: dependencies:
@@ -74,6 +74,40 @@ importers:
specifier: ^22.10.0 specifier: ^22.10.0
version: 22.19.15 version: 22.19.15
src/labd:
dependencies:
'@fastify/websocket':
specifier: ^11.0.2
version: 11.2.0
'@lab/shared':
specifier: workspace:*
version: link:../shared
'@prisma/client':
specifier: ^6.9.0
version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)
fastify:
specifier: ^5.3.3
version: 5.8.2
winston:
specifier: ^3.17.0
version: 3.19.0
devDependencies:
'@types/node':
specifier: ^22.14.1
version: 22.19.15
prisma:
specifier: ^6.9.0
version: 6.19.2(typescript@5.9.3)
rimraf:
specifier: ^6.1.3
version: 6.1.3
tsx:
specifier: ^4.21.0
version: 4.21.0
typescript:
specifier: ^5.9.3
version: 5.9.3
src/shared: {} src/shared: {}
packages: packages:
@@ -298,6 +332,9 @@ packages:
'@fastify/static@8.3.0': '@fastify/static@8.3.0':
resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==}
'@fastify/websocket@11.2.0':
resolution: {integrity: sha512-3HrDPbAG1CzUCqnslgJxppvzaAZffieOVbLp1DAy1huCSynUWPifSvfdEDUR8HlJLp3sp1A36uOM2tJogADS8w==}
'@humanfs/core@0.19.1': '@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
@@ -328,6 +365,36 @@ packages:
'@pinojs/redact@0.4.0': '@pinojs/redact@0.4.0':
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
'@prisma/client@6.19.2':
resolution: {integrity: sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==}
engines: {node: '>=18.18'}
peerDependencies:
prisma: '*'
typescript: '>=5.1.0'
peerDependenciesMeta:
prisma:
optional: true
typescript:
optional: true
'@prisma/config@6.19.2':
resolution: {integrity: sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==}
'@prisma/debug@6.19.2':
resolution: {integrity: sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==}
'@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7':
resolution: {integrity: sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==}
'@prisma/engines@6.19.2':
resolution: {integrity: sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==}
'@prisma/fetch-engine@6.19.2':
resolution: {integrity: sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==}
'@prisma/get-platform@6.19.2':
resolution: {integrity: sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==}
'@rollup/rollup-android-arm-eabi@4.59.0': '@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
cpu: [arm] cpu: [arm]
@@ -463,6 +530,9 @@ packages:
'@so-ric/colorspace@1.1.6': '@so-ric/colorspace@1.1.6':
resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==} resolution: {integrity: sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@types/chai@5.2.3': '@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -621,6 +691,14 @@ packages:
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
engines: {node: 18 || 20 || >=22} engines: {node: 18 || 20 || >=22}
c12@3.1.0:
resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==}
peerDependencies:
magicast: ^0.3.5
peerDependenciesMeta:
magicast:
optional: true
cac@6.7.14: cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -633,6 +711,16 @@ packages:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'} engines: {node: '>= 16'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
citty@0.1.6:
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
citty@0.2.1:
resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==}
color-convert@3.1.3: color-convert@3.1.3:
resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==} resolution: {integrity: sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==}
engines: {node: '>=14.6'} engines: {node: '>=14.6'}
@@ -653,6 +741,13 @@ packages:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'} engines: {node: '>=18'}
confbox@0.2.4:
resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
consola@3.4.2:
resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==}
engines: {node: ^14.18.0 || >=16.10.0}
content-disposition@0.5.4: content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@@ -681,6 +776,13 @@ packages:
deep-is@0.1.4: deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
deepmerge-ts@7.1.5:
resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==}
engines: {node: '>=16.0.0'}
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
depd@2.0.0: depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -689,9 +791,29 @@ packages:
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
destr@2.0.5:
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
dotenv@16.6.1:
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
engines: {node: '>=12'}
duplexify@4.1.3:
resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
effect@3.18.4:
resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==}
empathic@2.0.0:
resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
engines: {node: '>=14'}
enabled@2.0.0: enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
es-module-lexer@1.7.0: es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
@@ -766,6 +888,13 @@ packages:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
fast-check@3.23.2:
resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==}
engines: {node: '>=8.0.0'}
fast-decode-uri-component@1.0.1: fast-decode-uri-component@1.0.1:
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
@@ -850,6 +979,10 @@ packages:
get-tsconfig@4.13.6: get-tsconfig@4.13.6:
resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==}
giget@2.0.0:
resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==}
hasBin: true
glob-parent@6.0.2: glob-parent@6.0.2:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -922,6 +1055,10 @@ packages:
resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
js-tokens@9.0.1: js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
@@ -995,14 +1132,28 @@ packages:
natural-compare@1.4.0: natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-fetch-native@1.6.7:
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
npm-run-path@6.0.0: npm-run-path@6.0.0:
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
engines: {node: '>=18'} engines: {node: '>=18'}
nypm@0.6.5:
resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==}
engines: {node: '>=18'}
hasBin: true
ohash@2.0.11:
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
on-exit-leak-free@2.1.2: on-exit-leak-free@2.1.2:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
one-time@1.0.0: one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
@@ -1048,6 +1199,9 @@ packages:
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
engines: {node: '>= 14.16'} engines: {node: '>= 14.16'}
perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1065,6 +1219,9 @@ packages:
resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==}
hasBin: true hasBin: true
pkg-types@2.3.0:
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
postcss@8.5.8: postcss@8.5.8:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@@ -1077,6 +1234,16 @@ packages:
resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
prisma@6.19.2:
resolution: {integrity: sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==}
engines: {node: '>=18.18'}
hasBin: true
peerDependencies:
typescript: '>=5.1.0'
peerDependenciesMeta:
typescript:
optional: true
process-warning@4.0.1: process-warning@4.0.1:
resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==}
@@ -1087,13 +1254,23 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
pure-rand@6.1.0:
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
quick-format-unescaped@4.0.4: quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
rc9@2.1.2:
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
readable-stream@3.6.2: readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
real-require@0.2.0: real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'} engines: {node: '>= 12.13.0'}
@@ -1190,6 +1367,9 @@ packages:
std-env@3.10.0: std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
stream-shift@1.0.3:
resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
string_decoder@1.3.0: string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@@ -1213,6 +1393,10 @@ packages:
tinyexec@0.3.2: tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyexec@1.0.4:
resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==}
engines: {node: '>=18'}
tinyglobby@0.2.15: tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -1369,6 +1553,21 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
ws@8.19.0:
resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: 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'}
@@ -1465,9 +1664,9 @@ snapshots:
'@esbuild/win32-x64@0.27.4': '@esbuild/win32-x64@0.27.4':
optional: true optional: true
'@eslint-community/eslint-utils@4.9.1(eslint@10.0.3)': '@eslint-community/eslint-utils@4.9.1(eslint@10.0.3(jiti@2.6.1))':
dependencies: dependencies:
eslint: 10.0.3 eslint: 10.0.3(jiti@2.6.1)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {} '@eslint-community/regexpp@4.12.2': {}
@@ -1537,6 +1736,15 @@ snapshots:
fastq: 1.20.1 fastq: 1.20.1
glob: 11.1.0 glob: 11.1.0
'@fastify/websocket@11.2.0':
dependencies:
duplexify: 4.1.3
fastify-plugin: 5.1.0
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@humanfs/core@0.19.1': {} '@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7': '@humanfs/node@0.16.7':
@@ -1556,6 +1764,41 @@ snapshots:
'@pinojs/redact@0.4.0': {} '@pinojs/redact@0.4.0': {}
'@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)':
optionalDependencies:
prisma: 6.19.2(typescript@5.9.3)
typescript: 5.9.3
'@prisma/config@6.19.2':
dependencies:
c12: 3.1.0
deepmerge-ts: 7.1.5
effect: 3.18.4
empathic: 2.0.0
transitivePeerDependencies:
- magicast
'@prisma/debug@6.19.2': {}
'@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': {}
'@prisma/engines@6.19.2':
dependencies:
'@prisma/debug': 6.19.2
'@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7
'@prisma/fetch-engine': 6.19.2
'@prisma/get-platform': 6.19.2
'@prisma/fetch-engine@6.19.2':
dependencies:
'@prisma/debug': 6.19.2
'@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7
'@prisma/get-platform': 6.19.2
'@prisma/get-platform@6.19.2':
dependencies:
'@prisma/debug': 6.19.2
'@rollup/rollup-android-arm-eabi@4.59.0': '@rollup/rollup-android-arm-eabi@4.59.0':
optional: true optional: true
@@ -1640,6 +1883,8 @@ snapshots:
color: 5.0.3 color: 5.0.3
text-hex: 1.0.0 text-hex: 1.0.0
'@standard-schema/spec@1.1.0': {}
'@types/chai@5.2.3': '@types/chai@5.2.3':
dependencies: dependencies:
'@types/deep-eql': 4.0.2 '@types/deep-eql': 4.0.2
@@ -1659,15 +1904,15 @@ snapshots:
'@types/triple-beam@1.3.5': {} '@types/triple-beam@1.3.5': {}
'@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3)(typescript@5.9.3))(eslint@10.0.3)(typescript@5.9.3)': '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.57.1(eslint@10.0.3)(typescript@5.9.3) '@typescript-eslint/parser': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.57.1 '@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/type-utils': 8.57.1(eslint@10.0.3)(typescript@5.9.3) '@typescript-eslint/type-utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.57.1(eslint@10.0.3)(typescript@5.9.3) '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.57.1 '@typescript-eslint/visitor-keys': 8.57.1
eslint: 10.0.3 eslint: 10.0.3(jiti@2.6.1)
ignore: 7.0.5 ignore: 7.0.5
natural-compare: 1.4.0 natural-compare: 1.4.0
ts-api-utils: 2.4.0(typescript@5.9.3) ts-api-utils: 2.4.0(typescript@5.9.3)
@@ -1675,14 +1920,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/parser@8.57.1(eslint@10.0.3)(typescript@5.9.3)': '@typescript-eslint/parser@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/scope-manager': 8.57.1 '@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/types': 8.57.1 '@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.57.1 '@typescript-eslint/visitor-keys': 8.57.1
debug: 4.4.3 debug: 4.4.3
eslint: 10.0.3 eslint: 10.0.3(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -1705,13 +1950,13 @@ snapshots:
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
'@typescript-eslint/type-utils@8.57.1(eslint@10.0.3)(typescript@5.9.3)': '@typescript-eslint/type-utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/types': 8.57.1 '@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
'@typescript-eslint/utils': 8.57.1(eslint@10.0.3)(typescript@5.9.3) '@typescript-eslint/utils': 8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.3 debug: 4.4.3
eslint: 10.0.3 eslint: 10.0.3(jiti@2.6.1)
ts-api-utils: 2.4.0(typescript@5.9.3) ts-api-utils: 2.4.0(typescript@5.9.3)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
@@ -1734,13 +1979,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.57.1(eslint@10.0.3)(typescript@5.9.3)': '@typescript-eslint/utils@8.57.1(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3) '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1))
'@typescript-eslint/scope-manager': 8.57.1 '@typescript-eslint/scope-manager': 8.57.1
'@typescript-eslint/types': 8.57.1 '@typescript-eslint/types': 8.57.1
'@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3)
eslint: 10.0.3 eslint: 10.0.3(jiti@2.6.1)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -1758,13 +2003,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.3.1(@types/node@22.19.15)(tsx@4.21.0))': '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0))':
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.21 magic-string: 0.30.21
optionalDependencies: optionalDependencies:
vite: 7.3.1(@types/node@22.19.15)(tsx@4.21.0) vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)
'@vitest/pretty-format@3.2.4': '@vitest/pretty-format@3.2.4':
dependencies: dependencies:
@@ -1835,6 +2080,21 @@ snapshots:
dependencies: dependencies:
balanced-match: 4.0.4 balanced-match: 4.0.4
c12@3.1.0:
dependencies:
chokidar: 4.0.3
confbox: 0.2.4
defu: 6.1.4
dotenv: 16.6.1
exsolve: 1.0.8
giget: 2.0.0
jiti: 2.6.1
ohash: 2.0.11
pathe: 2.0.3
perfect-debounce: 1.0.0
pkg-types: 2.3.0
rc9: 2.1.2
cac@6.7.14: {} cac@6.7.14: {}
chai@5.3.3: chai@5.3.3:
@@ -1847,6 +2107,16 @@ snapshots:
check-error@2.1.3: {} check-error@2.1.3: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
citty@0.1.6:
dependencies:
consola: 3.4.2
citty@0.2.1: {}
color-convert@3.1.3: color-convert@3.1.3:
dependencies: dependencies:
color-name: 2.1.0 color-name: 2.1.0
@@ -1864,6 +2134,10 @@ snapshots:
commander@13.1.0: {} commander@13.1.0: {}
confbox@0.2.4: {}
consola@3.4.2: {}
content-disposition@0.5.4: content-disposition@0.5.4:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
@@ -1884,12 +2158,38 @@ snapshots:
deep-is@0.1.4: {} deep-is@0.1.4: {}
deepmerge-ts@7.1.5: {}
defu@6.1.4: {}
depd@2.0.0: {} depd@2.0.0: {}
dequal@2.0.3: {} dequal@2.0.3: {}
destr@2.0.5: {}
dotenv@16.6.1: {}
duplexify@4.1.3:
dependencies:
end-of-stream: 1.4.5
inherits: 2.0.4
readable-stream: 3.6.2
stream-shift: 1.0.3
effect@3.18.4:
dependencies:
'@standard-schema/spec': 1.1.0
fast-check: 3.23.2
empathic@2.0.0: {}
enabled@2.0.0: {} enabled@2.0.0: {}
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
es-module-lexer@1.7.0: {} es-module-lexer@1.7.0: {}
esbuild@0.27.4: esbuild@0.27.4:
@@ -1925,9 +2225,9 @@ snapshots:
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
eslint-config-prettier@10.1.8(eslint@10.0.3): eslint-config-prettier@10.1.8(eslint@10.0.3(jiti@2.6.1)):
dependencies: dependencies:
eslint: 10.0.3 eslint: 10.0.3(jiti@2.6.1)
eslint-scope@9.1.2: eslint-scope@9.1.2:
dependencies: dependencies:
@@ -1940,9 +2240,9 @@ snapshots:
eslint-visitor-keys@5.0.1: {} eslint-visitor-keys@5.0.1: {}
eslint@10.0.3: eslint@10.0.3(jiti@2.6.1):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3) '@eslint-community/eslint-utils': 4.9.1(eslint@10.0.3(jiti@2.6.1))
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.23.3 '@eslint/config-array': 0.23.3
'@eslint/config-helpers': 0.5.3 '@eslint/config-helpers': 0.5.3
@@ -1972,6 +2272,8 @@ snapshots:
minimatch: 10.2.4 minimatch: 10.2.4
natural-compare: 1.4.0 natural-compare: 1.4.0
optionator: 0.9.4 optionator: 0.9.4
optionalDependencies:
jiti: 2.6.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -2014,6 +2316,12 @@ snapshots:
expect-type@1.3.0: {} expect-type@1.3.0: {}
exsolve@1.0.8: {}
fast-check@3.23.2:
dependencies:
pure-rand: 6.1.0
fast-decode-uri-component@1.0.1: {} fast-decode-uri-component@1.0.1: {}
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
@@ -2112,6 +2420,15 @@ snapshots:
dependencies: dependencies:
resolve-pkg-maps: 1.0.0 resolve-pkg-maps: 1.0.0
giget@2.0.0:
dependencies:
citty: 0.1.6
consola: 3.4.2
defu: 6.1.4
node-fetch-native: 1.6.7
nypm: 0.6.5
pathe: 2.0.3
glob-parent@6.0.2: glob-parent@6.0.2:
dependencies: dependencies:
is-glob: 4.0.3 is-glob: 4.0.3
@@ -2171,6 +2488,8 @@ snapshots:
dependencies: dependencies:
'@isaacs/cliui': 9.0.0 '@isaacs/cliui': 9.0.0
jiti@2.6.1: {}
js-tokens@9.0.1: {} js-tokens@9.0.1: {}
json-buffer@3.0.1: {} json-buffer@3.0.1: {}
@@ -2237,13 +2556,27 @@ snapshots:
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
node-fetch-native@1.6.7: {}
npm-run-path@6.0.0: npm-run-path@6.0.0:
dependencies: dependencies:
path-key: 4.0.0 path-key: 4.0.0
unicorn-magic: 0.3.0 unicorn-magic: 0.3.0
nypm@0.6.5:
dependencies:
citty: 0.2.1
pathe: 2.0.3
tinyexec: 1.0.4
ohash@2.0.11: {}
on-exit-leak-free@2.1.2: {} on-exit-leak-free@2.1.2: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
one-time@1.0.0: one-time@1.0.0:
dependencies: dependencies:
fn.name: 1.1.0 fn.name: 1.1.0
@@ -2284,6 +2617,8 @@ snapshots:
pathval@2.0.1: {} pathval@2.0.1: {}
perfect-debounce@1.0.0: {}
picocolors@1.1.1: {} picocolors@1.1.1: {}
picomatch@4.0.3: {} picomatch@4.0.3: {}
@@ -2308,6 +2643,12 @@ snapshots:
sonic-boom: 4.2.1 sonic-boom: 4.2.1
thread-stream: 4.0.0 thread-stream: 4.0.0
pkg-types@2.3.0:
dependencies:
confbox: 0.2.4
exsolve: 1.0.8
pathe: 2.0.3
postcss@8.5.8: postcss@8.5.8:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
@@ -2320,20 +2661,38 @@ snapshots:
dependencies: dependencies:
parse-ms: 4.0.0 parse-ms: 4.0.0
prisma@6.19.2(typescript@5.9.3):
dependencies:
'@prisma/config': 6.19.2
'@prisma/engines': 6.19.2
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
- magicast
process-warning@4.0.1: {} process-warning@4.0.1: {}
process-warning@5.0.0: {} process-warning@5.0.0: {}
punycode@2.3.1: {} punycode@2.3.1: {}
pure-rand@6.1.0: {}
quick-format-unescaped@4.0.4: {} quick-format-unescaped@4.0.4: {}
rc9@2.1.2:
dependencies:
defu: 6.1.4
destr: 2.0.5
readable-stream@3.6.2: readable-stream@3.6.2:
dependencies: dependencies:
inherits: 2.0.4 inherits: 2.0.4
string_decoder: 1.3.0 string_decoder: 1.3.0
util-deprecate: 1.0.2 util-deprecate: 1.0.2
readdirp@4.1.2: {}
real-require@0.2.0: {} real-require@0.2.0: {}
require-from-string@2.0.2: {} require-from-string@2.0.2: {}
@@ -2424,6 +2783,8 @@ snapshots:
std-env@3.10.0: {} std-env@3.10.0: {}
stream-shift@1.0.3: {}
string_decoder@1.3.0: string_decoder@1.3.0:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
@@ -2444,6 +2805,8 @@ snapshots:
tinyexec@0.3.2: {} tinyexec@0.3.2: {}
tinyexec@1.0.4: {}
tinyglobby@0.2.15: tinyglobby@0.2.15:
dependencies: dependencies:
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@@ -2488,13 +2851,13 @@ snapshots:
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
vite-node@3.2.4(@types/node@22.19.15)(tsx@4.21.0): vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0):
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.3.1(@types/node@22.19.15)(tsx@4.21.0) vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- jiti - jiti
@@ -2509,7 +2872,7 @@ snapshots:
- tsx - tsx
- yaml - yaml
vite@7.3.1(@types/node@22.19.15)(tsx@4.21.0): vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0):
dependencies: dependencies:
esbuild: 0.27.4 esbuild: 0.27.4
fdir: 6.5.0(picomatch@4.0.3) fdir: 6.5.0(picomatch@4.0.3)
@@ -2520,13 +2883,14 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/node': 22.19.15 '@types/node': 22.19.15
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.6.1
tsx: 4.21.0 tsx: 4.21.0
vitest@3.2.4(@types/node@22.19.15)(tsx@4.21.0): vitest@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0):
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/expect': 3.2.4 '@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)(tsx@4.21.0)) '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0))
'@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
@@ -2544,8 +2908,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.3.1(@types/node@22.19.15)(tsx@4.21.0) vite: 7.3.1(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)
vite-node: 3.2.4(@types/node@22.19.15)(tsx@4.21.0) vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(tsx@4.21.0)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 22.19.15 '@types/node': 22.19.15
@@ -2594,6 +2958,10 @@ snapshots:
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
wrappy@1.0.2: {}
ws@8.19.0: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
yoctocolors@2.1.2: {} yoctocolors@2.1.2: {}

View File

@@ -0,0 +1,36 @@
{
"name": "@lab/labd",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./dist/main.js",
"types": "./dist/main.d.ts",
"exports": {
".": {
"import": "./dist/main.js",
"types": "./dist/main.d.ts"
}
},
"scripts": {
"build": "tsc --build",
"clean": "rimraf dist",
"dev": "tsx src/main.ts",
"db:push": "prisma db push",
"db:migrate": "prisma migrate dev",
"db:generate": "prisma generate"
},
"dependencies": {
"@lab/shared": "workspace:*",
"@prisma/client": "^6.9.0",
"fastify": "^5.3.3",
"@fastify/websocket": "^11.0.2",
"winston": "^3.17.0"
},
"devDependencies": {
"@types/node": "^22.14.1",
"prisma": "^6.9.0",
"rimraf": "^6.1.3",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,145 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "cockroachdb"
url = env("DATABASE_URL")
}
model Server {
id String @id @default(uuid())
hostname String @unique
mac String? @unique
cloud String @default("baremetal")
environment String @default("default")
role String @default("worker")
labels Json @default("{}")
ip String?
agentVersion String?
status String @default("unknown") // unknown, online, offline, provisioning
lastHeartbeat DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
agent Agent?
auditLogs AuditLog[]
}
model Agent {
id String @id @default(uuid())
serverId String @unique
server Server @relation(fields: [serverId], references: [id], onDelete: Cascade)
certificatePem String?
enrolledAt DateTime @default(now())
lastSeen DateTime?
@@index([serverId])
}
model User {
id String @id @default(uuid())
username String @unique
displayName String?
certFingerprint String? @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
roleBindings UserRole[]
auditLogs AuditLog[]
}
model Role {
id String @id @default(uuid())
name String @unique
description String?
createdAt DateTime @default(now())
permissions Permission[]
userBindings UserRole[]
}
model Permission {
id String @id @default(uuid())
roleId String
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
type String @default("allow") // allow or deny
action String // read, exec, apply, destroy, manage, admin, kubectl, *
cloud String @default("*")
environment String @default("*")
server String @default("*")
@@index([roleId])
}
model UserRole {
id String @id @default(uuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
roleId String
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
@@unique([userId, roleId])
@@index([userId])
@@index([roleId])
}
model JoinToken {
id String @id @default(uuid())
token String @unique
type String @default("one-time") // one-time or reusable
label String?
usedBy String? // server hostname that used it
usedAt DateTime?
revokedAt DateTime?
createdAt DateTime @default(now())
expiresAt DateTime?
}
model AuditLog {
id String @id @default(uuid())
userId String?
user User? @relation(fields: [userId], references: [id])
serverId String?
server Server? @relation(fields: [serverId], references: [id])
sessionId String?
action String // exec, kubectl, apply, login, rbac-denied, etc.
resourceType String? // server, cluster, role, app, etc.
resourceName String?
args String? // sanitized command args
result String @default("success") // success, denied, error
durationMs Int?
sourceIp String?
timestamp DateTime @default(now())
@@index([userId])
@@index([serverId])
@@index([sessionId])
@@index([timestamp])
@@index([action])
}
model PulumiRun {
id String @id @default(uuid())
userId String
stackName String
action String // up, preview, destroy
status String @default("pending") // pending, running, succeeded, failed
output String?
startedAt DateTime @default(now())
completedAt DateTime?
@@index([userId])
@@index([stackName])
}
model Cluster {
id String @id @default(uuid())
name String @unique
cloud String @default("baremetal")
environment String @default("default")
kubeconfigEnc String? // encrypted kubeconfig
labels Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -0,0 +1,19 @@
// Configuration from environment variables with sensible defaults.
export interface LabdConfig {
port: number;
host: string;
databaseUrl: string;
caDir: string;
logLevel: string;
}
export function loadConfig(overrides: Partial<LabdConfig> = {}): LabdConfig {
return {
port: overrides.port ?? parseInt(process.env["LABD_PORT"] ?? "3100", 10),
host: overrides.host ?? process.env["LABD_HOST"] ?? "0.0.0.0",
databaseUrl: overrides.databaseUrl ?? process.env["DATABASE_URL"] ?? "",
caDir: overrides.caDir ?? process.env["CA_DIR"] ?? "/etc/labd/ca",
logLevel: overrides.logLevel ?? process.env["LABD_LOG_LEVEL"] ?? "info",
};
}

View File

@@ -0,0 +1,91 @@
// Entry point for the lab master daemon (labd).
// Initializes Prisma, starts Fastify with WebSocket support, registers routes.
import { loadConfig } from "./config.js";
import { createApp } from "./server.js";
import { logger } from "./services/logger.js";
async function main(): Promise<void> {
const config = loadConfig();
// Initialize Prisma client (wrapped in try/catch for when DB isn't available)
let db;
try {
const { PrismaClient } = await import("@prisma/client");
const prisma = new PrismaClient({
datasources: config.databaseUrl
? { db: { url: config.databaseUrl } }
: undefined,
});
await prisma.$connect();
logger.info("Database connected");
db = prisma;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.warn(`Database not available: ${message}`);
logger.warn("Running without database -- some features will be unavailable");
// Create a stub db client that returns errors for all operations
db = {
$queryRaw: async () => {
throw new Error("Database not connected");
},
server: {
findMany: async () => {
throw new Error("Database not connected");
},
findUnique: async () => {
throw new Error("Database not connected");
},
},
joinToken: {
findUnique: async () => {
throw new Error("Database not connected");
},
findMany: async () => {
throw new Error("Database not connected");
},
create: async () => {
throw new Error("Database not connected");
},
update: async () => {
throw new Error("Database not connected");
},
},
};
}
// Create Fastify app
const { app } = createApp(config, db);
// Start server
try {
await app.listen({ port: config.port, host: config.host });
logger.info(`labd listening on ${config.host}:${config.port}`);
} catch (err) {
logger.error(`Failed to start server: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
}
// Graceful shutdown
const shutdown = async (): Promise<void> => {
logger.info("Shutting down...");
await app.close();
if (db !== null && "$disconnect" in db) {
await (db as { $disconnect: () => Promise<void> }).$disconnect();
}
logger.info("Goodbye");
process.exit(0);
};
process.on("SIGINT", () => void shutdown());
process.on("SIGTERM", () => void shutdown());
// Keep process alive
await new Promise(() => {});
}
main().catch((err) => {
console.error("Failed to start labd:", err);
process.exit(1);
});

View File

@@ -0,0 +1,33 @@
// Placeholder mTLS auth middleware.
// Extracts client certificate info from the request and resolves user/agent identity.
import type { FastifyRequest, FastifyReply } from "fastify";
import { logger } from "../services/logger.js";
declare module "fastify" {
interface FastifyRequest {
clientCertFingerprint?: string;
authenticatedUser?: string;
authenticatedAgent?: string;
}
}
export function createMtlsAuthMiddleware(): (
request: FastifyRequest,
reply: FastifyReply,
) => Promise<void> {
return async function mtlsAuthMiddleware(
request: FastifyRequest,
_reply: FastifyReply,
): Promise<void> {
// TODO: Extract client certificate from TLS connection
// const cert = (request.raw.socket as TLSSocket).getPeerCertificate();
// For now, this is a no-op placeholder
const certHeader = request.headers["x-client-cert-fingerprint"];
if (typeof certHeader === "string" && certHeader.length > 0) {
request.clientCertFingerprint = certHeader;
logger.info(`mTLS: client cert fingerprint=${certHeader.slice(0, 16)}...`);
}
};
}

View File

@@ -0,0 +1,163 @@
// Authentication and token management routes.
// POST /api/auth/enroll — agent enrollment (token + CSR -> signed cert)
// POST /api/tokens — create join token
// GET /api/tokens — list tokens
// DELETE /api/tokens/:id — revoke token
import { randomBytes } from "node:crypto";
import type { FastifyInstance } from "fastify";
import type { DbClient } from "../server.js";
import { logger } from "../services/logger.js";
export function registerAuthRoutes(app: FastifyInstance, db: DbClient): void {
// Agent enrollment: validate join token, accept CSR, return signed cert
app.post<{
Body: {
token?: string;
hostname?: string;
csr?: string;
};
}>("/api/auth/enroll", async (request, reply) => {
const { token, hostname, csr } = request.body ?? {};
if (token === undefined || token === "") {
return reply.code(400).send({ error: "token is required" });
}
if (hostname === undefined || hostname === "") {
return reply.code(400).send({ error: "hostname is required" });
}
try {
// Validate token
const joinToken = await db.joinToken.findUnique({
where: { token },
}) as { id: string; type: string; usedBy: string | null; revokedAt: Date | null; expiresAt: Date | null } | null;
if (joinToken === null) {
return reply.code(401).send({ error: "Invalid join token" });
}
if (joinToken.revokedAt !== null) {
return reply.code(401).send({ error: "Token has been revoked" });
}
if (joinToken.expiresAt !== null && joinToken.expiresAt < new Date()) {
return reply.code(401).send({ error: "Token has expired" });
}
if (joinToken.type === "one-time" && joinToken.usedBy !== null) {
return reply.code(401).send({ error: "Token has already been used" });
}
// Mark token as used
await db.joinToken.update({
where: { id: joinToken.id },
data: {
usedBy: hostname,
usedAt: new Date(),
},
});
logger.info(`AGENT ENROLLED: ${hostname} (token=${joinToken.id.slice(0, 8)}...)`);
// TODO: Sign CSR with CA and return certificate
// For now, return a placeholder acknowledging enrollment
return reply.send({
status: "enrolled",
hostname,
message: "Agent enrolled successfully",
certificatePem: null, // TODO: implement CA signing
csr: csr !== undefined ? "received" : "not provided",
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return reply.code(500).send({ error: "Enrollment failed", detail: message });
}
});
// Create a new join token
app.post<{
Body: {
type?: string;
label?: string;
expiresInHours?: number;
};
}>("/api/tokens", async (request, reply) => {
const { type, label, expiresInHours } = request.body ?? {};
const tokenType = type ?? "one-time";
if (tokenType !== "one-time" && tokenType !== "reusable") {
return reply.code(400).send({ error: "type must be 'one-time' or 'reusable'" });
}
const tokenValue = randomBytes(32).toString("hex");
const expiresAt = expiresInHours !== undefined
? new Date(Date.now() + expiresInHours * 60 * 60 * 1000)
: undefined;
try {
const created = await db.joinToken.create({
data: {
token: tokenValue,
type: tokenType,
label: label ?? null,
expiresAt: expiresAt ?? null,
},
});
logger.info(`TOKEN CREATED: ${(created as { id: string }).id} type=${tokenType} label=${label ?? "(none)"}`);
return reply.code(201).send({
id: (created as { id: string }).id,
token: tokenValue,
type: tokenType,
label: label ?? null,
expiresAt: expiresAt?.toISOString() ?? null,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return reply.code(500).send({ error: "Failed to create token", detail: message });
}
});
// List tokens
app.get("/api/tokens", async (_request, reply) => {
try {
const tokens = await db.joinToken.findMany({
orderBy: { createdAt: "desc" },
select: {
id: true,
type: true,
label: true,
usedBy: true,
usedAt: true,
revokedAt: true,
createdAt: true,
expiresAt: true,
// Intentionally omit token value for security
},
});
return reply.send(tokens);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return reply.code(500).send({ error: "Failed to list tokens", detail: message });
}
});
// Revoke a token
app.delete<{
Params: { id: string };
}>("/api/tokens/:id", async (request, reply) => {
const { id } = request.params;
try {
await db.joinToken.update({
where: { id },
data: { revokedAt: new Date() },
});
logger.info(`TOKEN REVOKED: ${id}`);
return reply.send({ status: "revoked", id });
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return reply.code(500).send({ error: "Failed to revoke token", detail: message });
}
});
}

View File

@@ -0,0 +1,28 @@
// Health check routes.
import type { FastifyInstance } from "fastify";
import type { DbClient } from "../server.js";
export function registerHealthRoutes(app: FastifyInstance, db: DbClient): void {
app.get("/healthz", async (_request, reply) => {
let dbOk = false;
try {
await db.$queryRaw`SELECT 1`;
dbOk = true;
} catch {
// DB not reachable
}
const status = dbOk ? "healthy" : "degraded";
const statusCode = dbOk ? 200 : 503;
return reply.code(statusCode).send({
status,
uptime: process.uptime(),
timestamp: new Date().toISOString(),
checks: {
database: dbOk ? "ok" : "error",
},
});
});
}

View File

@@ -0,0 +1,64 @@
// Server management routes.
// GET /api/servers — list servers with optional filters (cloud, environment, label)
// GET /api/servers/:id — get server details
import type { FastifyInstance } from "fastify";
import type { DbClient } from "../server.js";
export function registerServerRoutes(app: FastifyInstance, db: DbClient): void {
// List servers with optional filters
app.get<{
Querystring: {
cloud?: string;
environment?: string;
status?: string;
};
}>("/api/servers", async (request, reply) => {
const { cloud, environment, status } = request.query;
const where: Record<string, unknown> = {};
if (cloud !== undefined && cloud !== "") {
where["cloud"] = cloud;
}
if (environment !== undefined && environment !== "") {
where["environment"] = environment;
}
if (status !== undefined && status !== "") {
where["status"] = status;
}
try {
const servers = await db.server.findMany({
where: Object.keys(where).length > 0 ? where : undefined,
orderBy: { hostname: "asc" },
});
return reply.send(servers);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return reply.code(500).send({ error: "Failed to list servers", detail: message });
}
});
// Get server details by ID
app.get<{
Params: { id: string };
}>("/api/servers/:id", async (request, reply) => {
const { id } = request.params;
try {
const server = await db.server.findUnique({
where: { id },
include: { agent: true },
});
if (server === null) {
return reply.code(404).send({ error: "Server not found", id });
}
return reply.send(server);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return reply.code(500).send({ error: "Failed to get server", detail: message });
}
});
}

View File

@@ -0,0 +1,63 @@
// Fastify application setup with all routes registered.
import Fastify from "fastify";
import websocket from "@fastify/websocket";
import type { LabdConfig } from "./config.js";
import { logger } from "./services/logger.js";
import { registerHealthRoutes } from "./routes/health.js";
import { registerServerRoutes } from "./routes/servers.js";
import { registerAuthRoutes } from "./routes/auth.js";
export interface DbClient {
$queryRaw: (query: TemplateStringsArray) => Promise<unknown>;
server: {
findMany: (args?: unknown) => Promise<unknown[]>;
findUnique: (args: unknown) => Promise<unknown>;
};
joinToken: {
findUnique: (args: unknown) => Promise<unknown>;
findMany: (args?: unknown) => Promise<unknown[]>;
create: (args: unknown) => Promise<unknown>;
update: (args: unknown) => Promise<unknown>;
};
}
export function createApp(_config: LabdConfig, db: DbClient): {
app: ReturnType<typeof Fastify>;
} {
const app = Fastify({
logger: false, // We use winston instead
});
// Register WebSocket support
void app.register(websocket);
// Register route handlers
registerHealthRoutes(app, db);
registerServerRoutes(app, db);
registerAuthRoutes(app, db);
// WebSocket handler for agent connections
app.register(async (fastify) => {
fastify.get("/ws/agent", { websocket: true }, (socket, _request) => {
logger.info("Agent WebSocket connection established");
socket.on("message", (message: Buffer) => {
const data = message.toString();
logger.info(`Agent message: ${data}`);
// TODO: Handle agent heartbeat, command relay, etc.
});
socket.on("close", () => {
logger.info("Agent WebSocket connection closed");
});
});
});
// Log all requests
app.addHook("onRequest", async (request) => {
logger.info(`HTTP: ${request.ip} ${request.method} ${request.url}`);
});
return { app };
}

View File

@@ -0,0 +1,17 @@
// Winston logger instance shared across the labd application.
import winston from "winston";
export const logger = winston.createLogger({
level: process.env["LABD_LOG_LEVEL"] ?? "info",
format: winston.format.combine(
winston.format.timestamp({ format: "HH:mm:ss" }),
winston.format.printf(({ timestamp, level, message }) => {
const prefix = level === "error" ? "\x1b[31m[labd]\x1b[0m"
: level === "warn" ? "\x1b[33m[labd]\x1b[0m"
: "\x1b[36m[labd]\x1b[0m";
return `${prefix} ${timestamp as string} ${message as string}`;
}),
),
transports: [new winston.transports.Console()],
});

View File

@@ -0,0 +1,65 @@
import { describe, it, expect, vi } from "vitest";
import Fastify from "fastify";
import { registerHealthRoutes } from "../src/routes/health.js";
import type { DbClient } from "../src/server.js";
function createMockDb(overrides: Partial<DbClient> = {}): DbClient {
return {
$queryRaw: vi.fn().mockResolvedValue([{ "?column?": 1 }]),
server: {
findMany: vi.fn().mockResolvedValue([]),
findUnique: vi.fn().mockResolvedValue(null),
},
joinToken: {
findUnique: vi.fn().mockResolvedValue(null),
findMany: vi.fn().mockResolvedValue([]),
create: vi.fn().mockResolvedValue({ id: "test-id" }),
update: vi.fn().mockResolvedValue({}),
},
...overrides,
};
}
describe("Health endpoint", () => {
it("returns healthy when database is reachable", async () => {
const app = Fastify({ logger: false });
const db = createMockDb();
registerHealthRoutes(app, db);
const response = await app.inject({
method: "GET",
url: "/healthz",
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.status).toBe("healthy");
expect(body.checks.database).toBe("ok");
expect(body.uptime).toBeTypeOf("number");
expect(body.timestamp).toBeTypeOf("string");
await app.close();
});
it("returns degraded when database is unreachable", async () => {
const app = Fastify({ logger: false });
const db = createMockDb({
$queryRaw: vi.fn().mockRejectedValue(new Error("Connection refused")),
});
registerHealthRoutes(app, db);
const response = await app.inject({
method: "GET",
url: "/healthz",
});
expect(response.statusCode).toBe(503);
const body = JSON.parse(response.body);
expect(body.status).toBe("degraded");
expect(body.checks.database).toBe("error");
await app.close();
});
});

View File

@@ -0,0 +1,12 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"types": ["node"]
},
"include": ["src/**/*.ts"],
"references": [
{ "path": "../shared" }
]
}

View File

@@ -0,0 +1,8 @@
import { defineProject } from 'vitest/config';
export default defineProject({
test: {
name: 'labd',
include: ['tests/**/*.test.ts'],
},
});

View File

@@ -31,3 +31,6 @@ DHCP_RANGE_END=
# Path to SSH keys directory on host (mounted read-only) # Path to SSH keys directory on host (mounted read-only)
SSH_KEY_PATH=~/.ssh SSH_KEY_PATH=~/.ssh
# CockroachDB connection (used by labd)
DATABASE_URL=postgresql://root@localhost:26257/labctl?sslmode=disable

View File

@@ -15,6 +15,18 @@ services:
- NET_ADMIN - NET_ADMIN
- NET_RAW - NET_RAW
cockroachdb:
image: cockroachdb/cockroach:latest-v24.3
command: start-single-node --insecure --store=type=mem,size=256MiB
ports:
- "26257:26257"
- "8081:8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health?ready=1"]
interval: 5s
timeout: 5s
retries: 10
volumes: volumes:
bastion-state: bastion-state:
bastion-tftp: bastion-tftp:

View File

@@ -3,6 +3,7 @@
"references": [ "references": [
{ "path": "src/shared" }, { "path": "src/shared" },
{ "path": "src/bastion" }, { "path": "src/bastion" },
{ "path": "src/cli" } { "path": "src/cli" },
{ "path": "src/labd" }
] ]
} }