From 7ba80d7ab691763847d920711c6086726b97e8a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 15:52:59 +0000 Subject: [PATCH 1/2] enforce authentication on the CAP app; add /health; gate mirror commit on tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security (priority-1 finding: the backend was publicly reachable): - package.json: drop the "[production]": { auth: false } override that disabled CAP's auth middleware in production; production now uses xsuaa, development uses a mocked user so the suite stays runnable. - z2ui5-service.cds: @(requires: 'authenticated-user') on both rootService (the z2ui5 roundtrip action) and AdminService (the draft table + Northwind proxy) — the serialized-state draft table is no longer anonymously readable, and the mutating roundtrip is no longer anonymously callable. In BTP the approuter authenticates via xsuaa and forwards the JWT (HTML5.ForwardAuthToken), so the roundtrip still runs under the real user. - xs-security.json: add a User scope + role template (was empty). Deployment: - server.js: implement GET /health (public, cheap 200) — mta.yaml already declares it as the CF readiness-check endpoint but nothing served it. CI: - update_cap.yml: commit the refreshed run/input/core mirror only AFTER assemble + the app's jest suite pass, so a broken upstream core can no longer land on main permanently while the publish is (correctly) gated. Tests: starter/northwind updated to authenticate; new starter case asserts the roundtrip and the draft table reject unauthenticated access (401). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018FTxX214GdPJZXvz88Mbq8 --- .github/workflows/update_cap.yml | 16 ++++++++++------ src/package.json | 15 +++++++++++++-- src/srv/server.js | 7 +++++++ src/srv/z2ui5-service.cds | 10 ++++++++++ src/test/northwind.test.js | 4 +++- src/test/starter.test.js | 24 +++++++++++++++++++----- src/xs-security.json | 22 +++++++++++++++++----- 7 files changed, 79 insertions(+), 19 deletions(-) diff --git a/.github/workflows/update_cap.yml b/.github/workflows/update_cap.yml index 3e492cc..45c1793 100644 --- a/.github/workflows/update_cap.yml +++ b/.github/workflows/update_cap.yml @@ -44,6 +44,16 @@ jobs: # the builder's own dev deps (jest for the script tests, run by `npm test`) - run: npm ci - run: npm run mirror_core + - run: npm run assemble + # one install covers the app AND the vendored core (its deps are part + # of the app lock, placed under core/node_modules/) + - run: npm ci + working-directory: run/output/cap2UI5 + - run: npm test + # Commit the refreshed mirror ONLY after assemble + the app's jest suite + # pass. Committing earlier would land a broken upstream core on main + # permanently even though the publish is correctly gated; gating the + # mirror commit on the same green keeps main buildable. - name: commit refreshed mirror run: | git config user.name "github-actions[bot]" @@ -66,12 +76,6 @@ jobs: sleep $((i * 2)) done fi - - run: npm run assemble - # one install covers the app AND the vendored core (its deps are part - # of the app lock, placed under core/node_modules/) - - run: npm ci - working-directory: run/output/cap2UI5 - - run: npm test - name: publish built app to cap2UI5/cap2UI5 env: ACTION_KEY_APP: ${{ secrets.ACTION_KEY_APP }} diff --git a/src/package.json b/src/package.json index 872134e..a904d6c 100644 --- a/src/package.json +++ b/src/package.json @@ -43,8 +43,19 @@ "destinations": true, "html5-repo": true, "workzone": true, - "[production]": { - "auth": false + "auth": { + "[production]": "xsuaa", + "[development]": { + "kind": "mocked", + "users": { + "alice": { + "password": "alice", + "roles": [ + "authenticated-user" + ] + } + } + } } } }, diff --git a/src/srv/server.js b/src/srv/server.js index b553e37..66c4d4c 100644 --- a/src/srv/server.js +++ b/src/srv/server.js @@ -44,6 +44,13 @@ engine.register_app_dir(require("path").join(__dirname, "app")); cds.on("served", () => require("./draft-retention").start()); cds.on("bootstrap", (app) => { + // Readiness probe — mta.yaml declares + // readiness-health-check-http-endpoint: /health for the abap2UI5-srv + // module, so CF polls this route to decide the instance is up. It must stay + // public (the probe carries no auth) and cheap; a bare 200 is enough since + // the process answering at all is the signal CF needs. + app.get("/health", (_req, res) => res.status(200).json({ status: "UP" })); + // Serve the local UI5 runtime at /resources (must be registered before the // CDS services so it is not shadowed by the OData/REST routing) — the app // bootstraps from `/resources/sap-ui-core.js` (see patch-frontend.js / diff --git a/src/srv/z2ui5-service.cds b/src/srv/z2ui5-service.cds index 3404625..08cb509 100644 --- a/src/srv/z2ui5-service.cds +++ b/src/srv/z2ui5-service.cds @@ -4,7 +4,11 @@ using northwind from './external/northwind.csn'; /** * OData service exposing the z2ui5 draft table (used by the starter page * to prove persistence) and the remote Northwind sample entity. + * + * Restricted to authenticated users: the draft table holds serialized + * application state, so it must never be readable/countable anonymously. */ +@(requires: 'authenticated-user') service AdminService { entity z2ui5_t_01 as projection on cap2ui5.z2ui5_t_01; @@ -17,7 +21,13 @@ service AdminService { /** * REST-protocol service: the z2ui5 action is the single roundtrip endpoint. * Mounted at /rest/root/z2ui5 — frontends POST `{value: }`. + * + * Restricted to authenticated users. In the BTP deployment the approuter + * authenticates via xsuaa and the srv destination forwards the JWT + * (HTML5.ForwardAuthToken), so the roundtrip runs under the real user; a + * direct unauthenticated call to the srv route is rejected. */ +@(requires: 'authenticated-user') @protocol: 'rest' service rootService { diff --git a/src/test/northwind.test.js b/src/test/northwind.test.js index 783e64a..9476d61 100644 --- a/src/test/northwind.test.js +++ b/src/test/northwind.test.js @@ -22,7 +22,9 @@ describe("NorthwindCustomers remote proxy", () => { return orig.call(this, name, ...rest); }; try { - const res = await GET("/odata/v4/admin/NorthwindCustomers"); + const res = await GET("/odata/v4/admin/NorthwindCustomers", { + auth: { username: "alice", password: "alice" }, + }); expect(res.status).toBe(200); expect(res.data.value.map((r) => r.CustomerID)).toEqual(["ALFKI", "ANATR"]); expect(res.data.value[0].CompanyName).toBe("Alfreds Futterkiste"); diff --git a/src/test/starter.test.js b/src/test/starter.test.js index 2635e56..4d9e967 100644 --- a/src/test/starter.test.js +++ b/src/test/starter.test.js @@ -14,6 +14,11 @@ const cds = require("@sap/cds"); const { GET, POST } = cds.test(path.join(__dirname, "..")); +// The z2ui5 roundtrip action and the AdminService are now restricted to +// authenticated users (see z2ui5-service.cds). Under the mocked dev auth the +// requests run as the configured user (package.json cds.requires.auth). +const AUTH = { auth: { username: "alice", password: "alice" } }; + const roundtripBody = { value: { S_FRONT: { @@ -37,7 +42,7 @@ describe("minimal starter — frontend / service / database", () => { }); test("(2) roundtrip POST returns the startup app, a draft id and view XML", async () => { - const { status, data } = await POST("/rest/root/z2ui5", roundtripBody); + const { status, data } = await POST("/rest/root/z2ui5", roundtripBody, AUTH); expect(status).toBe(200); expect(data.S_FRONT.APP).toBe("z2ui5_cl_app_startup"); expect(data.S_FRONT.ID).toMatch(/^[0-9a-f-]{36}$/); @@ -45,15 +50,24 @@ describe("minimal starter — frontend / service / database", () => { }); test("(3) every roundtrip persists a draft row in cap2ui5.z2ui5_t_01", async () => { - const before = Number((await GET("/odata/v4/admin/z2ui5_t_01/$count")).data); + const before = Number((await GET("/odata/v4/admin/z2ui5_t_01/$count", AUTH)).data); - const { data } = await POST("/rest/root/z2ui5", roundtripBody); + const { data } = await POST("/rest/root/z2ui5", roundtripBody, AUTH); - const after = Number((await GET("/odata/v4/admin/z2ui5_t_01/$count")).data); + const after = Number((await GET("/odata/v4/admin/z2ui5_t_01/$count", AUTH)).data); expect(after).toBe(before + 1); - const row = await GET(`/odata/v4/admin/z2ui5_t_01(${data.S_FRONT.ID})`); + const row = await GET(`/odata/v4/admin/z2ui5_t_01(${data.S_FRONT.ID})`, AUTH); expect(row.status).toBe(200); expect(row.data.data).toContain("z2ui5_cl_app_startup"); }); + + test("(4) the roundtrip and the draft table reject unauthenticated access", async () => { + await expect(POST("/rest/root/z2ui5", roundtripBody)).rejects.toMatchObject({ + response: { status: 401 }, + }); + await expect(GET("/odata/v4/admin/z2ui5_t_01/$count")).rejects.toMatchObject({ + response: { status: 401 }, + }); + }); }); diff --git a/src/xs-security.json b/src/xs-security.json index 48653a7..b92ce62 100644 --- a/src/xs-security.json +++ b/src/xs-security.json @@ -1,6 +1,18 @@ { - "scopes": [], - "attributes": [], - "role-templates": [] - } - \ No newline at end of file + "scopes": [ + { + "name": "$XSAPPNAME.User", + "description": "Use the z2ui5 application" + } + ], + "attributes": [], + "role-templates": [ + { + "name": "User", + "description": "z2ui5 application user", + "scope-references": [ + "$XSAPPNAME.User" + ] + } + ] +} From 1bef772db4508bfb7eed218e3e5151a914fddeb9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Jul 2026 16:37:30 +0000 Subject: [PATCH 2/2] assemble drift guardrail, ESLint gate, npm dependabot, auth-boundary docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assemble-cap.js: add the stale core-lock drift guard — fail the build with an actionable message when the mirrored core's dependencies diverge from the frozen "core" entry in src/package-lock.json (the biggest latent break: it would otherwise surface only as an out-of-sync `npm ci` downstream). Also guard the pre-merge lockfile JSON.parse for symmetry with the later block. New guardrail test covers the drift case; the base fixture now carries the core's declared deps like the real lock. - ESLint flat config + `npm run lint` + CI gate over the build scripts, the builder tests and the CAP app source (src/srv, src/test). - dependabot: watch the npm ecosystem in /src (the app lock was untended). - server.js: document the auth boundary — data endpoints (POST roundtrip + OData) are @requires-protected; the GET/HEAD routes serve only the static shell + CSRF ack and are intentionally public. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018FTxX214GdPJZXvz88Mbq8 --- .github/dependabot.yml | 6 + .github/workflows/test.yml | 3 +- eslint.config.js | 72 ++++ package-lock.json | 861 +++++++++++++++++++++++++++++++++++++ package.json | 3 + scripts/assemble-cap.js | 34 +- src/srv/server.js | 7 + test/assemble-cap.test.js | 18 +- 8 files changed, 1000 insertions(+), 4 deletions(-) create mode 100644 eslint.config.js diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ca79ca5..bda23cd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,9 @@ updates: directory: / schedule: interval: weekly + # The CAP app's own dependencies live in src/ (@sap/cds, jest, mbt, …); the + # published app lock is regenerated from here, so keep this manifest current. + - package-ecosystem: npm + directory: /src + schedule: + interval: weekly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a81d300..a14c7ec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,8 +24,9 @@ jobs: - uses: actions/setup-node@v5 with: node-version: 22 - # the builder's own dev deps (jest for the script tests) + # the builder's own dev deps (jest for the script tests, eslint) - run: npm ci + - run: npm run lint - run: npm run assemble # one install covers the app AND the vendored core (its deps are part # of the app lock, placed under core/node_modules/) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..62db8b5 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,72 @@ +// ESLint flat config for the builder's own hand-written code: the build +// scripts (mirror/assemble/publish), the builder tests, and the CAP app +// source in src/srv + src/test. Generated/assembled trees (run/), the mirror +// (run/input), node_modules and the upstream UI5 webapp (src/app, browser +// globals) are excluded. +"use strict"; + +const js = require("@eslint/js"); + +module.exports = [ + { + ignores: [ + "run/**", + "node_modules/**", + "src/app/**", + "src/gen/**", + "coverage/**", + ], + }, + js.configs.recommended, + { + files: ["scripts/**/*.js", "test/**/*.js", "src/srv/**/*.js", "src/test/**/*.js"], + languageOptions: { + ecmaVersion: 2023, + sourceType: "commonjs", + globals: { + require: "readonly", + module: "writable", + exports: "writable", + process: "readonly", + __dirname: "readonly", + __filename: "readonly", + console: "readonly", + Buffer: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + setTimeout: "readonly", + globalThis: "readonly", + // CAP globals used in the service/server code and tests. + SELECT: "readonly", + INSERT: "readonly", + UPDATE: "readonly", + DELETE: "readonly", + cds: "readonly", + }, + }, + rules: { + "no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrors: "none" }], + "no-empty": ["warn", { allowEmptyCatch: true }], + }, + }, + { + files: ["test/**/*.js", "src/test/**/*.js", "**/*.test.js"], + languageOptions: { + globals: { + describe: "readonly", + test: "readonly", + it: "readonly", + expect: "readonly", + beforeEach: "readonly", + afterEach: "readonly", + beforeAll: "readonly", + afterAll: "readonly", + jest: "readonly", + }, + }, + }, + { + files: ["*.config.js"], + languageOptions: { globals: { require: "readonly", module: "writable" } }, + }, +]; diff --git a/package-lock.json b/package-lock.json index 757f158..9aacf32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "MIT", "devDependencies": { + "@eslint/js": "^9.39.5", + "eslint": "^9.39.5", "jest": "^30.4.2" }, "engines": { @@ -545,6 +547,294 @@ "tslib": "^2.4.0" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "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.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "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/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1107,6 +1397,13 @@ "@babel/types": "^7.28.2" } }, + "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/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1134,6 +1431,13 @@ "@types/istanbul-lib-report": "*" } }, + "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/@types/node": { "version": "26.1.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", @@ -1488,6 +1792,46 @@ "win32" ] }, + "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": "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/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2019,6 +2363,13 @@ } } }, + "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/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -2103,6 +2454,200 @@ "node": ">=8" } }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@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", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.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", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/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/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/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/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/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -2117,6 +2662,52 @@ "node": ">=4" } }, + "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/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2176,6 +2767,13 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.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", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "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", @@ -2183,6 +2781,13 @@ "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/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -2193,6 +2798,19 @@ "bser": "2.1.1" } }, + "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": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -2207,6 +2825,27 @@ "node": ">=8" } }, + "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.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "dev": true, + "license": "ISC" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2311,6 +2950,32 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2345,6 +3010,43 @@ "node": ">=10.17.0" } }, + "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/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2401,6 +3103,16 @@ "dev": true, "license": "MIT" }, + "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-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2421,6 +3133,19 @@ "node": ">=6" } }, + "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/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3168,6 +3893,13 @@ "node": ">=6" } }, + "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-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -3175,6 +3907,20 @@ "dev": true, "license": "MIT" }, + "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/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/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -3188,6 +3934,16 @@ "node": ">=6" } }, + "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/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -3198,6 +3954,20 @@ "node": ">=6" } }, + "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/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -3218,6 +3988,13 @@ "node": ">=8" } }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3406,6 +4183,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -3468,6 +4263,19 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -3584,6 +4392,16 @@ "node": ">=8" } }, + "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/pretty-format": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", @@ -3613,6 +4431,16 @@ "url": "https://github.com/chalk/ansi-styles?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/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -4055,6 +4883,19 @@ "license": "0BSD", "optional": true }, + "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/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4154,6 +4995,16 @@ "browserslist": ">= 4.21.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/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -4195,6 +5046,16 @@ "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/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/package.json b/package.json index 95e023f..aab618f 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "node": ">=22" }, "scripts": { + "lint": "eslint .", "mirror_core": "node scripts/mirror-core.js", "assemble": "node scripts/assemble-cap.js", "publish": "node scripts/publish-cap.js", @@ -27,6 +28,8 @@ }, "repository": "github:cap2UI5/builder-cap2UI5", "devDependencies": { + "@eslint/js": "^9.39.5", + "eslint": "^9.39.5", "jest": "^30.4.2" } } diff --git a/scripts/assemble-cap.js b/scripts/assemble-cap.js index 336d0c1..b74ddd6 100644 --- a/scripts/assemble-cap.js +++ b/scripts/assemble-cap.js @@ -127,8 +127,16 @@ console.log(` overlay webapp (from the core) → app/z2ui5/webapp: ${countFiles // inside core/ (same layout as a standalone install of the core) and the // app lock deterministic without a registry roundtrip. const appLockPath = path.join(dest, "package-lock.json"); -const appLock = JSON.parse(fs.readFileSync(appLockPath, "utf8")); -const coreLock = JSON.parse(fs.readFileSync(path.join(coreSrc, "package-lock.json"), "utf8")); +let appLock, coreLock; +try { + appLock = JSON.parse(fs.readFileSync(appLockPath, "utf8")); + coreLock = JSON.parse(fs.readFileSync(path.join(coreSrc, "package-lock.json"), "utf8")); +} catch (e) { + // Symmetry with the guarded parse in the validation block below — fail with + // a clear message instead of a raw stack trace. + console.error(`assemble: failed to read/parse a lockfile before the core-lock merge: ${e.message}`); + process.exit(1); +} let merged = 0; for (const [key, entry] of Object.entries(coreLock.packages || {})) { if (!key.startsWith("node_modules/")) continue; @@ -162,6 +170,28 @@ console.log(` merge core lock → package-lock.json: ${merged} entries under co } if (!appLock.packages?.["core"]) problems.push(`lock has no "core" package entry`); if (merged === 0) problems.push("core lock merge produced 0 entries — empty/renamed core lock?"); + + // Stale core-lock drift guard — the biggest latent risk in this pipeline. + // The app lock's "core" entry is a FROZEN snapshot of the core's + // package.json, taken when src/package-lock.json was last regenerated. + // mirror_core does NOT refresh it, so if the mirrored core changed its own + // dependencies, the vendored core/node_modules/* entries merged above + // disagree with this frozen entry and `npm ci` in the app fails with an + // out-of-sync lock. Catch it here with an actionable message. + try { + const coreManifest = JSON.parse(fs.readFileSync(path.join(coreSrc, "package.json"), "utf8")); + const frozen = JSON.stringify(appLock.packages?.["core"]?.dependencies ?? {}); + const actual = JSON.stringify(coreManifest.dependencies ?? {}); + if (frozen !== actual) { + problems.push( + `core dependencies drifted from the frozen lock: the mirrored core declares ${actual} but ` + + `src/package-lock.json's "core" entry has ${frozen}. Run \`npm install\` in src/ to refresh the lock.`, + ); + } + } catch (e) { + problems.push(`could not compare core dependencies for drift: ${e.message}`); + } + if (problems.length) { console.error(`assemble: output validation FAILED —\n - ${problems.join("\n - ")}`); process.exit(1); diff --git a/src/srv/server.js b/src/srv/server.js index 66c4d4c..8ab2d4b 100644 --- a/src/srv/server.js +++ b/src/srv/server.js @@ -67,6 +67,13 @@ cds.on("bootstrap", (app) => { (_req, res) => res.status(404).end(), ); + // Auth boundary: the DATA endpoints — the POST z2ui5 roundtrip action and + // the AdminService OData entities — are restricted to authenticated users + // (@requires in z2ui5-service.cds). The GET/HEAD routes below are + // deliberately left public: they serve only the static UI5 bootstrap shell + // and the CSRF/terminate ack, carry no user data, and keeping them open + // preserves the offline/dev flow. In BTP the approuter authenticates before + // the frontend can reach them anyway. app.get("/rest/root/z2ui5", (req, res) => { // The engine call renders arbitrary app HTML — never let a failure // escape as an unhandled express error (raw stack trace to the client). diff --git a/test/assemble-cap.test.js b/test/assemble-cap.test.js index 97a7339..0922133 100644 --- a/test/assemble-cap.test.js +++ b/test/assemble-cap.test.js @@ -45,7 +45,10 @@ function makeFixture() { name: "fixture-app", dependencies: { abap2UI5: "file:../run/input/core", express: "^5" }, }, - "../run/input/core": { name: "abap2UI5", version: "1.0.0" }, + // Mirrors the real lock: the frozen core entry carries the core's own + // declared dependencies, which the assemble drift-guard checks against + // run/input/core/package.json. + "../run/input/core": { name: "abap2UI5", version: "1.0.0", dependencies: { "openui5-dist": "1.113.0" } }, "node_modules/abap2UI5": { resolved: "../run/input/core", link: true }, "node_modules/express": { version: "5.0.0", resolved: "https://registry.npmjs.org/express/-/express-5.0.0.tgz" }, }, @@ -252,4 +255,17 @@ describe("assemble-cap guardrails", () => { expect(res.status).toBe(1); expect(res.stderr).toContain("still references run/input/core"); }); + + test("fails when the mirrored core's dependencies drift from the frozen lock entry", () => { + root = makeFixture(); + // The mirrored core adds a dependency that the frozen src lock entry + // (../run/input/core) does not know about — an npm-ci-breaking drift. + const corePkgPath = path.join(root, "run", "input", "core", "package.json"); + const corePkg = readJson(corePkgPath); + corePkg.dependencies["some-new-dep"] = "^2.0.0"; + writeJson(corePkgPath, corePkg); + const res = runAssemble(root); + expect(res.status).toBe(1); + expect(res.stderr).toContain("core dependencies drifted"); + }); });