ident
All repositories: gitoria
4.9 KB
// lib/apps-helpers.hl — what lib/apps.hl (and lib/invites.hl) look up: the app form's strict input, an origin,// a return URL against the app's origins, new keys and secrets, the row a page shows. No table access. Statics only.import { randomBytes } from 'hl:crypto'import { hasControl, isList, isObject, urlChars } from './util.hl'static maxOrigins = 10static maxName = 60// ---- validation -----------------------------------------------------------------------static checkName = (name) => {if (name == null || hlTypeName(name) != 'String') { return { error = 'field name must be a string' field = 'name' } }t = name.trim()if (t == '') { return { error = 'the app needs a name' field = 'name' } }if (t.length > maxName) { return { error = 'field name is longer than ' + maxName + ' characters' field = 'name' } }if (hasControl(t)) { return { error = 'field name contains a control character' field = 'name' } }return { name = t }}// AN ORIGIN is `http(s)://host[:port]`: lowercase, no path, no user, no query. One// trailing slash is forgiven. Answers { origin } or { error }.static hostChars = 'abcdefghijklmnopqrstuvwxyz0123456789.-[]:'static checkOrigin = (o) => {if (o == null || hlTypeName(o) != 'String') { return { error = 'an origin must be a string' } }let t = o.trim().toLowerCase()if (t.length > 0 && t[t.length - 1] == '/') { t = t.slice(0, t.length - 1) }if (t.length > 200) { return { error = 'the origin ' + t + ' is too long' } }scheme = t.slice(0, 7) == 'http://' ? 'http://' : (t.slice(0, 8) == 'https://' ? 'https://' : null)if (scheme == null) { return { error = 'an origin starts with http:// or https:// (' + t + ')' } }host = t.slice(scheme.length, t.length)if (host == '' || host[0] == '.' || host[0] == ':') { return { error = 'the origin ' + t + ' has no host' } }let i = 0while (i < host.length) {if (!hostChars.includes(host[i])) { return { error = 'an origin is only scheme://host[:port], no path (' + t + ')' } }i = i + 1}return { origin = t }}// the app form's input, STRICT: exactly { name, origins } — origins a list of 1-10// strings. Answers { error, field } or { name, origins (list, deduplicated) }.static appFields = ['name' 'origins']static checkAppInput = (input) => {if (!isObject(input)) { return { error = 'the app must be an object' field = '' } }for (k of input.keys()) {if (!appFields.includes(k)) { return { error = 'unknown field: ' + k field = k } }}for (k of appFields) {if (input[k] == null) { return { error = 'missing field: ' + k field = k } }}n = checkName(input.name)if (n.error != null) { return n }if (!isList(input.origins)) { return { error = 'field origins must be a list of strings' field = 'origins' } }out = []for (o of input.origins) {let c = checkOrigin(o)if (c.error != null) { return { error = c.error field = 'origins' } }if (!out.includes(c.origin)) { out.push(c.origin) }}if (out.length == 0) { return { error = 'give at least one origin the app runs on' field = 'origins' } }if (out.length > maxOrigins) { return { error = 'at most ' + maxOrigins + ' origins' field = 'origins' } }return { name = n.name origins = out }}// ---- apps -----------------------------------------------------------------------------static newKey = () => { return 'pk_' + randomBytes(16) } // 32 hexstatic newSecret = () => { return 'sk_' + randomBytes(24) } // 48 hexstatic originsOf = (rec) => { return rec.origins == '' ? [] : rec.origins.split(' ') }static appRowOf = (rec) => {if (rec == null) { return null }let origins = originsOf(rec)return { id = rec.id name = rec.name origins = origins originsText = origins.join(' ') apiKey = rec.apiKey }}// ---- the login button ---------------------------------------------------------------// A RETURN URL is absolute http(s), printable, no fragment, at most 2000 chars, and its// origin is one of the app's. Answers { error } or { origin }.static checkReturn = (url, appRec) => {if (url == null || url == '') { return { error = 'the return parameter is missing' } }if (url.length > 2000) { return { error = 'the return URL is too long' } }let i = 0while (i < url.length) {if (!urlChars.includes(url[i])) { return { error = 'the return URL contains a character that is not allowed' } }i = i + 1}lower = url.toLowerCase()scheme = lower.slice(0, 7) == 'http://' ? 'http://' : (lower.slice(0, 8) == 'https://' ? 'https://' : null)if (scheme == null) { return { error = 'the return URL must start with http:// or https://' } }rest = lower.slice(scheme.length, lower.length)let end = rest.lengthfor (ch of ['/' '?']) {let at = rest.indexOf(ch)if (at >= 0 && at < end) { end = at }}host = rest.slice(0, end)if (host == '') { return { error = 'the return URL has no host' } }let origin = scheme + hostif (!originsOf(appRec).includes(origin)) { return { error = 'the return URL is on ' + origin + ', which is not one of the origins registered for ' + appRec.name } }return { origin = origin }}
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