add authentik

This commit is contained in:
Morten Olsen
2025-07-30 13:42:25 +02:00
parent dd1e5a8124
commit 523637d40f
27 changed files with 59686 additions and 340 deletions

View File

@@ -46,6 +46,6 @@ export default tseslint.config(
}, },
...compat.extends('plugin:prettier/recommended'), ...compat.extends('plugin:prettier/recommended'),
{ {
ignores: ['**/node_modules/', '**/dist/', '**/.turbo/', '**/generated/'], ignores: ['**/node_modules/', '**/dist/', '**/.turbo/', '**/generated/', '**/clients/*.types.ts'],
}, },
); );

View File

@@ -4,15 +4,16 @@
"type": "module", "type": "module",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@types/bun": "latest",
"nodemon": "^3.1.10",
"@eslint/eslintrc": "3.3.1", "@eslint/eslintrc": "3.3.1",
"@eslint/js": "9.32.0", "@eslint/js": "9.32.0",
"@pnpm/find-workspace-packages": "6.0.9", "@pnpm/find-workspace-packages": "6.0.9",
"@types/bun": "latest",
"eslint": "9.32.0", "eslint": "9.32.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-prettier": "5.5.3", "eslint-plugin-prettier": "5.5.3",
"nodemon": "^3.1.10",
"openapi-typescript": "^7.8.0",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "5.8.3", "typescript": "5.8.3",
"typescript-eslint": "8.38.0" "typescript-eslint": "8.38.0"
@@ -21,17 +22,24 @@
"typescript": "^5" "typescript": "^5"
}, },
"dependencies": { "dependencies": {
"@goauthentik/api": "2025.6.3-1751754396",
"@kubernetes/client-node": "^1.3.0", "@kubernetes/client-node": "^1.3.0",
"@sinclair/typebox": "^0.34.38", "@sinclair/typebox": "^0.34.38",
"dotenv": "^17.2.1",
"knex": "^3.1.0", "knex": "^3.1.0",
"openapi-fetch": "^0.14.0",
"pg": "^8.16.3", "pg": "^8.16.3",
"sqlite3": "^5.1.7" "sqlite3": "^5.1.7",
"yaml": "^2.8.0"
}, },
"packageManager": "pnpm@10.6.0", "packageManager": "pnpm@10.6.0",
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"sqlite3" "sqlite3"
] ],
"patchedDependencies": {
"@kubernetes/client-node": "patches/@kubernetes__client-node.patch"
}
}, },
"scripts": { "scripts": {
"test": "echo 'No tests'", "test": "echo 'No tests'",

View File

@@ -0,0 +1,14 @@
diff --git a/dist/gen/models/ObjectSerializer.js b/dist/gen/models/ObjectSerializer.js
index 1d798b6a2d7c059165d1df9fbb77b89a8317ebca..c8bacfdc95be0f0146c6505f89a9372e013afea4 100644
--- a/dist/gen/models/ObjectSerializer.js
+++ b/dist/gen/models/ObjectSerializer.js
@@ -2216,6 +2216,9 @@ export class ObjectSerializer {
return transformedData;
}
else if (type === "Date") {
+ if (typeof data === "string") {
+ return data;
+ }
if (format == "date") {
let month = data.getMonth() + 1;
month = month < 10 ? "0" + month.toString() : month.toString();

239
pnpm-lock.yaml generated
View File

@@ -4,25 +4,42 @@ settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
patchedDependencies:
'@kubernetes/client-node':
hash: 0b0e5d32aa2930107c8c9b45df2639faf53fa12a389a551885d6e42d71f9429d
path: patches/@kubernetes__client-node.patch
importers: importers:
.: .:
dependencies: dependencies:
'@goauthentik/api':
specifier: 2025.6.3-1751754396
version: 2025.6.3-1751754396
'@kubernetes/client-node': '@kubernetes/client-node':
specifier: ^1.3.0 specifier: ^1.3.0
version: 1.3.0(encoding@0.1.13) version: 1.3.0(patch_hash=0b0e5d32aa2930107c8c9b45df2639faf53fa12a389a551885d6e42d71f9429d)(encoding@0.1.13)
'@sinclair/typebox': '@sinclair/typebox':
specifier: ^0.34.38 specifier: ^0.34.38
version: 0.34.38 version: 0.34.38
dotenv:
specifier: ^17.2.1
version: 17.2.1
knex: knex:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0(pg@8.16.3)(sqlite3@5.1.7) version: 3.1.0(pg@8.16.3)(sqlite3@5.1.7)
openapi-fetch:
specifier: ^0.14.0
version: 0.14.0
pg: pg:
specifier: ^8.16.3 specifier: ^8.16.3
version: 8.16.3 version: 8.16.3
sqlite3: sqlite3:
specifier: ^5.1.7 specifier: ^5.1.7
version: 5.1.7 version: 5.1.7
yaml:
specifier: ^2.8.0
version: 2.8.0
devDependencies: devDependencies:
'@eslint/eslintrc': '@eslint/eslintrc':
specifier: 3.3.1 specifier: 3.3.1
@@ -51,6 +68,9 @@ importers:
nodemon: nodemon:
specifier: ^3.1.10 specifier: ^3.1.10
version: 3.1.10 version: 3.1.10
openapi-typescript:
specifier: ^7.8.0
version: 7.8.0(typescript@5.8.3)
prettier: prettier:
specifier: 3.6.2 specifier: 3.6.2
version: 3.6.2 version: 3.6.2
@@ -112,6 +132,9 @@ packages:
'@gar/promisify@1.1.3': '@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
'@goauthentik/api@2025.6.3-1751754396':
resolution: {integrity: sha512-JL7V1OTRIYJYzWvqoo2TNEQZirFHHkUt2tludO82luVEPDZwWQ8NwvwcFvO74v/SDSqg9VGN6H9BDoUrPD6KHQ==}
'@gwhitney/detect-indent@7.0.1': '@gwhitney/detect-indent@7.0.1':
resolution: {integrity: sha512-7bQW+gkKa2kKZPeJf6+c6gFK9ARxQfn+FKy9ScTBppyKRWH2KzsmweXUoklqeEiHiNVWaeP5csIdsNq6w7QhzA==} resolution: {integrity: sha512-7bQW+gkKa2kKZPeJf6+c6gFK9ARxQfn+FKy9ScTBppyKRWH2KzsmweXUoklqeEiHiNVWaeP5csIdsNq6w7QhzA==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
@@ -316,6 +339,16 @@ packages:
resolution: {integrity: sha512-zU4vDfBUx/jUBPmR4CzCqPDOPObb/7iLT3UZvhXSJ8ZXDo9214V6agnJvxQ6bYBcypdiKva0hnb3tmo1chQBYg==} resolution: {integrity: sha512-zU4vDfBUx/jUBPmR4CzCqPDOPObb/7iLT3UZvhXSJ8ZXDo9214V6agnJvxQ6bYBcypdiKva0hnb3tmo1chQBYg==}
engines: {node: '>=16.14'} engines: {node: '>=16.14'}
'@redocly/ajv@8.11.2':
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
'@redocly/config@0.22.2':
resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
'@redocly/openapi-core@1.34.5':
resolution: {integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==}
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
'@rtsao/scc@1.1.0': '@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@@ -458,6 +491,10 @@ packages:
ansi-align@3.0.1: ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
ansi-diff@1.2.0: ansi-diff@1.2.0:
resolution: {integrity: sha512-BIXwHKpjzghBjcwEV10Y4b17tjHfK4nhEqK3LqyQ3JgcMcjmi3DIevozNgrOpfvBMmrq9dfvrPJSu5/5vNUBQg==} resolution: {integrity: sha512-BIXwHKpjzghBjcwEV10Y4b17tjHfK4nhEqK3LqyQ3JgcMcjmi3DIevozNgrOpfvBMmrq9dfvrPJSu5/5vNUBQg==}
@@ -650,6 +687,9 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'} engines: {node: '>=10'}
change-case@5.4.4:
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
char-regex@1.0.2: char-regex@1.0.2:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -692,6 +732,9 @@ packages:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true hasBin: true
colorette@1.4.0:
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
colorette@2.0.19: colorette@2.0.19:
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
@@ -801,6 +844,10 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dotenv@17.2.1:
resolution: {integrity: sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==}
engines: {node: '>=12'}
dunder-proto@1.0.1: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1164,6 +1211,10 @@ packages:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
human-signals@2.1.0: human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'} engines: {node: '>=10.17.0'}
@@ -1201,6 +1252,10 @@ packages:
resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
engines: {node: '>=8'} engines: {node: '>=8'}
index-to-position@1.1.0:
resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==}
engines: {node: '>=18'}
individual@3.0.0: individual@3.0.0:
resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==}
@@ -1377,6 +1432,10 @@ packages:
jose@6.0.12: jose@6.0.12:
resolution: {integrity: sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==} resolution: {integrity: sha512-T8xypXs8CpmiIi78k0E+Lk7T2zlK4zDyg+o1CZ4AkOHgDg98ogdP2BeZ61lTFKFyoEwJ9RgAgN+SdM3iPgNonQ==}
js-levenshtein@1.1.6:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -1400,6 +1459,9 @@ packages:
json-schema-traverse@0.4.1: json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-stable-stringify-without-jsonify@1.0.1: json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
@@ -1530,6 +1592,10 @@ packages:
minimatch@3.1.2: minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
minimatch@9.0.5: minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
@@ -1679,6 +1745,18 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'} engines: {node: '>=6'}
openapi-fetch@0.14.0:
resolution: {integrity: sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==}
openapi-typescript-helpers@0.0.15:
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
openapi-typescript@7.8.0:
resolution: {integrity: sha512-1EeVWmDzi16A+siQlo/SwSGIT7HwaFAVjvMA7/jG5HMLSnrUOzPL7uSTRZZa4v/LCRxHTApHKtNY6glApEoiUQ==}
hasBin: true
peerDependencies:
typescript: ^5.x
openid-client@6.6.2: openid-client@6.6.2:
resolution: {integrity: sha512-Xya5TNMnnZuTM6DbHdB4q0S3ig2NTAELnii/ASie1xDEr8iiB8zZbO871OWBdrw++sd3hW6bqWjgcmSy1RTWHA==} resolution: {integrity: sha512-Xya5TNMnnZuTM6DbHdB4q0S3ig2NTAELnii/ASie1xDEr8iiB8zZbO871OWBdrw++sd3hW6bqWjgcmSy1RTWHA==}
@@ -1722,6 +1800,10 @@ packages:
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
engines: {node: '>=8'} engines: {node: '>=8'}
parse-json@8.3.0:
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
engines: {node: '>=18'}
parse-ms@2.1.0: parse-ms@2.1.0:
resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -1796,6 +1878,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
possible-typed-array-names@1.1.0: possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -1913,6 +1999,10 @@ packages:
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
resolve-from@4.0.0: resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -2145,6 +2235,10 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'} engines: {node: '>=8'}
supports-color@10.0.0:
resolution: {integrity: sha512-HRVVSbCCMbj7/kdWF9Q+bbckjBHLtHMEoJWlkmYzzdwhYMkjkOwubLM6t7NbWKjgKamGDrWL1++KrjUO1t9oAQ==}
engines: {node: '>=18'}
supports-color@5.5.0: supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -2230,6 +2324,10 @@ packages:
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
engines: {node: '>=8'} engines: {node: '>=8'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typed-array-buffer@1.0.3: typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2281,6 +2379,9 @@ packages:
resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==}
engines: {node: '>=8'} engines: {node: '>=8'}
uri-js-replace@1.0.1:
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -2367,6 +2468,18 @@ packages:
yallist@4.0.0: yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml-ast-parser@0.0.43:
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
yaml@2.8.0:
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
engines: {node: '>= 14.6'}
hasBin: true
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
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'}
@@ -2391,7 +2504,7 @@ snapshots:
'@eslint/config-array@0.21.0': '@eslint/config-array@0.21.0':
dependencies: dependencies:
'@eslint/object-schema': 2.1.6 '@eslint/object-schema': 2.1.6
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
minimatch: 3.1.2 minimatch: 3.1.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -2405,7 +2518,7 @@ snapshots:
'@eslint/eslintrc@3.3.1': '@eslint/eslintrc@3.3.1':
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
espree: 10.4.0 espree: 10.4.0
globals: 14.0.0 globals: 14.0.0
ignore: 5.3.2 ignore: 5.3.2
@@ -2428,6 +2541,8 @@ snapshots:
'@gar/promisify@1.1.3': '@gar/promisify@1.1.3':
optional: true optional: true
'@goauthentik/api@2025.6.3-1751754396': {}
'@gwhitney/detect-indent@7.0.1': {} '@gwhitney/detect-indent@7.0.1': {}
'@humanfs/core@0.19.1': {} '@humanfs/core@0.19.1': {}
@@ -2451,7 +2566,7 @@ snapshots:
dependencies: dependencies:
jsep: 1.4.0 jsep: 1.4.0
'@kubernetes/client-node@1.3.0(encoding@0.1.13)': '@kubernetes/client-node@1.3.0(patch_hash=0b0e5d32aa2930107c8c9b45df2639faf53fa12a389a551885d6e42d71f9429d)(encoding@0.1.13)':
dependencies: dependencies:
'@types/js-yaml': 4.0.9 '@types/js-yaml': 4.0.9
'@types/node': 22.16.5 '@types/node': 22.16.5
@@ -2736,6 +2851,29 @@ snapshots:
write-file-atomic: 5.0.1 write-file-atomic: 5.0.1
write-yaml-file: 5.0.0 write-yaml-file: 5.0.0
'@redocly/ajv@8.11.2':
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js-replace: 1.0.1
'@redocly/config@0.22.2': {}
'@redocly/openapi-core@1.34.5(supports-color@10.0.0)':
dependencies:
'@redocly/ajv': 8.11.2
'@redocly/config': 0.22.2
colorette: 1.4.0
https-proxy-agent: 7.0.6(supports-color@10.0.0)
js-levenshtein: 1.1.6
js-yaml: 4.1.0
minimatch: 5.1.6
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
transitivePeerDependencies:
- supports-color
'@rtsao/scc@1.1.0': {} '@rtsao/scc@1.1.0': {}
'@sinclair/typebox@0.34.38': {} '@sinclair/typebox@0.34.38': {}
@@ -2805,7 +2943,7 @@ snapshots:
'@typescript-eslint/types': 8.38.0 '@typescript-eslint/types': 8.38.0
'@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3)
'@typescript-eslint/visitor-keys': 8.38.0 '@typescript-eslint/visitor-keys': 8.38.0
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
eslint: 9.32.0 eslint: 9.32.0
typescript: 5.8.3 typescript: 5.8.3
transitivePeerDependencies: transitivePeerDependencies:
@@ -2815,7 +2953,7 @@ snapshots:
dependencies: dependencies:
'@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3)
'@typescript-eslint/types': 8.38.0 '@typescript-eslint/types': 8.38.0
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
typescript: 5.8.3 typescript: 5.8.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -2834,7 +2972,7 @@ snapshots:
'@typescript-eslint/types': 8.38.0 '@typescript-eslint/types': 8.38.0
'@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3) '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.8.3)
'@typescript-eslint/utils': 8.38.0(eslint@9.32.0)(typescript@5.8.3) '@typescript-eslint/utils': 8.38.0(eslint@9.32.0)(typescript@5.8.3)
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
eslint: 9.32.0 eslint: 9.32.0
ts-api-utils: 2.1.0(typescript@5.8.3) ts-api-utils: 2.1.0(typescript@5.8.3)
typescript: 5.8.3 typescript: 5.8.3
@@ -2849,7 +2987,7 @@ snapshots:
'@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3) '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.8.3)
'@typescript-eslint/types': 8.38.0 '@typescript-eslint/types': 8.38.0
'@typescript-eslint/visitor-keys': 8.38.0 '@typescript-eslint/visitor-keys': 8.38.0
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
fast-glob: 3.3.3 fast-glob: 3.3.3
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.5 minimatch: 9.0.5
@@ -2890,7 +3028,7 @@ snapshots:
agent-base@6.0.2: agent-base@6.0.2:
dependencies: dependencies:
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
optional: true optional: true
@@ -2919,6 +3057,8 @@ snapshots:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
ansi-colors@4.1.3: {}
ansi-diff@1.2.0: ansi-diff@1.2.0:
dependencies: dependencies:
ansi-split: 1.0.1 ansi-split: 1.0.1
@@ -3164,6 +3304,8 @@ snapshots:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
supports-color: 7.2.0 supports-color: 7.2.0
change-case@5.4.4: {}
char-regex@1.0.2: {} char-regex@1.0.2: {}
chokidar@3.6.0: chokidar@3.6.0:
@@ -3203,6 +3345,8 @@ snapshots:
color-support@1.1.3: color-support@1.1.3:
optional: true optional: true
colorette@1.4.0: {}
colorette@2.0.19: {} colorette@2.0.19: {}
combined-stream@1.0.8: combined-stream@1.0.8:
@@ -3259,6 +3403,16 @@ snapshots:
dependencies: dependencies:
ms: 2.1.2 ms: 2.1.2
debug@4.4.1:
dependencies:
ms: 2.1.3
debug@4.4.1(supports-color@10.0.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 10.0.0
debug@4.4.1(supports-color@5.5.0): debug@4.4.1(supports-color@5.5.0):
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
@@ -3300,6 +3454,8 @@ snapshots:
dependencies: dependencies:
esutils: 2.0.3 esutils: 2.0.3
dotenv@17.2.1: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@@ -3500,7 +3656,7 @@ snapshots:
ajv: 6.12.6 ajv: 6.12.6
chalk: 4.1.2 chalk: 4.1.2
cross-spawn: 7.0.6 cross-spawn: 7.0.6
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
escape-string-regexp: 4.0.0 escape-string-regexp: 4.0.0
eslint-scope: 8.4.0 eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 4.2.1
@@ -3757,7 +3913,7 @@ snapshots:
dependencies: dependencies:
'@tootallnate/once': 1.1.2 '@tootallnate/once': 1.1.2
agent-base: 6.0.2 agent-base: 6.0.2
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
optional: true optional: true
@@ -3765,11 +3921,18 @@ snapshots:
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1:
dependencies: dependencies:
agent-base: 6.0.2 agent-base: 6.0.2
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
optional: true optional: true
https-proxy-agent@7.0.6(supports-color@10.0.0):
dependencies:
agent-base: 7.1.4
debug: 4.4.1(supports-color@10.0.0)
transitivePeerDependencies:
- supports-color
human-signals@2.1.0: {} human-signals@2.1.0: {}
humanize-ms@1.2.1: humanize-ms@1.2.1:
@@ -3800,6 +3963,8 @@ snapshots:
indent-string@4.0.0: indent-string@4.0.0:
optional: true optional: true
index-to-position@1.1.0: {}
individual@3.0.0: {} individual@3.0.0: {}
infer-owner@1.0.4: infer-owner@1.0.4:
@@ -3968,6 +4133,8 @@ snapshots:
jose@6.0.12: {} jose@6.0.12: {}
js-levenshtein@1.1.6: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-yaml@4.1.0: js-yaml@4.1.0:
@@ -3984,6 +4151,8 @@ snapshots:
json-schema-traverse@0.4.1: {} json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
json-stable-stringify-without-jsonify@1.0.1: {} json-stable-stringify-without-jsonify@1.0.1: {}
json-stringify-safe@5.0.1: {} json-stringify-safe@5.0.1: {}
@@ -4114,6 +4283,10 @@ snapshots:
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.12
minimatch@5.1.6:
dependencies:
brace-expansion: 2.0.2
minimatch@9.0.5: minimatch@9.0.5:
dependencies: dependencies:
brace-expansion: 2.0.2 brace-expansion: 2.0.2
@@ -4289,6 +4462,22 @@ snapshots:
dependencies: dependencies:
mimic-fn: 2.1.0 mimic-fn: 2.1.0
openapi-fetch@0.14.0:
dependencies:
openapi-typescript-helpers: 0.0.15
openapi-typescript-helpers@0.0.15: {}
openapi-typescript@7.8.0(typescript@5.8.3):
dependencies:
'@redocly/openapi-core': 1.34.5(supports-color@10.0.0)
ansi-colors: 4.1.3
change-case: 5.4.4
parse-json: 8.3.0
supports-color: 10.0.0
typescript: 5.8.3
yargs-parser: 21.1.1
openid-client@6.6.2: openid-client@6.6.2:
dependencies: dependencies:
jose: 6.0.12 jose: 6.0.12
@@ -4341,6 +4530,12 @@ snapshots:
json-parse-even-better-errors: 2.3.1 json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4 lines-and-columns: 1.2.4
parse-json@8.3.0:
dependencies:
'@babel/code-frame': 7.27.1
index-to-position: 1.1.0
type-fest: 4.41.0
parse-ms@2.1.0: {} parse-ms@2.1.0: {}
path-absolute@1.0.1: {} path-absolute@1.0.1: {}
@@ -4401,6 +4596,8 @@ snapshots:
picomatch@2.3.1: {} picomatch@2.3.1: {}
pluralize@8.0.0: {}
possible-typed-array-names@1.1.0: {} possible-typed-array-names@1.1.0: {}
postgres-array@2.0.0: {} postgres-array@2.0.0: {}
@@ -4521,6 +4718,8 @@ snapshots:
gopd: 1.2.0 gopd: 1.2.0
set-function-name: 2.0.2 set-function-name: 2.0.2
require-from-string@2.0.2: {}
resolve-from@4.0.0: {} resolve-from@4.0.0: {}
resolve-from@5.0.0: {} resolve-from@5.0.0: {}
@@ -4667,7 +4866,7 @@ snapshots:
socks-proxy-agent@6.2.1: socks-proxy-agent@6.2.1:
dependencies: dependencies:
agent-base: 6.0.2 agent-base: 6.0.2
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
socks: 2.8.6 socks: 2.8.6
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -4676,7 +4875,7 @@ snapshots:
socks-proxy-agent@8.0.5: socks-proxy-agent@8.0.5:
dependencies: dependencies:
agent-base: 7.1.4 agent-base: 7.1.4
debug: 4.4.1(supports-color@5.5.0) debug: 4.4.1
socks: 2.8.6 socks: 2.8.6
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -4790,6 +4989,8 @@ snapshots:
strip-json-comments@3.1.1: {} strip-json-comments@3.1.1: {}
supports-color@10.0.0: {}
supports-color@5.5.0: supports-color@5.5.0:
dependencies: dependencies:
has-flag: 3.0.0 has-flag: 3.0.0
@@ -4889,6 +5090,8 @@ snapshots:
type-fest@0.6.0: {} type-fest@0.6.0: {}
type-fest@4.41.0: {}
typed-array-buffer@1.0.3: typed-array-buffer@1.0.3:
dependencies: dependencies:
call-bound: 1.0.4 call-bound: 1.0.4
@@ -4962,6 +5165,8 @@ snapshots:
dependencies: dependencies:
crypto-random-string: 2.0.0 crypto-random-string: 2.0.0
uri-js-replace@1.0.1: {}
uri-js@4.4.1: uri-js@4.4.1:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
@@ -5063,4 +5268,10 @@ snapshots:
yallist@4.0.0: {} yallist@4.0.0: {}
yaml-ast-parser@0.0.43: {}
yaml@2.8.0: {}
yargs-parser@21.1.1: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}

View File

@@ -1,8 +0,0 @@
apiVersion: 'homelab.mortenolsen.pro/v1';
kind: 'PostgresDatabase';
name: 'test2';
namespace: 'playground';
foo: 'bar';
foo: 'bar';
{
}

24
scripts/create-clients.ts Normal file
View File

@@ -0,0 +1,24 @@
import fs from 'node:fs';
import { mkdir } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import YAML from 'yaml';
import openapiTS, { astToString } from 'openapi-typescript';
const schemaRequest = await fetch('https://authentik.olsen.cloud/api/v3/schema/');
if (!schemaRequest.ok) {
console.error(schemaRequest.status, schemaRequest.statusText);
throw new Error('Failed to fetch schema');
}
const schemaYaml = await schemaRequest.text();
const schema = YAML.parse(schemaYaml);
const ast = await openapiTS(schema);
const contents = astToString(ast);
const targetLocation = resolve(import.meta.dirname, '..', 'src', 'clients', 'authentik', 'authentik.types.d.ts');
await mkdir(dirname(targetLocation), { recursive: true });
fs.writeFileSync(
targetLocation,
['// This file is generated by scripts/create-clients.ts', '/* eslint-disable */', contents].join('\n'),
);

2
scripts/recreate.bash Executable file
View File

@@ -0,0 +1,2 @@
kubectl delete -f "$1"
kubectl apply -f "$1"

View File

@@ -0,0 +1,33 @@
import {
Configuration,
CoreApi,
FlowsApi,
PropertymappingsApi,
ProvidersApi,
instanceOfErrorDetail,
} from '@goauthentik/api';
type CreateAuthentikClientOptions = {
baseUrl: string;
token: string;
};
const createAuthentikClient = ({ baseUrl, token }: CreateAuthentikClientOptions) => {
const config = new Configuration({
basePath: baseUrl,
headers: {
Authorization: `Bearer ${token}`,
},
});
const client = {
core: new CoreApi(config),
providers: new ProvidersApi(config),
propertymappings: new PropertymappingsApi(config),
flows: new FlowsApi(config),
};
return client;
};
type AuthentikClient = ReturnType<typeof createAuthentikClient>;
export { createAuthentikClient, type AuthentikClient, instanceOfErrorDetail };

58661
src/clients/authentik/authentik.types.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
import { Type } from '@sinclair/typebox';
import { SubModeEnum } from '@goauthentik/api';
import { CustomResource, type CustomResourceHandlerOptions } from '../../../custom-resource/custom-resource.base.ts';
import { AuthentikService } from '../../../services/authentik/authentik.service.ts';
const authentikClientSpec = Type.Object({
subMode: Type.Optional(Type.Unsafe<SubModeEnum>(Type.String())),
clientType: Type.Optional(
Type.Unsafe<'confidential' | 'public'>(
Type.String({
enum: ['confidential', 'public'],
}),
),
),
redirectUris: Type.Array(
Type.Object({
url: Type.String(),
matchingMode: Type.Unsafe<'strict' | 'regex'>(
Type.String({
enum: ['strict', 'regex'],
}),
),
}),
),
});
const authentikClientSecret = Type.Object({
clientSecret: Type.String(),
});
class AuthentikClient extends CustomResource<typeof authentikClientSpec> {
constructor() {
super({
kind: 'AuthentikClient',
names: {
singular: 'authentikclient',
plural: 'authentikclients',
},
spec: authentikClientSpec,
});
}
public update = async (options: CustomResourceHandlerOptions<typeof authentikClientSpec>) => {
const { request, services, ensureSecret } = options;
const authentikService = services.get(AuthentikService);
const { clientSecret } = await ensureSecret({
name: `authentik-client-${request.metadata.name}`,
namespace: request.metadata.namespace ?? 'default',
schema: authentikClientSecret,
generator: async () => ({
clientSecret: crypto.randomUUID(),
}),
});
const client = await authentikService.upsertClient({
name: request.metadata.name,
secret: clientSecret,
subMode: request.spec.subMode,
clientType: request.spec.clientType,
redirectUris: request.spec.redirectUris.map((rule) => ({
url: rule.url,
matchingMode: rule.matchingMode ?? 'strict',
})),
});
console.log(client.config);
};
}
export { AuthentikClient };

View File

@@ -1,9 +1,6 @@
import { Type } from '@sinclair/typebox'; import { Type } from '@sinclair/typebox';
import { ApiException, type V1Secret } from '@kubernetes/client-node';
import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts'; import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts';
import { K8sService } from '../../services/k8s.ts';
import type { CustomResourceRequest } from '../../custom-resource/custom-resource.request.ts';
import { PostgresService } from '../../services/postgres/postgres.service.ts'; import { PostgresService } from '../../services/postgres/postgres.service.ts';
const postgresDatabaseSpecSchema = Type.Object({}); const postgresDatabaseSpecSchema = Type.Object({});
@@ -20,99 +17,39 @@ class PostgresDatabase extends CustomResource<typeof postgresDatabaseSpecSchema>
}); });
} }
#getVariables = async (request: CustomResourceRequest<typeof postgresDatabaseSpecSchema>) => {
const { metadata, services } = request;
const k8sService = services.get(K8sService);
const secretName = `postgres-database-${metadata.name}`;
let secret: V1Secret | undefined;
try {
secret = await k8sService.api.readNamespacedSecret({
name: secretName,
namespace: metadata.namespace ?? 'default',
});
} catch (error) {
if (!(error instanceof ApiException && error.code === 404)) {
throw error;
}
}
if (secret && request.isOwnerOf(secret) && secret.data) {
services.log.debug('PostgresRole secret found', { secret });
return secret.data;
}
if (secret && !request.isOwnerOf(secret)) {
throw new Error('The secret is not owned by this resource');
}
const data = {
name: Buffer.from(`${metadata.namespace}_${metadata.name}`).toString('base64'),
user: Buffer.from(metadata.name).toString('base64'),
password: Buffer.from(crypto.randomUUID()).toString('base64'),
};
const namespace = metadata.namespace ?? 'default';
services.log.debug('Creating secret', { data });
const response = await k8sService.api.createNamespacedSecret({
namespace,
body: {
kind: 'Secret',
metadata: {
name: secretName,
namespace,
ownerReferences: [
{
apiVersion: request.apiVersion,
kind: request.kind,
name: metadata.name,
uid: metadata.uid,
},
],
},
type: 'Opaque',
data,
},
});
services.log.debug('Secret created', { response });
return response.data!;
};
public update = async (options: CustomResourceHandlerOptions<typeof postgresDatabaseSpecSchema>) => { public update = async (options: CustomResourceHandlerOptions<typeof postgresDatabaseSpecSchema>) => {
const { request, services } = options; const { request, services, ensureSecret } = options;
const status = await request.getStatus(); const variables = await ensureSecret({
name: `postgres-database-${request.metadata.name}`,
namespace: request.metadata.namespace ?? 'default',
schema: Type.Object({
name: Type.String(),
user: Type.String(),
password: Type.String(),
}),
generator: async () => ({
name: `${request.metadata.namespace || 'default'}_${request.metadata.name}`,
user: `${request.metadata.namespace || 'default'}_${request.metadata.name}`,
password: `password_${Buffer.from(crypto.getRandomValues(new Uint8Array(12))).toString('hex')}`,
}),
});
const postgresService = services.get(PostgresService);
await postgresService.upsertRole({
name: variables.user,
password: variables.password,
});
try { await postgresService.upsertDatabase({
const variables = await this.#getVariables(request); name: variables.name,
const postgresService = services.get(PostgresService); owner: variables.user,
await postgresService.upsertRole({ });
name: Buffer.from(variables.user!, 'base64').toString('utf-8'),
password: Buffer.from(variables.password!, 'base64').toString('utf-8'),
});
await postgresService.upsertDatabase({ await request.addEvent({
name: Buffer.from(variables.name!, 'base64').toString('utf-8'), type: 'Normal',
owner: Buffer.from(variables.user!, 'base64').toString('utf-8'), reason: 'DatabaseUpserted',
}); message: 'Database has been upserted',
action: 'UPSERT',
status.setCondition('Ready', { });
status: 'True',
reason: 'Ready',
message: 'Role created',
});
services.log.info('PostgresRole updated', { status });
return await status.save();
} catch (error) {
const status = await request.getStatus();
status.setCondition('Ready', {
status: 'False',
reason: 'Error',
message: error instanceof Error ? error.message : 'Unknown error',
});
services.log.error('Error updating PostgresRole', { error });
return await status.save();
}
}; };
} }

