| … | … | @@ -31,6 +31,20 @@ export class IdentityRegistry { |
| 31 | 31 | // Separate Hyperbee for direct key lookups (identity records, pubkey index) |
| 32 | 32 | // This is NOT the Autobase view — it's a direct-write store for identity metadata |
| 33 | 33 | this.db = opts.db || null |
| 34 | |
| 35 | // Mirror support: when bootstrapKey is passed, join the primary's |
| 36 | // Autobase instead of creating a fresh one. |
| 37 | this._bootstrapKey = opts.bootstrapKey || null |
| 38 | |
| 39 | // keyPair: when provided, the Autobase writer's local core uses |
| 40 | // this Ed25519 keypair instead of a corestore-derived one. This |
| 41 | // lets the caller pin the autobase ROOT to something stable |
| 42 | // (e.g. the hyperident device keypair) so the root is deterministic |
| 43 | // across machines and doesn't need to be shipped as a separate |
| 44 | // bootstrap key via bundles. The primary's autobase root ends up |
| 45 | // equal to keyPair.publicKey; mirrors pass that same publicKey as |
| 46 | // bootstrapKey to join. |
| 47 | this._keyPair = opts.keyPair || null |
| 34 | 48 | } |
| 35 | 49 | |
| 36 | 50 | /** |
| … | … | @@ -41,7 +55,13 @@ export class IdentityRegistry { |
| 41 | 55 | |
| 42 | 56 | const apply = createApplyFunction({ prefix: this._prefix }) |
| 43 | 57 | |
| 44 | | this._base = new Autobase(this._store, null, { |
| 58 | const rootKey = this._bootstrapKey |
| 59 | ? (typeof this._bootstrapKey === 'string' |
| 60 | ? Buffer.from(this._bootstrapKey, 'hex') |
| 61 | : this._bootstrapKey) |
| 62 | : null |
| 63 | |
| 64 | this._base = new Autobase(this._store, rootKey, { |
| 45 | 65 | apply, |
| 46 | 66 | open (store) { |
| 47 | 67 | return new Hyperbee(store.get('identity-view'), { |
| … | … | @@ -50,7 +70,8 @@ export class IdentityRegistry { |
| 50 | 70 | valueEncoding: 'json' |
| 51 | 71 | }) |
| 52 | 72 | }, |
| 53 | | valueEncoding: 'binary' |
| 73 | valueEncoding: 'binary', |
| 74 | keyPair: this._keyPair || undefined |
| 54 | 75 | }) |
| 55 | 76 | |
| 56 | 77 | await this._base.ready() |
| … | … | @@ -856,11 +877,92 @@ export class IdentityRegistry { |
| 856 | 877 | // ==================== FEDERATION & LIFECYCLE ==================== |
| 857 | 878 | |
| 858 | 879 | /** |
| 859 | | * Add Autobase writer key |
| 880 | * Add an Autobase writer via the op pipeline. Appends an ADD_WRITER |
| 881 | * op that (a) records the writer in the view for audit, and (b) is |
| 882 | * picked up by identity-apply.js to call `base.addWriter` on the |
| 883 | * Autobase host. Going through the op pipeline (instead of calling |
| 884 | * `_base.addWriter` directly) ensures the writer registration |
| 885 | * linearizes across replicas and is visible in the shared view. |
| 886 | * |
| 887 | * @param {string|Buffer} key - writer's local core key (hex or buffer) |
| 888 | * @param {object} [opts] |
| 889 | * @param {string} [opts.role='writer'] - 'writer' | 'indexer' |
| 890 | * @param {string} [opts.addedBy] - who added this writer (audit) |
| 891 | */ |
| 892 | async addWriter (key, opts = {}) { |
| 893 | const writerKey = typeof key === 'string' |
| 894 | ? key |
| 895 | : b4a.toString(key, 'hex') |
| 896 | if (!writerKey || writerKey.length !== 64) { |
| 897 | throw new Error('addWriter requires a 32-byte (64-char hex) writer key') |
| 898 | } |
| 899 | const op = { |
| 900 | type: OpTypes.ADD_WRITER, |
| 901 | writerKey, |
| 902 | role: opts.role || 'writer', |
| 903 | addedBy: opts.addedBy || null |
| 904 | } |
| 905 | await this._base.append(encodeOp(op)) |
| 906 | } |
| 907 | |
| 908 | /** |
| 909 | * Announce the local-writer keys a linked device has on OTHER |
| 910 | * autobases in the enclosing app. Recorded in the view under |
| 911 | * `mirror-writers:<devicePubkey>`. The enclosing app is responsible |
| 912 | * for watching that key range and issuing canonical in-band |
| 913 | * addWriter ops on the matching autobases. |
| 914 | * |
| 915 | * @param {string} devicePubkey — this device's hyperident pubkey (hex) |
| 916 | * @param {Object<string,string>} keys — { namespace: localKeyHex, ... } |
| 917 | */ |
| 918 | async registerMirrorWriters (devicePubkey, keys) { |
| 919 | if (!devicePubkey || typeof devicePubkey !== 'string' || devicePubkey.length !== 64) { |
| 920 | throw new Error('registerMirrorWriters requires 64-char hex devicePubkey') |
| 921 | } |
| 922 | if (!keys || typeof keys !== 'object') { |
| 923 | throw new Error('registerMirrorWriters requires a namespace→hex map') |
| 924 | } |
| 925 | const op = { |
| 926 | type: OpTypes.REGISTER_MIRROR_WRITERS, |
| 927 | devicePubkey, |
| 928 | keys |
| 929 | } |
| 930 | await this._base.append(encodeOp(op)) |
| 931 | } |
| 932 | |
| 933 | /** |
| 934 | * Read the most recent mirror-writer announcement for a device. |
| 935 | */ |
| 936 | async getMirrorWriters (devicePubkey) { |
| 937 | const entry = await this._view.get(`${this._prefix}mirror-writers:${devicePubkey}`) |
| 938 | return entry ? entry.value : null |
| 939 | } |
| 940 | |
| 941 | /** |
| 942 | * Iterate all mirror-writer announcements (for watcher wiring). |
| 860 | 943 | */ |
| 861 | | async addWriter (key) { |
| 862 | | const keyBuf = typeof key === 'string' ? b4a.from(key, 'hex') : key |
| 863 | | await this._base.addWriter(keyBuf) |
| 944 | async * listMirrorWriters () { |
| 945 | for await (const entry of this._view.createReadStream({ |
| 946 | gte: `${this._prefix}mirror-writers:`, |
| 947 | lt: `${this._prefix}mirror-writers:\xff` |
| 948 | })) { |
| 949 | const devicePubkey = entry.key.slice((this._prefix + 'mirror-writers:').length) |
| 950 | yield { devicePubkey, ...entry.value } |
| 951 | } |
| 952 | } |
| 953 | |
| 954 | /** |
| 955 | * Remove an Autobase writer via the op pipeline. Symmetric with addWriter. |
| 956 | */ |
| 957 | async removeWriter (key) { |
| 958 | const writerKey = typeof key === 'string' |
| 959 | ? key |
| 960 | : b4a.toString(key, 'hex') |
| 961 | const op = { |
| 962 | type: OpTypes.REMOVE_WRITER, |
| 963 | writerKey |
| 964 | } |
| 965 | await this._base.append(encodeOp(op)) |
| 864 | 966 | } |
| 865 | 967 | |
| 866 | 968 | /** |