diff --git a/.env.example b/.env.example index a7c759f..f15d395 100644 --- a/.env.example +++ b/.env.example @@ -22,14 +22,9 @@ OSS_ACCESS_KEY_SECRET= OSS_BUCKET= OSS_REGION= -# Alibaba Cloud Fusion Auth (Phase 1e) -# 阿里云号码认证服务,用于手机号一键登录和短信验证码登录 -# RAM 子用户需授予 AliyunDypnsFullAccess 权限 +# Alibaba Cloud (Phase 1e) ALIYUN_ACCESS_KEY_ID= ALIYUN_ACCESS_KEY_SECRET= -ALIYUN_FUSION_SCHEME_CODE_ANDROID= -ALIYUN_FUSION_SCHEME_CODE_IOS= -ALIYUN_FUSION_SCHEME_CODE_HARMONY= # Huawei IAP (Phase 1c) HUAWEI_IAP_URL= diff --git a/.env.prod.example b/.env.prod.example index 53ebe35..2fff887 100644 --- a/.env.prod.example +++ b/.env.prod.example @@ -25,14 +25,9 @@ OSS_ACCESS_KEY_SECRET= OSS_BUCKET=duoqi-assets OSS_REGION=oss-cn-hangzhou -# Alibaba Cloud Fusion Auth -# 阿里云号码认证服务,用于手机号一键登录和短信验证码登录 -# RAM 子用户需授予 AliyunDypnsFullAccess 权限 +# Alibaba Cloud ALIYUN_ACCESS_KEY_ID= ALIYUN_ACCESS_KEY_SECRET= -ALIYUN_FUSION_SCHEME_CODE_ANDROID= -ALIYUN_FUSION_SCHEME_CODE_IOS= -ALIYUN_FUSION_SCHEME_CODE_HARMONY= # Huawei IAP HUAWEI_IAP_URL=https://subscr-drcn.iap.hicloud.com diff --git a/CLAUDE.md b/CLAUDE.md index 019c734..b4431cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,7 +66,7 @@ src/ │ ├── audit-log.ts # Admin 操作审计日志 │ └── request-logger.ts # 请求耗时日志 ├── services/ # 业务逻辑(按领域分目录) -│ ├── auth/ # jwt, guest, huawei-id-kit, fusion-auth-client, phone +│ ├── auth/ # jwt, guest, huawei-id-kit, phone │ ├── quiz/ # quiz-service(出题引擎 + 答题验证) │ ├── progress/ # progress, streak, xp, hearts │ ├── gamification/ # leaderboard, achievement diff --git a/bun.lock b/bun.lock index 0eeb4eb..6e92ad1 100644 --- a/bun.lock +++ b/bun.lock @@ -5,9 +5,6 @@ "": { "name": "duoqi-api", "dependencies": { - "@alicloud/dypnsapi20170525": "^2.0.0", - "@alicloud/openapi-client": "^0.4.15", - "@alicloud/tea-util": "^1.4.11", "@fastify/cors": "^11.0.0", "@fastify/helmet": "^13.0.0", "@fastify/jwt": "^9.0.0", @@ -39,38 +36,6 @@ }, }, "packages": { - "@alicloud/credentials": ["@alicloud/credentials@2.4.4", "https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.4.4.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.8.0", "httpx": "^2.3.3", "ini": "^1.3.5", "kitx": "^2.0.0" } }, "sha512-/eRAGSKcniLIFQ1UCpDhB/IrHUZisQ1sc65ws/c2avxUMpXwH1rWAohb76SVAUJhiF4mwvLzLJM1Mn1XL4Xe/Q=="], - - "@alicloud/darabonba-array": ["@alicloud/darabonba-array@0.1.2", "https://registry.npmmirror.com/@alicloud/darabonba-array/-/darabonba-array-0.1.2.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.7.1" } }, "sha512-ZPuQ+bJyjrd8XVVm55kl+ypk7OQoi1ZH/DiToaAEQaGvgEjrTcvQkg71//vUX/6cvbLIF5piQDvhrLb+lUEIPQ=="], - - "@alicloud/darabonba-encode-util": ["@alicloud/darabonba-encode-util@0.0.2", "https://registry.npmmirror.com/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.2.tgz", { "dependencies": { "moment": "^2.29.1" } }, "sha512-mlsNctkeqmR0RtgE1Rngyeadi5snLOAHBCWEtYf68d7tyKskosXDTNeZ6VCD/UfrUu4N51ItO8zlpfXiOgeg3A=="], - - "@alicloud/darabonba-map": ["@alicloud/darabonba-map@0.0.1", "https://registry.npmmirror.com/@alicloud/darabonba-map/-/darabonba-map-0.0.1.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.7.1" } }, "sha512-2ep+G3YDvuI+dRYVlmER1LVUQDhf9kEItmVB/bbEu1pgKzelcocCwAc79XZQjTcQGFgjDycf3vH87WLDGLFMlw=="], - - "@alicloud/darabonba-signature-util": ["@alicloud/darabonba-signature-util@0.0.4", "https://registry.npmmirror.com/@alicloud/darabonba-signature-util/-/darabonba-signature-util-0.0.4.tgz", { "dependencies": { "@alicloud/darabonba-encode-util": "^0.0.1" } }, "sha512-I1TtwtAnzLamgqnAaOkN0IGjwkiti//0a7/auyVThdqiC/3kyafSAn6znysWOmzub4mrzac2WiqblZKFcN5NWg=="], - - "@alicloud/darabonba-string": ["@alicloud/darabonba-string@1.0.3", "https://registry.npmmirror.com/@alicloud/darabonba-string/-/darabonba-string-1.0.3.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.5.1" } }, "sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA=="], - - "@alicloud/dypnsapi20170525": ["@alicloud/dypnsapi20170525@2.0.0", "https://registry.npmmirror.com/@alicloud/dypnsapi20170525/-/dypnsapi20170525-2.0.0.tgz", { "dependencies": { "@alicloud/openapi-core": "^1.0.0", "@darabonba/typescript": "^1.0.0" } }, "sha512-eVh1dJ2HA82bBHt+YZFIBzPEYW80FK+TSpcxSR9o0W+FgfTqBaj6eeIHnN7NFhyDAD/3+HtZ146Pmvr51JEEAg=="], - - "@alicloud/endpoint-util": ["@alicloud/endpoint-util@0.0.1", "https://registry.npmmirror.com/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.5.1", "kitx": "^2.0.0" } }, "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg=="], - - "@alicloud/gateway-pop": ["@alicloud/gateway-pop@0.0.6", "https://registry.npmmirror.com/@alicloud/gateway-pop/-/gateway-pop-0.0.6.tgz", { "dependencies": { "@alicloud/credentials": "^2", "@alicloud/darabonba-array": "^0.1.0", "@alicloud/darabonba-encode-util": "^0.0.2", "@alicloud/darabonba-map": "^0.0.1", "@alicloud/darabonba-signature-util": "^0.0.4", "@alicloud/darabonba-string": "^1.0.2", "@alicloud/endpoint-util": "^0.0.1", "@alicloud/gateway-spi": "^0.0.8", "@alicloud/openapi-util": "^0.3.2", "@alicloud/tea-typescript": "^1.7.1", "@alicloud/tea-util": "^1.4.8" } }, "sha512-KF4I+JvfYuLKc3fWeWYIZ7lOVJ9jRW0sQXdXidZn1DKZ978ncfGf7i0LBfONGk4OxvNb/HD3/0yYhkgZgPbKtA=="], - - "@alicloud/gateway-spi": ["@alicloud/gateway-spi@0.0.8", "https://registry.npmmirror.com/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz", { "dependencies": { "@alicloud/credentials": "^2", "@alicloud/tea-typescript": "^1.7.1" } }, "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g=="], - - "@alicloud/openapi-client": ["@alicloud/openapi-client@0.4.15", "https://registry.npmmirror.com/@alicloud/openapi-client/-/openapi-client-0.4.15.tgz", { "dependencies": { "@alicloud/credentials": "^2.4.2", "@alicloud/gateway-spi": "^0.0.8", "@alicloud/openapi-util": "^0.3.2", "@alicloud/tea-typescript": "^1.7.1", "@alicloud/tea-util": "1.4.9", "@alicloud/tea-xml": "0.0.3" } }, "sha512-4VE0/k5ZdQbAhOSTqniVhuX1k5DUeUMZv74degn3wIWjLY6Bq+hxjaGsaHYlLZ2gA5wUrs8NcI5TE+lIQS3iiA=="], - - "@alicloud/openapi-core": ["@alicloud/openapi-core@1.0.7", "https://registry.npmmirror.com/@alicloud/openapi-core/-/openapi-core-1.0.7.tgz", { "dependencies": { "@alicloud/credentials": "^2.4.2", "@alicloud/gateway-pop": "0.0.6", "@alicloud/gateway-spi": "^0.0.8", "@darabonba/typescript": "^1.0.2" } }, "sha512-I80PQVfmlzRiXGHwutMp2zTpiqUVv8ts30nWAfksfHUSTIapk3nj9IXaPbULMPGNV6xqEyshO2bj2a+pmwc2tQ=="], - - "@alicloud/openapi-util": ["@alicloud/openapi-util@0.3.3", "https://registry.npmmirror.com/@alicloud/openapi-util/-/openapi-util-0.3.3.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.7.1", "@alicloud/tea-util": "^1.3.0", "kitx": "^2.1.0", "sm3": "^1.0.3" } }, "sha512-vf0cQ/q8R2U7ZO88X5hDiu1yV3t/WexRj+YycWxRutkH/xVXfkmpRgps8lmNEk7Ar+0xnY8+daN2T+2OyB9F4A=="], - - "@alicloud/tea-typescript": ["@alicloud/tea-typescript@1.8.0", "https://registry.npmmirror.com/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz", { "dependencies": { "@types/node": "^12.0.2", "httpx": "^2.2.6" } }, "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ=="], - - "@alicloud/tea-util": ["@alicloud/tea-util@1.4.11", "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.11.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.5.1", "@darabonba/typescript": "^1.0.0", "kitx": "^2.0.0" } }, "sha512-HyPEEQ8F0WoZegiCp7sVdrdm6eBOB+GCvGl4182u69LDFktxfirGLcAx3WExUr1zFWkq2OSmBroTwKQ4w/+Yww=="], - - "@alicloud/tea-xml": ["@alicloud/tea-xml@0.0.3", "https://registry.npmmirror.com/@alicloud/tea-xml/-/tea-xml-0.0.3.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1", "@types/xml2js": "^0.4.5", "xml2js": "^0.6.0" } }, "sha512-+/9GliugjrLglsXVrd1D80EqqKgGpyA0eQ6+1ZdUOYCaRguaSwz44trX3PaxPu/HhIPJg9PsGQQ3cSLXWZjbAA=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], @@ -81,8 +46,6 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@darabonba/typescript": ["@darabonba/typescript@1.0.4", "https://registry.npmmirror.com/@darabonba/typescript/-/typescript-1.0.4.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.5.1", "httpx": "^2.3.2", "lodash": "^4.17.21", "moment": "^2.30.1", "moment-timezone": "^0.5.45", "xml2js": "^0.6.2" } }, "sha512-icl8RGTw4DiWRpco6dVh21RS0IqrH4s/eEV36TZvz/e1+paogSZjaAgox7ByrlEuvG+bo5d8miq/dRlqiUaL/w=="], - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "https://registry.npmmirror.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], "@emnapi/core": ["@emnapi/core@1.9.1", "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.1.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="], @@ -257,8 +220,6 @@ "@types/uuid": ["@types/uuid@10.0.0", "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="], - "@types/xml2js": ["@types/xml2js@0.4.14", "https://registry.npmmirror.com/@types/xml2js/-/xml2js-0.4.14.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.1", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/type-utils": "8.58.1", "@typescript-eslint/utils": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.1", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.58.1.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw=="], @@ -461,8 +422,6 @@ "html-escaper": ["html-escaper@2.0.2", "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], - "httpx": ["httpx@2.3.3", "https://registry.npmmirror.com/httpx/-/httpx-2.3.3.tgz", { "dependencies": { "@types/node": "^20", "debug": "^4.1.1" } }, "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw=="], - "iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ignore": ["ignore@5.3.2", "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -473,8 +432,6 @@ "inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@1.3.8", "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "ipaddr.js": ["ipaddr.js@2.3.0", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.3.0.tgz", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], "is-extglob": ["is-extglob@2.1.1", "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -509,8 +466,6 @@ "keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "kitx": ["kitx@2.2.0", "https://registry.npmmirror.com/kitx/-/kitx-2.2.0.tgz", { "dependencies": { "@types/node": "^22.5.4" } }, "sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg=="], - "levn": ["levn@0.4.1", "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "light-my-request": ["light-my-request@6.6.0", "https://registry.npmmirror.com/light-my-request/-/light-my-request-6.6.0.tgz", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], @@ -541,8 +496,6 @@ "locate-path": ["locate-path@6.0.0", "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash": ["lodash@4.18.1", "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - "lodash.merge": ["lodash.merge@4.6.2", "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], "long": ["long@5.3.2", "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -563,10 +516,6 @@ "mnemonist": ["mnemonist@0.40.3", "https://registry.npmmirror.com/mnemonist/-/mnemonist-0.40.3.tgz", { "dependencies": { "obliterator": "^2.0.4" } }, "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ=="], - "moment": ["moment@2.30.1", "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], - - "moment-timezone": ["moment-timezone@0.5.48", "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.48.tgz", { "dependencies": { "moment": "^2.29.4" } }, "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw=="], - "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mysql2": ["mysql2@3.20.0", "https://registry.npmmirror.com/mysql2/-/mysql2-3.20.0.tgz", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg=="], @@ -647,8 +596,6 @@ "safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "sax": ["sax@1.6.0", "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], - "secure-json-parse": ["secure-json-parse@4.1.0", "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-4.1.0.tgz", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], "semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -661,8 +608,6 @@ "siginfo": ["siginfo@2.0.0", "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "sm3": ["sm3@1.0.3", "https://registry.npmmirror.com/sm3/-/sm3-1.0.3.tgz", {}, "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g=="], - "sonic-boom": ["sonic-boom@4.2.1", "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.1.tgz", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], "source-map": ["source-map@0.6.1", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -727,22 +672,12 @@ "wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "xml2js": ["xml2js@0.6.2", "https://registry.npmmirror.com/xml2js/-/xml2js-0.6.2.tgz", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], - - "xmlbuilder": ["xmlbuilder@11.0.1", "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], - "xtend": ["xtend@4.0.2", "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@alicloud/darabonba-signature-util/@alicloud/darabonba-encode-util": ["@alicloud/darabonba-encode-util@0.0.1", "https://registry.npmmirror.com/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.1.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.7.1", "moment": "^2.29.1" } }, "sha512-Sl5vCRVAYMqwmvXpJLM9hYoCHOMsQlGxaWSGhGWulpKk/NaUBArtoO1B0yHruJf1C5uHhEJIaylYcM48icFHgw=="], - - "@alicloud/openapi-client/@alicloud/tea-util": ["@alicloud/tea-util@1.4.9", "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.9.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.5.1", "kitx": "^2.0.0" } }, "sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw=="], - - "@alicloud/tea-typescript/@types/node": ["@types/node@12.20.55", "https://registry.npmmirror.com/@types/node/-/node-12.20.55.tgz", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -761,10 +696,6 @@ "fast-json-stringify/ajv": ["ajv@8.18.0", "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - "httpx/@types/node": ["@types/node@20.19.41", "https://registry.npmmirror.com/@types/node/-/node-20.19.41.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ=="], - - "kitx/@types/node": ["@types/node@22.19.19", "https://registry.npmmirror.com/@types/node/-/node-22.19.19.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="], - "light-my-request/process-warning": ["process-warning@4.0.1", "https://registry.npmmirror.com/process-warning/-/process-warning-4.0.1.tgz", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], "pino/pino-abstract-transport": ["pino-abstract-transport@2.0.0", "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], @@ -823,10 +754,6 @@ "fast-json-stringify/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "httpx/@types/node/undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "kitx/@types/node/undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], diff --git a/docs/api-reference.md b/docs/api-reference.md index 0731310..7860fc3 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -19,7 +19,7 @@ | 类型 | Header | 适用路径 | |------|--------|----------| -| 无需认证 | - | `/health`, `/v1/auth/guest`, `/v1/auth/huawei`, `/v1/auth/refresh`, `/v1/auth/fusion/token`, `/v1/auth/fusion/verify`, `/v1/auth/providers`, `/v1/admin/login` | +| 无需认证 | - | `/health`, `/v1/auth/guest`, `/v1/auth/huawei`, `/v1/auth/refresh`, `/v1/auth/providers`, `/v1/admin/login` | | JWT(游客) | `Authorization: Bearer ` | `/v1/auth/link` | | JWT | `Authorization: Bearer ` | 大多数客户端 API | | Admin JWT | `Authorization: Bearer ` | `/v1/admin/*` | @@ -256,7 +256,7 @@ 认证:无 限流:10 次/分钟 -当前已注册但未实现,固定返回 HTTP 501。手机号登录请使用融合认证接口(`/auth/fusion/token` + `/auth/fusion/verify`)。 +当前已注册但未实现,固定返回 HTTP 501。 ```json { @@ -269,104 +269,6 @@ } ``` -#### POST /auth/fusion/token - -认证:无 -限流:10 次/分钟 - -用途:获取阿里云融合认证 SDK 鉴权 Token。客户端在初始化 SDK 前调用此接口。`schemeCode` 和 `durationSeconds` 由服务端配置,不暴露给客户端。 - -请求: - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `platform` | `"Android"` \| `"iOS"` \| `"Harmony"` | 是 | 客户端平台 | -| `packageName` | string | Android 必填 | App 包名 | -| `packageSign` | string | Android 必填 | App 包签名(MD5,去冒号小写) | -| `bundleId` | string | iOS 必填 | App Bundle ID | - -```json -{ - "platform": "Android", - "packageName": "com.example.app", - "packageSign": "a1b2c3d4e5f6..." -} -``` - -响应: - -```json -{ - "success": true, - "data": { - "fusionAuthToken": "FKcksloqk***********jalEc+" - }, - "error": null -} -``` - -错误响应(服务未配置): - -```json -{ - "success": false, - "data": null, - "error": { - "code": "SERVICE_UNAVAILABLE", - "message": "Fusion auth is not configured on the server" - } -} -``` - -#### POST /auth/fusion/verify - -认证:无 -限流:10 次/分钟 - -用途:客户端融合认证 SDK 完成认证后,将运营商返回的 verifyToken 发送到此端点。服务端用 verifyToken 向阿里云换取手机号,完成登录或自动注册。一键登录和短信验证码登录均使用此端点。 - -请求: - -```json -{ - "verifyToken": "LD108enNdlsl*******sFLKCks1==" -} -``` - -成功响应(同 `/auth/guest`): - -```json -{ - "success": true, - "data": { - "user": { - "id": "uuid", - "nickname": null, - "avatarUrl": null, - "tier": "free" - }, - "tokens": { - "accessToken": "jwt", - "refreshToken": "jwt" - } - }, - "error": null -} -``` - -错误响应: - -```json -{ - "success": false, - "data": null, - "error": { - "code": "UNAUTHORIZED", - "message": "Phone verification failed" - } -} -``` - #### GET /auth/providers 认证:无 @@ -380,7 +282,7 @@ | `platform` | `'ios' \| 'android' \| 'harmony'` | 是 | 客户端平台 | 平台过滤规则: -- 一键登录 / 短信验证码:全平台 +- 短信验证码:全平台 - 华为账号:仅 Android、Harmony - Apple 账号:仅 iOS - 微信 / QQ:全平台,enabled 由数据库配置控制 @@ -392,7 +294,6 @@ "success": true, "data": { "providers": [ - { "id": "fusion", "name": "一键登录", "type": "primary", "enabled": true }, { "id": "phone_sms", "name": "短信验证码登录", "type": "secondary", "enabled": true }, { "id": "apple", "name": "通过 Apple 登录", "type": "third_party", "iconKey": "apple", "enabled": true }, { "id": "wechat", "name": "通过微信登录", "type": "third_party", "iconKey": "wechat", "enabled": false } diff --git a/docs/fusion-auth-integration.md b/docs/fusion-auth-integration.md deleted file mode 100644 index f935792..0000000 --- a/docs/fusion-auth-integration.md +++ /dev/null @@ -1,422 +0,0 @@ -# 阿里云融合认证集成 — 设计与实施计划 - -> Phase 1e: Phone Number Authentication -> Created: 2026-05-27 -> Status: Implemented - -## 概述 - -集成阿里云号码认证服务的**融合认证**方案,为多奇平台提供手机号一键登录能力。融合认证是一种校验用户手机号的云原生服务,集成了号码认证(运营商网关取号)、短信验证码、语音验证码等通信功能。 - -### 目标 - -- 新增 `POST /auth/fusion/token` 端点,为客户端 SDK 提供鉴权 Token(对齐 Flutter 客户端 `FUSION_AUTH_INTEGRATION.md` 第 5.1 节) -- 新增 `POST /auth/fusion/verify` 端点,用运营商 verifyToken 换取手机号并完成登录(对齐 Flutter 客户端第 5.2 节) -- 支持游客账号通过手机号关联升级为正式账号 - -### 不在范围内 - -- 短信验证码/语音验证码登录(号码认证降级到短信时由阿里云模板自动处理,服务端感知不变) -- Flutter 客户端 SDK 集成(客户端侧独立进行,见 `duoqi-flutter/docs/FUSION_AUTH_INTEGRATION.md`) -- 修改现有 `POST /auth/phone`(保留 501 占位,未来可用于非融合认证的手机登录) - ---- - -## 架构分析 - -### 现有认证架构 - -项目已实现 `guest`、`huawei`、`apple` 三种登录方式,采用统一的 `findOrCreate*` 模式: - -``` -客户端 ──▶ 路由层(Zod校验) ──▶ 服务层(业务逻辑) ──▶ 数据库(users表) - │ - signTokens() + buildLoginResponse() -``` - -关键设计: -- `users` 表以 `(authType, authId)` 唯一索引标识用户 -- `authType` 枚举已包含 `'phone'`,`authId` 用于存储手机号 -- JWT payload 携带 `{ userId, authType, tier }` -- 登录响应统一为 `{ user, tokens }` 结构 - -### 融合认证集成点 - -融合认证引入一个**两步交互**流程,比现有的华为/Apple 认证多一个"预取 Token"步骤: - -``` -┌──────────┐ ┌──────────────┐ ┌──────────────────┐ -│ 客户端 │ │ duoqi-api │ │ 阿里云 Dypnsapi │ -└────┬─────┘ └──────┬───────┘ └────────┬─────────┘ - │ │ │ - │ ① POST /auth/fusion/token │ - │ { platform, packageName?, bundleId? } │ - │───────────────────▶│ │ - │ │ ② GetFusionAuthToken │ - │ │────────────────────────▶│ - │ │◀── { authToken } ────────│ - │◀── { fusionAuthToken } ──│ │ - │ │ │ - │ [客户端 SDK 使用 authToken 完成认证] │ - │ [获得 verifyToken] │ │ - │ │ │ - │ ③ POST /auth/fusion/verify │ - │ { verifyToken } │ │ - │───────────────────▶│ │ - │ │ ④ VerifyWithFusionAuthToken - │ │────────────────────────▶│ - │ │◀── { phoneNumber, verifyResult, phoneScore } - │ │ │ - │ │ ⑤ 查找或创建用户,签发JWT │ - │◀── { user, tokens } │ │ - │ │ │ -``` - -> 路由路径对齐 Flutter 客户端 `FUSION_AUTH_INTEGRATION.md` 第 5 节的定义。 - ---- - -## 关键设计决策 - -### 决策 1:手机号存储格式 - -**选择:存储阿里云返回的原始手机号字符串(如 `18012341234`)** - -理由: -- `VerifyWithFusionAuthToken` 在号码认证通过时返回完整手机号(非掩码) -- 完整手机号作为 `authId` 可确保用户唯一性 -- 掩码号(如 `180****1234`)仅出现在日志或审计场景,不用于身份标识 - -替代方案(未采纳): -- 存储掩码号:无法唯一标识用户,不可行 -- 对手机号做哈希:丧失可读性,且哈希碰撞风险虽极低但非零 - -### 决策 2:authToken 有效期 - -**选择:900 秒(15 分钟,API 允许的最小值)** - -理由: -- 融合认证的典型交互在几秒到几十秒内完成 -- 更短的 Token 有效期意味着更小的被盗用风险窗口 -- 与项目中 access_token 1 小时、refresh_token 30 天的分级策略一致——authToken 作为临时凭证应有最短有效期 - -### 决策 3:阿里云 SDK 客户端生命周期 - -**选择:懒初始化单例模式** - -理由: -- 阿里云 SDK 客户端是线程安全的,可复用 -- 避免每次请求创建新客户端的开销(HTTP 连接池复用) -- 与项目中 `db` 客户端的单例模式保持一致 -- 仅在配置了阿里云环境变量时才初始化,不影响其他认证方式 - -### 决策 4:路由设计 - -**选择:新增 `/auth/fusion/token` 和 `/auth/fusion/verify`,独立于 `/auth/phone`** - -理由: -- Flutter 客户端已在 `FUSION_AUTH_INTEGRATION.md` 第 5 节明确定义了这两个路径,服务端需对齐 -- `/auth/fusion/` 作为独立命名空间,语义上更准确——融合认证不只是一键取号,而是由阿里云模板编排的多种认证方式(号码认证、短信、图形验证等) -- `/auth/phone` 保留 501 占位,未来可用于非融合认证的原生短信验证码登录等场景 - -替代方案(未采纳): -- `POST /auth/phone/fusion-token` + 修改 `POST /auth/phone`:路径层级不合理,融合认证不等同于"手机号登录" -- 在 `/auth/phone` 请求体中加 `action: 'getToken' | 'verify'`:语义不够明确,增加请求体复杂度 - -### 决策 5:账号关联扩展 - -**选择:在 `/auth/link` 中支持 `provider: 'phone'`** - -理由: -- 已有游客关联华为/Apple 的完整流程(`account-link-service.ts`) -- 手机号关联复用相同的 `accountMigrations` 表和幂等逻辑 -- 仅需扩展 `linkSchema` 的 `provider` 枚举和 `credential` 结构 - ---- - -## 阿里云 API 参考 - -### GetFusionAuthToken - -| 项目 | 值 | -|------|------| -| Action | `GetFusionAuthToken` | -| 授权 | `dypns:GetFusionAuthToken` | -| Endpoint | `dypnsapi.aliyuncs.com` | - -**请求参数:** - -| 名称 | 类型 | 必填 | 描述 | 示例值 | -|------|------|------|------|--------| -| SchemeCode | string | 是 | 认证方案 Code | `FA1000*************201` | -| Platform | string | 是 | 平台:`Android` 或 `iOS` | `Android` | -| PackageName | string | Android 必填 | App 包名 | `com.example.test` | -| PackageSign | string | Android 必填 | App 包签名 | `47fcc************************278` | -| BundleId | string | iOS 必填 | App bundleId | `com.example.test` | -| DurationSeconds | long | 是 | Token 有效时长(秒),范围 900–43200 | `900` | - -**响应示例:** - -```json -{ - "Message": "成功", - "RequestId": "CC3BB6D2-2FDF-4321-9DCE-B38165CE4C47", - "Model": "FKcksloqk***********jalEc+", - "Code": "OK", - "Success": true -} -``` - -### VerifyWithFusionAuthToken - -| 项目 | 值 | -|------|------| -| Action | `VerifyWithFusionAuthToken` | -| 授权 | `dypns:VerifyWithFusionAuthToken` | -| Endpoint | `dypnsapi.aliyuncs.com` | - -**请求参数:** - -| 名称 | 类型 | 必填 | 描述 | 示例值 | -|------|------|------|------|--------| -| VerifyToken | string | 是 | 客户端 SDK 完成认证后返回的 Token | `LD108enNdlsl*******sFLKCks1==` | - -**响应示例:** - -```json -{ - "Message": "示例值", - "RequestId": "CC3BB6D2-2FDF-4321-9DCE-B38165CE4C47", - "Model": { - "PhoneNumber": "18012341234", - "VerifyResult": "PASS", - "PhoneScore": 20 - }, - "Code": "OK", - "Success": true -} -``` - -**关键错误码:** - -| HTTP 状态码 | 错误码 | 描述 | -|-------------|--------|------| -| 400 | `SmsCodeVerifyFail` | 短信验证码失败(号码认证降级到短信时) | -| 400 | `Throttling.System` | 接口被限流 | -| 400 | `VerifySchemeNotExist` | 认证方案不存在 | -| 400 | `SchemeNotPassed` | 认证方案未通过审核 | -| 400 | `Unsupported.Account` | 账号未开通号码认证服务 | -| 403 | `UnauthorizedOperation` | 权限校验失败 | -| 500 | `SystemError` | 系统异常 | - ---- - -## 分步实施计划 - -### Phase 1:基础设施(前置条件) - -| # | 任务 | 产出文件 | 验证标准 | -|---|------|----------|----------| -| 1.1 | 安装阿里云 SDK 依赖 | `package.json` | `@alicloud/dypnsapi20170525`、`@alicloud/openapi-client`、`@alicloud/tea-util` 安装成功 | -| 1.2 | 添加环境变量 | `src/utils/config.ts`、`.env.example` | `ALIYUN_ACCESS_KEY_ID`、`ALIYUN_ACCESS_KEY_SECRET`、`ALIYUN_FUSION_SCHEME_CODE` 加入 Zod schema,可选字段 | -| 1.3 | 类型检查通过 | — | `bun run typecheck` 无错误 | - -### Phase 2:服务层 - -| # | 任务 | 产出文件 | 验证标准 | -|---|------|----------|----------| -| 2.1 | 创建阿里云客户端封装 | `src/services/auth/fusion-auth-client.ts`(新建) | 懒初始化单例,配置从 `config` 读取 | -| 2.2 | 实现 `getFusionAuthToken` | `src/services/auth/fusion-auth-client.ts` | 接收平台参数,调用阿里云 API,返回 Token 字符串 | -| 2.3 | 实现 `verifyWithFusionAuthToken` | `src/services/auth/fusion-auth-client.ts` | 接收 verifyToken,调用阿里云 API,返回 `{ phoneNumber, verifyResult }` | -| 2.4 | 实现 `findOrCreatePhone` | `src/services/auth/jwt.ts` | 遵循 `findOrCreateGuest` / `findOrCreateHuawei` 相同模式 | -| 2.5 | 类型检查通过 | — | `bun run typecheck` 无错误 | - -### Phase 3:路由层 - -| # | 任务 | 产出文件 | 验证标准 | -|---|------|----------|----------| -| 3.1 | 新增 `POST /auth/fusion/token` 路由 | `src/routes/auth.ts` | 接收 `{ platform, packageName?, packageSign?, bundleId? }`,调用 `getFusionAuthToken`,返回 `{ fusionAuthToken }` | -| 3.2 | 新增 `POST /auth/fusion/verify` 路由 | `src/routes/auth.ts` | 接收 `{ verifyToken }`,调用 `verifyWithFusionAuthToken` + `findOrCreatePhone`,返回标准 `LoginResponse` | -| 3.3 | 扩展 `/auth/link` 支持 phone provider | `src/routes/auth.ts`、`src/services/auth/account-link-service.ts` | `linkSchema.provider` 增加 `'phone'`,credential 结构适配 | -| 3.4 | 类型检查通过 | — | `bun run typecheck` 无错误 | - -### Phase 4:测试 - -| # | 任务 | 产出文件 | 验证标准 | -|---|------|----------|----------| -| 4.1 | fusion-auth-client 单元测试 | `src/__tests__/services/fusion-auth.test.ts`(新建) | mock 阿里云 SDK,测试正常流程和错误处理 | -| 4.2 | findOrCreatePhone 单元测试 | `src/__tests__/services/auth.test.ts` | 测试新用户创建和已有用户查找 | -| 4.3 | 全量测试通过 | — | `bun run test` 全部通过 | - -### Phase 5:文档 - -| # | 任务 | 产出文件 | 验证标准 | -|---|------|----------|----------| -| 5.1 | 更新 API 文档 | `docs/api-reference.md` | 新增 `POST /auth/fusion/token` 和 `POST /auth/fusion/verify` 端点描述 | -| 5.2 | 更新 CLAUDE.md 项目结构 | `CLAUDE.md` | services/auth 目录列表新增 `fusion-auth-client` | - ---- - -## 新增环境变量 - -```bash -# .env 新增项(阿里云融合认证) -ALIYUN_ACCESS_KEY_ID= # 阿里云 RAM 用户的 AccessKey ID -ALIYUN_ACCESS_KEY_SECRET= # 阿里云 RAM 用户的 AccessKey Secret -ALIYUN_FUSION_SCHEME_CODE= # 融合认证方案 Code(在阿里云控制台创建) -``` - -配置特点: -- 三个变量均为可选——未配置时不影响游客、华为、Apple 等其他登录方式 -- 调用融合认证相关接口时检查配置,未配置则返回 `NOT_IMPLEMENTED` 或 `SERVICE_UNAVAILABLE` -- AccessKey 通过 RAM 子用户授权,仅需 `AliyunDypnsFullAccess` 权限 - ---- - -## 新增 API 端点定义 - -> 路由路径与 Flutter 客户端 `FUSION_AUTH_INTEGRATION.md` 第 5 节保持一致。 - -### POST /auth/fusion/token - -认证:无 -限流:10 次/分钟 - -用途:为客户端融合认证 SDK 提供鉴权 Token。客户端在调用 `FusionAuth.init(schemeCode, authToken)` 时需要此 Token。对应阿里云 `GetFusionAuthToken` API。 - -请求: - -```json -{ - "platform": "Android", - "packageName": "com.duoqi.app", - "packageSign": "47fcc************************278" -} -``` - -```json -{ - "platform": "iOS", - "bundleId": "com.duoqi.app" -} -``` - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `platform` | `'Android' \| 'iOS'` | 是 | 客户端平台 | -| `packageName` | string | Android 必填 | App 包名 | -| `packageSign` | string | Android 必填 | App 包签名 | -| `bundleId` | string | iOS 必填 | App bundleId | - -响应: - -```json -{ - "success": true, - "data": { - "fusionAuthToken": "FKcksloqk***********jalEc+" - }, - "error": null -} -``` - -错误响应(服务未配置): - -```json -{ - "success": false, - "data": null, - "error": { - "code": "SERVICE_UNAVAILABLE", - "message": "Fusion auth is not configured" - } -} -``` - -### POST /auth/fusion/verify - -认证:无 -限流:10 次/分钟 - -用途:客户端融合认证 SDK 完成认证后(`onVerifySuccess` 事件),将运营商返回的 verifyToken 发送到此端点。服务端用 verifyToken 向阿里云换取手机号,完成用户查找或创建,签发 JWT。对应阿里云 `VerifyWithFusionAuthToken` API。 - -请求: - -```json -{ - "verifyToken": "LD108enNdlsl*******sFLKCks1==" -} -``` - -| 字段 | 类型 | 必填 | 说明 | -|------|------|------|------| -| `verifyToken` | string | 是 | 客户端 SDK `onVerifySuccess` 事件返回的 Token | - -成功响应(同 `/auth/guest`): - -```json -{ - "success": true, - "data": { - "user": { - "id": "uuid", - "nickname": null, - "avatarUrl": null, - "tier": "free" - }, - "tokens": { - "accessToken": "jwt", - "refreshToken": "jwt" - } - }, - "error": null -} -``` - -错误响应(认证失败): - -```json -{ - "success": false, - "data": null, - "error": { - "code": "VALIDATION_ERROR", - "message": "Phone verification failed" - } -} -``` - -错误响应(服务未配置): - -```json -{ - "success": false, - "data": null, - "error": { - "code": "SERVICE_UNAVAILABLE", - "message": "Fusion auth is not configured" - } -} -``` - ---- - -## 前置条件检查清单 - -在开始编码之前,需要以下准备工作: - -- [ ] **阿里云 RAM 子用户**:创建 RAM 子用户,授予 `AliyunDypnsFullAccess` 权限,获取 AccessKey ID 和 Secret -- [ ] **融合认证方案**:在号码认证服务控制台创建融合认证方案,获取 SchemeCode -- [ ] **Android 签名信息**:确认 App 包名 (`PackageName`) 和签名 (`PackageSign`) -- [ ] **iOS Bundle 信息**:确认 App bundleId -- [ ] **Flutter 客户端状态**:确认 duoqi-flutter 是否已集成融合认证 SDK - ---- - -## 风险与注意事项 - -1. **手机号隐私合规**:存储用户手机号需符合《个人信息保护法》,建议在隐私政策中明确告知 -2. **运营商支持范围**:号码认证(一键取号)依赖运营商网关,WiFi 环境可能降级到短信验证码 -3. **限流**:阿里云 API 有自身限流(错误码 `Throttling.System`),服务端应做好重试和降级 -4. **错误码映射**:阿里云的错误码需映射到项目统一的 `error.code` 体系 -5. **账号冲突**:同一手机号可能已通过其他方式注册(如先游客后手机号),需考虑合并策略 diff --git a/package.json b/package.json index 1b3a98a..29a1000 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,6 @@ "test:coverage": "vitest run --coverage" }, "dependencies": { - "@alicloud/dypnsapi20170525": "^2.0.0", - "@alicloud/openapi-client": "^0.4.15", - "@alicloud/tea-util": "^1.4.11", "@fastify/cors": "^11.0.0", "@fastify/helmet": "^13.0.0", "@fastify/jwt": "^9.0.0", diff --git a/src/__tests__/services/fusion-auth.test.ts b/src/__tests__/services/fusion-auth.test.ts deleted file mode 100644 index 45e8053..0000000 --- a/src/__tests__/services/fusion-auth.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -describe('fusion-auth-client module structure', () => { - it('exports getFusionAuthToken and verifyFusionAuthToken', async () => { - const mod = await import('../../services/auth/fusion-auth-client.js'); - expect(typeof mod.getFusionAuthToken).toBe('function'); - expect(typeof mod.verifyFusionAuthToken).toBe('function'); - }); -}); diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 098f70d..a1c46b0 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -18,7 +18,6 @@ async function authMiddleware(app: FastifyInstance): Promise { '/v1/auth/guest', '/v1/auth/phone', '/v1/auth/refresh', - '/v1/auth/fusion', '/v1/auth/providers', ]; diff --git a/src/routes/admin/auth-providers.ts b/src/routes/admin/auth-providers.ts index fe142fe..d2295c9 100644 --- a/src/routes/admin/auth-providers.ts +++ b/src/routes/admin/auth-providers.ts @@ -4,7 +4,7 @@ import { db } from '../../db/client.js'; import { appSettings } from '../../db/schema.js'; import { like } from 'drizzle-orm'; -const VALID_PROVIDERS = ['wechat', 'qq', 'huawei', 'apple', 'fusion', 'phone_sms']; +const VALID_PROVIDERS = ['wechat', 'qq', 'huawei', 'apple', 'phone_sms']; export async function adminAuthProvidersRoutes(app: FastifyInstance): Promise { app.get('/', async () => { diff --git a/src/routes/auth.ts b/src/routes/auth.ts index 48481b4..b3a237e 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth.ts @@ -3,9 +3,8 @@ import { z } from 'zod'; import { db } from '../db/client.js'; import { users, appSettings } from '../db/schema.js'; import { eq, like } from 'drizzle-orm'; -import { findOrCreateGuest, findOrCreateHuawei, findOrCreatePhone, refreshJwt } from '../services/auth/jwt.js'; +import { findOrCreateGuest, findOrCreateHuawei, findOrCreatePhone as _findOrCreatePhone, refreshJwt } from '../services/auth/jwt.js'; import { verifyHuaweiToken } from '../services/auth/huawei-id-kit.js'; -import { getFusionAuthToken, verifyFusionAuthToken } from '../services/auth/fusion-auth-client.js'; import { linkGuestAccount } from '../services/auth/account-link-service.js'; import { NotFoundError } from '../utils/errors.js'; import { config } from '../utils/config.js'; @@ -32,17 +31,6 @@ const linkSchema = z.object({ clientMigrationId: z.string().min(1), }); -const fusionVerifySchema = z.object({ - verifyToken: z.string().min(1), -}); - -const fusionTokenSchema = z.object({ - platform: z.enum(['Android', 'iOS', 'Harmony']), - packageName: z.string().optional(), - packageSign: z.string().optional(), - bundleId: z.string().optional(), -}); - const providersQuerySchema = z.object({ platform: z.enum(['ios', 'android', 'harmony']), }); @@ -58,7 +46,6 @@ interface ProviderEntry { } const PLATFORM_AVAILABILITY: Record = { - fusion: ['ios', 'android', 'harmony'], phone_sms: ['ios', 'android', 'harmony'], huawei: ['android', 'harmony'], apple: ['ios'], @@ -67,7 +54,6 @@ const PLATFORM_AVAILABILITY: Record = { }; const PROVIDER_METADATA: Record = { - fusion: { name: '一键登录', type: 'primary' }, phone_sms: { name: '短信验证码登录', type: 'secondary' }, huawei: { name: '通过华为账号登录', type: 'third_party', iconKey: 'huawei' }, apple: { name: '通过 Apple 登录', type: 'third_party', iconKey: 'apple' }, @@ -75,23 +61,13 @@ const PROVIDER_METADATA: Record = { - android: config.ALIYUN_FUSION_SCHEME_CODE_ANDROID, - ios: config.ALIYUN_FUSION_SCHEME_CODE_IOS, - harmony: config.ALIYUN_FUSION_SCHEME_CODE_HARMONY, - }; - return !!schemeCodeMap[platform]; - } + case 'phone_sms': + return !!(config.ALIYUN_ACCESS_KEY_ID && config.ALIYUN_ACCESS_KEY_SECRET); case 'huawei': return !!(config.HUAWEI_CLIENT_ID && config.HUAWEI_CLIENT_SECRET); case 'apple': @@ -166,7 +142,7 @@ export async function authRoutes(app: FastifyInstance): Promise { return reply.send({ success: true, data: result, error: null }); }); - // Phase 2: Phone login (保留占位,融合认证使用 /auth/fusion/ 路径) + // Phase 2: Phone login app.post('/auth/phone', { config: { rateLimit: { max: 10, timeWindow: '1 minute' } } }, async (_request, reply) => { return reply.status(501).send({ success: false, @@ -175,63 +151,6 @@ export async function authRoutes(app: FastifyInstance): Promise { }); }); - // ── Fusion Auth ────────────────────────────────────────────────── - // Step 1: 客户端获取 SDK 鉴权 Token - app.post('/auth/fusion/token', { config: { rateLimit: { max: 10, timeWindow: '1 minute' } } }, async (request, reply) => { - const parsed = fusionTokenSchema.safeParse(request.body); - if (!parsed.success) { - return reply.status(400).send({ - success: false, - data: null, - error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message ?? 'Invalid input' }, - }); - } - - try { - const fusionAuthToken = await getFusionAuthToken({ - platform: parsed.data.platform, - packageName: parsed.data.packageName, - packageSign: parsed.data.packageSign, - bundleId: parsed.data.bundleId, - }); - return reply.send({ success: true, data: { fusionAuthToken }, error: null }); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to get fusion auth token'; - const statusCode = (err as Record)?.statusCode ?? 503; - return reply.status(statusCode as number).send({ - success: false, - data: null, - error: { code: 'SERVICE_UNAVAILABLE', message }, - }); - } - }); - - // Step 2: 客户端提交 verifyToken,服务端换取手机号并完成登录/注册 - app.post('/auth/fusion/verify', { config: { rateLimit: { max: 10, timeWindow: '1 minute' } } }, async (request, reply) => { - const parsed = fusionVerifySchema.safeParse(request.body); - if (!parsed.success) { - return reply.status(400).send({ - success: false, - data: null, - error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message ?? 'Invalid input' }, - }); - } - - try { - const phoneNumber = await verifyFusionAuthToken(parsed.data.verifyToken); - const result = await findOrCreatePhone(phoneNumber, app); - return reply.send({ success: true, data: result, error: null }); - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Phone verification failed'; - const statusCode = (err as Record)?.statusCode ?? 401; - return reply.status(statusCode as number).send({ - success: false, - data: null, - error: { code: 'UNAUTHORIZED', message }, - }); - } - }); - // ── Login Providers ────────────────────────────────────────────── app.get('/auth/providers', async (request, reply) => { const parsed = providersQuerySchema.safeParse(request.query); diff --git a/src/services/auth/fusion-auth-client.ts b/src/services/auth/fusion-auth-client.ts deleted file mode 100644 index a363ec8..0000000 --- a/src/services/auth/fusion-auth-client.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { createRequire } from 'node:module'; -import * as $Dypnsapi20170525 from '@alicloud/dypnsapi20170525'; -import * as $OpenApi from '@alicloud/openapi-client'; -import * as $Util from '@alicloud/tea-util'; -import { config } from '../../utils/config.js'; -import { UnauthorizedError } from '../../utils/errors.js'; - -const require = createRequire(import.meta.url); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const DypnsapiClient: any = require('@alicloud/dypnsapi20170525').default ?? require('@alicloud/dypnsapi20170525'); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let clientInstance: any = null; - -function getClient() { - if (!clientInstance) { - if (!config.ALIYUN_ACCESS_KEY_ID || !config.ALIYUN_ACCESS_KEY_SECRET) { - throw new UnauthorizedError('Fusion auth is not configured on the server'); - } - - const openApiConfig = new $OpenApi.Config({ - accessKeyId: config.ALIYUN_ACCESS_KEY_ID, - accessKeySecret: config.ALIYUN_ACCESS_KEY_SECRET, - endpoint: 'dypnsapi.aliyuncs.com', - }); - - clientInstance = new DypnsapiClient(openApiConfig); - } - return clientInstance; -} - -function requireSchemeCode(platform: string): string { - const schemeCodeMap: Record = { - Android: config.ALIYUN_FUSION_SCHEME_CODE_ANDROID, - iOS: config.ALIYUN_FUSION_SCHEME_CODE_IOS, - Harmony: config.ALIYUN_FUSION_SCHEME_CODE_HARMONY, - }; - const schemeCode = schemeCodeMap[platform]; - if (!schemeCode) { - throw new UnauthorizedError(`Fusion auth scheme code not configured for platform: ${platform}`); - } - return schemeCode; -} - -export async function getFusionAuthToken(options: { - platform: string; - packageName?: string; - packageSign?: string; - bundleId?: string; -}): Promise { - const client = getClient(); - const schemeCode = requireSchemeCode(options.platform); - - const request = new $Dypnsapi20170525.GetFusionAuthTokenRequest({ - schemeCode, - platform: options.platform, - durationSeconds: 900, - packageName: options.packageName, - packageSign: options.packageSign, - bundleId: options.bundleId, - }); - - const runtime = new $Util.RuntimeOptions({}); - - try { - const response = await client.getFusionAuthTokenWithOptions(request, runtime); - if (!response.body?.success || !response.body?.model) { - throw new UnauthorizedError( - `GetFusionAuthToken failed: ${response.body?.message ?? 'unknown error'}`, - ); - } - return response.body.model; - } catch (error: unknown) { - if (error instanceof UnauthorizedError) throw error; - const msg = error instanceof Error ? error.message : 'Unknown SDK error'; - throw new UnauthorizedError(`Failed to get fusion auth token: ${msg}`); - } -} - -export async function verifyFusionAuthToken(verifyToken: string): Promise { - const client = getClient(); - - const request = new $Dypnsapi20170525.VerifyWithFusionAuthTokenRequest({ - verifyToken, - }); - - const runtime = new $Util.RuntimeOptions({}); - - try { - const response = await client.verifyWithFusionAuthTokenWithOptions(request, runtime); - if (!response.body?.success || !response.body?.model) { - throw new UnauthorizedError( - `VerifyWithFusionAuthToken failed: ${response.body?.message ?? 'unknown error'}`, - ); - } - - const { phoneNumber, verifyResult } = response.body.model as { - phoneNumber?: string; - verifyResult?: string; - }; - - if (verifyResult !== 'PASS' || !phoneNumber) { - throw new UnauthorizedError('Phone verification failed'); - } - - return phoneNumber; - } catch (error: unknown) { - if (error instanceof UnauthorizedError) throw error; - throw new UnauthorizedError('Failed to verify fusion auth token'); - } -} diff --git a/src/utils/config.ts b/src/utils/config.ts index c05db4b..1277d25 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -18,9 +18,6 @@ const envSchema = z.object({ APPLE_BUNDLE_ID: z.string().optional(), ALIYUN_ACCESS_KEY_ID: z.string().optional(), ALIYUN_ACCESS_KEY_SECRET: z.string().optional(), - ALIYUN_FUSION_SCHEME_CODE_ANDROID: z.string().optional(), - ALIYUN_FUSION_SCHEME_CODE_IOS: z.string().optional(), - ALIYUN_FUSION_SCHEME_CODE_HARMONY: z.string().optional(), PORT: z.coerce.number().default(3000), NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),