From cc06ed034d8ab4c7f08c839c6061f7a99ea760c9 Mon Sep 17 00:00:00 2001 From: chenhang Date: Thu, 16 Jul 2026 19:56:15 +0800 Subject: [PATCH] fix(runtime): enforce session and environment contracts --- package.json | 8 +- pnpm-lock.yaml | 288 +++++++++--------- .../net/config/__tests__/api_config.test.ts | 83 +++++ src/core/net/config/api_config.ts | 82 +++-- .../auth_repository.facebook_identity.test.ts | 23 ++ .../__tests__/auth_repository.session.test.ts | 88 ++++++ src/data/repositories/auth_repository.ts | 75 ++--- 7 files changed, 444 insertions(+), 203 deletions(-) create mode 100644 src/core/net/config/__tests__/api_config.test.ts diff --git a/package.json b/package.json index 08d12345..8156c1be 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,10 @@ "name": "cozsweet-frontend-nextjs", "version": "0.1.0", "private": true, + "packageManager": "pnpm@10.30.3", + "engines": { + "node": "24.x" + }, "scripts": { "dev": "next dev", "build": "next build", @@ -37,7 +41,7 @@ "@xstate/react": "^5", "classnames": "^2.5.1", "dexie": "^4.4.3", - "esbuild": "^0.25.0", + "esbuild": "^0.28.0", "lucide-react": "^1.18.0", "next": "16.2.7", "next-auth": "^4.24.14", @@ -55,7 +59,7 @@ "devDependencies": { "@playwright/test": "^1.61.1", "@tailwindcss/postcss": "^4", - "@types/node": "^20", + "@types/node": "^24", "@types/react": "^19", "@types/react-dom": "^19", "@vitest/coverage-v8": "^4.1.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c7e2f9c..79b05f38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,10 +13,10 @@ importers: version: 5.2.0 '@sentry/nextjs': specifier: ^10.61.0 - version: 10.61.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(next@16.2.7(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.108.0(esbuild@0.25.12)) + version: 10.61.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(next@16.2.7(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.108.0(esbuild@0.28.1)) '@serwist/turbopack': specifier: ^9.0.0 - version: 9.5.11(esbuild@0.25.12)(next@16.2.7(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + version: 9.5.11(esbuild@0.28.1)(next@16.2.7(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3) '@stripe/react-stripe-js': specifier: ^6.6.0 version: 6.6.0(@stripe/stripe-js@9.8.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -33,8 +33,8 @@ importers: specifier: ^4.4.3 version: 4.4.3 esbuild: - specifier: ^0.25.0 - version: 0.25.12 + specifier: ^0.28.0 + version: 0.28.1 lucide-react: specifier: ^1.18.0 version: 1.18.0(react@19.2.4) @@ -82,8 +82,8 @@ importers: specifier: ^4 version: 4.3.0 '@types/node': - specifier: ^20 - version: 20.19.41 + specifier: ^24 + version: 24.13.3 '@types/react': specifier: ^19 version: 19.2.16 @@ -116,7 +116,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@20.19.41)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)) + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)) packages: @@ -274,158 +274,158 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1471,8 +1471,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@20.19.41': - resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} @@ -2180,8 +2180,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true @@ -3716,8 +3716,8 @@ packages: uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} undici@7.27.2: resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} @@ -4220,82 +4220,82 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.12': + '@esbuild/aix-ppc64@0.28.1': optional: true - '@esbuild/android-arm64@0.25.12': + '@esbuild/android-arm64@0.28.1': optional: true - '@esbuild/android-arm@0.25.12': + '@esbuild/android-arm@0.28.1': optional: true - '@esbuild/android-x64@0.25.12': + '@esbuild/android-x64@0.28.1': optional: true - '@esbuild/darwin-arm64@0.25.12': + '@esbuild/darwin-arm64@0.28.1': optional: true - '@esbuild/darwin-x64@0.25.12': + '@esbuild/darwin-x64@0.28.1': optional: true - '@esbuild/freebsd-arm64@0.25.12': + '@esbuild/freebsd-arm64@0.28.1': optional: true - '@esbuild/freebsd-x64@0.25.12': + '@esbuild/freebsd-x64@0.28.1': optional: true - '@esbuild/linux-arm64@0.25.12': + '@esbuild/linux-arm64@0.28.1': optional: true - '@esbuild/linux-arm@0.25.12': + '@esbuild/linux-arm@0.28.1': optional: true - '@esbuild/linux-ia32@0.25.12': + '@esbuild/linux-ia32@0.28.1': optional: true - '@esbuild/linux-loong64@0.25.12': + '@esbuild/linux-loong64@0.28.1': optional: true - '@esbuild/linux-mips64el@0.25.12': + '@esbuild/linux-mips64el@0.28.1': optional: true - '@esbuild/linux-ppc64@0.25.12': + '@esbuild/linux-ppc64@0.28.1': optional: true - '@esbuild/linux-riscv64@0.25.12': + '@esbuild/linux-riscv64@0.28.1': optional: true - '@esbuild/linux-s390x@0.25.12': + '@esbuild/linux-s390x@0.28.1': optional: true - '@esbuild/linux-x64@0.25.12': + '@esbuild/linux-x64@0.28.1': optional: true - '@esbuild/netbsd-arm64@0.25.12': + '@esbuild/netbsd-arm64@0.28.1': optional: true - '@esbuild/netbsd-x64@0.25.12': + '@esbuild/netbsd-x64@0.28.1': optional: true - '@esbuild/openbsd-arm64@0.25.12': + '@esbuild/openbsd-arm64@0.28.1': optional: true - '@esbuild/openbsd-x64@0.25.12': + '@esbuild/openbsd-x64@0.28.1': optional: true - '@esbuild/openharmony-arm64@0.25.12': + '@esbuild/openharmony-arm64@0.28.1': optional: true - '@esbuild/sunos-x64@0.25.12': + '@esbuild/sunos-x64@0.28.1': optional: true - '@esbuild/win32-arm64@0.25.12': + '@esbuild/win32-arm64@0.28.1': optional: true - '@esbuild/win32-ia32@0.25.12': + '@esbuild/win32-ia32@0.28.1': optional: true - '@esbuild/win32-x64@0.25.12': + '@esbuild/win32-x64@0.28.1': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': @@ -4810,7 +4810,7 @@ snapshots: dependencies: '@sentry/core': 10.61.0 - '@sentry/nextjs@10.61.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(next@16.2.7(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.108.0(esbuild@0.25.12))': + '@sentry/nextjs@10.61.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(next@16.2.7(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.108.0(esbuild@0.28.1))': dependencies: '@opentelemetry/api': 1.9.1 '@rollup/plugin-commonjs': 28.0.1(rollup@4.62.2) @@ -4822,7 +4822,7 @@ snapshots: '@sentry/opentelemetry': 10.61.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)) '@sentry/react': 10.61.0(react@19.2.4) '@sentry/vercel-edge': 10.61.0 - '@sentry/webpack-plugin': 5.3.0(webpack@5.108.0(esbuild@0.25.12)) + '@sentry/webpack-plugin': 5.3.0(webpack@5.108.0(esbuild@0.28.1)) next: 16.2.7(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) rollup: 4.62.2 stacktrace-parser: 0.1.11 @@ -4903,10 +4903,10 @@ snapshots: '@opentelemetry/api': 1.9.1 '@sentry/core': 10.61.0 - '@sentry/webpack-plugin@5.3.0(webpack@5.108.0(esbuild@0.25.12))': + '@sentry/webpack-plugin@5.3.0(webpack@5.108.0(esbuild@0.28.1))': dependencies: '@sentry/bundler-plugin-core': 5.3.0 - webpack: 5.108.0(esbuild@0.25.12) + webpack: 5.108.0(esbuild@0.28.1) transitivePeerDependencies: - encoding - supports-color @@ -4925,7 +4925,7 @@ snapshots: transitivePeerDependencies: - browserslist - '@serwist/turbopack@9.5.11(esbuild@0.25.12)(next@16.2.7(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)': + '@serwist/turbopack@9.5.11(esbuild@0.28.1)(next@16.2.7(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)': dependencies: '@serwist/build': 9.5.11(browserslist@4.28.2)(typescript@5.9.3) '@serwist/utils': 9.5.11(browserslist@4.28.2) @@ -4939,7 +4939,7 @@ snapshots: serwist: 9.5.11(browserslist@4.28.2)(typescript@5.9.3) zod: 4.4.1 optionalDependencies: - esbuild: 0.25.12 + esbuild: 0.28.1 typescript: 5.9.3 transitivePeerDependencies: - '@swc/helpers' @@ -5119,9 +5119,9 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@20.19.41': + '@types/node@24.13.3': dependencies: - undici-types: 6.21.0 + undici-types: 7.18.2 '@types/react-dom@19.2.3(@types/react@19.2.16)': dependencies: @@ -5312,7 +5312,7 @@ snapshots: obug: 2.1.2 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@20.19.41)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)) + vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)) '@vitest/expect@4.1.8': dependencies: @@ -5323,13 +5323,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@20.19.41)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0))': + '@vitest/mocker@4.1.8(vite@8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0))': dependencies: '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.0.16(@types/node@20.19.41)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0) + vite: 8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0) '@vitest/pretty-format@4.1.8': dependencies: @@ -5358,7 +5358,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@20.19.41)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)) + vitest: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)) '@vitest/utils@4.1.8': dependencies: @@ -5936,34 +5936,34 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.25.12: + esbuild@0.28.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 escalade@3.2.0: {} @@ -6568,7 +6568,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.19.41 + '@types/node': 24.13.3 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -6782,15 +6782,15 @@ snapshots: minimist@1.2.8: {} - minimizer-webpack-plugin@5.6.1(esbuild@0.25.12)(webpack@5.108.0(esbuild@0.25.12)): + minimizer-webpack-plugin@5.6.1(esbuild@0.28.1)(webpack@5.108.0(esbuild@0.28.1)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.48.0 - webpack: 5.108.0(esbuild@0.25.12) + webpack: 5.108.0(esbuild@0.28.1) optionalDependencies: - esbuild: 0.25.12 + esbuild: 0.28.1 minipass@7.1.3: {} @@ -7619,7 +7619,7 @@ snapshots: uncrypto@0.1.3: {} - undici-types@6.21.0: {} + undici-types@7.18.2: {} undici@7.27.2: {} @@ -7683,7 +7683,7 @@ snapshots: uuid@8.3.2: {} - vite@8.0.16(@types/node@20.19.41)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0): + vite@8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -7691,16 +7691,16 @@ snapshots: rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 20.19.41 - esbuild: 0.25.12 + '@types/node': 24.13.3 + esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 terser: 5.48.0 - vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@20.19.41)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@20.19.41)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)): + vitest@4.1.8(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.8)(@vitest/ui@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)): dependencies: '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@20.19.41)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0)) + '@vitest/mocker': 4.1.8(vite@8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)) '@vitest/pretty-format': 4.1.8 '@vitest/runner': 4.1.8 '@vitest/snapshot': 4.1.8 @@ -7717,11 +7717,11 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.0.16(@types/node@20.19.41)(esbuild@0.25.12)(jiti@2.7.0)(terser@5.48.0) + vite: 8.0.16(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 - '@types/node': 20.19.41 + '@types/node': 24.13.3 '@vitest/coverage-v8': 4.1.8(vitest@4.1.8) '@vitest/ui': 4.1.8(vitest@4.1.8) jsdom: 29.1.1 @@ -7744,7 +7744,7 @@ snapshots: webpack-sources@3.5.0: {} - webpack@5.108.0(esbuild@0.25.12): + webpack@5.108.0(esbuild@0.28.1): dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 @@ -7762,7 +7762,7 @@ snapshots: graceful-fs: 4.2.11 loader-runner: 4.3.2 mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(esbuild@0.25.12)(webpack@5.108.0(esbuild@0.25.12)) + minimizer-webpack-plugin: 5.6.1(esbuild@0.28.1)(webpack@5.108.0(esbuild@0.28.1)) neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 diff --git a/src/core/net/config/__tests__/api_config.test.ts b/src/core/net/config/__tests__/api_config.test.ts new file mode 100644 index 00000000..c1818415 --- /dev/null +++ b/src/core/net/config/__tests__/api_config.test.ts @@ -0,0 +1,83 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { getApiConfig } from "@/core/net/config/api_config"; + +const CONFIG_ENV_KEYS = [ + "NEXT_PUBLIC_API_BASE_URL", + "NEXT_PUBLIC_WS_BASE_URL", + "NEXT_PUBLIC_API_CONNECT_TIMEOUT", + "NEXT_PUBLIC_API_RECEIVE_TIMEOUT", + "NEXT_PUBLIC_API_SEND_TIMEOUT", +] as const; + +afterEach(() => { + vi.unstubAllEnvs(); +}); + +describe("getApiConfig", () => { + it("keeps local defaults in development", () => { + vi.stubEnv("NEXT_PUBLIC_APP_ENV", "development"); + unsetConfigEnv(); + + expect(getApiConfig()).toEqual({ + baseUrl: "http://172.16.48.49:3002", + wsUrl: "ws://172.16.48.49:3002/ws", + connectTimeout: 30_000, + receiveTimeout: 60_000, + sendTimeout: 30_000, + }); + }); + + it.each(["test", "production"])( + "requires explicit service URLs in %s", + (appEnv) => { + vi.stubEnv("NEXT_PUBLIC_APP_ENV", appEnv); + unsetConfigEnv(); + + expect(() => getApiConfig()).toThrow( + `[ApiConfig] Invalid ${appEnv} configuration`, + ); + }, + ); + + it("parses an explicit non-development configuration", () => { + vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production"); + vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "https://api.example.test"); + vi.stubEnv("NEXT_PUBLIC_WS_BASE_URL", "wss://api.example.test/ws"); + vi.stubEnv("NEXT_PUBLIC_API_CONNECT_TIMEOUT", "1000"); + vi.stubEnv("NEXT_PUBLIC_API_RECEIVE_TIMEOUT", "2000"); + vi.stubEnv("NEXT_PUBLIC_API_SEND_TIMEOUT", "3000"); + + expect(getApiConfig()).toEqual({ + baseUrl: "https://api.example.test", + wsUrl: "wss://api.example.test/ws", + connectTimeout: 1000, + receiveTimeout: 2000, + sendTimeout: 3000, + }); + }); + + it.each(["not-a-number", "0", "-1", "1.5"])( + "rejects invalid timeout %s", + (timeout) => { + vi.stubEnv("NEXT_PUBLIC_APP_ENV", "test"); + vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "https://api.example.test"); + vi.stubEnv("NEXT_PUBLIC_WS_BASE_URL", "wss://api.example.test/ws"); + vi.stubEnv("NEXT_PUBLIC_API_CONNECT_TIMEOUT", timeout); + + expect(() => getApiConfig()).toThrow(/connectTimeout/); + }, + ); + + it("rejects URLs with the wrong protocol", () => { + vi.stubEnv("NEXT_PUBLIC_APP_ENV", "production"); + vi.stubEnv("NEXT_PUBLIC_API_BASE_URL", "ftp://api.example.test"); + vi.stubEnv("NEXT_PUBLIC_WS_BASE_URL", "https://api.example.test/ws"); + + expect(() => getApiConfig()).toThrow(/baseUrl.*wsUrl/); + }); +}); + +function unsetConfigEnv(): void { + for (const key of CONFIG_ENV_KEYS) vi.stubEnv(key, undefined); +} diff --git a/src/core/net/config/api_config.ts b/src/core/net/config/api_config.ts index d44ea80c..afdac844 100644 --- a/src/core/net/config/api_config.ts +++ b/src/core/net/config/api_config.ts @@ -4,6 +4,8 @@ * 通过环境变量配置后端服务地址与超时时间。 * */ +import { z } from "zod"; + import { AppEnvUtil, type AppEnv } from "@/utils/app-env"; export type { AppEnv }; @@ -30,6 +32,29 @@ export class TimeoutConstants { static readonly longTimeout = 60000; } +const DEVELOPMENT_API_BASE_URL = "http://172.16.48.49:3002"; +const DEVELOPMENT_WS_BASE_URL = "ws://172.16.48.49:3002/ws"; + +const ApiConfigSchema = z.object({ + baseUrl: z + .string() + .trim() + .min(1) + .refine((value) => hasProtocol(value, ["http:", "https:"]), { + message: "must be an http(s) URL", + }), + wsUrl: z + .string() + .trim() + .min(1) + .refine((value) => hasProtocol(value, ["ws:", "wss:"]), { + message: "must be a ws(s) URL", + }), + connectTimeout: z.coerce.number().int().positive(), + receiveTimeout: z.coerce.number().int().positive(), + sendTimeout: z.coerce.number().int().positive(), +}); + /** * 解析当前环境 */ @@ -41,29 +66,40 @@ export function getAppEnv(): AppEnv { * 获取当前 API 配置 */ export function getApiConfig(): ApiConfig { - const baseUrl = - process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://172.16.48.49:3002"; - const wsUrl = - process.env.NEXT_PUBLIC_WS_BASE_URL ?? "ws://172.16.48.49:3002/ws"; + const appEnv = getAppEnv(); + const allowDevelopmentDefaults = appEnv === "development"; + const result = ApiConfigSchema.safeParse({ + baseUrl: + process.env.NEXT_PUBLIC_API_BASE_URL ?? + (allowDevelopmentDefaults ? DEVELOPMENT_API_BASE_URL : undefined), + wsUrl: + process.env.NEXT_PUBLIC_WS_BASE_URL ?? + (allowDevelopmentDefaults ? DEVELOPMENT_WS_BASE_URL : undefined), + connectTimeout: + process.env.NEXT_PUBLIC_API_CONNECT_TIMEOUT ?? + TimeoutConstants.shortTimeout, + receiveTimeout: + process.env.NEXT_PUBLIC_API_RECEIVE_TIMEOUT ?? + TimeoutConstants.longTimeout, + sendTimeout: + process.env.NEXT_PUBLIC_API_SEND_TIMEOUT ?? + TimeoutConstants.shortTimeout, + }); - const connectTimeout = Number.parseInt( - process.env.NEXT_PUBLIC_API_CONNECT_TIMEOUT ?? "30000", - 10 - ); - const receiveTimeout = Number.parseInt( - process.env.NEXT_PUBLIC_API_RECEIVE_TIMEOUT ?? "60000", - 10 - ); - const sendTimeout = Number.parseInt( - process.env.NEXT_PUBLIC_API_SEND_TIMEOUT ?? "30000", - 10 - ); + if (!result.success) { + const issues = result.error.issues + .map((issue) => `${issue.path.join(".")}: ${issue.message}`) + .join("; "); + throw new Error(`[ApiConfig] Invalid ${appEnv} configuration: ${issues}`); + } - return { - baseUrl, - wsUrl, - connectTimeout, - receiveTimeout, - sendTimeout, - }; + return result.data; +} + +function hasProtocol(value: string, protocols: readonly string[]): boolean { + try { + return protocols.includes(new URL(value).protocol); + } catch { + return false; + } } diff --git a/src/data/repositories/__tests__/auth_repository.facebook_identity.test.ts b/src/data/repositories/__tests__/auth_repository.facebook_identity.test.ts index 04776b80..cd34f9f8 100644 --- a/src/data/repositories/__tests__/auth_repository.facebook_identity.test.ts +++ b/src/data/repositories/__tests__/auth_repository.facebook_identity.test.ts @@ -112,4 +112,27 @@ describe("AuthRepository facebook identity", () => { expect(storage.setDeviceId).toHaveBeenCalledWith("device-1"); expect(userStorage.setUserId).toHaveBeenCalledWith("guest-1"); }); + + it("does not report guest psid login success when session persistence fails", async () => { + const storageError = new Error("guest token write failed"); + const facebookPsidLogin = vi.fn(async () => + FacebookPsidLoginResponse.fromJson({ + token: "guest-token", + matchedBy: "psid_incomplete", + fbPsid: "psid-1", + hasCompleteFacebookIdentity: false, + isGuest: true, + userId: "guest-1", + }), + ); + const { repository, storage } = createRepository({ facebookPsidLogin }); + vi.mocked(storage.setGuestToken).mockResolvedValueOnce( + Result.err(storageError), + ); + + const result = await repository.facebookPsidLogin({ psid: "psid-1" }); + + expect(Result.isErr(result)).toBe(true); + expect(storage.setLoginProvider).not.toHaveBeenCalled(); + }); }); diff --git a/src/data/repositories/__tests__/auth_repository.session.test.ts b/src/data/repositories/__tests__/auth_repository.session.test.ts index e9e86954..1c36f23f 100644 --- a/src/data/repositories/__tests__/auth_repository.session.test.ts +++ b/src/data/repositories/__tests__/auth_repository.session.test.ts @@ -2,6 +2,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { GuestLoginResponse, + LoginResponse, + LoginStatus, RefreshTokenResponse, } from "@/data/dto/auth"; import { AuthRepository } from "@/data/repositories/auth_repository"; @@ -27,6 +29,7 @@ function createRepository(input: { ...input.storage, } as unknown as IAuthStorage; const userStorage = { + setUser: vi.fn(async () => Result.ok(undefined)), setUserId: vi.fn(async () => Result.ok(undefined)), clearUserData: vi.fn(async () => Result.ok(undefined)), ...input.userStorage, @@ -49,6 +52,12 @@ afterEach(() => { }); describe("AuthRepository session Result handling", () => { + const loginResponse = LoginResponse.from({ + token: "login-token", + refreshToken: "refresh-token", + user: { id: "user-1", username: "Elio" }, + }); + it("returns a failed auth cleanup Result while still attempting user cleanup", async () => { const clearError = new Error("auth cleanup failed"); const { repository, userStorage } = createRepository({ @@ -149,4 +158,83 @@ describe("AuthRepository session Result handling", () => { ); expect(storage.setLoginProvider).not.toHaveBeenCalled(); }); + + it("does not report email login success when the login token cannot be stored", async () => { + const storageError = new Error("login token write failed"); + const { repository, storage, userStorage } = createRepository({ + api: { emailLogin: vi.fn(async () => loginResponse) }, + storage: { + setLoginToken: vi.fn(async () => Result.err(storageError)), + }, + }); + + const result = await repository.emailLogin({ + email: "elio@example.test", + password: "password", + }); + + expect(Result.isErr(result)).toBe(true); + expect(Result.isErr(result) && result.error.message).toBe( + storageError.message, + ); + expect(storage.setRefreshToken).not.toHaveBeenCalled(); + expect(userStorage.setUserId).not.toHaveBeenCalled(); + expect(storage.setLoginProvider).not.toHaveBeenCalled(); + }); + + it("writes the provider only after all other session identity fields", async () => { + const storageError = new Error("user id write failed"); + const { repository, storage, userStorage } = createRepository({ + api: { emailLogin: vi.fn(async () => loginResponse) }, + userStorage: { + setUserId: vi.fn(async () => Result.err(storageError)), + }, + }); + + const result = await repository.emailLogin({ + email: "elio@example.test", + password: "password", + }); + + expect(Result.isErr(result)).toBe(true); + expect(storage.setLoginToken).toHaveBeenCalledWith("login-token"); + expect(storage.setRefreshToken).toHaveBeenCalledWith("refresh-token"); + expect(userStorage.setUserId).toHaveBeenCalledWith("user-1"); + expect(storage.setLoginProvider).not.toHaveBeenCalled(); + expect(userStorage.setUser).not.toHaveBeenCalled(); + }); + + it("keeps login successful when the non-critical user cache fails", async () => { + const cacheError = new Error("user cache write failed"); + const { repository, storage, userStorage } = createRepository({ + api: { emailLogin: vi.fn(async () => loginResponse) }, + userStorage: { + setUser: vi.fn(async () => Result.err(cacheError)), + }, + }); + + const result = await repository.emailLogin({ + email: "elio@example.test", + password: "password", + }); + + expect(result).toEqual(Result.ok(loginResponse)); + expect(storage.setLoginProvider).toHaveBeenCalledWith(LoginStatus.Email); + expect(userStorage.setUser).toHaveBeenCalledOnce(); + }); + + it("keeps a current-user response successful when its cache write throws", async () => { + const { repository } = createRepository({ + api: { getCurrentUser: vi.fn(async () => loginResponse.user) }, + userStorage: { + setUser: vi.fn(async () => { + throw new Error("storage unavailable"); + }), + }, + }); + + const result = await repository.getCurrentUser(); + + expect(result).toEqual(Result.ok(loginResponse.user)); + }); }); diff --git a/src/data/repositories/auth_repository.ts b/src/data/repositories/auth_repository.ts index 63d3164b..52f8e3ff 100644 --- a/src/data/repositories/auth_repository.ts +++ b/src/data/repositories/auth_repository.ts @@ -284,15 +284,21 @@ export class AuthRepository implements IAuthRepository { }); if (data.isGuest || !data.hasCompleteFacebookIdentity) { - await this.storage.setGuestToken(data.token); - await this.storage.setLoginProvider(LoginStatus.Guest); + const tokenResult = await this.storage.setGuestToken(data.token); + if (Result.isErr(tokenResult)) throw tokenResult.error; if (input.deviceId) { - await this.storage.setDeviceId(input.deviceId); + const deviceResult = await this.storage.setDeviceId(input.deviceId); + if (Result.isErr(deviceResult)) throw deviceResult.error; } const userId = data.userId || data.user?.id || ""; if (userId) { - await this.userStorage.setUserId(userId); + const userIdResult = await this.userStorage.setUserId(userId); + if (Result.isErr(userIdResult)) throw userIdResult.error; } + const providerResult = await this.storage.setLoginProvider( + LoginStatus.Guest, + ); + if (Result.isErr(providerResult)) throw providerResult.error; return LoginStatus.Guest; } @@ -345,13 +351,7 @@ export class AuthRepository implements IAuthRepository { async getCurrentUser(): Promise> { return Result.wrap(async () => { const user = await this.api.getCurrentUser(); - const writeResult = await this.userStorage.setUser(user.toJson()); - if (!writeResult.success) { - log.warn( - "[AuthRepository] failed to cache current user", - writeResult.error, - ); - } + await this._cacheUser(user); return user; }); } @@ -360,7 +360,7 @@ export class AuthRepository implements IAuthRepository { /** * 社交登录的公共包装:调 API → 调 `_saveLoginData` 持久化。 - * `_saveLoginData` 内部全部 best-effort,存储写失败不抛错。 + * 会话关键数据写入失败时,登录 Result 必须失败。 */ private async _socialLogin( provider: LoginStatusT, @@ -374,35 +374,42 @@ export class AuthRepository implements IAuthRepository { } /** - * 持久化登录态:login token → refresh token → User 对象 → userId。 - * 全部 best-effort,错误用 `Logger.warn` 记录但不让登录「失败」—— - * 调用方已经从 API 拿到了 LoginResponse,缓存只是离线加速。 + * 持久化登录态:login token → refresh token → userId → provider。 + * 这些字段共同决定会话是否可恢复,任一写入失败都会让登录 Result 失败; + * provider 最后写入,作为会话完成标记。完整 User 对象仅用于离线展示, + * 缓存失败只记录告警,不影响已经建立的会话。 */ private async _saveLoginData( data: LoginResponse, provider: LoginStatusT, ): Promise { - const r1 = await this.storage.setLoginToken(data.token); - if (!r1.success) { - log.warn("[AuthRepository] setLoginToken failed", r1.error); - } - const providerResult = await this.storage.setLoginProvider(provider); - if (!providerResult.success) { - log.warn("[AuthRepository] setLoginProvider failed", providerResult.error); - } + const tokenResult = await this.storage.setLoginToken(data.token); + if (Result.isErr(tokenResult)) throw tokenResult.error; + if (data.refreshToken) { - const r2 = await this.storage.setRefreshToken(data.refreshToken); - if (!r2.success) { - log.warn("[AuthRepository] setRefreshToken failed", r2.error); + const refreshTokenResult = await this.storage.setRefreshToken( + data.refreshToken, + ); + if (Result.isErr(refreshTokenResult)) throw refreshTokenResult.error; + } + + const userIdResult = await this.userStorage.setUserId(data.user.id); + if (Result.isErr(userIdResult)) throw userIdResult.error; + + const providerResult = await this.storage.setLoginProvider(provider); + if (Result.isErr(providerResult)) throw providerResult.error; + + await this._cacheUser(data.user); + } + + private async _cacheUser(user: User): Promise { + try { + const userResult = await this.userStorage.setUser(user.toJson()); + if (Result.isErr(userResult)) { + log.warn("[AuthRepository] setUser failed", userResult.error); } - } - const r3 = await this.userStorage.setUser(data.user.toJson()); - if (!r3.success) { - log.warn("[AuthRepository] setUser failed", r3.error); - } - const r4 = await this.userStorage.setUserId(data.user.id); - if (!r4.success) { - log.warn("[AuthRepository] setUserId failed", r4.error); + } catch (error) { + log.warn("[AuthRepository] setUser threw", error); } }