View File

@@ -1,10 +1,8 @@
import { Type } from '@sinclair/typebox'; import { Type } from '@sinclair/typebox';
import { ApiException, type V1Secret } from '@kubernetes/client-node';
import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts'; import { CustomResource, type CustomResourceHandlerOptions } from '../../custom-resource/custom-resource.base.ts';
import { K8sService } from '../../services/k8s.ts';
const stringValueSchema = Type.String({ const stringValueSchema = Type.Object({
key: Type.String(), key: Type.String(),
chars: Type.Optional(Type.String()), chars: Type.Optional(Type.String()),
length: Type.Optional(Type.Number()), length: Type.Optional(Type.Number()),
@@ -33,71 +31,18 @@ class SecretRequest extends CustomResource<typeof secretRequestSpec> {
}); });
} }
#createSecret = async (options: CustomResourceHandlerOptions<typeof secretRequestSpec>) => {
const { request, services } = options;
const { apiVersion, kind, spec, metadata } = request;
const { secretName = metadata.name } = spec;
const { namespace = 'default' } = metadata;
const k8sService = services.get(K8sService);
let current: V1Secret | undefined;
try {
current = await k8sService.api.readNamespacedSecret({
name: secretName,
namespace,
});
} catch (error) {
if (!(error instanceof ApiException && error.code === 404)) {
throw error;
}
}
if (current) {
services.log.debug('secret already exists', { current });
// TODO: Add update logic
return;
}
await k8sService.api.createNamespacedSecret({
namespace,
body: {
kind: 'Secret',
metadata: {
name: secretName,
namespace,
ownerReferences: [
{
apiVersion,
kind,
name: metadata.name,
uid: metadata.uid,
},
],
},
type: 'Opaque',
data: {
// TODO: generate data from spec
test: 'test',
},
},
});
};
public update = async (options: CustomResourceHandlerOptions<typeof secretRequestSpec>) => { public update = async (options: CustomResourceHandlerOptions<typeof secretRequestSpec>) => {
const { request } = options; const { request, ensureSecret } = options;
const status = await request.getStatus(); const { secretName = request.metadata.name } = request.spec;
try { const { namespace = request.metadata.namespace ?? 'default' } = request.metadata;
await this.#createSecret(options); await ensureSecret({
status.setCondition('Ready', { name: secretName,
status: 'True', namespace,
reason: 'SecretCreated', schema: Type.Object({}, { additionalProperties: true }),
message: 'Secret created', generator: async () => ({
}); hello: 'world',
return await status.save(); }),
} catch { });
status.setCondition('Ready', {
status: 'False',
reason: 'SecretNotCreated',
message: 'Secret not created',
});
}
}; };
} }

