ident
All repositories: gitoria
4.5 KB
// lib/util.hl — SMALL SHARED HELPERS of ident (mission 009, code order): the storage directory and env numbers,// lists and records, ids, text checks, the email shape, oldest-first order. Each one exists once; the topic files// import what they use. Statics only.import { env } from 'hl:proc'// IDENT_STORAGE names another table DIRECTORY (the gates run on their own). Relative// paths resolve against the ENTRY SCRIPT's directory — tools must pass an absolute one.static dir = env('IDENT_STORAGE') != null ? env('IDENT_STORAGE') : './storage/mpackdb'static envNumber = (name, fallback) => {v = env(name)if (v == null || v == '') { return fallback }n = toNumber(v)return n == null ? fallback : n}// an empty list written to hl:mpackdb comes back as an empty hybrid (`.length` null)static countOf = (list) => {if (list == null) { return 0 }n = list.lengthreturn n == null ? 0 : n}static first = (list) => { return countOf(list) > 0 ? list[0] : null }static str = (v) => { return v == null ? '' : v }static isList = (v) => { return v != null && hlTypeName(v) == 'Hybrid' && v.length != null }static isObject = (v) => { return v != null && hlTypeName(v) == 'Hybrid' && v.length == null }// A PUBLIC ID is an mpackdb UUID: a non-empty string (a number is never one — an old// numeric id from before mission 009 finds nothing)static isId = (v) => { return v != null && hlTypeName(v) == 'String' && v.length > 0 && v.length <= 64 }// every record is written as a whole (hl:mpackdb's update replaces it)static merged = (rec, changes) => {out = {}for (k of rec.keys()) { out[k] = rec[k] }for (k of changes.keys()) { out[k] = changes[k] }return out}// OLDEST FIRST: by the stored `created`, a tie by the id (never by key order)static olderThan = (a, b) => {ca = a.created == null ? 0 : a.createdcb = b.created == null ? 0 : b.createdif (ca != cb) { return ca < cb }return a.id < b.id // strings order by code point (hybriel#2)}// a found list, oldest first (insertion sort by hand: no list sort(), hybriel #6)static oldestFirst = (list) => {out = []if (countOf(list) == 0) { return out }for (x of list) { out.push(x) }let i = 1while (i < out.length) {let cur = out[i]let j = i - 1while (j >= 0 && olderThan(cur, out[j])) {out[j + 1] = out[j]j = j - 1}out[j + 1] = curi = i + 1}return out}// ---- text checks ------------------------------------------------------------------------// no control characters (a field is one line of text)static hasControl = (s) => {let i = 0while (i < s.length) {let c = s.charCodeAt(i)if (c < 32 || c == 127) { return true }i = i + 1}return false}// the characters a URL may hold (a return URL, a notification's icon / link)static urlChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~:/?&=%+,;@!$()*[]'// AN EMAIL ADDRESS: lowercased and trimmed, then a plain whitelist shapestatic normEmail = (email) => { return email == null ? '' : ('' + email).trim().toLowerCase() }static validEmail = (email) => {if (email == null || email.length < 3 || email.length > 200) { return false }at = email.indexOf('@')if (at < 1 || at != email.lastIndexOf('@') || at == email.length - 1) { return false }if (!email.slice(at + 1, email.length).includes('.')) { return false }// a whitelist: the language has no escapes to name a CR or a tab withok = 'abcdefghijklmnopqrstuvwxyz0123456789.-_+@'let i = 0while (i < email.length) {if (!ok.includes(email[i])) { return false }i = i + 1}return true}// A TIME ZONE is an IANA name as the browser reports it (`Europe/Vienna`, `UTC`,// `America/Argentina/Buenos_Aires`, `Etc/GMT+5`). hl:time knows no zones (hybriel #11),// so the server checks the SHAPE only; the page checks the name against the browser's// own list (Intl.supportedValuesOf) before it sends it.static zoneChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/_+-'static checkTimeZone = (tz) => {if (tz == null || hlTypeName(tz) != 'String') { return { error = 'the time zone must be a string' } }t = tz.trim()if (t == '') { return { error = 'the time zone is empty' } }if (t.length > 64) { return { error = 'the time zone name is too long' } }let i = 0while (i < t.length) {if (!zoneChars.includes(t[i])) { return { error = 'that is not a time zone name (like Europe/Vienna)' } }i = i + 1}if (t[0] == '/' || t[t.length - 1] == '/' || t.includes('//')) { return { error = 'that is not a time zone name (like Europe/Vienna)' } }return { timeZone = t }}
Branches
- mainmain branch
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