{"version":3,"sources":["email-core.mjs"],"sourcesContent":["// KUBICOR EMAILS: the rules both ends must agree on (the owner's \"Kubicor Emails\" decisions, Oct 11).\n//\n// Byte-identical in public/ and functions/lib/ (generate-version-hash.js holds them equal). The page reads it to draw\n// Settings → Email and to decide whether the signup step offers the news box; the functions read it to validate what\n// Settings sends, to decide whether an event is worth an email, and to keep news away from anyone under 16. Pure, so\n// tests/emails.test.mjs can pin it.\n//\n// THREE CLASSES OF EMAIL (decision 6):\n//   ACCOUNT   about the account itself: always sent, no unsubscribe (welcome, a refund or dispute, the account deleted,\n//             the terms changed, a gift). Never held back or batched.\n//   ACTIVITY  about something that happened in the game while the player was away. Each kind below is switchable in\n//             Settings → Email, with the default the owner chose, and every one carries a one-click unsubscribe. Queued,\n//             batched and capped (decision 8), and skipped when a push for the same event reached a device (decision 9).\n//   NEWS      about Kubicor itself: the launch (to those who ticked the signup box, users/{uid}.emailOptIn) and the\n//             occasional update (off until switched on). Never to a player under 16.\n// Purchase receipts are Stripe's (the tokens decision), so there is no receipt here.\n\n// THE ACTIVITY KINDS, WITH THEIR DEFAULTS (decision 6). Off means no email of that kind, whatever happens.\nexport const EMAIL_KINDS = Object.freeze({\n  trades:      Object.freeze({ def: true,  label: 'Trades: you are outbid, an auction you are in ends, a world of yours sells' }),\n  vending:     Object.freeze({ def: true,  label: 'Vending machines: yesterday’s sales, sold out, a buy order filled' }),\n  friends:     Object.freeze({ def: true,  label: 'Friend requests, and requests accepted' }),\n  auctionSoon: Object.freeze({ def: false, label: 'An auction you bid on ends within the hour and you are no longer top' }),\n  builder:     Object.freeze({ def: false, label: 'You are made a builder on a world, or no longer one' }),\n  visitors:    Object.freeze({ def: false, label: 'Once a day: who visited your worlds' }),\n  digest:      Object.freeze({ def: false, label: 'Once a week: your worlds’ seasons, visitors, sales and what’s new' }),\n});\nexport const EMAIL_KIND_IDS = Object.freeze(Object.keys(EMAIL_KINDS));\n// THE NEWS. `news` is the signup box (\"Send me the occasional email about Kubicor news\"), stored where it always was,\n// users/{uid}.emailOptIn, and it is what the launch email goes to. `updates` is the occasional game update, off until a\n// player switches it on, kept with the activity kinds in users/{uid}.emailPrefs.\nexport const NEWS_LABEL = 'Send me the occasional email about Kubicor news';\nexport const UPDATES_LABEL = 'Game updates: what is new in Kubicor, a few times a year';\nexport const UPDATES_DEFAULT = false;\n// Every key users/{uid}.emailPrefs may hold.\nexport const EMAIL_PREF_IDS = Object.freeze([...EMAIL_KIND_IDS, 'updates']);\n// The kinds an unsubscribe link can name: an activity kind, the news, or the updates.\nexport const UNSUB_KINDS = Object.freeze([...EMAIL_KIND_IDS, 'news', 'updates']);\n// What a footer and the unsubscribe page call each kind: \"Unsubscribed from trades emails\".\nexport const KIND_NOUN = Object.freeze({ trades: 'trades', vending: 'vending machine', friends: 'friends',\n  auctionSoon: 'auction-ending', builder: 'builder', visitors: 'visitors', digest: 'weekly digest', news: 'Kubicor news', updates: 'game update' });\n\n// The stored preferences, read with every default filled in. Anything unrecognised is ignored rather than trusted.\nexport function emailPrefsOf(raw) {\n  const r = (raw && typeof raw === 'object') ? raw : {};\n  const out = {};\n  for (const k of EMAIL_KIND_IDS) out[k] = typeof r[k] === 'boolean' ? r[k] : EMAIL_KINDS[k].def;\n  out.updates = typeof r.updates === 'boolean' ? r.updates : UPDATES_DEFAULT;\n  return out;\n}\n// What is wrong with a change Settings sends, or null. Only known keys, only booleans.\nexport function emailPrefsProblem(p) {\n  if (!p || typeof p !== 'object' || Array.isArray(p)) return 'Send the preferences as an object.';\n  const keys = Object.keys(p);\n  if (!keys.length || keys.length > EMAIL_PREF_IDS.length) return 'Nothing to change.';\n  for (const k of keys) {\n    if (!EMAIL_PREF_IDS.includes(k)) return 'Not a kind of email: ' + String(k).slice(0, 20);\n    if (typeof p[k] !== 'boolean') return 'Each kind is on or off.';\n  }\n  return null;\n}\nexport const mergeEmailPrefs = (current, patch) => emailPrefsOf({ ...emailPrefsOf(current), ...(patch || {}) });\n\n// HOW OLD, from the month and year of birth the terms step records: whole years, month-accurate, exactly as acceptTerms\n// counts them for the minimum age. NaN when either is missing, which every check below reads as \"too young\".\nexport function ageOf(birthYear, birthMonth, now = new Date()) {\n  const y = Math.floor(Number(birthYear) || 0), m = Math.floor(Number(birthMonth) || 0);\n  if (!(y >= 1900) || !(m >= 1 && m <= 12)) return NaN;\n  let age = now.getUTCFullYear() - y;\n  if (now.getUTCMonth() + 1 < m) age -= 1;\n  return age;\n}\n// NEWS IS FOR 16 AND OVER (decision 7). Under 16 the signup box is never shown, the Settings switch is not offered, and\n// no news email is ever sent, whatever was stored. 16 is the GDPR's default age of digital consent: below it, consent to\n// marketing is a parent's to give in much of Europe, and the game does not ask parents.\nexport const NEWS_MIN_AGE = 16;\nexport const newsAllowedAt = (birthYear, birthMonth, now) => ageOf(birthYear, birthMonth, now) >= NEWS_MIN_AGE;\nexport const newsAllowed = (user, now) => !!user && newsAllowedAt(user.birthYear, user.birthMonth, now);\n\n// BATCHED, NEVER WHILE PLAYING, AND CAPPED (decision 8). An activity event waits EMAIL_DELAY_MS in email_queue/{uid}, and\n// is dropped if the player comes back in that time; whatever is still waiting then goes out as one email; no player gets\n// more than EMAIL_DAILY_CAP activity emails in a (UTC) day. Account email is never held back.\nexport const EMAIL_DELAY_MS = 15 * 60e3;\nexport const EMAIL_DAILY_CAP = 3;\n\n// WHERE AN EMAIL'S BUTTON TAKES YOU: the game's own deep links (push-core.mjs worldUrl, marketUrl, FRIENDS_URL), plus\n// Settings → Email itself, which the page opens on ?tab=email (app.js, beside ?tab=support).\nexport const EMAIL_SETTINGS_URL = '/?tab=email';\n"],"mappings":"AAkBO,aAAM,YAAc,OAAO,OAAO,CACvC,OAAa,OAAO,OAAO,CAAE,IAAK,GAAO,MAAO,4EAA6E,CAAC,EAC9H,QAAa,OAAO,OAAO,CAAE,IAAK,GAAO,MAAO,mEAAoE,CAAC,EACrH,QAAa,OAAO,OAAO,CAAE,IAAK,GAAO,MAAO,wCAAyC,CAAC,EAC1F,YAAa,OAAO,OAAO,CAAE,IAAK,GAAO,MAAO,sEAAuE,CAAC,EACxH,QAAa,OAAO,OAAO,CAAE,IAAK,GAAO,MAAO,qDAAsD,CAAC,EACvG,SAAa,OAAO,OAAO,CAAE,IAAK,GAAO,MAAO,qCAAsC,CAAC,EACvF,OAAa,OAAO,OAAO,CAAE,IAAK,GAAO,MAAO,mEAAoE,CAAC,CACvH,CAAC,EACY,eAAiB,OAAO,OAAO,OAAO,KAAK,WAAW,CAAC,EAIvD,WAAa,kDACb,cAAgB,2DAChB,gBAAkB,GAElB,eAAiB,OAAO,OAAO,CAAC,GAAG,eAAgB,SAAS,CAAC,EAE7D,YAAc,OAAO,OAAO,CAAC,GAAG,eAAgB,OAAQ,SAAS,CAAC,EAElE,UAAY,OAAO,OAAO,CAAE,OAAQ,SAAU,QAAS,kBAAmB,QAAS,UAC9F,YAAa,iBAAkB,QAAS,UAAW,SAAU,WAAY,OAAQ,gBAAiB,KAAM,eAAgB,QAAS,aAAc,CAAC,EAG3I,gBAAS,aAAa,IAAK,CAChC,MAAM,EAAK,KAAO,OAAO,KAAQ,SAAY,IAAM,CAAC,EAC9C,IAAM,CAAC,EACb,UAAW,KAAK,eAAgB,IAAI,CAAC,EAAI,OAAO,EAAE,CAAC,GAAM,UAAY,EAAE,CAAC,EAAI,YAAY,CAAC,EAAE,IAC3F,WAAI,QAAU,OAAO,EAAE,SAAY,UAAY,EAAE,QAAU,gBACpD,GACT,CAEO,gBAAS,kBAAkB,EAAG,CACnC,GAAI,CAAC,GAAK,OAAO,GAAM,UAAY,MAAM,QAAQ,CAAC,EAAG,MAAO,qCAC5D,MAAM,KAAO,OAAO,KAAK,CAAC,EAC1B,GAAI,CAAC,KAAK,QAAU,KAAK,OAAS,eAAe,OAAQ,MAAO,qBAChE,UAAW,KAAK,KAAM,CACpB,GAAI,CAAC,eAAe,SAAS,CAAC,EAAG,MAAO,wBAA0B,OAAO,CAAC,EAAE,MAAM,EAAG,EAAE,EACvF,GAAI,OAAO,EAAE,CAAC,GAAM,UAAW,MAAO,yBACxC,CACA,OAAO,IACT,CACO,aAAM,gBAAkB,CAAC,QAAS,QAAU,aAAa,CAAE,GAAG,aAAa,OAAO,EAAG,GAAI,OAAS,CAAC,CAAG,CAAC,EAIvG,gBAAS,MAAM,UAAW,WAAY,IAAM,IAAI,KAAQ,CAC7D,MAAM,EAAI,KAAK,MAAM,OAAO,SAAS,GAAK,CAAC,EAAG,EAAI,KAAK,MAAM,OAAO,UAAU,GAAK,CAAC,EACpF,GAAI,EAAE,GAAK,OAAS,EAAE,GAAK,GAAK,GAAK,IAAK,MAAO,KACjD,IAAI,IAAM,IAAI,eAAe,EAAI,EACjC,OAAI,IAAI,YAAY,EAAI,EAAI,IAAG,KAAO,GAC/B,GACT,CAIO,aAAM,aAAe,GACf,cAAgB,CAAC,UAAW,WAAY,MAAQ,MAAM,UAAW,WAAY,GAAG,GAAK,aACrF,YAAc,CAAC,KAAM,MAAQ,CAAC,CAAC,MAAQ,cAAc,KAAK,UAAW,KAAK,WAAY,GAAG,EAKzF,eAAiB,GAAK,IACtB,gBAAkB,EAIlB,mBAAqB","names":[],"file":"email-core.mjs"}