diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 171a2240ad..58229b3deb 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -1,4 +1,4 @@ -import {type DidString} from '@atproto/syntax' +import {isDidString} from '@atproto/lex' import {z} from 'zod' import {deviceLanguageCodes, deviceLocales} from '#/locale/deviceLocales' @@ -8,22 +8,23 @@ import {PlatformInfo} from '../../../modules/expo-bluesky-swiss-army' const externalEmbedOptions = ['show', 'hide'] as const -/** - * Types a persisted string field with a branded type WITHOUT validating the - * brand at runtime. Persisted values predate the brands and must never fail - * schema validation over one (that would drop the account on upgrade). - */ -function unvalidatedBranded(): z.ZodType { - return z.string() as unknown as z.ZodType -} - /** * A account persisted to storage. Stored in the `accounts[]` array. Contains * base account info and access tokens. */ const accountSchema = z.object({ service: z.string(), - did: unvalidatedBranded(), + /** + * Genuinely validated, not just branded: the refinement rejects malformed + * values at runtime and narrows the inferred type to `DidString`. + * + * Weigh any further tightening of this field carefully. One failing field + * fails the whole root schema, and {@link tryParse} then discards the ENTIRE + * persisted state - every account and every preference - so the app boots + * logged out with defaults. Persisted dids come from com.atproto.server + * responses and are always canonical, so this particular check is safe. + */ + did: z.string().refine(isDidString), handle: z.string(), email: z.string().optional(), emailConfirmed: z.boolean().optional(),