gitoriaLog in with ident

ident

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitfe183516fe183516ident mission 009 (3/4): let only where reassigned — 293 never-reassigned lets are plain declarations; kept: reassigned, loop bodies, names of a file member, a name declared twice in one function; same outputmrefe183516/lib/util.hl

4.5 KB

  1. // lib/util.hl — SMALL SHARED HELPERS of ident (mission 009, code order): the storage directory and env numbers,
  2. // lists and records, ids, text checks, the email shape, oldest-first order. Each one exists once; the topic files
  3. // import what they use. Statics only.
  4. import { env } from 'hl:proc'
  5. // IDENT_STORAGE names another table DIRECTORY (the gates run on their own). Relative
  6. // paths resolve against the ENTRY SCRIPT's directory — tools must pass an absolute one.
  7. static dir = env('IDENT_STORAGE') != null ? env('IDENT_STORAGE') : './storage/mpackdb'
  8. static envNumber = (name, fallback) => {
  9. v = env(name)
  10. if (v == null || v == '') { return fallback }
  11. n = toNumber(v)
  12. return n == null ? fallback : n
  13. }
  14. // an empty list written to hl:mpackdb comes back as an empty hybrid (`.length` null)
  15. static countOf = (list) => {
  16. if (list == null) { return 0 }
  17. n = list.length
  18. return n == null ? 0 : n
  19. }
  20. static first = (list) => { return countOf(list) > 0 ? list[0] : null }
  21. static str = (v) => { return v == null ? '' : v }
  22. static isList = (v) => { return v != null && hlTypeName(v) == 'Hybrid' && v.length != null }
  23. static isObject = (v) => { return v != null && hlTypeName(v) == 'Hybrid' && v.length == null }
  24. // A PUBLIC ID is an mpackdb UUID: a non-empty string (a number is never one — an old
  25. // numeric id from before mission 009 finds nothing)
  26. static isId = (v) => { return v != null && hlTypeName(v) == 'String' && v.length > 0 && v.length <= 64 }
  27. // every record is written as a whole (hl:mpackdb's update replaces it)
  28. static merged = (rec, changes) => {
  29. out = {}
  30. for (k of rec.keys()) { out[k] = rec[k] }
  31. for (k of changes.keys()) { out[k] = changes[k] }
  32. return out
  33. }
  34. // OLDEST FIRST: by the stored `created`, a tie by the id (never by key order)
  35. static olderThan = (a, b) => {
  36. ca = a.created == null ? 0 : a.created
  37. cb = b.created == null ? 0 : b.created
  38. if (ca != cb) { return ca < cb }
  39. return a.id < b.id // strings order by code point (hybriel#2)
  40. }
  41. // a found list, oldest first (insertion sort by hand: no list sort(), hybriel #6)
  42. static oldestFirst = (list) => {
  43. out = []
  44. if (countOf(list) == 0) { return out }
  45. for (x of list) { out.push(x) }
  46. let i = 1
  47. while (i < out.length) {
  48. let cur = out[i]
  49. let j = i - 1
  50. while (j >= 0 && olderThan(cur, out[j])) {
  51. out[j + 1] = out[j]
  52. j = j - 1
  53. }
  54. out[j + 1] = cur
  55. i = i + 1
  56. }
  57. return out
  58. }
  59. // ---- text checks ------------------------------------------------------------------------
  60. // no control characters (a field is one line of text)
  61. static hasControl = (s) => {
  62. let i = 0
  63. while (i < s.length) {
  64. let c = s.charCodeAt(i)
  65. if (c < 32 || c == 127) { return true }
  66. i = i + 1
  67. }
  68. return false
  69. }
  70. // the characters a URL may hold (a return URL, a notification's icon / link)
  71. static urlChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~:/?&=%+,;@!$()*[]'
  72. // AN EMAIL ADDRESS: lowercased and trimmed, then a plain whitelist shape
  73. static normEmail = (email) => { return email == null ? '' : ('' + email).trim().toLowerCase() }
  74. static validEmail = (email) => {
  75. if (email == null || email.length < 3 || email.length > 200) { return false }
  76. at = email.indexOf('@')
  77. if (at < 1 || at != email.lastIndexOf('@') || at == email.length - 1) { return false }
  78. if (!email.slice(at + 1, email.length).includes('.')) { return false }
  79. // a whitelist: the language has no escapes to name a CR or a tab with
  80. ok = 'abcdefghijklmnopqrstuvwxyz0123456789.-_+@'
  81. let i = 0
  82. while (i < email.length) {
  83. if (!ok.includes(email[i])) { return false }
  84. i = i + 1
  85. }
  86. return true
  87. }
  88. // A TIME ZONE is an IANA name as the browser reports it (`Europe/Vienna`, `UTC`,
  89. // `America/Argentina/Buenos_Aires`, `Etc/GMT+5`). hl:time knows no zones (hybriel #11),
  90. // so the server checks the SHAPE only; the page checks the name against the browser's
  91. // own list (Intl.supportedValuesOf) before it sends it.
  92. static zoneChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/_+-'
  93. static checkTimeZone = (tz) => {
  94. if (tz == null || hlTypeName(tz) != 'String') { return { error = 'the time zone must be a string' } }
  95. t = tz.trim()
  96. if (t == '') { return { error = 'the time zone is empty' } }
  97. if (t.length > 64) { return { error = 'the time zone name is too long' } }
  98. let i = 0
  99. while (i < t.length) {
  100. if (!zoneChars.includes(t[i])) { return { error = 'that is not a time zone name (like Europe/Vienna)' } }
  101. i = i + 1
  102. }
  103. if (t[0] == '/' || t[t.length - 1] == '/' || t.includes('//')) { return { error = 'that is not a time zone name (like Europe/Vienna)' } }
  104. return { timeZone = t }
  105. }

Branches

Latest commits

  • fe183516ident mission 009 (3/4): let only where reassigned — 293 never-reassigned lets are plain declarations; kept: reassigned, loop bodies, names of a file member, a name declared twice in one function; same outputmre
  • d2e7f91bident mission 009 (2/4): one lib file per topic (login, accounts, identities, apps, invites, selector, notify + helpers, util), function routes as thin wrappers in lib/api.hl, project.hl = the map; same outputmre
  • 91017164ident mission 009 (1/4): file moves — the root .hl files into lib/ (api.hl → lib/api-helpers.hl), styles.hl → components/styles.hl; imports adjusted, no other changemre
  • f8bdcbc2ident: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); all gates greenmre
  • ff78726cident: Hybriel master 190aa11d (fc838894 GC correctness, #127, #126 closure scopes); gates all greenmre
  • a3a7d21aident: Hybriel master 8efba065 (#126 GC by bytes, #48 lambda params copy); session-writing lambdas take &sessionmre
  • 98226b41antcolony#40: mission references point to the moved missionsmre
  • ff805b9aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 51a7bcdfident: Hybriel master 73267707 (#122); /code uses the new page() signature; pending address passed as parameter; once-checksmre
  • 836f644fident#24: installable app (manifest, service worker, data-free offline /start), own iconmre
  • 8bebbbf2deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • cc063ea2deploy.sh: never send .git or .gitignore to Byrodinmre
  • 81b15b7bState of 2026-09-27, before the move to gitoriamre