diff --git a/apps/accounts/src/accounts.app.ts b/apps/accounts/src/accounts.app.ts index 8100f2c..d010dec 100644 --- a/apps/accounts/src/accounts.app.ts +++ b/apps/accounts/src/accounts.app.ts @@ -119,13 +119,17 @@ function toAccountDto(account: Account) { /** * Project a stored account into the private self DTO (the /account/me shape) — * the public DTO plus owner-only fields. `juniorState`/`parentAccountId` are - * OMITTED when null (emitting `null` makes the client's enum parser throw); - * `email`/`birthday` are kept as null (not enums, so null is fine). + * OMITTED when null (emitting `null` makes the client's enum parser throw). + * + * An unset `email` is `""`, never null — same as `bio`. Two reasons: the client reads + * it as a string, and this DTO also rides the `SelfAccountUpdate` hub frame, where the + * hub DROPS null values from `Msg` — so a null email doesn't arrive as null, it + * vanishes from the frame entirely. */ function toSelfAccountDto(account: Account) { return { ...toAccountDto(account), - email: account.email ?? null, + email: account.email ?? '', // @todo he game client needs this to be set. I forget how birthdays were set, so for now // everyone can be old. birthday: '1904-01-01T00:00:00.000Z', diff --git a/apps/accounts/src/openapi.ts b/apps/accounts/src/openapi.ts index a5f501f..d4a2ac0 100644 --- a/apps/accounts/src/openapi.ts +++ b/apps/accounts/src/openapi.ts @@ -77,12 +77,16 @@ export const AccountDto = z.object({ /** * The private self DTO (`toSelfAccountDto`, the `/account/me` shape) — the public DTO * plus owner-only fields. `juniorState`/`parentAccountId` are omitted entirely when - * unset (emitting `null` makes the client's enum parser throw); `email`/`birthday` are - * kept as nullable since they aren't enums. + * unset (emitting `null` makes the client's enum parser throw). */ export const SelfAccountDto = AccountDto.extend({ - email: z.string().nullable(), - birthday: z.null().describe('Always null — birthday is not stored'), + email: z + .string() + .describe( + '"" when unset — never null: the client reads it as a string, and the hub frame this ' + + 'DTO also rides drops null values outright' + ), + birthday: z.iso.datetime().describe('A fixed placeholder — birthdays are not stored'), availableUsernameChanges: z.int().describe('Remaining username changes'), }) diff --git a/apps/accounts/src/test/integration/api.test.ts b/apps/accounts/src/test/integration/api.test.ts index 143df0b..686f301 100644 --- a/apps/accounts/src/test/integration/api.test.ts +++ b/apps/accounts/src/test/integration/api.test.ts @@ -161,6 +161,9 @@ describe('auth-gated endpoints', () => { personalPronouns: 0, identityFlags: 0, availableUsernameChanges: 1, + // An unset email is "", not null — the client reads it as a string, and the + // hub frame this DTO also rides drops null values outright. + email: '', }) // juniorState + parentAccountId must be omitted when null, not emitted as // null, or the client's enum parser throws on `juniorState`. `phone` isn't