notes
All repositories: gitoria
6.2 KB
// tools/migrate-short-ids.hl — ONE-OFF MIGRATION (mission 039, ident#23): ident answers every identity's public// 5-character SHORT ID (e.g. `a68sz`) instead of the old per-app id (32 hex). notes.worldapi.org keeps ident ids in ONE place:// users.db `identity` (users.hl usersTable, unique index). notes.db `owner` is the users @id.// Everything else points at the users @id, which does not change — sessions (`user = { id = <users @id> }`) stay signed in.//// What it does: POST <ident>/api/migrate-ids {key, secret} (NEVER `finish` — that is a separate, later step, runbook// antcolony-docs/docs/short-id-switch.md) → { ids: { <old id>: <short id> } }, then for every user:// * old id in the map → `identity` rewritten to the short id (MAPPED)// * already a short id → left alone (ALREADY) — so a second run changes nothing (idempotent)// * old id NOT in the map → left alone (UNMAPPED): an identity deleted in ident since; nobody can log in as it// * the short id is already another user's `identity` → left alone (CONFLICT; cannot happen while the app was// stopped between the ident switch and this run — needs a human, runbook "Conflicts"; the tool then exits non-zero)// Prints counts; the key/secret are read from the environment and never printed. Exit is non-zero on any refusal.//// RUN IT WITH THE APP STOPPED (the server holds the tables), after ident#23 is live, storage backed up:// NOTES_STORAGE=$PWD/storage/mpackdb IDENT_API_KEY=… IDENT_API_SECRET=… IDENT_EXCHANGE_URL=<ident> bin/hybriel tools/migrate-short-ids.hl// On Byrodin (the key/secret come from the app's .env through docker's --env-file; hybriel itself only loads a .env// beside the ENTRY script, i.e. tools/.env, which does not exist):// docker run --rm --network host --env-file .env -e NOTES_STORAGE=/home/notes/storage/mpackdb \// -e IDENT_EXCHANGE_URL=http://127.0.0.1:45002 -v $PWD:/home/notes -w /home/notes \// worldapi-hybriel-runtime:debian12 ./bin/hybriel tools/migrate-short-ids.hl// NOTES_STORAGE must be ABSOLUTE: Hybriel resolves a relative path against the entry script's folder (tools/).// Test: tests/short-id-switch.mjs (old ident → data → new ident → this tool twice → the same user logs in again).import { env } from 'hl:proc'import { fetch } from 'hl:fetch'import { usersTable, identKey, identSecret, identExchangeUrl, countOfList, firstOf } from '../users.hl'storage = env('NOTES_STORAGE')if (storage == null || !storage.startsWith('/')) {hlError('set NOTES_STORAGE to the ABSOLUTE table directory, e.g. NOTES_STORAGE=$PWD/storage/mpackdb')}if (identKey == '' || identSecret == '') {hlError('IDENT_API_KEY / IDENT_API_SECRET are not set (on Byrodin: docker run --env-file .env …)')}// the characters of an id, as ident#23 makes them: 5+ of 2-9 and a-z without i, l, o (lower case)static isShortId = (s) => {if (s == null || hlTypeName(s) != 'String' || s.length < 5 || s.length > 16) { return false }let i = 0while (i < s.length) {let c = s.charCodeAt(i)let digit = c >= 50 && c <= 57let letter = c >= 97 && c <= 122 && c != 105 && c != 108 && c != 111if (!digit && !letter) { return false }i = i + 1}return true}// the old per-app id: exactly 32 lowercase hexstatic isOldId = (s) => {if (s == null || hlTypeName(s) != 'String' || s.length != 32) { return false }let i = 0while (i < s.length) {let c = s.charCodeAt(i)if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102))) { return false }i = i + 1}return true}// every field of the record, `identity` replaced (hl:mpackdb wants the whole @id record, its `id` included)static withIdentity = (rec, identity) => {let out = {}for (k of rec.keys()) { out[k] = rec[k] }out.identity = identityreturn out}console.log('migrate-short-ids: asking ' + identExchangeUrl + '/api/migrate-ids (storage ' + storage + ')')r = fetch(identExchangeUrl + '/api/migrate-ids', { method = 'POST' json = { key = identKey secret = identSecret } headers = { 'user-agent' = 'notes.worldapi.org (migrate-short-ids)' } timeoutMs = 30000 })if (r == null || r.status == null || r.status == 0) { hlError('ident did not answer at ' + identExchangeUrl) }if (r.status != 200) {let e = r.json()hlError('ident refused migrate-ids (' + r.status + (e != null && e.error != null ? ': ' + e.error : '') + ') — is ident#23 live? right key/secret?')}j = r.json()if (j == null || j.ids == null || hlTypeName(j.ids) != 'Hybrid') { hlError('ident answered no id map: ' + JSON.stringify(j)) }ids = j.idsconsole.log('ident: ' + j.count + ' old ids in the map, finished = ' + j.finished)seen = 0mapped = 0already = 0unmapped = 0conflicts = 0failed = 0all = usersTable.find(null, null)if (countOfList(all) > 0) {for (u of all) {seen = seen + 1let old = u.identitylet sid = isOldId(old) ? ids[old] : nullif (sid != null && isShortId(sid)) {let other = firstOf(usersTable.find('identity', sid))if (other != null && other.id != u.id) {conflicts = conflicts + 1console.log('CONFLICT user ' + u.id + ': its short id ' + sid + ' is already user ' + other.id + ' — left alone')} else {usersTable.update(u.id, withIdentity(u, sid))let after = usersTable.fetch(u.id)if (after != null && after.identity == sid && firstOf(usersTable.find('identity', sid)) != null && countOfList(usersTable.find('identity', old)) == 0) {mapped = mapped + 1console.log('MAPPED user ' + u.id + ' → ' + sid)} else {failed = failed + 1console.log('FAILED user ' + u.id + ': ' + usersTable.lastError())}}} else if (isShortId(old) && !isOldId(old)) {already = already + 1} else {unmapped = unmapped + 1console.log('UNMAPPED user ' + u.id + ' (ident does not know its old id any more — deleted identity?)')}}}console.log('migrate-short-ids: users seen ' + seen + ', mapped ' + mapped + ', already short ' + already + ', unmapped ' + unmapped + ', conflicts ' + conflicts + ', failed ' + failed)if (failed > 0) { hlError('some users could not be written — restore the backup and look') }if (conflicts > 0) { hlError('CONFLICTS: the app made a new user for a short id before this ran (it was not stopped?) — the old user keeps its old id; see the runbook "Conflicts"') }
Branches
- mainmain branch
Latest commits
- 2bebebdanotes: Hybriel master ff51cf46 (re-vendor round)mre
- 3eff126dnotes#3: installable app (manifest + own icon/favicon; notes' own sw.js kept)mre
- 9883c540deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- eee693b8deploy.sh: never send .git or .gitignore to Byrodinmre
- c8904061State of 2026-09-27, before the move to gitoriamre