View File

@@ -1,13 +1,20 @@
import { type TSchema } from '@sinclair/typebox'; import { type Static, type TObject, type TSchema } from '@sinclair/typebox';
import { GROUP } from '../utils/consts.ts'; import { GROUP } from '../utils/consts.ts';
import type { Services } from '../utils/service.ts'; import type { Services } from '../utils/service.ts';
import { statusSchema } from './custom-resource.status.ts'; import { customResourceStatusSchema, type CustomResourceRequest } from './custom-resource.request.ts';
import type { CustomResourceRequest } from './custom-resource.request.ts';
type EnsureSecretOptions<T extends TObject> = {
schema: T;
name: string;
namespace: string;
generator: () => Promise<Static<T>>;
};
type CustomResourceHandlerOptions<TSpec extends TSchema> = { type CustomResourceHandlerOptions<TSpec extends TSchema> = {
request: CustomResourceRequest<TSpec>; request: CustomResourceRequest<TSpec>;
ensureSecret: <T extends TObject>(options: EnsureSecretOptions<T>) => Promise<Static<T>>;
services: Services; services: Services;
}; };
@@ -82,7 +89,7 @@ abstract class CustomResource<TSpec extends TSchema> {
type: 'object', type: 'object',
properties: { properties: {
spec: this.spec, spec: this.spec,
status: statusSchema, status: customResourceStatusSchema as ExpectedAny,
}, },
}, },
}, },
@@ -96,4 +103,4 @@ abstract class CustomResource<TSpec extends TSchema> {
}; };
} }
export { CustomResource, type CustomResourceConstructor, type CustomResourceHandlerOptions }; export { CustomResource, type CustomResourceConstructor, type CustomResourceHandlerOptions, type EnsureSecretOptions };

