diff --git a/AGENTS.md b/AGENTS.md index 95d9032..57d0414 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,3 +35,4 @@ This application uses Cloudflare Developer Platform, including Workers and Durab ## Implementation - When coding new features, create tests that cover the new code +- Run code linting with `npm run lint` before commiting the code and fix the issues it highlights diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..c24e517 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,50 @@ +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import globals from 'globals'; + +export default [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.browser, + }, + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', // Too many existing instances to fix now + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'no-empty': ['error', { allowEmptyCatch: true }], + 'prefer-const': 'error', + 'preserve-caught-error': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + }, + }, + { + files: ['public/**/*.js'], + rules: { + 'no-unused-vars': 'off', // Handled by typescript-eslint for ts files, but public js might need a different approach + }, + }, + { + ignores: [ + 'node_modules/', + '.wrangler/', + 'dist/', + 'coverage/', + 'worker-configuration.d.ts', + 'public/users/power-strip.js', // Existing legacy JS + ], + }, +]; diff --git a/package-lock.json b/package-lock.json index ebf512c..0f756b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,18 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { - "prettier": "^3.8.1" + "prettier": "^3.8.1", + "zod": "^3.25.76" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.12.4", + "@eslint/js": "^10.0.1", "@vitest/coverage-istanbul": "^3.2.4", "@vitest/coverage-v8": "^3.2.4", + "eslint": "^10.0.1", + "globals": "^17.3.0", "typescript": "^5.5.2", + "typescript-eslint": "^8.56.0", "vitest": "~3.2.0", "wrangler": "^4.60.0" } @@ -939,6 +944,225 @@ "node": ">=18" } }, + "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.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", + "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.2", + "debug": "^4.3.1", + "minimatch": "^10.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-array/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/@eslint/config-array/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", + "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", + "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "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.2", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", + "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", + "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.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/@img/colour": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", @@ -1957,6 +2181,13 @@ "dev": true, "license": "MIT" }, + "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.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1964,6 +2195,243 @@ "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.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.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.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" + }, + "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.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "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.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.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.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "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/@vitest/coverage-istanbul": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/coverage-istanbul/-/coverage-istanbul-3.2.4.tgz", @@ -2138,22 +2606,62 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "bin": { + "acorn": "bin/acorn" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "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.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "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-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", @@ -2421,6 +2929,13 @@ "node": ">=6" } }, + "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/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2521,6 +3036,200 @@ "node": ">=6" } }, + "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.0.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.1.tgz", + "integrity": "sha512-20MV9SUdeN6Jd84xESsKhRly+/vxI+hwvpBMA93s+9dAcjdCuCojn4IqUGS3lvVaqjVYGYHSRMCpeFtF2rQYxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.2", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.0", + "@eslint/plugin-kit": "^0.6.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.1", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "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.1", + "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.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", + "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "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/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/eslint/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/espree": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", + "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", + "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/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -2531,6 +3240,16 @@ "@types/estree": "^1.0.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/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -2541,6 +3260,27 @@ "node": ">=12.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", + "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/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2559,6 +3299,57 @@ } } }, + "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.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "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", @@ -2622,6 +3413,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": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2639,6 +3456,36 @@ "dev": true, "license": "MIT" }, + "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-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2649,6 +3496,19 @@ "node": ">=8" } }, + "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", @@ -2787,6 +3647,27 @@ "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-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", @@ -2800,6 +3681,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/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -2810,6 +3701,36 @@ "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/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/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -2936,6 +3857,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -2943,6 +3871,56 @@ "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/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -2950,6 +3928,16 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "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", @@ -3050,6 +4038,16 @@ "node": "^10 || ^12 || >=14" } }, + "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.8.1", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", @@ -3065,6 +4063,16 @@ "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/rollup": { "version": "4.56.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", @@ -3441,6 +4449,19 @@ "node": ">=14.0.0" } }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3449,6 +4470,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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3463,6 +4497,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.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.0.0" + } + }, "node_modules/undici": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", @@ -3514,6 +4572,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/vite": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", @@ -3718,6 +4786,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/workerd": { "version": "1.20260120.0", "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260120.0.tgz", @@ -3901,6 +4979,19 @@ "dev": true, "license": "ISC" }, + "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" + } + }, "node_modules/youch": { "version": "4.1.0-beta.10", "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", @@ -3930,7 +5021,6 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index ef4abef..9369f50 100644 --- a/package.json +++ b/package.json @@ -11,20 +11,27 @@ "deploy": "wrangler deploy", "dev": "wrangler dev -c wrangler.local.jsonc", "preview": "wrangler dev --env preview", - "test": "vitest run", + "test": "eslint . && vitest run", "test:coverage": "vitest run --coverage --coverage.provider=istanbul", "cf-typegen": "wrangler types", - "format": "npx prettier . --write" + "format": "npx prettier . --write", + "lint": "eslint .", + "lint:fix": "eslint . --fix" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.12.4", + "@eslint/js": "^10.0.1", "@vitest/coverage-istanbul": "^3.2.4", "@vitest/coverage-v8": "^3.2.4", + "eslint": "^10.0.1", + "globals": "^17.3.0", "typescript": "^5.5.2", + "typescript-eslint": "^8.56.0", "vitest": "~3.2.0", "wrangler": "^4.60.0" }, "dependencies": { - "prettier": "^3.8.1" + "prettier": "^3.8.1", + "zod": "^3.25.76" } } diff --git a/src/AccountDO.ts b/src/AccountDO.ts index c95d037..cdaf6b0 100644 --- a/src/AccountDO.ts +++ b/src/AccountDO.ts @@ -3,6 +3,10 @@ import { initPlans } from './billing/plansConfig'; import { Plan } from './billing/Plan'; import { MockPaymentEngine } from './billing/PaymentEngine'; import { StartupAPIEnv } from './StartupAPIEnv'; +import { AccountInfoSchema, MemberSchema } from './schemas/account'; +import { BillingStateSchema } from './schemas/billing'; +import type { AccountInfo, Member } from './schemas/account'; +import type { BillingState } from './schemas/billing'; /** * A Durable Object representing an Account (Tenant). @@ -68,39 +72,40 @@ export class AccountDO extends DurableObject { return { success: true }; } - async getInfo() { + async getInfo(): Promise { try { const result = this.sql.exec('SELECT * FROM account_info WHERE id = 1'); const row = result.next().value as any; if (!row) return {}; - return { + return AccountInfoSchema.parse({ name: row.name, plan: row.plan, personal: row.personal === 1, billing: row.billing ? JSON.parse(row.billing) : undefined, - }; - } catch (e) { + }); + } catch (_e) { return {}; } } async updateInfo(data: Record) { try { + const validatedData = AccountInfoSchema.partial().parse(data); const updates: string[] = []; const values: any[] = []; - if ('name' in data) { + if ('name' in validatedData) { updates.push('name = ?'); - values.push(typeof data.name === 'string' ? data.name.substring(0, 50) : data.name); + values.push(typeof validatedData.name === 'string' ? validatedData.name.substring(0, 50) : validatedData.name); } - if ('plan' in data) { + if ('plan' in validatedData) { updates.push('plan = ?'); - values.push(data.plan); + values.push(validatedData.plan); } - if ('personal' in data) { + if ('personal' in validatedData) { updates.push('personal = ?'); - values.push(data.personal ? 1 : 0); + values.push(validatedData.personal ? 1 : 0); } if (updates.length > 0) { @@ -111,7 +116,7 @@ export class AccountDO extends DurableObject { if ('plan' in data) { const currentState = this.getBillingState(); if (currentState.plan_slug !== data.plan) { - const newState = { + const newState: BillingState = { ...currentState, plan_slug: data.plan, }; @@ -121,12 +126,12 @@ export class AccountDO extends DurableObject { }); } return { success: true }; - } catch (e: any) { - throw new Error(e.message); + } catch (e) { + throw new Error(e instanceof Error ? e.message : String(e)); } } - async getMembers() { + async getMembers(): Promise { const result = Array.from(this.sql.exec('SELECT user_id, role, joined_at FROM members')); const membersWithNames = await Promise.all( result.map(async (m: any) => { @@ -142,13 +147,13 @@ export class AccountDO extends DurableObject { picture = null; } - return { + return MemberSchema.parse({ ...m, name: profile.name || 'Unknown User', picture: picture, - }; - } catch (e) { - return { ...m, name: 'Unknown User', picture: null }; + }); + } catch (_e) { + return MemberSchema.parse({ ...m, name: 'Unknown User', picture: null }); } }), ); @@ -168,8 +173,8 @@ export class AccountDO extends DurableObject { try { const userStub = this.env.USER.get(this.env.USER.idFromString(user_id)); await userStub.addMembership(this.ctx.id.toString(), role, false); - } catch (e) { - console.error('Failed to sync membership to UserDO', e); + } catch (_e) { + console.error('Failed to sync membership to UserDO', _e); } return { success: true }; @@ -182,8 +187,8 @@ export class AccountDO extends DurableObject { try { const userStub = this.env.USER.get(this.env.USER.idFromString(userId)); await userStub.addMembership(this.ctx.id.toString(), role, false); - } catch (e) { - console.error('Failed to sync membership role to UserDO', e); + } catch (_e) { + console.error('Failed to sync membership role to UserDO', _e); } return { success: true }; @@ -199,8 +204,8 @@ export class AccountDO extends DurableObject { try { const userStub = this.env.USER.get(this.env.USER.idFromString(userId)); await userStub.deleteMembership(this.ctx.id.toString()); - } catch (e) { - console.error('Failed to sync membership removal to UserDO', e); + } catch (_e) { + console.error('Failed to sync membership removal to UserDO', _e); } return { success: true }; @@ -214,8 +219,8 @@ export class AccountDO extends DurableObject { const systemStub = this.env.SYSTEM.get(this.env.SYSTEM.idFromName('global')); await systemStub.updateMemberCount(this.ctx.id.toString(), count); - } catch (e) { - console.error('Failed to update member count in SystemDO', e); + } catch (_e) { + console.error('Failed to update member count in SystemDO', _e); } } @@ -226,8 +231,8 @@ export class AccountDO extends DurableObject { try { const userStub = this.env.USER.get(this.env.USER.idFromString(member.user_id)); await userStub.deleteMembership(this.ctx.id.toString()); - } catch (e) { - console.error(`Failed to notify UserDO ${member.user_id} of account deletion`, e); + } catch (_e) { + console.error(`Failed to notify UserDO ${member.user_id} of account deletion`, _e); } } @@ -247,21 +252,23 @@ export class AccountDO extends DurableObject { // Billing Implementation - private getBillingState(): any { + private getBillingState(): BillingState { try { const result = this.sql.exec('SELECT billing FROM account_info WHERE id = 1'); const row = result.next().value as any; if (row && row.billing) { - return JSON.parse(row.billing); + return BillingStateSchema.parse(JSON.parse(row.billing)); } - } catch (e) {} + } catch (_e) { + // ignore + } return { plan_slug: 'free', status: 'active', }; } - private setBillingState(state: any) { + private setBillingState(state: BillingState) { this.ctx.storage.transactionSync(() => { this.sql.exec('UPDATE account_info SET billing = ?, plan = ? WHERE id = 1', JSON.stringify(state), state.plan_slug); }); @@ -318,11 +325,11 @@ export class AccountDO extends DurableObject { // Setup recurring payment try { await this.paymentEngine.setupRecurring(this.ctx.id.toString(), plan_slug, schedule_idx); - } catch (e: any) { - throw new Error(`Payment setup failed: ${e.message}`); + } catch (e) { + throw new Error(`Payment setup failed: ${e instanceof Error ? e.message : String(e)}`); } - const newState = { + const newState: BillingState = { ...currentState, plan_slug, status: 'active', @@ -348,7 +355,7 @@ export class AccountDO extends DurableObject { // Downgrade logic (immediate or scheduled - simplification: scheduled if downgrade_to_slug exists) // For this prototype, we'll mark it as canceled and set the next plan if applicable. - const newState = { + const newState: BillingState = { ...currentState, status: 'canceled', next_plan_slug: currentPlan.downgrade_to_slug, diff --git a/src/CredentialDO.ts b/src/CredentialDO.ts index b623d42..1365106 100644 --- a/src/CredentialDO.ts +++ b/src/CredentialDO.ts @@ -1,5 +1,7 @@ import { DurableObject } from 'cloudflare:workers'; import { StartupAPIEnv } from './StartupAPIEnv'; +import { OAuthCredentialSchema } from './schemas/credential'; +import type { OAuthCredential } from './schemas/credential'; /** * A Durable Object representing all OAuth credentials for a specific provider. @@ -47,21 +49,22 @@ export class CredentialDO extends DurableObject { return credentials; } - async put(data: any) { + async put(data: OAuthCredential) { + const validatedData = OAuthCredentialSchema.parse(data); const now = Date.now(); this.sql.exec( `INSERT OR REPLACE INTO credentials (subject_id, user_id, access_token, refresh_token, expires_at, scope, profile_data, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - data.subject_id, - data.user_id, - data.access_token, - data.refresh_token, - data.expires_at, - data.scope, - JSON.stringify(data.profile_data), - data.created_at || now, + validatedData.subject_id, + validatedData.user_id, + validatedData.access_token, + validatedData.refresh_token, + validatedData.expires_at, + validatedData.scope, + JSON.stringify(validatedData.profile_data), + validatedData.created_at || now, now, ); return { success: true }; diff --git a/src/SystemDO.ts b/src/SystemDO.ts index e0739e8..c39fbb8 100644 --- a/src/SystemDO.ts +++ b/src/SystemDO.ts @@ -1,5 +1,9 @@ import { DurableObject } from 'cloudflare:workers'; import { StartupAPIEnv } from './StartupAPIEnv'; +import { SystemUserSchema } from './schemas/user'; +import { SystemAccountSchema } from './schemas/account'; +import type { SystemUser } from './schemas/user'; +import type { SystemAccount } from './schemas/account'; export class SystemDO extends DurableObject { sql: SqlStorage; @@ -48,7 +52,7 @@ export class SystemDO extends DurableObject { adminIds.some((id) => { try { return u.id === this.env.USER.idFromName(id).toString(); - } catch (e) { + } catch (_e) { return false; } })); @@ -71,20 +75,21 @@ export class SystemDO extends DurableObject { const userStub = this.env.USER.get(this.env.USER.idFromString(userId)); const profile = await userStub.getProfile(); return profile; - } catch (e: any) { - throw new Error(e.message); + } catch (e) { + throw new Error(e instanceof Error ? e.message : String(e)); } } - async registerUser(data: { id: string; name: string; email?: string; provider?: string }) { + async registerUser(data: SystemUser) { + const validatedData = SystemUserSchema.parse(data); const now = Date.now(); this.sql.exec( 'INSERT OR REPLACE INTO users (id, name, email, provider, created_at) VALUES (?, ?, ?, ?, ?)', - data.id, - data.name, - data.email || null, - data.provider || null, + validatedData.id, + validatedData.name, + validatedData.email || null, + validatedData.provider || null, now, ); @@ -106,26 +111,27 @@ export class SystemDO extends DurableObject { return { success: true }; } - async updateUser(userId: string, data: any) { + async updateUser(userId: string, data: Partial) { + const validatedData = SystemUserSchema.partial().parse(data); // Update UserDO try { const userStub = this.env.USER.get(this.env.USER.idFromString(userId)); - await userStub.updateProfile(data); + await userStub.updateProfile(validatedData); } catch (e) { console.error('Failed to update UserDO', e); } // Update Index - if (data.name || data.email) { + if (validatedData.name || validatedData.email) { const updates: string[] = []; const args: any[] = []; - if (data.name !== undefined) { + if (validatedData.name !== undefined) { updates.push('name = ?'); - args.push(data.name); + args.push(validatedData.name); } - if (data.email !== undefined) { + if (validatedData.email !== undefined) { updates.push('email = ?'); - args.push(data.email); + args.push(validatedData.email); } if (updates.length > 0) { @@ -159,14 +165,15 @@ export class SystemDO extends DurableObject { const billing = await stub.getBillingInfo(); return { ...info, billing }; - } catch (e: any) { - throw new Error(e.message); + } catch (e) { + throw new Error(e instanceof Error ? e.message : String(e)); } } - async registerAccount(data: { id?: string; name: string; status?: string; plan?: string; ownerId?: string }) { - let accountIdStr = data.id; - const accountName = (data.name || '').substring(0, 50); + async registerAccount(data: SystemAccount) { + const validatedData = SystemAccountSchema.parse(data); + let accountIdStr = validatedData.id; + const accountName = validatedData.name; if (!accountIdStr) { const id = this.env.ACCOUNT.newUniqueId(); @@ -179,8 +186,8 @@ export class SystemDO extends DurableObject { }); // If owner provided, add them as ADMIN - if (data.ownerId) { - await stub.addMember(data.ownerId, 1); + if (validatedData.ownerId) { + await stub.addMember(validatedData.ownerId, 1); } } @@ -190,9 +197,9 @@ export class SystemDO extends DurableObject { 'INSERT OR REPLACE INTO accounts (id, name, status, plan, member_count, created_at) VALUES (?, ?, ?, ?, ?, ?)', accountIdStr, accountName, - data.status || 'active', - data.plan || 'free', - data.ownerId ? 1 : 0, + validatedData.status || 'active', + validatedData.plan || 'free', + validatedData.ownerId ? 1 : 0, now, ); @@ -218,16 +225,13 @@ export class SystemDO extends DurableObject { this.sql.exec('UPDATE accounts SET member_count = ? WHERE id = ?', count, accountId); } - async updateAccount(accountId: string, data: any) { - const sanitizedData = { ...data }; - if (sanitizedData.name !== undefined) { - sanitizedData.name = sanitizedData.name.substring(0, 50); - } + async updateAccount(accountId: string, data: Partial) { + const validatedData = SystemAccountSchema.partial().parse(data); // Update AccountDO try { const stub = this.env.ACCOUNT.get(this.env.ACCOUNT.idFromString(accountId)); - await stub.updateInfo(sanitizedData); + await stub.updateInfo(validatedData); } catch (e) { console.error('Failed to update AccountDO', e); } @@ -236,18 +240,18 @@ export class SystemDO extends DurableObject { const updates: string[] = []; const args: any[] = []; - if (sanitizedData.name !== undefined) { + if (validatedData.name !== undefined) { updates.push('name = ?'); - args.push(sanitizedData.name); + args.push(validatedData.name); } - if (data.status !== undefined) { + if (validatedData.status !== undefined) { updates.push('status = ?'); - args.push(data.status); + args.push(validatedData.status); } // Plan update usually via billing, but if forced: - if (data.plan !== undefined) { + if (validatedData.plan !== undefined) { updates.push('plan = ?'); - args.push(data.plan); + args.push(validatedData.plan); } if (updates.length > 0) { diff --git a/src/UserDO.ts b/src/UserDO.ts index ed22899..d08034a 100644 --- a/src/UserDO.ts +++ b/src/UserDO.ts @@ -1,5 +1,7 @@ import { DurableObject } from 'cloudflare:workers'; import { StartupAPIEnv } from './StartupAPIEnv'; +import { UserProfileSchema } from './schemas/user'; +import type { UserProfile } from './schemas/user'; /** * A Durable Object representing a User. @@ -81,7 +83,7 @@ export class UserDO extends DurableObject { // Determine login context (provider and subject_id) const sessionMeta = session.meta ? JSON.parse(session.meta) : {}; const loginProvider = sessionMeta.provider; - let credential: Record = {}; + const credential: Record = {}; if (loginProvider) { credential.provider = loginProvider; @@ -104,7 +106,7 @@ export class UserDO extends DurableObject { profile.id = this.ctx.id.toString(); return { valid: true, profile, credential }; - } catch (e) { + } catch (_e) { return { valid: false }; } } @@ -112,22 +114,22 @@ export class UserDO extends DurableObject { /** * Retrieves the user's profile data. * - * @returns A Promise resolving to a JSON response containing the profile key-value pairs. + * @returns A Promise resolving to the user profile. */ - async getProfile() { + async getProfile(): Promise { try { const result = this.sql.exec('SELECT * FROM profile WHERE id = 1'); const row = result.next().value as any; if (!row) return {}; - return { + return UserProfileSchema.parse({ name: row.name, email: row.email, picture: row.picture, provider: row.provider, verified_email: row.verified_email === 1, - }; - } catch (e) { + }); + } catch (_e) { return {}; } } @@ -141,28 +143,29 @@ export class UserDO extends DurableObject { */ async updateProfile(data: Record) { try { + const validatedData = UserProfileSchema.partial().parse(data); const updates: string[] = []; const values: any[] = []; - if ('name' in data) { + if ('name' in validatedData) { updates.push('name = ?'); - values.push(data.name); + values.push(validatedData.name); } - if ('email' in data) { + if ('email' in validatedData) { updates.push('email = ?'); - values.push(data.email); + values.push(validatedData.email); } - if ('picture' in data) { + if ('picture' in validatedData) { updates.push('picture = ?'); - values.push(data.picture); + values.push(validatedData.picture); } - if ('provider' in data) { + if ('provider' in validatedData) { updates.push('provider = ?'); - values.push(data.provider); + values.push(validatedData.provider); } - if ('verified_email' in data) { + if ('verified_email' in validatedData) { updates.push('verified_email = ?'); - values.push(data.verified_email ? 1 : 0); + values.push(validatedData.verified_email ? 1 : 0); } if (updates.length > 0) { @@ -171,8 +174,8 @@ export class UserDO extends DurableObject { }); } return { success: true }; - } catch (e: any) { - return { success: false, error: e.message }; + } catch (e) { + return { success: false, error: e instanceof Error ? e.message : String(e) }; } } @@ -243,7 +246,9 @@ export class UserDO extends DurableObject { async deleteSession(sessionId: string) { try { this.sql.exec('DELETE FROM sessions WHERE id = ?', sessionId); - } catch (e) {} + } catch (_e) { + // Ignore + } return { success: true }; } @@ -251,7 +256,7 @@ export class UserDO extends DurableObject { try { const result = this.sql.exec('SELECT account_id, role, is_current FROM memberships'); return Array.from(result); - } catch (e) { + } catch (_e) { return []; } } @@ -292,8 +297,8 @@ export class UserDO extends DurableObject { this.sql.exec('UPDATE memberships SET is_current = 1 WHERE account_id = ?', account_id); }); return { success: true }; - } catch (e: any) { - throw new Error(e.message); + } catch (e) { + throw new Error(e instanceof Error ? e.message : String(e)); } } @@ -350,8 +355,8 @@ export class UserDO extends DurableObject { try { const stub = this.env.CREDENTIAL.get(this.env.CREDENTIAL.idFromName(row.provider as string)); await stub.delete(row.subject_id as string); - } catch (e) { - console.error(`Failed to delete credential mapping for provider ${row.provider}`, e); + } catch (_e) { + console.error(`Failed to delete credential mapping for provider ${row.provider}`, _e); } } diff --git a/src/auth/index.ts b/src/auth/index.ts index 6bdd353..edc2d6d 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -103,7 +103,9 @@ export async function handleAuth( if (staleSessionId) { try { await userStub.deleteSession(staleSessionId); - } catch (e) {} + } catch (_e) { + // ignore + } } userIdStr = null; } @@ -132,7 +134,6 @@ export async function handleAuth( // Register credential in provider-specific CredentialDO await credentialStub.put({ user_id: userIdStr, - provider: provider.name, subject_id: profile.id, access_token: token.access_token, refresh_token: token.refresh_token, @@ -200,7 +201,7 @@ export async function handleAuth( if (parsedReturn.origin === origin) { redirectUrl = parsedReturn.toString(); } - } catch (e) { + } catch (_e) { if (returnUrl.startsWith('/')) { redirectUrl = returnUrl; } @@ -209,8 +210,8 @@ export async function handleAuth( headers.set('Location', redirectUrl); return new Response(null, { status: 302, headers }); - } catch (e: any) { - return new Response('Auth failed: ' + e.message, { status: 500 }); + } catch (e) { + return new Response(`Auth failed: ${e instanceof Error ? e.message : String(e)}`, { status: 500 }); } } } diff --git a/src/index.ts b/src/index.ts index f3dbb89..e339e57 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,10 @@ import { CredentialDO } from './CredentialDO'; import { CookieManager } from './CookieManager'; import { initPlans } from './billing/plansConfig'; import { Plan } from './billing/Plan'; +import { UserProfileSchema } from './schemas/user'; +import { AccountInfoSchema, MemberSchema, SwitchAccountSchema, SystemAccountSchema } from './schemas/account'; +import { DeleteCredentialSchema } from './schemas/credential'; +import { ImpersonateSchema } from './schemas/admin'; const DEFAULT_USERS_PATH = '/users/'; @@ -18,7 +22,7 @@ export default { /** * Main Worker fetch handler. */ - async fetch(request: Request, env: StartupAPIEnv, ctx): Promise { + async fetch(request: Request, env: StartupAPIEnv): Promise { initPlans(); // Prevent infinite loops when serving assets @@ -205,82 +209,97 @@ async function handleAdmin(request: Request, env: StartupAPIEnv, usersPath: stri const systemStub = env.SYSTEM.get(env.SYSTEM.idFromName('global')); if (path.startsWith('/api/')) { - const apiPath = path.replace('/api/', ''); - const parts = apiPath.split('/'); + try { + const apiPath = path.replace('/api/', ''); + const parts = apiPath.split('/'); - if (parts[0] === 'users') { - if (parts.length === 1 && request.method === 'GET') { - return Response.json(await systemStub.listUsers(url.searchParams.get('q') || undefined)); - } - if (parts.length === 2) { - const userId = parts[1]; - if (request.method === 'GET') return Response.json(await systemStub.getUser(userId)); - if (request.method === 'DELETE') return Response.json(await systemStub.deleteUser(userId)); - if (request.method === 'PATCH' || request.method === 'PUT') { - const data = (await request.json()) as any; - return Response.json(await systemStub.updateUser(userId, data)); + if (parts[0] === 'users') { + if (parts.length === 1 && request.method === 'GET') { + return Response.json(await systemStub.listUsers(url.searchParams.get('q') || undefined)); } - } - if (parts.length === 3 && parts[2] === 'memberships' && request.method === 'GET') { - const userId = parts[1]; - return Response.json(await systemStub.getUserMemberships(userId)); - } - } else if (parts[0] === 'accounts') { - if (parts.length === 1) { - if (request.method === 'GET') return Response.json(await systemStub.listAccounts(url.searchParams.get('q') || undefined)); - if (request.method === 'POST') return Response.json(await systemStub.registerAccount(await request.json())); - } - if (parts.length === 2) { - const accountId = parts[1]; - if (request.method === 'GET') return Response.json(await systemStub.getAccount(accountId)); - if (request.method === 'PUT') return Response.json(await systemStub.updateAccount(accountId, await request.json())); - if (request.method === 'DELETE') return Response.json(await systemStub.deleteAccount(accountId)); - } - if (parts.length >= 3 && parts[2] === 'members') { - const accountId = parts[1]; - const accountStub = env.ACCOUNT.get(env.ACCOUNT.idFromString(accountId)); - if (parts.length === 3) { - if (request.method === 'GET') return Response.json(await accountStub.getMembers()); + if (parts.length === 2) { + const userId = parts[1]; + if (request.method === 'GET') return Response.json(await systemStub.getUser(userId)); + if (request.method === 'DELETE') return Response.json(await systemStub.deleteUser(userId)); + if (request.method === 'PATCH' || request.method === 'PUT') { + const data = await request.json(); + const validatedData = UserProfileSchema.partial().parse(data); + return Response.json(await systemStub.updateUser(userId, validatedData)); + } + } + if (parts.length === 3 && parts[2] === 'memberships' && request.method === 'GET') { + const userId = parts[1]; + return Response.json(await systemStub.getUserMemberships(userId)); + } + } else if (parts[0] === 'accounts') { + if (parts.length === 1) { + if (request.method === 'GET') return Response.json(await systemStub.listAccounts(url.searchParams.get('q') || undefined)); if (request.method === 'POST') { - const data = (await request.json()) as any; - return Response.json(await accountStub.addMember(data.user_id, data.role)); + const data = await request.json(); + const validatedData = SystemAccountSchema.parse(data); + return Response.json(await systemStub.registerAccount(validatedData)); } - } else if (parts.length === 4 && request.method === 'DELETE') { - return Response.json(await accountStub.removeMember(parts[3])); } - } - } else if (parts[0] === 'impersonate' && request.method === 'POST') { - const data = (await request.json()) as any; - const user_id = data.user_id || data.userId; - if (!user_id) return new Response('Missing user_id', { status: 400 }); + if (parts.length === 2) { + const accountId = parts[1]; + if (request.method === 'GET') return Response.json(await systemStub.getAccount(accountId)); + if (request.method === 'PUT') { + const data = await request.json(); + const validatedData = SystemAccountSchema.partial().parse(data); + return Response.json(await systemStub.updateAccount(accountId, validatedData)); + } + if (request.method === 'DELETE') return Response.json(await systemStub.deleteAccount(accountId)); + } + if (parts.length >= 3 && parts[2] === 'members') { + const accountId = parts[1]; + const accountStub = env.ACCOUNT.get(env.ACCOUNT.idFromString(accountId)); + if (parts.length === 3) { + if (request.method === 'GET') return Response.json(await accountStub.getMembers()); + if (request.method === 'POST') { + const data = await request.json(); + const { user_id, role } = MemberSchema.parse(data); + return Response.json(await accountStub.addMember(user_id, role)); + } + } else if (parts.length === 4 && request.method === 'DELETE') { + return Response.json(await accountStub.removeMember(parts[3])); + } + } + } else if (parts[0] === 'impersonate' && request.method === 'POST') { + const body = await request.json(); + const data = ImpersonateSchema.parse(body); + const user_id = data.user_id || data.userId; + if (!user_id) return new Response('Missing user_id', { status: 400 }); + + if (user_id === user.id) { + return new Response('Cannot impersonate yourself', { status: 400 }); + } - if (user_id === user.id) { - return new Response('Cannot impersonate yourself', { status: 400 }); - } + const userDOId = env.USER.idFromString(user_id); + const userStub = env.USER.get(userDOId); + const session = await userStub.createSession({ provider: 'admin-impersonation', impersonator: user.id }); - const userDOId = env.USER.idFromString(user_id); - const userStub = env.USER.get(userDOId); - const session = await userStub.createSession({ provider: 'admin-impersonation', impersonator: user.id }); - - const cookieHeader = request.headers.get('Cookie'); - const cookies = parseCookies(cookieHeader || ''); - const currentSessionEncrypted = cookies['session_id']; - - const headers = new Headers(); - const newSessionIdEncrypted = await cookieManager.encrypt(`${session.sessionId}:${user_id}`); - headers.set('Set-Cookie', `session_id=${newSessionIdEncrypted}; Path=/; HttpOnly; Secure; SameSite=Lax`); - if (currentSessionEncrypted) { - const backupSession = await cookieManager.decrypt(currentSessionEncrypted); - if (backupSession) { - const backupSessionEncrypted = await cookieManager.encrypt(backupSession); - headers.append('Set-Cookie', `backup_session_id=${backupSessionEncrypted}; Path=/; HttpOnly; Secure; SameSite=Lax`); + const cookieHeader = request.headers.get('Cookie'); + const cookies = parseCookies(cookieHeader || ''); + const currentSessionEncrypted = cookies['session_id']; + + const headers = new Headers(); + const newSessionIdEncrypted = await cookieManager.encrypt(`${session.sessionId}:${user_id}`); + headers.set('Set-Cookie', `session_id=${newSessionIdEncrypted}; Path=/; HttpOnly; Secure; SameSite=Lax`); + if (currentSessionEncrypted) { + const backupSession = await cookieManager.decrypt(currentSessionEncrypted); + if (backupSession) { + const backupSessionEncrypted = await cookieManager.encrypt(backupSession); + headers.append('Set-Cookie', `backup_session_id=${backupSessionEncrypted}; Path=/; HttpOnly; Secure; SameSite=Lax`); + } } + + return Response.json({ success: true }, { headers }); } - return Response.json({ success: true }, { headers }); + return new Response('Not Found', { status: 404 }); + } catch (e) { + return new Response(e instanceof Error ? e.message : String(e), { status: 400 }); } - - return new Response('Not Found', { status: 404 }); } url.pathname = '/users/admin' + path; @@ -343,7 +362,7 @@ async function handleMe(request: Request, env: StartupAPIEnv, cookieManager: Coo } return Response.json(data); - } catch (e) { + } catch (_e) { return new Response('Unauthorized', { status: 401 }); } } @@ -354,11 +373,16 @@ async function handleUpdateProfile(request: Request, env: StartupAPIEnv, cookieM return checkAndClearStaleSession(request, env, cookieManager, new Response('Unauthorized', { status: 401 })); } - const profileData = await request.json(); - const userStub = env.USER.get(env.USER.idFromString(user.id)); - await userStub.updateProfile(profileData); + try { + const profileData = await request.json(); + const validatedData = UserProfileSchema.partial().parse(profileData); + const userStub = env.USER.get(env.USER.idFromString(user.id)); + await userStub.updateProfile(validatedData); - return Response.json({ success: true }); + return Response.json({ success: true }); + } catch (e) { + return new Response(e instanceof Error ? e.message : String(e), { status: 400 }); + } } function isAdmin(user: any, env: StartupAPIEnv): boolean { @@ -375,7 +399,7 @@ function isAdmin(user: any, env: StartupAPIEnv): boolean { adminIds.some((id) => { try { return userId === env.USER.idFromName(id).toString(); - } catch (e) { + } catch (_e) { return false; } })) @@ -412,8 +436,16 @@ async function handleAccountMembers( return Response.json(await accountStub.getMembers()); } if (request.method === 'POST') { - const { user_id, role } = (await request.json()) as { user_id: string; role: number }; - return Response.json(await accountStub.addMember(user_id, role)); + try { + const data = await request.json(); + const { user_id, role } = MemberSchema.partial().parse(data); + if (!user_id || role === undefined) { + return new Response('Missing user_id or role', { status: 400 }); + } + return Response.json(await accountStub.addMember(user_id, role)); + } catch (e) { + return new Response(e instanceof Error ? e.message : String(e), { status: 400 }); + } } } else if (pathParts.length === 1) { const targetUserId = pathParts[0]; @@ -424,11 +456,19 @@ async function handleAccountMembers( return Response.json(await accountStub.removeMember(targetUserId)); } if (request.method === 'PATCH') { - const { role } = (await request.json()) as { role: number }; - if (targetUserId === user.id && role !== AccountDO.ROLE_ADMIN) { - return new Response('Cannot demote yourself', { status: 400 }); + try { + const data = await request.json(); + const { role } = MemberSchema.partial().parse(data); + if (role === undefined) { + return new Response('Missing role', { status: 400 }); + } + if (targetUserId === user.id && role !== AccountDO.ROLE_ADMIN) { + return new Response('Cannot demote yourself', { status: 400 }); + } + return Response.json(await accountStub.updateMemberRole(targetUserId, role)); + } catch (e) { + return new Response(e instanceof Error ? e.message : String(e), { status: 400 }); } - return Response.json(await accountStub.updateMemberRole(targetUserId, role)); } } @@ -466,22 +506,27 @@ async function handleAccountDetails( } if (request.method === 'POST') { - const data = await request.json(); - const result = await accountStub.updateInfo(data); + try { + const data = await request.json(); + const validatedData = AccountInfoSchema.partial().parse(data); + const result = await accountStub.updateInfo(validatedData); - // Sync with SystemDO index if name or plan changed - if (data.name || data.plan) { - try { - const systemStub = env.SYSTEM.get(env.SYSTEM.idFromName('global')); - const updates: any = {}; - if (data.name) updates.name = data.name; - if (data.plan) updates.plan = data.plan; - await systemStub.updateAccount(accountId, updates); - } catch (e) { - console.error('Failed to sync account updates to SystemDO', e); + // Sync with SystemDO index if name or plan changed + if (validatedData.name || validatedData.plan) { + try { + const systemStub = env.SYSTEM.get(env.SYSTEM.idFromName('global')); + const updates: any = {}; + if (validatedData.name) updates.name = validatedData.name; + if (validatedData.plan) updates.plan = validatedData.plan; + await systemStub.updateAccount(accountId, updates); + } catch (_e) { + console.error('Failed to sync account updates to SystemDO', _e); + } } + return Response.json(result); + } catch (e) { + return new Response(e instanceof Error ? e.message : String(e), { status: 400 }); } - return Response.json(result); } return new Response('Method Not Allowed', { status: 405 }); @@ -504,7 +549,9 @@ async function getUserFromSession(request: Request, env: StartupAPIEnv, cookieMa const userStub = env.USER.get(id); const result = await userStub.validateSession(sessionId); if (result.valid) return { id: doId, sessionId, profile: result.profile, credential: result.credential }; - } catch (e) {} + } catch (_e) { + // ignore + } return null; } @@ -553,7 +600,9 @@ async function checkAndClearStaleSession( headers, }); } - } catch (e) {} + } catch (_e) { + // ignore + } return originalResponse; } @@ -574,13 +623,14 @@ async function handleDeleteCredential(request: Request, env: StartupAPIEnv, cook return checkAndClearStaleSession(request, env, cookieManager, new Response('Unauthorized', { status: 401 })); } - const { provider } = (await request.json()) as { provider: string }; - const userStub = env.USER.get(env.USER.idFromString(user.id)); - try { + const data = await request.json(); + const { provider } = DeleteCredentialSchema.parse(data); + const userStub = env.USER.get(env.USER.idFromString(user.id)); + return Response.json(await userStub.deleteCredential(provider)); - } catch (e: any) { - return new Response(e.message, { status: 400 }); + } catch (e) { + return new Response(e instanceof Error ? e.message : String(e), { status: 400 }); } } @@ -615,9 +665,9 @@ async function handleMeImage(request: Request, env: StartupAPIEnv, type: string, } return handleUserImage(request, env, user.id, type, cookieManager); - } catch (e: any) { - console.error('[handleMeImage] Error:', e.message, e.stack); - return new Response('Error fetching image: ' + e.message, { status: 500 }); + } catch (e) { + console.error('[handleMeImage] Error:', e instanceof Error ? e.message : String(e), e instanceof Error ? e.stack : ''); + return new Response(`Error fetching image: ${e instanceof Error ? e.message : String(e)}`, { status: 500 }); } } @@ -626,7 +676,7 @@ async function handleUserImage( env: StartupAPIEnv, userId: string, type: string, - cookieManager: CookieManager, + _cookieManager: CookieManager, ): Promise { // Public access to user avatars (if we want them to be public in member lists) // Or we could check if current user has permission to see it. @@ -639,7 +689,7 @@ async function handleUserImage( const image = await stub.getImage(type); if (!image) return new Response('Not Found', { status: 404 }); return new Response(image.value, { headers: { 'Content-Type': image.mime_type } }); - } catch (e) { + } catch (_e) { return new Response('Error fetching image', { status: 500 }); } } @@ -699,9 +749,9 @@ async function handleAccountImage( const image = await accountStub.getImage(type); if (!image) return new Response('Not Found', { status: 404 }); return new Response(image.value, { headers: { 'Content-Type': image.mime_type } }); - } catch (e: any) { - console.error('[handleAccountImage] Error:', e.message, e.stack); - return new Response('Error handling account image: ' + e.message, { status: 500 }); + } catch (e) { + console.error('[handleAccountImage] Error:', e instanceof Error ? e.message : String(e), e instanceof Error ? e.stack : ''); + return new Response(`Error handling account image: ${e instanceof Error ? e.message : String(e)}`, { status: 500 }); } } @@ -725,8 +775,8 @@ async function handleLogout( const id = env.USER.idFromString(doId); const stub = env.USER.get(id); await stub.deleteSession(sessionId); - } catch (e) { - console.error('Error deleting session:', e); + } catch (_e) { + console.error('Error deleting session:', _e); // Continue to clear cookie even if DO call fails } } @@ -745,7 +795,7 @@ async function handleLogout( if (parsedReturn.origin === origin) { redirectUrl = parsedReturn.toString(); } - } catch (e) { + } catch (_e) { if (returnUrl.startsWith('/')) { redirectUrl = returnUrl; } @@ -791,14 +841,14 @@ async function handleMyAccounts(request: Request, env: StartupAPIEnv, cookieMana role: m.role, is_current: m.is_current, }; - } catch (e) { + } catch (_e) { return { account_id: m.account_id, name: 'Unknown Account', role: m.role, is_current: m.is_current }; } }), ); return Response.json(accounts); - } catch (e) { + } catch (_e) { return new Response('Unauthorized', { status: 401 }); } } @@ -809,18 +859,15 @@ async function handleSwitchAccount(request: Request, env: StartupAPIEnv, cookieM return checkAndClearStaleSession(request, env, cookieManager, new Response('Unauthorized', { status: 401 })); } - const { account_id } = (await request.json()) as { account_id: string }; - - if (!account_id) { - return new Response('Missing account_id', { status: 400 }); - } - try { + const data = await request.json(); + const { account_id } = SwitchAccountSchema.parse(data); + const id = env.USER.idFromString(user.id); const userStub = env.USER.get(id); return Response.json(await userStub.switchAccount(account_id)); - } catch (e: any) { - return new Response(e.message, { status: 400 }); + } catch (e) { + return new Response(e instanceof Error ? e.message : String(e), { status: 400 }); } } @@ -836,7 +883,7 @@ async function handleSSR( return checkAndClearStaleSession(request, env, cookieManager, Response.redirect(url.origin + '/', 302)); } - const { id: doId, sessionId, profile: initialProfile, credential } = user; + const { id: doId, sessionId: _sessionId, profile: initialProfile, credential } = user; try { const id = env.USER.idFromString(doId); @@ -977,9 +1024,9 @@ async function handleSSR( 'Content-Type': 'text/html', }, }); - } catch (e: any) { - console.error('[handleSSR] Error:', e.message, e.stack); - return new Response('Error rendering page: ' + e.message, { status: 500 }); + } catch (e) { + console.error('[handleSSR] Error:', e instanceof Error ? e.message : String(e), e instanceof Error ? e.stack : ''); + return new Response(`Error rendering page: ${e instanceof Error ? e.message : String(e)}`, { status: 500 }); } } diff --git a/src/schemas/account.ts b/src/schemas/account.ts new file mode 100644 index 0000000..288ec44 --- /dev/null +++ b/src/schemas/account.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; + +export const AccountInfoSchema = z.object({ + id: z.string().optional(), + name: z.string().nullable().optional(), + plan: z.string().optional(), + personal: z.boolean().optional(), + billing: z.record(z.any()).optional(), +}); + +export type AccountInfo = z.infer; + +export const MemberSchema = z.object({ + user_id: z.string(), + role: z.number(), + joined_at: z.number().optional(), + name: z.string().optional(), + picture: z.string().nullable().optional(), +}); + +export type Member = z.infer; + +export const SystemAccountSchema = z.object({ + id: z.string().optional(), + name: z.string().max(50), + status: z.string().optional(), + plan: z.string().optional(), + member_count: z.number().optional(), + created_at: z.number().optional(), + ownerId: z.string().optional(), +}); + +export type SystemAccount = z.infer; + +export const SwitchAccountSchema = z.object({ + account_id: z.string(), +}); diff --git a/src/schemas/admin.ts b/src/schemas/admin.ts new file mode 100644 index 0000000..388852a --- /dev/null +++ b/src/schemas/admin.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const ImpersonateSchema = z + .object({ + user_id: z.string().optional(), + userId: z.string().optional(), + }) + .refine((data) => data.user_id || data.userId, { + message: 'Either user_id or userId must be provided', + }); diff --git a/src/schemas/billing.ts b/src/schemas/billing.ts new file mode 100644 index 0000000..0270ee1 --- /dev/null +++ b/src/schemas/billing.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const BillingStateSchema = z.object({ + plan_slug: z.string(), + status: z.enum(['active', 'canceled', 'past_due', 'unpaid', 'trialing']), + schedule_idx: z.number().optional(), + next_billing_date: z.number().optional(), + next_plan_slug: z.string().optional(), +}); + +export type BillingState = z.infer; diff --git a/src/schemas/credential.ts b/src/schemas/credential.ts new file mode 100644 index 0000000..0511fbb --- /dev/null +++ b/src/schemas/credential.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +export const CredentialSchema = z.object({ + provider: z.string(), + subject_id: z.string(), +}); + +export type Credential = z.infer; + +export const OAuthCredentialSchema = z.object({ + subject_id: z.string(), + user_id: z.string(), + access_token: z.string().optional(), + refresh_token: z.string().optional(), + expires_at: z.number().optional(), + scope: z.string().optional(), + profile_data: z.record(z.any()).optional(), + created_at: z.number().optional(), + updated_at: z.number().optional(), +}); + +export type OAuthCredential = z.infer; + +export const DeleteCredentialSchema = z.object({ + provider: z.string(), +}); diff --git a/src/schemas/membership.ts b/src/schemas/membership.ts new file mode 100644 index 0000000..a2d7c1a --- /dev/null +++ b/src/schemas/membership.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const MembershipSchema = z.object({ + account_id: z.string(), + role: z.number().optional(), + is_current: z.boolean().optional(), +}); + +export type Membership = z.infer; diff --git a/src/schemas/session.ts b/src/schemas/session.ts new file mode 100644 index 0000000..bbb78fa --- /dev/null +++ b/src/schemas/session.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const SessionSchema = z.object({ + id: z.string(), + created_at: z.number(), + expires_at: z.number(), + meta: z.record(z.any()).nullable().optional(), +}); + +export type Session = z.infer; diff --git a/src/schemas/user.ts b/src/schemas/user.ts new file mode 100644 index 0000000..dbda0e3 --- /dev/null +++ b/src/schemas/user.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; + +export const UserProfileSchema = z.object({ + id: z.string().optional(), + name: z.string().optional(), + email: z.string().email().nullable().optional(), + picture: z.string().nullable().optional(), + provider: z.string().nullable().optional(), + verified_email: z.boolean().optional(), +}); + +export type UserProfile = z.infer; + +export const SystemUserSchema = z.object({ + id: z.string(), + name: z.string(), + email: z.string().email().nullable().optional(), + provider: z.string().nullable().optional(), + created_at: z.number().optional(), +}); + +export type SystemUser = z.infer; diff --git a/test/integration.spec.ts b/test/integration.spec.ts index e23b3ae..47e6243 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -14,7 +14,6 @@ describe('Integration Tests', () => { // 1. Manually set up a UserDO with a session const id = env.USER.newUniqueId(); const stub = env.USER.get(id); - const userIdStr = id.toString(); // Create session const { sessionId } = await stub.createSession(); @@ -23,7 +22,6 @@ describe('Integration Tests', () => { const credentialStub = env.CREDENTIAL.get(env.CREDENTIAL.idFromName('test-provider')); await credentialStub.put({ user_id: id.toString(), - provider: 'test-provider', subject_id: '123', profile_data: { name: 'Integration Tester' }, }); @@ -65,7 +63,6 @@ describe('Integration Tests', () => { const credentialStub = env.CREDENTIAL.get(env.CREDENTIAL.idFromName('test-provider')); await credentialStub.put({ user_id: id.toString(), - provider: 'test-provider', subject_id: '123', profile_data: { name: 'Original Name' }, }); @@ -111,7 +108,6 @@ describe('Integration Tests', () => { const googleCredStub = env.CREDENTIAL.get(env.CREDENTIAL.idFromName('google')); await googleCredStub.put({ user_id: id.toString(), - provider: 'google', subject_id: 'g123', profile_data: { email: 'google@example.com' }, }); @@ -120,7 +116,6 @@ describe('Integration Tests', () => { const twitchCredStub = env.CREDENTIAL.get(env.CREDENTIAL.idFromName('twitch')); await twitchCredStub.put({ user_id: id.toString(), - provider: 'twitch', subject_id: 't123', profile_data: { email: 'twitch@example.com' }, }); @@ -162,7 +157,6 @@ describe('Integration Tests', () => { it('should serve avatar image from /me/avatar', async () => { const id = env.USER.newUniqueId(); const stub = env.USER.get(id); - const userIdStr = id.toString(); // Create session const { sessionId } = await stub.createSession(); @@ -262,7 +256,6 @@ describe('Integration Tests', () => { const googleCredStub = env.CREDENTIAL.get(env.CREDENTIAL.idFromName('google')); await googleCredStub.put({ user_id: userIdStr, - provider: 'google', subject_id: 'g123', profile_data: { name: 'Google User', picture: 'http://google.com/pic.jpg' }, }); @@ -272,7 +265,6 @@ describe('Integration Tests', () => { const twitchCredStub = env.CREDENTIAL.get(env.CREDENTIAL.idFromName('twitch')); await twitchCredStub.put({ user_id: userIdStr, - provider: 'twitch', subject_id: 't123', profile_data: { name: 'Twitch User', picture: 'http://twitch.tv/pic.jpg' }, }); @@ -296,8 +288,8 @@ describe('Integration Tests', () => { // 3. Verify that the avatar remains the same const storedImage = await userStub.getImage('avatar'); expect(storedImage).not.toBeNull(); - expect(storedImage.mime_type).toBe('image/png'); - expect(new Uint8Array(storedImage.value)).toEqual(initialAvatar); + expect(storedImage!.mime_type).toBe('image/png'); + expect(new Uint8Array(storedImage!.value)).toEqual(initialAvatar); }); it('should server-side render profile.html', async () => { @@ -313,7 +305,6 @@ describe('Integration Tests', () => { const googleCredStub = env.CREDENTIAL.get(env.CREDENTIAL.idFromName('google')); await googleCredStub.put({ user_id: userIdStr, - provider: 'google', subject_id: 'google-123', profile_data: { email: 'google@example.com' }, }); @@ -322,7 +313,6 @@ describe('Integration Tests', () => { const twitchCredStub = env.CREDENTIAL.get(env.CREDENTIAL.idFromName('twitch')); await twitchCredStub.put({ user_id: userIdStr, - provider: 'twitch', subject_id: 'twitch-456', profile_data: { email: 'twitch@example.com' }, }); diff --git a/test/membership.spec.ts b/test/membership.spec.ts index 2ca0578..d788b8f 100644 --- a/test/membership.spec.ts +++ b/test/membership.spec.ts @@ -1,5 +1,5 @@ import { env, SELF } from 'cloudflare:test'; -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { CookieManager } from '../src/CookieManager'; describe('Permission Enforcement', () => { diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index bec96ac..a02719e 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: b67527d07bd06e5d88bcac89eae3c0e4) +// Generated by Wrangler by running `wrangler types` (hash: c70f035350bd314cfa3f52dffa2be560) // Runtime types generated with workerd@1.20260120.0 2025-09-27 global_fetch_strictly_public declare namespace Cloudflare { interface GlobalProps { @@ -21,18 +21,6 @@ declare namespace Cloudflare { SYSTEM: DurableObjectNamespace; CREDENTIAL: DurableObjectNamespace; } - interface MinimalEnv { - IMAGE_STORAGE: R2Bucket; - ASSETS: Fetcher; - ENVIRONMENT: "minimal"; - SESSION_SECRET: ""; - ORIGIN_URL: ""; - GITHUB_PROJECT_ID: string; - USER: DurableObjectNamespace; - ACCOUNT: DurableObjectNamespace; - SYSTEM: DurableObjectNamespace; - CREDENTIAL: DurableObjectNamespace; - } interface OptionalEnv { IMAGE_STORAGE: R2Bucket; ASSETS: Fetcher; @@ -51,23 +39,40 @@ declare namespace Cloudflare { SYSTEM: DurableObjectNamespace; CREDENTIAL: DurableObjectNamespace; } + interface TestEnv { + IMAGE_STORAGE: R2Bucket; + ASSETS: Fetcher; + ENVIRONMENT: "test"; + SESSION_SECRET: "dev-secret"; + ORIGIN_URL: "http://example.com"; + ADMIN_IDS: "admin"; + GOOGLE_CLIENT_ID: "google-id"; + GOOGLE_CLIENT_SECRET: "google-secret"; + TWITCH_CLIENT_ID: "twitch-id"; + TWITCH_CLIENT_SECRET: "twitch-secret"; + GITHUB_PROJECT_ID: string; + USER: DurableObjectNamespace; + ACCOUNT: DurableObjectNamespace; + SYSTEM: DurableObjectNamespace; + CREDENTIAL: DurableObjectNamespace; + } interface Env { GITHUB_PROJECT_ID: string; IMAGE_STORAGE: R2Bucket; ASSETS: Fetcher; - ENVIRONMENT?: "preview" | "minimal" | ""; - SESSION_SECRET?: ""; - ADMIN_IDS?: ""; - ORIGIN_URL?: "https://startup-api-demo-origin.sergeychernyshev.workers.dev/" | ""; - TWITCH_CLIENT_ID?: ""; - TWITCH_CLIENT_SECRET?: ""; + ENVIRONMENT?: "preview" | "" | "test"; + SESSION_SECRET: "" | "dev-secret"; + ADMIN_IDS?: "" | "admin"; + ORIGIN_URL: "https://startup-api-demo-origin.sergeychernyshev.workers.dev/" | "" | "http://example.com"; + TWITCH_CLIENT_ID?: "" | "twitch-id"; + TWITCH_CLIENT_SECRET?: "" | "twitch-secret"; USER: DurableObjectNamespace; ACCOUNT: DurableObjectNamespace; SYSTEM: DurableObjectNamespace; CREDENTIAL: DurableObjectNamespace; AUTH_ORIGIN?: ""; - GOOGLE_CLIENT_ID?: ""; - GOOGLE_CLIENT_SECRET?: ""; + GOOGLE_CLIENT_ID?: "" | "google-id"; + GOOGLE_CLIENT_SECRET?: "" | "google-secret"; } } interface Env extends Cloudflare.Env {} diff --git a/wrangler.jsonc b/wrangler.jsonc index 4a0e1e1..96e4a8d 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -60,6 +60,10 @@ "new_sqlite_classes": ["CredentialDO"], }, ], + "vars": { + "SESSION_SECRET": "", + "ORIGIN_URL": "", + }, "env": { // preview environment used for preview site deployments in PRs and etc. "preview": { @@ -103,9 +107,7 @@ "TWITCH_CLIENT_SECRET": "", }, }, - // environment used to define the minimal number of variables - // so the rest are properly generated as optional in type definitions - "minimal": { + "optional": { "assets": { "directory": "./public", "run_worker_first": true, @@ -138,12 +140,18 @@ ], }, "vars": { - "ENVIRONMENT": "minimal", + "ENVIRONMENT": "", "SESSION_SECRET": "", + "ADMIN_IDS": "", "ORIGIN_URL": "", + "AUTH_ORIGIN": "", + "TWITCH_CLIENT_ID": "", + "TWITCH_CLIENT_SECRET": "", + "GOOGLE_CLIENT_ID": "", + "GOOGLE_CLIENT_SECRET": "", }, }, - "optional": { + "test": { "assets": { "directory": "./public", "run_worker_first": true, @@ -152,39 +160,44 @@ "r2_buckets": [ { "binding": "IMAGE_STORAGE", - "bucket_name": "startup-api-images", + "bucket_name": "startup-api-images-test", }, ], "durable_objects": { "bindings": [ - { - "name": "USER", - "class_name": "UserDO", - }, - { - "name": "ACCOUNT", - "class_name": "AccountDO", - }, - { - "name": "SYSTEM", - "class_name": "SystemDO", - }, - { - "name": "CREDENTIAL", - "class_name": "CredentialDO", - }, + { "name": "USER", "class_name": "UserDO" }, + { "name": "ACCOUNT", "class_name": "AccountDO" }, + { "name": "SYSTEM", "class_name": "SystemDO" }, + { "name": "CREDENTIAL", "class_name": "CredentialDO" }, ], }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["UserDO"], + }, + { + "tag": "v2", + "new_sqlite_classes": ["AccountDO"], + }, + { + "tag": "v3", + "new_sqlite_classes": ["SystemDO"], + }, + { + "tag": "v4", + "new_sqlite_classes": ["CredentialDO"], + }, + ], "vars": { - "ENVIRONMENT": "", - "SESSION_SECRET": "", - "ADMIN_IDS": "", - "ORIGIN_URL": "", - "AUTH_ORIGIN": "", - "TWITCH_CLIENT_ID": "", - "TWITCH_CLIENT_SECRET": "", - "GOOGLE_CLIENT_ID": "", - "GOOGLE_CLIENT_SECRET": "", + "ENVIRONMENT": "test", + "SESSION_SECRET": "dev-secret", + "ORIGIN_URL": "http://example.com", + "ADMIN_IDS": "admin", + "GOOGLE_CLIENT_ID": "google-id", + "GOOGLE_CLIENT_SECRET": "google-secret", + "TWITCH_CLIENT_ID": "twitch-id", + "TWITCH_CLIENT_SECRET": "twitch-secret", }, }, },