gitoriaLog in with ident

gitoria

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitcc7bf7bacc7bf7bamission 002 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (289 lets → plain declarations; 213 left: 125 reassigned, 88 loop-bound; no member/import/param clash). gates 200/0, 46/0, 44/0; real-data reads + writes identical (browser modules: var → const only)mrecc7bf7ba/tools/migrate-short-ids.hl

6.3 KB

  1. // tools/migrate-short-ids.hl — ONE-OFF MIGRATION (mission 039, ident#23): ident answers every identity's public
  2. // 5-character SHORT ID (e.g. `a68sz`) instead of the old per-app id (32 hex). gitoria.worldapi.org keeps ident ids in ONE place:
  3. // users.db `identity` (lib/users.hl usersTable, unique index). repos.db `owner`, sshkeys.db / tokens.db `user` are the users @id.
  4. // Everything else points at the users @id, which does not change — sessions (`user = { id = <users @id> }`) stay signed in.
  5. //
  6. // What it does: POST <ident>/api/migrate-ids {key, secret} (NEVER `finish` — that is a separate, later step, runbook
  7. // antcolony-docs/docs/short-id-switch.md) → { ids: { <old id>: <short id> } }, then for every user:
  8. // * old id in the map → `identity` rewritten to the short id (MAPPED)
  9. // * already a short id → left alone (ALREADY) — so a second run changes nothing (idempotent)
  10. // * old id NOT in the map → left alone (UNMAPPED): an identity deleted in ident since; nobody can log in as it
  11. // * the short id is already another user's `identity` → left alone (CONFLICT; cannot happen while the app was
  12. // stopped between the ident switch and this run — needs a human, runbook "Conflicts"; the tool then exits non-zero)
  13. // Prints counts; the key/secret are read from the environment and never printed. Exit is non-zero on any refusal.
  14. //
  15. // RUN IT WITH THE APP STOPPED (the server holds the tables), after ident#23 is live, storage backed up:
  16. // GITORIA_STORAGE=$PWD/storage/mpackdb IDENT_API_KEY=… IDENT_API_SECRET=… IDENT_EXCHANGE_URL=<ident> bin/hybriel tools/migrate-short-ids.hl
  17. // On Byrodin (the key/secret come from the app's .env through docker's --env-file; hybriel itself only loads a .env
  18. // beside the ENTRY script, i.e. tools/.env, which does not exist):
  19. // docker run --rm --network host --env-file .env -e GITORIA_STORAGE=/home/gitoria/storage/mpackdb \
  20. // -e IDENT_EXCHANGE_URL=http://127.0.0.1:45002 -v $PWD:/home/gitoria -w /home/gitoria \
  21. // worldapi-hybriel-runtime:debian12 ./bin/hybriel tools/migrate-short-ids.hl
  22. // GITORIA_STORAGE must be ABSOLUTE: Hybriel resolves a relative path against the entry script's folder (tools/).
  23. // Test: tests/short-id-switch.mjs (old ident → data → new ident → this tool twice → the same user logs in again).
  24. import { env } from 'hl:proc'
  25. import { fetch } from 'hl:fetch'
  26. import { countOfList, firstOf } from '../lib/util.hl'
  27. import { usersTable, identKey, identSecret, identExchangeUrl } from '../lib/users.hl'
  28. storage = env('GITORIA_STORAGE')
  29. if (storage == null || !storage.startsWith('/')) {
  30. hlError('set GITORIA_STORAGE to the ABSOLUTE table directory, e.g. GITORIA_STORAGE=$PWD/storage/mpackdb')
  31. }
  32. if (identKey == '' || identSecret == '') {
  33. hlError('IDENT_API_KEY / IDENT_API_SECRET are not set (on Byrodin: docker run --env-file .env …)')
  34. }
  35. // the characters of an id, as ident#23 makes them: 5+ of 2-9 and a-z without i, l, o (lower case)
  36. static isShortId = (s) => {
  37. if (s == null || hlTypeName(s) != 'String' || s.length < 5 || s.length > 16) { return false }
  38. let i = 0
  39. while (i < s.length) {
  40. let c = s.charCodeAt(i)
  41. let digit = c >= 50 && c <= 57
  42. let letter = c >= 97 && c <= 122 && c != 105 && c != 108 && c != 111
  43. if (!digit && !letter) { return false }
  44. i = i + 1
  45. }
  46. return true
  47. }
  48. // the old per-app id: exactly 32 lowercase hex
  49. static isOldId = (s) => {
  50. if (s == null || hlTypeName(s) != 'String' || s.length != 32) { return false }
  51. let i = 0
  52. while (i < s.length) {
  53. let c = s.charCodeAt(i)
  54. if (!((c >= 48 && c <= 57) || (c >= 97 && c <= 102))) { return false }
  55. i = i + 1
  56. }
  57. return true
  58. }
  59. // every field of the record, `identity` replaced (hl:mpackdb wants the whole @id record, its `id` included)
  60. static withIdentity = (rec, identity) => {
  61. out = {}
  62. for (k of rec.keys()) { out[k] = rec[k] }
  63. out.identity = identity
  64. return out
  65. }
  66. console.log('migrate-short-ids: asking ' + identExchangeUrl + '/api/migrate-ids (storage ' + storage + ')')
  67. r = fetch(identExchangeUrl + '/api/migrate-ids', { method = 'POST' json = { key = identKey secret = identSecret } headers = { 'user-agent' = 'gitoria.worldapi.org (migrate-short-ids)' } timeoutMs = 30000 })
  68. if (r == null || r.status == null || r.status == 0) { hlError('ident did not answer at ' + identExchangeUrl) }
  69. if (r.status != 200) {
  70. e = r.json()
  71. hlError('ident refused migrate-ids (' + r.status + (e != null && e.error != null ? ': ' + e.error : '') + ') — is ident#23 live? right key/secret?')
  72. }
  73. j = r.json()
  74. if (j == null || j.ids == null || hlTypeName(j.ids) != 'Hybrid') { hlError('ident answered no id map: ' + JSON.stringify(j)) }
  75. ids = j.ids
  76. console.log('ident: ' + j.count + ' old ids in the map, finished = ' + j.finished)
  77. seen = 0
  78. mapped = 0
  79. already = 0
  80. unmapped = 0
  81. conflicts = 0
  82. failed = 0
  83. all = usersTable.find(null, null)
  84. if (countOfList(all) > 0) {
  85. for (u of all) {
  86. seen = seen + 1
  87. let old = u.identity
  88. let sid = isOldId(old) ? ids[old] : null
  89. if (sid != null && isShortId(sid)) {
  90. let other = firstOf(usersTable.find('identity', sid))
  91. if (other != null && other.id != u.id) {
  92. conflicts = conflicts + 1
  93. console.log('CONFLICT user ' + u.id + ': its short id ' + sid + ' is already user ' + other.id + ' — left alone')
  94. } else {
  95. usersTable.update(u.id, withIdentity(u, sid))
  96. let after = usersTable.fetch(u.id)
  97. if (after != null && after.identity == sid && firstOf(usersTable.find('identity', sid)) != null && countOfList(usersTable.find('identity', old)) == 0) {
  98. mapped = mapped + 1
  99. console.log('MAPPED user ' + u.id + ' → ' + sid)
  100. } else {
  101. failed = failed + 1
  102. console.log('FAILED user ' + u.id + ': ' + usersTable.lastError())
  103. }
  104. }
  105. } else if (isShortId(old) && !isOldId(old)) {
  106. already = already + 1
  107. } else {
  108. unmapped = unmapped + 1
  109. console.log('UNMAPPED user ' + u.id + ' (ident does not know its old id any more — deleted identity?)')
  110. }
  111. }
  112. }
  113. console.log('migrate-short-ids: users seen ' + seen + ', mapped ' + mapped + ', already short ' + already + ', unmapped ' + unmapped + ', conflicts ' + conflicts + ', failed ' + failed)
  114. if (failed > 0) { hlError('some users could not be written — restore the backup and look') }
  115. 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

