From 2ccc07fec87bb34ee39bb7a171eade98776698d0 Mon Sep 17 00:00:00 2001 From: Agents Agent Date: Thu, 2 Jul 2026 19:40:17 +0000 Subject: [PATCH] ci: standardize node workflow --- .github/workflows/ci.yml | 38 +- .prettierignore | 16 + .prettierrc.json | 9 + eslint.config.mjs | 37 + model/personal-stack.sample.json | 91 +- package-lock.json | 1244 ++++++++++++++++++++++- package.json | 10 +- packages/typescript/tsconfig.build.json | 4 +- packages/typescript/tsconfig.json | 5 +- schema/authz-model.schema.json | 92 +- tools/generator/check-generated.mjs | 42 +- tools/generator/generate.mjs | 334 +++--- tools/generator/validate.mjs | 137 ++- 13 files changed, 1635 insertions(+), 424 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 eslint.config.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fac250f..546858f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,37 +9,21 @@ name: 'CI' permissions: contents: 'read' + packages: 'read' jobs: vocabulary: name: 'Validate Vocabulary' - runs-on: 'ubuntu-latest' - steps: - - name: 'Checkout' - uses: 'actions/checkout@v6' - - - name: 'Setup Node' - uses: 'actions/setup-node@v6' - with: - node-version: '22' - cache: 'npm' - - - name: 'Install npm dependencies' - run: 'npm ci' - env: - NODE_AUTH_TOKEN: '${{ secrets.GITHUB_TOKEN }}' - - - name: 'Validate schema and sample' - run: 'npm run validate' - - - name: 'Run generator on sample' - run: 'npm run generate -- --output-root "$RUNNER_TEMP/authz-generated"' - - - name: 'Check generated output' - run: 'npm run check:generated' - - - name: 'Typecheck TypeScript constants' - run: 'npm run typecheck' + uses: 'JorisJonkers-dev/github-workflows/.github/workflows/node-ci.yml@v0.10.2' + with: + node-version: '22' + package-manager: 'npm' + install-command: 'npm ci' + typecheck-command: 'npm run typecheck' + build-command: 'npm run build:typescript' + package-check-command: 'npm run validate && npm run generate -- --output-root "$RUNNER_TEMP/authz-generated" && npm run check:generated' + secrets: + packages-token: '${{ secrets.GITHUB_TOKEN }}' kotlin: name: 'Build Kotlin Artifact' diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..14ff4a8 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,16 @@ +# Vendored reusable workflow actions tree checked out by shared CI. +.github-workflows + +# Release metadata and generated files not owned by hand formatting. +.github/workflows +.release-please-manifest.json +CHANGELOG.md +packages/kotlin/src/generated +packages/typescript/src/generated + +# Build output / dependencies. +build +coverage +dist +node_modules +packages/typescript/dist diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..40268d2 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "tabWidth": 2, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..625ea30 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,37 @@ +import js from '@eslint/js' +import globals from 'globals' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { + ignores: [ + '.github-workflows/**', + 'build/**', + 'dist/**', + 'node_modules/**', + 'packages/kotlin/src/generated/**', + 'packages/typescript/dist/**', + 'packages/typescript/src/generated/**', + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{js,mjs,ts}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: globals.node, + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + }, + }, +) diff --git a/model/personal-stack.sample.json b/model/personal-stack.sample.json index f6da6e3..0eb7a7e 100644 --- a/model/personal-stack.sample.json +++ b/model/personal-stack.sample.json @@ -57,10 +57,7 @@ "name": "roles", "channel": "jwt-claim", "valueShape": "authority-array", - "allowedAuthorityFormats": [ - "role", - "service-permission" - ], + "allowedAuthorityFormats": ["role", "service-permission"], "description": "Token claim carrying role and service-permission authorities.", "namespace": "shared", "owner": "authz-model", @@ -181,10 +178,7 @@ "name": "X-User-Roles", "channel": "http-header", "valueShape": "comma-separated-authorities", - "allowedAuthorityFormats": [ - "role", - "service-permission" - ], + "allowedAuthorityFormats": ["role", "service-permission"], "description": "Forwarded identity header carrying comma-separated authorities.", "namespace": "shared", "owner": "authz-model", @@ -433,9 +427,7 @@ "id": "personal-stack.host-gate.vault", "key": "VAULT", "permissionKey": "VAULT", - "aliases": [ - "vault" - ], + "aliases": ["vault"], "description": "Host aliases requiring the Vault service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -446,9 +438,7 @@ "id": "personal-stack.host-gate.mail", "key": "MAIL", "permissionKey": "MAIL", - "aliases": [ - "stalwart" - ], + "aliases": ["stalwart"], "description": "Host aliases requiring the mail service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -459,9 +449,7 @@ "id": "personal-stack.host-gate.n8n", "key": "N8N", "permissionKey": "N8N", - "aliases": [ - "n8n" - ], + "aliases": ["n8n"], "description": "Host aliases requiring the n8n service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -472,9 +460,7 @@ "id": "personal-stack.host-gate.grafana", "key": "GRAFANA", "permissionKey": "GRAFANA", - "aliases": [ - "grafana" - ], + "aliases": ["grafana"], "description": "Host aliases requiring the Grafana service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -485,9 +471,7 @@ "id": "personal-stack.host-gate.dashboard", "key": "DASHBOARD", "permissionKey": "DASHBOARD", - "aliases": [ - "dashboard" - ], + "aliases": ["dashboard"], "description": "Host aliases requiring the dashboard service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -498,9 +482,7 @@ "id": "personal-stack.host-gate.traefik", "key": "TRAEFIK", "permissionKey": "TRAEFIK", - "aliases": [ - "traefik" - ], + "aliases": ["traefik"], "description": "Host aliases requiring the Traefik service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -511,9 +493,7 @@ "id": "personal-stack.host-gate.rabbitmq", "key": "RABBITMQ", "permissionKey": "RABBITMQ", - "aliases": [ - "rabbitmq" - ], + "aliases": ["rabbitmq"], "description": "Host aliases requiring the RabbitMQ service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -524,10 +504,7 @@ "id": "personal-stack.host-gate.assistant", "key": "ASSISTANT", "permissionKey": "ASSISTANT", - "aliases": [ - "assistant", - "assistant-ws" - ], + "aliases": ["assistant", "assistant-ws"], "description": "Host aliases requiring the assistant service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -538,9 +515,7 @@ "id": "personal-stack.host-gate.status", "key": "STATUS", "permissionKey": "STATUS", - "aliases": [ - "status" - ], + "aliases": ["status"], "description": "Host aliases requiring the status service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -551,9 +526,7 @@ "id": "personal-stack.host-gate.jellyfin", "key": "JELLYFIN", "permissionKey": "JELLYFIN", - "aliases": [ - "jellyfin" - ], + "aliases": ["jellyfin"], "description": "Host aliases requiring the Jellyfin service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -564,9 +537,7 @@ "id": "personal-stack.host-gate.jellyseerr", "key": "JELLYSEERR", "permissionKey": "JELLYSEERR", - "aliases": [ - "jellyseerr" - ], + "aliases": ["jellyseerr"], "description": "Host aliases requiring the Jellyseerr service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -577,9 +548,7 @@ "id": "personal-stack.host-gate.sonarr", "key": "SONARR", "permissionKey": "SONARR", - "aliases": [ - "sonarr" - ], + "aliases": ["sonarr"], "description": "Host aliases requiring the Sonarr service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -590,9 +559,7 @@ "id": "personal-stack.host-gate.radarr", "key": "RADARR", "permissionKey": "RADARR", - "aliases": [ - "radarr" - ], + "aliases": ["radarr"], "description": "Host aliases requiring the Radarr service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -603,9 +570,7 @@ "id": "personal-stack.host-gate.bazarr", "key": "BAZARR", "permissionKey": "BAZARR", - "aliases": [ - "bazarr" - ], + "aliases": ["bazarr"], "description": "Host aliases requiring the Bazarr service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -616,9 +581,7 @@ "id": "personal-stack.host-gate.prowlarr", "key": "PROWLARR", "permissionKey": "PROWLARR", - "aliases": [ - "prowlarr" - ], + "aliases": ["prowlarr"], "description": "Host aliases requiring the Prowlarr service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -629,9 +592,7 @@ "id": "personal-stack.host-gate.qbittorrent", "key": "QBITTORRENT", "permissionKey": "QBITTORRENT", - "aliases": [ - "qbittorrent" - ], + "aliases": ["qbittorrent"], "description": "Host aliases requiring the qBittorrent service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -642,9 +603,7 @@ "id": "personal-stack.host-gate.immich", "key": "IMMICH", "permissionKey": "IMMICH", - "aliases": [ - "immich" - ], + "aliases": ["immich"], "description": "Host aliases requiring the Immich service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -655,9 +614,7 @@ "id": "personal-stack.host-gate.adguard", "key": "ADGUARD", "permissionKey": "ADGUARD", - "aliases": [ - "adguard" - ], + "aliases": ["adguard"], "description": "Host aliases requiring the AdGuard service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -668,9 +625,7 @@ "id": "personal-stack.host-gate.wolf", "key": "WOLF", "permissionKey": "WOLF", - "aliases": [ - "wolf" - ], + "aliases": ["wolf"], "description": "Host aliases requiring the Wolf service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", @@ -681,9 +636,7 @@ "id": "personal-stack.host-gate.knowledge-api", "key": "KNOWLEDGE_API", "permissionKey": "KNOWLEDGE_API", - "aliases": [ - "kb" - ], + "aliases": ["kb"], "description": "Host aliases requiring the knowledge API service permission.", "namespace": "personal-stack", "owner": "personal-stack/auth-api", diff --git a/package-lock.json b/package-lock.json index 5563595..5faf562 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,14 +12,487 @@ "packages/typescript" ], "devDependencies": { + "@eslint/js": "^10.0.1", "ajv": "^8.17.1", - "typescript": "^5.9.3" + "eslint": "^10.6.0", + "globals": "^17.7.0", + "prettier": "^3.9.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.62.1" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@jorisjonkers-dev/authz-model": { "resolved": "packages/typescript", "link": true }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", + "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/type-utils": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.62.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", + "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", + "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.62.1", + "@typescript-eslint/types": "^8.62.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", + "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", + "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", + "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", + "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", + "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.62.1", + "@typescript-eslint/tsconfig-utils": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", + "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", + "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -37,6 +510,261 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -44,6 +772,20 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", @@ -61,6 +803,158 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -68,6 +962,202 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", + "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -78,6 +1168,85 @@ "node": ">=0.10.0" } }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -92,6 +1261,79 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz", + "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.62.1", + "@typescript-eslint/parser": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/typescript": { "name": "@jorisjonkers-dev/authz-model", "version": "0.0.0", diff --git a/package.json b/package.json index 02c0a47..4befc5a 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,19 @@ "validate": "node tools/generator/validate.mjs", "generate": "node tools/generator/generate.mjs", "check:generated": "node tools/generator/check-generated.mjs", + "lint": "eslint . --max-warnings=0", + "format": "prettier --write .", + "format:check": "prettier --check .", "typecheck": "npm --workspace packages/typescript run typecheck", "build:typescript": "npm --workspace packages/typescript run build" }, "devDependencies": { + "@eslint/js": "^10.0.1", "ajv": "^8.17.1", - "typescript": "^5.9.3" + "eslint": "^10.6.0", + "globals": "^17.7.0", + "prettier": "^3.9.4", + "typescript": "^5.9.3", + "typescript-eslint": "^8.62.1" } } diff --git a/packages/typescript/tsconfig.build.json b/packages/typescript/tsconfig.build.json index c07f8cb..874e3fd 100644 --- a/packages/typescript/tsconfig.build.json +++ b/packages/typescript/tsconfig.build.json @@ -1,6 +1,4 @@ { "extends": "./tsconfig.json", - "include": [ - "src/generated/authz-vocabulary.ts" - ] + "include": ["src/generated/authz-vocabulary.ts"] } diff --git a/packages/typescript/tsconfig.json b/packages/typescript/tsconfig.json index 85134ed..66106e9 100644 --- a/packages/typescript/tsconfig.json +++ b/packages/typescript/tsconfig.json @@ -9,8 +9,5 @@ "target": "ES2022", "verbatimModuleSyntax": true }, - "include": [ - "src/generated/authz-vocabulary.ts", - "src/generated/authz-vocabulary.test-d.ts" - ] + "include": ["src/generated/authz-vocabulary.ts", "src/generated/authz-vocabulary.test-d.ts"] } diff --git a/schema/authz-model.schema.json b/schema/authz-model.schema.json index 77a85ee..29e800f 100644 --- a/schema/authz-model.schema.json +++ b/schema/authz-model.schema.json @@ -37,11 +37,7 @@ "authorityFormats": { "type": "object", "additionalProperties": false, - "required": [ - "rolePrefix", - "servicePermissionPrefix", - "rolesHeaderSeparator" - ], + "required": ["rolePrefix", "servicePermissionPrefix", "rolesHeaderSeparator"], "properties": { "rolePrefix": { "type": "string", @@ -60,19 +56,12 @@ "artifacts": { "type": "object", "additionalProperties": false, - "required": [ - "maven", - "typescript" - ], + "required": ["maven", "typescript"], "properties": { "maven": { "type": "object", "additionalProperties": false, - "required": [ - "groupId", - "artifactId", - "packageName" - ], + "required": ["groupId", "artifactId", "packageName"], "properties": { "groupId": { "type": "string", @@ -91,9 +80,7 @@ "typescript": { "type": "object", "additionalProperties": false, - "required": [ - "packageName" - ], + "required": ["packageName"], "properties": { "packageName": { "type": "string", @@ -148,14 +135,7 @@ }, "recordMetadata": { "type": "object", - "required": [ - "id", - "description", - "namespace", - "owner", - "scope", - "status" - ], + "required": ["id", "description", "namespace", "owner", "scope", "status"], "properties": { "id": { "$ref": "#/$defs/id" @@ -174,19 +154,11 @@ }, "scope": { "type": "string", - "enum": [ - "shared", - "consumer-specific" - ] + "enum": ["shared", "consumer-specific"] }, "status": { "type": "string", - "enum": [ - "active", - "deprecated", - "replaced", - "removed" - ] + "enum": ["active", "deprecated", "replaced", "removed"] }, "replacementId": { "$ref": "#/$defs/id" @@ -209,10 +181,7 @@ }, { "type": "object", - "required": [ - "key", - "authorityValue" - ], + "required": ["key", "authorityValue"], "properties": { "key": { "$ref": "#/$defs/upperKey" @@ -234,12 +203,7 @@ }, { "type": "object", - "required": [ - "key", - "name", - "channel", - "valueShape" - ], + "required": ["key", "name", "channel", "valueShape"], "properties": { "key": { "$ref": "#/$defs/claimKey" @@ -250,11 +214,7 @@ }, "channel": { "type": "string", - "enum": [ - "jwt-claim", - "http-header", - "authority" - ] + "enum": ["jwt-claim", "http-header", "authority"] }, "valueShape": { "type": "string", @@ -272,10 +232,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "role", - "service-permission" - ] + "enum": ["role", "service-permission"] }, "uniqueItems": true } @@ -292,10 +249,7 @@ }, { "type": "object", - "required": [ - "key", - "authorityValue" - ], + "required": ["key", "authorityValue"], "properties": { "key": { "$ref": "#/$defs/upperKey" @@ -312,17 +266,11 @@ "groupCause": { "type": "object", "additionalProperties": false, - "required": [ - "type", - "key" - ], + "required": ["type", "key"], "properties": { "type": { "type": "string", - "enum": [ - "role", - "permission" - ] + "enum": ["role", "permission"] }, "key": { "$ref": "#/$defs/upperKey" @@ -337,11 +285,7 @@ }, { "type": "object", - "required": [ - "key", - "value", - "causes" - ], + "required": ["key", "value", "causes"], "properties": { "key": { "$ref": "#/$defs/upperKey" @@ -370,11 +314,7 @@ }, { "type": "object", - "required": [ - "key", - "permissionKey", - "aliases" - ], + "required": ["key", "permissionKey", "aliases"], "properties": { "key": { "$ref": "#/$defs/upperKey" diff --git a/tools/generator/check-generated.mjs b/tools/generator/check-generated.mjs index ed67f47..43700cd 100644 --- a/tools/generator/check-generated.mjs +++ b/tools/generator/check-generated.mjs @@ -1,43 +1,43 @@ #!/usr/bin/env node -import { mkdtemp, readFile, rm } from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { generate } from "./generate.mjs"; +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { generate } from './generate.mjs' -const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') async function checkGenerated() { - const tempDir = await mkdtemp(path.join(os.tmpdir(), "authz-model-")); - const mismatches = []; + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'authz-model-')) + const mismatches = [] try { - const outputs = await generate({ outputRoot: tempDir }); + const outputs = await generate({ outputRoot: tempDir }) for (const output of outputs) { - const expectedPath = path.join(ROOT_DIR, output.path); - const actualPath = path.join(tempDir, output.path); + const expectedPath = path.join(ROOT_DIR, output.path) + const actualPath = path.join(tempDir, output.path) const [expected, actual] = await Promise.all([ - readFile(expectedPath, "utf8").catch(() => ""), - readFile(actualPath, "utf8") - ]); + readFile(expectedPath, 'utf8').catch(() => ''), + readFile(actualPath, 'utf8'), + ]) if (expected !== actual) { - mismatches.push(output.path); + mismatches.push(output.path) } } } finally { - await rm(tempDir, { recursive: true, force: true }); + await rm(tempDir, { recursive: true, force: true }) } if (mismatches.length > 0) { - throw new Error(`Generated output is stale:\n${mismatches.map((file) => `- ${file}`).join("\n")}`); + throw new Error(`Generated output is stale:\n${mismatches.map((file) => `- ${file}`).join('\n')}`) } } checkGenerated() .then(() => { - console.log("Generated output is current"); + console.log('Generated output is current') }) .catch((error) => { - console.error(error.message); - process.exit(1); - }); + console.error(error.message) + process.exit(1) + }) diff --git a/tools/generator/generate.mjs b/tools/generator/generate.mjs index 28d9b1f..f486f7a 100644 --- a/tools/generator/generate.mjs +++ b/tools/generator/generate.mjs @@ -1,43 +1,43 @@ #!/usr/bin/env node -import { mkdir, writeFile } from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { validateModel } from "./validate.mjs"; +import { mkdir, writeFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { validateModel } from './validate.mjs' -const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); -const KOTLIN_OUTPUT = "packages/kotlin/src/generated/kotlin/dev/jorisjonkers/authz/model/AuthzVocabulary.kt"; -const TYPESCRIPT_OUTPUT = "packages/typescript/src/generated/authz-vocabulary.ts"; +const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') +const KOTLIN_OUTPUT = 'packages/kotlin/src/generated/kotlin/dev/jorisjonkers/authz/model/AuthzVocabulary.kt' +const TYPESCRIPT_OUTPUT = 'packages/typescript/src/generated/authz-vocabulary.ts' export async function generate({ rootDir = ROOT_DIR, outputRoot = ROOT_DIR, - modelPath = path.join(rootDir, "model/personal-stack.sample.json"), - schemaPath = path.join(rootDir, "schema/authz-model.schema.json") + modelPath = path.join(rootDir, 'model/personal-stack.sample.json'), + schemaPath = path.join(rootDir, 'schema/authz-model.schema.json'), } = {}) { - const { model } = await validateModel({ modelPath, schemaPath }); + const { model } = await validateModel({ modelPath, schemaPath }) const outputs = [ { path: KOTLIN_OUTPUT, - content: renderKotlin(model) + content: renderKotlin(model), }, { path: TYPESCRIPT_OUTPUT, - content: renderTypeScript(model) - } - ]; + content: renderTypeScript(model), + }, + ] for (const output of outputs) { - const target = path.join(outputRoot, output.path); - await mkdir(path.dirname(target), { recursive: true }); - await writeFile(target, output.content, "utf8"); + const target = path.join(outputRoot, output.path) + await mkdir(path.dirname(target), { recursive: true }) + await writeFile(target, output.content, 'utf8') } - return outputs; + return outputs } function renderKotlin(model) { - const aliases = hostAliasPairs(model); - return `${generatedBanner("//")} + const aliases = hostAliasPairs(model) + return `${generatedBanner('//')} package ${model.artifacts.maven.packageName} public data class AuthzRoleRecord( @@ -125,67 +125,97 @@ public object AuthzVocabulary { public fun permissionKeyForHost(host: String?): String? = permissionKeyForHostAlias(host?.substringBefore(".")) public object RoleKeys { -${model.roles.map((role) => ` public const val ${role.key}: String = ${kotlinString(role.key)}`).join("\n")} -${renderKotlinPropertyList("ALL", "List", model.roles.map((role) => role.key), 8)} +${model.roles.map((role) => ` public const val ${role.key}: String = ${kotlinString(role.key)}`).join('\n')} +${renderKotlinPropertyList( + 'ALL', + 'List', + model.roles.map((role) => role.key), + 8, +)} } public object RoleAuthorities { -${model.roles.map((role) => ` public const val ${role.key}: String = ${kotlinString(role.authorityValue)}`).join("\n")} -${renderKotlinPropertyList("ALL", "List", model.roles.map((role) => role.key), 8)} +${model.roles.map((role) => ` public const val ${role.key}: String = ${kotlinString(role.authorityValue)}`).join('\n')} +${renderKotlinPropertyList( + 'ALL', + 'List', + model.roles.map((role) => role.key), + 8, +)} } public object ClaimNames { -${model.claims.map((claim) => ` public const val ${constantName(claim.key)}: String = ${kotlinString(claim.name)}`).join("\n")} -${renderKotlinPropertyList("ALL", "List", model.claims.map((claim) => constantName(claim.key)), 8)} +${model.claims.map((claim) => ` public const val ${constantName(claim.key)}: String = ${kotlinString(claim.name)}`).join('\n')} +${renderKotlinPropertyList( + 'ALL', + 'List', + model.claims.map((claim) => constantName(claim.key)), + 8, +)} } public object PermissionKeys { -${model.permissions.map((permission) => ` public const val ${permission.key}: String = ${kotlinString(permission.key)}`).join("\n")} -${renderKotlinPropertyList("ALL", "List", model.permissions.map((permission) => permission.key), 8)} +${model.permissions.map((permission) => ` public const val ${permission.key}: String = ${kotlinString(permission.key)}`).join('\n')} +${renderKotlinPropertyList( + 'ALL', + 'List', + model.permissions.map((permission) => permission.key), + 8, +)} } public object PermissionAuthorities { -${model.permissions.map((permission) => ` public const val ${permission.key}: String = ${kotlinString(permission.authorityValue)}`).join("\n")} -${renderKotlinPropertyList("ALL", "List", model.permissions.map((permission) => permission.key), 8)} +${model.permissions.map((permission) => ` public const val ${permission.key}: String = ${kotlinString(permission.authorityValue)}`).join('\n')} +${renderKotlinPropertyList( + 'ALL', + 'List', + model.permissions.map((permission) => permission.key), + 8, +)} } public object GroupValues { -${model.groups.map((group) => ` public const val ${group.key}: String = ${kotlinString(group.value)}`).join("\n")} -${renderKotlinPropertyList("ALL", "List", model.groups.map((group) => group.key), 8)} +${model.groups.map((group) => ` public const val ${group.key}: String = ${kotlinString(group.value)}`).join('\n')} +${renderKotlinPropertyList( + 'ALL', + 'List', + model.groups.map((group) => group.key), + 8, +)} } public object Roles { -${renderKotlinPropertyList("ALL", "List", model.roles.map(renderKotlinRole), 8)} +${renderKotlinPropertyList('ALL', 'List', model.roles.map(renderKotlinRole), 8)} } public object Claims { -${renderKotlinPropertyList("ALL", "List", model.claims.map(renderKotlinClaim), 8)} +${renderKotlinPropertyList('ALL', 'List', model.claims.map(renderKotlinClaim), 8)} } public object Permissions { -${renderKotlinPropertyList("ALL", "List", model.permissions.map(renderKotlinPermission), 8)} +${renderKotlinPropertyList('ALL', 'List', model.permissions.map(renderKotlinPermission), 8)} } public object Groups { -${renderKotlinPropertyList("ALL", "List", model.groups.map(renderKotlinGroup), 8)} +${renderKotlinPropertyList('ALL', 'List', model.groups.map(renderKotlinGroup), 8)} } public object HostGates { -${renderKotlinPropertyList("ALL", "List", model.hostGates.map(renderKotlinHostGate), 8)} +${renderKotlinPropertyList('ALL', 'List', model.hostGates.map(renderKotlinHostGate), 8)} ${renderKotlinPropertyMap( - "ALIAS_TO_PERMISSION_KEY", - "Map", + 'ALIAS_TO_PERMISSION_KEY', + 'Map', aliases.map(([alias, permissionKey]) => `${kotlinString(alias)} to PermissionKeys.${permissionKey}`), - 8 + 8, )} } } -`; +` } function renderTypeScript(model) { - const aliases = hostAliasPairs(model); - return `${generatedBanner("//")} + const aliases = hostAliasPairs(model) + return `${generatedBanner('//')} export const MODEL_ID = ${tsString(model.modelId)}; export const MODEL_VERSION = ${tsString(model.version)}; export const ROLE_AUTHORITY_PREFIX = ${tsString(model.authorityFormats.rolePrefix)}; @@ -193,40 +223,40 @@ export const SERVICE_PERMISSION_AUTHORITY_PREFIX = ${tsString(model.authorityFor export const ROLES_HEADER_SEPARATOR = ${tsString(model.authorityFormats.rolesHeaderSeparator)}; export const UNMAPPED_HOST_BEHAVIOR = ${tsString(model.unmappedHostBehavior)}; -export const ROLE_KEYS = [${model.roles.map((role) => tsString(role.key)).join(", ")}] as const; +export const ROLE_KEYS = [${model.roles.map((role) => tsString(role.key)).join(', ')}] as const; export type RoleKey = (typeof ROLE_KEYS)[number]; export const RoleKeys = { -${model.roles.map((role) => ` ${role.key}: ${tsString(role.key)}`).join(",\n")} +${model.roles.map((role) => ` ${role.key}: ${tsString(role.key)}`).join(',\n')} } as const satisfies Record; export const RoleAuthorities = { -${model.roles.map((role) => ` ${role.key}: ${tsString(role.authorityValue)}`).join(",\n")} +${model.roles.map((role) => ` ${role.key}: ${tsString(role.authorityValue)}`).join(',\n')} } as const satisfies Record; -export const CLAIM_KEYS = [${model.claims.map((claim) => tsString(claim.key)).join(", ")}] as const; +export const CLAIM_KEYS = [${model.claims.map((claim) => tsString(claim.key)).join(', ')}] as const; export type ClaimKey = (typeof CLAIM_KEYS)[number]; export const ClaimNames = { -${model.claims.map((claim) => ` ${claim.key}: ${tsString(claim.name)}`).join(",\n")} +${model.claims.map((claim) => ` ${claim.key}: ${tsString(claim.name)}`).join(',\n')} } as const satisfies Record; -export const PERMISSION_KEYS = [${model.permissions.map((permission) => tsString(permission.key)).join(", ")}] as const; +export const PERMISSION_KEYS = [${model.permissions.map((permission) => tsString(permission.key)).join(', ')}] as const; export type PermissionKey = (typeof PERMISSION_KEYS)[number]; export const PermissionKeys = { -${model.permissions.map((permission) => ` ${permission.key}: ${tsString(permission.key)}`).join(",\n")} +${model.permissions.map((permission) => ` ${permission.key}: ${tsString(permission.key)}`).join(',\n')} } as const satisfies Record; export const PermissionAuthorities = { -${model.permissions.map((permission) => ` ${permission.key}: ${tsString(permission.authorityValue)}`).join(",\n")} +${model.permissions.map((permission) => ` ${permission.key}: ${tsString(permission.authorityValue)}`).join(',\n')} } as const satisfies Record; -export const GROUP_KEYS = [${model.groups.map((group) => tsString(group.key)).join(", ")}] as const; +export const GROUP_KEYS = [${model.groups.map((group) => tsString(group.key)).join(', ')}] as const; export type GroupKey = (typeof GROUP_KEYS)[number]; export const GroupValues = { -${model.groups.map((group) => ` ${group.key}: ${tsString(group.value)}`).join(",\n")} +${model.groups.map((group) => ` ${group.key}: ${tsString(group.value)}`).join(',\n')} } as const satisfies Record; export interface AuthzRoleRecord { @@ -301,7 +331,7 @@ export const Groups = ${tsValue(model.groups)} as const satisfies readonly Authz export const HostGates = ${tsValue(model.hostGates)} as const satisfies readonly AuthzHostGateRecord[]; export const HostGateAliases = { -${aliases.map(([alias, permissionKey]) => ` ${JSON.stringify(alias)}: ${tsString(permissionKey)}`).join(",\n")} +${aliases.map(([alias, permissionKey]) => ` ${JSON.stringify(alias)}: ${tsString(permissionKey)}`).join(',\n')} } as const satisfies Record; export function roleAuthority(key: RoleKey | string): string { @@ -323,188 +353,188 @@ export function permissionKeyForHostAlias(alias: string | null | undefined): Per export function permissionKeyForHost(host: string | null | undefined): PermissionKey | undefined { return permissionKeyForHostAlias(host?.split(".")[0]); } -`; +` } function renderKotlinRole(role) { - return renderKotlinCall("AuthzRoleRecord", [ - ["id", kotlinString(role.id)], - ["key", kotlinString(role.key)], - ["authorityValue", kotlinString(role.authorityValue)], - ["description", kotlinString(role.description)], - ["namespace", kotlinString(role.namespace)], - ["owner", kotlinString(role.owner)], - ["scope", kotlinString(role.scope)], - ["status", kotlinString(role.status)] - ]); + return renderKotlinCall('AuthzRoleRecord', [ + ['id', kotlinString(role.id)], + ['key', kotlinString(role.key)], + ['authorityValue', kotlinString(role.authorityValue)], + ['description', kotlinString(role.description)], + ['namespace', kotlinString(role.namespace)], + ['owner', kotlinString(role.owner)], + ['scope', kotlinString(role.scope)], + ['status', kotlinString(role.status)], + ]) } function renderKotlinClaim(claim) { - const allowed = claim.allowedAuthorityFormats ?? []; - return renderKotlinCall("AuthzClaimRecord", [ - ["id", kotlinString(claim.id)], - ["key", kotlinString(claim.key)], - ["name", kotlinString(claim.name)], - ["channel", kotlinString(claim.channel)], - ["valueShape", kotlinString(claim.valueShape)], - ["allowedAuthorityFormats", renderKotlinInlineList(allowed.map(kotlinString))], - ["description", kotlinString(claim.description)], - ["namespace", kotlinString(claim.namespace)], - ["owner", kotlinString(claim.owner)], - ["scope", kotlinString(claim.scope)], - ["status", kotlinString(claim.status)] - ]); + const allowed = claim.allowedAuthorityFormats ?? [] + return renderKotlinCall('AuthzClaimRecord', [ + ['id', kotlinString(claim.id)], + ['key', kotlinString(claim.key)], + ['name', kotlinString(claim.name)], + ['channel', kotlinString(claim.channel)], + ['valueShape', kotlinString(claim.valueShape)], + ['allowedAuthorityFormats', renderKotlinInlineList(allowed.map(kotlinString))], + ['description', kotlinString(claim.description)], + ['namespace', kotlinString(claim.namespace)], + ['owner', kotlinString(claim.owner)], + ['scope', kotlinString(claim.scope)], + ['status', kotlinString(claim.status)], + ]) } function renderKotlinPermission(permission) { - return renderKotlinCall("AuthzPermissionRecord", [ - ["id", kotlinString(permission.id)], - ["key", kotlinString(permission.key)], - ["authorityValue", kotlinString(permission.authorityValue)], - ["description", kotlinString(permission.description)], - ["namespace", kotlinString(permission.namespace)], - ["owner", kotlinString(permission.owner)], - ["scope", kotlinString(permission.scope)], - ["status", kotlinString(permission.status)] - ]); + return renderKotlinCall('AuthzPermissionRecord', [ + ['id', kotlinString(permission.id)], + ['key', kotlinString(permission.key)], + ['authorityValue', kotlinString(permission.authorityValue)], + ['description', kotlinString(permission.description)], + ['namespace', kotlinString(permission.namespace)], + ['owner', kotlinString(permission.owner)], + ['scope', kotlinString(permission.scope)], + ['status', kotlinString(permission.status)], + ]) } function renderKotlinGroup(group) { const causes = group.causes.map((cause) => - renderKotlinCall("AuthzGroupCause", [ - ["type", kotlinString(cause.type)], - ["key", kotlinString(cause.key)] - ]) - ); - return renderKotlinCall("AuthzGroupRecord", [ - ["id", kotlinString(group.id)], - ["key", kotlinString(group.key)], - ["value", kotlinString(group.value)], - ["causes", renderKotlinExpressionList("listOf", causes)], - ["description", kotlinString(group.description)], - ["namespace", kotlinString(group.namespace)], - ["owner", kotlinString(group.owner)], - ["scope", kotlinString(group.scope)], - ["status", kotlinString(group.status)] - ]); + renderKotlinCall('AuthzGroupCause', [ + ['type', kotlinString(cause.type)], + ['key', kotlinString(cause.key)], + ]), + ) + return renderKotlinCall('AuthzGroupRecord', [ + ['id', kotlinString(group.id)], + ['key', kotlinString(group.key)], + ['value', kotlinString(group.value)], + ['causes', renderKotlinExpressionList('listOf', causes)], + ['description', kotlinString(group.description)], + ['namespace', kotlinString(group.namespace)], + ['owner', kotlinString(group.owner)], + ['scope', kotlinString(group.scope)], + ['status', kotlinString(group.status)], + ]) } function renderKotlinHostGate(hostGate) { - return renderKotlinCall("AuthzHostGateRecord", [ - ["id", kotlinString(hostGate.id)], - ["key", kotlinString(hostGate.key)], - ["permissionKey", kotlinString(hostGate.permissionKey)], - ["aliases", renderKotlinInlineList(hostGate.aliases.map(kotlinString))], - ["description", kotlinString(hostGate.description)], - ["namespace", kotlinString(hostGate.namespace)], - ["owner", kotlinString(hostGate.owner)], - ["scope", kotlinString(hostGate.scope)], - ["status", kotlinString(hostGate.status)] - ]); + return renderKotlinCall('AuthzHostGateRecord', [ + ['id', kotlinString(hostGate.id)], + ['key', kotlinString(hostGate.key)], + ['permissionKey', kotlinString(hostGate.permissionKey)], + ['aliases', renderKotlinInlineList(hostGate.aliases.map(kotlinString))], + ['description', kotlinString(hostGate.description)], + ['namespace', kotlinString(hostGate.namespace)], + ['owner', kotlinString(hostGate.owner)], + ['scope', kotlinString(hostGate.scope)], + ['status', kotlinString(hostGate.status)], + ]) } function renderKotlinPropertyList(name, type, elements, indentSize) { - return renderKotlinProperty(name, type, renderKotlinExpressionList("listOf", elements), indentSize); + return renderKotlinProperty(name, type, renderKotlinExpressionList('listOf', elements), indentSize) } function renderKotlinPropertyMap(name, type, entries, indentSize) { - return renderKotlinProperty(name, type, renderKotlinExpressionList("mapOf", entries), indentSize); + return renderKotlinProperty(name, type, renderKotlinExpressionList('mapOf', entries), indentSize) } function renderKotlinProperty(name, type, value, indentSize) { - const indent = spaces(indentSize); - return `${indent}public val ${name}: ${type} =\n${indentBlock(value, indentSize + 4)}`; + const indent = spaces(indentSize) + return `${indent}public val ${name}: ${type} =\n${indentBlock(value, indentSize + 4)}` } function renderKotlinExpressionList(functionName, elements) { if (elements.length === 0) { - return `${functionName}()`; + return `${functionName}()` } - return `${functionName}(\n${elements.map((element) => `${indentBlock(element, 4)},`).join("\n")}\n)`; + return `${functionName}(\n${elements.map((element) => `${indentBlock(element, 4)},`).join('\n')}\n)` } function renderKotlinInlineList(elements) { - return `listOf(${elements.join(", ")})`; + return `listOf(${elements.join(', ')})` } function renderKotlinCall(name, args) { - return `${name}(\n${args.map(([key, value]) => renderKotlinArgument(key, value)).join("\n")}\n)`; + return `${name}(\n${args.map(([key, value]) => renderKotlinArgument(key, value)).join('\n')}\n)` } function renderKotlinArgument(key, value) { - if (value.includes("\n")) { - return ` ${key} =\n${indentBlock(value, 8)},`; + if (value.includes('\n')) { + return ` ${key} =\n${indentBlock(value, 8)},` } - return ` ${key} = ${value},`; + return ` ${key} = ${value},` } function indentBlock(value, indentSize) { - const indent = spaces(indentSize); + const indent = spaces(indentSize) return value - .split("\n") + .split('\n') .map((line) => `${indent}${line}`) - .join("\n"); + .join('\n') } function spaces(size) { - return " ".repeat(size); + return ' '.repeat(size) } function hostAliasPairs(model) { - return model.hostGates.flatMap((hostGate) => hostGate.aliases.map((alias) => [alias, hostGate.permissionKey])); + return model.hostGates.flatMap((hostGate) => hostGate.aliases.map((alias) => [alias, hostGate.permissionKey])) } function normalizeClaims(claims) { return claims.map((claim) => ({ ...claim, - allowedAuthorityFormats: claim.allowedAuthorityFormats ?? [] - })); + allowedAuthorityFormats: claim.allowedAuthorityFormats ?? [], + })) } function constantName(key) { - return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase(); + return key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase() } function kotlinString(value) { - return JSON.stringify(value); + return JSON.stringify(value) } function tsString(value) { - return JSON.stringify(value); + return JSON.stringify(value) } function tsValue(value) { - return JSON.stringify(value, null, 2); + return JSON.stringify(value, null, 2) } function generatedBanner(prefix) { - return `${prefix} Generated from model/personal-stack.sample.json. Do not edit manually.\n`; + return `${prefix} Generated from model/personal-stack.sample.json. Do not edit manually.\n` } function parseArgs(argv) { - const options = {}; + const options = {} for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index]; - if (arg === "--output-root") { - options.outputRoot = path.resolve(argv[++index]); - } else if (arg === "--model") { - options.modelPath = path.resolve(argv[++index]); - } else if (arg === "--schema") { - options.schemaPath = path.resolve(argv[++index]); + const arg = argv[index] + if (arg === '--output-root') { + options.outputRoot = path.resolve(argv[++index]) + } else if (arg === '--model') { + options.modelPath = path.resolve(argv[++index]) + } else if (arg === '--schema') { + options.schemaPath = path.resolve(argv[++index]) } } - return options; + return options } if (import.meta.url === pathToFileURL(process.argv[1]).href) { generate(parseArgs(process.argv.slice(2))) .then((outputs) => { for (const output of outputs) { - console.log(`Wrote ${output.path}`); + console.log(`Wrote ${output.path}`) } }) .catch((error) => { - console.error(error.message); - process.exit(1); - }); + console.error(error.message) + process.exit(1) + }) } diff --git a/tools/generator/validate.mjs b/tools/generator/validate.mjs index 68e1084..547dfe1 100644 --- a/tools/generator/validate.mjs +++ b/tools/generator/validate.mjs @@ -1,121 +1,118 @@ #!/usr/bin/env node -import Ajv2020 from "ajv/dist/2020.js"; -import { readFile } from "node:fs/promises"; -import path from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import Ajv2020 from 'ajv/dist/2020.js' +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' -const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); -const DEFAULT_SCHEMA_PATH = path.join(ROOT_DIR, "schema/authz-model.schema.json"); -const DEFAULT_MODEL_PATH = path.join(ROOT_DIR, "model/personal-stack.sample.json"); +const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') +const DEFAULT_SCHEMA_PATH = path.join(ROOT_DIR, 'schema/authz-model.schema.json') +const DEFAULT_MODEL_PATH = path.join(ROOT_DIR, 'model/personal-stack.sample.json') export class ValidationError extends Error { constructor(errors) { - super(`Vocabulary validation failed:\n${errors.map((error) => `- ${error}`).join("\n")}`); - this.name = "ValidationError"; - this.errors = errors; + super(`Vocabulary validation failed:\n${errors.map((error) => `- ${error}`).join('\n')}`) + this.name = 'ValidationError' + this.errors = errors } } export async function loadJson(filePath) { - return JSON.parse(await readFile(filePath, "utf8")); + return JSON.parse(await readFile(filePath, 'utf8')) } -export async function validateModel({ - schemaPath = DEFAULT_SCHEMA_PATH, - modelPath = DEFAULT_MODEL_PATH -} = {}) { - const [schema, model] = await Promise.all([loadJson(schemaPath), loadJson(modelPath)]); - const ajv = new Ajv2020({ allErrors: true, strict: true }); - const validate = ajv.compile(schema); - const valid = validate(model); - const errors = []; +export async function validateModel({ schemaPath = DEFAULT_SCHEMA_PATH, modelPath = DEFAULT_MODEL_PATH } = {}) { + const [schema, model] = await Promise.all([loadJson(schemaPath), loadJson(modelPath)]) + const ajv = new Ajv2020({ allErrors: true, strict: true }) + const validate = ajv.compile(schema) + const valid = validate(model) + const errors = [] if (!valid) { for (const error of validate.errors ?? []) { - const location = error.instancePath || "/"; - errors.push(`${location} ${error.message}`); + const location = error.instancePath || '/' + errors.push(`${location} ${error.message}`) } } - errors.push(...validateSemanticRules(model)); + errors.push(...validateSemanticRules(model)) if (errors.length > 0) { - throw new ValidationError(errors); + throw new ValidationError(errors) } - return { schema, model }; + return { schema, model } } function validateSemanticRules(model) { - const errors = []; - const roleKeys = new Set(model.roles.map((role) => role.key)); - const permissionKeys = new Set(model.permissions.map((permission) => permission.key)); - const recordIds = new Set(); + const errors = [] + const roleKeys = new Set(model.roles.map((role) => role.key)) + const permissionKeys = new Set(model.permissions.map((permission) => permission.key)) + const recordIds = new Set() for (const [collectionName, records] of Object.entries(recordCollections(model))) { - collectUnique(records, "id", collectionName, errors, recordIds); - collectUnique(records, "key", collectionName, errors); + collectUnique(records, 'id', collectionName, errors, recordIds) + collectUnique(records, 'key', collectionName, errors) } - collectUnique(model.roles, "authorityValue", "roles", errors); - collectUnique(model.permissions, "authorityValue", "permissions", errors); - collectUnique(model.claims, "name", "claims", errors, undefined, (claim) => `${claim.channel}:${claim.name}`); + collectUnique(model.roles, 'authorityValue', 'roles', errors) + collectUnique(model.permissions, 'authorityValue', 'permissions', errors) + collectUnique(model.claims, 'name', 'claims', errors, undefined, (claim) => `${claim.channel}:${claim.name}`) for (const role of model.roles) { - const expected = `${model.authorityFormats.rolePrefix}${role.key}`; + const expected = `${model.authorityFormats.rolePrefix}${role.key}` if (role.authorityValue !== expected) { - errors.push(`roles.${role.key}.authorityValue must be ${expected}`); + errors.push(`roles.${role.key}.authorityValue must be ${expected}`) } } for (const permission of model.permissions) { - const expected = `${model.authorityFormats.servicePermissionPrefix}${permission.key}`; + const expected = `${model.authorityFormats.servicePermissionPrefix}${permission.key}` if (permission.authorityValue !== expected) { - errors.push(`permissions.${permission.key}.authorityValue must be ${expected}`); + errors.push(`permissions.${permission.key}.authorityValue must be ${expected}`) } } for (const group of model.groups) { for (const cause of group.causes) { - if (cause.type === "role" && !roleKeys.has(cause.key)) { - errors.push(`groups.${group.key} references unknown role ${cause.key}`); + if (cause.type === 'role' && !roleKeys.has(cause.key)) { + errors.push(`groups.${group.key} references unknown role ${cause.key}`) } - if (cause.type === "permission" && !permissionKeys.has(cause.key)) { - errors.push(`groups.${group.key} references unknown permission ${cause.key}`); + if (cause.type === 'permission' && !permissionKeys.has(cause.key)) { + errors.push(`groups.${group.key} references unknown permission ${cause.key}`) } } } - const hostAliases = new Map(); + const hostAliases = new Map() for (const hostGate of model.hostGates) { if (!permissionKeys.has(hostGate.permissionKey)) { - errors.push(`hostGates.${hostGate.key} references unknown permission ${hostGate.permissionKey}`); + errors.push(`hostGates.${hostGate.key} references unknown permission ${hostGate.permissionKey}`) } for (const alias of hostGate.aliases) { - const normalized = alias.toLowerCase(); + const normalized = alias.toLowerCase() if (alias !== normalized) { - errors.push(`hostGates.${hostGate.key} alias ${alias} must be lowercase`); + errors.push(`hostGates.${hostGate.key} alias ${alias} must be lowercase`) } - const existing = hostAliases.get(normalized); + const existing = hostAliases.get(normalized) if (existing) { - errors.push(`host alias ${alias} is used by both ${existing} and ${hostGate.key}`); + errors.push(`host alias ${alias} is used by both ${existing} and ${hostGate.key}`) } - hostAliases.set(normalized, hostGate.key); + hostAliases.set(normalized, hostGate.key) } } for (const [collectionName, records] of Object.entries(recordCollections(model))) { for (const record of records) { - if (record.status === "replaced" && !record.replacementId) { - errors.push(`${collectionName}.${record.key}.replacementId is required when status is replaced`); + if (record.status === 'replaced' && !record.replacementId) { + errors.push(`${collectionName}.${record.key}.replacementId is required when status is replaced`) } if (record.replacementId && !recordIds.has(record.replacementId)) { - errors.push(`${collectionName}.${record.key}.replacementId references unknown record ${record.replacementId}`); + errors.push(`${collectionName}.${record.key}.replacementId references unknown record ${record.replacementId}`) } } } - return errors; + return errors } function recordCollections(model) { @@ -124,41 +121,41 @@ function recordCollections(model) { claims: model.claims, permissions: model.permissions, groups: model.groups, - hostGates: model.hostGates - }; + hostGates: model.hostGates, + } } function collectUnique(records, field, collectionName, errors, externalSet, selector) { - const seen = externalSet ?? new Set(); + const seen = externalSet ?? new Set() for (const record of records) { - const value = selector ? selector(record) : record[field]; + const value = selector ? selector(record) : record[field] if (seen.has(value)) { - errors.push(`${collectionName} contains duplicate ${field} ${value}`); + errors.push(`${collectionName} contains duplicate ${field} ${value}`) } - seen.add(value); + seen.add(value) } } function parseArgs(argv) { - const options = {}; + const options = {} for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index]; - if (arg === "--schema") { - options.schemaPath = path.resolve(argv[++index]); - } else if (arg === "--model") { - options.modelPath = path.resolve(argv[++index]); + const arg = argv[index] + if (arg === '--schema') { + options.schemaPath = path.resolve(argv[++index]) + } else if (arg === '--model') { + options.modelPath = path.resolve(argv[++index]) } } - return options; + return options } if (import.meta.url === pathToFileURL(process.argv[1]).href) { validateModel(parseArgs(process.argv.slice(2))) .then(({ model }) => { - console.log(`Validated ${model.modelId} ${model.version}`); + console.log(`Validated ${model.modelId} ${model.version}`) }) .catch((error) => { - console.error(error.message); - process.exit(1); - }); + console.error(error.message) + process.exit(1) + }) }