fix(runtime): enforce session and environment contracts

This commit is contained in:
2026-07-16 19:56:15 +08:00
parent acabf165cd
commit cc06ed034d
7 changed files with 444 additions and 203 deletions
+6 -2
View File
@@ -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",
+144 -144
View File
@@ -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
@@ -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);
}
+60 -24
View File
@@ -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
);
return {
baseUrl,
wsUrl,
connectTimeout,
receiveTimeout,
sendTimeout,
};
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 result.data;
}
function hasProtocol(value: string, protocols: readonly string[]): boolean {
try {
return protocols.includes(new URL(value).protocol);
} catch {
return false;
}
}
@@ -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();
});
});
@@ -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));
});
});
+39 -32
View File
@@ -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<Result<User>> {
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<void> {
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);
}
const r3 = await this.userStorage.setUser(data.user.toJson());
if (!r3.success) {
log.warn("[AuthRepository] setUser failed", r3.error);
private async _cacheUser(user: User): Promise<void> {
try {
const userResult = await this.userStorage.setUser(user.toJson());
if (Result.isErr(userResult)) {
log.warn("[AuthRepository] setUser failed", userResult.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);
}
}