feat(cli): validate prerequisites and seal installation plans
This commit is contained in:
Generated
+403
@@ -6,11 +6,13 @@
|
||||
"": {
|
||||
"name": "thothii-backend",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1141.0",
|
||||
"@fastify/cookie": "11.1.2",
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/rate-limit": "11.2.0",
|
||||
"@types/pg": "^8.20.3",
|
||||
"fastify": "^5.0.0",
|
||||
"ipaddr.js": "2.4.0",
|
||||
"kysely": "^0.29.5",
|
||||
"libphonenumber-js": "1.13.12",
|
||||
"openid-client": "6.8.5",
|
||||
@@ -29,6 +31,314 @@
|
||||
"vitest": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/checksums": {
|
||||
"version": "3.1001.1",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.1.tgz",
|
||||
"integrity": "sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-s3": {
|
||||
"version": "3.1141.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1141.0.tgz",
|
||||
"integrity": "sha512-uOVH37xGLenAdJkCPCin/JJG2PgWrFcSsDnQ9+C9Zq8N9Oalo5ol4xmn5fG28iWAlA/b/9boQZgHbMh+UsIhcg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/checksums": "^3.1001.1",
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/credential-provider-node": "^3.972.84",
|
||||
"@aws-sdk/middleware-sdk-s3": "^3.972.77",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/fetch-http-handler": "^5.8.0",
|
||||
"@smithy/node-http-handler": "^4.12.1",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/core": {
|
||||
"version": "3.978.1",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz",
|
||||
"integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@aws-sdk/xml-builder": "^3.972.41",
|
||||
"@aws/lambda-invoke-store": "^0.3.0",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/signature-v4": "^5.7.3",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"bowser": "^2.11.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-env": {
|
||||
"version": "3.972.72",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz",
|
||||
"integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-http": {
|
||||
"version": "3.972.74",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz",
|
||||
"integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/fetch-http-handler": "^5.8.0",
|
||||
"@smithy/node-http-handler": "^4.12.1",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-ini": {
|
||||
"version": "3.973.17",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz",
|
||||
"integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/credential-provider-env": "^3.972.72",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.74",
|
||||
"@aws-sdk/credential-provider-login": "^3.972.79",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.72",
|
||||
"@aws-sdk/credential-provider-sso": "^3.973.16",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.78",
|
||||
"@aws-sdk/nested-clients": "^3.997.46",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/credential-provider-imds": "^4.5.2",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-login": {
|
||||
"version": "3.972.79",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz",
|
||||
"integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/nested-clients": "^3.997.46",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-node": {
|
||||
"version": "3.972.84",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz",
|
||||
"integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-provider-env": "^3.972.72",
|
||||
"@aws-sdk/credential-provider-http": "^3.972.74",
|
||||
"@aws-sdk/credential-provider-ini": "^3.973.17",
|
||||
"@aws-sdk/credential-provider-process": "^3.972.72",
|
||||
"@aws-sdk/credential-provider-sso": "^3.973.16",
|
||||
"@aws-sdk/credential-provider-web-identity": "^3.972.78",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/credential-provider-imds": "^4.5.2",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-process": {
|
||||
"version": "3.972.72",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz",
|
||||
"integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-sso": {
|
||||
"version": "3.973.16",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz",
|
||||
"integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/nested-clients": "^3.997.46",
|
||||
"@aws-sdk/token-providers": "3.1138.0",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||
"version": "3.972.78",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz",
|
||||
"integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/nested-clients": "^3.997.46",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/middleware-sdk-s3": {
|
||||
"version": "3.972.77",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.77.tgz",
|
||||
"integrity": "sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/nested-clients": {
|
||||
"version": "3.997.46",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz",
|
||||
"integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/fetch-http-handler": "^5.8.0",
|
||||
"@smithy/node-http-handler": "^4.12.1",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/signature-v4-multi-region": {
|
||||
"version": "3.996.47",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz",
|
||||
"integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/signature-v4": "^5.7.3",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/token-providers": {
|
||||
"version": "3.1138.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz",
|
||||
"integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.978.1",
|
||||
"@aws-sdk/nested-clients": "^3.997.46",
|
||||
"@aws-sdk/types": "^3.974.6",
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/types": {
|
||||
"version": "3.974.6",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz",
|
||||
"integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.41",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz",
|
||||
"integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws/lambda-invoke-store": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
|
||||
"integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@balena/dockerignore": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz",
|
||||
@@ -1404,6 +1714,87 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@smithy/core": {
|
||||
"version": "3.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.0.tgz",
|
||||
"integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/credential-provider-imds": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz",
|
||||
"integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.33.2",
|
||||
"@smithy/types": "^4.17.2",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/fetch-http-handler": {
|
||||
"version": "5.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz",
|
||||
"integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.18.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/node-http-handler": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz",
|
||||
"integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.33.3",
|
||||
"@smithy/types": "^4.18.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/signature-v4": {
|
||||
"version": "5.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz",
|
||||
"integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/core": "^3.35.0",
|
||||
"@smithy/types": "^4.19.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz",
|
||||
"integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testcontainers/postgresql": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-12.1.0.tgz",
|
||||
@@ -1990,6 +2381,12 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/bowser": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
|
||||
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
@@ -4320,6 +4717,12 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
"test:schema-v3-verifier": "npm run test:schema-v4-verifier"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1141.0",
|
||||
"@fastify/cookie": "11.1.2",
|
||||
"@fastify/cors": "^11.2.0",
|
||||
"@fastify/rate-limit": "11.2.0",
|
||||
"@types/pg": "^8.20.3",
|
||||
"fastify": "^5.0.0",
|
||||
"ipaddr.js": "2.4.0",
|
||||
"kysely": "^0.29.5",
|
||||
"libphonenumber-js": "1.13.12",
|
||||
"openid-client": "6.8.5",
|
||||
|
||||
@@ -13,6 +13,8 @@ const database = databaseConfigurationSchema.extend({
|
||||
});
|
||||
const bootstrap = z.object({ schemaVersion: z.literal(1), databases: z.array(database).min(1).max(1000) }).strict();
|
||||
|
||||
export const parseDatabaseBootstrap = (value: unknown) => bootstrap.parse(value);
|
||||
|
||||
export interface BootstrapReference { field: string; path: string }
|
||||
|
||||
/** Offline bootstrap boundary: runtime Catalog owns the resulting bindings after import. */
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { S3Client, ListObjectsV2Command } from "@aws-sdk/client-s3";
|
||||
import { parseWorkspaceYaml } from "../workspaces/schema.js";
|
||||
import { evidencePolicy } from "../workspaces/evidence/preprocessing.js";
|
||||
import type { parseDatabaseBootstrap } from "./bootstrap-documents.js";
|
||||
import { probePublicEvidenceUrl, publicEvidenceAgent } from "./evidence-probe-http.js";
|
||||
|
||||
type Entry = ReturnType<typeof parseDatabaseBootstrap>["databases"][number];
|
||||
|
||||
/** Read-only availability probes; domain correctness and materialization remain runtime gates. */
|
||||
export async function probeEvidence(entry: Entry, root: string): Promise<void> {
|
||||
const evidence = parseWorkspaceYaml(readFileSync(join(root, entry.workspaceId, "workspace.yaml"), "utf8")).evidence;
|
||||
if (!evidence || evidence.source.type === "filesystem") return;
|
||||
if (evidencePolicy(evidence)) throw new Error("Evidence egress policy refused");
|
||||
const secret = (name: keyof NonNullable<Entry["evidenceSecretFiles"]>) => {
|
||||
const path = entry.evidenceSecretFiles?.[name];
|
||||
if (!path) throw new Error("Evidence credential missing");
|
||||
return readFileSync(path, "utf8").trim();
|
||||
};
|
||||
const source = evidence.source;
|
||||
if (source.type === "http") {
|
||||
const urls: unknown = source.authentication === "signed_urls_file"
|
||||
? JSON.parse(secret("evidence.signed_urls")) : source.uris;
|
||||
if (!Array.isArray(urls) || urls.length !== source.uris.length || urls.length > 1000) throw new Error("Invalid signed URLs");
|
||||
for (const [index, value] of urls.entries()) {
|
||||
if (typeof value !== "string") throw new Error("Invalid signed URL");
|
||||
const url = new URL(value);
|
||||
const provenance = new URL(source.uris[index]);
|
||||
// Signed queries may authorize the same identity, never a different host/path.
|
||||
if (url.origin !== provenance.origin || url.pathname !== provenance.pathname || url.username || url.password || url.hash) throw new Error("Invalid signed URL identity");
|
||||
await probePublicEvidenceUrl(url);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// The shared runtime policy currently permits trusted AWS endpoints with explicit file credentials.
|
||||
const location = new URL(source.uri);
|
||||
const client = new S3Client({
|
||||
region: source.region ?? "us-east-1", maxAttempts: 1,
|
||||
requestHandler: { httpsAgent: publicEvidenceAgent(), connectionTimeout: 5_000, requestTimeout: 5_000 },
|
||||
credentials: { accessKeyId: secret("evidence.access_key"), secretAccessKey: secret("evidence.secret_key"),
|
||||
...(entry.evidenceSecretFiles?.["evidence.session_token"] ? { sessionToken: secret("evidence.session_token") } : {}) },
|
||||
});
|
||||
try {
|
||||
await client.send(new ListObjectsV2Command({ Bucket: location.hostname, Prefix: decodeURIComponent(location.pathname.slice(1)), MaxKeys: 1 }), { abortSignal: AbortSignal.timeout(5_000) });
|
||||
} finally { client.destroy(); }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { parse } from "yaml";
|
||||
import { parseDatabaseBootstrap } from "./bootstrap-documents.js";
|
||||
import { runBootstrapValidation } from "./bootstrap-cli.js";
|
||||
import { createConcreteDiagnosticAdapters, type DiagnosticAdapters } from "../workspaces/diagnostics.js";
|
||||
import { probeEvidence } from "./bootstrap-evidence-probes.js";
|
||||
|
||||
interface ProbeCheck { id: string; outcome: "passed" | "error"; field: string; action: string }
|
||||
|
||||
/** Uses the same read-only, authenticated connector diagnostics as the Catalog. */
|
||||
export async function probeBootstrapDependencies(value: unknown, adapters: DiagnosticAdapters = createConcreteDiagnosticAdapters(), workspaceRoot?: string) {
|
||||
const document = parseDatabaseBootstrap(value);
|
||||
const checks: ProbeCheck[] = [];
|
||||
for (const [index, entry] of document.databases.entries()) {
|
||||
let outcome: ProbeCheck["outcome"] = "passed";
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 5_000);
|
||||
try {
|
||||
if (entry.binding.transport === "ssh_tunnel") throw new Error("session transport unavailable");
|
||||
await adapters.probeConnector({
|
||||
role: "dwh", transport: entry.binding.transport,
|
||||
host: entry.binding.host, port: entry.binding.port, user: entry.binding.username,
|
||||
baseUrl: entry.binding.baseUrl,
|
||||
credentialFile: entry.binding.transport === "rest_api" ? entry.secretFiles.apiKey : entry.secretFiles.password,
|
||||
tlsCaFile: entry.secretFiles.tlsCa, tlsServername: entry.binding.tlsServername,
|
||||
resource: { database: entry.databaseName, schema: entry.schema },
|
||||
timeoutMs: 5_000, signal: controller.signal,
|
||||
diagnostic: { method: "GET", path: entry.binding.restPath ?? "/health", auth: entry.binding.restAuth ?? "bearer" },
|
||||
});
|
||||
} catch { outcome = "error"; } finally { clearTimeout(timer); }
|
||||
checks.push({ id: `database-${index}`, outcome, field: `database-bootstrap.databases.${index}`,
|
||||
action: entry.binding.transport === "ssh_tunnel"
|
||||
? "Choose postgres_direct or rest_api for NL-to-SQL practice; SSH diagnostics alone cannot establish session readiness."
|
||||
: "Require an authenticated read-only connection and access to the configured database/schema; correct endpoint, permissions or protected credentials." });
|
||||
if (workspaceRoot) {
|
||||
let evidenceOutcome: ProbeCheck["outcome"] = "passed";
|
||||
try { await probeEvidence(entry, workspaceRoot); } catch { evidenceOutcome = "error"; }
|
||||
checks.push({ id: `evidence-${index}`, outcome: evidenceOutcome, field: `workspaces.${index}.evidence`, action: "Require readable local Evidence or authenticated bounded HTTP/S3 access under the canonical egress policy; domain meaning is verified during practice." });
|
||||
}
|
||||
}
|
||||
return { schema_version: 1, ok: checks.every((check) => check.outcome === "passed"), checks };
|
||||
}
|
||||
|
||||
export async function runBootstrapProbes(args: string[]) {
|
||||
const validation = runBootstrapValidation(args);
|
||||
if (validation.status !== 0) return validation;
|
||||
try {
|
||||
const report = await probeBootstrapDependencies(parse(readFileSync(args[3], "utf8")), undefined, args[1]);
|
||||
return { status: report.ok ? 0 : 1, output: JSON.stringify(report) };
|
||||
} catch {
|
||||
return { status: 1, output: JSON.stringify({ schema_version: 1, ok: false, checks: [{ id: "database-probes", outcome: "error", field: "database-bootstrap", action: "Revalidate prepared documents and protected credential references." }] }) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { lookup } from "node:dns/promises";
|
||||
import { request as httpRequest } from "node:http";
|
||||
import { request as httpsRequest, Agent } from "node:https";
|
||||
import type { LookupFunction } from "node:net";
|
||||
import ipaddr from "ipaddr.js";
|
||||
|
||||
const refused = () => new Error("Evidence network policy refused");
|
||||
function normalizedPublicAddress(value: string): string {
|
||||
const address = ipaddr.process(value);
|
||||
if (address.range() !== "unicast") throw refused();
|
||||
return address.toString();
|
||||
}
|
||||
|
||||
/** Reject the entire DNS answer set, then pin the connection to that verified set. */
|
||||
export async function resolvePublicEvidenceHost(hostname: string) {
|
||||
const values = await lookup(hostname.replace(/^\[|\]$/g, ""), { all: true });
|
||||
if (!values.length) throw refused();
|
||||
values.forEach((value) => normalizedPublicAddress(value.address));
|
||||
return values;
|
||||
}
|
||||
const publicLookup: LookupFunction = (hostname, options, callback) => {
|
||||
void resolvePublicEvidenceHost(hostname).then((values) => {
|
||||
if (options.all) callback(null, values);
|
||||
else callback(null, values[0].address, values[0].family);
|
||||
}, () => callback(refused(), "", 0));
|
||||
};
|
||||
|
||||
// Node's direct agent does not inherit HTTP proxy environment or ambient credentials.
|
||||
export const publicEvidenceAgent = () => new Agent({ lookup: publicLookup });
|
||||
|
||||
export async function probePublicEvidenceUrl(url: URL): Promise<void> {
|
||||
if (!['http:', 'https:'].includes(url.protocol)) throw refused();
|
||||
const signal = AbortSignal.timeout(5_000);
|
||||
const values = await Promise.race([
|
||||
resolvePublicEvidenceHost(url.hostname),
|
||||
new Promise<never>((_, reject) => signal.addEventListener("abort", () => reject(refused()), { once: true })),
|
||||
]);
|
||||
signal.throwIfAborted();
|
||||
const allowed = new Set(values.map((value) => normalizedPublicAddress(value.address)));
|
||||
const pinned: LookupFunction = (_hostname, options, callback) => {
|
||||
if (options.all) callback(null, values);
|
||||
else callback(null, values[0].address, values[0].family);
|
||||
};
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
|
||||
method: "GET", lookup: pinned, signal, agent: false,
|
||||
}, (response) => {
|
||||
try {
|
||||
const peer = response.socket.remoteAddress;
|
||||
if (!peer || !allowed.has(normalizedPublicAddress(peer)) || !response.statusCode || response.statusCode < 200 || response.statusCode >= 300) throw refused();
|
||||
resolve();
|
||||
} catch { reject(refused()); } finally { response.destroy(); }
|
||||
});
|
||||
request.on("error", () => reject(refused()));
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
/** Compiled with its runtime for the host CLI: no installation, Docker or host Node required. */
|
||||
import { runWorkspaceDocuments } from "./workspaces/documents.js";
|
||||
import { runBootstrapValidation } from "./catalog/bootstrap-cli.js";
|
||||
import { runBootstrapProbes } from "./catalog/bootstrap-probes.js";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const result = args[0] === "bootstrap" ? runBootstrapValidation(args.slice(1)) : runWorkspaceDocuments(args);
|
||||
const result = args[0] === "probe" ? await runBootstrapProbes(args.slice(1))
|
||||
: args[0] === "bootstrap" ? runBootstrapValidation(args.slice(1)) : runWorkspaceDocuments(args);
|
||||
console.log(result.output);
|
||||
process.exitCode = result.status;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createServer } from "node:http";
|
||||
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { probeBootstrapDependencies } from "../src/catalog/bootstrap-probes.js";
|
||||
import { probePublicEvidenceUrl } from "../src/catalog/evidence-probe-http.js";
|
||||
|
||||
it("refuses loopback literals and DNS answers before sending an Evidence GET", async () => {
|
||||
for (const hostname of ["[::1]", "127.0.0.1", "localhost", "[::ffff:127.0.0.1]"]) {
|
||||
await expect(probePublicEvidenceUrl(new URL(`http://${hostname}/private`))).rejects.toThrow("Evidence network policy refused");
|
||||
}
|
||||
});
|
||||
|
||||
it("authenticates a bounded read-only REST probe and blocks unavailable credentials/services", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "tht-probe-"));
|
||||
const secret = join(directory, "key");
|
||||
writeFileSync(secret, "PRIVATE_SENTINEL", { mode: 0o600 });
|
||||
let status = 200;
|
||||
const requests: string[] = [];
|
||||
const server = createServer((req, res) => {
|
||||
requests.push(`${req.method} ${req.url}`);
|
||||
res.writeHead(req.headers.authorization === "Bearer PRIVATE_SENTINEL" ? status : 401);
|
||||
res.end("PRIVATE_SERVER_RESPONSE");
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address() as { port: number };
|
||||
const document = { schemaVersion: 1, databases: [{ workspaceId: "demo", engine: "postgres", databaseName: "demo", schema: "public", binding: { transport: "rest_api", baseUrl: `http://127.0.0.1:${address.port}`, restPath: "/health", restAuth: "bearer" }, secretFiles: { apiKey: secret } }] };
|
||||
try {
|
||||
expect((await probeBootstrapDependencies(document)).ok).toBe(true);
|
||||
status = 503;
|
||||
const failed = await probeBootstrapDependencies(document);
|
||||
expect(failed.ok).toBe(false);
|
||||
expect(JSON.stringify(failed)).not.toContain("PRIVATE");
|
||||
status = 200;
|
||||
writeFileSync(secret, "rotated-but-invalid");
|
||||
expect((await probeBootstrapDependencies(document)).ok).toBe(false);
|
||||
expect(requests).toEqual(["GET /health", "GET /health", "GET /health"]);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
rmSync(directory, { recursive: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user