anproto personal data server

rebrand bog5 server as APDS

+3 -44
README.md
··· 1 - # Bog5 Protocol 2 - 3 - The 5th Bogbook protocol implementation. 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. 47 48 --- 49 MIT
··· 1 + # APDS 2 3 + A personal data server (pds) for ANProto https://anproto.com/ 4 5 + Try it at https://pds.anproto.com/ 6 7 --- 8 MIT
+50 -64
bogbot.js
··· 1 - import nacl from './lib/nacl-fast-es.js' 2 - import { decode, encode } from './lib/base64.js' 3 import { cachekv } from './lib/cachekv.js' 4 import { human } from './lib/human.js' 5 import { vb } from './lib/vb.js' 6 7 let db 8 let hashLog = [] ··· 10 let newMessages = false 11 let sort = true 12 13 - export const bogbot = {} 14 15 - bogbot.start = async (appId) => { 16 db = await cachekv(appId) 17 18 setInterval(async () => { ··· 40 try { 41 const obj = { 42 hash, 43 - sig: await bogbot.get(hash) 44 } 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)) 48 obj.ts = obj.opened.substring(0, 13) 49 newArray.push(obj) 50 } catch (err) { console.log(err) } ··· 65 }, 20000) 66 } 67 68 - bogbot.generate = async () => { 69 - const genkey = nacl.sign.keyPair() 70 - const keygen = encode(genkey.publicKey) + encode(genkey.secretKey) 71 - return keygen 72 } 73 74 - bogbot.keypair = async () => { 75 const keypair = await db.get('keypair') 76 if (keypair) { 77 return keypair 78 } 79 } 80 81 - bogbot.pubkey = async () => { 82 - const keypair = await bogbot.keypair() 83 if (keypair) { 84 return keypair.substring(0, 44) 85 } 86 } 87 88 - bogbot.privkey = async () => { 89 - const keypair = await bogbot.keypair() 90 if (keypair) { 91 return keypair.substring(44) 92 } 93 } 94 95 - bogbot.deletekey = async () => { 96 db.rm('keypair') 97 } 98 99 - bogbot.clear = async () => { 100 db.clear() 101 } 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() 115 - 116 - const hash = await bogbot.make(data) 117 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 db.put('previous', protocolMsg) 122 return protocolMsg 123 } 124 125 - bogbot.open = async (msg) => { 126 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 133 } catch (err) { 134 //console.log('Not a valid Bog5 protocol message') 135 } ··· 137 138 import { yaml } from './lib/yaml.js' 139 140 - bogbot.parseYaml = async (doc) => { 141 return await yaml.parse(doc) 142 } 143 144 - bogbot.createYaml = async (obj, content) => { 145 return await yaml.create(obj, content) 146 } 147 148 - bogbot.compose = async (content, prev) => { 149 let obj = {} 150 if (prev) { obj = prev } 151 ··· 158 if (previous) { obj.previous = previous} 159 160 if (Object.keys(obj).length > 0) { 161 - const yaml = await bogbot.createYaml(obj, content) 162 - return await bogbot.sign(yaml) 163 } else { 164 - return await bogbot.sign(content) 165 } 166 } 167 168 - bogbot.make = async (data) => { 169 - const hash = await bogbot.hash(data) 170 171 await db.put(hash, data) 172 173 return hash 174 } 175 176 - bogbot.get = async (hash) => { 177 const blob = await db.get(hash) 178 179 return blob 180 } 181 182 - bogbot.put = async (key, value) => { 183 await db.put(key, value) 184 } 185 186 - bogbot.rm = async (key) => { 187 await db.rm(key) 188 } 189 190 - bogbot.add = async (msg) => { 191 - const opened = await bogbot.open(msg) 192 if (opened) { 193 - const hash = await bogbot.make(msg) 194 if (!hashLog.includes(hash)) { 195 hashLog.push(hash) 196 const obj = { ··· 199 } 200 obj.author = obj.sig.substring(0, 44) 201 obj.opened = opened 202 - obj.text = await bogbot.get(obj.opened.substring(13)) 203 obj.ts = obj.opened.substring(0, 13) 204 openedLog.push(obj) 205 newMessages = true ··· 208 } 209 } 210 211 - bogbot.getHashLog = async () => { return hashLog } 212 213 - bogbot.getOpenedLog = async () => { return openedLog } 214 215 - bogbot.query = async (query) => { 216 if (openedLog[0] && !query) { return openedLog } 217 if (openedLog[0] && query.startsWith('?')) { 218 const search = query.substring(1).replace(/%20/g, ' ').toUpperCase() ··· 224 } 225 } 226 227 - bogbot.getPubkeys = async () => { 228 - const arr = await bogbot.query() 229 const newSet = new Set() 230 for (const msg of arr) { 231 newSet.add(msg.author) ··· 234 return newArr 235 } 236 237 - bogbot.getLatest = async (pubkey) => { 238 const q = openedLog.filter(msg => msg.author === pubkey) 239 return q[q.length -1] 240 } 241 242 - bogbot.human = async (ts) => { 243 return await human(new Date(parseInt(ts))) 244 } 245 246 - bogbot.visual = async (pubkey) => { 247 return vb(decode(pubkey), 256) 248 } 249
··· 1 + import { decode } from './lib/base64.js' 2 import { cachekv } from './lib/cachekv.js' 3 import { human } from './lib/human.js' 4 import { vb } from './lib/vb.js' 5 + import { an } from 'https://esm.sh/gh/evbogue/anproto@f7cd761/an.js' 6 7 let db 8 let hashLog = [] ··· 10 let newMessages = false 11 let sort = true 12 13 + export const apds = {} 14 15 + apds.start = async (appId) => { 16 db = await cachekv(appId) 17 18 setInterval(async () => { ··· 40 try { 41 const obj = { 42 hash, 43 + sig: await apds.get(hash) 44 } 45 obj.author = obj.sig.substring(0, 44) 46 + obj.opened = await apds.open(obj.sig) 47 + obj.text = await apds.get(obj.opened.substring(13)) 48 obj.ts = obj.opened.substring(0, 13) 49 newArray.push(obj) 50 } catch (err) { console.log(err) } ··· 65 }, 20000) 66 } 67 68 + apds.generate = async () => { 69 + const genkey = await an.gen() 70 + return genkey 71 } 72 73 + apds.keypair = async () => { 74 const keypair = await db.get('keypair') 75 if (keypair) { 76 return keypair 77 } 78 } 79 80 + apds.pubkey = async () => { 81 + const keypair = await apds.keypair() 82 + console.log(keypair) 83 if (keypair) { 84 return keypair.substring(0, 44) 85 } 86 } 87 88 + apds.privkey = async () => { 89 + const keypair = await apds.keypair() 90 if (keypair) { 91 return keypair.substring(44) 92 } 93 } 94 95 + apds.deletekey = async () => { 96 db.rm('keypair') 97 } 98 99 + apds.clear = async () => { 100 db.clear() 101 } 102 103 + apds.hash = async (data) => { return await an.hash(data) } 104 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) 111 112 db.put('previous', protocolMsg) 113 return protocolMsg 114 } 115 116 + apds.open = async (msg) => { 117 try { 118 + return await an.open(msg) 119 } catch (err) { 120 //console.log('Not a valid Bog5 protocol message') 121 } ··· 123 124 import { yaml } from './lib/yaml.js' 125 126 + apds.parseYaml = async (doc) => { 127 return await yaml.parse(doc) 128 } 129 130 + apds.createYaml = async (obj, content) => { 131 return await yaml.create(obj, content) 132 } 133 134 + apds.compose = async (content, prev) => { 135 let obj = {} 136 if (prev) { obj = prev } 137 ··· 144 if (previous) { obj.previous = previous} 145 146 if (Object.keys(obj).length > 0) { 147 + const yaml = await apds.createYaml(obj, content) 148 + return await apds.sign(yaml) 149 } else { 150 + return await apds.sign(content) 151 } 152 } 153 154 + apds.make = async (data) => { 155 + const hash = await apds.hash(data) 156 157 await db.put(hash, data) 158 159 return hash 160 } 161 162 + apds.get = async (hash) => { 163 const blob = await db.get(hash) 164 165 return blob 166 } 167 168 + apds.put = async (key, value) => { 169 await db.put(key, value) 170 } 171 172 + apds.rm = async (key) => { 173 await db.rm(key) 174 } 175 176 + apds.add = async (msg) => { 177 + const opened = await apds.open(msg) 178 if (opened) { 179 + const hash = await apds.make(msg) 180 if (!hashLog.includes(hash)) { 181 hashLog.push(hash) 182 const obj = { ··· 185 } 186 obj.author = obj.sig.substring(0, 44) 187 obj.opened = opened 188 + obj.text = await apds.get(obj.opened.substring(13)) 189 obj.ts = obj.opened.substring(0, 13) 190 openedLog.push(obj) 191 newMessages = true ··· 194 } 195 } 196 197 + apds.getHashLog = async () => { return hashLog } 198 199 + apds.getOpenedLog = async () => { return openedLog } 200 201 + apds.query = async (query) => { 202 if (openedLog[0] && !query) { return openedLog } 203 if (openedLog[0] && query.startsWith('?')) { 204 const search = query.substring(1).replace(/%20/g, ' ').toUpperCase() ··· 210 } 211 } 212 213 + apds.getPubkeys = async () => { 214 + const arr = await apds.query() 215 const newSet = new Set() 216 for (const msg of arr) { 217 newSet.add(msg.author) ··· 220 return newArr 221 } 222 223 + apds.getLatest = async (pubkey) => { 224 const q = openedLog.filter(msg => msg.author === pubkey) 225 return q[q.length -1] 226 } 227 228 + apds.human = async (ts) => { 229 return await human(new Date(parseInt(ts))) 230 } 231 232 + apds.visual = async (pubkey) => { 233 return vb(decode(pubkey), 256) 234 } 235
+2 -2
composer.js
··· 1 - import { bogbot } from './bogbot.js' 2 import { render } from './render.js' 3 4 export const composer = async () => { ··· 14 b.textContent = 'Sign' 15 16 b.onclick = async () => { 17 - const published = await bogbot.compose(ta.value) 18 ta.value = '' 19 const scroller = document.getElementById('scroller') 20 await render.hash(published, scroller)
··· 1 + import { apds } from './apds.js' 2 import { render } from './render.js' 3 4 export const composer = async () => { ··· 14 b.textContent = 'Sign' 15 16 b.onclick = async () => { 17 + const published = await apds.compose(ta.value) 18 ta.value = '' 19 const scroller = document.getElementById('scroller') 20 await render.hash(published, scroller)
+7 -6
example.js
··· 1 - import { bogbot } from './bogbot.js' 2 import { profile } from './profile.js' 3 import { composer } from './composer.js' 4 import { render } from './render.js' 5 6 - await bogbot.start('bog5example') 7 8 - if (!await bogbot.pubkey()) { 9 - const keypair = await bogbot.generate() 10 - await bogbot.put('keypair', keypair) 11 } 12 13 document.body.appendChild(await profile()) ··· 18 19 document.body.appendChild(scroller) 20 21 - const log = await bogbot.query() 22 23 log.forEach(async (obj) => { 24 await render.hash(obj.hash, scroller)
··· 1 + import { apds } from './apds.js' 2 import { profile } from './profile.js' 3 import { composer } from './composer.js' 4 import { render } from './render.js' 5 6 + await apds.start('apds1') 7 8 + if (!await apds.pubkey()) { 9 + const keypair = await apds.generate() 10 + console.log(keypair) 11 + await apds.put('keypair', keypair) 12 } 13 14 document.body.appendChild(await profile()) ··· 19 20 document.body.appendChild(scroller) 21 22 + const log = await apds.query() 23 24 log.forEach(async (obj) => { 25 await render.hash(obj.hash, scroller)
+11 -11
profile.js
··· 1 import { h } from './lib/h.js' 2 - import { bogbot } from './bogbot.js' 3 4 export const profile = async () => { 5 const div = h('div') 6 7 - const avatarImg = await bogbot.visual(await bogbot.pubkey()) 8 9 - const existingImage = await bogbot.get('image') 10 11 - if (existingImage) { avatarImg.src = await bogbot.get(existingImage)} 12 13 avatarImg.style = 'height: 30px; width: 30px; float: left; margin-right: 5px; object-fit: cover;' 14 ··· 46 ctx.drawImage(img, 0, 0, width, height, 0, 0, cropWidth, cropHeight) 47 const croppedImage = canvas.toDataURL() 48 avatarImg.src = croppedImage 49 - const hash = await bogbot.make(croppedImage) 50 - await bogbot.put('image', hash) 51 } else { 52 const croppedImage = canvas.toDataURL() 53 avatarImg.src = img.src 54 - const hash = await bogbot.make(img.src) 55 - await bogbot.put('image', hash) 56 } 57 } 58 img.src = e.target.result ··· 65 66 div.appendChild(avatarImg) 67 68 - div.appendChild(h('div', [await bogbot.pubkey()])) 69 70 - const name = await bogbot.get('name') 71 72 const namer = h('input', { 73 placeholder: name || 'Name yourself' ··· 79 h('button', {onclick: async () => { 80 if (namer.value) { 81 namer.placeholder = namer.value 82 - await bogbot.put('name', namer.value) 83 namer.value = '' 84 } 85 }}, ['Save'])
··· 1 import { h } from './lib/h.js' 2 + import { apds } from './apds.js' 3 4 export const profile = async () => { 5 const div = h('div') 6 7 + const avatarImg = await apds.visual(await apds.pubkey()) 8 9 + const existingImage = await apds.get('image') 10 11 + if (existingImage) { avatarImg.src = await apds.get(existingImage)} 12 13 avatarImg.style = 'height: 30px; width: 30px; float: left; margin-right: 5px; object-fit: cover;' 14 ··· 46 ctx.drawImage(img, 0, 0, width, height, 0, 0, cropWidth, cropHeight) 47 const croppedImage = canvas.toDataURL() 48 avatarImg.src = croppedImage 49 + const hash = await apds.make(croppedImage) 50 + await apds.put('image', hash) 51 } else { 52 const croppedImage = canvas.toDataURL() 53 avatarImg.src = img.src 54 + const hash = await apds.make(img.src) 55 + await apds.put('image', hash) 56 } 57 } 58 img.src = e.target.result ··· 65 66 div.appendChild(avatarImg) 67 68 + div.appendChild(h('div', [await apds.pubkey()])) 69 70 + const name = await apds.get('name') 71 72 const namer = h('input', { 73 placeholder: name || 'Name yourself' ··· 79 h('button', {onclick: async () => { 80 if (namer.value) { 81 namer.placeholder = namer.value 82 + await apds.put('name', namer.value) 83 namer.value = '' 84 } 85 }}, ['Save'])
+10 -10
render.js
··· 1 - import { bogbot } from './bogbot.js' 2 import { h } from './lib/h.js' 3 4 export const render = {} 5 6 render.blob = async (blob) => { 7 - const hash = await bogbot.hash(blob) 8 9 const div = await document.getElementById(hash) 10 11 try { 12 - const opened = await bogbot.open(blob) 13 - const ts = h('span', [await bogbot.human(opened.substring(0, 13))]) 14 setInterval(async () => { 15 - ts.textContent = await bogbot.human(opened.substring(0, 13)) 16 }, 1000) 17 if (div) { 18 - const img = await bogbot.visual(blob.substring(0, 44)) 19 img.id = 'image' 20 img.style = 'width: 30px; height: 30px; float: left; margin-right: 5px; object-fit: cover;' 21 div.appendChild(img) 22 div.appendChild(h('a', {href: '#' + blob.substring(0, 44), id: 'name'}, [blob.substring(0, 10)])) 23 div.appendChild(h('a', {href: '#' + hash, style: 'float: right;'}, [ts])) 24 div.appendChild(h('div', {id: opened.substring(13)})) 25 - const content = await bogbot.get(opened.substring(13)) 26 if (content) { 27 await render.blob(content) 28 } ··· 31 } 32 } catch (err) { 33 console.log('Not a valid protocol message') 34 - const yaml = await bogbot.parseYaml(blob) 35 if (div) { 36 div.textContent = yaml.body 37 div.parentNode.childNodes.forEach(async (node) => { ··· 39 node.textContent = yaml.name 40 } 41 if (yaml.image && node.id === 'image') { 42 - const image = await bogbot.get(yaml.image) 43 node.src = image 44 } 45 }) ··· 53 54 scroller.insertBefore(div, scroller.firstChild) 55 56 - const sig = await bogbot.get(hash) 57 58 if (sig) { 59 await render.blob(sig)
··· 1 + import { apds } from './apds.js' 2 import { h } from './lib/h.js' 3 4 export const render = {} 5 6 render.blob = async (blob) => { 7 + const hash = await apds.hash(blob) 8 9 const div = await document.getElementById(hash) 10 11 try { 12 + const opened = await apds.open(blob) 13 + const ts = h('span', [await apds.human(opened.substring(0, 13))]) 14 setInterval(async () => { 15 + ts.textContent = await apds.human(opened.substring(0, 13)) 16 }, 1000) 17 if (div) { 18 + const img = await apds.visual(blob.substring(0, 44)) 19 img.id = 'image' 20 img.style = 'width: 30px; height: 30px; float: left; margin-right: 5px; object-fit: cover;' 21 div.appendChild(img) 22 div.appendChild(h('a', {href: '#' + blob.substring(0, 44), id: 'name'}, [blob.substring(0, 10)])) 23 div.appendChild(h('a', {href: '#' + hash, style: 'float: right;'}, [ts])) 24 div.appendChild(h('div', {id: opened.substring(13)})) 25 + const content = await apds.get(opened.substring(13)) 26 if (content) { 27 await render.blob(content) 28 } ··· 31 } 32 } catch (err) { 33 console.log('Not a valid protocol message') 34 + const yaml = await apds.parseYaml(blob) 35 if (div) { 36 div.textContent = yaml.body 37 div.parentNode.childNodes.forEach(async (node) => { ··· 39 node.textContent = yaml.name 40 } 41 if (yaml.image && node.id === 'image') { 42 + const image = await apds.get(yaml.image) 43 node.src = image 44 } 45 }) ··· 53 54 scroller.insertBefore(div, scroller.firstChild) 55 56 + const sig = await apds.get(hash) 57 58 if (sig) { 59 await render.blob(sig)