View File

@@ -1,14 +1,15 @@
import { ApiException, Watch } from '@kubernetes/client-node'; import { ApiException, Watch } from '@kubernetes/client-node';
import { type TObject } from '@sinclair/typebox';
import { K8sService } from '../services/k8s.ts'; import { K8sService } from '../services/k8s.ts';
import type { Services } from '../utils/service.ts'; import type { Services } from '../utils/service.ts';
import { isSchemaValid } from '../utils/schemas.ts';
import { type CustomResource } from './custom-resource.base.ts'; import { type CustomResource, type EnsureSecretOptions } from './custom-resource.base.ts';
import { CustomResourceRequest } from './custom-resource.request.ts'; import { CustomResourceRequest } from './custom-resource.request.ts';
class CustomResourceRegistry { class CustomResourceRegistry {
#services: Services; #services: Services;
#resources = new Set<CustomResource<any>>(); #resources = new Set<CustomResource<ExpectedAny>>();
#watchers = new Map<string, AbortController>(); #watchers = new Map<string, AbortController>();
constructor(services: Services) { constructor(services: Services) {
@@ -23,11 +24,11 @@ class CustomResourceRegistry {
return Array.from(this.#resources).find((r) => r.kind === kind); return Array.from(this.#resources).find((r) => r.kind === kind);
}; };
public register = (resource: CustomResource<any>) => { public register = (resource: CustomResource<ExpectedAny>) => {
this.#resources.add(resource); this.#resources.add(resource);
}; };
public unregister = (resource: CustomResource<any>) => { public unregister = (resource: CustomResource<ExpectedAny>) => {
this.#resources.delete(resource); this.#resources.delete(resource);
this.#watchers.forEach((controller, kind) => { this.#watchers.forEach((controller, kind) => {
if (kind === resource.kind) { if (kind === resource.kind) {
@@ -50,7 +51,70 @@ class CustomResourceRegistry {
} }
}; };
#onResourceEvent = async (type: string, obj: any) => { #ensureSecret =
(request: CustomResourceRequest<ExpectedAny>) =>
async <T extends TObject>(options: EnsureSecretOptions<T>) => {
const { schema, name, namespace, generator } = options;
const { metadata } = request;
const k8sService = this.#services.get(K8sService);
let exists = false;
try {
const secret = await k8sService.api.readNamespacedSecret({
name,
namespace,
});
exists = true;
if (secret?.data) {
const decoded = Object.fromEntries(
Object.entries(secret.data).map(([key, value]) => [key, Buffer.from(value, 'base64').toString('utf-8')]),
);
if (isSchemaValid(schema, decoded)) {
return decoded;
}
}
} catch (error) {
if (!(error instanceof ApiException && error.code === 404)) {
throw error;
}
}
const value = await generator();
const data = Object.fromEntries(
Object.entries(value).map(([key, value]) => [key, Buffer.from(value as string).toString('base64')]),
);
const body = {
kind: 'Secret',
metadata: {
name,
namespace,
ownerReferences: [
{
apiVersion: request.apiVersion,
kind: request.kind,
name: metadata.name,
uid: metadata.uid,
},
],
},
type: 'Opaque',
data,
};
if (exists) {
await k8sService.api.replaceNamespacedSecret({
name,
namespace,
body,
});
} else {
const response = await k8sService.api.createNamespacedSecret({
namespace,
body,
});
return response.data;
}
};
#onResourceEvent = async (type: string, obj: ExpectedAny) => {
const { kind } = obj; const { kind } = obj;
const crd = this.getByKind(kind); const crd = this.getByKind(kind);
if (!crd) { if (!crd) {
@@ -65,45 +129,101 @@ class CustomResourceRegistry {
}); });
const status = await request.getStatus(); const status = await request.getStatus();
if (status.observedGeneration === obj.metadata.generation) { if (status && (type === 'ADDED' || type === 'MODIFIED')) {
this.#services.log.debug('Skipping resource update', { if (status.observedGeneration === obj.metadata.generation) {
observedGeneration: status.observedGeneration, this.#services.log.debug('Skipping resource update', {
generation: obj.metadata.generation, kind,
}); name: obj.metadata.name,
return; namespace: obj.metadata.namespace,
observedGeneration: status.observedGeneration,
generation: obj.metadata.generation,
});
return;
}
}
this.#services.log.debug('Updating resource', {
type,
kind,
name: obj.metadata.name,
namespace: obj.metadata.namespace,
observedGeneration: status?.observedGeneration,
generation: obj.metadata.generation,
});
if (type === 'ADDED' || type === 'MODIFIED') {
await request.markSeen();
} }
if (type === 'ADDED' && crd.create) { if (type === 'ADDED' && crd.create) {
handler = crd.create; handler = crd.create;
} }
await handler?.({ try {
request, await handler?.({
services: this.#services, request,
}); services: this.#services,
ensureSecret: this.#ensureSecret(request) as ExpectedAny,
});
if (type === 'ADDED' || type === 'MODIFIED') {
await request.setCondition({
type: 'Ready',
status: 'True',
message: 'Resource created',
});
}
} catch (error) {
let message = 'Unknown error';
if (error instanceof ApiException) {
message = error.body;
this.#services.log.error('Error handling resource', { reason: error.body });
} else if (error instanceof Error) {
message = error.message;
this.#services.log.error('Error handling resource', { reason: error.message });
} else {
message = String(error);
this.#services.log.error('Error handling resource', { reason: String(error) });
}
if (type === 'ADDED' || type === 'MODIFIED') {
await request.setCondition({
type: 'Ready',
status: 'False',
reason: 'Error',
message,
});
}
}
}; };
#onError = (error: any) => { #onError = (error: ExpectedAny) => {
console.error(error); this.#services.log.error('Error watching resource', { error });
}; };
public install = async (replace = false) => { public install = async (replace = false) => {
const k8sService = this.#services.get(K8sService); const k8sService = this.#services.get(K8sService);
for (const crd of this.#resources) { for (const crd of this.#resources) {
const manifest = crd.toManifest();
try { try {
await k8sService.extensionsApi.createCustomResourceDefinition({ const manifest = crd.toManifest();
body: manifest, try {
}); await k8sService.extensionsApi.createCustomResourceDefinition({
} catch (error) { body: manifest,
if (error instanceof ApiException && error.code === 409) { });
if (replace) { } catch (error) {
await k8sService.extensionsApi.patchCustomResourceDefinition({ if (error instanceof ApiException && error.code === 409) {
name: crd.name, if (replace) {
body: [{ op: 'replace', path: '/spec', value: manifest.spec }], await k8sService.extensionsApi.patchCustomResourceDefinition({
}); name: crd.name,
body: [{ op: 'replace', path: '/spec', value: manifest.spec }],
});
}
continue;
} }
continue; throw error;
}
} catch (error) {
if (error instanceof ApiException) {
throw new Error(`Failed to install ${crd.kind}: ${error.body}`);
} }
throw error; throw error;
} }

View File

@@ -1,15 +1,15 @@
import type { Static, TSchema } from '@sinclair/typebox'; import { Type, type Static, type TSchema } from '@sinclair/typebox';
import { ApiException, PatchStrategy, setHeaderOptions } from '@kubernetes/client-node'; import { ApiException, PatchStrategy, setHeaderOptions, V1MicroTime } from '@kubernetes/client-node';
import type { Services } from '../utils/service.ts'; import type { Services } from '../utils/service.ts';
import { K8sService } from '../services/k8s.ts'; import { K8sService } from '../services/k8s.ts';
import { GROUP } from '../utils/consts.ts';
import { CustomResourceRegistry } from './custom-resource.registry.ts'; import { CustomResourceRegistry } from './custom-resource.registry.ts';
import { CustomResourceStatus, type CustomResourceStatusType } from './custom-resource.status.ts';
type CustomResourceRequestOptions = { type CustomResourceRequestOptions = {
type: 'ADDED' | 'DELETED' | 'MODIFIED'; type: 'ADDED' | 'DELETED' | 'MODIFIED';
manifest: any; manifest: ExpectedAny;
services: Services; services: Services;
}; };
@@ -24,6 +24,30 @@ type CustomResourceRequestMetadata = Record<string, string> & {
generation: number; generation: number;
}; };
type CustomResourceEvent = {
reason: string;
message: string;
action: string;
type: 'Normal' | 'Warning' | 'Error';
};
const customResourceStatusSchema = Type.Object({
observedGeneration: Type.Number(),
conditions: Type.Array(
Type.Object({
type: Type.String(),
status: Type.String({
enum: ['True', 'False', 'Unknown'],
}),
lastTransitionTime: Type.String({ format: 'date-time' }),
reason: Type.Optional(Type.String()),
message: Type.Optional(Type.String()),
}),
),
});
type CustomResourceStatus = Static<typeof customResourceStatusSchema>;
class CustomResourceRequest<TSpec extends TSchema> { class CustomResourceRequest<TSpec extends TSchema> {
#options: CustomResourceRequestOptions; #options: CustomResourceRequestOptions;
@@ -59,10 +83,10 @@ class CustomResourceRequest<TSpec extends TSchema> {
return this.#options.manifest.metadata; return this.#options.manifest.metadata;
} }
public isOwnerOf = (manifest: any) => { public isOwnerOf = (manifest: ExpectedAny) => {
const ownerRef = manifest?.metadata?.ownerReferences || []; const ownerRef = manifest?.metadata?.ownerReferences || [];
return ownerRef.some( return ownerRef.some(
(ref: any) => (ref: ExpectedAny) =>
ref.apiVersion === this.apiVersion && ref.apiVersion === this.apiVersion &&
ref.kind === this.kind && ref.kind === this.kind &&
ref.name === this.metadata.name && ref.name === this.metadata.name &&
@@ -70,11 +94,73 @@ class CustomResourceRequest<TSpec extends TSchema> {
); );
}; };
public setStatus = async (status: CustomResourceStatusType) => { public markSeen = async () => {
const { manifest } = this.#options;
await this.setStatus({
observedGeneration: manifest.metadata.generation,
});
};
public setCondition = async (condition: Omit<CustomResourceStatus['conditions'][number], 'lastTransitionTime'>) => {
const fullCondition = {
...condition,
lastTransitionTime: new Date().toISOString(),
};
const current = await this.getCurrent();
const conditions: CustomResourceStatus['conditions'] = current?.status?.conditions || [];
const index = conditions.findIndex((c) => c.type === condition.type);
if (index === -1) {
conditions.push(fullCondition);
} else {
conditions[index] = fullCondition;
}
await this.setStatus({
conditions,
});
};
public getStatus = async () => {
const current = await this.getCurrent();
return current?.status as CustomResourceStatus | undefined;
};
public addEvent = async (event: CustomResourceEvent) => {
const { manifest, services } = this.#options;
const k8sService = services.get(K8sService);
await k8sService.eventsApi.createNamespacedEvent({
namespace: manifest.metadata.namespace,
body: {
kind: 'Event',
metadata: {
name: `${manifest.metadata.name}-${Date.now()}`,
namespace: manifest.metadata.namespace,
},
eventTime: new V1MicroTime(),
note: event.message,
action: event.action,
reason: event.reason,
type: event.type,
reportingController: GROUP,
reportingInstance: manifest.metadata.name,
regarding: {
apiVersion: manifest.apiVersion,
resourceVersion: manifest.metadata.resourceVersion,
kind: manifest.kind,
name: manifest.metadata.name,
namespace: manifest.metadata.namespace,
uid: manifest.metadata.uid,
},
},
});
};
public setStatus = async (status: Partial<CustomResourceStatus>) => {
const { manifest, services } = this.#options; const { manifest, services } = this.#options;
const { kind, metadata } = manifest; const { kind, metadata } = manifest;
const registry = services.get(CustomResourceRegistry); const registry = services.get(CustomResourceRegistry);
const crd = registry.getByKind(kind); const crd = registry.getByKind(kind);
const current = await this.getCurrent();
if (!crd) { if (!crd) {
throw new Error(`Custom resource ${kind} not found`); throw new Error(`Custom resource ${kind} not found`);
} }
@@ -90,7 +176,14 @@ class CustomResourceRequest<TSpec extends TSchema> {
namespace, namespace,
plural: crd.names.plural, plural: crd.names.plural,
name, name,
body: { status }, body: {
status: {
observedGeneration: manifest.metadata.generation,
conditions: current?.status?.conditions || [],
...current?.status,
...status,
},
},
fieldValidation: 'Strict', fieldValidation: 'Strict',
}, },
setHeaderOptions('Content-Type', PatchStrategy.MergePatch), setHeaderOptions('Content-Type', PatchStrategy.MergePatch),
@@ -119,7 +212,7 @@ class CustomResourceRequest<TSpec extends TSchema> {
kind: string; kind: string;
metadata: CustomResourceRequestMetadata; metadata: CustomResourceRequestMetadata;
spec: Static<TSpec>; spec: Static<TSpec>;
status: CustomResourceStatusType; status: CustomResourceStatus;
}; };
} catch (error) { } catch (error) {
if (error instanceof ApiException && error.code === 404) { if (error instanceof ApiException && error.code === 404) {
@@ -128,25 +221,6 @@ class CustomResourceRequest<TSpec extends TSchema> {
throw error; throw error;
} }
}; };
public getStatus = async () => {
const resource = await this.getCurrent();
if (!resource || !resource.status) {
return new CustomResourceStatus({
status: {
observedGeneration: 0,
conditions: [],
},
generation: 0,
save: this.setStatus,
});
}
return new CustomResourceStatus({
status: { ...resource.status, observedGeneration: resource.status.observedGeneration },
generation: resource.metadata.generation,
save: this.setStatus,
});
};
} }
export { CustomResourceRequest }; export { CustomResourceRequest, customResourceStatusSchema };

View File

@@ -1,85 +0,0 @@
import { Type, type Static } from '@sinclair/typebox';
type CustomResourceStatusType = Static<typeof statusSchema>;
const statusSchema = Type.Object({
observedGeneration: Type.Number(),
conditions: Type.Array(
Type.Object({
type: Type.String(),
status: Type.String({
enum: ['True', 'False', 'Unknown'],
}),
lastTransitionTime: Type.String(),
reason: Type.String(),
message: Type.String(),
}),
),
});
type CustomResourceStatusOptions = {
status?: CustomResourceStatusType;
generation: number;
save: (status: CustomResourceStatusType) => Promise<void>;
};
class CustomResourceStatus {
#status: CustomResourceStatusType;
#generation: number;
#save: (status: CustomResourceStatusType) => Promise<void>;
constructor(options: CustomResourceStatusOptions) {
this.#save = options.save;
this.#status = {
observedGeneration: options.status?.observedGeneration ?? 0,
conditions: options.status?.conditions ?? [],
};
this.#generation = options.generation;
}
public get generation() {
return this.#generation;
}
public get observedGeneration() {
return this.#status.observedGeneration;
}
public set observedGeneration(observedGeneration: number) {
this.#status.observedGeneration = observedGeneration;
}
public getCondition = (type: string) => {
return this.#status.conditions?.find((condition) => condition.type === type)?.status;
};
public setCondition = (
type: string,
condition: Omit<CustomResourceStatusType['conditions'][number], 'type' | 'lastTransitionTime'>,
) => {
const currentCondition = this.getCondition(type);
const newCondition = {
...condition,
type,
lastTransitionTime: new Date().toISOString(),
};
if (currentCondition) {
this.#status.conditions = this.#status.conditions.map((c) => (c.type === type ? newCondition : c));
} else {
this.#status.conditions.push(newCondition);
}
};
public save = async () => {
await this.#save({
...this.#status,
observedGeneration: this.#generation,
});
};
public toJSON = () => {
return this.#status;
};
}
export { CustomResourceStatus, statusSchema, type CustomResourceStatusType };

