From d8016383580b7dc4b85a0b3f00f1686f4b075f92 Mon Sep 17 00:00:00 2001 From: mjnong Date: Wed, 12 Aug 2026 21:54:30 +0200 Subject: [PATCH 1/4] [CEL-1364] DEV-only sign-in bypass + devLogin() store helper --- .reposkein/edges.jsonl | 35 ++- .reposkein/nodes.jsonl | 93 +++++--- AGENTS.md | 30 ++- CHANGELOG.md | 7 + README.md | 24 ++- __tests__/dev-login.test.ts | 240 +++++++++++++++++++++ __tests__/login-form-dev-bypass.test.tsx | 261 +++++++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- src/auth-store.ts | 125 +++++++++++ src/import-meta-env.d.ts | 23 ++ src/index.ts | 4 + src/react/dev-sign-in.tsx | 122 +++++++++++ src/react/index.ts | 7 + src/react/login-form.tsx | 89 +++++++- src/types.ts | 61 ++++++ 16 files changed, 1083 insertions(+), 44 deletions(-) create mode 100644 __tests__/dev-login.test.ts create mode 100644 __tests__/login-form-dev-bypass.test.tsx create mode 100644 src/import-meta-env.d.ts create mode 100644 src/react/dev-sign-in.tsx diff --git a/.reposkein/edges.jsonl b/.reposkein/edges.jsonl index b7ada41..2796b9b 100644 --- a/.reposkein/edges.jsonl +++ b/.reposkein/edges.jsonl @@ -14,6 +14,8 @@ {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/auth-guard.test.ts"} {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts"} {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts"} +{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts"} +{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx"} {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx"} {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx"} {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/reduced-motion.test.tsx"} @@ -25,8 +27,10 @@ {"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/auth-guard.ts"} {"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/auth-store.ts"} {"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/extract-token.ts"} +{"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/import-meta-env.d.ts"} {"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/index.ts"} {"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/types.ts"} +{"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx"} {"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/index.ts"} {"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx"} {"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx"} @@ -50,6 +54,18 @@ {"from":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-store.ts","symbols":["createAuthStore"]} {"from":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthUser"]} {"from":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/class-tokens.ts#classTokensAt@2"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#flush@0"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#jsonResponse@3"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#routedFetch@1"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/dev-login.test.ts#OK_BODY"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/dev-login.test.ts#baseMe"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-store.ts","symbols":["createAuthStore"]} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthUser"]} +{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/login-form-dev-bypass.test.tsx#buildProps@1"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/login-form-dev-bypass.test.tsx#renderLogin@1"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","symbols":["DEV_LOGIN_EMAIL_STORAGE_KEY"]} +{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","symbols":["LoginForm"]} +{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["DevLoginResult"]} {"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/login-form-responsive.test.tsx#authApi"} {"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/login-form-responsive.test.tsx#authStore"} {"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","symbols":["InputOTP","InputOTPGroup","InputOTPSeparator","InputOTPSlot"]} @@ -86,15 +102,25 @@ {"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1"} {"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#toIdentity@1"} {"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/auth-store.ts#Identity"} +{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/auth-store.ts#DEFAULT_ACCESS_TOKEN_TTL"} +{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/auth-store.ts#DEV_LOGIN_MESSAGES"} {"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/extract-token.ts","symbols":["extractAccessToken"]} -{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AccessTokenSetListener","AuthStore","AuthStoreConfig","AuthUser","LogoutListener","OrgChangeListener","SessionUserType"]} +{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AccessTokenSetListener","AuthStore","AuthStoreConfig","AuthUser","DevLoginResult","LogoutListener","OrgChangeListener","SessionUserType"]} {"from":"rs1:7dc1dbe8d512:file:src/extract-token.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/extract-token.ts#extractAccessToken@1"} +{"from":"rs1:7dc1dbe8d512:file:src/import-meta-env.d.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/import-meta-env.d.ts#ImportMeta"} +{"from":"rs1:7dc1dbe8d512:file:src/import-meta-env.d.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/import-meta-env.d.ts#ImportMetaEnv"} {"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-api.ts","symbols":["createAuthApi"]} {"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-client.ts","symbols":["createAuthClient"]} {"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-guard.ts","symbols":["hasEntitlement","validateUserType"]} {"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-store.ts","symbols":["createAuthStore"]} {"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/extract-token.ts","symbols":["extractAccessToken"]} -{"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AccessTokenSetListener","AuthApi","AuthClient","AuthClientConfig","AuthError","AuthErrorResponse","AuthStore","AuthStoreConfig","AuthUser","DashboardLink","LogoutListener","OrgChangeListener","RegisterInput","RequestOtpResponse","SessionClaims","SessionUserType","UserType","VerifyOtpResponse"]} +{"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AccessTokenSetListener","AuthApi","AuthClient","AuthClientConfig","AuthError","AuthErrorResponse","AuthStore","AuthStoreConfig","AuthUser","DashboardLink","DevLoginFailure","DevLoginFailureReason","DevLoginResult","DevLoginSuccess","LogoutListener","OrgChangeListener","RegisterInput","RequestOtpResponse","SessionClaims","SessionUserType","UserType","VerifyOtpResponse"]} +{"from":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#DevSignInBypass@1"} +{"from":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#readDevLoginEmail@0"} +{"from":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#rememberDevLoginEmail@1"} +{"from":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/react/dev-sign-in.tsx#DevSignInBypassProps"} +{"from":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/react/dev-sign-in.tsx#DEV_LOGIN_EMAIL_STORAGE_KEY"} +{"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","symbols":["DEV_LOGIN_EMAIL_STORAGE_KEY","DevSignInBypass","DevSignInBypassProps","readDevLoginEmail","rememberDevLoginEmail"]} {"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","symbols":["InputOTP","InputOTPGroup","InputOTPProps","InputOTPSeparator","InputOTPSlot"]} {"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","symbols":["LoginForm","LoginFormProps"]} {"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","symbols":["RegisterForm","RegisterFormProps"]} @@ -113,6 +139,7 @@ {"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/react/login-form.tsx#LoginFormProps"} {"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/react/login-form.tsx#ERROR_MESSAGES"} {"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/react/login-form.tsx#LazySquircleShift"} +{"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","symbols":["DevSignInBypass","readDevLoginEmail","rememberDevLoginEmail"]} {"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","symbols":["InputOTP","InputOTPGroup","InputOTPSeparator","InputOTPSlot"]} {"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthApi","AuthError","AuthStore"]} {"from":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterCard@1"} @@ -139,6 +166,8 @@ {"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthStoreConfig"} {"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthUser"} {"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#DashboardLink"} +{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#DevLoginFailure"} +{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#DevLoginSuccess"} {"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#RegisterInput"} {"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#RequestOtpResponse"} {"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#SessionClaims"} @@ -165,11 +194,13 @@ {"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.performRefresh@0"} {"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.scheduleRefresh@1"} {"from":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTP@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#countOtpSeparators@1","call_sites":1,"confidence":1.0,"resolution":"exact"} +{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleDevLogin@0","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#rememberDevLoginEmail@1","call_sites":1,"confidence":1.0,"resolution":"exact"} {"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleEmailSubmit@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","call_sites":1,"confidence":0.7,"resolution":"name_match"} {"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleOtpSubmit@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","call_sites":1,"confidence":0.7,"resolution":"name_match"} {"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleResend@0","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","call_sites":1,"confidence":0.7,"resolution":"name_match"} {"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#otpSentNotice@1","call_sites":1,"confidence":1.0,"resolution":"exact"} {"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2"} +{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleDevLogin@0"} {"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleEmailSubmit@1"} {"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleOtpSubmit@1"} {"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleResend@0"} diff --git a/.reposkein/nodes.jsonl b/.reposkein/nodes.jsonl index 89e5270..1c69536 100644 --- a/.reposkein/nodes.jsonl +++ b/.reposkein/nodes.jsonl @@ -3,35 +3,39 @@ {"id":"rs1:7dc1dbe8d512:dir:__tests__","labels":["Directory"],"name":"__tests__","path":"__tests__"} {"id":"rs1:7dc1dbe8d512:dir:src","labels":["Directory"],"name":"src","path":"src"} {"id":"rs1:7dc1dbe8d512:dir:src/react","labels":["Directory"],"name":"react","path":"src/react"} -{"id":"rs1:7dc1dbe8d512:file:AGENTS.md","labels":["File"],"content_hash":"cd2218f6435a4474090879647fc0f3be7700d9e910afbfb3589d44be26a7b8dd","extension":"md","language":"markdown","name":"AGENTS.md","path":"AGENTS.md","role":"docs"} -{"id":"rs1:7dc1dbe8d512:file:CHANGELOG.md","labels":["File"],"content_hash":"a014b210a0bcd46697937aff2cb1898c2771b73565fab5aa53389346e0a0f80e","extension":"md","language":"markdown","name":"CHANGELOG.md","path":"CHANGELOG.md","role":"docs"} +{"id":"rs1:7dc1dbe8d512:file:AGENTS.md","labels":["File"],"content_hash":"90521eec765e0c506ca87171a3866588c3f33dd129ac05bd0e155860f01f676a","extension":"md","language":"markdown","name":"AGENTS.md","path":"AGENTS.md","role":"docs"} +{"id":"rs1:7dc1dbe8d512:file:CHANGELOG.md","labels":["File"],"content_hash":"9683f60a75556aeceb2ac3abfd0f6d8c7e9b47a025d2aa060385b7f1cac42952","extension":"md","language":"markdown","name":"CHANGELOG.md","path":"CHANGELOG.md","role":"docs"} {"id":"rs1:7dc1dbe8d512:file:LICENSE","labels":["File"],"content_hash":"109ee5eff963883c455d2db66a4bcc6a9d1d85e2d443468738ec4203087155f2","language":"unknown","name":"LICENSE","path":"LICENSE","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:README.md","labels":["File"],"content_hash":"0397b7c0a14603d5ab6d71efbba4d5f80c39ce46dc74b8ac17475d09ab6b0f7c","extension":"md","language":"markdown","name":"README.md","path":"README.md","role":"docs"} +{"id":"rs1:7dc1dbe8d512:file:README.md","labels":["File"],"content_hash":"23078a21dea08176c89997a7575e4806aa96896bdff10fdac30e183198656ff2","extension":"md","language":"markdown","name":"README.md","path":"README.md","role":"docs"} {"id":"rs1:7dc1dbe8d512:file:__tests__/auth-api.test.ts","labels":["File"],"content_hash":"b8de8a8ba02d1afcba7edf49d14b3d40d7aa5409c49dc12117bfaa2000362ee2","extension":"ts","language":"typescript","name":"auth-api.test.ts","path":"__tests__/auth-api.test.ts","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/auth-client.test.ts","labels":["File"],"content_hash":"74a706baefe4808229cee783014d494210f6c4870aecff6765656e9e41c6a0de","extension":"ts","language":"typescript","name":"auth-client.test.ts","path":"__tests__/auth-client.test.ts","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/auth-guard.test.ts","labels":["File"],"content_hash":"f9d44e2db4c7e9e35a51b0a522d31217f93339fc627c3f8e0fb68955f060ca58","extension":"ts","language":"typescript","name":"auth-guard.test.ts","path":"__tests__/auth-guard.test.ts","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","labels":["File"],"content_hash":"e9cd0c90ef702d907ed4141d8425d28fd3ca8cd0cf41902148af7fa8ea589387","extension":"ts","language":"typescript","name":"auth-store.test.ts","path":"__tests__/auth-store.test.ts","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts","labels":["File"],"content_hash":"7429ad6c713311fdd7d0b882966747864b5810038bc02e729854d6a6cf473688","extension":"ts","language":"typescript","name":"class-tokens.ts","path":"__tests__/class-tokens.ts","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","labels":["File"],"content_hash":"69abb1e2d818334ae82fc9bb8616a824d393ed274849bfd652d0261dad14c6d2","extension":"ts","language":"typescript","name":"dev-login.test.ts","path":"__tests__/dev-login.test.ts","role":"testing"} +{"id":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","labels":["File"],"content_hash":"347db1bfa5e2c1c2b1ba3572f7a573187c562b60361c464c0f4bfe5ac0d7fd26","extension":"tsx","language":"typescript","name":"login-form-dev-bypass.test.tsx","path":"__tests__/login-form-dev-bypass.test.tsx","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","labels":["File"],"content_hash":"62ec42748690e0447f2eac5ff7aa55fe7750bd47baeac54368d73e407a9c8e33","extension":"tsx","language":"typescript","name":"login-form-responsive.test.tsx","path":"__tests__/login-form-responsive.test.tsx","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx","labels":["File"],"content_hash":"9d65ce91df6266f361382c669bcf2b1bfc48417e3514378eb0943039bbb3333e","extension":"tsx","language":"typescript","name":"login-form.test.tsx","path":"__tests__/login-form.test.tsx","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/reduced-motion.test.tsx","labels":["File"],"content_hash":"653ec5f4c87e1d735dec03349aefc5eef643111b1065f554a79c823d08171df9","extension":"tsx","language":"typescript","name":"reduced-motion.test.tsx","path":"__tests__/reduced-motion.test.tsx","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/register-form.test.tsx","labels":["File"],"content_hash":"b599b8889ceb61f9feb89fb8e22278c523abc337bce08cc8ac39e576409ad15e","extension":"tsx","language":"typescript","name":"register-form.test.tsx","path":"__tests__/register-form.test.tsx","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/unauthorized.test.tsx","labels":["File"],"content_hash":"8b9e4c343174d8ba8708219612a6076c9c2882972febf7f0f5b3a49bb4d8efff","extension":"tsx","language":"typescript","name":"unauthorized.test.tsx","path":"__tests__/unauthorized.test.tsx","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:package-lock.json","labels":["File"],"content_hash":"6daa3483d36c2c142f21aba9cec0b709d5430e96593d24ee8974f20ec89f40ab","extension":"json","language":"json","name":"package-lock.json","path":"package-lock.json","role":"config"} -{"id":"rs1:7dc1dbe8d512:file:package.json","labels":["File"],"content_hash":"34e7c7afe0149f92ac052cab28a8b37f5c45b37305a72c5cd0196de05c7220d0","extension":"json","language":"json","name":"package.json","path":"package.json","role":"config"} +{"id":"rs1:7dc1dbe8d512:file:package-lock.json","labels":["File"],"content_hash":"083986b1a781bd245e2ce0aa9c5a3147d9ebf9f511dbc637c50d4fc2f3917cca","extension":"json","language":"json","name":"package-lock.json","path":"package-lock.json","role":"config"} +{"id":"rs1:7dc1dbe8d512:file:package.json","labels":["File"],"content_hash":"6642bc6bba56baefa8ed2095e8124b5d4f231d9058cc16ba02fb06c1c46d2501","extension":"json","language":"json","name":"package.json","path":"package.json","role":"config"} {"id":"rs1:7dc1dbe8d512:file:src/auth-api.ts","labels":["File"],"content_hash":"8283d185b74e761d0fae0dac016cb8af9dd6c57b05e1f92fc69a7a7b3397eebd","extension":"ts","language":"typescript","name":"auth-api.ts","path":"src/auth-api.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/auth-client.ts","labels":["File"],"content_hash":"77d879b0baa25a46ad0e1e1383facda3e08b7f6aec571e873f48de895e0c0dc9","extension":"ts","language":"typescript","name":"auth-client.ts","path":"src/auth-client.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/auth-guard.ts","labels":["File"],"content_hash":"89d4edf77fbb31cf613d80f98adc92d8d4ad36b3e27b987e296b087fa844c9c6","extension":"ts","language":"typescript","name":"auth-guard.ts","path":"src/auth-guard.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/auth-store.ts","labels":["File"],"content_hash":"6a323b83e14f6e52e6822fc67942364148c146bd52b198a928814f590e774fcf","extension":"ts","language":"typescript","name":"auth-store.ts","path":"src/auth-store.ts","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:src/auth-store.ts","labels":["File"],"content_hash":"53e77292bc67b84d68c9b100c77e7676fec0702503d04b88a97aa2fa7dd68162","extension":"ts","language":"typescript","name":"auth-store.ts","path":"src/auth-store.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/extract-token.ts","labels":["File"],"content_hash":"e04f6297414658e88b723749629b451963d5741a8fbfc5fc08a9eaeff8135d3a","extension":"ts","language":"typescript","name":"extract-token.ts","path":"src/extract-token.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/index.ts","labels":["File"],"content_hash":"3e701f8c20aea5c16d1839091ce14360a055f10feb2aa8c437d0d34c7d7fd13d","extension":"ts","language":"typescript","name":"index.ts","path":"src/index.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/index.ts","labels":["File"],"content_hash":"aab9c87e5f44f0ea2f661f02f28df1a7d205dc04bcb61fb9ee532f8fa625dfc5","extension":"ts","language":"typescript","name":"index.ts","path":"src/react/index.ts","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:src/import-meta-env.d.ts","labels":["File"],"content_hash":"4fa5f8bbaea58d65d147a9d1d4a196327786af1a4cbeb6ce3a214b2b8adbb11d","extension":"ts","language":"typescript","name":"import-meta-env.d.ts","path":"src/import-meta-env.d.ts","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:src/index.ts","labels":["File"],"content_hash":"e53a186066933f327df1d3e08cd4d51acbe4ef899bc5ff6a771952688c44a480","extension":"ts","language":"typescript","name":"index.ts","path":"src/index.ts","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","labels":["File"],"content_hash":"4533035dd9bc608fe9bf7ab93398d82d1b317a393417594538e7f673792c215d","extension":"tsx","language":"typescript","name":"dev-sign-in.tsx","path":"src/react/dev-sign-in.tsx","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:src/react/index.ts","labels":["File"],"content_hash":"84d0a8ca28e67f430e32d3b8fe75e6f43d961bdfa0aef7d03a55c72b3641f1a4","extension":"ts","language":"typescript","name":"index.ts","path":"src/react/index.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","labels":["File"],"content_hash":"8caa2423faf18f14cd614bef274fa79e46e3f1b2f4235728f551b2d2b0494a98","extension":"tsx","language":"typescript","name":"input-otp-slots.tsx","path":"src/react/input-otp-slots.tsx","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","labels":["File"],"content_hash":"b4a0a2756a210eb841204427b3c3a73f5038e1070edb746f80006885af946fc3","extension":"tsx","language":"typescript","name":"login-form.tsx","path":"src/react/login-form.tsx","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","labels":["File"],"content_hash":"122497278f3f2a4aa1a32eb9c3d554f0955a354b8f3f20ad93e07d1338cf752c","extension":"tsx","language":"typescript","name":"login-form.tsx","path":"src/react/login-form.tsx","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","labels":["File"],"content_hash":"6787e344349e2ee9e9f739b826421e1cb17e11e6b3fa6d518912b9248310f47a","extension":"tsx","language":"typescript","name":"register-form.tsx","path":"src/react/register-form.tsx","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","labels":["File"],"content_hash":"8317e602a3ebb638d3abac1313a1898a8e1f7673e8fad5ec09dd6bb5bd450291","extension":"tsx","language":"typescript","name":"squircle-shift.tsx","path":"src/react/squircle-shift.tsx","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/react/unauthorized.tsx","labels":["File"],"content_hash":"9c476bd4ef3bdd0c7b0a8e6763f8bba136c9f0fa8b7ace9463f06bfd67aafbef","extension":"tsx","language":"typescript","name":"unauthorized.tsx","path":"src/react/unauthorized.tsx","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/types.ts","labels":["File"],"content_hash":"1c18de7a298edae8ee933f2ec519edba95f556088b1a8762381333bc03610a50","extension":"ts","language":"typescript","name":"types.ts","path":"src/types.ts","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:src/types.ts","labels":["File"],"content_hash":"8ed9764623f369ee8839c9f8d6f21b4296c182795755b8b1b40bb08839894646","extension":"ts","language":"typescript","name":"types.ts","path":"src/types.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:tsconfig.json","labels":["File"],"content_hash":"b74e706331680ec7b061a2c9935af8ea7e0375542f17656f4949143aed03597c","extension":"json","language":"json","name":"tsconfig.json","path":"tsconfig.json","role":"config"} {"id":"rs1:7dc1dbe8d512:file:vitest.config.ts","labels":["File"],"content_hash":"919d683032c69311ef96ec0816a3f74ace196ebb2c0cca768511729b1841c67c","extension":"ts","language":"typescript","name":"vitest.config.ts","path":"vitest.config.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:func:__tests__/auth-api.test.ts#mockClient@0","labels":["Function"],"content_hash":"03a6959d9fd58d56b92c53255cc07458cba2abec1e5d0aea8b3e39e264d07448","end_line":10,"file_path":"__tests__/auth-api.test.ts","name":"mockClient","qualified_name":"mockClient","signature":"function mockClient(): AuthClient {","start_line":6} @@ -42,6 +46,11 @@ {"id":"rs1:7dc1dbe8d512:func:__tests__/auth-store.test.ts#jsonResponse@3","labels":["Function"],"content_hash":"a19b0610471fe1230eacae3ea6e0c8a4498614687127a0e67d605f67307fe0da","end_line":38,"file_path":"__tests__/auth-store.test.ts","name":"jsonResponse","qualified_name":"jsonResponse","signature":"function jsonResponse(body: unknown, ok = true, status = 200) {","start_line":32} {"id":"rs1:7dc1dbe8d512:func:__tests__/auth-store.test.ts#routedFetch@1","labels":["Function"],"content_hash":"8b101389b55728d3eed76d73c5f47f8aea80f5896a939d5b3455f4a70d7cac32","end_line":59,"file_path":"__tests__/auth-store.test.ts","name":"routedFetch","qualified_name":"routedFetch","signature":"function routedFetch(opts: {","start_line":44} {"id":"rs1:7dc1dbe8d512:func:__tests__/class-tokens.ts#classTokensAt@2","labels":["Function"],"content_hash":"792f2f9d677cb34c1eccef5080efd009b3606654c1bc3fb4218cc9aec5a4f0c2","end_line":10,"file_path":"__tests__/class-tokens.ts","name":"classTokensAt","qualified_name":"classTokensAt","signature":"function classTokensAt(","start_line":1} +{"id":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#flush@0","labels":["Function"],"content_hash":"b61870178230350a5ce5908d28d1d04e17a8e5ee21c5a8ae2f9d2a16e88ad676","end_line":70,"file_path":"__tests__/dev-login.test.ts","name":"flush","qualified_name":"flush","signature":"async function flush(): Promise {","start_line":65} +{"id":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#jsonResponse@3","labels":["Function"],"content_hash":"02d091bdbb7f54154347eec1dfc500c2a5891196cac87b3007bcfaa39a466877","end_line":30,"file_path":"__tests__/dev-login.test.ts","name":"jsonResponse","qualified_name":"jsonResponse","signature":"function jsonResponse(body: unknown, ok = true, status = 200) {","start_line":28} +{"id":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#routedFetch@1","labels":["Function"],"content_hash":"4ad93bb58e06908b0873a1ada62b9a9c0909e009710ae9ce5888b4e96e8b10cb","end_line":62,"file_path":"__tests__/dev-login.test.ts","name":"routedFetch","qualified_name":"routedFetch","signature":"function routedFetch(opts: {","start_line":36} +{"id":"rs1:7dc1dbe8d512:func:__tests__/login-form-dev-bypass.test.tsx#buildProps@1","labels":["Function"],"content_hash":"7a287538606744aacf003ec4bd0264ca502a6280fdbb122df7c2781b1acfcc35","end_line":66,"file_path":"__tests__/login-form-dev-bypass.test.tsx","name":"buildProps","qualified_name":"buildProps","signature":"function buildProps(overrides: Record = {}) {","start_line":28} +{"id":"rs1:7dc1dbe8d512:func:__tests__/login-form-dev-bypass.test.tsx#renderLogin@1","labels":["Function"],"content_hash":"910f55b9a12185e47d43cebec525b5b72eceed7109de00da1ba18500b32f7a3e","end_line":71,"file_path":"__tests__/login-form-dev-bypass.test.tsx","name":"renderLogin","qualified_name":"renderLogin","signature":"function renderLogin(props: ReturnType) {","start_line":68} {"id":"rs1:7dc1dbe8d512:func:__tests__/login-form.test.tsx#noop@0","labels":["Function"],"content_hash":"5663608f35c5064bc27d9aff074d4a458cc30c8e6fda27521f691a621743c6be","end_line":16,"file_path":"__tests__/login-form.test.tsx","name":"noop","qualified_name":"noop","signature":"() => {}","start_line":16} {"id":"rs1:7dc1dbe8d512:func:__tests__/login-form.test.tsx#renderEmailStep@1","labels":["Function"],"content_hash":"ede73403c426aa609369412f00e1b6dfafbade6db4f51b86c1513c69df0e509e","end_line":48,"file_path":"__tests__/login-form.test.tsx","name":"renderEmailStep","qualified_name":"renderEmailStep","signature":"function renderEmailStep(extraProps: Record = {}) {","start_line":37} {"id":"rs1:7dc1dbe8d512:func:__tests__/reduced-motion.test.tsx#createMatchMediaController@1","labels":["Function"],"content_hash":"28a1d131fb3c016c21951f9f0fd3c5e518cfca94662c15dd5b0d09d2176c01de","end_line":95,"file_path":"__tests__/reduced-motion.test.tsx","name":"createMatchMediaController","qualified_name":"createMatchMediaController","signature":"function createMatchMediaController(initialMatches: boolean): MatchMediaController {","start_line":58} @@ -52,30 +61,34 @@ {"id":"rs1:7dc1dbe8d512:func:src/auth-client.ts#createAuthClient@1","labels":["Function"],"content_hash":"67c7657e2bd9c4cbd979de39b5479620ccadb1ebc22194001511da0d94275dbe","end_line":87,"file_path":"src/auth-client.ts","name":"createAuthClient","qualified_name":"createAuthClient","signature":"function createAuthClient(config: AuthClientConfig): AuthClient {","start_line":4} {"id":"rs1:7dc1dbe8d512:func:src/auth-guard.ts#hasEntitlement@2","labels":["Function"],"content_hash":"4e9c7a87605be060a029139c441ddfa231b889a85b3518d5cb784f5a138c5e6e","end_line":20,"file_path":"src/auth-guard.ts","name":"hasEntitlement","qualified_name":"hasEntitlement","signature":"function hasEntitlement(","start_line":15} {"id":"rs1:7dc1dbe8d512:func:src/auth-guard.ts#validateUserType@2","labels":["Function"],"content_hash":"82f592f4a306ab35c778241085f90f37ff078f82826f3bdbeba8b87fb3687b4c","end_line":5,"file_path":"src/auth-guard.ts","name":"validateUserType","qualified_name":"validateUserType","signature":"function validateUserType(user: AuthUser, expected: UserType): boolean {","start_line":3} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.applyToken@1","labels":["Function"],"content_hash":"f9b3c823076506eb4578fddcc8a9e62048ccdb31e79672a4b1ade9d24df08a40","end_line":208,"file_path":"src/auth-store.ts","name":"applyToken","qualified_name":"createAuthStore.applyToken","signature":"function applyToken(token: string): void {","start_line":193} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.clearToken@0","labels":["Function"],"content_hash":"56cf8dcfea4d1cc138f252faa2c8da1b556b0627afb7bb9fd463b710dc61ed13","end_line":224,"file_path":"src/auth-store.ts","name":"clearToken","qualified_name":"createAuthStore.clearToken","signature":"function clearToken(): void {","start_line":217} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.commitIdentity@3","labels":["Function"],"content_hash":"f9d36b09b1496e29f9e6b4418e8411d6d471e97fa06ac06eef885eb73c1a651c","end_line":184,"file_path":"src/auth-store.ts","name":"commitIdentity","qualified_name":"createAuthStore.commitIdentity","signature":"function commitIdentity(","start_line":159} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitAccessTokenSet@1","labels":["Function"],"content_hash":"7ef01910748fda976dfdecaccdab4379c658cfe97bfd9f0dee5b051dea3181a0","end_line":95,"file_path":"src/auth-store.ts","name":"emitAccessTokenSet","qualified_name":"createAuthStore.emitAccessTokenSet","signature":"function emitAccessTokenSet(token: string | null): void {","start_line":87} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitLogout@0","labels":["Function"],"content_hash":"3c8240bd971a8b627c5b26ecd0ef45c537fb6a0016758b2277d42624956e67d8","end_line":105,"file_path":"src/auth-store.ts","name":"emitLogout","qualified_name":"createAuthStore.emitLogout","signature":"function emitLogout(): void {","start_line":97} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitOrgChange@1","labels":["Function"],"content_hash":"116a2dc2c42fe9ba7752dc3502969f620bb8e1d13a95017e984229df92224039","end_line":85,"file_path":"src/auth-store.ts","name":"emitOrgChange","qualified_name":"createAuthStore.emitOrgChange","signature":"function emitOrgChange(orgId: string | null): void {","start_line":76} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.fetchIdentity@1","labels":["Function"],"content_hash":"83d6c28991f1bc8e01fad5b15c0a1c6fe0876600311c361748be53e7949dd511","end_line":140,"file_path":"src/auth-store.ts","name":"fetchIdentity","qualified_name":"createAuthStore.fetchIdentity","signature":"async function fetchIdentity(token: string): Promise {","start_line":124} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.performRefresh@0","labels":["Function"],"content_hash":"1098883236ae94f6f6863168a874211d463cd7b5b81d8bcc3dc6db66bfeb7c69","end_line":250,"file_path":"src/auth-store.ts","name":"performRefresh","qualified_name":"createAuthStore.performRefresh","signature":"async function performRefresh(): Promise {","start_line":226} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.scheduleRefresh@1","labels":["Function"],"content_hash":"7795ef507ea882fe257e5c89025281e803e24cdf19084695cd2c0a2b7209e381","end_line":113,"file_path":"src/auth-store.ts","name":"scheduleRefresh","qualified_name":"createAuthStore.scheduleRefresh","signature":"function scheduleRefresh(expiresInSeconds: number): void {","start_line":107} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","labels":["Function"],"content_hash":"ed907fc0f91dceb678d05807585c9903548391ee37d29de0c84c8d0f19cc3c70","end_line":326,"file_path":"src/auth-store.ts","name":"createAuthStore","qualified_name":"createAuthStore","signature":"function createAuthStore(config: AuthStoreConfig): AuthStore {","start_line":48} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#toIdentity@1","labels":["Function"],"content_hash":"dcade69ae796b4c682344cefe22cd5da81f9aea5af102ef1557b0ba6f4dae123","end_line":46,"file_path":"src/auth-store.ts","name":"toIdentity","qualified_name":"toIdentity","signature":"function toIdentity(user: AuthUser): Identity {","start_line":36} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.applyToken@1","labels":["Function"],"content_hash":"f9b3c823076506eb4578fddcc8a9e62048ccdb31e79672a4b1ade9d24df08a40","end_line":234,"file_path":"src/auth-store.ts","name":"applyToken","qualified_name":"createAuthStore.applyToken","signature":"function applyToken(token: string): void {","start_line":219} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.clearToken@0","labels":["Function"],"content_hash":"56cf8dcfea4d1cc138f252faa2c8da1b556b0627afb7bb9fd463b710dc61ed13","end_line":250,"file_path":"src/auth-store.ts","name":"clearToken","qualified_name":"createAuthStore.clearToken","signature":"function clearToken(): void {","start_line":243} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.commitIdentity@3","labels":["Function"],"content_hash":"f9d36b09b1496e29f9e6b4418e8411d6d471e97fa06ac06eef885eb73c1a651c","end_line":210,"file_path":"src/auth-store.ts","name":"commitIdentity","qualified_name":"createAuthStore.commitIdentity","signature":"function commitIdentity(","start_line":185} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitAccessTokenSet@1","labels":["Function"],"content_hash":"7ef01910748fda976dfdecaccdab4379c658cfe97bfd9f0dee5b051dea3181a0","end_line":121,"file_path":"src/auth-store.ts","name":"emitAccessTokenSet","qualified_name":"createAuthStore.emitAccessTokenSet","signature":"function emitAccessTokenSet(token: string | null): void {","start_line":113} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitLogout@0","labels":["Function"],"content_hash":"3c8240bd971a8b627c5b26ecd0ef45c537fb6a0016758b2277d42624956e67d8","end_line":131,"file_path":"src/auth-store.ts","name":"emitLogout","qualified_name":"createAuthStore.emitLogout","signature":"function emitLogout(): void {","start_line":123} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitOrgChange@1","labels":["Function"],"content_hash":"116a2dc2c42fe9ba7752dc3502969f620bb8e1d13a95017e984229df92224039","end_line":111,"file_path":"src/auth-store.ts","name":"emitOrgChange","qualified_name":"createAuthStore.emitOrgChange","signature":"function emitOrgChange(orgId: string | null): void {","start_line":102} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.fetchIdentity@1","labels":["Function"],"content_hash":"83d6c28991f1bc8e01fad5b15c0a1c6fe0876600311c361748be53e7949dd511","end_line":166,"file_path":"src/auth-store.ts","name":"fetchIdentity","qualified_name":"createAuthStore.fetchIdentity","signature":"async function fetchIdentity(token: string): Promise {","start_line":150} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.performRefresh@0","labels":["Function"],"content_hash":"1098883236ae94f6f6863168a874211d463cd7b5b81d8bcc3dc6db66bfeb7c69","end_line":276,"file_path":"src/auth-store.ts","name":"performRefresh","qualified_name":"createAuthStore.performRefresh","signature":"async function performRefresh(): Promise {","start_line":252} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.scheduleRefresh@1","labels":["Function"],"content_hash":"7795ef507ea882fe257e5c89025281e803e24cdf19084695cd2c0a2b7209e381","end_line":139,"file_path":"src/auth-store.ts","name":"scheduleRefresh","qualified_name":"createAuthStore.scheduleRefresh","signature":"function scheduleRefresh(expiresInSeconds: number): void {","start_line":133} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","labels":["Function"],"content_hash":"d5910737b2aa500912cd3ac95056e2d42cff44acfc4324211e6c9b6519696668","end_line":451,"file_path":"src/auth-store.ts","name":"createAuthStore","qualified_name":"createAuthStore","signature":"function createAuthStore(config: AuthStoreConfig): AuthStore {","start_line":74} +{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#toIdentity@1","labels":["Function"],"content_hash":"dcade69ae796b4c682344cefe22cd5da81f9aea5af102ef1557b0ba6f4dae123","end_line":47,"file_path":"src/auth-store.ts","name":"toIdentity","qualified_name":"toIdentity","signature":"function toIdentity(user: AuthUser): Identity {","start_line":37} {"id":"rs1:7dc1dbe8d512:func:src/extract-token.ts#extractAccessToken@1","labels":["Function"],"content_hash":"6e5e82d686561af6ee3fbfa63e13318e21552ccb0594cfc4b7ddad57c3e27d97","end_line":21,"file_path":"src/extract-token.ts","name":"extractAccessToken","qualified_name":"extractAccessToken","signature":"function extractAccessToken(payload: Record): string | null {","start_line":1} +{"id":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#DevSignInBypass@1","labels":["Function"],"content_hash":"8a6202922b647200971f1c2cc8d2121841fe716b67418a4114ac4cbe09890100","end_line":122,"file_path":"src/react/dev-sign-in.tsx","name":"DevSignInBypass","qualified_name":"DevSignInBypass","signature":"function DevSignInBypass({","start_line":62} +{"id":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#readDevLoginEmail@0","labels":["Function"],"content_hash":"7fd24bffb01d2f426e2334e8d73b9ccbe98fa6eb0cf4fa4a71f564e814e68e4a","end_line":28,"file_path":"src/react/dev-sign-in.tsx","name":"readDevLoginEmail","qualified_name":"readDevLoginEmail","signature":"function readDevLoginEmail(): string | null {","start_line":21} +{"id":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#rememberDevLoginEmail@1","labels":["Function"],"content_hash":"0fa564de72d864c19311d78a6f0a23f9402b8a42dd0e5593f999a5a29676703c","end_line":37,"file_path":"src/react/dev-sign-in.tsx","name":"rememberDevLoginEmail","qualified_name":"rememberDevLoginEmail","signature":"function rememberDevLoginEmail(email: string): void {","start_line":31} {"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTP@1","labels":["Function"],"content_hash":"0e294ca03d9502a4861427bc066da3c24d0482a32d0684fd70ac70bb125b8867","end_line":119,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTP","qualified_name":"InputOTP","signature":"function InputOTP({","start_line":80} {"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPGroup@1","labels":["Function"],"content_hash":"1b5a1cffedb2ec6bf7030682b30429cf8e946bbb5c535003d7208129454e3e00","end_line":134,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTPGroup","qualified_name":"InputOTPGroup","signature":"function InputOTPGroup({","start_line":121} {"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPSeparator@1","labels":["Function"],"content_hash":"fe4ea8aa11e774fd8387e62e881e1101f02b93d787dad90fab219a878ffecc24","end_line":246,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTPSeparator","qualified_name":"InputOTPSeparator","signature":"function InputOTPSeparator({","start_line":213} {"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPSlot@1","labels":["Function"],"content_hash":"a662eb381a3b57fa35edfc9214784240a0d13e161e4cb381c519fa64deffceeb","end_line":211,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTPSlot","qualified_name":"InputOTPSlot","signature":"function InputOTPSlot({","start_line":136} {"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#countOtpSeparators@1","labels":["Function"],"content_hash":"31051096fe80dba352dc5a94d061465937fc76e1f8dd3ddfbdc0f517df6be277","end_line":258,"file_path":"src/react/input-otp-slots.tsx","name":"countOtpSeparators","qualified_name":"countOtpSeparators","signature":"function countOtpSeparators(children: React.ReactNode): number {","start_line":248} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","labels":["Function"],"content_hash":"424b761d84a979d0c88679166df7a8865d15d97cd437fbdf70ba574c61886882","end_line":170,"file_path":"src/react/login-form.tsx","name":"handleAuthError","qualified_name":"LoginForm.handleAuthError","signature":"function handleAuthError(err: unknown, fallbackMessage: string) {","start_line":138} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleEmailSubmit@1","labels":["Function"],"content_hash":"3b375c8d01a368bfb12271a564b665e7036ad42d41daa929d10f428e2d3b1efb","end_line":188,"file_path":"src/react/login-form.tsx","name":"handleEmailSubmit","qualified_name":"LoginForm.handleEmailSubmit","signature":"async function handleEmailSubmit(e: React.FormEvent) {","start_line":172} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleOtpSubmit@1","labels":["Function"],"content_hash":"e58436a58b839c45c006455a79fd160e727fcee417b3e2471f8900b37a08ad19","end_line":216,"file_path":"src/react/login-form.tsx","name":"handleOtpSubmit","qualified_name":"LoginForm.handleOtpSubmit","signature":"async function handleOtpSubmit(e: React.FormEvent) {","start_line":190} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleResend@0","labels":["Function"],"content_hash":"8793829b5b0a2a807dab8e22e706fffd1ec12efa20353ac5df6f86160b4be10d","end_line":233,"file_path":"src/react/login-form.tsx","name":"handleResend","qualified_name":"LoginForm.handleResend","signature":"async function handleResend() {","start_line":218} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","labels":["Function"],"content_hash":"aec528667c75df3acf5611e317ca1283cd730fd669e0c77b4ecce99f9d9f988c","end_line":471,"file_path":"src/react/login-form.tsx","name":"LoginForm","qualified_name":"LoginForm","signature":"function LoginForm({","start_line":82} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#formatCountdown@2","labels":["Function"],"content_hash":"296f135630b3c9fa000bd6aef44f63c4744704ce80c94449e546be1b6e0f254e","end_line":73,"file_path":"src/react/login-form.tsx","name":"formatCountdown","qualified_name":"formatCountdown","signature":"function formatCountdown(target: string | null, fallback = \"10:00\") {","start_line":67} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#otpSentNotice@1","labels":["Function"],"content_hash":"47772925fbd776164e3662e83d665698453aacb62860eeeff13c637cc9eb733f","end_line":65,"file_path":"src/react/login-form.tsx","name":"otpSentNotice","qualified_name":"otpSentNotice","signature":"function otpSentNotice(email: string): string {","start_line":63} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","labels":["Function"],"content_hash":"424b761d84a979d0c88679166df7a8865d15d97cd437fbdf70ba574c61886882","end_line":188,"file_path":"src/react/login-form.tsx","name":"handleAuthError","qualified_name":"LoginForm.handleAuthError","signature":"function handleAuthError(err: unknown, fallbackMessage: string) {","start_line":156} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleDevLogin@0","labels":["Function"],"content_hash":"f03adbb04d23a53d2c182d8fb82d2d59461555c103c39860f02e0acfad1efe95","end_line":289,"file_path":"src/react/login-form.tsx","name":"handleDevLogin","qualified_name":"LoginForm.handleDevLogin","signature":"async function handleDevLogin() {","start_line":243} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleEmailSubmit@1","labels":["Function"],"content_hash":"3b375c8d01a368bfb12271a564b665e7036ad42d41daa929d10f428e2d3b1efb","end_line":206,"file_path":"src/react/login-form.tsx","name":"handleEmailSubmit","qualified_name":"LoginForm.handleEmailSubmit","signature":"async function handleEmailSubmit(e: React.FormEvent) {","start_line":190} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleOtpSubmit@1","labels":["Function"],"content_hash":"e58436a58b839c45c006455a79fd160e727fcee417b3e2471f8900b37a08ad19","end_line":234,"file_path":"src/react/login-form.tsx","name":"handleOtpSubmit","qualified_name":"LoginForm.handleOtpSubmit","signature":"async function handleOtpSubmit(e: React.FormEvent) {","start_line":208} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleResend@0","labels":["Function"],"content_hash":"3acb77db12e60743ac0c21800ebe9b18295b8bd4354dde69dcb2f60c221b6a12","end_line":305,"file_path":"src/react/login-form.tsx","name":"handleResend","qualified_name":"LoginForm.handleResend","signature":"async function handleResend() { setError(\"\");","start_line":291} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","labels":["Function"],"content_hash":"86e38c2a129dffa1f6210874e78ca7e4c78fc53b0b36a1b599fd39dd6f59b83a","end_line":558,"file_path":"src/react/login-form.tsx","name":"LoginForm","qualified_name":"LoginForm","signature":"function LoginForm({","start_line":87} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#formatCountdown@2","labels":["Function"],"content_hash":"296f135630b3c9fa000bd6aef44f63c4744704ce80c94449e546be1b6e0f254e","end_line":78,"file_path":"src/react/login-form.tsx","name":"formatCountdown","qualified_name":"formatCountdown","signature":"function formatCountdown(target: string | null, fallback = \"10:00\") {","start_line":72} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#otpSentNotice@1","labels":["Function"],"content_hash":"47772925fbd776164e3662e83d665698453aacb62860eeeff13c637cc9eb733f","end_line":70,"file_path":"src/react/login-form.tsx","name":"otpSentNotice","qualified_name":"otpSentNotice","signature":"function otpSentNotice(email: string): string {","start_line":68} {"id":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterCard@1","labels":["Function"],"content_hash":"ed4aa9d087ff807ca6c93693a406597484999091857fea8500b2d2362b4d06f9","end_line":22,"file_path":"src/react/register-form.tsx","name":"RegisterCard","qualified_name":"RegisterCard","signature":"function RegisterCard({ children }: { readonly children: ReactNode }) {","start_line":16} {"id":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterForm.handleSubmit@1","labels":["Function"],"content_hash":"d31124199e93b952600473854f3aff3d81d101ed14dc4bbde4ec41d554fd4ad7","end_line":72,"file_path":"src/react/register-form.tsx","name":"handleSubmit","qualified_name":"RegisterForm.handleSubmit","signature":"async function handleSubmit(e: React.FormEvent) {","start_line":37} {"id":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterForm@1","labels":["Function"],"content_hash":"5120756d96f23ee18698965dcebe6a57fd6d3afe89468948edd1db1a85ed923e","end_line":180,"file_path":"src/react/register-form.tsx","name":"RegisterForm","qualified_name":"RegisterForm","signature":"function RegisterForm({","start_line":24} @@ -85,27 +98,34 @@ {"id":"rs1:7dc1dbe8d512:func:src/react/unauthorized.tsx#UnauthorizedPage@1","labels":["Function"],"content_hash":"2af9fbea25483885f6b2a9e074f9300a1db8574193a586a20a39527ab0364ebc","end_line":131,"file_path":"src/react/unauthorized.tsx","name":"UnauthorizedPage","qualified_name":"UnauthorizedPage","signature":"function UnauthorizedPage({","start_line":48} {"id":"rs1:7dc1dbe8d512:func:src/types.ts#AuthError.constructor@4","labels":["Function"],"content_hash":"72320bc3bb2a97b66f0286f95edd7206c97af02f3be6043e6d79e2cb6a1414a5","end_line":52,"file_path":"src/types.ts","name":"constructor","qualified_name":"AuthError.constructor","signature":"constructor(","start_line":44} {"id":"rs1:7dc1dbe8d512:iface:__tests__/reduced-motion.test.tsx#MatchMediaController","labels":["Interface"],"content_hash":"c7b196a531ffc6a50ba261d950bb80032a71440465d83e2f26bd0ddbb3efe656","end_line":56,"file_path":"__tests__/reduced-motion.test.tsx","name":"MatchMediaController","qualified_name":"MatchMediaController","start_line":51} -{"id":"rs1:7dc1dbe8d512:iface:src/auth-store.ts#Identity","labels":["Interface"],"content_hash":"ba4e73dcaad1e250a22c05729aeba21b77a770808d46081d2ead65d860b77535","end_line":27,"file_path":"src/auth-store.ts","name":"Identity","qualified_name":"Identity","start_line":22} -{"id":"rs1:7dc1dbe8d512:iface:src/react/login-form.tsx#LoginFormProps","labels":["Interface"],"content_hash":"a00986970dfb549055e671439d6f4e5897e1684a08c8720d34491ce6587d38da","end_line":53,"file_path":"src/react/login-form.tsx","name":"LoginFormProps","qualified_name":"LoginFormProps","start_line":27} +{"id":"rs1:7dc1dbe8d512:iface:src/auth-store.ts#Identity","labels":["Interface"],"content_hash":"ba4e73dcaad1e250a22c05729aeba21b77a770808d46081d2ead65d860b77535","end_line":28,"file_path":"src/auth-store.ts","name":"Identity","qualified_name":"Identity","start_line":23} +{"id":"rs1:7dc1dbe8d512:iface:src/import-meta-env.d.ts#ImportMeta","labels":["Interface"],"content_hash":"16414b05097540ff5bf70d7f317c3057753244db1aff5f12c514d4260795e9d8","end_line":23,"file_path":"src/import-meta-env.d.ts","name":"ImportMeta","qualified_name":"ImportMeta","start_line":21} +{"id":"rs1:7dc1dbe8d512:iface:src/import-meta-env.d.ts#ImportMetaEnv","labels":["Interface"],"content_hash":"7b46b327a3405664f52e8c62032d64e98fb04d11158638bd8d077148fa3ec3cf","end_line":19,"file_path":"src/import-meta-env.d.ts","name":"ImportMetaEnv","qualified_name":"ImportMetaEnv","start_line":16} +{"id":"rs1:7dc1dbe8d512:iface:src/react/dev-sign-in.tsx#DevSignInBypassProps","labels":["Interface"],"content_hash":"6c4d8840b8130b3d3880c782a982700414dbc76015c8358824402e370ca99297","end_line":49,"file_path":"src/react/dev-sign-in.tsx","name":"DevSignInBypassProps","qualified_name":"DevSignInBypassProps","start_line":39} +{"id":"rs1:7dc1dbe8d512:iface:src/react/login-form.tsx#LoginFormProps","labels":["Interface"],"content_hash":"a00986970dfb549055e671439d6f4e5897e1684a08c8720d34491ce6587d38da","end_line":58,"file_path":"src/react/login-form.tsx","name":"LoginFormProps","qualified_name":"LoginFormProps","start_line":32} {"id":"rs1:7dc1dbe8d512:iface:src/react/register-form.tsx#RegisterFormProps","labels":["Interface"],"content_hash":"6d17593cd5f6cc72f71ac93ca267026f690395567d3530d1e6a18567df5ce35f","end_line":14,"file_path":"src/react/register-form.tsx","name":"RegisterFormProps","qualified_name":"RegisterFormProps","start_line":9} {"id":"rs1:7dc1dbe8d512:iface:src/react/squircle-shift.tsx#ShaderPlaneProps","labels":["Interface"],"content_hash":"d980adaed194f62465ed25724c438301bd8c793f9f3a21c3d6152dedc2c00806","end_line":134,"file_path":"src/react/squircle-shift.tsx","name":"ShaderPlaneProps","qualified_name":"ShaderPlaneProps","start_line":117} {"id":"rs1:7dc1dbe8d512:iface:src/react/squircle-shift.tsx#SquircleShiftProps","labels":["Interface"],"content_hash":"b8704fc2b29133da226eb9e348e1e986dda9d06048061df7a4656987f599e7eb","end_line":11,"file_path":"src/react/squircle-shift.tsx","name":"SquircleShiftProps","qualified_name":"SquircleShiftProps","start_line":8} {"id":"rs1:7dc1dbe8d512:iface:src/react/unauthorized.tsx#UnauthorizedPageLabels","labels":["Interface"],"content_hash":"bd8ba8c03ef34e9a6082e886302eca9ed8c0e6e7ba496d336940ca24efd741cd","end_line":37,"file_path":"src/react/unauthorized.tsx","name":"UnauthorizedPageLabels","qualified_name":"UnauthorizedPageLabels","start_line":12} {"id":"rs1:7dc1dbe8d512:iface:src/react/unauthorized.tsx#UnauthorizedPageProps","labels":["Interface"],"content_hash":"a329a3f36d7bc787f0b281c76f1c6796b438fd47b12d27abd0b61d9952fe7743","end_line":46,"file_path":"src/react/unauthorized.tsx","name":"UnauthorizedPageProps","qualified_name":"UnauthorizedPageProps","start_line":39} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthApi","labels":["Interface"],"content_hash":"b2404433e6bee90a57b22215daa746a22300475aa861d5eca3f30b22aef8b78f","end_line":155,"file_path":"src/types.ts","name":"AuthApi","qualified_name":"AuthApi","start_line":149} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthClient","labels":["Interface"],"content_hash":"99c3af7a72994c9a152c3a42fd2d6d1a6c236bd7aa77bd651e0e808c58b94051","end_line":147,"file_path":"src/types.ts","name":"AuthClient","qualified_name":"AuthClient","start_line":145} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthClientConfig","labels":["Interface"],"content_hash":"c411afdd845bf9d0431f8dab207e64de8e4bed55aa3c73b0b62f3ad13adaa95f","end_line":143,"file_path":"src/types.ts","name":"AuthClientConfig","qualified_name":"AuthClientConfig","start_line":139} +{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthApi","labels":["Interface"],"content_hash":"b2404433e6bee90a57b22215daa746a22300475aa861d5eca3f30b22aef8b78f","end_line":216,"file_path":"src/types.ts","name":"AuthApi","qualified_name":"AuthApi","start_line":210} +{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthClient","labels":["Interface"],"content_hash":"99c3af7a72994c9a152c3a42fd2d6d1a6c236bd7aa77bd651e0e808c58b94051","end_line":208,"file_path":"src/types.ts","name":"AuthClient","qualified_name":"AuthClient","start_line":206} +{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthClientConfig","labels":["Interface"],"content_hash":"c411afdd845bf9d0431f8dab207e64de8e4bed55aa3c73b0b62f3ad13adaa95f","end_line":204,"file_path":"src/types.ts","name":"AuthClientConfig","qualified_name":"AuthClientConfig","start_line":200} {"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthErrorResponse","labels":["Interface"],"content_hash":"a7292ce71d472c11204a2256482ff0e77c52c5aea68ec07f237c239dc371382b","end_line":41,"file_path":"src/types.ts","name":"AuthErrorResponse","qualified_name":"AuthErrorResponse","start_line":37} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthStore","labels":["Interface"],"content_hash":"d1fc2016450962927c396329f7c092a0d3c38c26e3ed3bf122d9f7980708193d","end_line":137,"file_path":"src/types.ts","name":"AuthStore","qualified_name":"AuthStore","start_line":100} +{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthStore","labels":["Interface"],"content_hash":"caec88f9918d24d7111014be09f1ff00d0bd069a7b8a7dfb8c9c8736ccc1387b","end_line":198,"file_path":"src/types.ts","name":"AuthStore","qualified_name":"AuthStore","start_line":137} {"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthStoreConfig","labels":["Interface"],"content_hash":"9dc081ea4de2d71a6841a0b30c5339169cc1673aaac40b23b0ecc66f2fe8dda7","end_line":59,"file_path":"src/types.ts","name":"AuthStoreConfig","qualified_name":"AuthStoreConfig","start_line":55} {"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthUser","labels":["Interface"],"content_hash":"b1e49ea966e4cdbf19b08b13dcb8d9cb724b0c3e7477f31ec3fba70988711238","end_line":17,"file_path":"src/types.ts","name":"AuthUser","qualified_name":"AuthUser","start_line":1} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#DashboardLink","labels":["Interface"],"content_hash":"ea65e8560b77698ccefce8eb986f388da19b805443bcd6de25b9487d78939677","end_line":163,"file_path":"src/types.ts","name":"DashboardLink","qualified_name":"DashboardLink","start_line":159} +{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#DashboardLink","labels":["Interface"],"content_hash":"ea65e8560b77698ccefce8eb986f388da19b805443bcd6de25b9487d78939677","end_line":224,"file_path":"src/types.ts","name":"DashboardLink","qualified_name":"DashboardLink","start_line":220} +{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#DevLoginFailure","labels":["Interface"],"content_hash":"8ad9ca29b0befb9c09d863718e52be90b3a7f68b9e624556983e6d7f1b63e28e","end_line":129,"file_path":"src/types.ts","name":"DevLoginFailure","qualified_name":"DevLoginFailure","start_line":122} +{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#DevLoginSuccess","labels":["Interface"],"content_hash":"b832ec43e2512494e56a435550dde0e08288a15cbc6de0a593df4bad5dae1a26","end_line":120,"file_path":"src/types.ts","name":"DevLoginSuccess","qualified_name":"DevLoginSuccess","start_line":114} {"id":"rs1:7dc1dbe8d512:iface:src/types.ts#RegisterInput","labels":["Interface"],"content_hash":"ed243378b006d3afb761d5532766d6cbb6ae050139ec8a3fca1231e22663f1a5","end_line":24,"file_path":"src/types.ts","name":"RegisterInput","qualified_name":"RegisterInput","start_line":19} {"id":"rs1:7dc1dbe8d512:iface:src/types.ts#RequestOtpResponse","labels":["Interface"],"content_hash":"a15ee39ff6cf2e18ace8cc4a0c2c7e8521b65cac3fad3dc7396b9fa0b4843889","end_line":29,"file_path":"src/types.ts","name":"RequestOtpResponse","qualified_name":"RequestOtpResponse","start_line":26} {"id":"rs1:7dc1dbe8d512:iface:src/types.ts#SessionClaims","labels":["Interface"],"content_hash":"598263987b713cff9ce894ac955bacdad8f3531516d2d39f6c8cb2b7631f855a","end_line":94,"file_path":"src/types.ts","name":"SessionClaims","qualified_name":"SessionClaims","start_line":80} {"id":"rs1:7dc1dbe8d512:iface:src/types.ts#VerifyOtpResponse","labels":["Interface"],"content_hash":"be10167a9f925b16ff7a13c0047d9fc9349fe790d9f3bdd9006ef365a2da0951","end_line":35,"file_path":"src/types.ts","name":"VerifyOtpResponse","qualified_name":"VerifyOtpResponse","start_line":31} {"id":"rs1:7dc1dbe8d512:repo:.","labels":["Repository"],"is_nested":false,"name":"cellarnode-auth","root_path":"."} {"id":"rs1:7dc1dbe8d512:var:__tests__/auth-store.test.ts#baseMe","labels":["Variable"],"file_path":"__tests__/auth-store.test.ts","kind":"module","name":"baseMe"} +{"id":"rs1:7dc1dbe8d512:var:__tests__/dev-login.test.ts#OK_BODY","labels":["Variable"],"file_path":"__tests__/dev-login.test.ts","kind":"const","name":"OK_BODY"} +{"id":"rs1:7dc1dbe8d512:var:__tests__/dev-login.test.ts#baseMe","labels":["Variable"],"file_path":"__tests__/dev-login.test.ts","kind":"module","name":"baseMe"} {"id":"rs1:7dc1dbe8d512:var:__tests__/login-form-responsive.test.tsx#authApi","labels":["Variable"],"file_path":"__tests__/login-form-responsive.test.tsx","kind":"module","name":"authApi"} {"id":"rs1:7dc1dbe8d512:var:__tests__/login-form-responsive.test.tsx#authStore","labels":["Variable"],"file_path":"__tests__/login-form-responsive.test.tsx","kind":"module","name":"authStore"} {"id":"rs1:7dc1dbe8d512:var:__tests__/login-form.test.tsx#authApi","labels":["Variable"],"file_path":"__tests__/login-form.test.tsx","kind":"module","name":"authApi"} @@ -113,6 +133,9 @@ {"id":"rs1:7dc1dbe8d512:var:__tests__/reduced-motion.test.tsx#matchMediaController","labels":["Variable"],"file_path":"__tests__/reduced-motion.test.tsx","kind":"module","name":"matchMediaController"} {"id":"rs1:7dc1dbe8d512:var:__tests__/register-form.test.tsx#authApi","labels":["Variable"],"file_path":"__tests__/register-form.test.tsx","kind":"module","name":"authApi"} {"id":"rs1:7dc1dbe8d512:var:__tests__/unauthorized.test.tsx#dashboardLinks","labels":["Variable"],"file_path":"__tests__/unauthorized.test.tsx","kind":"module","name":"dashboardLinks"} +{"id":"rs1:7dc1dbe8d512:var:src/auth-store.ts#DEFAULT_ACCESS_TOKEN_TTL","labels":["Variable"],"file_path":"src/auth-store.ts","kind":"const","name":"DEFAULT_ACCESS_TOKEN_TTL"} +{"id":"rs1:7dc1dbe8d512:var:src/auth-store.ts#DEV_LOGIN_MESSAGES","labels":["Variable"],"file_path":"src/auth-store.ts","kind":"const","name":"DEV_LOGIN_MESSAGES"} +{"id":"rs1:7dc1dbe8d512:var:src/react/dev-sign-in.tsx#DEV_LOGIN_EMAIL_STORAGE_KEY","labels":["Variable"],"file_path":"src/react/dev-sign-in.tsx","kind":"const","name":"DEV_LOGIN_EMAIL_STORAGE_KEY"} {"id":"rs1:7dc1dbe8d512:var:src/react/input-otp-slots.tsx#OTP_STYLES","labels":["Variable"],"file_path":"src/react/input-otp-slots.tsx","kind":"const","name":"OTP_STYLES"} {"id":"rs1:7dc1dbe8d512:var:src/react/login-form.tsx#ERROR_MESSAGES","labels":["Variable"],"file_path":"src/react/login-form.tsx","kind":"const","name":"ERROR_MESSAGES"} {"id":"rs1:7dc1dbe8d512:var:src/react/login-form.tsx#LazySquircleShift","labels":["Variable"],"file_path":"src/react/login-form.tsx","kind":"module","name":"LazySquircleShift"} diff --git a/AGENTS.md b/AGENTS.md index 167e167..4d3fa5b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Always `make build` before opening a PR or publishing. ### Core API -- `createAuthStore({ baseUrl })` — token persistence (localStorage in browser; mobile uses an `expo-secure-store` adapter on the consumer side). +- `createAuthStore({ baseUrl })` — token persistence (localStorage in browser; mobile uses an `expo-secure-store` adapter on the consumer side). Also exposes `devLogin(email)` (CEL-1364) — see "Dev sign-in bypass". - `createAuthClient({ baseUrl, store, onAuthFailure })` — fetch wrapper, auto-attaches Bearer, calls `onAuthFailure` on 401. - `createAuthApi({ client, store })` — typed login/register/logout helpers. - `validateUserType(userType)` — `"producer" | "importer" | "distributor" | "admin"`. @@ -52,9 +52,34 @@ src/ ├── auth-store.ts # Token storage abstraction ├── extract-token.ts # JWT extraction helpers ├── types.ts # AuthStore, AuthClient, UserType, ... -└── react/ # LoginForm, RegisterForm, UnauthorizedPage, SquircleShift +└── react/ # LoginForm, RegisterForm, UnauthorizedPage, SquircleShift, DevSignInBypass ``` +## Dev sign-in bypass (CEL-1364) + +`LoginForm` renders a "Dev sign-in (skip the code)" control **alongside** the +email form, and `AuthStore.devLogin(email)` backs it by POSTing the backend's +`POST /test/login` and adopting the returned JWE through `setAccessToken()` — +the same adoption path `verifyOtp` uses (same identity fetch, same refresh +scheduling, same listener fan-out). + +Rules that must not drift: + +- **Additive only.** The OTP flow is untouched, always rendered, never + auto-skipped, never auto-redirected. The OTP page stays testable. +- **No new env vars.** The frontend gate is the literal `import.meta.env.DEV` + (Vite folds it to `false` in production, so Rollup drops the branch and + `src/react/dev-sign-in.tsx` with it). The backend gate stays + `ENABLE_TEST_ENDPOINTS`. Do not add a `VITE_*` flag. +- **Anti-enumeration (backend T3-1).** `/test/login` returns the SAME 404 for + "gate off" and "no such account". `devLogin()` maps it to the single reason + `"test-endpoints-disabled"` and frames the copy as "set + `ENABLE_TEST_ENDPOINTS=true`" — never as a claim about the address. +- `devLogin` is **optional** on the `AuthStore` interface so custom store + implementations stay source-compatible; `createAuthStore()` always provides it. +- Optional prefill: `localStorage["cellarnode.dev.login-email"]`, read and + written only behind `import.meta.env.DEV`. + ## Tailwind v4 content scan (consumer step) When using `@cellarnode/auth/react`, consumers must register the lib's compiled JS so Tailwind picks up the utility classes inside the React components: @@ -75,5 +100,6 @@ OTP flow against backend V2 public API (port 4000): | POST | `/auth/refresh` | Rotate access token (replay-detection revokes session) | | POST | `/auth/logout` | Revoke session in Redis (`cellarnode:session:*`) | | GET | `/auth/me` | Current user; backend `authGuard()` accepts EITHER Bearer JWE (OTP path) OR cookie (admin BFF path). Cookie wins. | +| POST | `/test/login` | LOCAL DEV ONLY (CEL-1364). Body `{ email }` → `{ accessToken, userId, orgId }` + the OTP flow's refresh cookies. 404s uniformly unless the API runs with `ENABLE_TEST_ENDPOINTS=true` outside production. | Session TTL defaults: access 15min, refresh 7d. See `cellarnode-backend-v2/AGENTS.md` for the full server-side schema. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a82751..ef52a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.14.0 + +### Added +- `AuthStore.devLogin(email)` (CEL-1364) — LOCAL-DEV helper that mints a session from the backend's `POST /test/login` and adopts the JWE through the same path `verifyOtp` uses (identity fetch, refresh scheduling, `onAccessTokenSet` / `onOrgChange` fan-out). Returns a `DevLoginResult` instead of throwing; the backend's uniform 404 maps to `reason: "test-endpoints-disabled"` with a "set `ENABLE_TEST_ENDPOINTS=true`" hint, never a claim about the address. Optional on the interface, so custom `AuthStore` implementations stay source-compatible. +- `LoginForm` renders a DEV-only "Dev sign-in (skip the code)" control **alongside** the email form — additive, never a replacement, no auto-redirect. Gated on the literal `import.meta.env.DEV`, so production builds tree-shake the control and its module. No new env vars. +- `DevSignInBypass`, `readDevLoginEmail`, `rememberDevLoginEmail`, `DEV_LOGIN_EMAIL_STORAGE_KEY` from `@cellarnode/auth/react`; `DevLoginResult` / `DevLoginSuccess` / `DevLoginFailure` / `DevLoginFailureReason` types from `@cellarnode/auth`. + ## 0.13.3 ### Fixed diff --git a/README.md b/README.md index e7d289b..f3dc258 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,30 @@ const authApi = createAuthApi({ client: authClient, store: authStore }); import { LoginForm, RegisterForm, UnauthorizedPage } from "@cellarnode/auth/react"; ``` -### Tailwind CSS Content Scan +### Dev sign-in bypass (local development only) + +`LoginForm` renders an extra "Dev sign-in (skip the code)" control **beside** the +email form when `import.meta.env.DEV` is true. It calls +`authStore.devLogin(email)`, which POSTs the backend's `/test/login` and adopts +the returned JWE through the same path `verifyOtp` uses. The OTP flow is +unchanged and remains the only path in production builds — Vite folds +`import.meta.env.DEV` to `false`, so the control and its module are dropped from +the bundle. There is no env var to set on the frontend. + +The backend side is gated by `ENABLE_TEST_ENDPOINTS=true` (non-production only). +When it is off, `/test/login` returns a uniform 404 and `devLogin()` resolves to +`{ ok: false, reason: "test-endpoints-disabled" }`. That 404 is deliberately +identical to the "no local account for this address" case, so neither the helper +nor the UI may present it as a statement about the account. +```ts +const result = await authStore.devLogin?.("producer@example.com"); +if (result?.ok) { + // session adopted: token set, refresh scheduled, listeners fired +} +``` + +### Tailwind CSS Content Scan Add this to your CSS file so Tailwind picks up utility classes from the package: ```css diff --git a/__tests__/dev-login.test.ts b/__tests__/dev-login.test.ts new file mode 100644 index 0000000..eb8e1f7 --- /dev/null +++ b/__tests__/dev-login.test.ts @@ -0,0 +1,240 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { createAuthStore } from "../src/auth-store.js"; +import type { AuthUser } from "../src/types.js"; + +/** + * CEL-1364 — `devLogin()` is the local-dev bypass of the OTP round-trip. It + * POSTs the backend's `/test/login` and must adopt the returned JWE through the + * SAME store path verify-otp uses: identity fetch, refresh scheduling, and the + * `onAccessTokenSet` / `onOrgChange` fan-out all behave identically. + * + * The other half of the contract is anti-enumeration (backend T3-1): the 404 + * body is identical for "gate off" and "no such user", so the client maps it to + * ONE reason and frames it as "the gate is off" — never as a statement about + * the account. + */ + +const baseMe: AuthUser = { + id: "user_dev", + email: "dev@example.com", + name: "Dev", + userType: "producer", + orgId: "org_dev", + roles: ["member"], + entitlements: ["elabel"], + createdAt: "2024-01-01T00:00:00.000Z", +}; + +function jsonResponse(body: unknown, ok = true, status = 200) { + return { ok, status, json: () => Promise.resolve(body) }; +} + +/** + * Route a mock fetch by URL: `/test/login` returns `devLogin`, `/auth/me` + * returns identity. Recorded calls are asserted on directly. + */ +function routedFetch(opts: { + devLogin: { body: unknown; ok?: boolean; status?: number } | "throws"; + me?: AuthUser | null; +}) { + return vi.fn((url: string, init?: RequestInit) => { + if (typeof url === "string" && url.includes("/test/login")) { + if (opts.devLogin === "throws") { + return Promise.reject(new TypeError("Failed to fetch")); + } + void init; + return Promise.resolve( + jsonResponse( + opts.devLogin.body, + opts.devLogin.ok ?? true, + opts.devLogin.status ?? 200, + ), + ); + } + if (typeof url === "string" && url.includes("/auth/me")) { + if (opts.me == null) { + return Promise.resolve(jsonResponse({ error: "unauthorized" }, false, 401)); + } + return Promise.resolve(jsonResponse(opts.me)); + } + return Promise.resolve(jsonResponse({}, false, 404)); + }); +} + +/** Flush pending microtasks so async identity resolution settles. */ +async function flush(): Promise { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); +} + +const OK_BODY = { accessToken: "jwe.dev.token", userId: "user_dev", orgId: "org_dev" }; + +describe("createAuthStore — devLogin adoption (CEL-1364)", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.useRealTimers(); + }); + + it("POSTs the email to /test/login with credentials so the refresh cookie is stored", async () => { + // Given: a store pointed at a local backend. + const fetchMock = routedFetch({ devLogin: { body: OK_BODY }, me: baseMe }); + global.fetch = fetchMock as unknown as typeof fetch; + const store = createAuthStore({ baseUrl: "http://localhost:4000" }); + + // When: a developer triggers the bypass. + await store.devLogin?.("dev@example.com"); + + // Then: the request carries the address and includes cookies — the backend + // sets the same refresh cookies the OTP flow sets, and `/auth/refresh` + // afterwards depends on them being stored. + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe("http://localhost:4000/test/login"); + expect(init.method).toBe("POST"); + expect(init.credentials).toBe("include"); + expect(JSON.parse(init.body as string)).toEqual({ email: "dev@example.com" }); + }); + + it("adopts the token through the same path verify-otp uses", async () => { + // Given: a backend that mints a session and an identity behind it. + global.fetch = routedFetch({ devLogin: { body: OK_BODY }, me: baseMe }) as unknown as typeof fetch; + const store = createAuthStore({ baseUrl: "http://localhost:4000" }); + const tokenSet = vi.fn(); + const orgChanged = vi.fn(); + store.onAccessTokenSet(tokenSet); + store.onOrgChange(orgChanged); + + // When: the bypass succeeds. + const result = await store.devLogin?.("dev@example.com"); + await flush(); + + // Then: the token is live and identity resolved from /auth/me, exactly as + // after a real verify-otp — not a parallel adoption path. + expect(result).toMatchObject({ ok: true, accessToken: "jwe.dev.token" }); + expect(store.getAccessToken()).toBe("jwe.dev.token"); + expect(store.getUserId()).toBe("user_dev"); + expect(store.getOrgId()).toBe("org_dev"); + expect(store.getUserType()).toBe("producer"); + expect(store.getEntitlements()).toEqual(["elabel"]); + + // And: both listener families fired, in the documented order. + expect(tokenSet).toHaveBeenCalledWith("jwe.dev.token"); + expect(orgChanged).toHaveBeenCalledWith("org_dev"); + }); + + it("schedules the refresh timer from expiresIn, defaulting to 900s", async () => { + // Given: fake timers so the scheduled refresh is observable, and a backend + // whose /test/login body omits expiresIn (the real route's email branch + // returns accessToken/userId/orgId only). + vi.useFakeTimers(); + const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); + global.fetch = routedFetch({ devLogin: { body: OK_BODY }, me: baseMe }) as unknown as typeof fetch; + const store = createAuthStore({ baseUrl: "http://localhost:4000" }); + + // When: the bypass succeeds. + const result = await store.devLogin?.("dev@example.com"); + + // Then: the token carries the shared 900s fallback and a refresh is armed + // for (900 - 60)s — the same scheduling verify-otp gets. + expect(result).toMatchObject({ ok: true, expiresIn: 900 }); + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 840_000); + }); + + it("honours a server-supplied expiresIn", async () => { + // Given: a backend that returns an explicit lifetime. + global.fetch = routedFetch({ + devLogin: { body: { ...OK_BODY, expiresIn: 60 } }, + me: baseMe, + }) as unknown as typeof fetch; + const store = createAuthStore({ baseUrl: "http://localhost:4000" }); + + // When / Then: the store reports the server's value, not the fallback. + await expect(store.devLogin?.("dev@example.com")).resolves.toMatchObject({ + ok: true, + expiresIn: 60, + }); + }); +}); + +describe("createAuthStore — devLogin failure mapping (CEL-1364)", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("maps the uniform 404 to the gate-off hint without claiming anything about the account", async () => { + // Given: the backend's T3-1 response — the SAME body for "gate off" and + // "no such user". + global.fetch = routedFetch({ + devLogin: { body: { error: "Test endpoints disabled", code: "TEST_ENDPOINTS_DISABLED" }, ok: false, status: 404 }, + }) as unknown as typeof fetch; + const store = createAuthStore({ baseUrl: "http://localhost:4000" }); + + // When: the bypass is attempted. + const result = await store.devLogin?.("ghost@example.com"); + + // Then: one actionable reason naming the env gate. + expect(result).toMatchObject({ ok: false, reason: "test-endpoints-disabled", status: 404 }); + + // And: the copy never asserts the account's existence either way — doing so + // would turn a deliberately uniform response into an enumeration oracle. + const message = (result as { message: string }).message; + expect(message).toContain("ENABLE_TEST_ENDPOINTS=true"); + expect(message).not.toMatch(/no account|not found|does not exist|unknown (user|email)/i); + expect(message).not.toContain("ghost@example.com"); + + // And: no session was adopted. + expect(store.getAccessToken()).toBeNull(); + }); + + it("distinguishes rate limiting, fixture-secret rejection, and other statuses", async () => { + for (const [status, reason] of [ + [429, "rate-limited"], + [403, "forbidden"], + [500, "unexpected"], + ] as const) { + // Given: a backend returning each non-404 failure. + global.fetch = routedFetch({ + devLogin: { body: { error: "nope", code: "X" }, ok: false, status }, + }) as unknown as typeof fetch; + const store = createAuthStore({ baseUrl: "http://localhost:4000" }); + + // When / Then: each maps to its own reason, so the UI can say something + // useful instead of blaming the env gate for everything. + await expect(store.devLogin?.("dev@example.com")).resolves.toMatchObject({ + ok: false, + reason, + status, + }); + } + }); + + it("reports a network failure instead of rejecting", async () => { + // Given: no backend listening. + global.fetch = routedFetch({ devLogin: "throws" }) as unknown as typeof fetch; + const store = createAuthStore({ baseUrl: "http://localhost:4000" }); + + // When / Then: the caller gets a result, never a thrown error — the DEV + // button has no other error channel. + await expect(store.devLogin?.("dev@example.com")).resolves.toMatchObject({ + ok: false, + reason: "network", + status: null, + }); + }); + + it("does not adopt a session when the response carries no token", async () => { + // Given: a 200 with an unexpected shape. + global.fetch = routedFetch({ devLogin: { body: { ok: true } }, me: baseMe }) as unknown as typeof fetch; + const store = createAuthStore({ baseUrl: "http://localhost:4000" }); + + // When: the bypass "succeeds" at the HTTP layer. + const result = await store.devLogin?.("dev@example.com"); + await flush(); + + // Then: the store stays logged out rather than half-adopting. + expect(result).toMatchObject({ ok: false, reason: "malformed-response" }); + expect(store.getAccessToken()).toBeNull(); + expect(store.hasAccessToken()).toBe(false); + }); +}); diff --git a/__tests__/login-form-dev-bypass.test.tsx b/__tests__/login-form-dev-bypass.test.tsx new file mode 100644 index 0000000..52c4684 --- /dev/null +++ b/__tests__/login-form-dev-bypass.test.tsx @@ -0,0 +1,261 @@ +// @vitest-environment happy-dom + +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { LoginForm } from "../src/react/login-form.js"; +import { DEV_LOGIN_EMAIL_STORAGE_KEY } from "../src/react/dev-sign-in.js"; +import type { DevLoginResult } from "../src/types.js"; + +/** + * CEL-1364 — the dev sign-in bypass is ADDITIVE. These tests pin the three + * properties that make it safe to ship inside the shared login surface: + * + * 1. It never replaces or pre-empts the OTP flow (the email form is still + * there, and nothing signs in without a click). + * 2. It is absent when `import.meta.env.DEV` is false, which is what Vite + * folds in a production build. + * 3. A uniform-404 backend produces the env-gate hint, never a claim about + * the account. + * + * The SquircleShift panel is React.lazy'd and pulls in @react-three/fiber, so + * it is stubbed out — it is irrelevant to the bypass. + */ + +vi.mock("../src/react/squircle-shift.js", () => ({ + SquircleShift: () =>
, +})); + +function buildProps(overrides: Record = {}) { + return { + userType: "producer" as const, + brandName: "CellarNode", + authApi: { + requestOtp: vi.fn(async () => ({ + expiresAt: new Date(Date.now() + 600_000).toISOString(), + resendAvailableAt: new Date(Date.now() + 60_000).toISOString(), + })), + verifyOtp: vi.fn(), + register: vi.fn(), + logout: vi.fn(), + getMe: vi.fn(async () => ({ + id: "user_dev", + email: "dev@example.com", + name: "Dev", + userType: "producer", + orgId: "org_dev", + roles: [], + createdAt: "2024-01-01T00:00:00.000Z", + })), + }, + authStore: { + setAccessToken: vi.fn(), + clearAccessToken: vi.fn(), + devLogin: vi.fn( + async (): Promise => ({ + ok: true, + accessToken: "jwe.dev.token", + expiresIn: 900, + userId: "user_dev", + orgId: "org_dev", + }), + ), + }, + onLoginSuccess: vi.fn(), + ...overrides, + }; +} + +function renderLogin(props: ReturnType) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return render(); +} + +afterEach(() => { + cleanup(); + vi.unstubAllEnvs(); + vi.clearAllMocks(); + window.localStorage.clear(); +}); + +describe("LoginForm dev bypass — DEV builds (CEL-1364)", () => { + beforeEach(() => { + vi.stubEnv("DEV", true); + }); + + it("renders alongside the OTP email form rather than replacing it", () => { + // Given: a developer opens the shared sign-in surface locally. + renderLogin(buildProps()); + + // Then: the real OTP flow is intact and reachable... + expect(screen.getByLabelText(/email address/i)).toBeTruthy(); + expect(screen.getByRole("button", { name: /continue/i })).toBeTruthy(); + + // ...and the bypass sits beside it as a second, clearly-labelled option. + expect(screen.getByRole("button", { name: /dev sign-in/i })).toBeTruthy(); + expect(screen.getByRole("heading", { name: /development only/i })).toBeTruthy(); + }); + + it("never signs in on its own", () => { + // Given: the surface renders with a remembered address available. + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "dev@example.com"); + const props = buildProps(); + + // When: nothing is clicked. + renderLogin(props); + + // Then: no session is minted and no redirect fires — the bypass is opt-in + // per click, so the OTP page itself stays testable. + expect(props.authStore.devLogin).not.toHaveBeenCalled(); + expect(props.onLoginSuccess).not.toHaveBeenCalled(); + }); + + it("prefills the email from the dev-only localStorage key", async () => { + // Given: a previous dev sign-in remembered an address. + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "remembered@example.com"); + + // When: the surface mounts. + renderLogin(buildProps()); + + // Then: the shared email field is prefilled, so the bypass is one click and + // the OTP flow starts from the same address. + await waitFor(() => { + const input = screen.getByLabelText(/email address/i) as HTMLInputElement; + expect(input.value).toBe("remembered@example.com"); + }); + }); + + it("does not override an explicit initialEmail", async () => { + // Given: the consumer passes an address (e.g. an invite link). + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "remembered@example.com"); + + // When: the surface mounts with initialEmail set. + renderLogin(buildProps({ initialEmail: "invited@example.com" })); + + // Then: the consumer's value wins over the dev convenience. + await waitFor(() => { + const input = screen.getByLabelText(/email address/i) as HTMLInputElement; + expect(input.value).toBe("invited@example.com"); + }); + }); + + it("signs in through devLogin on click and remembers the address", async () => { + // Given: a remembered address so the button is enabled without typing. + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "dev@example.com"); + const props = buildProps(); + renderLogin(props); + + const button = await waitFor(() => { + const el = screen.getByRole("button", { name: /dev sign-in/i }) as HTMLButtonElement; + expect(el.disabled).toBe(false); + return el; + }); + + // When: the developer clicks the bypass. + button.click(); + + // Then: the store helper mints the session and the consumer's success + // callback runs — same terminal behaviour as a verified OTP. + await waitFor(() => { + expect(props.authStore.devLogin).toHaveBeenCalledWith("dev@example.com"); + expect(props.onLoginSuccess).toHaveBeenCalledTimes(1); + }); + expect(window.localStorage.getItem(DEV_LOGIN_EMAIL_STORAGE_KEY)).toBe("dev@example.com"); + }); + + it("surfaces the gate-off hint when the backend 404s, without blaming the address", async () => { + // Given: a backend running without ENABLE_TEST_ENDPOINTS — the same 404 it + // returns for an unknown address. + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "dev@example.com"); + const props = buildProps(); + props.authStore.devLogin = vi.fn(async (): Promise => ({ + ok: false, + reason: "test-endpoints-disabled", + status: 404, + message: + "Dev sign-in unavailable: backend test endpoints are disabled. Set ENABLE_TEST_ENDPOINTS=true on the API and restart it.", + })); + renderLogin(props); + + const button = await waitFor(() => { + const el = screen.getByRole("button", { name: /dev sign-in/i }) as HTMLButtonElement; + expect(el.disabled).toBe(false); + return el; + }); + + // When: the bypass is attempted. + button.click(); + + // Then: the developer is told which env var to set, announced via an alert, + // and no session is claimed. + const alert = await screen.findByRole("alert"); + expect(alert.textContent).toContain("ENABLE_TEST_ENDPOINTS=true"); + expect(alert.textContent).not.toMatch(/no account|does not exist|not found/i); + expect(props.onLoginSuccess).not.toHaveBeenCalled(); + }); + + it("keeps the portal guard the OTP path applies", async () => { + // Given: the address belongs to an importer but this is the producer portal. + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "importer@example.com"); + const props = buildProps(); + props.authApi.getMe = vi.fn(async () => ({ + id: "user_imp", + email: "importer@example.com", + name: "Imp", + userType: "importer", + orgId: "org_imp", + roles: [], + createdAt: "2024-01-01T00:00:00.000Z", + })); + renderLogin(props); + + const button = await waitFor(() => { + const el = screen.getByRole("button", { name: /dev sign-in/i }) as HTMLButtonElement; + expect(el.disabled).toBe(false); + return el; + }); + + // When: the bypass mints that session. + button.click(); + + // Then: the session is discarded and the portal message shown — the bypass + // skips the code, not the access rules. + await waitFor(() => { + expect(props.authStore.clearAccessToken).toHaveBeenCalled(); + }); + expect((await screen.findByRole("alert")).textContent).toMatch(/producer accounts only/i); + expect(props.onLoginSuccess).not.toHaveBeenCalled(); + }); +}); + +describe("LoginForm dev bypass — production builds (CEL-1364)", () => { + beforeEach(() => { + // What Vite substitutes when it builds for production. + vi.stubEnv("DEV", false); + }); + + it("is absent while the OTP flow is untouched", () => { + // Given: a production build of a consumer dashboard. + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "dev@example.com"); + renderLogin(buildProps()); + + // Then: no bypass affordance exists at all... + expect(screen.queryByRole("button", { name: /dev sign-in/i })).toBeNull(); + expect(screen.queryByRole("heading", { name: /development only/i })).toBeNull(); + + // ...and the OTP flow is exactly what it was before this feature. + expect(screen.getByLabelText(/email address/i)).toBeTruthy(); + expect(screen.getByRole("button", { name: /continue/i })).toBeTruthy(); + }); + + it("does not read the dev localStorage key", () => { + // Given: a stale dev key left in the browser. + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "dev@example.com"); + + // When: a production build mounts. + renderLogin(buildProps()); + + // Then: the email field stays empty — no dev-only state leaks into prod UX. + const input = screen.getByLabelText(/email address/i) as HTMLInputElement; + expect(input.value).toBe(""); + }); +}); diff --git a/package-lock.json b/package-lock.json index 1ff3e71..a6105fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cellarnode/auth", - "version": "0.13.3", + "version": "0.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cellarnode/auth", - "version": "0.13.3", + "version": "0.14.0", "license": "MIT", "dependencies": { "clsx": "^2.1.0" diff --git a/package.json b/package.json index 114e9c7..e390646 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cellarnode/auth", - "version": "0.13.3", + "version": "0.14.0", "description": "Shared OTP-based authentication — token store, API client, React UI components for CellarNode dashboards.", "license": "MIT", "type": "module", diff --git a/src/auth-store.ts b/src/auth-store.ts index 6ab3e77..cd477f4 100644 --- a/src/auth-store.ts +++ b/src/auth-store.ts @@ -3,6 +3,7 @@ import type { AuthStore, AuthStoreConfig, AuthUser, + DevLoginResult, LogoutListener, OrgChangeListener, SessionUserType, @@ -45,6 +46,31 @@ function toIdentity(user: AuthUser): Identity { }; } +/** + * Fallback access-token lifetime, in seconds, when the server omits + * `expiresIn`. Mirrors the verify-otp adoption path in `auth-api.ts`. + */ +const DEFAULT_ACCESS_TOKEN_TTL = 900; + +/** + * Developer-facing copy for each `devLogin()` failure (CEL-1364). + * + * The 404 message frames the outcome as "the backend gate is off" and nothing + * else. The backend deliberately returns an identical 404 for "gate off" and + * "no such user" (T3-1), so any copy that named the account would be both a + * guess and a weakening of that contract. + */ +const DEV_LOGIN_MESSAGES = { + "test-endpoints-disabled": + "Dev sign-in unavailable: backend test endpoints are disabled. Set ENABLE_TEST_ENDPOINTS=true on the API and restart it.", + "rate-limited": "Dev sign-in rate limit hit (5/min). Wait a minute and retry.", + forbidden: + "Dev sign-in rejected: the API requires a fixture secret (TEST_FIXTURE_SECRET is set).", + network: "Dev sign-in could not reach the API. Is the backend running?", + "malformed-response": + "Dev sign-in succeeded but the API returned no access token.", +} as const; + export function createAuthStore(config: AuthStoreConfig): AuthStore { const { baseUrl, refreshPath = "/auth/refresh", refreshBuffer = 60 } = config; @@ -283,6 +309,105 @@ export function createAuthStore(config: AuthStoreConfig): AuthStore { return refreshPromise; }, + /** + * LOCAL-DEV ONLY (CEL-1364) — see the `AuthStore.devLogin` doc comment. + * + * Adoption deliberately routes through `store.setAccessToken()` rather than + * touching `applyToken` / `scheduleRefresh` directly, so there is exactly + * ONE token-adoption path shared with verify-otp: same identity fetch, same + * refresh scheduling, same listener fan-out and ordering. + */ + async devLogin(email: string): Promise { + let res: Response; + try { + res = await fetch(`${baseUrl}/test/login`, { + method: "POST", + // The route also sets the BFF session + refresh cookies the OTP flow + // sets; `include` is what lets a subsequent `/auth/refresh` work. + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }); + } catch { + return { + ok: false, + reason: "network", + status: null, + message: DEV_LOGIN_MESSAGES.network, + }; + } + + if (!res.ok) { + if (res.status === 404) { + return { + ok: false, + reason: "test-endpoints-disabled", + status: 404, + message: DEV_LOGIN_MESSAGES["test-endpoints-disabled"], + }; + } + if (res.status === 429) { + return { + ok: false, + reason: "rate-limited", + status: 429, + message: DEV_LOGIN_MESSAGES["rate-limited"], + }; + } + if (res.status === 403) { + return { + ok: false, + reason: "forbidden", + status: 403, + message: DEV_LOGIN_MESSAGES.forbidden, + }; + } + return { + ok: false, + reason: "unexpected", + status: res.status, + message: `Dev sign-in failed (HTTP ${res.status}).`, + }; + } + + let json: Record; + try { + json = (await res.json()) as Record; + } catch { + return { + ok: false, + reason: "malformed-response", + status: res.status, + message: DEV_LOGIN_MESSAGES["malformed-response"], + }; + } + + const token = extractAccessToken(json); + if (!token) { + return { + ok: false, + reason: "malformed-response", + status: res.status, + message: DEV_LOGIN_MESSAGES["malformed-response"], + }; + } + + const expiresIn = + typeof json.expiresIn === "number" + ? json.expiresIn + : DEFAULT_ACCESS_TOKEN_TTL; + + store.setAccessToken(token, expiresIn); + + return { + ok: true, + accessToken: token, + expiresIn, + userId: typeof json.userId === "string" ? json.userId : null, + orgId: typeof json.orgId === "string" ? json.orgId : null, + }; + }, + getUserId() { return identity?.userId ?? null; }, diff --git a/src/import-meta-env.d.ts b/src/import-meta-env.d.ts new file mode 100644 index 0000000..183cb79 --- /dev/null +++ b/src/import-meta-env.d.ts @@ -0,0 +1,23 @@ +/** + * Minimal ambient typing for the bundler-injected `import.meta.env` (CEL-1364). + * + * The package compiles with `tsc` (no bundler) and must not depend on + * `vite/client`, so the two flags the DEV-only sign-in bypass reads are + * declared here. This file is a `.d.ts` INPUT: `tsc` never emits it into + * `dist`, so consumers keep their own `vite/client` declarations without a + * duplicate-interface conflict. + * + * `import.meta.env.DEV` must be written out literally at the use site — Vite + * statically replaces that exact expression with `true` / `false`, which is + * what lets Rollup drop the DEV branch (and every module it references) from + * production bundles. Aliasing it through a helper or optional chaining + * defeats the replacement and ships the bypass to production. + */ +interface ImportMetaEnv { + readonly DEV?: boolean; + readonly PROD?: boolean; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/src/index.ts b/src/index.ts index 8bdc39a..f186379 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,10 @@ export { type RequestOtpResponse, type VerifyOtpResponse, type AuthErrorResponse, + type DevLoginResult, + type DevLoginSuccess, + type DevLoginFailure, + type DevLoginFailureReason, type UserType, type DashboardLink, type SessionClaims, diff --git a/src/react/dev-sign-in.tsx b/src/react/dev-sign-in.tsx new file mode 100644 index 0000000..a6a1410 --- /dev/null +++ b/src/react/dev-sign-in.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { useId } from "react"; +import { FlaskConical, Loader2, Zap } from "lucide-react"; +import { clsx } from "clsx"; + +/** + * localStorage key holding the last address used with the dev bypass + * (CEL-1364). DEV-only convenience: it prefills the sign-in email so a fresh + * tab is one click from a session. Read and written exclusively behind + * `import.meta.env.DEV`, so production builds never touch it. + */ +export const DEV_LOGIN_EMAIL_STORAGE_KEY = "cellarnode.dev.login-email"; + +/** + * Last address used with the dev bypass, or null. + * + * Tolerant by design — storage access throws in Safari private mode and in + * any non-browser host (SSR, tests), and a prefill is never worth a crash. + */ +export function readDevLoginEmail(): string | null { + try { + const value = globalThis.localStorage?.getItem(DEV_LOGIN_EMAIL_STORAGE_KEY); + return value && value.length > 0 ? value : null; + } catch { + return null; + } +} + +/** Remember `email` for the next dev sign-in. Silently no-ops on failure. */ +export function rememberDevLoginEmail(email: string): void { + try { + globalThis.localStorage?.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, email); + } catch { + // Storage unavailable — the prefill is optional. + } +} + +export interface DevSignInBypassProps { + /** Address the bypass will sign in as — the sign-in form's own email field. */ + email: string; + /** True while the bypass request is in flight. */ + isSubmitting: boolean; + /** True while the OTP form is busy, so the two affordances can't race. */ + disabled?: boolean; + /** Developer-facing failure message from `authStore.devLogin()`. */ + error?: string; + onDevSignIn: () => void; +} + +/** + * DEV-only affordance rendered ALONGSIDE the OTP email form (CEL-1364). + * + * It is strictly additive: the OTP flow above it stays fully reachable and + * testable, and nothing here auto-submits or auto-redirects. A developer opts + * in with a click. + * + * The component is only ever referenced from inside an + * `import.meta.env.DEV && …` branch, so production bundles drop both the + * branch and this module (the package sets `sideEffects: false`). + */ +export function DevSignInBypass({ + email, + isSubmitting, + disabled = false, + error, + onDevSignIn, +}: DevSignInBypassProps) { + const headingId = useId(); + const hintId = useId(); + const hasEmail = email.length > 0; + + return ( +
+

+

+ + {/* Static copy — identical on every render, so it carries no signal about + the address. It names both preconditions up front, which is what makes + the uniform 404 tolerable to debug without the client guessing which + one failed. */} +

+ Sign in as the address above without an emailed code. Requires the API + to run with ENABLE_TEST_ENDPOINTS=true{" "} + and a local account for that address. +

+ + + + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/src/react/index.ts b/src/react/index.ts index 9900777..e625dd5 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -1,6 +1,13 @@ "use client"; export { LoginForm, type LoginFormProps } from "./login-form.js"; +export { + DevSignInBypass, + type DevSignInBypassProps, + DEV_LOGIN_EMAIL_STORAGE_KEY, + readDevLoginEmail, + rememberDevLoginEmail, +} from "./dev-sign-in.js"; export { RegisterForm, type RegisterFormProps } from "./register-form.js"; export { UnauthorizedPage, diff --git a/src/react/login-form.tsx b/src/react/login-form.tsx index cd23ff4..e0aa3fb 100644 --- a/src/react/login-form.tsx +++ b/src/react/login-form.tsx @@ -19,6 +19,11 @@ import { InputOTPSeparator, InputOTPSlot, } from "./input-otp-slots.js"; +import { + DevSignInBypass, + readDevLoginEmail, + rememberDevLoginEmail, +} from "./dev-sign-in.js"; const LazySquircleShift = React.lazy(() => import("./squircle-shift.js").then((m) => ({ default: m.SquircleShift })), @@ -106,6 +111,19 @@ export function LoginForm({ const [countdown, setCountdown] = useState("10:00"); const [resendCountdown, setResendCountdown] = useState("01:00"); const [isShaking, setIsShaking] = useState(false); + const [devError, setDevError] = useState(""); + const [isDevSubmitting, setIsDevSubmitting] = useState(false); + + // DEV-only email prefill (CEL-1364). Guarded INSIDE the effect so the hook + // itself stays unconditional; Vite folds `import.meta.env.DEV` to `false` in + // production, and Rollup then drops the body along with its imports. + useEffect(() => { + if (!import.meta.env.DEV) return; + if (initialEmail) return; + const remembered = readDevLoginEmail(); + if (!remembered) return; + setEmail((current) => (current.length > 0 ? current : remembered)); + }, [initialEmail]); useEffect(() => { if (!expiresAt) return; @@ -215,8 +233,62 @@ export function LoginForm({ } } - async function handleResend() { + /** + * DEV-only bypass (CEL-1364): mint a session straight from `/test/login`. + * + * Never auto-runs — it fires only from the dev button's click. On success it + * applies the SAME portal guard the OTP path applies, so a producer address + * still can't land inside the importer portal (and vice versa). + */ + async function handleDevLogin() { + if (!authStore.devLogin) return; setError(""); + setDevError(""); + setIsDevSubmitting(true); + + try { + const result = await authStore.devLogin(normalizedEmail); + + if (!result.ok) { + setDevError(result.message); + onError?.({ + code: `DEV_LOGIN_${result.reason.replace(/-/g, "_").toUpperCase()}`, + message: result.message, + }); + return; + } + + // Portal guard, mirroring handleOtpSubmit. `/auth/me` is advisory here: + // if it fails we let the session stand rather than stranding a developer + // on a transient error. + let authenticatedUserType: string | null = null; + try { + const me = await authApi.getMe(result.accessToken); + authenticatedUserType = me?.userType ?? null; + } catch { + authenticatedUserType = null; + } + + if (authenticatedUserType && authenticatedUserType !== userType) { + const msg = `This portal is for ${userType} accounts only.`; + authStore.clearAccessToken(); + setDevError(msg); + onError?.({ + code: "USER_TYPE_MISMATCH", + message: msg, + authenticatedUserType, + }); + return; + } + + rememberDevLoginEmail(normalizedEmail); + onLoginSuccess(); + } finally { + setIsDevSubmitting(false); + } + } + + async function handleResend() { setError(""); setIsSubmitting(true); try { @@ -437,6 +509,21 @@ export function LoginForm({ )}
+ {/* DEV-only bypass (CEL-1364) — ADDITIVE. It sits beside the email + form on the same step; the OTP flow above is untouched and stays + the only path that exists in production builds. The literal + `import.meta.env.DEV` is what Vite folds to `false`, letting + Rollup drop this branch and the `./dev-sign-in.js` module. */} + {import.meta.env.DEV && step === "email" && authStore.devLogin && ( + + )} + {/* Footer — only on email step */} {step === "email" && (

diff --git a/src/types.ts b/src/types.ts index 53faa90..6902a53 100644 --- a/src/types.ts +++ b/src/types.ts @@ -93,6 +93,43 @@ export interface SessionClaims { entitlements: string[]; } +/** + * Why a `devLogin()` attempt did not produce a session (CEL-1364). + * + * ANTI-ENUMERATION CONTRACT (backend T3-1): `POST /test/login` returns the SAME + * 404 body whether the test-endpoint gate is off OR the address has no local + * account. The client therefore CANNOT tell those cases apart, and must not + * pretend it can — `"test-endpoints-disabled"` is client-side framing of "the + * gate is off", never "this email does not exist". Never derive account + * existence from this reason, and never surface it as such in UI copy. + */ +export type DevLoginFailureReason = + | "test-endpoints-disabled" + | "rate-limited" + | "forbidden" + | "network" + | "malformed-response" + | "unexpected"; + +export interface DevLoginSuccess { + ok: true; + accessToken: string; + expiresIn: number; + userId: string | null; + orgId: string | null; +} + +export interface DevLoginFailure { + ok: false; + reason: DevLoginFailureReason; + /** HTTP status, or null when the request never completed (network error). */ + status: number | null; + /** Ready-to-render, developer-facing message. Safe for DEV-only UI. */ + message: string; +} + +export type DevLoginResult = DevLoginSuccess | DevLoginFailure; + export type OrgChangeListener = (orgId: string | null) => void; export type AccessTokenSetListener = (token: string | null) => void; export type LogoutListener = () => void; @@ -104,6 +141,30 @@ export interface AuthStore { clearAccessToken(): void; ensureAccessToken(forceRefresh?: boolean): Promise; + /** + * LOCAL-DEV ONLY — mint a session for `email` via the backend's + * `POST /test/login`, bypassing the OTP round-trip (CEL-1364). + * + * Adoption is the verify-otp path verbatim: the returned JWE goes through + * `setAccessToken()`, so identity resolution, the refresh timer, and the + * `onAccessTokenSet` / `onOrgChange` listeners all fire exactly as they do + * after a real OTP verification. The backend additionally sets the same + * refresh cookies as the OTP flow, so `/auth/refresh` works afterwards. + * + * Rejects nothing: every outcome is returned as a `DevLoginResult` so the + * DEV-only UI can render an actionable hint. + * + * SAFETY: this is a client of a server-gated route, not a gate of its own. + * The route only exists when the backend runs with `ENABLE_TEST_ENDPOINTS=true` + * outside production; otherwise it 404s and this resolves to + * `{ ok: false, reason: "test-endpoints-disabled" }`. + * + * Optional on the interface so custom `AuthStore` implementations (e.g. a + * mobile secure-store adapter) stay source-compatible. `createAuthStore()` + * always provides it. + */ + devLogin?(email: string): Promise; + /** * userId of the current session, or null. * From a34f34793a7ea7750508b9a6006ba04ec32800da Mon Sep 17 00:00:00 2001 From: mjnong Date: Thu, 13 Aug 2026 07:23:28 +0200 Subject: [PATCH 2/4] refine(CEL-1364): tree-shaking verification test + unreachable-code clarification Add esbuild-based tree-shaking test (dev-bypass-treeshake.test.ts) to verify DEV-only markup is actually dropped from production bundles. Clarify in dev-sign-in.tsx that storage helpers survive as unreachable code behind runtime guards, not as dead branches. Addresses distinction required for production safety verification: proves DevSignInBypass component is eliminated but storage-key literal persists. --- .reposkein/.gitignore | 2 + .reposkein/edges.jsonl | 5 ++ .reposkein/nodes.jsonl | 33 +++++---- AGENTS.md | 14 +++- CHANGELOG.md | 2 +- __tests__/dev-bypass-treeshake.test.ts | 85 ++++++++++++++++++++++++ __tests__/login-form-dev-bypass.test.tsx | 55 +++++++++++++++ package-lock.json | 1 + package.json | 1 + src/react/dev-sign-in.tsx | 7 +- src/react/login-form.tsx | 26 ++++++-- 11 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 __tests__/dev-bypass-treeshake.test.ts diff --git a/.reposkein/.gitignore b/.reposkein/.gitignore index 51b4cfd..d0d18cd 100644 --- a/.reposkein/.gitignore +++ b/.reposkein/.gitignore @@ -1 +1,3 @@ local/ +nodes.jsonl +edges.jsonl diff --git a/.reposkein/edges.jsonl b/.reposkein/edges.jsonl index 2796b9b..70310fa 100644 --- a/.reposkein/edges.jsonl +++ b/.reposkein/edges.jsonl @@ -14,6 +14,7 @@ {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/auth-guard.test.ts"} {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts"} {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts"} +{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts"} {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts"} {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx"} {"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx"} @@ -54,6 +55,10 @@ {"from":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-store.ts","symbols":["createAuthStore"]} {"from":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthUser"]} {"from":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/class-tokens.ts#classTokensAt@2"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-bypass-treeshake.test.ts#bundleLoginForm@1"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#DEV_ONLY_MARKUP"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#EXTERNALS"} +{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#ROOT"} {"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#flush@0"} {"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#jsonResponse@3"} {"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#routedFetch@1"} diff --git a/.reposkein/nodes.jsonl b/.reposkein/nodes.jsonl index 1c69536..e2769e1 100644 --- a/.reposkein/nodes.jsonl +++ b/.reposkein/nodes.jsonl @@ -3,8 +3,8 @@ {"id":"rs1:7dc1dbe8d512:dir:__tests__","labels":["Directory"],"name":"__tests__","path":"__tests__"} {"id":"rs1:7dc1dbe8d512:dir:src","labels":["Directory"],"name":"src","path":"src"} {"id":"rs1:7dc1dbe8d512:dir:src/react","labels":["Directory"],"name":"react","path":"src/react"} -{"id":"rs1:7dc1dbe8d512:file:AGENTS.md","labels":["File"],"content_hash":"90521eec765e0c506ca87171a3866588c3f33dd129ac05bd0e155860f01f676a","extension":"md","language":"markdown","name":"AGENTS.md","path":"AGENTS.md","role":"docs"} -{"id":"rs1:7dc1dbe8d512:file:CHANGELOG.md","labels":["File"],"content_hash":"9683f60a75556aeceb2ac3abfd0f6d8c7e9b47a025d2aa060385b7f1cac42952","extension":"md","language":"markdown","name":"CHANGELOG.md","path":"CHANGELOG.md","role":"docs"} +{"id":"rs1:7dc1dbe8d512:file:AGENTS.md","labels":["File"],"content_hash":"8d318f0f61be58b3e4670249f9250562b36031db16f9776822d61763e539bfc8","extension":"md","language":"markdown","name":"AGENTS.md","path":"AGENTS.md","role":"docs"} +{"id":"rs1:7dc1dbe8d512:file:CHANGELOG.md","labels":["File"],"content_hash":"ea5a885807764414d9838ece35f13e2a4f25b82de1439fde651f4e10e76d3ae8","extension":"md","language":"markdown","name":"CHANGELOG.md","path":"CHANGELOG.md","role":"docs"} {"id":"rs1:7dc1dbe8d512:file:LICENSE","labels":["File"],"content_hash":"109ee5eff963883c455d2db66a4bcc6a9d1d85e2d443468738ec4203087155f2","language":"unknown","name":"LICENSE","path":"LICENSE","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:README.md","labels":["File"],"content_hash":"23078a21dea08176c89997a7575e4806aa96896bdff10fdac30e183198656ff2","extension":"md","language":"markdown","name":"README.md","path":"README.md","role":"docs"} {"id":"rs1:7dc1dbe8d512:file:__tests__/auth-api.test.ts","labels":["File"],"content_hash":"b8de8a8ba02d1afcba7edf49d14b3d40d7aa5409c49dc12117bfaa2000362ee2","extension":"ts","language":"typescript","name":"auth-api.test.ts","path":"__tests__/auth-api.test.ts","role":"testing"} @@ -12,15 +12,16 @@ {"id":"rs1:7dc1dbe8d512:file:__tests__/auth-guard.test.ts","labels":["File"],"content_hash":"f9d44e2db4c7e9e35a51b0a522d31217f93339fc627c3f8e0fb68955f060ca58","extension":"ts","language":"typescript","name":"auth-guard.test.ts","path":"__tests__/auth-guard.test.ts","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","labels":["File"],"content_hash":"e9cd0c90ef702d907ed4141d8425d28fd3ca8cd0cf41902148af7fa8ea589387","extension":"ts","language":"typescript","name":"auth-store.test.ts","path":"__tests__/auth-store.test.ts","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts","labels":["File"],"content_hash":"7429ad6c713311fdd7d0b882966747864b5810038bc02e729854d6a6cf473688","extension":"ts","language":"typescript","name":"class-tokens.ts","path":"__tests__/class-tokens.ts","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts","labels":["File"],"content_hash":"58ba5052d86fe19dff8c1b19d4ed4932e2113a52e9863d96a23640df35413871","extension":"ts","language":"typescript","name":"dev-bypass-treeshake.test.ts","path":"__tests__/dev-bypass-treeshake.test.ts","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","labels":["File"],"content_hash":"69abb1e2d818334ae82fc9bb8616a824d393ed274849bfd652d0261dad14c6d2","extension":"ts","language":"typescript","name":"dev-login.test.ts","path":"__tests__/dev-login.test.ts","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","labels":["File"],"content_hash":"347db1bfa5e2c1c2b1ba3572f7a573187c562b60361c464c0f4bfe5ac0d7fd26","extension":"tsx","language":"typescript","name":"login-form-dev-bypass.test.tsx","path":"__tests__/login-form-dev-bypass.test.tsx","role":"testing"} +{"id":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","labels":["File"],"content_hash":"196c82c16d5bc5d37f206f2c1a82f148ed9b965bed4582c6974bfc32b80226c1","extension":"tsx","language":"typescript","name":"login-form-dev-bypass.test.tsx","path":"__tests__/login-form-dev-bypass.test.tsx","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","labels":["File"],"content_hash":"62ec42748690e0447f2eac5ff7aa55fe7750bd47baeac54368d73e407a9c8e33","extension":"tsx","language":"typescript","name":"login-form-responsive.test.tsx","path":"__tests__/login-form-responsive.test.tsx","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx","labels":["File"],"content_hash":"9d65ce91df6266f361382c669bcf2b1bfc48417e3514378eb0943039bbb3333e","extension":"tsx","language":"typescript","name":"login-form.test.tsx","path":"__tests__/login-form.test.tsx","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/reduced-motion.test.tsx","labels":["File"],"content_hash":"653ec5f4c87e1d735dec03349aefc5eef643111b1065f554a79c823d08171df9","extension":"tsx","language":"typescript","name":"reduced-motion.test.tsx","path":"__tests__/reduced-motion.test.tsx","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/register-form.test.tsx","labels":["File"],"content_hash":"b599b8889ceb61f9feb89fb8e22278c523abc337bce08cc8ac39e576409ad15e","extension":"tsx","language":"typescript","name":"register-form.test.tsx","path":"__tests__/register-form.test.tsx","role":"testing"} {"id":"rs1:7dc1dbe8d512:file:__tests__/unauthorized.test.tsx","labels":["File"],"content_hash":"8b9e4c343174d8ba8708219612a6076c9c2882972febf7f0f5b3a49bb4d8efff","extension":"tsx","language":"typescript","name":"unauthorized.test.tsx","path":"__tests__/unauthorized.test.tsx","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:package-lock.json","labels":["File"],"content_hash":"083986b1a781bd245e2ce0aa9c5a3147d9ebf9f511dbc637c50d4fc2f3917cca","extension":"json","language":"json","name":"package-lock.json","path":"package-lock.json","role":"config"} -{"id":"rs1:7dc1dbe8d512:file:package.json","labels":["File"],"content_hash":"6642bc6bba56baefa8ed2095e8124b5d4f231d9058cc16ba02fb06c1c46d2501","extension":"json","language":"json","name":"package.json","path":"package.json","role":"config"} +{"id":"rs1:7dc1dbe8d512:file:package-lock.json","labels":["File"],"content_hash":"9a01ad5780d9d1d709ec6019493f2c54436d32820b40170182996f1be9c472b5","extension":"json","language":"json","name":"package-lock.json","path":"package-lock.json","role":"config"} +{"id":"rs1:7dc1dbe8d512:file:package.json","labels":["File"],"content_hash":"b626977de07ac9b721f0381d857930cfc5e9351f90e13d39c29d7121d5d45a47","extension":"json","language":"json","name":"package.json","path":"package.json","role":"config"} {"id":"rs1:7dc1dbe8d512:file:src/auth-api.ts","labels":["File"],"content_hash":"8283d185b74e761d0fae0dac016cb8af9dd6c57b05e1f92fc69a7a7b3397eebd","extension":"ts","language":"typescript","name":"auth-api.ts","path":"src/auth-api.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/auth-client.ts","labels":["File"],"content_hash":"77d879b0baa25a46ad0e1e1383facda3e08b7f6aec571e873f48de895e0c0dc9","extension":"ts","language":"typescript","name":"auth-client.ts","path":"src/auth-client.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/auth-guard.ts","labels":["File"],"content_hash":"89d4edf77fbb31cf613d80f98adc92d8d4ad36b3e27b987e296b087fa844c9c6","extension":"ts","language":"typescript","name":"auth-guard.ts","path":"src/auth-guard.ts","role":"doing"} @@ -28,10 +29,10 @@ {"id":"rs1:7dc1dbe8d512:file:src/extract-token.ts","labels":["File"],"content_hash":"e04f6297414658e88b723749629b451963d5741a8fbfc5fc08a9eaeff8135d3a","extension":"ts","language":"typescript","name":"extract-token.ts","path":"src/extract-token.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/import-meta-env.d.ts","labels":["File"],"content_hash":"4fa5f8bbaea58d65d147a9d1d4a196327786af1a4cbeb6ce3a214b2b8adbb11d","extension":"ts","language":"typescript","name":"import-meta-env.d.ts","path":"src/import-meta-env.d.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/index.ts","labels":["File"],"content_hash":"e53a186066933f327df1d3e08cd4d51acbe4ef899bc5ff6a771952688c44a480","extension":"ts","language":"typescript","name":"index.ts","path":"src/index.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","labels":["File"],"content_hash":"4533035dd9bc608fe9bf7ab93398d82d1b317a393417594538e7f673792c215d","extension":"tsx","language":"typescript","name":"dev-sign-in.tsx","path":"src/react/dev-sign-in.tsx","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","labels":["File"],"content_hash":"89ae486b67d4f24b831c53116c8682f3050d5c76eff03d36038ab071ca2967d8","extension":"tsx","language":"typescript","name":"dev-sign-in.tsx","path":"src/react/dev-sign-in.tsx","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/react/index.ts","labels":["File"],"content_hash":"84d0a8ca28e67f430e32d3b8fe75e6f43d961bdfa0aef7d03a55c72b3641f1a4","extension":"ts","language":"typescript","name":"index.ts","path":"src/react/index.ts","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","labels":["File"],"content_hash":"8caa2423faf18f14cd614bef274fa79e46e3f1b2f4235728f551b2d2b0494a98","extension":"tsx","language":"typescript","name":"input-otp-slots.tsx","path":"src/react/input-otp-slots.tsx","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","labels":["File"],"content_hash":"122497278f3f2a4aa1a32eb9c3d554f0955a354b8f3f20ad93e07d1338cf752c","extension":"tsx","language":"typescript","name":"login-form.tsx","path":"src/react/login-form.tsx","role":"doing"} +{"id":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","labels":["File"],"content_hash":"f90d13d35d590f064bec46301117a23e887dcca55b1e8acfa3694969fbcfec7e","extension":"tsx","language":"typescript","name":"login-form.tsx","path":"src/react/login-form.tsx","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","labels":["File"],"content_hash":"6787e344349e2ee9e9f739b826421e1cb17e11e6b3fa6d518912b9248310f47a","extension":"tsx","language":"typescript","name":"register-form.tsx","path":"src/react/register-form.tsx","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","labels":["File"],"content_hash":"8317e602a3ebb638d3abac1313a1898a8e1f7673e8fad5ec09dd6bb5bd450291","extension":"tsx","language":"typescript","name":"squircle-shift.tsx","path":"src/react/squircle-shift.tsx","role":"doing"} {"id":"rs1:7dc1dbe8d512:file:src/react/unauthorized.tsx","labels":["File"],"content_hash":"9c476bd4ef3bdd0c7b0a8e6763f8bba136c9f0fa8b7ace9463f06bfd67aafbef","extension":"tsx","language":"typescript","name":"unauthorized.tsx","path":"src/react/unauthorized.tsx","role":"doing"} @@ -46,6 +47,7 @@ {"id":"rs1:7dc1dbe8d512:func:__tests__/auth-store.test.ts#jsonResponse@3","labels":["Function"],"content_hash":"a19b0610471fe1230eacae3ea6e0c8a4498614687127a0e67d605f67307fe0da","end_line":38,"file_path":"__tests__/auth-store.test.ts","name":"jsonResponse","qualified_name":"jsonResponse","signature":"function jsonResponse(body: unknown, ok = true, status = 200) {","start_line":32} {"id":"rs1:7dc1dbe8d512:func:__tests__/auth-store.test.ts#routedFetch@1","labels":["Function"],"content_hash":"8b101389b55728d3eed76d73c5f47f8aea80f5896a939d5b3455f4a70d7cac32","end_line":59,"file_path":"__tests__/auth-store.test.ts","name":"routedFetch","qualified_name":"routedFetch","signature":"function routedFetch(opts: {","start_line":44} {"id":"rs1:7dc1dbe8d512:func:__tests__/class-tokens.ts#classTokensAt@2","labels":["Function"],"content_hash":"792f2f9d677cb34c1eccef5080efd009b3606654c1bc3fb4218cc9aec5a4f0c2","end_line":10,"file_path":"__tests__/class-tokens.ts","name":"classTokensAt","qualified_name":"classTokensAt","signature":"function classTokensAt(","start_line":1} +{"id":"rs1:7dc1dbe8d512:func:__tests__/dev-bypass-treeshake.test.ts#bundleLoginForm@1","labels":["Function"],"content_hash":"26d013ffa3d7120fbbbc735687ddfdedeebc005196371c7c2705b3c69d5edb63","end_line":66,"file_path":"__tests__/dev-bypass-treeshake.test.ts","name":"bundleLoginForm","qualified_name":"bundleLoginForm","signature":"async function bundleLoginForm(dev: boolean): Promise {","start_line":50} {"id":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#flush@0","labels":["Function"],"content_hash":"b61870178230350a5ce5908d28d1d04e17a8e5ee21c5a8ae2f9d2a16e88ad676","end_line":70,"file_path":"__tests__/dev-login.test.ts","name":"flush","qualified_name":"flush","signature":"async function flush(): Promise {","start_line":65} {"id":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#jsonResponse@3","labels":["Function"],"content_hash":"02d091bdbb7f54154347eec1dfc500c2a5891196cac87b3007bcfaa39a466877","end_line":30,"file_path":"__tests__/dev-login.test.ts","name":"jsonResponse","qualified_name":"jsonResponse","signature":"function jsonResponse(body: unknown, ok = true, status = 200) {","start_line":28} {"id":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#routedFetch@1","labels":["Function"],"content_hash":"4ad93bb58e06908b0873a1ada62b9a9c0909e009710ae9ce5888b4e96e8b10cb","end_line":62,"file_path":"__tests__/dev-login.test.ts","name":"routedFetch","qualified_name":"routedFetch","signature":"function routedFetch(opts: {","start_line":36} @@ -73,7 +75,7 @@ {"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","labels":["Function"],"content_hash":"d5910737b2aa500912cd3ac95056e2d42cff44acfc4324211e6c9b6519696668","end_line":451,"file_path":"src/auth-store.ts","name":"createAuthStore","qualified_name":"createAuthStore","signature":"function createAuthStore(config: AuthStoreConfig): AuthStore {","start_line":74} {"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#toIdentity@1","labels":["Function"],"content_hash":"dcade69ae796b4c682344cefe22cd5da81f9aea5af102ef1557b0ba6f4dae123","end_line":47,"file_path":"src/auth-store.ts","name":"toIdentity","qualified_name":"toIdentity","signature":"function toIdentity(user: AuthUser): Identity {","start_line":37} {"id":"rs1:7dc1dbe8d512:func:src/extract-token.ts#extractAccessToken@1","labels":["Function"],"content_hash":"6e5e82d686561af6ee3fbfa63e13318e21552ccb0594cfc4b7ddad57c3e27d97","end_line":21,"file_path":"src/extract-token.ts","name":"extractAccessToken","qualified_name":"extractAccessToken","signature":"function extractAccessToken(payload: Record): string | null {","start_line":1} -{"id":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#DevSignInBypass@1","labels":["Function"],"content_hash":"8a6202922b647200971f1c2cc8d2121841fe716b67418a4114ac4cbe09890100","end_line":122,"file_path":"src/react/dev-sign-in.tsx","name":"DevSignInBypass","qualified_name":"DevSignInBypass","signature":"function DevSignInBypass({","start_line":62} +{"id":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#DevSignInBypass@1","labels":["Function"],"content_hash":"8a6202922b647200971f1c2cc8d2121841fe716b67418a4114ac4cbe09890100","end_line":125,"file_path":"src/react/dev-sign-in.tsx","name":"DevSignInBypass","qualified_name":"DevSignInBypass","signature":"function DevSignInBypass({","start_line":65} {"id":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#readDevLoginEmail@0","labels":["Function"],"content_hash":"7fd24bffb01d2f426e2334e8d73b9ccbe98fa6eb0cf4fa4a71f564e814e68e4a","end_line":28,"file_path":"src/react/dev-sign-in.tsx","name":"readDevLoginEmail","qualified_name":"readDevLoginEmail","signature":"function readDevLoginEmail(): string | null {","start_line":21} {"id":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#rememberDevLoginEmail@1","labels":["Function"],"content_hash":"0fa564de72d864c19311d78a6f0a23f9402b8a42dd0e5593f999a5a29676703c","end_line":37,"file_path":"src/react/dev-sign-in.tsx","name":"rememberDevLoginEmail","qualified_name":"rememberDevLoginEmail","signature":"function rememberDevLoginEmail(email: string): void {","start_line":31} {"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTP@1","labels":["Function"],"content_hash":"0e294ca03d9502a4861427bc066da3c24d0482a32d0684fd70ac70bb125b8867","end_line":119,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTP","qualified_name":"InputOTP","signature":"function InputOTP({","start_line":80} @@ -81,12 +83,12 @@ {"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPSeparator@1","labels":["Function"],"content_hash":"fe4ea8aa11e774fd8387e62e881e1101f02b93d787dad90fab219a878ffecc24","end_line":246,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTPSeparator","qualified_name":"InputOTPSeparator","signature":"function InputOTPSeparator({","start_line":213} {"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPSlot@1","labels":["Function"],"content_hash":"a662eb381a3b57fa35edfc9214784240a0d13e161e4cb381c519fa64deffceeb","end_line":211,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTPSlot","qualified_name":"InputOTPSlot","signature":"function InputOTPSlot({","start_line":136} {"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#countOtpSeparators@1","labels":["Function"],"content_hash":"31051096fe80dba352dc5a94d061465937fc76e1f8dd3ddfbdc0f517df6be277","end_line":258,"file_path":"src/react/input-otp-slots.tsx","name":"countOtpSeparators","qualified_name":"countOtpSeparators","signature":"function countOtpSeparators(children: React.ReactNode): number {","start_line":248} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","labels":["Function"],"content_hash":"424b761d84a979d0c88679166df7a8865d15d97cd437fbdf70ba574c61886882","end_line":188,"file_path":"src/react/login-form.tsx","name":"handleAuthError","qualified_name":"LoginForm.handleAuthError","signature":"function handleAuthError(err: unknown, fallbackMessage: string) {","start_line":156} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleDevLogin@0","labels":["Function"],"content_hash":"f03adbb04d23a53d2c182d8fb82d2d59461555c103c39860f02e0acfad1efe95","end_line":289,"file_path":"src/react/login-form.tsx","name":"handleDevLogin","qualified_name":"LoginForm.handleDevLogin","signature":"async function handleDevLogin() {","start_line":243} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleEmailSubmit@1","labels":["Function"],"content_hash":"3b375c8d01a368bfb12271a564b665e7036ad42d41daa929d10f428e2d3b1efb","end_line":206,"file_path":"src/react/login-form.tsx","name":"handleEmailSubmit","qualified_name":"LoginForm.handleEmailSubmit","signature":"async function handleEmailSubmit(e: React.FormEvent) {","start_line":190} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleOtpSubmit@1","labels":["Function"],"content_hash":"e58436a58b839c45c006455a79fd160e727fcee417b3e2471f8900b37a08ad19","end_line":234,"file_path":"src/react/login-form.tsx","name":"handleOtpSubmit","qualified_name":"LoginForm.handleOtpSubmit","signature":"async function handleOtpSubmit(e: React.FormEvent) {","start_line":208} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleResend@0","labels":["Function"],"content_hash":"3acb77db12e60743ac0c21800ebe9b18295b8bd4354dde69dcb2f60c221b6a12","end_line":305,"file_path":"src/react/login-form.tsx","name":"handleResend","qualified_name":"LoginForm.handleResend","signature":"async function handleResend() { setError(\"\");","start_line":291} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","labels":["Function"],"content_hash":"86e38c2a129dffa1f6210874e78ca7e4c78fc53b0b36a1b599fd39dd6f59b83a","end_line":558,"file_path":"src/react/login-form.tsx","name":"LoginForm","qualified_name":"LoginForm","signature":"function LoginForm({","start_line":87} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","labels":["Function"],"content_hash":"424b761d84a979d0c88679166df7a8865d15d97cd437fbdf70ba574c61886882","end_line":190,"file_path":"src/react/login-form.tsx","name":"handleAuthError","qualified_name":"LoginForm.handleAuthError","signature":"function handleAuthError(err: unknown, fallbackMessage: string) {","start_line":158} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleDevLogin@0","labels":["Function"],"content_hash":"b518db175965828d9bfdbc407dfa1a41e346e04734f4b7e1b4cebb8f76b7e0ba","end_line":299,"file_path":"src/react/login-form.tsx","name":"handleDevLogin","qualified_name":"LoginForm.handleDevLogin","signature":"async function handleDevLogin() {","start_line":251} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleEmailSubmit@1","labels":["Function"],"content_hash":"3b375c8d01a368bfb12271a564b665e7036ad42d41daa929d10f428e2d3b1efb","end_line":208,"file_path":"src/react/login-form.tsx","name":"handleEmailSubmit","qualified_name":"LoginForm.handleEmailSubmit","signature":"async function handleEmailSubmit(e: React.FormEvent) {","start_line":192} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleOtpSubmit@1","labels":["Function"],"content_hash":"e58436a58b839c45c006455a79fd160e727fcee417b3e2471f8900b37a08ad19","end_line":236,"file_path":"src/react/login-form.tsx","name":"handleOtpSubmit","qualified_name":"LoginForm.handleOtpSubmit","signature":"async function handleOtpSubmit(e: React.FormEvent) {","start_line":210} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleResend@0","labels":["Function"],"content_hash":"8793829b5b0a2a807dab8e22e706fffd1ec12efa20353ac5df6f86160b4be10d","end_line":316,"file_path":"src/react/login-form.tsx","name":"handleResend","qualified_name":"LoginForm.handleResend","signature":"async function handleResend() {","start_line":301} +{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","labels":["Function"],"content_hash":"23ee0f686b5b1c00b038a6f633479ed36cc78a8d955887aaf50e0a5f1dc9c6ea","end_line":570,"file_path":"src/react/login-form.tsx","name":"LoginForm","qualified_name":"LoginForm","signature":"function LoginForm({","start_line":87} {"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#formatCountdown@2","labels":["Function"],"content_hash":"296f135630b3c9fa000bd6aef44f63c4744704ce80c94449e546be1b6e0f254e","end_line":78,"file_path":"src/react/login-form.tsx","name":"formatCountdown","qualified_name":"formatCountdown","signature":"function formatCountdown(target: string | null, fallback = \"10:00\") {","start_line":72} {"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#otpSentNotice@1","labels":["Function"],"content_hash":"47772925fbd776164e3662e83d665698453aacb62860eeeff13c637cc9eb733f","end_line":70,"file_path":"src/react/login-form.tsx","name":"otpSentNotice","qualified_name":"otpSentNotice","signature":"function otpSentNotice(email: string): string {","start_line":68} {"id":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterCard@1","labels":["Function"],"content_hash":"ed4aa9d087ff807ca6c93693a406597484999091857fea8500b2d2362b4d06f9","end_line":22,"file_path":"src/react/register-form.tsx","name":"RegisterCard","qualified_name":"RegisterCard","signature":"function RegisterCard({ children }: { readonly children: ReactNode }) {","start_line":16} @@ -124,6 +126,9 @@ {"id":"rs1:7dc1dbe8d512:iface:src/types.ts#VerifyOtpResponse","labels":["Interface"],"content_hash":"be10167a9f925b16ff7a13c0047d9fc9349fe790d9f3bdd9006ef365a2da0951","end_line":35,"file_path":"src/types.ts","name":"VerifyOtpResponse","qualified_name":"VerifyOtpResponse","start_line":31} {"id":"rs1:7dc1dbe8d512:repo:.","labels":["Repository"],"is_nested":false,"name":"cellarnode-auth","root_path":"."} {"id":"rs1:7dc1dbe8d512:var:__tests__/auth-store.test.ts#baseMe","labels":["Variable"],"file_path":"__tests__/auth-store.test.ts","kind":"module","name":"baseMe"} +{"id":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#DEV_ONLY_MARKUP","labels":["Variable"],"file_path":"__tests__/dev-bypass-treeshake.test.ts","kind":"const","name":"DEV_ONLY_MARKUP"} +{"id":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#EXTERNALS","labels":["Variable"],"file_path":"__tests__/dev-bypass-treeshake.test.ts","kind":"const","name":"EXTERNALS"} +{"id":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#ROOT","labels":["Variable"],"file_path":"__tests__/dev-bypass-treeshake.test.ts","kind":"const","name":"ROOT"} {"id":"rs1:7dc1dbe8d512:var:__tests__/dev-login.test.ts#OK_BODY","labels":["Variable"],"file_path":"__tests__/dev-login.test.ts","kind":"const","name":"OK_BODY"} {"id":"rs1:7dc1dbe8d512:var:__tests__/dev-login.test.ts#baseMe","labels":["Variable"],"file_path":"__tests__/dev-login.test.ts","kind":"module","name":"baseMe"} {"id":"rs1:7dc1dbe8d512:var:__tests__/login-form-responsive.test.tsx#authApi","labels":["Variable"],"file_path":"__tests__/login-form-responsive.test.tsx","kind":"module","name":"authApi"} diff --git a/AGENTS.md b/AGENTS.md index 4d3fa5b..3ebf96d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,10 +67,18 @@ Rules that must not drift: - **Additive only.** The OTP flow is untouched, always rendered, never auto-skipped, never auto-redirected. The OTP page stays testable. +- **Fails closed.** The dev path resolves the user type through a separate + `GET /auth/me` (verify-otp gets it inline). If that call fails or answers + without a `userType`, the bypass clears the token and errors — an + unresolvable type is never treated as a passing portal check. - **No new env vars.** The frontend gate is the literal `import.meta.env.DEV` - (Vite folds it to `false` in production, so Rollup drops the branch and - `src/react/dev-sign-in.tsx` with it). The backend gate stays - `ENABLE_TEST_ENDPOINTS`. Do not add a `VITE_*` flag. + (Vite folds it to `false` in production, so Rollup drops the branch and the + `DevSignInBypass` component with it — pinned by + `__tests__/dev-bypass-treeshake.test.ts`, which bundles the form both ways + through esbuild and greps the output. Note the scope: only the component is + statically eliminated; `readDevLoginEmail` / `rememberDevLoginEmail` sit + behind runtime guards and survive as unreachable code). The backend gate + stays `ENABLE_TEST_ENDPOINTS`. Do not add a `VITE_*` flag. - **Anti-enumeration (backend T3-1).** `/test/login` returns the SAME 404 for "gate off" and "no such account". `devLogin()` maps it to the single reason `"test-endpoints-disabled"` and frames the copy as "set diff --git a/CHANGELOG.md b/CHANGELOG.md index ef52a35..01ac0b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added - `AuthStore.devLogin(email)` (CEL-1364) — LOCAL-DEV helper that mints a session from the backend's `POST /test/login` and adopts the JWE through the same path `verifyOtp` uses (identity fetch, refresh scheduling, `onAccessTokenSet` / `onOrgChange` fan-out). Returns a `DevLoginResult` instead of throwing; the backend's uniform 404 maps to `reason: "test-endpoints-disabled"` with a "set `ENABLE_TEST_ENDPOINTS=true`" hint, never a claim about the address. Optional on the interface, so custom `AuthStore` implementations stay source-compatible. -- `LoginForm` renders a DEV-only "Dev sign-in (skip the code)" control **alongside** the email form — additive, never a replacement, no auto-redirect. Gated on the literal `import.meta.env.DEV`, so production builds tree-shake the control and its module. No new env vars. +- `LoginForm` renders a DEV-only "Dev sign-in (skip the code)" control **alongside** the email form — additive, never a replacement, no auto-redirect. Gated on the literal `import.meta.env.DEV`, so production builds tree-shake the control away (asserted against real bundler output, not just the runtime conditional). It applies the same portal guard as the OTP path and fails closed: if `/auth/me` cannot resolve a `userType`, the token is cleared instead of the session standing. No new env vars. - `DevSignInBypass`, `readDevLoginEmail`, `rememberDevLoginEmail`, `DEV_LOGIN_EMAIL_STORAGE_KEY` from `@cellarnode/auth/react`; `DevLoginResult` / `DevLoginSuccess` / `DevLoginFailure` / `DevLoginFailureReason` types from `@cellarnode/auth`. ## 0.13.3 diff --git a/__tests__/dev-bypass-treeshake.test.ts b/__tests__/dev-bypass-treeshake.test.ts new file mode 100644 index 0000000..0d7f0ef --- /dev/null +++ b/__tests__/dev-bypass-treeshake.test.ts @@ -0,0 +1,85 @@ +import { fileURLToPath } from "node:url"; +import * as esbuild from "esbuild"; +import { describe, expect, it } from "vitest"; + +/** + * CEL-1364 — build-output proof that the DEV-only sign-in bypass leaves + * production bundles. + * + * The behavioural tests in `login-form-dev-bypass.test.tsx` pin the RUNTIME + * conditional: with `import.meta.env.DEV` stubbed false, nothing renders. That + * is not the same claim. A refactor that reads the flag through an indirection + * the bundler cannot fold statically would keep those tests green while + * shipping the bypass UI to production. + * + * So this bundles `login-form.tsx` the way a consumer's Vite production build + * does — `import.meta.env.DEV` statically defined to `false`, tree-shaking on — + * and asserts the `DevSignInBypass` markup is absent from the emitted code, + * while the same bundle built with DEV=true contains it. Building both + * directions is what stops the assertion going inert if the sentinels drift. + * + * esbuild stands in for Rollup here: it applies the same `define` + DCE that + * makes the elision work, and it is already installed as the transform half of + * vitest's own toolchain. + * + * SCOPE — read before widening. Only the COMPONENT is statically eliminated. + * `readDevLoginEmail` / `rememberDevLoginEmail` / `DEV_LOGIN_EMAIL_STORAGE_KEY` + * are called from live function bodies behind runtime `if` guards, so they + * survive into production bundles as unreachable code. That is a handful of + * bytes and no behaviour (both are no-ops unless called), but it means the + * storage-key literal is NOT a valid sentinel for this test. + */ + +const ROOT = fileURLToPath(new URL("..", import.meta.url)); + +/** Strings that exist only inside `DevSignInBypass`. */ +const DEV_ONLY_MARKUP = ["Dev sign-in (skip the code)", "Development only"]; + +/** Peers a consumer app supplies; irrelevant to what we are measuring. */ +const EXTERNALS = [ + "react", + "react/jsx-runtime", + "react-dom", + "lucide-react", + "input-otp", + "clsx", + "three", + "@react-three/fiber", +]; + +async function bundleLoginForm(dev: boolean): Promise { + const result = await esbuild.build({ + entryPoints: [`${ROOT}src/react/login-form.tsx`], + bundle: true, + write: false, + format: "esm", + treeShaking: true, + jsx: "automatic", + define: { + "import.meta.env.DEV": String(dev), + "import.meta.env.PROD": String(!dev), + }, + external: EXTERNALS, + }); + + return result.outputFiles[0].text; +} + +describe("dev sign-in bypass tree-shaking (CEL-1364)", () => { + it("drops the bypass UI from a production bundle and keeps it in a dev one", async () => { + const [prod, dev] = await Promise.all([ + bundleLoginForm(false), + bundleLoginForm(true), + ]); + + // Guards against a stale sentinel: if these strings ever stop existing, the + // production assertion below would pass for the wrong reason. + for (const marker of DEV_ONLY_MARKUP) { + expect(dev, `DEV bundle should contain ${JSON.stringify(marker)}`).toContain(marker); + } + + for (const marker of DEV_ONLY_MARKUP) { + expect(prod, `PROD bundle must not contain ${JSON.stringify(marker)}`).not.toContain(marker); + } + }, 60_000); +}); diff --git a/__tests__/login-form-dev-bypass.test.tsx b/__tests__/login-form-dev-bypass.test.tsx index 52c4684..c57bcc1 100644 --- a/__tests__/login-form-dev-bypass.test.tsx +++ b/__tests__/login-form-dev-bypass.test.tsx @@ -225,6 +225,61 @@ describe("LoginForm dev bypass — DEV builds (CEL-1364)", () => { expect((await screen.findByRole("alert")).textContent).toMatch(/producer accounts only/i); expect(props.onLoginSuccess).not.toHaveBeenCalled(); }); + + it("fails closed when /auth/me throws — an unresolvable user type is not a pass", async () => { + // The dev path resolves the user type through a SEPARATE `/auth/me` call + // (verifyOtp gets it inline). A transient failure there must NOT be allowed + // to seat a session in the wrong portal. + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "dev@example.com"); + const props = buildProps(); + props.authApi.getMe = vi.fn(async () => { + throw new Error("network"); + }); + renderLogin(props); + + const button = await waitFor(() => { + const el = screen.getByRole("button", { name: /dev sign-in/i }) as HTMLButtonElement; + expect(el.disabled).toBe(false); + return el; + }); + + button.click(); + + await waitFor(() => { + expect(props.authStore.clearAccessToken).toHaveBeenCalled(); + }); + expect((await screen.findByRole("alert")).textContent).toMatch( + /couldn't verify your account type/i, + ); + expect(props.onLoginSuccess).not.toHaveBeenCalled(); + }); + + it("fails closed when /auth/me answers without a userType", async () => { + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "dev@example.com"); + const props = buildProps(); + props.authApi.getMe = vi.fn(async () => ({ + id: "user_dev", + email: "dev@example.com", + name: "Dev", + orgId: "org_dev", + roles: [], + createdAt: "2024-01-01T00:00:00.000Z", + })); + renderLogin(props); + + const button = await waitFor(() => { + const el = screen.getByRole("button", { name: /dev sign-in/i }) as HTMLButtonElement; + expect(el.disabled).toBe(false); + return el; + }); + + button.click(); + + await waitFor(() => { + expect(props.authStore.clearAccessToken).toHaveBeenCalled(); + }); + expect(props.onLoginSuccess).not.toHaveBeenCalled(); + }); }); describe("LoginForm dev bypass — production builds (CEL-1364)", () => { diff --git a/package-lock.json b/package-lock.json index a6105fa..e3b5570 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@testing-library/react": "^16.0.0", "@types/react": "^19.0.0", "@types/three": ">=0.160.0", + "esbuild": "^0.27.0", "happy-dom": "^20.0.0", "input-otp": ">=1.4.0", "lucide-react": ">=0.400.0", diff --git a/package.json b/package.json index e390646..c3a9e06 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@testing-library/react": "^16.0.0", "@types/react": "^19.0.0", "@types/three": ">=0.160.0", + "esbuild": "^0.27.0", "happy-dom": "^20.0.0", "input-otp": ">=1.4.0", "lucide-react": ">=0.400.0", diff --git a/src/react/dev-sign-in.tsx b/src/react/dev-sign-in.tsx index a6a1410..4a27559 100644 --- a/src/react/dev-sign-in.tsx +++ b/src/react/dev-sign-in.tsx @@ -56,8 +56,11 @@ export interface DevSignInBypassProps { * in with a click. * * The component is only ever referenced from inside an - * `import.meta.env.DEV && …` branch, so production bundles drop both the - * branch and this module (the package sets `sideEffects: false`). + * `import.meta.env.DEV && …` branch, so production bundles drop the branch and + * this component with it (the package sets `sideEffects: false`). The two + * storage helpers above are called from live function bodies behind runtime + * guards, so they survive as unreachable code — that difference is pinned in + * `__tests__/dev-bypass-treeshake.test.ts`. */ export function DevSignInBypass({ email, diff --git a/src/react/login-form.tsx b/src/react/login-form.tsx index e0aa3fb..df68730 100644 --- a/src/react/login-form.tsx +++ b/src/react/login-form.tsx @@ -116,7 +116,9 @@ export function LoginForm({ // DEV-only email prefill (CEL-1364). Guarded INSIDE the effect so the hook // itself stays unconditional; Vite folds `import.meta.env.DEV` to `false` in - // production, and Rollup then drops the body along with its imports. + // production, so this body never runs there. (The two tiny storage helpers + // stay in the bundle as unreachable code — only the `DevSignInBypass` + // component is statically dropped. See __tests__/dev-bypass-treeshake.) useEffect(() => { if (!import.meta.env.DEV) return; if (initialEmail) return; @@ -238,7 +240,13 @@ export function LoginForm({ * * Never auto-runs — it fires only from the dev button's click. On success it * applies the SAME portal guard the OTP path applies, so a producer address - * still can't land inside the importer portal (and vice versa). + * still can't land inside the importer portal (and vice versa), and it fails + * CLOSED: an unresolvable user type clears the token instead of standing. + * + * Token-adoption ordering matches `handleOtpSubmit` deliberately: both adopt + * the token first and clear it when the portal check rejects. If that order + * is ever tightened, tighten BOTH paths — fixing only this one hides the + * remaining hole in the OTP path. */ async function handleDevLogin() { if (!authStore.devLogin) return; @@ -258,9 +266,11 @@ export function LoginForm({ return; } - // Portal guard, mirroring handleOtpSubmit. `/auth/me` is advisory here: - // if it fails we let the session stand rather than stranding a developer - // on a transient error. + // Portal guard, mirroring handleOtpSubmit. `verifyOtp` returns the user + // inline; the dev path has to ask `/auth/me` separately, so an + // unresolvable answer is a FAILED check, never a pass. Letting it pass + // would hand a transient `/auth/me` failure the power to seat an importer + // session inside the producer portal. let authenticatedUserType: string | null = null; try { const me = await authApi.getMe(result.accessToken); @@ -288,7 +298,8 @@ export function LoginForm({ } } - async function handleResend() { setError(""); + async function handleResend() { + setError(""); setIsSubmitting(true); try { @@ -513,7 +524,8 @@ export function LoginForm({ form on the same step; the OTP flow above is untouched and stays the only path that exists in production builds. The literal `import.meta.env.DEV` is what Vite folds to `false`, letting - Rollup drop this branch and the `./dev-sign-in.js` module. */} + Rollup drop this branch and the `DevSignInBypass` component + itself — asserted in __tests__/dev-bypass-treeshake.test.ts. */} {import.meta.env.DEV && step === "email" && authStore.devLogin && ( Date: Thu, 13 Aug 2026 07:26:44 +0200 Subject: [PATCH 3/4] [CEL-1364] Review follow-ups: fail-closed portal guard + untrack derived graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finding 3 — handleDevLogin swallowed getMe() failures and then only rejected `if (authenticatedUserType && ...)`, so a transient /auth/me error let an importer session stand in the producer portal. verifyOtp cannot have this hole because it returns result.user inline. An unresolvable user type is now a FAILED portal check: clear the token, surface an actionable error. Two tests cover the closed branches (getMe throws, getMe answers without userType). Token adoption still precedes the portal guard, matching handleOtpSubmit exactly. That parity is pre-existing, not a regression — documented at the call site so nobody tightens only the dev path. Finding 2 — .reposkein/{nodes,edges}.jsonl stay tracked despite the ignore rules, so the ignore did nothing and every index run re-committed generated churn. git rm --cached them; the files remain on disk, untracked. Zero nodes carry summaries, so nothing authored is lost. --- .reposkein/edges.jsonl | 213 --------------------------------------- .reposkein/nodes.jsonl | 148 --------------------------- src/react/login-form.tsx | 10 +- 3 files changed, 9 insertions(+), 362 deletions(-) delete mode 100644 .reposkein/edges.jsonl delete mode 100644 .reposkein/nodes.jsonl diff --git a/.reposkein/edges.jsonl b/.reposkein/edges.jsonl deleted file mode 100644 index 70310fa..0000000 --- a/.reposkein/edges.jsonl +++ /dev/null @@ -1,213 +0,0 @@ -{"from":"rs1:7dc1dbe8d512:class:src/types.ts#AuthError","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/types.ts#AuthError.constructor@4"} -{"from":"rs1:7dc1dbe8d512:dir:.","type":"CONTAINS","to":"rs1:7dc1dbe8d512:dir:__tests__"} -{"from":"rs1:7dc1dbe8d512:dir:.","type":"CONTAINS","to":"rs1:7dc1dbe8d512:dir:src"} -{"from":"rs1:7dc1dbe8d512:dir:.","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:AGENTS.md"} -{"from":"rs1:7dc1dbe8d512:dir:.","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:CHANGELOG.md"} -{"from":"rs1:7dc1dbe8d512:dir:.","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:LICENSE"} -{"from":"rs1:7dc1dbe8d512:dir:.","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:README.md"} -{"from":"rs1:7dc1dbe8d512:dir:.","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:package-lock.json"} -{"from":"rs1:7dc1dbe8d512:dir:.","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:package.json"} -{"from":"rs1:7dc1dbe8d512:dir:.","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:tsconfig.json"} -{"from":"rs1:7dc1dbe8d512:dir:.","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:vitest.config.ts"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/auth-api.test.ts"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/auth-client.test.ts"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/auth-guard.test.ts"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/reduced-motion.test.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/register-form.test.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:__tests__","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:__tests__/unauthorized.test.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:dir:src/react"} -{"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/auth-api.ts"} -{"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/auth-client.ts"} -{"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/auth-guard.ts"} -{"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/auth-store.ts"} -{"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/extract-token.ts"} -{"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/import-meta-env.d.ts"} -{"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/index.ts"} -{"from":"rs1:7dc1dbe8d512:dir:src","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/types.ts"} -{"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/index.ts"} -{"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx"} -{"from":"rs1:7dc1dbe8d512:dir:src/react","type":"CONTAINS","to":"rs1:7dc1dbe8d512:file:src/react/unauthorized.tsx"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-api.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/auth-api.test.ts#mockClient@0"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-api.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/auth-api.test.ts#mockStore@0"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-api.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-api.ts","symbols":["createAuthApi"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-api.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthClient","AuthError","AuthStore"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-client.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/auth-client.test.ts#mockStore@1"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-client.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-client.ts","symbols":["createAuthClient"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-client.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthError","AuthStore"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-guard.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/auth-guard.test.ts#makeUser@1"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-guard.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-guard.ts","symbols":["hasEntitlement","validateUserType"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-guard.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthUser","SessionClaims"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/auth-store.test.ts#flush@0"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/auth-store.test.ts#jsonResponse@3"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/auth-store.test.ts#routedFetch@1"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/auth-store.test.ts#baseMe"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-store.ts","symbols":["createAuthStore"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthUser"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/class-tokens.ts#classTokensAt@2"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-bypass-treeshake.test.ts#bundleLoginForm@1"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#DEV_ONLY_MARKUP"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#EXTERNALS"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#ROOT"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#flush@0"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#jsonResponse@3"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#routedFetch@1"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/dev-login.test.ts#OK_BODY"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/dev-login.test.ts#baseMe"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-store.ts","symbols":["createAuthStore"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthUser"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/login-form-dev-bypass.test.tsx#buildProps@1"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/login-form-dev-bypass.test.tsx#renderLogin@1"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","symbols":["DEV_LOGIN_EMAIL_STORAGE_KEY"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","symbols":["LoginForm"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["DevLoginResult"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/login-form-responsive.test.tsx#authApi"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/login-form-responsive.test.tsx#authStore"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","symbols":["InputOTP","InputOTPGroup","InputOTPSeparator","InputOTPSlot"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","symbols":["LoginForm"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthApi","AuthStore"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/login-form.test.tsx#noop@0"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/login-form.test.tsx#renderEmailStep@1"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/login-form.test.tsx#authApi"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/login-form.test.tsx#authStore"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts","symbols":["classTokensAt"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","symbols":["LoginForm","otpSentNotice"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/reduced-motion.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/reduced-motion.test.tsx#createMatchMediaController@1"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/reduced-motion.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:__tests__/reduced-motion.test.tsx#MatchMediaController"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/reduced-motion.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/reduced-motion.test.tsx#matchMediaController"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/reduced-motion.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","symbols":["InputOTP","InputOTPGroup","InputOTPSlot"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/reduced-motion.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","symbols":["SquircleShift"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/register-form.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/register-form.test.tsx#noop@0"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/register-form.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/register-form.test.tsx#authApi"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/register-form.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts","symbols":["classTokensAt"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/register-form.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","symbols":["RegisterForm"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/register-form.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthApi"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/unauthorized.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:__tests__/unauthorized.test.tsx#noop@0"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/unauthorized.test.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:__tests__/unauthorized.test.tsx#dashboardLinks"} -{"from":"rs1:7dc1dbe8d512:file:__tests__/unauthorized.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts","symbols":["classTokensAt"]} -{"from":"rs1:7dc1dbe8d512:file:__tests__/unauthorized.test.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/unauthorized.tsx","symbols":["UnauthorizedPage"]} -{"from":"rs1:7dc1dbe8d512:file:src/auth-api.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-api.ts#createAuthApi@1"} -{"from":"rs1:7dc1dbe8d512:file:src/auth-api.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/extract-token.ts","symbols":["extractAccessToken"]} -{"from":"rs1:7dc1dbe8d512:file:src/auth-api.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthApi","AuthClient","AuthError","AuthStore","AuthUser","RegisterInput","RequestOtpResponse","VerifyOtpResponse"]} -{"from":"rs1:7dc1dbe8d512:file:src/auth-client.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-client.ts#createAuthClient@1"} -{"from":"rs1:7dc1dbe8d512:file:src/auth-client.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthClient","AuthClientConfig","AuthError","AuthErrorResponse"]} -{"from":"rs1:7dc1dbe8d512:file:src/auth-guard.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-guard.ts#hasEntitlement@2"} -{"from":"rs1:7dc1dbe8d512:file:src/auth-guard.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-guard.ts#validateUserType@2"} -{"from":"rs1:7dc1dbe8d512:file:src/auth-guard.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthUser","SessionClaims","UserType"]} -{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1"} -{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#toIdentity@1"} -{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/auth-store.ts#Identity"} -{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/auth-store.ts#DEFAULT_ACCESS_TOKEN_TTL"} -{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/auth-store.ts#DEV_LOGIN_MESSAGES"} -{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/extract-token.ts","symbols":["extractAccessToken"]} -{"from":"rs1:7dc1dbe8d512:file:src/auth-store.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AccessTokenSetListener","AuthStore","AuthStoreConfig","AuthUser","DevLoginResult","LogoutListener","OrgChangeListener","SessionUserType"]} -{"from":"rs1:7dc1dbe8d512:file:src/extract-token.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/extract-token.ts#extractAccessToken@1"} -{"from":"rs1:7dc1dbe8d512:file:src/import-meta-env.d.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/import-meta-env.d.ts#ImportMeta"} -{"from":"rs1:7dc1dbe8d512:file:src/import-meta-env.d.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/import-meta-env.d.ts#ImportMetaEnv"} -{"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-api.ts","symbols":["createAuthApi"]} -{"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-client.ts","symbols":["createAuthClient"]} -{"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-guard.ts","symbols":["hasEntitlement","validateUserType"]} -{"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/auth-store.ts","symbols":["createAuthStore"]} -{"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/extract-token.ts","symbols":["extractAccessToken"]} -{"from":"rs1:7dc1dbe8d512:file:src/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AccessTokenSetListener","AuthApi","AuthClient","AuthClientConfig","AuthError","AuthErrorResponse","AuthStore","AuthStoreConfig","AuthUser","DashboardLink","DevLoginFailure","DevLoginFailureReason","DevLoginResult","DevLoginSuccess","LogoutListener","OrgChangeListener","RegisterInput","RequestOtpResponse","SessionClaims","SessionUserType","UserType","VerifyOtpResponse"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#DevSignInBypass@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#readDevLoginEmail@0"} -{"from":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#rememberDevLoginEmail@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/react/dev-sign-in.tsx#DevSignInBypassProps"} -{"from":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/react/dev-sign-in.tsx#DEV_LOGIN_EMAIL_STORAGE_KEY"} -{"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","symbols":["DEV_LOGIN_EMAIL_STORAGE_KEY","DevSignInBypass","DevSignInBypassProps","readDevLoginEmail","rememberDevLoginEmail"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","symbols":["InputOTP","InputOTPGroup","InputOTPProps","InputOTPSeparator","InputOTPSlot"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","symbols":["LoginForm","LoginFormProps"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","symbols":["RegisterForm","RegisterFormProps"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","symbols":["SquircleShift","SquircleShiftProps"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/unauthorized.tsx","symbols":["UnauthorizedPage","UnauthorizedPageLabels","UnauthorizedPageProps"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/index.ts","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["DashboardLink"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTP@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPGroup@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPSeparator@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPSlot@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#countOtpSeparators@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/react/input-otp-slots.tsx#OTP_STYLES"} -{"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#formatCountdown@2"} -{"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#otpSentNotice@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/react/login-form.tsx#LoginFormProps"} -{"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/react/login-form.tsx#ERROR_MESSAGES"} -{"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/react/login-form.tsx#LazySquircleShift"} -{"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","symbols":["DevSignInBypass","readDevLoginEmail","rememberDevLoginEmail"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","symbols":["InputOTP","InputOTPGroup","InputOTPSeparator","InputOTPSlot"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthApi","AuthError","AuthStore"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterCard@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterForm@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/react/register-form.tsx#RegisterFormProps"} -{"from":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["AuthApi","AuthError"]} -{"from":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/squircle-shift.tsx#ShaderPlane@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/squircle-shift.tsx#SquircleShift@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/squircle-shift.tsx#useMotionEnabled@0"} -{"from":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/react/squircle-shift.tsx#ShaderPlaneProps"} -{"from":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/react/squircle-shift.tsx#SquircleShiftProps"} -{"from":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/react/squircle-shift.tsx#fragmentShader"} -{"from":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:var:src/react/squircle-shift.tsx#vertexShader"} -{"from":"rs1:7dc1dbe8d512:file:src/react/unauthorized.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/unauthorized.tsx#UnauthorizedPage@1"} -{"from":"rs1:7dc1dbe8d512:file:src/react/unauthorized.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/react/unauthorized.tsx#UnauthorizedPageLabels"} -{"from":"rs1:7dc1dbe8d512:file:src/react/unauthorized.tsx","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/react/unauthorized.tsx#UnauthorizedPageProps"} -{"from":"rs1:7dc1dbe8d512:file:src/react/unauthorized.tsx","type":"IMPORTS","to":"rs1:7dc1dbe8d512:file:src/types.ts","symbols":["DashboardLink"]} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:class:src/types.ts#AuthError"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthApi"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthClient"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthClientConfig"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthErrorResponse"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthStore"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthStoreConfig"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthUser"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#DashboardLink"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#DevLoginFailure"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#DevLoginSuccess"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#RegisterInput"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#RequestOtpResponse"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#SessionClaims"} -{"from":"rs1:7dc1dbe8d512:file:src/types.ts","type":"DEFINES","to":"rs1:7dc1dbe8d512:iface:src/types.ts#VerifyOtpResponse"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-api.ts#createAuthApi@1","type":"INSTANTIATES","to":"rs1:7dc1dbe8d512:class:src/types.ts#AuthError","confidence":1.0,"resolution":"exact","sites":1} -{"from":"rs1:7dc1dbe8d512:func:src/auth-client.ts#createAuthClient@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-client.ts#createAuthClient.parseErrorResponse@1"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-client.ts#createAuthClient@1","type":"INSTANTIATES","to":"rs1:7dc1dbe8d512:class:src/types.ts#AuthError","confidence":1.0,"resolution":"exact","sites":1} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.applyToken@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.fetchIdentity@1","call_sites":1,"confidence":0.7,"resolution":"name_match"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.clearToken@0","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitAccessTokenSet@1","call_sites":1,"confidence":0.7,"resolution":"name_match"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.commitIdentity@3","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitAccessTokenSet@1","call_sites":1,"confidence":0.7,"resolution":"name_match"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.commitIdentity@3","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitOrgChange@1","call_sites":1,"confidence":0.7,"resolution":"name_match"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.fetchIdentity@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#toIdentity@1","call_sites":1,"confidence":1.0,"resolution":"exact"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.performRefresh@0","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.applyToken@1","call_sites":1,"confidence":0.7,"resolution":"name_match"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.performRefresh@0","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.clearToken@0","call_sites":2,"confidence":0.7,"resolution":"name_match"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.performRefresh@0","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.scheduleRefresh@1","call_sites":1,"confidence":0.7,"resolution":"name_match"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.performRefresh@0","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/extract-token.ts#extractAccessToken@1","call_sites":1,"confidence":1.0,"resolution":"exact"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.applyToken@1"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.clearToken@0"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.commitIdentity@3"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitAccessTokenSet@1"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitLogout@0"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitOrgChange@1"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.fetchIdentity@1"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.performRefresh@0"} -{"from":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.scheduleRefresh@1"} -{"from":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTP@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#countOtpSeparators@1","call_sites":1,"confidence":1.0,"resolution":"exact"} -{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleDevLogin@0","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#rememberDevLoginEmail@1","call_sites":1,"confidence":1.0,"resolution":"exact"} -{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleEmailSubmit@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","call_sites":1,"confidence":0.7,"resolution":"name_match"} -{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleOtpSubmit@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","call_sites":1,"confidence":0.7,"resolution":"name_match"} -{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleResend@0","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","call_sites":1,"confidence":0.7,"resolution":"name_match"} -{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#otpSentNotice@1","call_sites":1,"confidence":1.0,"resolution":"exact"} -{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2"} -{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleDevLogin@0"} -{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleEmailSubmit@1"} -{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleOtpSubmit@1"} -{"from":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleResend@0"} -{"from":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterForm@1","type":"DEFINES","to":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterForm.handleSubmit@1"} -{"from":"rs1:7dc1dbe8d512:func:src/react/squircle-shift.tsx#SquircleShift@1","type":"CALLS","to":"rs1:7dc1dbe8d512:func:src/react/squircle-shift.tsx#useMotionEnabled@0","call_sites":1,"confidence":1.0,"resolution":"exact"} diff --git a/.reposkein/nodes.jsonl b/.reposkein/nodes.jsonl deleted file mode 100644 index e2769e1..0000000 --- a/.reposkein/nodes.jsonl +++ /dev/null @@ -1,148 +0,0 @@ -{"id":"rs1:7dc1dbe8d512:class:src/types.ts#AuthError","labels":["Class"],"content_hash":"9fdf144fbb6c45a76f11f6d10e3e573276aeec9db49da44694e7afb94f33e144","end_line":53,"file_path":"src/types.ts","name":"AuthError","qualified_name":"AuthError","start_line":43} -{"id":"rs1:7dc1dbe8d512:dir:.","labels":["Directory"],"name":".","path":"."} -{"id":"rs1:7dc1dbe8d512:dir:__tests__","labels":["Directory"],"name":"__tests__","path":"__tests__"} -{"id":"rs1:7dc1dbe8d512:dir:src","labels":["Directory"],"name":"src","path":"src"} -{"id":"rs1:7dc1dbe8d512:dir:src/react","labels":["Directory"],"name":"react","path":"src/react"} -{"id":"rs1:7dc1dbe8d512:file:AGENTS.md","labels":["File"],"content_hash":"8d318f0f61be58b3e4670249f9250562b36031db16f9776822d61763e539bfc8","extension":"md","language":"markdown","name":"AGENTS.md","path":"AGENTS.md","role":"docs"} -{"id":"rs1:7dc1dbe8d512:file:CHANGELOG.md","labels":["File"],"content_hash":"ea5a885807764414d9838ece35f13e2a4f25b82de1439fde651f4e10e76d3ae8","extension":"md","language":"markdown","name":"CHANGELOG.md","path":"CHANGELOG.md","role":"docs"} -{"id":"rs1:7dc1dbe8d512:file:LICENSE","labels":["File"],"content_hash":"109ee5eff963883c455d2db66a4bcc6a9d1d85e2d443468738ec4203087155f2","language":"unknown","name":"LICENSE","path":"LICENSE","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:README.md","labels":["File"],"content_hash":"23078a21dea08176c89997a7575e4806aa96896bdff10fdac30e183198656ff2","extension":"md","language":"markdown","name":"README.md","path":"README.md","role":"docs"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/auth-api.test.ts","labels":["File"],"content_hash":"b8de8a8ba02d1afcba7edf49d14b3d40d7aa5409c49dc12117bfaa2000362ee2","extension":"ts","language":"typescript","name":"auth-api.test.ts","path":"__tests__/auth-api.test.ts","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/auth-client.test.ts","labels":["File"],"content_hash":"74a706baefe4808229cee783014d494210f6c4870aecff6765656e9e41c6a0de","extension":"ts","language":"typescript","name":"auth-client.test.ts","path":"__tests__/auth-client.test.ts","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/auth-guard.test.ts","labels":["File"],"content_hash":"f9d44e2db4c7e9e35a51b0a522d31217f93339fc627c3f8e0fb68955f060ca58","extension":"ts","language":"typescript","name":"auth-guard.test.ts","path":"__tests__/auth-guard.test.ts","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/auth-store.test.ts","labels":["File"],"content_hash":"e9cd0c90ef702d907ed4141d8425d28fd3ca8cd0cf41902148af7fa8ea589387","extension":"ts","language":"typescript","name":"auth-store.test.ts","path":"__tests__/auth-store.test.ts","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/class-tokens.ts","labels":["File"],"content_hash":"7429ad6c713311fdd7d0b882966747864b5810038bc02e729854d6a6cf473688","extension":"ts","language":"typescript","name":"class-tokens.ts","path":"__tests__/class-tokens.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/dev-bypass-treeshake.test.ts","labels":["File"],"content_hash":"58ba5052d86fe19dff8c1b19d4ed4932e2113a52e9863d96a23640df35413871","extension":"ts","language":"typescript","name":"dev-bypass-treeshake.test.ts","path":"__tests__/dev-bypass-treeshake.test.ts","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/dev-login.test.ts","labels":["File"],"content_hash":"69abb1e2d818334ae82fc9bb8616a824d393ed274849bfd652d0261dad14c6d2","extension":"ts","language":"typescript","name":"dev-login.test.ts","path":"__tests__/dev-login.test.ts","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/login-form-dev-bypass.test.tsx","labels":["File"],"content_hash":"196c82c16d5bc5d37f206f2c1a82f148ed9b965bed4582c6974bfc32b80226c1","extension":"tsx","language":"typescript","name":"login-form-dev-bypass.test.tsx","path":"__tests__/login-form-dev-bypass.test.tsx","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/login-form-responsive.test.tsx","labels":["File"],"content_hash":"62ec42748690e0447f2eac5ff7aa55fe7750bd47baeac54368d73e407a9c8e33","extension":"tsx","language":"typescript","name":"login-form-responsive.test.tsx","path":"__tests__/login-form-responsive.test.tsx","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/login-form.test.tsx","labels":["File"],"content_hash":"9d65ce91df6266f361382c669bcf2b1bfc48417e3514378eb0943039bbb3333e","extension":"tsx","language":"typescript","name":"login-form.test.tsx","path":"__tests__/login-form.test.tsx","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/reduced-motion.test.tsx","labels":["File"],"content_hash":"653ec5f4c87e1d735dec03349aefc5eef643111b1065f554a79c823d08171df9","extension":"tsx","language":"typescript","name":"reduced-motion.test.tsx","path":"__tests__/reduced-motion.test.tsx","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/register-form.test.tsx","labels":["File"],"content_hash":"b599b8889ceb61f9feb89fb8e22278c523abc337bce08cc8ac39e576409ad15e","extension":"tsx","language":"typescript","name":"register-form.test.tsx","path":"__tests__/register-form.test.tsx","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:__tests__/unauthorized.test.tsx","labels":["File"],"content_hash":"8b9e4c343174d8ba8708219612a6076c9c2882972febf7f0f5b3a49bb4d8efff","extension":"tsx","language":"typescript","name":"unauthorized.test.tsx","path":"__tests__/unauthorized.test.tsx","role":"testing"} -{"id":"rs1:7dc1dbe8d512:file:package-lock.json","labels":["File"],"content_hash":"9a01ad5780d9d1d709ec6019493f2c54436d32820b40170182996f1be9c472b5","extension":"json","language":"json","name":"package-lock.json","path":"package-lock.json","role":"config"} -{"id":"rs1:7dc1dbe8d512:file:package.json","labels":["File"],"content_hash":"b626977de07ac9b721f0381d857930cfc5e9351f90e13d39c29d7121d5d45a47","extension":"json","language":"json","name":"package.json","path":"package.json","role":"config"} -{"id":"rs1:7dc1dbe8d512:file:src/auth-api.ts","labels":["File"],"content_hash":"8283d185b74e761d0fae0dac016cb8af9dd6c57b05e1f92fc69a7a7b3397eebd","extension":"ts","language":"typescript","name":"auth-api.ts","path":"src/auth-api.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/auth-client.ts","labels":["File"],"content_hash":"77d879b0baa25a46ad0e1e1383facda3e08b7f6aec571e873f48de895e0c0dc9","extension":"ts","language":"typescript","name":"auth-client.ts","path":"src/auth-client.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/auth-guard.ts","labels":["File"],"content_hash":"89d4edf77fbb31cf613d80f98adc92d8d4ad36b3e27b987e296b087fa844c9c6","extension":"ts","language":"typescript","name":"auth-guard.ts","path":"src/auth-guard.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/auth-store.ts","labels":["File"],"content_hash":"53e77292bc67b84d68c9b100c77e7676fec0702503d04b88a97aa2fa7dd68162","extension":"ts","language":"typescript","name":"auth-store.ts","path":"src/auth-store.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/extract-token.ts","labels":["File"],"content_hash":"e04f6297414658e88b723749629b451963d5741a8fbfc5fc08a9eaeff8135d3a","extension":"ts","language":"typescript","name":"extract-token.ts","path":"src/extract-token.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/import-meta-env.d.ts","labels":["File"],"content_hash":"4fa5f8bbaea58d65d147a9d1d4a196327786af1a4cbeb6ce3a214b2b8adbb11d","extension":"ts","language":"typescript","name":"import-meta-env.d.ts","path":"src/import-meta-env.d.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/index.ts","labels":["File"],"content_hash":"e53a186066933f327df1d3e08cd4d51acbe4ef899bc5ff6a771952688c44a480","extension":"ts","language":"typescript","name":"index.ts","path":"src/index.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/dev-sign-in.tsx","labels":["File"],"content_hash":"89ae486b67d4f24b831c53116c8682f3050d5c76eff03d36038ab071ca2967d8","extension":"tsx","language":"typescript","name":"dev-sign-in.tsx","path":"src/react/dev-sign-in.tsx","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/index.ts","labels":["File"],"content_hash":"84d0a8ca28e67f430e32d3b8fe75e6f43d961bdfa0aef7d03a55c72b3641f1a4","extension":"ts","language":"typescript","name":"index.ts","path":"src/react/index.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/input-otp-slots.tsx","labels":["File"],"content_hash":"8caa2423faf18f14cd614bef274fa79e46e3f1b2f4235728f551b2d2b0494a98","extension":"tsx","language":"typescript","name":"input-otp-slots.tsx","path":"src/react/input-otp-slots.tsx","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/login-form.tsx","labels":["File"],"content_hash":"f90d13d35d590f064bec46301117a23e887dcca55b1e8acfa3694969fbcfec7e","extension":"tsx","language":"typescript","name":"login-form.tsx","path":"src/react/login-form.tsx","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/register-form.tsx","labels":["File"],"content_hash":"6787e344349e2ee9e9f739b826421e1cb17e11e6b3fa6d518912b9248310f47a","extension":"tsx","language":"typescript","name":"register-form.tsx","path":"src/react/register-form.tsx","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/squircle-shift.tsx","labels":["File"],"content_hash":"8317e602a3ebb638d3abac1313a1898a8e1f7673e8fad5ec09dd6bb5bd450291","extension":"tsx","language":"typescript","name":"squircle-shift.tsx","path":"src/react/squircle-shift.tsx","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/react/unauthorized.tsx","labels":["File"],"content_hash":"9c476bd4ef3bdd0c7b0a8e6763f8bba136c9f0fa8b7ace9463f06bfd67aafbef","extension":"tsx","language":"typescript","name":"unauthorized.tsx","path":"src/react/unauthorized.tsx","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:src/types.ts","labels":["File"],"content_hash":"8ed9764623f369ee8839c9f8d6f21b4296c182795755b8b1b40bb08839894646","extension":"ts","language":"typescript","name":"types.ts","path":"src/types.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:file:tsconfig.json","labels":["File"],"content_hash":"b74e706331680ec7b061a2c9935af8ea7e0375542f17656f4949143aed03597c","extension":"json","language":"json","name":"tsconfig.json","path":"tsconfig.json","role":"config"} -{"id":"rs1:7dc1dbe8d512:file:vitest.config.ts","labels":["File"],"content_hash":"919d683032c69311ef96ec0816a3f74ace196ebb2c0cca768511729b1841c67c","extension":"ts","language":"typescript","name":"vitest.config.ts","path":"vitest.config.ts","role":"doing"} -{"id":"rs1:7dc1dbe8d512:func:__tests__/auth-api.test.ts#mockClient@0","labels":["Function"],"content_hash":"03a6959d9fd58d56b92c53255cc07458cba2abec1e5d0aea8b3e39e264d07448","end_line":10,"file_path":"__tests__/auth-api.test.ts","name":"mockClient","qualified_name":"mockClient","signature":"function mockClient(): AuthClient {","start_line":6} -{"id":"rs1:7dc1dbe8d512:func:__tests__/auth-api.test.ts#mockStore@0","labels":["Function"],"content_hash":"bb4cca7af6b051853303f677a3b49492e2b7c46e01eb4090621db4eb9508dc6d","end_line":20,"file_path":"__tests__/auth-api.test.ts","name":"mockStore","qualified_name":"mockStore","signature":"function mockStore(): AuthStore {","start_line":12} -{"id":"rs1:7dc1dbe8d512:func:__tests__/auth-client.test.ts#mockStore@1","labels":["Function"],"content_hash":"67689ddd804e002c328404208a0274d63093337a8f385c2d7f57ed208c56a129","end_line":14,"file_path":"__tests__/auth-client.test.ts","name":"mockStore","qualified_name":"mockStore","signature":"function mockStore(token: string | null = \"tok_123\"): AuthStore {","start_line":6} -{"id":"rs1:7dc1dbe8d512:func:__tests__/auth-guard.test.ts#makeUser@1","labels":["Function"],"content_hash":"e71a7659212e50475708a1b966b56b332f0a61cd13121f6a7a6af40d4d0069ea","end_line":13,"file_path":"__tests__/auth-guard.test.ts","name":"makeUser","qualified_name":"makeUser","signature":"(userType: AuthUser[\"userType\"]): AuthUser => ({","start_line":5} -{"id":"rs1:7dc1dbe8d512:func:__tests__/auth-store.test.ts#flush@0","labels":["Function"],"content_hash":"2b1a69488049c64230487709876345115fbd04ecfe2ac014cb40129207250683","end_line":68,"file_path":"__tests__/auth-store.test.ts","name":"flush","qualified_name":"flush","signature":"async function flush(): Promise {","start_line":62} -{"id":"rs1:7dc1dbe8d512:func:__tests__/auth-store.test.ts#jsonResponse@3","labels":["Function"],"content_hash":"a19b0610471fe1230eacae3ea6e0c8a4498614687127a0e67d605f67307fe0da","end_line":38,"file_path":"__tests__/auth-store.test.ts","name":"jsonResponse","qualified_name":"jsonResponse","signature":"function jsonResponse(body: unknown, ok = true, status = 200) {","start_line":32} -{"id":"rs1:7dc1dbe8d512:func:__tests__/auth-store.test.ts#routedFetch@1","labels":["Function"],"content_hash":"8b101389b55728d3eed76d73c5f47f8aea80f5896a939d5b3455f4a70d7cac32","end_line":59,"file_path":"__tests__/auth-store.test.ts","name":"routedFetch","qualified_name":"routedFetch","signature":"function routedFetch(opts: {","start_line":44} -{"id":"rs1:7dc1dbe8d512:func:__tests__/class-tokens.ts#classTokensAt@2","labels":["Function"],"content_hash":"792f2f9d677cb34c1eccef5080efd009b3606654c1bc3fb4218cc9aec5a4f0c2","end_line":10,"file_path":"__tests__/class-tokens.ts","name":"classTokensAt","qualified_name":"classTokensAt","signature":"function classTokensAt(","start_line":1} -{"id":"rs1:7dc1dbe8d512:func:__tests__/dev-bypass-treeshake.test.ts#bundleLoginForm@1","labels":["Function"],"content_hash":"26d013ffa3d7120fbbbc735687ddfdedeebc005196371c7c2705b3c69d5edb63","end_line":66,"file_path":"__tests__/dev-bypass-treeshake.test.ts","name":"bundleLoginForm","qualified_name":"bundleLoginForm","signature":"async function bundleLoginForm(dev: boolean): Promise {","start_line":50} -{"id":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#flush@0","labels":["Function"],"content_hash":"b61870178230350a5ce5908d28d1d04e17a8e5ee21c5a8ae2f9d2a16e88ad676","end_line":70,"file_path":"__tests__/dev-login.test.ts","name":"flush","qualified_name":"flush","signature":"async function flush(): Promise {","start_line":65} -{"id":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#jsonResponse@3","labels":["Function"],"content_hash":"02d091bdbb7f54154347eec1dfc500c2a5891196cac87b3007bcfaa39a466877","end_line":30,"file_path":"__tests__/dev-login.test.ts","name":"jsonResponse","qualified_name":"jsonResponse","signature":"function jsonResponse(body: unknown, ok = true, status = 200) {","start_line":28} -{"id":"rs1:7dc1dbe8d512:func:__tests__/dev-login.test.ts#routedFetch@1","labels":["Function"],"content_hash":"4ad93bb58e06908b0873a1ada62b9a9c0909e009710ae9ce5888b4e96e8b10cb","end_line":62,"file_path":"__tests__/dev-login.test.ts","name":"routedFetch","qualified_name":"routedFetch","signature":"function routedFetch(opts: {","start_line":36} -{"id":"rs1:7dc1dbe8d512:func:__tests__/login-form-dev-bypass.test.tsx#buildProps@1","labels":["Function"],"content_hash":"7a287538606744aacf003ec4bd0264ca502a6280fdbb122df7c2781b1acfcc35","end_line":66,"file_path":"__tests__/login-form-dev-bypass.test.tsx","name":"buildProps","qualified_name":"buildProps","signature":"function buildProps(overrides: Record = {}) {","start_line":28} -{"id":"rs1:7dc1dbe8d512:func:__tests__/login-form-dev-bypass.test.tsx#renderLogin@1","labels":["Function"],"content_hash":"910f55b9a12185e47d43cebec525b5b72eceed7109de00da1ba18500b32f7a3e","end_line":71,"file_path":"__tests__/login-form-dev-bypass.test.tsx","name":"renderLogin","qualified_name":"renderLogin","signature":"function renderLogin(props: ReturnType) {","start_line":68} -{"id":"rs1:7dc1dbe8d512:func:__tests__/login-form.test.tsx#noop@0","labels":["Function"],"content_hash":"5663608f35c5064bc27d9aff074d4a458cc30c8e6fda27521f691a621743c6be","end_line":16,"file_path":"__tests__/login-form.test.tsx","name":"noop","qualified_name":"noop","signature":"() => {}","start_line":16} -{"id":"rs1:7dc1dbe8d512:func:__tests__/login-form.test.tsx#renderEmailStep@1","labels":["Function"],"content_hash":"ede73403c426aa609369412f00e1b6dfafbade6db4f51b86c1513c69df0e509e","end_line":48,"file_path":"__tests__/login-form.test.tsx","name":"renderEmailStep","qualified_name":"renderEmailStep","signature":"function renderEmailStep(extraProps: Record = {}) {","start_line":37} -{"id":"rs1:7dc1dbe8d512:func:__tests__/reduced-motion.test.tsx#createMatchMediaController@1","labels":["Function"],"content_hash":"28a1d131fb3c016c21951f9f0fd3c5e518cfca94662c15dd5b0d09d2176c01de","end_line":95,"file_path":"__tests__/reduced-motion.test.tsx","name":"createMatchMediaController","qualified_name":"createMatchMediaController","signature":"function createMatchMediaController(initialMatches: boolean): MatchMediaController {","start_line":58} -{"id":"rs1:7dc1dbe8d512:func:__tests__/register-form.test.tsx#noop@0","labels":["Function"],"content_hash":"5663608f35c5064bc27d9aff074d4a458cc30c8e6fda27521f691a621743c6be","end_line":10,"file_path":"__tests__/register-form.test.tsx","name":"noop","qualified_name":"noop","signature":"() => {}","start_line":10} -{"id":"rs1:7dc1dbe8d512:func:__tests__/unauthorized.test.tsx#noop@0","labels":["Function"],"content_hash":"5663608f35c5064bc27d9aff074d4a458cc30c8e6fda27521f691a621743c6be","end_line":10,"file_path":"__tests__/unauthorized.test.tsx","name":"noop","qualified_name":"noop","signature":"() => {}","start_line":10} -{"id":"rs1:7dc1dbe8d512:func:src/auth-api.ts#createAuthApi@1","labels":["Function"],"content_hash":"9f09ff451723ba36d6d14b368ce7ac42527fec624e2ff38e8c05ceac8172095f","end_line":81,"file_path":"src/auth-api.ts","name":"createAuthApi","qualified_name":"createAuthApi","signature":"function createAuthApi(config: {","start_line":13} -{"id":"rs1:7dc1dbe8d512:func:src/auth-client.ts#createAuthClient.parseErrorResponse@1","labels":["Function"],"content_hash":"f296fa0f32d059ff86edb8a3a6283cc48a68d5ebdd6fb4ff5d01bf8b2373beb6","end_line":24,"file_path":"src/auth-client.ts","name":"parseErrorResponse","qualified_name":"createAuthClient.parseErrorResponse","signature":"async function parseErrorResponse(","start_line":7} -{"id":"rs1:7dc1dbe8d512:func:src/auth-client.ts#createAuthClient@1","labels":["Function"],"content_hash":"67c7657e2bd9c4cbd979de39b5479620ccadb1ebc22194001511da0d94275dbe","end_line":87,"file_path":"src/auth-client.ts","name":"createAuthClient","qualified_name":"createAuthClient","signature":"function createAuthClient(config: AuthClientConfig): AuthClient {","start_line":4} -{"id":"rs1:7dc1dbe8d512:func:src/auth-guard.ts#hasEntitlement@2","labels":["Function"],"content_hash":"4e9c7a87605be060a029139c441ddfa231b889a85b3518d5cb784f5a138c5e6e","end_line":20,"file_path":"src/auth-guard.ts","name":"hasEntitlement","qualified_name":"hasEntitlement","signature":"function hasEntitlement(","start_line":15} -{"id":"rs1:7dc1dbe8d512:func:src/auth-guard.ts#validateUserType@2","labels":["Function"],"content_hash":"82f592f4a306ab35c778241085f90f37ff078f82826f3bdbeba8b87fb3687b4c","end_line":5,"file_path":"src/auth-guard.ts","name":"validateUserType","qualified_name":"validateUserType","signature":"function validateUserType(user: AuthUser, expected: UserType): boolean {","start_line":3} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.applyToken@1","labels":["Function"],"content_hash":"f9b3c823076506eb4578fddcc8a9e62048ccdb31e79672a4b1ade9d24df08a40","end_line":234,"file_path":"src/auth-store.ts","name":"applyToken","qualified_name":"createAuthStore.applyToken","signature":"function applyToken(token: string): void {","start_line":219} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.clearToken@0","labels":["Function"],"content_hash":"56cf8dcfea4d1cc138f252faa2c8da1b556b0627afb7bb9fd463b710dc61ed13","end_line":250,"file_path":"src/auth-store.ts","name":"clearToken","qualified_name":"createAuthStore.clearToken","signature":"function clearToken(): void {","start_line":243} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.commitIdentity@3","labels":["Function"],"content_hash":"f9d36b09b1496e29f9e6b4418e8411d6d471e97fa06ac06eef885eb73c1a651c","end_line":210,"file_path":"src/auth-store.ts","name":"commitIdentity","qualified_name":"createAuthStore.commitIdentity","signature":"function commitIdentity(","start_line":185} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitAccessTokenSet@1","labels":["Function"],"content_hash":"7ef01910748fda976dfdecaccdab4379c658cfe97bfd9f0dee5b051dea3181a0","end_line":121,"file_path":"src/auth-store.ts","name":"emitAccessTokenSet","qualified_name":"createAuthStore.emitAccessTokenSet","signature":"function emitAccessTokenSet(token: string | null): void {","start_line":113} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitLogout@0","labels":["Function"],"content_hash":"3c8240bd971a8b627c5b26ecd0ef45c537fb6a0016758b2277d42624956e67d8","end_line":131,"file_path":"src/auth-store.ts","name":"emitLogout","qualified_name":"createAuthStore.emitLogout","signature":"function emitLogout(): void {","start_line":123} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.emitOrgChange@1","labels":["Function"],"content_hash":"116a2dc2c42fe9ba7752dc3502969f620bb8e1d13a95017e984229df92224039","end_line":111,"file_path":"src/auth-store.ts","name":"emitOrgChange","qualified_name":"createAuthStore.emitOrgChange","signature":"function emitOrgChange(orgId: string | null): void {","start_line":102} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.fetchIdentity@1","labels":["Function"],"content_hash":"83d6c28991f1bc8e01fad5b15c0a1c6fe0876600311c361748be53e7949dd511","end_line":166,"file_path":"src/auth-store.ts","name":"fetchIdentity","qualified_name":"createAuthStore.fetchIdentity","signature":"async function fetchIdentity(token: string): Promise {","start_line":150} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.performRefresh@0","labels":["Function"],"content_hash":"1098883236ae94f6f6863168a874211d463cd7b5b81d8bcc3dc6db66bfeb7c69","end_line":276,"file_path":"src/auth-store.ts","name":"performRefresh","qualified_name":"createAuthStore.performRefresh","signature":"async function performRefresh(): Promise {","start_line":252} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore.scheduleRefresh@1","labels":["Function"],"content_hash":"7795ef507ea882fe257e5c89025281e803e24cdf19084695cd2c0a2b7209e381","end_line":139,"file_path":"src/auth-store.ts","name":"scheduleRefresh","qualified_name":"createAuthStore.scheduleRefresh","signature":"function scheduleRefresh(expiresInSeconds: number): void {","start_line":133} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#createAuthStore@1","labels":["Function"],"content_hash":"d5910737b2aa500912cd3ac95056e2d42cff44acfc4324211e6c9b6519696668","end_line":451,"file_path":"src/auth-store.ts","name":"createAuthStore","qualified_name":"createAuthStore","signature":"function createAuthStore(config: AuthStoreConfig): AuthStore {","start_line":74} -{"id":"rs1:7dc1dbe8d512:func:src/auth-store.ts#toIdentity@1","labels":["Function"],"content_hash":"dcade69ae796b4c682344cefe22cd5da81f9aea5af102ef1557b0ba6f4dae123","end_line":47,"file_path":"src/auth-store.ts","name":"toIdentity","qualified_name":"toIdentity","signature":"function toIdentity(user: AuthUser): Identity {","start_line":37} -{"id":"rs1:7dc1dbe8d512:func:src/extract-token.ts#extractAccessToken@1","labels":["Function"],"content_hash":"6e5e82d686561af6ee3fbfa63e13318e21552ccb0594cfc4b7ddad57c3e27d97","end_line":21,"file_path":"src/extract-token.ts","name":"extractAccessToken","qualified_name":"extractAccessToken","signature":"function extractAccessToken(payload: Record): string | null {","start_line":1} -{"id":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#DevSignInBypass@1","labels":["Function"],"content_hash":"8a6202922b647200971f1c2cc8d2121841fe716b67418a4114ac4cbe09890100","end_line":125,"file_path":"src/react/dev-sign-in.tsx","name":"DevSignInBypass","qualified_name":"DevSignInBypass","signature":"function DevSignInBypass({","start_line":65} -{"id":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#readDevLoginEmail@0","labels":["Function"],"content_hash":"7fd24bffb01d2f426e2334e8d73b9ccbe98fa6eb0cf4fa4a71f564e814e68e4a","end_line":28,"file_path":"src/react/dev-sign-in.tsx","name":"readDevLoginEmail","qualified_name":"readDevLoginEmail","signature":"function readDevLoginEmail(): string | null {","start_line":21} -{"id":"rs1:7dc1dbe8d512:func:src/react/dev-sign-in.tsx#rememberDevLoginEmail@1","labels":["Function"],"content_hash":"0fa564de72d864c19311d78a6f0a23f9402b8a42dd0e5593f999a5a29676703c","end_line":37,"file_path":"src/react/dev-sign-in.tsx","name":"rememberDevLoginEmail","qualified_name":"rememberDevLoginEmail","signature":"function rememberDevLoginEmail(email: string): void {","start_line":31} -{"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTP@1","labels":["Function"],"content_hash":"0e294ca03d9502a4861427bc066da3c24d0482a32d0684fd70ac70bb125b8867","end_line":119,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTP","qualified_name":"InputOTP","signature":"function InputOTP({","start_line":80} -{"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPGroup@1","labels":["Function"],"content_hash":"1b5a1cffedb2ec6bf7030682b30429cf8e946bbb5c535003d7208129454e3e00","end_line":134,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTPGroup","qualified_name":"InputOTPGroup","signature":"function InputOTPGroup({","start_line":121} -{"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPSeparator@1","labels":["Function"],"content_hash":"fe4ea8aa11e774fd8387e62e881e1101f02b93d787dad90fab219a878ffecc24","end_line":246,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTPSeparator","qualified_name":"InputOTPSeparator","signature":"function InputOTPSeparator({","start_line":213} -{"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#InputOTPSlot@1","labels":["Function"],"content_hash":"a662eb381a3b57fa35edfc9214784240a0d13e161e4cb381c519fa64deffceeb","end_line":211,"file_path":"src/react/input-otp-slots.tsx","name":"InputOTPSlot","qualified_name":"InputOTPSlot","signature":"function InputOTPSlot({","start_line":136} -{"id":"rs1:7dc1dbe8d512:func:src/react/input-otp-slots.tsx#countOtpSeparators@1","labels":["Function"],"content_hash":"31051096fe80dba352dc5a94d061465937fc76e1f8dd3ddfbdc0f517df6be277","end_line":258,"file_path":"src/react/input-otp-slots.tsx","name":"countOtpSeparators","qualified_name":"countOtpSeparators","signature":"function countOtpSeparators(children: React.ReactNode): number {","start_line":248} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleAuthError@2","labels":["Function"],"content_hash":"424b761d84a979d0c88679166df7a8865d15d97cd437fbdf70ba574c61886882","end_line":190,"file_path":"src/react/login-form.tsx","name":"handleAuthError","qualified_name":"LoginForm.handleAuthError","signature":"function handleAuthError(err: unknown, fallbackMessage: string) {","start_line":158} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleDevLogin@0","labels":["Function"],"content_hash":"b518db175965828d9bfdbc407dfa1a41e346e04734f4b7e1b4cebb8f76b7e0ba","end_line":299,"file_path":"src/react/login-form.tsx","name":"handleDevLogin","qualified_name":"LoginForm.handleDevLogin","signature":"async function handleDevLogin() {","start_line":251} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleEmailSubmit@1","labels":["Function"],"content_hash":"3b375c8d01a368bfb12271a564b665e7036ad42d41daa929d10f428e2d3b1efb","end_line":208,"file_path":"src/react/login-form.tsx","name":"handleEmailSubmit","qualified_name":"LoginForm.handleEmailSubmit","signature":"async function handleEmailSubmit(e: React.FormEvent) {","start_line":192} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleOtpSubmit@1","labels":["Function"],"content_hash":"e58436a58b839c45c006455a79fd160e727fcee417b3e2471f8900b37a08ad19","end_line":236,"file_path":"src/react/login-form.tsx","name":"handleOtpSubmit","qualified_name":"LoginForm.handleOtpSubmit","signature":"async function handleOtpSubmit(e: React.FormEvent) {","start_line":210} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm.handleResend@0","labels":["Function"],"content_hash":"8793829b5b0a2a807dab8e22e706fffd1ec12efa20353ac5df6f86160b4be10d","end_line":316,"file_path":"src/react/login-form.tsx","name":"handleResend","qualified_name":"LoginForm.handleResend","signature":"async function handleResend() {","start_line":301} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#LoginForm@1","labels":["Function"],"content_hash":"23ee0f686b5b1c00b038a6f633479ed36cc78a8d955887aaf50e0a5f1dc9c6ea","end_line":570,"file_path":"src/react/login-form.tsx","name":"LoginForm","qualified_name":"LoginForm","signature":"function LoginForm({","start_line":87} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#formatCountdown@2","labels":["Function"],"content_hash":"296f135630b3c9fa000bd6aef44f63c4744704ce80c94449e546be1b6e0f254e","end_line":78,"file_path":"src/react/login-form.tsx","name":"formatCountdown","qualified_name":"formatCountdown","signature":"function formatCountdown(target: string | null, fallback = \"10:00\") {","start_line":72} -{"id":"rs1:7dc1dbe8d512:func:src/react/login-form.tsx#otpSentNotice@1","labels":["Function"],"content_hash":"47772925fbd776164e3662e83d665698453aacb62860eeeff13c637cc9eb733f","end_line":70,"file_path":"src/react/login-form.tsx","name":"otpSentNotice","qualified_name":"otpSentNotice","signature":"function otpSentNotice(email: string): string {","start_line":68} -{"id":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterCard@1","labels":["Function"],"content_hash":"ed4aa9d087ff807ca6c93693a406597484999091857fea8500b2d2362b4d06f9","end_line":22,"file_path":"src/react/register-form.tsx","name":"RegisterCard","qualified_name":"RegisterCard","signature":"function RegisterCard({ children }: { readonly children: ReactNode }) {","start_line":16} -{"id":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterForm.handleSubmit@1","labels":["Function"],"content_hash":"d31124199e93b952600473854f3aff3d81d101ed14dc4bbde4ec41d554fd4ad7","end_line":72,"file_path":"src/react/register-form.tsx","name":"handleSubmit","qualified_name":"RegisterForm.handleSubmit","signature":"async function handleSubmit(e: React.FormEvent) {","start_line":37} -{"id":"rs1:7dc1dbe8d512:func:src/react/register-form.tsx#RegisterForm@1","labels":["Function"],"content_hash":"5120756d96f23ee18698965dcebe6a57fd6d3afe89468948edd1db1a85ed923e","end_line":180,"file_path":"src/react/register-form.tsx","name":"RegisterForm","qualified_name":"RegisterForm","signature":"function RegisterForm({","start_line":24} -{"id":"rs1:7dc1dbe8d512:func:src/react/squircle-shift.tsx#ShaderPlane@1","labels":["Function"],"content_hash":"d923932fde59435b0c8c67dcd73d3d0036abdc1eba2eb42c106bc795394d4ebe","end_line":222,"file_path":"src/react/squircle-shift.tsx","name":"ShaderPlane","qualified_name":"ShaderPlane","signature":"function ShaderPlane({","start_line":136} -{"id":"rs1:7dc1dbe8d512:func:src/react/squircle-shift.tsx#SquircleShift@1","labels":["Function"],"content_hash":"1d0a8e71faf0dea74ec6c9127fef3296a1299b3c2184923d1b6d9c854393aa0e","end_line":263,"file_path":"src/react/squircle-shift.tsx","name":"SquircleShift","qualified_name":"SquircleShift","signature":"({","start_line":224} -{"id":"rs1:7dc1dbe8d512:func:src/react/squircle-shift.tsx#useMotionEnabled@0","labels":["Function"],"content_hash":"bbdb55afd82646f12c970ff844e2b869acfac9270e38daa1b3607802e8d26922","end_line":27,"file_path":"src/react/squircle-shift.tsx","name":"useMotionEnabled","qualified_name":"useMotionEnabled","signature":"function useMotionEnabled(): boolean {","start_line":13} -{"id":"rs1:7dc1dbe8d512:func:src/react/unauthorized.tsx#UnauthorizedPage@1","labels":["Function"],"content_hash":"2af9fbea25483885f6b2a9e074f9300a1db8574193a586a20a39527ab0364ebc","end_line":131,"file_path":"src/react/unauthorized.tsx","name":"UnauthorizedPage","qualified_name":"UnauthorizedPage","signature":"function UnauthorizedPage({","start_line":48} -{"id":"rs1:7dc1dbe8d512:func:src/types.ts#AuthError.constructor@4","labels":["Function"],"content_hash":"72320bc3bb2a97b66f0286f95edd7206c97af02f3be6043e6d79e2cb6a1414a5","end_line":52,"file_path":"src/types.ts","name":"constructor","qualified_name":"AuthError.constructor","signature":"constructor(","start_line":44} -{"id":"rs1:7dc1dbe8d512:iface:__tests__/reduced-motion.test.tsx#MatchMediaController","labels":["Interface"],"content_hash":"c7b196a531ffc6a50ba261d950bb80032a71440465d83e2f26bd0ddbb3efe656","end_line":56,"file_path":"__tests__/reduced-motion.test.tsx","name":"MatchMediaController","qualified_name":"MatchMediaController","start_line":51} -{"id":"rs1:7dc1dbe8d512:iface:src/auth-store.ts#Identity","labels":["Interface"],"content_hash":"ba4e73dcaad1e250a22c05729aeba21b77a770808d46081d2ead65d860b77535","end_line":28,"file_path":"src/auth-store.ts","name":"Identity","qualified_name":"Identity","start_line":23} -{"id":"rs1:7dc1dbe8d512:iface:src/import-meta-env.d.ts#ImportMeta","labels":["Interface"],"content_hash":"16414b05097540ff5bf70d7f317c3057753244db1aff5f12c514d4260795e9d8","end_line":23,"file_path":"src/import-meta-env.d.ts","name":"ImportMeta","qualified_name":"ImportMeta","start_line":21} -{"id":"rs1:7dc1dbe8d512:iface:src/import-meta-env.d.ts#ImportMetaEnv","labels":["Interface"],"content_hash":"7b46b327a3405664f52e8c62032d64e98fb04d11158638bd8d077148fa3ec3cf","end_line":19,"file_path":"src/import-meta-env.d.ts","name":"ImportMetaEnv","qualified_name":"ImportMetaEnv","start_line":16} -{"id":"rs1:7dc1dbe8d512:iface:src/react/dev-sign-in.tsx#DevSignInBypassProps","labels":["Interface"],"content_hash":"6c4d8840b8130b3d3880c782a982700414dbc76015c8358824402e370ca99297","end_line":49,"file_path":"src/react/dev-sign-in.tsx","name":"DevSignInBypassProps","qualified_name":"DevSignInBypassProps","start_line":39} -{"id":"rs1:7dc1dbe8d512:iface:src/react/login-form.tsx#LoginFormProps","labels":["Interface"],"content_hash":"a00986970dfb549055e671439d6f4e5897e1684a08c8720d34491ce6587d38da","end_line":58,"file_path":"src/react/login-form.tsx","name":"LoginFormProps","qualified_name":"LoginFormProps","start_line":32} -{"id":"rs1:7dc1dbe8d512:iface:src/react/register-form.tsx#RegisterFormProps","labels":["Interface"],"content_hash":"6d17593cd5f6cc72f71ac93ca267026f690395567d3530d1e6a18567df5ce35f","end_line":14,"file_path":"src/react/register-form.tsx","name":"RegisterFormProps","qualified_name":"RegisterFormProps","start_line":9} -{"id":"rs1:7dc1dbe8d512:iface:src/react/squircle-shift.tsx#ShaderPlaneProps","labels":["Interface"],"content_hash":"d980adaed194f62465ed25724c438301bd8c793f9f3a21c3d6152dedc2c00806","end_line":134,"file_path":"src/react/squircle-shift.tsx","name":"ShaderPlaneProps","qualified_name":"ShaderPlaneProps","start_line":117} -{"id":"rs1:7dc1dbe8d512:iface:src/react/squircle-shift.tsx#SquircleShiftProps","labels":["Interface"],"content_hash":"b8704fc2b29133da226eb9e348e1e986dda9d06048061df7a4656987f599e7eb","end_line":11,"file_path":"src/react/squircle-shift.tsx","name":"SquircleShiftProps","qualified_name":"SquircleShiftProps","start_line":8} -{"id":"rs1:7dc1dbe8d512:iface:src/react/unauthorized.tsx#UnauthorizedPageLabels","labels":["Interface"],"content_hash":"bd8ba8c03ef34e9a6082e886302eca9ed8c0e6e7ba496d336940ca24efd741cd","end_line":37,"file_path":"src/react/unauthorized.tsx","name":"UnauthorizedPageLabels","qualified_name":"UnauthorizedPageLabels","start_line":12} -{"id":"rs1:7dc1dbe8d512:iface:src/react/unauthorized.tsx#UnauthorizedPageProps","labels":["Interface"],"content_hash":"a329a3f36d7bc787f0b281c76f1c6796b438fd47b12d27abd0b61d9952fe7743","end_line":46,"file_path":"src/react/unauthorized.tsx","name":"UnauthorizedPageProps","qualified_name":"UnauthorizedPageProps","start_line":39} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthApi","labels":["Interface"],"content_hash":"b2404433e6bee90a57b22215daa746a22300475aa861d5eca3f30b22aef8b78f","end_line":216,"file_path":"src/types.ts","name":"AuthApi","qualified_name":"AuthApi","start_line":210} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthClient","labels":["Interface"],"content_hash":"99c3af7a72994c9a152c3a42fd2d6d1a6c236bd7aa77bd651e0e808c58b94051","end_line":208,"file_path":"src/types.ts","name":"AuthClient","qualified_name":"AuthClient","start_line":206} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthClientConfig","labels":["Interface"],"content_hash":"c411afdd845bf9d0431f8dab207e64de8e4bed55aa3c73b0b62f3ad13adaa95f","end_line":204,"file_path":"src/types.ts","name":"AuthClientConfig","qualified_name":"AuthClientConfig","start_line":200} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthErrorResponse","labels":["Interface"],"content_hash":"a7292ce71d472c11204a2256482ff0e77c52c5aea68ec07f237c239dc371382b","end_line":41,"file_path":"src/types.ts","name":"AuthErrorResponse","qualified_name":"AuthErrorResponse","start_line":37} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthStore","labels":["Interface"],"content_hash":"caec88f9918d24d7111014be09f1ff00d0bd069a7b8a7dfb8c9c8736ccc1387b","end_line":198,"file_path":"src/types.ts","name":"AuthStore","qualified_name":"AuthStore","start_line":137} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthStoreConfig","labels":["Interface"],"content_hash":"9dc081ea4de2d71a6841a0b30c5339169cc1673aaac40b23b0ecc66f2fe8dda7","end_line":59,"file_path":"src/types.ts","name":"AuthStoreConfig","qualified_name":"AuthStoreConfig","start_line":55} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#AuthUser","labels":["Interface"],"content_hash":"b1e49ea966e4cdbf19b08b13dcb8d9cb724b0c3e7477f31ec3fba70988711238","end_line":17,"file_path":"src/types.ts","name":"AuthUser","qualified_name":"AuthUser","start_line":1} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#DashboardLink","labels":["Interface"],"content_hash":"ea65e8560b77698ccefce8eb986f388da19b805443bcd6de25b9487d78939677","end_line":224,"file_path":"src/types.ts","name":"DashboardLink","qualified_name":"DashboardLink","start_line":220} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#DevLoginFailure","labels":["Interface"],"content_hash":"8ad9ca29b0befb9c09d863718e52be90b3a7f68b9e624556983e6d7f1b63e28e","end_line":129,"file_path":"src/types.ts","name":"DevLoginFailure","qualified_name":"DevLoginFailure","start_line":122} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#DevLoginSuccess","labels":["Interface"],"content_hash":"b832ec43e2512494e56a435550dde0e08288a15cbc6de0a593df4bad5dae1a26","end_line":120,"file_path":"src/types.ts","name":"DevLoginSuccess","qualified_name":"DevLoginSuccess","start_line":114} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#RegisterInput","labels":["Interface"],"content_hash":"ed243378b006d3afb761d5532766d6cbb6ae050139ec8a3fca1231e22663f1a5","end_line":24,"file_path":"src/types.ts","name":"RegisterInput","qualified_name":"RegisterInput","start_line":19} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#RequestOtpResponse","labels":["Interface"],"content_hash":"a15ee39ff6cf2e18ace8cc4a0c2c7e8521b65cac3fad3dc7396b9fa0b4843889","end_line":29,"file_path":"src/types.ts","name":"RequestOtpResponse","qualified_name":"RequestOtpResponse","start_line":26} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#SessionClaims","labels":["Interface"],"content_hash":"598263987b713cff9ce894ac955bacdad8f3531516d2d39f6c8cb2b7631f855a","end_line":94,"file_path":"src/types.ts","name":"SessionClaims","qualified_name":"SessionClaims","start_line":80} -{"id":"rs1:7dc1dbe8d512:iface:src/types.ts#VerifyOtpResponse","labels":["Interface"],"content_hash":"be10167a9f925b16ff7a13c0047d9fc9349fe790d9f3bdd9006ef365a2da0951","end_line":35,"file_path":"src/types.ts","name":"VerifyOtpResponse","qualified_name":"VerifyOtpResponse","start_line":31} -{"id":"rs1:7dc1dbe8d512:repo:.","labels":["Repository"],"is_nested":false,"name":"cellarnode-auth","root_path":"."} -{"id":"rs1:7dc1dbe8d512:var:__tests__/auth-store.test.ts#baseMe","labels":["Variable"],"file_path":"__tests__/auth-store.test.ts","kind":"module","name":"baseMe"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#DEV_ONLY_MARKUP","labels":["Variable"],"file_path":"__tests__/dev-bypass-treeshake.test.ts","kind":"const","name":"DEV_ONLY_MARKUP"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#EXTERNALS","labels":["Variable"],"file_path":"__tests__/dev-bypass-treeshake.test.ts","kind":"const","name":"EXTERNALS"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/dev-bypass-treeshake.test.ts#ROOT","labels":["Variable"],"file_path":"__tests__/dev-bypass-treeshake.test.ts","kind":"const","name":"ROOT"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/dev-login.test.ts#OK_BODY","labels":["Variable"],"file_path":"__tests__/dev-login.test.ts","kind":"const","name":"OK_BODY"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/dev-login.test.ts#baseMe","labels":["Variable"],"file_path":"__tests__/dev-login.test.ts","kind":"module","name":"baseMe"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/login-form-responsive.test.tsx#authApi","labels":["Variable"],"file_path":"__tests__/login-form-responsive.test.tsx","kind":"module","name":"authApi"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/login-form-responsive.test.tsx#authStore","labels":["Variable"],"file_path":"__tests__/login-form-responsive.test.tsx","kind":"module","name":"authStore"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/login-form.test.tsx#authApi","labels":["Variable"],"file_path":"__tests__/login-form.test.tsx","kind":"module","name":"authApi"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/login-form.test.tsx#authStore","labels":["Variable"],"file_path":"__tests__/login-form.test.tsx","kind":"module","name":"authStore"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/reduced-motion.test.tsx#matchMediaController","labels":["Variable"],"file_path":"__tests__/reduced-motion.test.tsx","kind":"module","name":"matchMediaController"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/register-form.test.tsx#authApi","labels":["Variable"],"file_path":"__tests__/register-form.test.tsx","kind":"module","name":"authApi"} -{"id":"rs1:7dc1dbe8d512:var:__tests__/unauthorized.test.tsx#dashboardLinks","labels":["Variable"],"file_path":"__tests__/unauthorized.test.tsx","kind":"module","name":"dashboardLinks"} -{"id":"rs1:7dc1dbe8d512:var:src/auth-store.ts#DEFAULT_ACCESS_TOKEN_TTL","labels":["Variable"],"file_path":"src/auth-store.ts","kind":"const","name":"DEFAULT_ACCESS_TOKEN_TTL"} -{"id":"rs1:7dc1dbe8d512:var:src/auth-store.ts#DEV_LOGIN_MESSAGES","labels":["Variable"],"file_path":"src/auth-store.ts","kind":"const","name":"DEV_LOGIN_MESSAGES"} -{"id":"rs1:7dc1dbe8d512:var:src/react/dev-sign-in.tsx#DEV_LOGIN_EMAIL_STORAGE_KEY","labels":["Variable"],"file_path":"src/react/dev-sign-in.tsx","kind":"const","name":"DEV_LOGIN_EMAIL_STORAGE_KEY"} -{"id":"rs1:7dc1dbe8d512:var:src/react/input-otp-slots.tsx#OTP_STYLES","labels":["Variable"],"file_path":"src/react/input-otp-slots.tsx","kind":"const","name":"OTP_STYLES"} -{"id":"rs1:7dc1dbe8d512:var:src/react/login-form.tsx#ERROR_MESSAGES","labels":["Variable"],"file_path":"src/react/login-form.tsx","kind":"const","name":"ERROR_MESSAGES"} -{"id":"rs1:7dc1dbe8d512:var:src/react/login-form.tsx#LazySquircleShift","labels":["Variable"],"file_path":"src/react/login-form.tsx","kind":"module","name":"LazySquircleShift"} -{"id":"rs1:7dc1dbe8d512:var:src/react/squircle-shift.tsx#fragmentShader","labels":["Variable"],"file_path":"src/react/squircle-shift.tsx","kind":"module","name":"fragmentShader"} -{"id":"rs1:7dc1dbe8d512:var:src/react/squircle-shift.tsx#vertexShader","labels":["Variable"],"file_path":"src/react/squircle-shift.tsx","kind":"module","name":"vertexShader"} diff --git a/src/react/login-form.tsx b/src/react/login-form.tsx index df68730..06b85b3 100644 --- a/src/react/login-form.tsx +++ b/src/react/login-form.tsx @@ -279,7 +279,15 @@ export function LoginForm({ authenticatedUserType = null; } - if (authenticatedUserType && authenticatedUserType !== userType) { + if (!authenticatedUserType) { + const msg = "Couldn't verify your account type. Try again."; + authStore.clearAccessToken(); + setDevError(msg); + onError?.({ code: "DEV_LOGIN_USER_TYPE_UNVERIFIED", message: msg }); + return; + } + + if (authenticatedUserType !== userType) { const msg = `This portal is for ${userType} accounts only.`; authStore.clearAccessToken(); setDevError(msg); From 5a8e235bd2213817c1e32049fbeea6e10e1b7fb6 Mon Sep 17 00:00:00 2001 From: mjnong Date: Thu, 13 Aug 2026 08:12:21 +0200 Subject: [PATCH 4/4] [CEL-1364] Review comments: inert test, missing catch, OTP/bypass race, null body, barrel exports, docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bot review follow-ups on PR #15. - Inert test fixed. login-form-dev-bypass seeded the storage key with the address it then asserted, so it passed with rememberDevLoginEmail deleted. It now seeds a STALE address, types a different one, and asserts the typed one was written. Verified by mutation: removing the call fails the test. - handleDevLogin gains a catch. devLogin is optional on AuthStore, so a custom store may reject; clearAccessToken and onLoginSuccess can throw too. From a click handler that was an unhandled rejection — finally re-enabled the button with no message. Failures now route to the same devError/onError channel. - The race the comment claimed impossible is now impossible. The OTP Continue button was disabled on isSubmitting only, so an in-flight devLogin could be overtaken by requestOtp, and the resolving bypass would call onLoginSuccess from the OTP step. isDevSubmitting now participates in both directions. - devLogin no longer rejects on a literal null JSON body. res.json() resolving null parses fine, and extractAccessToken dereferences it — breaking the never-throws contract DevLoginResult promises. Guarded before extraction. - The dev-bypass symbols leave the public barrel. DevSignInBypass, DEV_LOGIN_EMAIL_STORAGE_KEY, readDevLoginEmail and rememberDevLoginEmail were exported from @cellarnode/auth/react, letting a consumer render the bypass or write to localStorage from a production build with no gate at all. Tests already imported the module path; the tree-shaking test is unchanged and still asserts both directions. - Store-side import.meta.env.DEV gate on devLogin declined, deliberately. The gate is server-side and double-enforced (route mounted only when !isProdLike() && ENABLE_TEST_ENDPOINTS=true, each handler re-checking), so in production the route does not exist. A runtime check would eliminate no code — it sits in the same live body — and would introduce import.meta into the bundler-agnostic core, where import.meta.env is undefined under plain Node ESM and reading .DEV throws out of the very method just fixed not to. Documented instead: README, AGENTS.md, CHANGELOG and the JSDoc now state that devLogin and its copy DO ship, and that only the DevSignInBypass component is statically eliminated. - Docs made consistent. Export lists in AGENTS.md and README.md match the new surface; the stale `make build` gate is replaced by the four real CI steps (typecheck, test, build, publint). Gates: npm run typecheck, npm test (93 passed), npm run build, npx publint — all green. --- AGENTS.md | 68 ++++++++++--- CHANGELOG.md | 4 +- README.md | 38 +++++-- __tests__/dev-login.test.ts | 23 +++++ __tests__/login-form-dev-bypass.test.tsx | 121 ++++++++++++++++++++--- src/auth-store.ts | 48 ++++++--- src/react/dev-sign-in.tsx | 21 +++- src/react/index.ts | 18 ++-- src/react/login-form.tsx | 24 ++++- src/types.ts | 25 +++-- 10 files changed, 321 insertions(+), 69 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3ebf96d..03314f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,21 +19,29 @@ Powers OTP auth in: `producer-dashboard`, `cellarnode-importer-dashboard`, `cell ## Commands ```bash -pnpm install -pnpm build # tsc only -pnpm test # vitest run -make build # clean + lint + typecheck + compile (PREFERRED pre-publish gate) +npm install +npm run typecheck # tsc --noEmit +npm test # vitest run +npm run build # tsc +npx publint # package.json / exports lint ``` -Always `make build` before opening a PR or publishing. +Those four are exactly the CI steps — run all four before opening a PR. There is +no Makefile in this repo; an earlier revision of this file recommended +`make build`, which never existed here. ## Exports ``` -@cellarnode/auth # Core: store + client + api + types -@cellarnode/auth/react # LoginForm, RegisterForm, UnauthorizedPage, SquircleShift +@cellarnode/auth # Core: store + client + api + guard helpers + types +@cellarnode/auth/react # LoginForm, RegisterForm, UnauthorizedPage, SquircleShift, + # InputOTP (+ Group / Slot / Separator) ``` +The dev-bypass internals (`DevSignInBypass`, `DEV_LOGIN_EMAIL_STORAGE_KEY`, +`readDevLoginEmail`, `rememberDevLoginEmail`) are NOT exported from +`@cellarnode/auth/react` — see "Dev sign-in bypass" below. + ### Core API - `createAuthStore({ baseUrl })` — token persistence (localStorage in browser; mobile uses an `expo-secure-store` adapter on the consumer side). Also exposes `devLogin(email)` (CEL-1364) — see "Dev sign-in bypass". @@ -52,7 +60,9 @@ src/ ├── auth-store.ts # Token storage abstraction ├── extract-token.ts # JWT extraction helpers ├── types.ts # AuthStore, AuthClient, UserType, ... -└── react/ # LoginForm, RegisterForm, UnauthorizedPage, SquircleShift, DevSignInBypass +└── react/ # LoginForm, RegisterForm, UnauthorizedPage, SquircleShift, + # InputOTP; plus dev-sign-in.tsx (INTERNAL — not in the + # react barrel) ``` ## Dev sign-in bypass (CEL-1364) @@ -67,22 +77,48 @@ Rules that must not drift: - **Additive only.** The OTP flow is untouched, always rendered, never auto-skipped, never auto-redirected. The OTP page stays testable. +- **The two affordances must not race, in BOTH directions.** The bypass button + is disabled while the OTP form is busy, AND the OTP "Continue" button is + disabled while the bypass is in flight. Drop the second half and a developer + can advance to the OTP step mid-`devLogin`, after which the resolving bypass + calls `onLoginSuccess()` from a step that no longer renders it. +- **Every failure reaches the UI.** `handleDevLogin` catches as well as + `finally`s. `devLogin` is optional on `AuthStore`, so a custom store may + reject; from a click handler that would be an unhandled rejection and the + button would silently re-enable with no message. - **Fails closed.** The dev path resolves the user type through a separate `GET /auth/me` (verify-otp gets it inline). If that call fails or answers without a `userType`, the bypass clears the token and errors — an unresolvable type is never treated as a passing portal check. -- **No new env vars.** The frontend gate is the literal `import.meta.env.DEV` - (Vite folds it to `false` in production, so Rollup drops the branch and the - `DevSignInBypass` component with it — pinned by - `__tests__/dev-bypass-treeshake.test.ts`, which bundles the form both ways - through esbuild and greps the output. Note the scope: only the component is - statically eliminated; `readDevLoginEmail` / `rememberDevLoginEmail` sit - behind runtime guards and survive as unreachable code). The backend gate - stays `ENABLE_TEST_ENDPOINTS`. Do not add a `VITE_*` flag. +- **Not exported.** `DevSignInBypass` and the three storage symbols are absent + from `src/react/index.ts` on purpose. The gate is the single + `import.meta.env.DEV` call site inside `LoginForm`; an exported symbol has no + gate, and a consumer could render the bypass or write to `localStorage` from a + production build. Tests import `../src/react/dev-sign-in.js` directly. +- **No new env vars.** The frontend gate is the literal `import.meta.env.DEV`. + The backend gate stays `ENABLE_TEST_ENDPOINTS`. Do not add a `VITE_*` flag. +- **Be exact about what production drops.** Only the `DevSignInBypass` + COMPONENT is statically eliminated (Vite folds the literal, Rollup drops the + branch and the module; pinned by `__tests__/dev-bypass-treeshake.test.ts`, + which bundles the form both ways through esbuild and greps the output). + `authStore.devLogin`, its failure copy, and `readDevLoginEmail` / + `rememberDevLoginEmail` all sit in live function bodies behind runtime guards + and SHIP. Do not write docs or comments claiming otherwise. +- **The gate is the server, not the bundle.** `POST /test/login` is only mounted + when `!isProdLike() && ENABLE_TEST_ENDPOINTS === "true"`, and each handler + re-checks the same predicate, so in production the route does not exist and a + shipped `devLogin` can only resolve `test-endpoints-disabled`. A runtime + `import.meta.env.DEV` check inside `devLogin` was considered and declined: the + core entry is bundler-agnostic (`import.meta.env` is `undefined` under plain + Node ESM, so reading `.DEV` would THROW out of a method contracted never to), + and it would eliminate no code, since the guard sits in the same live body. - **Anti-enumeration (backend T3-1).** `/test/login` returns the SAME 404 for "gate off" and "no such account". `devLogin()` maps it to the single reason `"test-endpoints-disabled"` and frames the copy as "set `ENABLE_TEST_ENDPOINTS=true`" — never as a claim about the address. +- **`devLogin()` never rejects.** Every outcome, including a body that parses as + literal `null`, comes back as a `DevLoginResult`; its only caller is a click + handler with no other error channel. - `devLogin` is **optional** on the `AuthStore` interface so custom store implementations stay source-compatible; `createAuthStore()` always provides it. - Optional prefill: `localStorage["cellarnode.dev.login-email"]`, read and diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ac0b0..1fb124e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ ### Added - `AuthStore.devLogin(email)` (CEL-1364) — LOCAL-DEV helper that mints a session from the backend's `POST /test/login` and adopts the JWE through the same path `verifyOtp` uses (identity fetch, refresh scheduling, `onAccessTokenSet` / `onOrgChange` fan-out). Returns a `DevLoginResult` instead of throwing; the backend's uniform 404 maps to `reason: "test-endpoints-disabled"` with a "set `ENABLE_TEST_ENDPOINTS=true`" hint, never a claim about the address. Optional on the interface, so custom `AuthStore` implementations stay source-compatible. -- `LoginForm` renders a DEV-only "Dev sign-in (skip the code)" control **alongside** the email form — additive, never a replacement, no auto-redirect. Gated on the literal `import.meta.env.DEV`, so production builds tree-shake the control away (asserted against real bundler output, not just the runtime conditional). It applies the same portal guard as the OTP path and fails closed: if `/auth/me` cannot resolve a `userType`, the token is cleared instead of the session standing. No new env vars. -- `DevSignInBypass`, `readDevLoginEmail`, `rememberDevLoginEmail`, `DEV_LOGIN_EMAIL_STORAGE_KEY` from `@cellarnode/auth/react`; `DevLoginResult` / `DevLoginSuccess` / `DevLoginFailure` / `DevLoginFailureReason` types from `@cellarnode/auth`. +- `LoginForm` renders a DEV-only "Dev sign-in (skip the code)" control **alongside** the email form — additive, never a replacement, no auto-redirect. Gated on the literal `import.meta.env.DEV`, so production builds tree-shake the control away (asserted against real bundler output, not just the runtime conditional). It applies the same portal guard as the OTP path and fails closed: if `/auth/me` cannot resolve a `userType`, the token is cleared instead of the session standing. While the bypass is in flight the OTP "Continue" button is disabled, so the two affordances cannot race. No new env vars. +- `DevLoginResult` / `DevLoginSuccess` / `DevLoginFailure` / `DevLoginFailureReason` types from `@cellarnode/auth`. The bypass internals (`DevSignInBypass`, `readDevLoginEmail`, `rememberDevLoginEmail`, `DEV_LOGIN_EMAIL_STORAGE_KEY`) are intentionally NOT exported from `@cellarnode/auth/react` — the DEV gate lives at `LoginForm`'s single call site, and an exported symbol would carry none. Note that `devLogin` itself and its failure copy do ship in production bundles; the gate is the server-side `ENABLE_TEST_ENDPOINTS` mount check, so the route simply does not exist there. ## 0.13.3 diff --git a/README.md b/README.md index f3dc258..087f07c 100644 --- a/README.md +++ b/README.md @@ -40,16 +40,34 @@ import { LoginForm, RegisterForm, UnauthorizedPage } from "@cellarnode/auth/reac email form when `import.meta.env.DEV` is true. It calls `authStore.devLogin(email)`, which POSTs the backend's `/test/login` and adopts the returned JWE through the same path `verifyOtp` uses. The OTP flow is -unchanged and remains the only path in production builds — Vite folds -`import.meta.env.DEV` to `false`, so the control and its module are dropped from -the bundle. There is no env var to set on the frontend. - -The backend side is gated by `ENABLE_TEST_ENDPOINTS=true` (non-production only). +unchanged and remains the only path in production builds. There is no env var to +set on the frontend. + +What is and is not dropped from a production bundle — the distinction matters, +so do not compress it: + +- **Dropped.** The `DevSignInBypass` component and its markup. Vite folds + `import.meta.env.DEV` to `false`, Rollup removes the branch, and the module + goes with it. Pinned by `__tests__/dev-bypass-treeshake.test.ts`, which + bundles the form both ways and greps the output. +- **Kept.** `authStore.devLogin` and its failure copy, plus the + `readDevLoginEmail` / `rememberDevLoginEmail` helpers. All are reached from + live function bodies behind runtime `if` guards, so no bundler can prove them + unreachable. They are inert — the helpers only run inside the DEV branch, and + `devLogin` calls a route that is not mounted in production. + +The security boundary is the **server**, not the bundle. `/test/login` is only +mounted when `NODE_ENV`/`MODE` is non-production **and** +`ENABLE_TEST_ENDPOINTS=true`, and every handler re-checks the same predicate. When it is off, `/test/login` returns a uniform 404 and `devLogin()` resolves to `{ ok: false, reason: "test-endpoints-disabled" }`. That 404 is deliberately identical to the "no local account for this address" case, so neither the helper nor the UI may present it as a statement about the account. +`devLogin()` never rejects; every outcome is a `DevLoginResult`. Callers should +still wrap it, because `devLogin` is optional on the `AuthStore` interface and a +custom store may reject. + ```ts const result = await authStore.devLogin?.("producer@example.com"); if (result?.ok) { @@ -66,8 +84,14 @@ Add this to your CSS file so Tailwind picks up utility classes from the package: ## Exports -- `@cellarnode/auth` — Core: `createAuthStore`, `createAuthClient`, `createAuthApi`, `validateUserType`, types -- `@cellarnode/auth/react` — React: `LoginForm`, `RegisterForm`, `UnauthorizedPage`, `SquircleShift` +- `@cellarnode/auth` — Core: `createAuthStore`, `createAuthClient`, `createAuthApi`, `validateUserType`, `hasEntitlement`, `extractAccessToken`, types (incl. `DevLoginResult`) +- `@cellarnode/auth/react` — React: `LoginForm`, `RegisterForm`, `UnauthorizedPage`, `SquircleShift`, `InputOTP` (+ `Group` / `Slot` / `Separator`) + +`DevSignInBypass`, `DEV_LOGIN_EMAIL_STORAGE_KEY`, `readDevLoginEmail` and +`rememberDevLoginEmail` are **deliberately not exported**. They are internals of +`LoginForm`'s `import.meta.env.DEV` branch; the gate lives at that one call site, +and an exported symbol carries no gate — a consumer importing it could render the +bypass UI, or write a sign-in address to `localStorage`, from a production build. ## License diff --git a/__tests__/dev-login.test.ts b/__tests__/dev-login.test.ts index eb8e1f7..e021a15 100644 --- a/__tests__/dev-login.test.ts +++ b/__tests__/dev-login.test.ts @@ -237,4 +237,27 @@ describe("createAuthStore — devLogin failure mapping (CEL-1364)", () => { expect(store.getAccessToken()).toBeNull(); expect(store.hasAccessToken()).toBe(false); }); + + it("reports malformed-response for a literal null body instead of rejecting", async () => { + // Given: a 200 whose body is valid JSON `null`. It PARSES, so the + // json()-throws branch never runs, and `extractAccessToken` would + // dereference null and throw straight out of `devLogin` — breaking the + // never-throws contract that `DevLoginResult` promises its only caller (a + // click handler with no other error channel). + global.fetch = routedFetch({ + devLogin: { body: null }, + me: baseMe, + }) as unknown as typeof fetch; + const store = createAuthStore({ baseUrl: "http://localhost:4000" }); + + // When / Then: a resolved failure, never a rejection. + await expect(store.devLogin?.("dev@example.com")).resolves.toMatchObject({ + ok: false, + reason: "malformed-response", + status: 200, + }); + await flush(); + expect(store.getAccessToken()).toBeNull(); + expect(store.hasAccessToken()).toBe(false); + }); }); diff --git a/__tests__/login-form-dev-bypass.test.tsx b/__tests__/login-form-dev-bypass.test.tsx index c57bcc1..dc3b51c 100644 --- a/__tests__/login-form-dev-bypass.test.tsx +++ b/__tests__/login-form-dev-bypass.test.tsx @@ -1,6 +1,12 @@ // @vitest-environment happy-dom -import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { LoginForm } from "../src/react/login-form.js"; import { DEV_LOGIN_EMAIL_STORAGE_KEY } from "../src/react/dev-sign-in.js"; @@ -138,28 +144,121 @@ describe("LoginForm dev bypass — DEV builds (CEL-1364)", () => { }); }); - it("signs in through devLogin on click and remembers the address", async () => { - // Given: a remembered address so the button is enabled without typing. - window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "dev@example.com"); + it("signs in as the address in the form and remembers THAT address", async () => { + // Given: a STALE remembered address, which the DEV prefill loads. + window.localStorage.setItem(DEV_LOGIN_EMAIL_STORAGE_KEY, "stale@example.com"); const props = buildProps(); renderLogin(props); - const button = await waitFor(() => { - const el = screen.getByRole("button", { name: /dev sign-in/i }) as HTMLButtonElement; - expect(el.disabled).toBe(false); + const input = await waitFor(() => { + const el = screen.getByLabelText(/email address/i) as HTMLInputElement; + expect(el.value).toBe("stale@example.com"); return el; }); + // ...and the developer types a DIFFERENT one. Seeding the key with the very + // address the assertion then expects is what made the earlier version of + // this test inert — it passed with `rememberDevLoginEmail` deleted. The + // seed and the expectation must DISAGREE, so that only the code under test + // can reconcile them. + fireEvent.change(input, { target: { value: "fresh@example.com" } }); + + const button = screen.getByRole("button", { + name: /dev sign-in/i, + }) as HTMLButtonElement; + expect(button.disabled).toBe(false); + // When: the developer clicks the bypass. button.click(); - // Then: the store helper mints the session and the consumer's success - // callback runs — same terminal behaviour as a verified OTP. + // Then: it signs in as the TYPED address, and that address replaces the + // stale one in storage. + await waitFor(() => { + expect(props.authStore.devLogin).toHaveBeenCalledWith("fresh@example.com"); + expect(props.onLoginSuccess).toHaveBeenCalledTimes(1); + }); + expect(window.localStorage.getItem(DEV_LOGIN_EMAIL_STORAGE_KEY)).toBe( + "fresh@example.com", + ); + }); + + it("surfaces an error when devLogin REJECTS rather than resolving a failure", async () => { + // Given: a custom AuthStore that throws. `devLogin` is OPTIONAL on the + // interface, so nothing forces an implementation to resolve DevLoginResult. + const onError = vi.fn(); + const props = buildProps({ initialEmail: "dev@example.com", onError }); + props.authStore.devLogin = vi.fn(async (): Promise => { + throw new Error("store exploded"); + }); + renderLogin(props); + + const button = screen.getByRole("button", { + name: /dev sign-in/i, + }) as HTMLButtonElement; + + // When: the developer clicks the bypass. + button.click(); + + // Then: the failure reaches the SAME channel every other failure uses. Were + // the rejection to escape the handler, `finally` would still re-enable the + // button and this alert would never exist — a button that silently does + // nothing is precisely the symptom this pins. + const alert = await screen.findByRole("alert"); + expect(alert.textContent).toMatch(/store exploded/); + expect(onError).toHaveBeenCalledWith( + expect.objectContaining({ code: "DEV_LOGIN_UNEXPECTED" }), + ); + expect(props.onLoginSuccess).not.toHaveBeenCalled(); + + // And: the button is usable again, so a retry is one click away. + await waitFor(() => { + const el = screen.getByRole("button", { + name: /dev sign-in/i, + }) as HTMLButtonElement; + expect(el.disabled).toBe(false); + }); + }); + + it("blocks the OTP submit while the bypass is in flight, so the two cannot race", async () => { + // Given: a devLogin that never settles on its own. + let release: (result: DevLoginResult) => void = () => {}; + const pending = new Promise((resolve) => { + release = resolve; + }); + const props = buildProps({ initialEmail: "dev@example.com" }); + props.authStore.devLogin = vi.fn(() => pending); + renderLogin(props); + + // When: the bypass is clicked and left in flight. + ( + screen.getByRole("button", { name: /dev sign-in/i }) as HTMLButtonElement + ).click(); + + // Then: "Continue" is disabled too. Without the reciprocal guard, requesting + // an OTP here would advance to the OTP step, and the resolving bypass would + // call onLoginSuccess() from a step that no longer renders it — the race + // `DevSignInBypassProps.disabled` claims cannot happen. + const cont = await waitFor(() => { + const el = screen.getByRole("button", { + name: /continue/i, + }) as HTMLButtonElement; + expect(el.disabled).toBe(true); + return el; + }); + cont.click(); + expect(props.authApi.requestOtp).not.toHaveBeenCalled(); + + // Cleanup: settle the promise so the component is not left mid-update. + release({ + ok: true, + accessToken: "jwe.dev.token", + expiresIn: 900, + userId: "user_dev", + orgId: "org_dev", + }); await waitFor(() => { - expect(props.authStore.devLogin).toHaveBeenCalledWith("dev@example.com"); expect(props.onLoginSuccess).toHaveBeenCalledTimes(1); }); - expect(window.localStorage.getItem(DEV_LOGIN_EMAIL_STORAGE_KEY)).toBe("dev@example.com"); }); it("surfaces the gate-off hint when the backend 404s, without blaming the address", async () => { diff --git a/src/auth-store.ts b/src/auth-store.ts index cd477f4..b91a0d9 100644 --- a/src/auth-store.ts +++ b/src/auth-store.ts @@ -3,6 +3,7 @@ import type { AuthStore, AuthStoreConfig, AuthUser, + DevLoginFailure, DevLoginResult, LogoutListener, OrgChangeListener, @@ -59,6 +60,15 @@ const DEFAULT_ACCESS_TOKEN_TTL = 900; * else. The backend deliberately returns an identical 404 for "gate off" and * "no such user" (T3-1), so any copy that named the account would be both a * guess and a weakening of that contract. + * + * These strings SHIP in production bundles — they are referenced from + * `devLogin`'s live body, which no bundler can prove unreachable. The + * `ENABLE_TEST_ENDPOINTS` mention is therefore public, which is fine: the flag + * is documented in this package's README and in the backend repo, and knowing + * its name grants nothing when the route is not mounted. Moving the copy behind + * the DEV-only React module would eliminate it, but only by taking the + * ready-to-render `message` off `DevLoginFailure` — a public-API change, not a + * review fixup. */ const DEV_LOGIN_MESSAGES = { "test-endpoints-disabled": @@ -370,26 +380,36 @@ export function createAuthStore(config: AuthStoreConfig): AuthStore { }; } - let json: Record; + // Built once: three distinct malformed shapes converge on it below. + const malformed: DevLoginFailure = { + ok: false, + reason: "malformed-response", + status: res.status, + message: DEV_LOGIN_MESSAGES["malformed-response"], + }; + + let parsed: unknown; try { - json = (await res.json()) as Record; + parsed = await res.json(); } catch { - return { - ok: false, - reason: "malformed-response", - status: res.status, - message: DEV_LOGIN_MESSAGES["malformed-response"], - }; + return malformed; } + // `res.json()` resolving is not the same as "we got an object". A body of + // literal `null` (or a bare string/number) parses fine, and + // `extractAccessToken` dereferences its argument — so passing `null` + // through would THROW out of a function whose result type promises it + // never does, leaving the DEV button with no error channel at all. + // Arrays fall through: they are objects, carry no token, and reach the + // same `malformed` below. + if (parsed === null || typeof parsed !== "object") { + return malformed; + } + const json = parsed as Record; + const token = extractAccessToken(json); if (!token) { - return { - ok: false, - reason: "malformed-response", - status: res.status, - message: DEV_LOGIN_MESSAGES["malformed-response"], - }; + return malformed; } const expiresIn = diff --git a/src/react/dev-sign-in.tsx b/src/react/dev-sign-in.tsx index 4a27559..154fa06 100644 --- a/src/react/dev-sign-in.tsx +++ b/src/react/dev-sign-in.tsx @@ -5,10 +5,16 @@ import { FlaskConical, Loader2, Zap } from "lucide-react"; import { clsx } from "clsx"; /** - * localStorage key holding the last address used with the dev bypass - * (CEL-1364). DEV-only convenience: it prefills the sign-in email so a fresh - * tab is one click from a session. Read and written exclusively behind - * `import.meta.env.DEV`, so production builds never touch it. + * INTERNAL (CEL-1364) — nothing in this module is re-exported from + * `@cellarnode/auth/react`. `LoginForm` is the only supported entry point; see + * the note in `src/react/index.ts`. + * + * localStorage key holding the last address used with the dev bypass. DEV-only + * convenience: it prefills the sign-in email so a fresh tab is one click from a + * session. Both call sites sit inside `LoginForm`'s `import.meta.env.DEV` + * branch, so production never reaches them — though the helpers themselves are + * called from live function bodies and therefore stay in the bundle as + * unreachable code (see `__tests__/dev-bypass-treeshake.test.ts`). */ export const DEV_LOGIN_EMAIL_STORAGE_KEY = "cellarnode.dev.login-email"; @@ -41,7 +47,12 @@ export interface DevSignInBypassProps { email: string; /** True while the bypass request is in flight. */ isSubmitting: boolean; - /** True while the OTP form is busy, so the two affordances can't race. */ + /** + * True while the OTP form is busy. The guard is reciprocal — `LoginForm` + * also feeds its own dev-submitting state into the OTP submit button — which + * is what actually makes "the two affordances can't race" true rather than + * aspirational. + */ disabled?: boolean; /** Developer-facing failure message from `authStore.devLogin()`. */ error?: string; diff --git a/src/react/index.ts b/src/react/index.ts index e625dd5..396304c 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -1,13 +1,17 @@ "use client"; export { LoginForm, type LoginFormProps } from "./login-form.js"; -export { - DevSignInBypass, - type DevSignInBypassProps, - DEV_LOGIN_EMAIL_STORAGE_KEY, - readDevLoginEmail, - rememberDevLoginEmail, -} from "./dev-sign-in.js"; +/* + * `./dev-sign-in.js` is DELIBERATELY not re-exported (CEL-1364). + * + * `DevSignInBypass`, `DEV_LOGIN_EMAIL_STORAGE_KEY`, `readDevLoginEmail` and + * `rememberDevLoginEmail` are internals of `LoginForm`'s + * `import.meta.env.DEV` branch. Naming them here would hand a consumer a + * supported way to render the bypass UI, or to persist a sign-in address to + * `localStorage`, from a PRODUCTION build — the gate lives at the single call + * site inside `LoginForm`, and an exported symbol has no gate at all. + * Import the module path directly if you are testing this package. + */ export { RegisterForm, type RegisterFormProps } from "./register-form.js"; export { UnauthorizedPage, diff --git a/src/react/login-form.tsx b/src/react/login-form.tsx index 06b85b3..2b2b685 100644 --- a/src/react/login-form.tsx +++ b/src/react/login-form.tsx @@ -247,6 +247,11 @@ export function LoginForm({ * the token first and clear it when the portal check rejects. If that order * is ever tightened, tighten BOTH paths — fixing only this one hides the * remaining hole in the OTP path. + * + * Every exit goes through `setDevError` (rendered as the bypass's `role= + * "alert"`), including the catch-all: a click handler is not an awaited + * caller, so anything that escapes here is an unhandled rejection nobody + * reads. */ async function handleDevLogin() { if (!authStore.devLogin) return; @@ -301,6 +306,17 @@ export function LoginForm({ rememberDevLoginEmail(normalizedEmail); onLoginSuccess(); + } catch (err) { + // `devLogin` is OPTIONAL on `AuthStore`, so a custom implementation is + // free to REJECT instead of resolving a `DevLoginFailure`; `getMe` is + // already wrapped, but `clearAccessToken` and `onLoginSuccess` can throw + // too. Without this catch the rejection escapes the click handler as an + // unhandled rejection — `finally` still re-enables the button, so the + // developer sees a form that silently did nothing. + const detail = err instanceof Error ? err.message : String(err); + const msg = `Dev sign-in failed unexpectedly: ${detail}`; + setDevError(msg); + onError?.({ code: "DEV_LOGIN_UNEXPECTED", message: msg }); } finally { setIsDevSubmitting(false); } @@ -424,10 +440,16 @@ export function LoginForm({ )} + {/* `isDevSubmitting` belongs here, not just on the bypass + button. The bypass is already disabled while the OTP form + is busy; without the reciprocal guard a developer could + request an OTP mid-`devLogin` and advance to the OTP step, + and the resolving bypass would then call + `onLoginSuccess()` from a step that no longer shows it. */}