ident
All repositories: gitoria
4.9 KB
// lib/identities-helpers.hl — what lib/identities.hl looks up: the strict identity fields, the avatar check, the// short id's alphabet, the label a list shows. No table access. Statics only.import { randomBytes } from 'hl:crypto'import { str, hasControl } from './util.hl'static maxField = 60 // chars per identity fieldstatic fieldNames = ['identityName' 'nickname' 'firstname' 'lastname']// THE AVATAR (ticket #15): an uploaded picture, stored as a small data URL (the page scales it to// 128×128) — not a text field, so it is checked and stored separately from fieldNames/maxFieldstatic maxAvatarUrl = 60000static avatarChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/='// AN AVATAR: empty (none) or the uploaded picture as a data URL — data:image/(png|jpeg|webp);base64,// with the file's own magic bytes at the start of the base64 text (the page scales the upload to// 128×128 first, avatar.js), ≤ maxAvatarUrl characters, base64 characters onlystatic checkAvatarUrl = (v) => {if (v == null || hlTypeName(v) != 'String') { return { error = 'field avatar must be a string' } }t = v.trim()if (t == '') { return { avatar = '' } }if (t.length > maxAvatarUrl) { return { error = 'field avatar is longer than ' + maxAvatarUrl + ' characters' } }let kind = ''let start = 0if (t.startsWith('data:image/png;base64,')) { kind = 'iVBORw0KGgo' start = 22 }else if (t.startsWith('data:image/jpeg;base64,')) { kind = '/9j/' start = 23 }else if (t.startsWith('data:image/webp;base64,')) { kind = 'UklGR' start = 23 }if (kind == '') { return { error = 'field avatar must be an uploaded PNG, JPEG or WebP picture' } }data = t.slice(start, t.length)if (!data.startsWith(kind)) { return { error = 'field avatar is not a valid picture' } }let i = 0while (i < data.length) {if (!avatarChars.includes(data[i])) { return { error = 'field avatar is not valid base64' } }i = i + 1}return { avatar = t }}// the avatar to show: only an uploaded picture (a data URL). Before the upload existed an avatar// was an http(s) URL; those are no longer shown (never fetched from a third-party address).static shownAvatar = (r) => { return str(r.avatar).startsWith('data:image/') ? str(r.avatar) : '' }// THE IDENTITY FIELDS, STRICT: `fields` is an object whose keys are among fieldNames plus// `avatar`. A name field is a string (trimmed, at most 60 chars, no control characters);// `avatar` is checked by checkAvatarUrl. Absent keys are absent from the answer.// Answers { error, field } or { fields }.static checkFields = (fields) => {if (fields == null || hlTypeName(fields) != 'Hybrid' || fields.length != null) { return { error = 'the identity fields must be an object' field = '' } }out = {}for (k of fields.keys()) {if (k == 'avatar') {let c = checkAvatarUrl(fields[k])if (c.error != null) { return { error = c.error field = 'avatar' } }out.avatar = c.avatar} else if (!fieldNames.includes(k)) { return { error = 'unknown field: ' + k field = k } }else {let v = fields[k]if (v == null || hlTypeName(v) != 'String') { return { error = 'field ' + k + ' must be a string' field = k } }let t = v.trim()if (t.length > maxField) { return { error = 'field ' + k + ' is longer than ' + maxField + ' characters' field = k } }if (hasControl(t)) { return { error = 'field ' + k + ' contains a control character' field = k } }out[k] = t}}return { fields = out }}// WHAT A LIST SHOWS: the identity name; without one, nickname, then first + last name,// then "Identity <n>" (n = its place in the list)static labelOf = (r, n) => {if (str(r.identityName) != '') { return r.identityName }if (str(r.nickname) != '') { return r.nickname }full = (str(r.firstname) + ' ' + str(r.lastname)).trim()if (full != '') { return full }return 'Identity ' + n}// ---- THE SHORT ID's shape (ident#23; lib/identities.hl makes and finds them) -------------------// 5 characters from an alphabet without look-alikes — digits 2-9 and the letters minus i, l, o// (no 0/O, 1/l/I): 31 characters, 28.6 million idsstatic shortAlphabet = '23456789abcdefghjkmnpqrstuvwxyz'static shortLength = 5static newShortId = () => {let out = ''while (out.length < shortLength) {let hex = randomBytes(16)let i = 0while (i < 32 && out.length < shortLength) {let b = '0123456789abcdef'.indexOf(hex[i]) * 16 + '0123456789abcdef'.indexOf(hex[i + 1])// 248 = 8 * 31: bytes above it are dropped so every character is equally likelyif (b < 248) { out = out + shortAlphabet[b % 31] }i = i + 2}}return out}// what a person typed → the lowercase form, or null when it cannot be a short idstatic normShortId = (v) => {if (v == null || hlTypeName(v) != 'String') { return null }t = v.trim().toLowerCase()if (t.length != shortLength) { return null }let i = 0while (i < t.length) {if (shortAlphabet.indexOf(t[i]) < 0) { return null }i = i + 1}return t}
Branches
- mainmain branch
Latest commits
- 74235815ident mission 009 (4/4): docs (README files map + same-output test, STATUS, LOG), report, tests/letcount.py + realdata-baseline/comparemre
- 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