View File

@@ -1,11 +1,44 @@
import 'dotenv/config';
import { ApiException } from '@kubernetes/client-node';
import { CustomResourceRegistry } from './custom-resource/custom-resource.registry.ts'; import { CustomResourceRegistry } from './custom-resource/custom-resource.registry.ts';
import { Services } from './utils/service.ts'; import { Services } from './utils/service.ts';
import { SecretRequest } from './crds/secrets/secrets.request.ts'; import { SecretRequest } from './crds/secrets/secrets.request.ts';
import { PostgresDatabase } from './crds/postgres/postgres.database.ts'; import { PostgresDatabase } from './crds/postgres/postgres.database.ts';
import { AuthentikService } from './services/authentik/authentik.service.ts';
import { AuthentikClient } from './crds/authentik/client/client.ts';
const services = new Services(); const services = new Services();
const registry = services.get(CustomResourceRegistry); const registry = services.get(CustomResourceRegistry);
registry.register(new SecretRequest()); registry.register(new SecretRequest());
registry.register(new PostgresDatabase()); registry.register(new PostgresDatabase());
registry.register(new AuthentikClient());
await registry.install(true); await registry.install(true);
await registry.watch(); await registry.watch();
const authentikService = services.get(AuthentikService);
await authentikService.upsertClient({
name: 'foo',
secret: 'foo',
redirectUris: [{ url: 'http://localhost:3000/api/auth/callback', matchingMode: 'strict' }],
});
process.on('uncaughtException', (error) => {
console.log('UNCAUGHT EXCEPTION');
if (error instanceof ApiException) {
return console.error(error.body);
}
console.error(error);
});
process.on('unhandledRejection', (error) => {
console.log('UNHANDLED REJECTION');
if (error instanceof Error) {
// show stack trace
console.error(error.stack);
}
if (error instanceof ApiException) {
return console.error(error.body);
}
console.error(error);
});