Latest commits

  • cc7bf7bamission 002 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (289 lets → plain declarations; 213 left: 125 reassigned, 88 loop-bound; no member/import/param clash). gates 200/0, 46/0, 44/0; real-data reads + writes identical (browser modules: var → const only)mre
  • 090a20c6mission 002 (code order) 2/4: one lib/ file per topic — git.hl split into git (calls, branches, init, temp folder) / homepage / code / pulls / releases (+ git-helpers: paths, ids, |||PR/|||RL markers); repos-helpers, tickets-helpers, transport-helpers; util.hl = localtime + env, storage dir, addresses, lists, text checks, one newest-first sort (was 3 copies); the function routes out of project.hl into lib/api.hl (thin; plumbing in api-helpers.hl), sshgate.hl folded into api.hl + sshkeys.hl keyLine + repos.hl mayPush; 'Make main' and the merge answer out of the faces (code.hl makeMain, pulls.hl pullsView), one login helper (users.hl userOfLoginCode); project.hl is the map. Session-writing routes get &req + &server.sessions. gates 200/0, 46/0, 44/0; real-data identical except /login/failed now shows the parked reason for a browser that already had a session (the old copy-of-req lost it)mre
  • 110c2799mission 002 (code order) 1/4: .hl files out of the root — lib/ (api, git, localtime, markdown, repos, sshgate, sshkeys, tickets, tokens, transport, users), components/styles.hl; jsoncheck.hl removed (imported nowhere); import paths only. gates 200/0, 46/0, 44/0; real-data reads + writes identicalmre
  • fdfb4b1bgitoria: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 200/0, 46/0, 44/0mre
  • 5b46ac84antcolony#40: LOG.md — missions 069/072 are antcolony missions (report paths on Byrodin)mre
  • 5602ff41gitoria: Hybriel master 190aa11d (fc838894 GC correctness, #127 mountKids by reference, #126, #48) — tracker README flat; gates 200/0, 46/0, 44/0mre
  • e85eaf01gitoria: 069 round 2 — hybriel 1a096ad3 not adopted (Markdown SSR still grows); browser gate waits for the server-side logout before restartmre
  • 09ce4f3fgitoria: mission 069 re-vendor hybriel 8efba065 stopped (big SSR pages grow + slow down); lambda audit clean; old vendor keptmre
  • 3dc43108antcolony#40: mission references point to the moved missionsmre
  • 8d9450fdantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • 205d5fe4gitoria: Hybriel master ff51cf46; ssh keys/tokens no double rows (session sync); gates follow #20mre
  • 9b27cb26gitoria#21: installable app (manifest, service worker, offline start page), own iconmre
  • 68dcb603deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • e2deed6dgitoria#20: "Add code" only on the Code page of an empty repository, no collapsiblemre
  • 8bb97ffddeploy.sh: never send .git or .gitignore to Byrodinmre
  • fd981932State of 2026-09-27; bin/ no longer tracked (Hybriel commit is in README)mre
  • 4a2d7125initial commitmre