Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol. wisp.place
86
fork

Configure Feed

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

use keychain for dpop

+104 -3
+29
bun.lock
··· 162 162 "bin": { 163 163 "wispctl": "dist/index.js", 164 164 }, 165 + "dependencies": { 166 + "@napi-rs/keyring": "^1.2.0", 167 + }, 165 168 "devDependencies": { 166 169 "@atproto/api": "^0.18.17", 167 170 "@atproto/identity": "^0.4.10", ··· 677 680 "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], 678 681 679 682 "@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="], 683 + 684 + "@napi-rs/keyring": ["@napi-rs/keyring@1.2.0", "", { "optionalDependencies": { "@napi-rs/keyring-darwin-arm64": "1.2.0", "@napi-rs/keyring-darwin-x64": "1.2.0", "@napi-rs/keyring-freebsd-x64": "1.2.0", "@napi-rs/keyring-linux-arm-gnueabihf": "1.2.0", "@napi-rs/keyring-linux-arm64-gnu": "1.2.0", "@napi-rs/keyring-linux-arm64-musl": "1.2.0", "@napi-rs/keyring-linux-riscv64-gnu": "1.2.0", "@napi-rs/keyring-linux-x64-gnu": "1.2.0", "@napi-rs/keyring-linux-x64-musl": "1.2.0", "@napi-rs/keyring-win32-arm64-msvc": "1.2.0", "@napi-rs/keyring-win32-ia32-msvc": "1.2.0", "@napi-rs/keyring-win32-x64-msvc": "1.2.0" } }, "sha512-d0d4Oyxm+v980PEq1ZH2PmS6cvpMIRc17eYpiU47KgW+lzxklMu6+HOEOPmxrpnF/XQZ0+Q78I2mgMhbIIo/dg=="], 685 + 686 + "@napi-rs/keyring-darwin-arm64": ["@napi-rs/keyring-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CA83rDeyONDADO25JLZsh3eHY8yTEtm/RS6ecPsY+1v+dSawzT9GywBMu2r6uOp1IEhQs/xAfxgybGAFr17lSA=="], 687 + 688 + "@napi-rs/keyring-darwin-x64": ["@napi-rs/keyring-darwin-x64@1.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-dBHjtKRCj4ByfnfqIKIJLo3wueQNJhLRyuxtX/rR4K/XtcS7VLlRD01XXizjpre54vpmObj63w+ZpHG+mGM8uA=="], 689 + 690 + "@napi-rs/keyring-freebsd-x64": ["@napi-rs/keyring-freebsd-x64@1.2.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DPZFr11pNJSnaoh0dzSUNF+T6ORhy3CkzUT3uGixbA71cAOPJ24iG8e8QrLOkuC/StWrAku3gBnth2XMWOcR3Q=="], 691 + 692 + "@napi-rs/keyring-linux-arm-gnueabihf": ["@napi-rs/keyring-linux-arm-gnueabihf@1.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-8xv6DyEMlvRdqJzp4F39RLUmmTQsLcGYYv/3eIfZNZN1O5257tHxTrFYqAsny659rJJK2EKeSa7PhrSibQqRWQ=="], 693 + 694 + "@napi-rs/keyring-linux-arm64-gnu": ["@napi-rs/keyring-linux-arm64-gnu@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Pu2V6Py+PBt7inryEecirl+t+ti8bhZphjP+W68iVaXHUxLdWmkgL9KI1VkbRHbx5k8K5Tew9OP218YfmVguIA=="], 695 + 696 + "@napi-rs/keyring-linux-arm64-musl": ["@napi-rs/keyring-linux-arm64-musl@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8TDymrpC4P1a9iDEaegT7RnrkmrJN5eNZh3Im3UEV5PPYGtrb82CRxsuFohthCWQW81O483u1bu+25+XA4nKUw=="], 697 + 698 + "@napi-rs/keyring-linux-riscv64-gnu": ["@napi-rs/keyring-linux-riscv64-gnu@1.2.0", "", { "os": "linux", "cpu": "none" }, "sha512-awsB5XI1MYL7fwfjMDGmKOWvNgJEO7mM7iVEMS0fO39f0kVJnOSjlu7RHcXAF0LOx+0VfF3oxbWqJmZbvRCRHw=="], 699 + 700 + "@napi-rs/keyring-linux-x64-gnu": ["@napi-rs/keyring-linux-x64-gnu@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8E+7z4tbxSJXxIBqA+vfB1CGajpCDRyTyqXkBig5NtASrv4YXcntSo96Iah2QDR5zD3dSTsmbqJudcj9rKKuHQ=="], 701 + 702 + "@napi-rs/keyring-linux-x64-musl": ["@napi-rs/keyring-linux-x64-musl@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8RZ8yVEnmWr/3BxKgBSzmgntI7lNEsY7xouNfOsQkuVAiCNmxzJwETspzK3PQ2FHtDxgz5vHQDEBVGMyM4hUHA=="], 703 + 704 + "@napi-rs/keyring-win32-arm64-msvc": ["@napi-rs/keyring-win32-arm64-msvc@1.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-AoqaDZpQ6KPE19VBLpxyORcp+yWmHI9Xs9Oo0PJ4mfHma4nFSLVdhAubJCxdlNptHe5va7ghGCHj3L9Akiv4cQ=="], 705 + 706 + "@napi-rs/keyring-win32-ia32-msvc": ["@napi-rs/keyring-win32-ia32-msvc@1.2.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EYL+EEI6bCsYi3LfwcQdnX3P/R76ENKNn+3PmpGheBsUFLuh0gQuP7aMVHM4rTw6UVe+L3vCLZSptq/oeacz0A=="], 707 + 708 + "@napi-rs/keyring-win32-x64-msvc": ["@napi-rs/keyring-win32-x64-msvc@1.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-xFlx/TsmqmCwNU9v+AVnEJgoEAlBYgzFF5Ihz1rMpPAt4qQWWkMd4sCyM1gMJ1A/GnRqRegDiQpwaxGUHFtFbA=="], 680 709 681 710 "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], 682 711
+59 -2
cli/lib/auth.ts
··· 14 14 import { serve as honoNodeServe } from '@hono/node-server' 15 15 import { resolvePdsFromHandle } from '@wispplace/atproto-utils' 16 16 import { isBun } from '@wispplace/bun-firehose' 17 + import { Entry as KeyringEntry } from '@napi-rs/keyring' 18 + import { confirm, log } from '@clack/prompts' 17 19 import { Hono } from 'hono' 18 20 import open from 'open' 21 + 22 + const KEYCHAIN_SERVICE = 'wispctl' 23 + 24 + function probeKeychain(): boolean { 25 + const testKey = '__wispctl_probe__' 26 + try { 27 + const entry = new KeyringEntry(KEYCHAIN_SERVICE, testKey) 28 + entry.setPassword('1') 29 + entry.deletePassword() 30 + return true 31 + } catch { 32 + return false 33 + } 34 + } 19 35 20 36 // All scopes requested upfront so the client_id is stable across commands 21 37 const OAUTH_SCOPE = [ ··· 51 67 set(key: string, value: string, expiresAt: number | null): void 52 68 del(key: string): void 53 69 clear(): void 70 + valuesByPrefix(prefix: string): string[] 54 71 } 55 72 56 73 const SCHEMA = ` ··· 72 89 const getStmt = db.query<KvRow, [string]>('SELECT value, expires_at FROM kv WHERE key = ?') 73 90 const setStmt = db.query('INSERT OR REPLACE INTO kv (key, value, expires_at) VALUES (?, ?, ?)') 74 91 const delStmt = db.query('DELETE FROM kv WHERE key = ?') 92 + const prefixStmt = db.query<{ value: string }, [string]>('SELECT value FROM kv WHERE key LIKE ?') 75 93 return { 76 94 get: (key) => getStmt.get(key) ?? undefined, 77 95 set: (key, value, expiresAt) => { setStmt.run(key, value, expiresAt) }, 78 96 del: (key) => { delStmt.run(key) }, 79 97 clear: () => db.run('DELETE FROM kv'), 98 + valuesByPrefix: (prefix) => prefixStmt.all(`${prefix}%`).map((r) => r.value), 80 99 } 81 100 } else { 82 101 const { DatabaseSync } = await import('node:sqlite') ··· 86 105 const getStmt = db.prepare('SELECT value, expires_at FROM kv WHERE key = ?') 87 106 const setStmt = db.prepare('INSERT OR REPLACE INTO kv (key, value, expires_at) VALUES (?, ?, ?)') 88 107 const delStmt = db.prepare('DELETE FROM kv WHERE key = ?') 108 + const prefixStmt = db.prepare('SELECT value FROM kv WHERE key LIKE ?') 89 109 return { 90 110 get: (key) => getStmt.get(key) as KvRow | undefined, 91 111 set: (key, value, expiresAt) => { setStmt.run(key, value, expiresAt) }, 92 112 del: (key) => { delStmt.run(key) }, 93 113 clear: () => db.exec('DELETE FROM kv'), 114 + valuesByPrefix: (prefix) => (prefixStmt.all(`${prefix}%`) as { value: string }[]).map((r) => r.value), 94 115 } 95 116 } 96 117 } ··· 126 147 } 127 148 } 128 149 129 - function createSessionStore(kv: KvAdapter): NodeSavedSessionStore { 150 + function createSessionStore(kv: KvAdapter, useKeychain: boolean): NodeSavedSessionStore { 151 + if (useKeychain) { 152 + return { 153 + async set(sub, session) { 154 + new KeyringEntry(KEYCHAIN_SERVICE, sub).setPassword(JSON.stringify(session)) 155 + }, 156 + async get(sub) { 157 + try { 158 + const raw = new KeyringEntry(KEYCHAIN_SERVICE, sub).getPassword() 159 + if (!raw) return undefined 160 + return JSON.parse(raw) as NodeSavedSession 161 + } catch { 162 + return undefined 163 + } 164 + }, 165 + async del(sub) { 166 + try { new KeyringEntry(KEYCHAIN_SERVICE, sub).deletePassword() } catch {} 167 + }, 168 + } 169 + } 130 170 return { 131 171 async set(sub: string, session: NodeSavedSession) { 132 172 kvSet(kv, `oauth_session:${sub}`, JSON.stringify(session), 60 * 60 * 24 * 14) ··· 185 225 ): Promise<{ agent: Agent; did: string }> { 186 226 const kv = await openKv(options.dbPath || DEFAULT_DB_PATH) 187 227 228 + let useKeychain = probeKeychain() 229 + if (!useKeychain) { 230 + log.warn('System keychain is unavailable (no Secret Service daemon or equivalent).') 231 + const fallback = await confirm({ 232 + message: 'Fall back to storing session tokens unencrypted in SQLite? (On headless systems, prefer --password instead.)', 233 + initialValue: false, 234 + }) 235 + if (!fallback) { 236 + throw new Error('Cannot store session securely. Use --password for app-password authentication.') 237 + } 238 + } 239 + 188 240 const redirectUri = `http://${LOOPBACK_HOST}:${LOOPBACK_PORT}/oauth/callback` 189 241 const clientIdParams = new URLSearchParams() 190 242 clientIdParams.append('redirect_uri', redirectUri) ··· 204 256 dpop_bound_access_tokens: false, 205 257 }, 206 258 stateStore: createStateStore(kv), 207 - sessionStore: createSessionStore(kv), 259 + sessionStore: createSessionStore(kv, useKeychain), 208 260 requestLock: requestLocalLock, 209 261 }) 210 262 ··· 429 481 export async function clearSessions(dbPath?: string) { 430 482 try { 431 483 const kv = await openKv(dbPath || DEFAULT_DB_PATH) 484 + // Delete any keychain entries for DIDs we know about via dir mappings 485 + const dids = kv.valuesByPrefix('dir:') 486 + for (const did of dids) { 487 + try { new KeyringEntry(KEYCHAIN_SERVICE, did).deletePassword() } catch {} 488 + } 432 489 kv.clear() 433 490 console.log('Cleared all stored OAuth sessions') 434 491 } catch {
+4 -1
cli/package.json
··· 44 44 "build": "bun build ./index.ts --outdir ./dist --target node --sourcemap=linked && sed -i '' '1s|#!/usr/bin/env bun|#!/usr/bin/env node|' ./dist/index.js", 45 45 "typecheck": "tsc --noEmit" 46 46 }, 47 - "type": "module" 47 + "type": "module", 48 + "dependencies": { 49 + "@napi-rs/keyring": "^1.2.0" 50 + } 48 51 }
+12
cli/test/index.html
··· 1 + <!doctype> 2 + <html> 3 + <style> 4 + body { 5 + background-color: #000; 6 + color: white; 7 + } 8 + </style> 9 + <BODY> 10 + wehhhh 11 + </BODY> 12 + </html>