View File

@@ -0,0 +1,216 @@
import type { Services } from '../../utils/service.ts';
import { ConfigService } from '../config/config.ts';
import { createAuthentikClient, type AuthentikClient } from '../../clients/authentik/authentik.ts';
import type { UpsertClientRequest, UpsertGroupRequest } from './authentik.types.ts';
const DEFAULT_AUTHORIZATION_FLOW = 'default-provider-authorization-implicit-consent';
const DEFAULT_INVALIDATION_FLOW = 'default-invalidation-flow';
const DEFAULT_SCOPES = ['openid', 'email', 'profile', 'offline_access'];
class AuthentikService {
#client: AuthentikClient;
#services: Services;
constructor(services: Services) {
const config = services.get(ConfigService);
this.#client = createAuthentikClient({
baseUrl: new URL('api/v3', config.authentik.url).toString(),
token: config.authentik.token,
});
this.#services = services;
}
public get url() {
const config = this.#services.get(ConfigService);
return config.authentik.url;
}
#upsertApplication = async (request: UpsertClientRequest, provider: number, pk?: string) => {
if (!pk) {
return await this.#client.core.coreApplicationsCreate({
applicationRequest: {
name: request.name,
slug: request.name,
provider,
},
});
}
return await this.#client.core.coreApplicationsUpdate({
slug: request.name,
applicationRequest: {
name: request.name,
slug: request.name,
provider,
},
});
};
#upsertProvider = async (request: UpsertClientRequest, pk?: number) => {
const flows = await this.getFlows();
const authorizationFlow = flows.results.find(
(flow) => flow.slug === (request.flows?.authorization ?? DEFAULT_AUTHORIZATION_FLOW),
);
const invalidationFlow = flows.results.find(
(flow) => flow.slug === (request.flows?.invalidation ?? DEFAULT_INVALIDATION_FLOW),
);
if (!authorizationFlow || !invalidationFlow) {
throw new Error('Authorization and invalidation flows not found');
}
const scopes = await this.getScopePropertyMappings();
const scopePropertyMapping = (request.scopes ?? DEFAULT_SCOPES)
.map((scope) => scopes.results.find((mapping) => mapping.scopeName === scope)?.pk)
.filter(Boolean) as string[];
if (!pk) {
return await this.#client.providers.providersOauth2Create({
oAuth2ProviderRequest: {
name: request.name,
clientId: request.name,
clientSecret: request.secret,
redirectUris: request.redirectUris,
authorizationFlow: authorizationFlow.pk,
invalidationFlow: invalidationFlow.pk,
propertyMappings: scopePropertyMapping,
clientType: request.clientType,
subMode: request.subMode,
accessCodeValidity: request.timing?.accessCodeValidity,
accessTokenValidity: request.timing?.accessTokenValidity,
refreshTokenValidity: request.timing?.refreshTokenValidity,
},
});
}
return await this.#client.providers.providersOauth2Update({
id: pk,
oAuth2ProviderRequest: {
name: request.name,
clientId: request.name,
clientSecret: request.secret,
redirectUris: request.redirectUris,
authorizationFlow: authorizationFlow.pk,
invalidationFlow: invalidationFlow.pk,
propertyMappings: scopePropertyMapping,
clientType: request.clientType,
subMode: request.subMode,
accessCodeValidity: request.timing?.accessCodeValidity,
accessTokenValidity: request.timing?.accessTokenValidity,
refreshTokenValidity: request.timing?.refreshTokenValidity,
},
});
};
public getGroupFromName = async (name: string) => {
const groups = await this.#client.core.coreGroupsList({
search: name,
});
return groups.results.find((group) => group.name === name);
};
public getScopePropertyMappings = async () => {
const mappings = await this.#client.propertymappings.propertymappingsProviderScopeList({});
return mappings;
};
public getApplicationFromSlug = async (slug: string) => {
const applications = await this.#client.core.coreApplicationsList({
search: slug,
});
const application = applications.results.find((app) => app.slug === slug);
return application;
};
public getProviderFromClientId = async (clientId: string) => {
const providers = await this.#client.providers.providersOauth2List({
clientId,
});
return providers.results.find((provider) => provider.clientId === clientId);
};
public getFlows = async () => {
const flows = await this.#client.flows.flowsInstancesList();
return flows;
};
public upsertClient = async (request: UpsertClientRequest) => {
try {
let provider = await this.getProviderFromClientId(request.name);
provider = await this.#upsertProvider(request, provider?.pk);
let application = await this.getApplicationFromSlug(request.name);
application = await this.#upsertApplication(request, provider.pk, application?.pk);
const config = {
provider: {
id: provider.pk,
name: provider.name,
clientId: provider.clientId,
clientSecret: provider.clientSecret,
clientType: provider.clientType,
subMode: provider.subMode,
redirectUris: provider.redirectUris,
scopes: provider.propertyMappings,
timing: {
accessCodeValidity: provider.accessCodeValidity,
accessTokenValidity: provider.accessTokenValidity,
refreshTokenValidity: provider.refreshTokenValidity,
},
},
application: {
id: application.pk,
name: application.name,
slug: application.slug,
provider: provider.pk,
},
urls: {
configuration: new URL(
`/application/o/${provider.name}/.well-known/openid-configuration`,
this.url,
).toString(),
configurationIssuer: new URL(`/application/o/${provider.name}/`, this.url).toString(),
authorization: new URL(`/application/o/${provider.name}/authorize/`, this.url).toString(),
token: new URL(`/application/o/${provider.name}/token/`, this.url).toString(),
userinfo: new URL(`/application/o/${provider.name}/userinfo/`, this.url).toString(),
endSession: new URL(`/application/o/${provider.name}/end-session/`, this.url).toString(),
jwks: new URL(`/application/o/${provider.name}/jwks/`, this.url).toString(),
},
};
return { provider, application, config };
} catch (error: ExpectedAny) {
if ('response' in error) {
throw new Error(await error.response.text());
}
throw error;
}
};
public deleteClient = async (name: string) => {
const provider = await this.getProviderFromClientId(name);
if (provider) {
await this.#client.providers.providersOauth2Destroy({ id: provider.pk });
}
const application = await this.getApplicationFromSlug(name);
if (application) {
await this.#client.core.coreApplicationsDestroy({ slug: application.name });
}
};
public upsertGroup = async (request: UpsertGroupRequest) => {
const group = await this.getGroupFromName(request.name);
if (!group) {
await this.#client.core.coreGroupsCreate({
groupRequest: {
name: request.name,
attributes: request.attributes,
},
});
} else {
await this.#client.core.coreGroupsUpdate({
groupUuid: group.pk,
groupRequest: {
name: request.name,
attributes: request.attributes,
},
});
}
};
}
export { AuthentikService };

