oxedyne/daimond/www/js/accounts.js
6.3 KiB, 1 run
created by r2519314175:1339, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | /* accounts.js — several people, one browser, one at a time. |
| 2 | * |
| 3 | * An account is a passphrase identity with its own chats, provider keys, credits, mail and |
| 4 | * workspace. Nobody sees another's. That isolation is achieved by NAMESPACING storage: every |
| 5 | * `daimond-*` localStorage key, the FSA handle store, and the OPFS workspace are prefixed with the |
| 6 | * current account — with ONE exception. The first account (the "primary") keeps the raw keys and |
| 7 | * the OPFS root exactly as a single-user install always had them, so adopting an existing install |
| 8 | * as account one moves not a single byte, and a browser with one account behaves byte-for-byte as |
| 9 | * it did before accounts existed. Only a SECOND account brings a prefix into being. |
| 10 | * |
| 11 | * This script loads before every other, so the shim over `localStorage` is in place before any |
| 12 | * module reads or writes. Switching account is a reload: modules read their account's data once, |
| 13 | * at load, so the clean way to hand them a different account is to start them again. |
| 14 | * |
| 15 | * What is NOT namespaced (browser-wide, shared by everyone at this browser): the account registry |
| 16 | * itself, the theme, the language, the display currency, the panel layout, and the line-number |
| 17 | * toggle. These are preferences of the device, not facts about a person. |
| 18 | */ |
| 19 | (function () { |
| 20 | 'use strict'; |
| 21 | |
| 22 | var REG = 'daimond-accounts'; // [{ id, name, fp, primary }] — the registry, raw. |
| 23 | var CUR = 'daimond-current'; // the current account's id, raw. |
| 24 | |
| 25 | // Device-wide, never namespaced: the registry, and pure UI preferences. |
| 26 | var GLOBAL = { |
| 27 | 'daimond-accounts': 1, 'daimond-current': 1, |
| 28 | 'daimond-theme': 1, 'daimond-layout': 1, 'daimond-files-lineno': 1, |
| 29 | // The language and the display currency sit with the theme: they say how |
| 30 | // this browser presents Daimond, not who is using it. |
| 31 | 'daimond-locale': 1, 'daimond-currency': 1, |
| 32 | }; |
| 33 | |
| 34 | // The unshimmed methods, captured before the shim shadows them. All raw access below goes |
| 35 | // through these, so the registry and the sweep in remove() see true, un-prefixed keys. |
| 36 | var proto = window.Storage.prototype; |
| 37 | var rawGet = proto.getItem, rawSet = proto.setItem, rawDel = proto.removeItem, rawKey = proto.key; |
| 38 | var LS = window.localStorage; |
| 39 | function rget(k) { return rawGet.call(LS, k); } |
| 40 | function rset(k, v){ return rawSet.call(LS, k, v); } |
| 41 | function rdel(k) { return rawDel.call(LS, k); } |
| 42 | |
| 43 | function list() { try { return JSON.parse(rget(REG) || '[]') || []; } catch (e) { return []; } } |
| 44 | function save(a) { rset(REG, JSON.stringify(a)); } |
| 45 | function find(id) { |
| 46 | var a = list(); |
| 47 | for (var i = 0; i < a.length; i++) if (a[i].id === id) return a[i]; |
| 48 | return null; |
| 49 | } |
| 50 | |
| 51 | /// A device-unique account id: 16 hex chars from the CSPRNG. |
| 52 | function mint() { |
| 53 | var b = new Uint8Array(8); |
| 54 | crypto.getRandomValues(b); |
| 55 | return Array.prototype.map.call(b, function (x) { return (x + 256).toString(16).slice(1); }).join(''); |
| 56 | } |
| 57 | |
| 58 | // Ensure there is at least one account. An existing single-user install becomes the primary, |
| 59 | // carrying its name and fingerprint across so the registry can show it without unlocking; a |
| 60 | // fresh install gets a primary too. Either way the primary uses the RAW keys, so nothing moves. |
| 61 | (function ensure() { |
| 62 | if (list().length) return; |
| 63 | var primary = { id: mint(), name: rget('daimond-id-name') || '', fp: rget('daimond-id-fp') || '', primary: true }; |
| 64 | save([primary]); |
| 65 | rset(CUR, primary.id); |
| 66 | })(); |
| 67 | |
| 68 | function currentId() { |
| 69 | var c = rget(CUR); |
| 70 | if (c && find(c)) return c; |
| 71 | var a = list(); |
| 72 | var id = a.length ? a[0].id : null; |
| 73 | if (id) rset(CUR, id); |
| 74 | return id; |
| 75 | } |
| 76 | function account() { return find(currentId()); } |
| 77 | |
| 78 | // The primary keeps raw keys and the OPFS root; every other account is prefixed. |
| 79 | function prefix() { var x = account(); return (x && !x.primary) ? ('d~' + x.id + '~') : ''; } |
| 80 | /// The OPFS subdirectory for the current account ('' for the primary, i.e. the root). |
| 81 | function opfsNs() { var x = account(); return (x && !x.primary) ? ('d~' + x.id) : ''; } |
| 82 | |
| 83 | function nsKey(k) { |
| 84 | return (typeof k === 'string' && k.indexOf('daimond-') === 0 && !GLOBAL[k]) ? prefix() + k : k; |
| 85 | } |
| 86 | |
| 87 | // The shim. Shadows the prototype methods on the instance, so every daimond-* read and write in |
| 88 | // every module lands in the current account's namespace with no call site aware of it. |
| 89 | LS.getItem = function (k) { return rget(nsKey(k)); }; |
| 90 | LS.setItem = function (k, v) { return rset(nsKey(k), v); }; |
| 91 | LS.removeItem = function (k) { return rdel(nsKey(k)); }; |
| 92 | |
| 93 | // ── Managing accounts ─────────────────────────────────────────── |
| 94 | |
| 95 | /// Add a new (non-primary) account and make it current. The caller reloads. |
| 96 | function add(name) { |
| 97 | var a = list(); |
| 98 | var na = { id: mint(), name: String(name || '').trim(), fp: '', primary: false }; |
| 99 | a.push(na); save(a); rset(CUR, na.id); |
| 100 | return na.id; |
| 101 | } |
| 102 | function setCurrent(id) { if (find(id)) { rset(CUR, id); return true; } return false; } |
| 103 | function rename(id, name) { |
| 104 | var a = list(); |
| 105 | for (var i = 0; i < a.length; i++) if (a[i].id === id) a[i].name = String(name || '').trim(); |
| 106 | save(a); |
| 107 | } |
| 108 | /// Record an account's fingerprint, so the picker can show it without unlocking. |
| 109 | function setFp(id, fp) { |
| 110 | var a = list(); |
| 111 | for (var i = 0; i < a.length; i++) if (a[i].id === id) a[i].fp = fp || ''; |
| 112 | save(a); |
| 113 | } |
| 114 | |
| 115 | /// Remove a non-primary account and ALL of its namespaced localStorage. Its OPFS subdirectory |
| 116 | /// and its FSA handle store are the caller's to clear (they live outside localStorage). The |
| 117 | /// primary cannot be removed here — it owns the raw keys and the OPFS root, and dropping it is |
| 118 | /// "Forget this identity", which wipes the raw keys directly. |
| 119 | function remove(id) { |
| 120 | var x = find(id); |
| 121 | if (!x || x.primary) return false; |
| 122 | var pre = 'd~' + id + '~'; |
| 123 | var kills = []; |
| 124 | for (var i = 0; i < LS.length; i++) { |
| 125 | var k = rawKey.call(LS, i); |
| 126 | if (k && k.indexOf(pre) === 0) kills.push(k); |
| 127 | } |
| 128 | kills.forEach(function (k) { rdel(k); }); |
| 129 | var a = list().filter(function (y) { return y.id !== id; }); |
| 130 | save(a); |
| 131 | if (rget(CUR) === id) rset(CUR, a.length ? a[0].id : ''); |
| 132 | return true; |
| 133 | } |
| 134 | |
| 135 | window.DaimondAccounts = { |
| 136 | list: list, |
| 137 | current: currentId, |
| 138 | account: account, |
| 139 | count: function () { return list().length; }, |
| 140 | add: add, |
| 141 | setCurrent: setCurrent, |
| 142 | rename: rename, |
| 143 | setFp: setFp, |
| 144 | remove: remove, |
| 145 | prefix: prefix, |
| 146 | opfsNs: opfsNs, |
| 147 | }; |
| 148 | })(); |