{"record":{"id":"0ab8213294691c0a","repo":"koala73/worldmonitor","slug":"digesttimezone-must-be-a-valid-iana-timezone-e-g","errorCode":null,"errorMessage":"digestTimezone must be a valid IANA timezone (e.g. America/New_York)","messagePattern":"digestTimezone must be a valid IANA timezone \\(e\\.g\\. America/New_York\\)","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/alertRules.ts","lineNumber":293,"sourceCode":"    variant: v.string(),\n    digestMode: digestModeValidator,\n    digestHour: v.optional(v.number()),\n    digestTimezone: v.optional(v.string()),\n  },\n  handler: async (ctx, args) => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) throw new ConvexError(\"UNAUTHENTICATED\");\n    const userId = identity.subject;\n    await assertProEntitlement(ctx, userId);\n\n    if (args.digestHour !== undefined && (args.digestHour < 0 || args.digestHour > 23 || !Number.isInteger(args.digestHour))) {\n      throw new ConvexError(\"digestHour must be an integer 0–23\");\n    }\n    if (args.digestTimezone !== undefined) {\n      try {\n        Intl.DateTimeFormat(undefined, { timeZone: args.digestTimezone });\n      } catch {\n        throw new ConvexError(\"digestTimezone must be a valid IANA timezone (e.g. America/New_York)\");\n      }\n    }\n\n    const existing = await ctx.db\n      .query(\"alertRules\")\n      .withIndex(\"by_user_variant\", (q) =>\n        q.eq(\"userId\", userId).eq(\"variant\", args.variant),\n      )\n      .unique();\n\n    const pair = resolveEffectivePair({\n      incomingDigestMode: args.digestMode,\n      existing: existing ?? undefined,\n    });\n    assertCompatibleDeliveryMode(pair);\n\n    const now = Date.now();\n    const patch = {","sourceCodeStart":275,"sourceCodeEnd":311,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/alertRules.ts#L275-L311","documentation":"Thrown by setDigestSettings when args.digestTimezone is present but Intl.DateTimeFormat rejects it as a timeZone option — i.e. it is not a valid IANA timezone identifier (e.g. 'America/New_York', 'Europe/London'). The check uses the runtime's Intl database, so the validation is as authoritative as the host environment.","triggerScenarios":"Calling setDigestSettings with digestTimezone set to an abbreviation ('EST', 'PST'), a fixed offset ('UTC-5', '+05:30'), a misspelled zone ('America/New_York '), or any non-IANA string.","commonSituations":"Client deriving timezone from Date.getTimezoneOffset() and converting to an offset string instead of resolving the IANA name; user typing a custom timezone; trailing whitespace; copy-paste from a non-IANA source.","solutions":["Resolve the IANA timezone client-side via Intl.DateTimeFormat().resolvedOptions().timeZone and pass that value.","If accepting user input, validate against a known IANA list (e.g. Intl.supportedValuesOf('timeZone')) before submitting.","Trim whitespace before submission."],"exampleFix":"// before\nawait setDigestSettings({ variant, digestMode: 'daily', digestTimezone: 'EST' });\n// after\nconst tz = Intl.DateTimeFormat().resolvedOptions().timeZone; // e.g. 'America/New_York'\nawait setDigestSettings({ variant, digestMode: 'daily', digestTimezone: tz });","handlingStrategy":"validation","validationCode":"function isValidIanaTimezone(tz) {\n  if (typeof tz !== 'string') return false;\n  try {\n    Intl.DateTimeFormat(undefined, { timeZone: tz });\n    return true;\n  } catch {\n    return false;\n  }\n}\nif (args.digestTimezone && !isValidIanaTimezone(args.digestTimezone)) {\n  // reject before submit\n}","typeGuard":"function isIanaTimezone(tz): tz is string {\n  if (typeof tz !== 'string') return false;\n  try { Intl.DateTimeFormat(undefined, { timeZone: tz }); return true; }\n  catch { return false; }\n}","tryCatchPattern":"try {\n  await setDigestSettings(args);\n} catch (e) {\n  if (e instanceof ConvexError && /valid IANA timezone/.test(String(e.message))) {\n    // fall back to browser tz: args.digestTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone\n  } else throw e;\n}","preventionTips":["Derive timezone from Intl.DateTimeFormat().resolvedOptions().timeZone rather than offsets.","For user-supplied zones, validate against Intl.supportedValuesOf('timeZone').","Trim whitespace before submission."],"tags":["convex","validation","notifications","datetime","timezone"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}