View File

@@ -0,0 +1,29 @@
import type { ClientTypeEnum, SubModeEnum } from '@goauthentik/api';
type UpsertClientRequest = {
name: string;
secret: string;
scopes?: string[];
flows?: {
authorization: string;
invalidation: string;
};
clientType?: ClientTypeEnum;
subMode?: SubModeEnum;
redirectUris: {
url: string;
matchingMode: 'strict' | 'regex';
}[];
timing?: {
accessCodeValidity?: string;
accessTokenValidity?: string;
refreshTokenValidity?: string;
};
};
type UpsertGroupRequest = {
name: string;
attributes?: Record<string, string[]>;
};
export type { UpsertClientRequest, UpsertGroupRequest };

View File

@@ -11,6 +11,17 @@ class ConfigService {
return { host, user, password, port }; return { host, user, password, port };
} }
public get authentik() {
const url = process.env.AUTHENTIK_URL;
const token = process.env.AUTHENTIK_TOKEN;
if (!url || !token) {
throw new Error('AUTHENTIK_URL and AUTHENTIK_TOKEN must be set');
}
return { url, token };
}
} }
export { ConfigService }; export { ConfigService };

View File

@@ -1,10 +1,19 @@
import { KubeConfig, CoreV1Api, ApiextensionsV1Api, CustomObjectsApi } from '@kubernetes/client-node'; import {
KubeConfig,
CoreV1Api,
ApiextensionsV1Api,
CustomObjectsApi,
EventsV1Api,
KubernetesObjectApi,
} from '@kubernetes/client-node';
class K8sService { class K8sService {
#kc: KubeConfig; #kc: KubeConfig;
#k8sApi: CoreV1Api; #k8sApi: CoreV1Api;
#k8sExtensionsApi: ApiextensionsV1Api; #k8sExtensionsApi: ApiextensionsV1Api;
#k8sCustomObjectsApi: CustomObjectsApi; #k8sCustomObjectsApi: CustomObjectsApi;
#k8sEventsApi: EventsV1Api;
#k8sObjectsApi: KubernetesObjectApi;
constructor() { constructor() {
this.#kc = new KubeConfig(); this.#kc = new KubeConfig();
@@ -12,6 +21,8 @@ class K8sService {
this.#k8sApi = this.#kc.makeApiClient(CoreV1Api); this.#k8sApi = this.#kc.makeApiClient(CoreV1Api);
this.#k8sExtensionsApi = this.#kc.makeApiClient(ApiextensionsV1Api); this.#k8sExtensionsApi = this.#kc.makeApiClient(ApiextensionsV1Api);
this.#k8sCustomObjectsApi = this.#kc.makeApiClient(CustomObjectsApi); this.#k8sCustomObjectsApi = this.#kc.makeApiClient(CustomObjectsApi);
this.#k8sEventsApi = this.#kc.makeApiClient(EventsV1Api);
this.#k8sObjectsApi = this.#kc.makeApiClient(KubernetesObjectApi);
} }
public get config() { public get config() {
@@ -29,6 +40,14 @@ class K8sService {
public get customObjectsApi() { public get customObjectsApi() {
return this.#k8sCustomObjectsApi; return this.#k8sCustomObjectsApi;
} }
public get eventsApi() {
return this.#k8sEventsApi;
}
public get objectsApi() {
return this.#k8sObjectsApi;
}
} }
export { K8sService }; export { K8sService };

9
src/utils/schemas.ts Normal file
View File

@@ -0,0 +1,9 @@
import type { Static, TSchema } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';
const isSchemaValid = <T extends TSchema>(schema: T, data: unknown): data is Static<T> => {
const compiler = TypeCompiler.Compile(schema);
return compiler.Check(data);
};
export { isSchemaValid };

6
src/utils/types.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
declare global {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ExpectedAny = any;
}
export {};

View File

@@ -0,0 +1,8 @@
apiVersion: homelab.mortenolsen.pro/v1
kind: AuthentikClient
metadata:
name: foobas
spec:
redirectUris:
- url: http://localhost:3000/api/auth/callback
matchingMode: strict

View File

@@ -0,0 +1,5 @@
apiVersion: 'homelab.mortenolsen.pro/v1'
kind: 'PostgresDatabase'
metadata:
name: 'test2'
namespace: 'playground'

View File

@@ -16,9 +16,8 @@
// Best practices // Best practices
"strict": true, "strict": true,
"skipLibCheck": true, "skipLibCheck": true,
"noFallthroughCasesInSwitch": true, // "noUncheckedIndexedAccess": true,
"noUncheckedIndexedAccess": true, // "noImplicitOverride": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default) // Some stricter flags (disabled by default)
"noUnusedLocals": false, "noUnusedLocals": false,