plan98
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

allow peer to go public

+302 -170
+283 -163
client/public/elves/was-repl.js
··· 1 + import { StorageClient } from "@wallet.storage/fetch-client"; 2 + import { Ed25519Signer } from "@did.coop/did-key-ed25519" 1 3 import elf from '@plan98/elf' 2 - import { getQuickJS } from "quickjs-emscripten" 3 4 4 - const sampleHTML = `<!DOCTYPE html> 5 - <html lang="en"> 6 - <head> 7 - <meta charset="UTF-8" /> 8 - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 9 - <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1" /> 10 - <meta name="apple-mobile-web-app-capable" content="yes"> 11 - <title>&lt;:-)</title> 12 - <style> 13 - :root { 14 - --shadow: 0px 0px 2px 2px rgba(0,0,0,.25), 15 - 0px 0px 6px 6px rgba(0,0,0,.15), 16 - 0px 0px 2rem 2rem rgba(0,0,0,.05); 17 - --red: firebrick; 18 - --orange: darkorange; 19 - --yellow: gold; 20 - --green: mediumseagreen; 21 - --blue: dodgerblue; 22 - --indigo: slateblue; 23 - --purple: mediumpurple; 24 - --violet: mediumpurple; 25 - --gray: dimgray; 26 - } 5 + const contentTypes = { 6 + // Web documents 7 + '.html': 'text/html', 8 + '.css': 'text/css', 9 + '.js': 'text/javascript', 10 + '.json': 'application/json', 11 + '.txt': 'text/plain', 12 + '.jpg': 'image/jpeg', 13 + '.jpeg': 'image/jpeg', 14 + '.png': 'image/png', 15 + '.gif': 'image/gif', 16 + '.svg': 'image/svg+xml', 17 + '.webp': 'image/webp', 18 + '.mp3': 'audio/mpeg', 19 + '.mp4': 'video/mp4', 20 + '.pdf': 'application/pdf', 21 + 'default': 'application/octet-stream' 22 + }; 27 23 28 - * { 29 - box-sizing: border-box; 30 - } 24 + function getContentType(filename) { 25 + const ext = filename.toLowerCase().substring(filename.lastIndexOf('.')); 26 + return contentTypes[ext] || contentTypes.default; 27 + } 31 28 32 - html, body { 33 - height: 100%; 34 - background: rgba(255,255,255,.85); 35 - overscroll-behavior: none; 36 - transform: translateZ(0); 37 - padding: 0; 38 - margin: 0; 39 - } 29 + function getContentTypeByPath(filePath) { 30 + const filename = filePath.split('/').pop() || ''; 31 + return getContentType(filename); 32 + } 40 33 41 - body > *{ 42 - position: relative; 43 - z-index: 2; 44 - } 34 + const $ = elf('was-repl', { 35 + host: plan98.env.PLAN98_WAS_HOST, 36 + path: '', 37 + home: '', 38 + input: 'Hello World' 39 + }) 45 40 46 - main { 47 - position: relative; 48 - height: 100%; 49 - } 41 + function fetchSauce(src) { 42 + fetch(src).then(res => res.text()).then(file => { 43 + $.teach({ input: file, path: src, src: null }) 44 + }) 45 + } 50 46 51 - img { 52 - max-width: 100%; 53 - max-height: 100%; 54 - margin: auto; 47 + let signer 48 + async function init(target) { 49 + if (target.initialized) return 50 + target.initialized = true 51 + 52 + const src = target.getAttribute('src') || '/server.js' 53 + fetchSauce(src) 54 + 55 + const { host } = $.learn() 56 + const credentials = localStorage.getItem('was/signer') 57 + 58 + if (credentials) { 59 + signer = await Ed25519Signer.fromJSON(credentials) 60 + } else { 61 + // This signer can create cryptographic signatures 62 + signer = await Ed25519Signer.generate() 63 + localStorage.setItem('was/signer', JSON.stringify(signer.toJSON())) 64 + } 65 + const storageId = host 66 + const storageUrl = new URL(storageId) 67 + const storage = new StorageClient(storageUrl) 68 + 69 + // create the space with signer so all requests get signed by it 70 + const space = storage.space({ 71 + signer, 72 + id: `urn:uuid:${target.id}` 73 + }) 74 + 75 + const resource = space.resource(target.getAttribute('src') || '/tmp') 76 + const response = await resource.get() 77 + .then(res => { 78 + if (res.status === 200) { 79 + const indexUrl = new URL(resource.path, storageUrl) 80 + $.teach({ home: indexUrl.toString() }) 55 81 } 82 + return res 83 + }) 84 + .catch(e => { 85 + console.debug(e) 86 + }) 87 + } 56 88 57 - button * { 58 - pointer-events: none; 59 - } 89 + async function publish(spaceId) { 90 + const { host, input, path } = $.learn() 91 + const storageId = host 92 + const storageUrl = new URL(storageId) 93 + const storage = new StorageClient(storageUrl) 60 94 61 - body[pathname$=".saga"] main { 62 - height: auto; 63 - } 64 - </style> 65 - <script type="importmap"> 66 - { 67 - "imports": { 68 - "@did.coop/did-key-ed25519": "https://esm.sh/@did.coop/did-key-ed25519", 69 - "@wallet.storage/fetch-client": "https://esm.sh/@wallet.storage/fetch-client@^1.1.3", 70 - "@plan98/elf": "/elf.js" 71 - } 72 - } 73 - </script> 74 - <script> 75 - plan98 = { 76 - env: { 77 - PLAN98_WAS_HOST: "http://localhost:8080" 78 - } 79 - } 80 - </script> 81 - <script async type="module" src="/main.js"></script> 82 - </head> 83 - <body> 84 - <main> 85 - <was-hello></was-hello> 86 - </main> 87 - </body> 88 - </html>` 95 + // create the space with signer so all requests get signed by it 96 + const space = storage.space({ 97 + signer, 98 + id: `urn:uuid:${spaceId}` 99 + }) 89 100 90 - const data = { 91 - src: '/tmp', 92 - file: sampleHTML, 93 - } 101 + const linkset = space.resource(`linkset`) 102 + const spaceObject = { 103 + controller: signer.controller, 104 + // configure which resource to use as a linkset 105 + link: linkset.path, 106 + } 107 + const spaceObjectBlob = new Blob( 108 + [JSON.stringify(spaceObject)], 109 + { type: 'application/json' }, 110 + ) 94 111 95 - const $ = elf('was-repl', data) 96 - export default $ 112 + // send PUT request to update the space 113 + const responseToPutSpace = await space.put(spaceObjectBlob) 114 + .then(res => { 115 + console.debug({ res }) 116 + return res 117 + }) 118 + .catch(e => { 119 + console.debug(e) 120 + }) 97 121 98 - async function publish() { 99 - const { file } = $.learn() 100 - $.teach({ output }) 101 - } 122 + if (!responseToPutSpace.ok) throw new Error( 123 + `Failed to put space: ${responseToPutSpace.status} ${responseToPutSpace.statusText}`, { 124 + cause: { 125 + responseToPutSpace 126 + } 127 + }) 128 + if (!responseToPutSpace) return 102 129 103 - $.when('click', '[data-publish]', publish) 130 + // GET the space to make sure the PUT persisted it 131 + const responseToGetSpace = await space.get() 132 + .then(res => { 133 + console.debug({ res }) 134 + return res 135 + }) 136 + .catch(e => { 137 + console.debug(e) 138 + }) 104 139 105 - $.draw(render, { beforeUpdate, afterUpdate }) 140 + if (!responseToGetSpace.ok) throw new Error( 141 + `Failed to get space: ${responseToGetSpace.status} ${responseToGetSpace.statusText}`, { 142 + cause: { 143 + responseToGetSpace 144 + } 145 + }) 146 + if (!responseToGetSpace) return 106 147 107 - function render(target) { 108 - const { file, src } = $.learn() 148 + const index = space.resource(path) 149 + const blobForIndex = new Blob([input], { type: getContentTypeByPath(path) }) 150 + const responseToPutIndex = await index.put(blobForIndex, { signer }) 151 + .then(res => { 152 + console.debug({ res }) 153 + return res 154 + }) 155 + .catch(e => { 156 + console.debug(e) 157 + }) 158 + 159 + if (!responseToPutIndex.ok) throw new Error(`Failed to put index: ${responseToPutIndex.status} ${responseToPutIndex.statusText}`, { 160 + cause: { 161 + responseToPutIndex 162 + } 163 + }) 164 + 165 + if (!responseToPutIndex) return 166 + 167 + // Add a policy that makes things PublicCanRead 168 + const aclAllowingPublicReads = space.resource('policy/published') 169 + { 170 + const policy = { type: 'PublicCanRead' } 171 + const policyBlob = new Blob([JSON.stringify(policy)], { type: 'application/json' }) 172 + const responseToPutPolicy = await aclAllowingPublicReads.put(policyBlob, { signer }) 173 + .then(res => { 174 + if (!res.ok) throw new Error(`Failed to put policy: ${res.status} ${res.statusText}`, { cause: { res } }) 175 + return res 176 + }) 177 + .catch(e => { 178 + console.error(e) 179 + }) 180 + } 181 + 182 + // Update linkset so the index acl is aclAllowingPublicReads 183 + { 184 + const linksetObject = { 185 + "linkset": [ 186 + { 187 + "anchor": index.path, 188 + "acl": [ 189 + { 190 + "href": aclAllowingPublicReads.path, 191 + } 192 + ] 193 + }, 194 + { 195 + "anchor": space.resource(`tmp`).path, 196 + "acl": [ 197 + { 198 + "href": aclAllowingPublicReads.path, 199 + } 200 + ] 201 + } 202 + ] 203 + }; 204 + const linksetBlob = new Blob([JSON.stringify(linksetObject)], { type: 'application/linkset+json' }) 205 + const response = await linkset.put(linksetBlob) 206 + if (!response.ok) throw new Error(`Failed to put linkset: ${response.status} ${response.statusText}`, { cause: { response } }); 207 + } 208 + 209 + const indexUrl = new URL(index.path, storageUrl) 210 + $.teach({ src: indexUrl.toString() }) 211 + } 212 + 213 + $.draw((target) => { 214 + init(target) 215 + const { host, path, input, src } = $.learn() 216 + const url = src?src:path 217 + 109 218 return ` 110 219 <div class="action-bar"> 111 - <div class="title"> 112 - <input name="src" data-bind type="text" value="${escapeHyperText(src)}"> 220 + <div class="title">was98</div> 221 + <div class="was-form"> 222 + <input data-bind name="host" value="${escapeHyperText(host)}"/> 223 + <input data-bind name="path" value="${escapeHyperText(path)}"/> 224 + <button style="float: right;" data-run>Run</button> 113 225 </div> 114 - <button data-publish class="standard-button">Publish</button> 115 226 </div> 116 227 <div class="input"> 117 228 <textarea 118 - name="file" 119 - data-bind 229 + name="input" 230 + data-bind="input" 120 231 placeholder="Say it, don't spray it." 121 - value="${escapeHyperText(file)}" 122 - ></textarea> 232 + >${escapeHyperText(input)}</textarea> 123 233 </div> 124 234 <div class="output"> 125 - <iframe src="${src}"></iframe> 235 + <iframe src="${url}"></iframe> 236 + </div> 237 + <div class="footer-bar"> 238 + ${url} 126 239 </div> 127 240 ` 241 + },{ 242 + beforeUpdate(target) { 243 + saveCursor(target) 244 + }, 245 + afterUpdate(target) { 246 + replaceCursor(target) 247 + } 248 + }) 249 + 250 + let sel = [] 251 + const tags = ['TEXTAREA', 'INPUT'] 252 + function saveCursor(target) { 253 + if(target.contains(document.activeElement)) { 254 + target.dataset.field = document.activeElement.name 255 + if(tags.includes(document.activeElement.tagName)) { 256 + const textarea = document.activeElement 257 + sel = [textarea.selectionStart, textarea.selectionEnd]; 258 + } 259 + } 128 260 } 129 261 130 - function beforeUpdate(target) { 131 - { // convert a query string to new post 132 - const q = target.getAttribute('q') 133 - const src = target.getAttribute('src') 134 - if(!target.initialized) { 135 - target.initialized = true 136 - if(q) { 137 - const file = decodeURIComponent(q) 138 - $.teach({ file }) 139 - } 262 + function replaceCursor(target) { 263 + const field = target.querySelector(`[name="${target.dataset.field}"]`) 264 + 265 + if(field) { 266 + field.focus() 140 267 141 - if(src) { 142 - $.teach({ src }) 143 - } 268 + if(tags.includes(field.tagName)) { 269 + field.selectionStart = sel[0]; 270 + field.selectionEnd = sel[1]; 144 271 } 145 272 } 146 - 147 - 148 273 } 149 274 150 - function afterUpdate(target) { 151 275 152 - } 153 276 154 - function mergeOutput(state, payload) { 155 - return { 156 - ...state, 157 - output: [...state.output, payload] 158 - } 159 - } 160 - 161 - function escapeHyperText(text = '') { 162 - if(!text) return '' 163 - return text.replace(/[&<>'"]/g, 164 - actor => ({ 165 - '&': '&amp;', 166 - '<': '&lt;', 167 - '>': '&gt;', 168 - "'": '&#39;', 169 - '"': '&quot;' 170 - }[actor]) 171 - ) 172 - } 277 + $.when('click', '[data-run]', async (event) => { 278 + const root = event.target.closest($.link) 279 + publish(root.id) 280 + }) 173 281 174 282 $.when('input', '[data-bind]', (event) => { 175 - $.teach({[event.target.name]: event.target.value }) 283 + $.teach({ [event.target.name]: event.target.value }) 176 284 }) 177 285 286 + $.when('blur', '[name="path"]', (event) => { 287 + fetchSauce(event.target.value) 288 + }) 289 + 290 + 178 291 $.style(` 179 292 & { 180 293 display: grid; 181 - grid-template-rows: auto 1fr 1fr; 294 + grid-template-rows: auto 1fr 1fr auto; 182 295 grid-template-columns: 1fr; 183 296 height: 100%; 184 297 overflow: hidden; ··· 187 300 & .action-bar { 188 301 background: rgba(0,0,0,1); 189 302 padding: .5rem; 190 - display: grid; 191 - grid-template-columns: 1fr auto; 303 + display: flex; 304 + } 305 + 306 + & .was-form { 307 + margin-left: auto; 192 308 } 193 309 194 310 & .title { 195 311 color: rgba(255,255,255,.85); 196 312 font-weight: bold; 197 - } 198 - 199 - & .title input { 200 - max-width: 100%; 201 - padding: .5rem; 202 - width: 100%; 313 + font-size: 1.5rem; 203 314 } 204 315 205 316 & .input textarea { ··· 210 321 background: rgba(0,0,0,.85); 211 322 color: rgba(255,255,255,.85); 212 323 padding: .5rem; 213 - border-radius: 0; 214 324 } 215 325 216 326 & .output { 217 327 height: 100%; 218 328 overflow: auto; 219 - } 220 - 221 - & .output .textarea { 222 - white-space: preserve; 223 - } 224 - 225 - & .invisible { 226 - display: none; 329 + padding: .5rem; 227 330 } 228 331 229 332 @media (min-width: 36rem) { 230 333 & { 231 334 display: grid; 232 - grid-template-rows: auto 1fr; 335 + grid-template-rows: auto 1fr auto; 233 336 grid-template-columns: 1fr 1fr; 234 337 } 235 338 236 - & .action-bar { 339 + & .action-bar, 340 + & .footer-bar { 237 341 grid-column: -1 / 1; 238 342 } 343 + } 239 344 240 - & .invisible { 241 - display: block; 242 - } 243 345 244 - & .hide-full { 245 - display: none; 246 - } 346 + & iframe { 347 + width: 100%; 348 + height: 100%; 349 + border: 0; 350 + } 351 + 352 + & flying-disk { 353 + width: 100%; 354 + height: 100%; 247 355 } 248 356 `) 357 + 358 + function escapeHyperText(text = '') { 359 + return text.replace(/[&<>'"]/g, 360 + actor => ({ 361 + '&': '&amp;', 362 + '<': '&lt;', 363 + '>': '&gt;', 364 + "'": '&#39;', 365 + '"': '&quot;' 366 + }[actor]) 367 + ) 368 + }
+14 -4
client/server.js
··· 63 63 } 64 64 65 65 } catch { 66 + 67 + const configObject = { 68 + PLAN98_WAS_HOST: "http://localhost:8080", 69 + PLAN98_WAS_SPACE_ID: spaceId, 70 + PLAN98_WAS_SIGNER: JSON.stringify(signer.toJSON()) 71 + } 72 + const configArray = [] 73 + for(const key of Object.keys(configObject)) { 74 + configArray.push(`${key}: '${configObject[key]}'`) 75 + } 76 + const ENVIRONMENT_VARIABLES = configArray.join(',\n') 66 77 return new Response(`<!DOCTYPE html> 67 78 <html lang="en"> 68 79 <head> ··· 133 144 <script> 134 145 plan98 = { 135 146 env: { 136 - PLAN98_WAS_HOST: "http://localhost:8080", 137 - PLAN98_JSON_SIGNER: ${JSON.stringify(signer.toJSON())} 147 + ${ENVIRONMENT_VARIABLES} 138 148 } 139 149 } 140 150 </script> ··· 154 164 import { Ed25519Signer } from "@did.coop/did-key-ed25519" 155 165 156 166 (async function init() { 157 - const signer = await Ed25519Signer.fromJSON(JSON.stringify(plan98.env.PLAN98_JSON_SIGNER)) 167 + const signer = await Ed25519Signer.fromJSON(${JSON.stringify(configObject.PLAN98_WAS_SIGNER)}) 158 168 159 169 const storageId = plan98.env.PLAN98_WAS_HOST 160 170 if(!storageId) return ··· 164 174 // create the space with signer so all requests get signed by it 165 175 const space = storage.space({ 166 176 signer, 167 - id: ${`"urn:uuid:${spaceId}"`} 177 + id: "urn:uuid:${configObject.PLAN98_WAS_SPACE_ID}" 168 178 }) 169 179 170 180 const linkset = space.resource('linkset')
+5 -3
deno.json
··· 17 17 "debug-client": "deno run -A --inspect-brk client.js", 18 18 "reverse-client": "ssh -N -R 1998:localhost:8000 local.$USER.me", 19 19 "start-peer": "cd client && deno run -A server.js", 20 - "deploy-peer": "cd client && deployctl deploy --entrypoint=./server.js --prod --exclude=./private --exclude=./public", 20 + "deploy-peer": "cd client && deployctl deploy --entrypoint=./server.js --prod --exclude=./private", 21 21 "debug-peer": "cd client && deno run -A --inspect-brk server.js", 22 22 "reverse-peer": "ssh -N -R 1024:localhost:1024 $USER.plan98.org", 23 + "start-was": "cd server/was/wallet-attached-storage-server-main && CORS_ALLOW_ALL_ORIGINS=TRUE npm run dev", 24 + "reverse-was": "ssh -N -R 1024:localhost:1024 $USER.wallet.storage", 23 25 "start-server": "plan98client=$(pwd)/client; cd distros/subsystems/rust-9p/example/unpfs && cargo run --release 'tcp!0.0.0.0!7777' $plan98client", 24 26 "reverse-server": "ssh -N -R 7777:localhost:7777 drive.$USER.me", 25 27 "start-identity": "deno run -A identity.js", 26 28 "reverse-identity": "ssh -N -R 3001:localhost:3001 wallet.$USER.me", 27 29 "start-modules": "cd ../SourceCode/esm.sh && go run main.go --config=config.json --dev", 28 - "reverse-modules": "ssh -N -R 8080:localhost:8090 esm.$USER.me", 30 + "reverse-modules": "ssh -N -R 8090:localhost:8090 esm.$USER.me", 29 31 "start-relay": "cd server/relay && npm start", 30 32 "reverse-relay": "ssh -N -R 8765:localhost:8765 relay.$USER.me", 31 33 "provision-braidmail": "cd server/braidmail && ./provision.sh", ··· 119 121 "include": [], 120 122 "entrypoint": "client/server.js" 121 123 } 122 - } 124 + }