| … | … | @@ -1,49 +1,8 @@ |
| 1 | # Bog5 Protocol | |
| 1 | # APDS | |
| 2 | 2 | |
| 3 | The 5th Bogbook protocol implementation. | |
| 3 | A personal data server (pds) for ANProto https://anproto.com/ | |
| 4 | 4 | |
| 5 | Try it at https://bog5.deno.dev/ | |
| 6 | ||
| 7 | Please note the above link is not networked. For a fuller and networked version be sure to check out https://wiredove.net/ | |
| 8 | ||
| 9 | ### The Protocol | |
| 10 | ||
| 11 | Bogbook has been thru a few iterations. For the fans, thanks for sticking with me. For the people who want standards, I solicit your feedback. And now, the protocol... | |
| 12 | ||
| 13 | We send around sha256 hashes since we're using content-addressable storage for everything. First we request the hash that finds us a **protocol message**: | |
| 14 | ||
| 15 | ``` | |
| 16 | <ed25519 pubkey><ed25519 sig> | |
| 17 | ``` | |
| 18 | ||
| 19 | This opens to: | |
| 20 | ||
| 21 | ``` | |
| 22 | <unix timestamp><sha256 hash> | |
| 23 | ``` | |
| 24 | ||
| 25 | The timestamp is required since this is a social network. The sha256 hashlinks either locally or over the gossip network to a **content message**, which is Yaml file that contains everything else we need to make a post render while not duplicating too much of what we might have already such as avatar photos. | |
| 26 | ||
| 27 | Everything in the **protocol message** is we need to sort and authenticate a feed. Everything in the **content message** is what we need to render a message. | |
| 28 | ||
| 29 | ``` | |
| 30 | Text content | |
| 31 | ``` | |
| 32 | ||
| 33 | ||
| 34 | A full post might look like this: | |
| 35 | ||
| 36 | ``` | |
| 37 | --- | |
| 38 | previous: sha256 hash previous protocol message | |
| 39 | name: Ev | |
| 40 | image: sha256 of image blob | |
| 41 | edit: sha256 hash of edited protocol message | |
| 42 | --- | |
| 43 | Content | |
| 44 | ``` | |
| 45 | ||
| 46 | This means you could in theory edit your name and/or image on a post! But we also want a record of what your name and image was on the original post, since it is a security issue with some actors on decentralized social networks if you allow publishers to change their name over their entire feed. Or, for example, in a hack it would be harder to rewrite history. | |
| 5 | Try it at https://pds.anproto.com/ | |
| 47 | 6 | |
| 48 | 7 | --- |
| 49 | 8 | MIT |
| … | … | @@ -1,8 +1,8 @@ |
| 1 | import nacl from './lib/nacl-fast-es.js' | |
| 2 | import { decode, encode } from './lib/base64.js' | |
| 1 | import { decode } from './lib/base64.js' | |
| 3 | 2 | import { cachekv } from './lib/cachekv.js' |
| 4 | 3 | import { human } from './lib/human.js' |
| 5 | 4 | import { vb } from './lib/vb.js' |
| 5 | import { an } from 'https://esm.sh/gh/evbogue/anproto@f7cd761/an.js' | |
| 6 | 6 | |
| 7 | 7 | let db |
| 8 | 8 | let hashLog = [] |
| … | … | @@ -10,9 +10,9 @@ let openedLog = [] |
| 10 | 10 | let newMessages = false |
| 11 | 11 | let sort = true |
| 12 | 12 | |
| 13 | export const bogbot = {} | |
| 13 | export const apds = {} | |
| 14 | 14 | |
| 15 | bogbot.start = async (appId) => { | |
| 15 | apds.start = async (appId) => { | |
| 16 | 16 | db = await cachekv(appId) |
| 17 | 17 | |
| 18 | 18 | setInterval(async () => { |
| … | … | @@ -40,11 +40,11 @@ bogbot.start = async (appId) => { |
| 40 | 40 | try { |
| 41 | 41 | const obj = { |
| 42 | 42 | hash, |
| 43 | sig: await bogbot.get(hash) | |
| 43 | sig: await apds.get(hash) | |
| 44 | 44 | } |
| 45 | 45 | obj.author = obj.sig.substring(0, 44) |
| 46 | obj.opened = await bogbot.open(obj.sig) | |
| 47 | obj.text = await bogbot.get(obj.opened.substring(13)) | |
| 46 | obj.opened = await apds.open(obj.sig) | |
| 47 | obj.text = await apds.get(obj.opened.substring(13)) | |
| 48 | 48 | obj.ts = obj.opened.substring(0, 13) |
| 49 | 49 | newArray.push(obj) |
| 50 | 50 | } catch (err) { console.log(err) } |
| … | … | @@ -65,71 +65,57 @@ bogbot.start = async (appId) => { |
| 65 | 65 | }, 20000) |
| 66 | 66 | } |
| 67 | 67 | |
| 68 | bogbot.generate = async () => { | |
| 69 | const genkey = nacl.sign.keyPair() | |
| 70 | const keygen = encode(genkey.publicKey) + encode(genkey.secretKey) | |
| 71 | return keygen | |
| 68 | apds.generate = async () => { | |
| 69 | const genkey = await an.gen() | |
| 70 | return genkey | |
| 72 | 71 | } |
| 73 | 72 | |
| 74 | bogbot.keypair = async () => { | |
| 73 | apds.keypair = async () => { | |
| 75 | 74 | const keypair = await db.get('keypair') |
| 76 | 75 | if (keypair) { |
| 77 | 76 | return keypair |
| 78 | 77 | } |
| 79 | 78 | } |
| 80 | 79 | |
| 81 | bogbot.pubkey = async () => { | |
| 82 | const keypair = await bogbot.keypair() | |
| 80 | apds.pubkey = async () => { | |
| 81 | const keypair = await apds.keypair() | |
| 82 | console.log(keypair) | |
| 83 | 83 | if (keypair) { |
| 84 | 84 | return keypair.substring(0, 44) |
| 85 | 85 | } |
| 86 | 86 | } |
| 87 | 87 | |
| 88 | bogbot.privkey = async () => { | |
| 89 | const keypair = await bogbot.keypair() | |
| 88 | apds.privkey = async () => { | |
| 89 | const keypair = await apds.keypair() | |
| 90 | 90 | if (keypair) { |
| 91 | 91 | return keypair.substring(44) |
| 92 | 92 | } |
| 93 | 93 | } |
| 94 | 94 | |
| 95 | bogbot.deletekey = async () => { | |
| 95 | apds.deletekey = async () => { | |
| 96 | 96 | db.rm('keypair') |
| 97 | 97 | } |
| 98 | 98 | |
| 99 | bogbot.clear = async () => { | |
| 99 | apds.clear = async () => { | |
| 100 | 100 | db.clear() |
| 101 | 101 | } |
| 102 | 102 | |
| 103 | bogbot.hash = async (data) => { | |
| 104 | return encode( | |
| 105 | Array.from( | |
| 106 | new Uint8Array( | |
| 107 | await crypto.subtle.digest("SHA-256", new TextEncoder().encode(data)) | |
| 108 | ) | |
| 109 | ) | |
| 110 | ) | |
| 111 | } | |
| 112 | ||
| 113 | bogbot.sign = async (data) => { | |
| 114 | const timestamp = Date.now() | |
| 103 | apds.hash = async (data) => { return await an.hash(data) } | |
| 115 | 104 | |
| 116 | const hash = await bogbot.make(data) | |
| 105 | apds.sign = async (data) => { | |
| 106 | const hash = await apds.make(data) | |
| 107 | const sig = await an.sign(hash, await apds.keypair()) | |
| 108 | console.log(sig) | |
| 109 | await apds.add(sig) | |
| 110 | const protocolMsg = await apds.make(sig) | |
| 117 | 111 | |
| 118 | const sig = encode(nacl.sign(new TextEncoder().encode(timestamp + hash), decode(await bogbot.privkey()))) | |
| 119 | await bogbot.add(await bogbot.pubkey() + sig) | |
| 120 | const protocolMsg = await bogbot.make(await bogbot.pubkey() + sig) | |
| 121 | 112 | db.put('previous', protocolMsg) |
| 122 | 113 | return protocolMsg |
| 123 | 114 | } |
| 124 | 115 | |
| 125 | bogbot.open = async (msg) => { | |
| 116 | apds.open = async (msg) => { | |
| 126 | 117 | try { |
| 127 | const pubkey = msg.substring(0, 44) | |
| 128 | const sig = msg.substring(44) | |
| 129 | ||
| 130 | const opened = new TextDecoder().decode(nacl.sign.open(decode(sig), decode(pubkey))) | |
| 131 | ||
| 132 | return opened | |
| 118 | return await an.open(msg) | |
| 133 | 119 | } catch (err) { |
| 134 | 120 | //console.log('Not a valid Bog5 protocol message') |
| 135 | 121 | } |
| … | … | @@ -137,15 +123,15 @@ bogbot.open = async (msg) => { |
| 137 | 123 | |
| 138 | 124 | import { yaml } from './lib/yaml.js' |
| 139 | 125 | |
| 140 | bogbot.parseYaml = async (doc) => { | |
| 126 | apds.parseYaml = async (doc) => { | |
| 141 | 127 | return await yaml.parse(doc) |
| 142 | 128 | } |
| 143 | 129 | |
| 144 | bogbot.createYaml = async (obj, content) => { | |
| 130 | apds.createYaml = async (obj, content) => { | |
| 145 | 131 | return await yaml.create(obj, content) |
| 146 | 132 | } |
| 147 | 133 | |
| 148 | bogbot.compose = async (content, prev) => { | |
| 134 | apds.compose = async (content, prev) => { | |
| 149 | 135 | let obj = {} |
| 150 | 136 | if (prev) { obj = prev } |
| 151 | 137 | |
| … | … | @@ -158,39 +144,39 @@ bogbot.compose = async (content, prev) => { |
| 158 | 144 | if (previous) { obj.previous = previous} |
| 159 | 145 | |
| 160 | 146 | if (Object.keys(obj).length > 0) { |
| 161 | const yaml = await bogbot.createYaml(obj, content) | |
| 162 | return await bogbot.sign(yaml) | |
| 147 | const yaml = await apds.createYaml(obj, content) | |
| 148 | return await apds.sign(yaml) | |
| 163 | 149 | } else { |
| 164 | return await bogbot.sign(content) | |
| 150 | return await apds.sign(content) | |
| 165 | 151 | } |
| 166 | 152 | } |
| 167 | 153 | |
| 168 | bogbot.make = async (data) => { | |
| 169 | const hash = await bogbot.hash(data) | |
| 154 | apds.make = async (data) => { | |
| 155 | const hash = await apds.hash(data) | |
| 170 | 156 | |
| 171 | 157 | await db.put(hash, data) |
| 172 | 158 | |
| 173 | 159 | return hash |
| 174 | 160 | } |
| 175 | 161 | |
| 176 | bogbot.get = async (hash) => { | |
| 162 | apds.get = async (hash) => { | |
| 177 | 163 | const blob = await db.get(hash) |
| 178 | 164 | |
| 179 | 165 | return blob |
| 180 | 166 | } |
| 181 | 167 | |
| 182 | bogbot.put = async (key, value) => { | |
| 168 | apds.put = async (key, value) => { | |
| 183 | 169 | await db.put(key, value) |
| 184 | 170 | } |
| 185 | 171 | |
| 186 | bogbot.rm = async (key) => { | |
| 172 | apds.rm = async (key) => { | |
| 187 | 173 | await db.rm(key) |
| 188 | 174 | } |
| 189 | 175 | |
| 190 | bogbot.add = async (msg) => { | |
| 191 | const opened = await bogbot.open(msg) | |
| 176 | apds.add = async (msg) => { | |
| 177 | const opened = await apds.open(msg) | |
| 192 | 178 | if (opened) { |
| 193 | const hash = await bogbot.make(msg) | |
| 179 | const hash = await apds.make(msg) | |
| 194 | 180 | if (!hashLog.includes(hash)) { |
| 195 | 181 | hashLog.push(hash) |
| 196 | 182 | const obj = { |
| … | … | @@ -199,7 +185,7 @@ bogbot.add = async (msg) => { |
| 199 | 185 | } |
| 200 | 186 | obj.author = obj.sig.substring(0, 44) |
| 201 | 187 | obj.opened = opened |
| 202 | obj.text = await bogbot.get(obj.opened.substring(13)) | |
| 188 | obj.text = await apds.get(obj.opened.substring(13)) | |
| 203 | 189 | obj.ts = obj.opened.substring(0, 13) |
| 204 | 190 | openedLog.push(obj) |
| 205 | 191 | newMessages = true |
| … | … | @@ -208,11 +194,11 @@ bogbot.add = async (msg) => { |
| 208 | 194 | } |
| 209 | 195 | } |
| 210 | 196 | |
| 211 | bogbot.getHashLog = async () => { return hashLog } | |
| 197 | apds.getHashLog = async () => { return hashLog } | |
| 212 | 198 | |
| 213 | bogbot.getOpenedLog = async () => { return openedLog } | |
| 199 | apds.getOpenedLog = async () => { return openedLog } | |
| 214 | 200 | |
| 215 | bogbot.query = async (query) => { | |
| 201 | apds.query = async (query) => { | |
| 216 | 202 | if (openedLog[0] && !query) { return openedLog } |
| 217 | 203 | if (openedLog[0] && query.startsWith('?')) { |
| 218 | 204 | const search = query.substring(1).replace(/%20/g, ' ').toUpperCase() |
| … | … | @@ -224,8 +210,8 @@ bogbot.query = async (query) => { |
| 224 | 210 | } |
| 225 | 211 | } |
| 226 | 212 | |
| 227 | bogbot.getPubkeys = async () => { | |
| 228 | const arr = await bogbot.query() | |
| 213 | apds.getPubkeys = async () => { | |
| 214 | const arr = await apds.query() | |
| 229 | 215 | const newSet = new Set() |
| 230 | 216 | for (const msg of arr) { |
| 231 | 217 | newSet.add(msg.author) |
| … | … | @@ -234,16 +220,16 @@ bogbot.getPubkeys = async () => { |
| 234 | 220 | return newArr |
| 235 | 221 | } |
| 236 | 222 | |
| 237 | bogbot.getLatest = async (pubkey) => { | |
| 223 | apds.getLatest = async (pubkey) => { | |
| 238 | 224 | const q = openedLog.filter(msg => msg.author === pubkey) |
| 239 | 225 | return q[q.length -1] |
| 240 | 226 | } |
| 241 | 227 | |
| 242 | bogbot.human = async (ts) => { | |
| 228 | apds.human = async (ts) => { | |
| 243 | 229 | return await human(new Date(parseInt(ts))) |
| 244 | 230 | } |
| 245 | 231 | |
| 246 | bogbot.visual = async (pubkey) => { | |
| 232 | apds.visual = async (pubkey) => { | |
| 247 | 233 | return vb(decode(pubkey), 256) |
| 248 | 234 | } |
| 249 | 235 |
| … | … | @@ -1,4 +1,4 @@ |
| 1 | import { bogbot } from './bogbot.js' | |
| 1 | import { apds } from './apds.js' | |
| 2 | 2 | import { render } from './render.js' |
| 3 | 3 | |
| 4 | 4 | export const composer = async () => { |
| … | … | @@ -14,7 +14,7 @@ export const composer = async () => { |
| 14 | 14 | b.textContent = 'Sign' |
| 15 | 15 | |
| 16 | 16 | b.onclick = async () => { |
| 17 | const published = await bogbot.compose(ta.value) | |
| 17 | const published = await apds.compose(ta.value) | |
| 18 | 18 | ta.value = '' |
| 19 | 19 | const scroller = document.getElementById('scroller') |
| 20 | 20 | await render.hash(published, scroller) |
| … | … | @@ -1,13 +1,14 @@ |
| 1 | import { bogbot } from './bogbot.js' | |
| 1 | import { apds } from './apds.js' | |
| 2 | 2 | import { profile } from './profile.js' |
| 3 | 3 | import { composer } from './composer.js' |
| 4 | 4 | import { render } from './render.js' |
| 5 | 5 | |
| 6 | await bogbot.start('bog5example') | |
| 6 | await apds.start('apds1') | |
| 7 | 7 | |
| 8 | if (!await bogbot.pubkey()) { | |
| 9 | const keypair = await bogbot.generate() | |
| 10 | await bogbot.put('keypair', keypair) | |
| 8 | if (!await apds.pubkey()) { | |
| 9 | const keypair = await apds.generate() | |
| 10 | console.log(keypair) | |
| 11 | await apds.put('keypair', keypair) | |
| 11 | 12 | } |
| 12 | 13 | |
| 13 | 14 | document.body.appendChild(await profile()) |
| … | … | @@ -18,7 +19,7 @@ scroller.id = 'scroller' |
| 18 | 19 | |
| 19 | 20 | document.body.appendChild(scroller) |
| 20 | 21 | |
| 21 | const log = await bogbot.query() | |
| 22 | const log = await apds.query() | |
| 22 | 23 | |
| 23 | 24 | log.forEach(async (obj) => { |
| 24 | 25 | await render.hash(obj.hash, scroller) |
| … | … | @@ -1,14 +1,14 @@ |
| 1 | 1 | import { h } from './lib/h.js' |
| 2 | import { bogbot } from './bogbot.js' | |
| 2 | import { apds } from './apds.js' | |
| 3 | 3 | |
| 4 | 4 | export const profile = async () => { |
| 5 | 5 | const div = h('div') |
| 6 | 6 | |
| 7 | const avatarImg = await bogbot.visual(await bogbot.pubkey()) | |
| 7 | const avatarImg = await apds.visual(await apds.pubkey()) | |
| 8 | 8 | |
| 9 | const existingImage = await bogbot.get('image') | |
| 9 | const existingImage = await apds.get('image') | |
| 10 | 10 | |
| 11 | if (existingImage) { avatarImg.src = await bogbot.get(existingImage)} | |
| 11 | if (existingImage) { avatarImg.src = await apds.get(existingImage)} | |
| 12 | 12 | |
| 13 | 13 | avatarImg.style = 'height: 30px; width: 30px; float: left; margin-right: 5px; object-fit: cover;' |
| 14 | 14 | |
| … | … | @@ -46,13 +46,13 @@ export const profile = async () => { |
| 46 | 46 | ctx.drawImage(img, 0, 0, width, height, 0, 0, cropWidth, cropHeight) |
| 47 | 47 | const croppedImage = canvas.toDataURL() |
| 48 | 48 | avatarImg.src = croppedImage |
| 49 | const hash = await bogbot.make(croppedImage) | |
| 50 | await bogbot.put('image', hash) | |
| 49 | const hash = await apds.make(croppedImage) | |
| 50 | await apds.put('image', hash) | |
| 51 | 51 | } else { |
| 52 | 52 | const croppedImage = canvas.toDataURL() |
| 53 | 53 | avatarImg.src = img.src |
| 54 | const hash = await bogbot.make(img.src) | |
| 55 | await bogbot.put('image', hash) | |
| 54 | const hash = await apds.make(img.src) | |
| 55 | await apds.put('image', hash) | |
| 56 | 56 | } |
| 57 | 57 | } |
| 58 | 58 | img.src = e.target.result |
| … | … | @@ -65,9 +65,9 @@ export const profile = async () => { |
| 65 | 65 | |
| 66 | 66 | div.appendChild(avatarImg) |
| 67 | 67 | |
| 68 | div.appendChild(h('div', [await bogbot.pubkey()])) | |
| 68 | div.appendChild(h('div', [await apds.pubkey()])) | |
| 69 | 69 | |
| 70 | const name = await bogbot.get('name') | |
| 70 | const name = await apds.get('name') | |
| 71 | 71 | |
| 72 | 72 | const namer = h('input', { |
| 73 | 73 | placeholder: name || 'Name yourself' |
| … | … | @@ -79,7 +79,7 @@ export const profile = async () => { |
| 79 | 79 | h('button', {onclick: async () => { |
| 80 | 80 | if (namer.value) { |
| 81 | 81 | namer.placeholder = namer.value |
| 82 | await bogbot.put('name', namer.value) | |
| 82 | await apds.put('name', namer.value) | |
| 83 | 83 | namer.value = '' |
| 84 | 84 | } |
| 85 | 85 | }}, ['Save']) |
| … | … | @@ -1,28 +1,28 @@ |
| 1 | import { bogbot } from './bogbot.js' | |
| 1 | import { apds } from './apds.js' | |
| 2 | 2 | import { h } from './lib/h.js' |
| 3 | 3 | |
| 4 | 4 | export const render = {} |
| 5 | 5 | |
| 6 | 6 | render.blob = async (blob) => { |
| 7 | const hash = await bogbot.hash(blob) | |
| 7 | const hash = await apds.hash(blob) | |
| 8 | 8 | |
| 9 | 9 | const div = await document.getElementById(hash) |
| 10 | 10 | |
| 11 | 11 | try { |
| 12 | const opened = await bogbot.open(blob) | |
| 13 | const ts = h('span', [await bogbot.human(opened.substring(0, 13))]) | |
| 12 | const opened = await apds.open(blob) | |
| 13 | const ts = h('span', [await apds.human(opened.substring(0, 13))]) | |
| 14 | 14 | setInterval(async () => { |
| 15 | ts.textContent = await bogbot.human(opened.substring(0, 13)) | |
| 15 | ts.textContent = await apds.human(opened.substring(0, 13)) | |
| 16 | 16 | }, 1000) |
| 17 | 17 | if (div) { |
| 18 | const img = await bogbot.visual(blob.substring(0, 44)) | |
| 18 | const img = await apds.visual(blob.substring(0, 44)) | |
| 19 | 19 | img.id = 'image' |
| 20 | 20 | img.style = 'width: 30px; height: 30px; float: left; margin-right: 5px; object-fit: cover;' |
| 21 | 21 | div.appendChild(img) |
| 22 | 22 | div.appendChild(h('a', {href: '#' + blob.substring(0, 44), id: 'name'}, [blob.substring(0, 10)])) |
| 23 | 23 | div.appendChild(h('a', {href: '#' + hash, style: 'float: right;'}, [ts])) |
| 24 | 24 | div.appendChild(h('div', {id: opened.substring(13)})) |
| 25 | const content = await bogbot.get(opened.substring(13)) | |
| 25 | const content = await apds.get(opened.substring(13)) | |
| 26 | 26 | if (content) { |
| 27 | 27 | await render.blob(content) |
| 28 | 28 | } |
| … | … | @@ -31,7 +31,7 @@ render.blob = async (blob) => { |
| 31 | 31 | } |
| 32 | 32 | } catch (err) { |
| 33 | 33 | console.log('Not a valid protocol message') |
| 34 | const yaml = await bogbot.parseYaml(blob) | |
| 34 | const yaml = await apds.parseYaml(blob) | |
| 35 | 35 | if (div) { |
| 36 | 36 | div.textContent = yaml.body |
| 37 | 37 | div.parentNode.childNodes.forEach(async (node) => { |
| … | … | @@ -39,7 +39,7 @@ render.blob = async (blob) => { |
| 39 | 39 | node.textContent = yaml.name |
| 40 | 40 | } |
| 41 | 41 | if (yaml.image && node.id === 'image') { |
| 42 | const image = await bogbot.get(yaml.image) | |
| 42 | const image = await apds.get(yaml.image) | |
| 43 | 43 | node.src = image |
| 44 | 44 | } |
| 45 | 45 | }) |
| … | … | @@ -53,7 +53,7 @@ render.hash = async (hash, scroller) => { |
| 53 | 53 | |
| 54 | 54 | scroller.insertBefore(div, scroller.firstChild) |
| 55 | 55 | |
| 56 | const sig = await bogbot.get(hash) | |
| 56 | const sig = await apds.get(hash) | |
| 57 | 57 | |
| 58 | 58 | if (sig) { |
| 59 | 59 | await render.blob(sig) |