Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
1
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>