gitoriaLog in with ident

tickets

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit7538b0347538b034mission 011 (ticket #25 web part): short page URLs /<slug> and /<slug>/<number>; /projects/<slug>, /projects/<slug>/<n>, /projects/<slug>/tickets/<n> and /tickets/<ref> answer 301 (current slug); slug pages moved behind hl:web's own routes after construction (they answered /__hl/* and /components/*.hl); reserved slugs refused; API paths unchanged. gate 293/0, connect 60/0, live-data copy 187/0, API old vs new: only page links differmre7538b034/tools/migrate-short-ids.hl

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

  • 7538b034mission 011 (ticket #25 web part): short page URLs /<slug> and /<slug>/<number>; /projects/<slug>, /projects/<slug>/<n>, /projects/<slug>/tickets/<n> and /tickets/<ref> answer 301 (current slug); slug pages moved behind hl:web's own routes after construction (they answered /__hl/* and /components/*.hl); reserved slugs refused; API paths unchanged. gate 293/0, connect 60/0, live-data copy 187/0, API old vs new: only page links differmre
  • 0369106emission 010 (code order) 4/4: README file map + import order + 'Same output' test, STATUS (entry, lessons, how to verify), LOG, report; tests/realdata-baseline.mjs + realdata-compare*.py (a cleanup answers the same on live data), tests/letcount.pymre
  • a75e0279mission 010 (code order) 3/4: let only where a variable is reassigned or re-bound in a loop body (456 lets → plain declarations; Hybriel refuses a plain declaration inside a loop on its 2nd pass). gate 249/0, connect 60/0, real-data reads identical, a 50-step write sequence (API + faces) identical to the old codemre
  • e9d5c618mission 010 (code order) 2/4: one lib/ file per topic — store.hl split into projects / tickets (+ relations) / events / tickets-helpers, util.hl shared helpers (env, storage dir, URLs, sorts, Vienna time), the function routes out of project.hl into lib/api.hl (thin; auth/filters/Accept in api-helpers.hl), invite + member-removal logic out of the faces/routes into invites.hl / tickets.hl; project.hl is the map. /login/callback gets req + the session store by reference. gate 249/0, connect 60/0, real-data reads identicalmre
  • 97e269b5mission 010 (code order) 1/4: .hl files out of the root — lib/ (store, users, connections, invites, migrate, markdown, mdview, import = ticketfile, util = localtime, jsoncheck, api-helpers = api), tools/import.hl, components/styles.hl; import paths only. gate 249/0, connect 60/0, real-data reads identicalmre
  • 38f9d10ftickets: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gate 249/0, connect 60/0mre
  • d3db6139tickets: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 249/0, connect 60/0mre
  • bce182e3tickets: Hybriel master 7eea0d32 (#126 memory, #48 lambda copies its argument); migrate.hl lambdas take &logmre
  • 4137be0fantcolony#40: mission references point to the moved missionsmre
  • 9bfba36aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
  • c7bd2645tickets: Hybriel master 73267707 (#122 fixed); compactNow workaround removed (#110 covered)mre
  • 2ab91ee9tickets: gate checks rows appear once (session sync); re-vendor to ff51cf46 stopped on hybriel#122, stays 837fe120mre
  • e01c2b1dtickets#24: installable app (manifest, service worker, offline list), own icon; gate waits for the hello's pongmre
  • 752fbb7fdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
  • 38bdd5e4deploy.sh: never send .git or .gitignore to Byrodinmre
  • f12fa1bcState of 2026-09-27, before the move to gitoriamre