OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-web-ui commits / 84a74a5

Initial hyper-web-ui: shared web framework for P2P apps
Server-side: lib/router.js — JSON API router with content negotiation - Pattern matching with params - Accept: text/html + renderer → SSR - Accept: application/json (default) → JSON - Query string parsing, error handling lib/auth.js — auth middleware + challenge-response - Bearer token extraction - Challenge generation + signature verification - Token issuance (30-day expiry) lib/static.js — static file server with SPA fallback Browser-side: web/js/identity.js — Ed25519 keypair in localStorage + auth flow web/js/api.js — fetch wrapper with auth headers web/js/router.js — hash-based SPA router web/css/theme.css — shared dark theme 9 tests, 20 assertions, all passing. Consumers: pear-git, hypernote, hyper-booking (SSR), hyper-social, hyper-wallet, wirecore — all use same router + identity client. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84a74a5ce330776ffc4586be1d509c51284e2113
by plankatron <fleeky@gmail.com> · 2026-03-30T02:33:55.000Z

Changed files (12) +1787 -0

.gitignore +1 -0
……@@ -0,0 +1 @@
1node_modules/
index.js +4 -0
……@@ -0,0 +1,4 @@
1// Server-side exports
2export { Router, json, html, readBody, readJson } from './lib/router.js'
3export { authMiddleware, createChallenge, verifyChallenge } from './lib/auth.js'
4export { serveStatic } from './lib/static.js'
lib/auth.js +111 -0
……@@ -0,0 +1,111 @@
1/**
2 * Auth Middleware + Challenge-Response
3 *
4 * Shared authentication for all P2P apps.
5 * Bearer token extraction + challenge-response flow.
6 */
7
8import crypto from 'hypercore-crypto'
9
10/**
11 * Create auth middleware for routes
12 *
13 * @param {Object} opts
14 * @param {Object} opts.registry — Hyperbee for token storage
15 * @param {boolean} opts.required — reject if not authenticated (default: false)
16 * @returns {Function} middleware(req, res) → { userId, token } or null
17 */
18export function authMiddleware (opts = {}) {
19 return async function (req, res) {
20 const token = extractToken(req)
21 if (!token) {
22 if (opts.required) {
23 res.writeHead(401, { 'Content-Type': 'application/json' })
24 res.end(JSON.stringify({ error: 'Authentication required' }))
25 return null
26 }
27 req.auth = null
28 return null
29 }
30
31 if (opts.registry) {
32 const entry = await opts.registry.get(`token:${token}`)
33 if (!entry || entry.value.expiresAt < Date.now()) {
34 if (opts.required) {
35 res.writeHead(401, { 'Content-Type': 'application/json' })
36 res.end(JSON.stringify({ error: 'Invalid or expired token' }))
37 return null
38 }
39 req.auth = null
40 return null
41 }
42
43 req.auth = entry.value
44 return entry.value
45 }
46
47 req.auth = { token }
48 return { token }
49 }
50}
51
52/**
53 * Generate a challenge for signature-based auth
54 */
55export async function createChallenge (registry) {
56 const challengeId = crypto.randomBytes(16).toString('hex')
57 const challenge = crypto.randomBytes(32).toString('base64')
58
59 const entry = {
60 challenge,
61 expiresAt: Date.now() + 5 * 60 * 1000,
62 consumed: false
63 }
64
65 await registry.put(`challenge:${challengeId}`, entry)
66 return { challengeId, challenge }
67}
68
69/**
70 * Verify a signed challenge and issue a token
71 */
72export async function verifyChallenge (registry, { challengeId, publicKey, signature }) {
73 const entry = await registry.get(`challenge:${challengeId}`)
74 if (!entry) return { error: 'Invalid challenge' }
75
76 const ch = entry.value
77 if (ch.consumed) return { error: 'Challenge already used' }
78 if (ch.expiresAt < Date.now()) return { error: 'Challenge expired' }
79
80 try {
81 const valid = crypto.verify(
82 Buffer.from(ch.challenge),
83 Buffer.from(signature, 'base64'),
84 Buffer.from(publicKey, 'hex')
85 )
86 if (!valid) return { error: 'Invalid signature' }
87 } catch {
88 return { error: 'Signature verification failed' }
89 }
90
91 // Consume challenge
92 ch.consumed = true
93 await registry.put(`challenge:${challengeId}`, ch)
94
95 // Generate token
96 const token = crypto.randomBytes(32).toString('hex')
97 await registry.put(`token:${token}`, {
98 publicKey,
99 createdAt: Date.now(),
100 expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000
101 })
102
103 return { token, publicKey }
104}
105
106function extractToken (req) {
107 const auth = req.headers.authorization
108 if (!auth) return null
109 if (auth.startsWith('Bearer ')) return auth.slice(7)
110 return null
111}
lib/router.js +125 -0
……@@ -0,0 +1,125 @@
1/**
2 * JSON API Router with Content Negotiation
3 *
4 * Pattern-matched routes. Returns JSON by default.
5 * If Accept: text/html AND a renderer is provided, returns SSR HTML.
6 *
7 * Usage:
8 * const router = new Router('/prefix')
9 * router.get('/api/repos', handler)
10 * router.get('/repos/:name', handler, { render: renderRepoPage })
11 *
12 * // In HTTP server:
13 * const handled = await router.handle(req, res)
14 */
15
16export class Router {
17 constructor (prefix = '') {
18 this.prefix = prefix
19 this._routes = []
20 }
21
22 get (pattern, handler, opts) { this._routes.push({ method: 'GET', pattern, handler, opts }) }
23 post (pattern, handler, opts) { this._routes.push({ method: 'POST', pattern, handler, opts }) }
24 patch (pattern, handler, opts) { this._routes.push({ method: 'PATCH', pattern, handler, opts }) }
25 del (pattern, handler, opts) { this._routes.push({ method: 'DELETE', pattern, handler, opts }) }
26
27 async handle (req, res) {
28 const method = req.method
29 const url = req.url.replace(this.prefix, '') || '/'
30 const [pathStr, queryString] = url.split('?')
31
32 for (const route of this._routes) {
33 if (route.method !== method) continue
34 const params = match(route.pattern, pathStr)
35 if (params === null) continue
36
37 req.params = params
38 req.query = parseQuery(queryString)
39
40 try {
41 const data = await route.handler(req, res)
42
43 // If handler already sent response, we're done
44 if (res.writableEnded) return true
45
46 // Content negotiation
47 if (data !== undefined) {
48 const wantsHtml = req.headers.accept && req.headers.accept.includes('text/html')
49
50 if (wantsHtml && route.opts && route.opts.render) {
51 // SSR: render HTML from data
52 html(res, route.opts.render(data))
53 } else {
54 // JSON (default)
55 json(res, data)
56 }
57 }
58 } catch (err) {
59 if (!res.writableEnded) {
60 json(res, { error: err.message }, 500)
61 }
62 }
63 return true
64 }
65
66 return false
67 }
68}
69
70function match (pattern, pathStr) {
71 const patternParts = pattern.split('/').filter(Boolean)
72 const pathParts = pathStr.split('/').filter(Boolean)
73
74 if (patternParts.length !== pathParts.length) return null
75
76 const params = {}
77 for (let i = 0; i < patternParts.length; i++) {
78 if (patternParts[i].startsWith(':')) {
79 params[patternParts[i].slice(1)] = decodeURIComponent(pathParts[i])
80 } else if (patternParts[i] !== pathParts[i]) {
81 return null
82 }
83 }
84 return params
85}
86
87function parseQuery (qs) {
88 if (!qs) return {}
89 const params = {}
90 for (const pair of qs.split('&')) {
91 const [k, v] = pair.split('=')
92 if (k) params[decodeURIComponent(k)] = v ? decodeURIComponent(v) : ''
93 }
94 return params
95}
96
97// Response helpers
98export function json (res, data, status = 200) {
99 const body = JSON.stringify(data)
100 res.writeHead(status, {
101 'Content-Type': 'application/json',
102 'Content-Length': Buffer.byteLength(body)
103 })
104 res.end(body)
105}
106
107export function html (res, content, status = 200) {
108 const body = typeof content === 'string' ? content : String(content)
109 res.writeHead(status, {
110 'Content-Type': 'text/html; charset=utf-8',
111 'Content-Length': Buffer.byteLength(body)
112 })
113 res.end(body)
114}
115
116export async function readBody (req) {
117 const chunks = []
118 for await (const chunk of req) chunks.push(chunk)
119 return Buffer.concat(chunks)
120}
121
122export async function readJson (req) {
123 const body = await readBody(req)
124 return JSON.parse(body.toString())
125}
lib/static.js +76 -0
……@@ -0,0 +1,76 @@
1/**
2 * Static File Server + SPA Fallback
3 *
4 * Serves files from a web/ directory. Falls back to index.html
5 * for SPA routing (any path not matching a real file → index.html).
6 */
7
8import fs from 'bare-fs'
9import path from 'bare-path'
10
11const MIME_TYPES = {
12 '.html': 'text/html',
13 '.css': 'text/css',
14 '.js': 'application/javascript',
15 '.json': 'application/json',
16 '.png': 'image/png',
17 '.jpg': 'image/jpeg',
18 '.svg': 'image/svg+xml',
19 '.ico': 'image/x-icon',
20 '.woff2': 'font/woff2',
21 '.woff': 'font/woff'
22}
23
24/**
25 * Serve static files from a directory
26 *
27 * @param {string} webDir — root directory for static files
28 * @param {Object} opts
29 * @param {string} opts.prefix — URL prefix (e.g., '/web')
30 * @param {boolean} opts.spaFallback — serve index.html for unmatched paths (default: true)
31 * @returns {Function} handler(req, res) → boolean (true if handled)
32 */
33export function serveStatic (webDir, opts = {}) {
34 const prefix = opts.prefix || '/web'
35 const spaFallback = opts.spaFallback !== false
36 const indexPath = path.join(webDir, 'index.html')
37
38 return function handleStatic (req, res) {
39 if (req.method !== 'GET') return false
40
41 const urlPath = req.url.split('?')[0]
42
43 // Serve files under prefix
44 if (urlPath.startsWith(prefix + '/')) {
45 const relPath = urlPath.slice(prefix.length)
46 const filePath = path.join(webDir, relPath)
47
48 try {
49 const data = fs.readFileSync(filePath)
50 const ext = path.extname(filePath)
51 res.writeHead(200, {
52 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream',
53 'Content-Length': data.length
54 })
55 res.end(data)
56 return true
57 } catch {
58 return false
59 }
60 }
61
62 // SPA fallback — serve index.html for any non-API, non-static path
63 if (spaFallback && !urlPath.startsWith('/api/') && !urlPath.includes('.')) {
64 try {
65 const data = fs.readFileSync(indexPath)
66 res.writeHead(200, { 'Content-Type': 'text/html' })
67 res.end(data)
68 return true
69 } catch {
70 return false
71 }
72 }
73
74 return false
75 }
76}
package-lock.json +929 -0
……@@ -0,0 +1,929 @@
1{
2 "name": "hyper-web-ui",
3 "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "hyper-web-ui",
9 "version": "1.0.0",
10 "dependencies": {
11 "hypercore-crypto": "^3.0.0"
12 },
13 "devDependencies": {
14 "brittle": "^3.0.0"
15 }
16 },
17 "node_modules/@jridgewell/resolve-uri": {
18 "version": "3.1.2",
19 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
20 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
21 "dev": true,
22 "license": "MIT",
23 "engines": {
24 "node": ">=6.0.0"
25 }
26 },
27 "node_modules/@jridgewell/sourcemap-codec": {
28 "version": "1.5.5",
29 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
30 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
31 "dev": true,
32 "license": "MIT"
33 },
34 "node_modules/@jridgewell/trace-mapping": {
35 "version": "0.3.31",
36 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
37 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
38 "dev": true,
39 "license": "MIT",
40 "dependencies": {
41 "@jridgewell/resolve-uri": "^3.1.0",
42 "@jridgewell/sourcemap-codec": "^1.4.14"
43 }
44 },
45 "node_modules/@types/istanbul-lib-coverage": {
46 "version": "2.0.6",
47 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
48 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
49 "dev": true,
50 "license": "MIT"
51 },
52 "node_modules/b4a": {
53 "version": "1.8.0",
54 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
55 "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
56 "license": "Apache-2.0",
57 "peerDependencies": {
58 "react-native-b4a": "*"
59 },
60 "peerDependenciesMeta": {
61 "react-native-b4a": {
62 "optional": true
63 }
64 }
65 },
66 "node_modules/bare-abort": {
67 "version": "2.0.13",
68 "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz",
69 "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
70 "dev": true,
71 "license": "Apache-2.0"
72 },
73 "node_modules/bare-addon-resolve": {
74 "version": "1.10.0",
75 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
76 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
77 "license": "Apache-2.0",
78 "dependencies": {
79 "bare-module-resolve": "^1.10.0",
80 "bare-semver": "^1.0.0"
81 },
82 "peerDependencies": {
83 "bare-url": "*"
84 },
85 "peerDependenciesMeta": {
86 "bare-url": {
87 "optional": true
88 }
89 }
90 },
91 "node_modules/bare-ansi-escapes": {
92 "version": "2.2.3",
93 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
94 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
95 "license": "Apache-2.0",
96 "dependencies": {
97 "bare-stream": "^2.6.5"
98 },
99 "peerDependencies": {
100 "bare-buffer": "*"
101 },
102 "peerDependenciesMeta": {
103 "bare-buffer": {
104 "optional": true
105 }
106 }
107 },
108 "node_modules/bare-assert": {
109 "version": "1.2.0",
110 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
111 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
112 "license": "Apache-2.0",
113 "dependencies": {
114 "bare-inspect": "^3.1.2"
115 }
116 },
117 "node_modules/bare-bundle": {
118 "version": "1.10.0",
119 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
120 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
121 "dev": true,
122 "license": "Apache-2.0",
123 "peerDependencies": {
124 "bare-buffer": "*",
125 "bare-url": "*"
126 },
127 "peerDependenciesMeta": {
128 "bare-buffer": {
129 "optional": true
130 },
131 "bare-url": {
132 "optional": true
133 }
134 }
135 },
136 "node_modules/bare-cov": {
137 "version": "1.2.1",
138 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
139 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
140 "dev": true,
141 "license": "Apache-2.0",
142 "dependencies": {
143 "bare-fs": "^4.1.2",
144 "bare-inspector": "^4.0.1",
145 "bare-path": "^3.0.0",
146 "bare-process": "^4.2.1",
147 "bare-url": "^2.1.5",
148 "bare-v8-to-istanbul": "^1.0.2",
149 "picomatch": "^4.0.2",
150 "which-runtime": "^1.2.1"
151 }
152 },
153 "node_modules/bare-crypto": {
154 "version": "1.13.4",
155 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz",
156 "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==",
157 "dev": true,
158 "license": "Apache-2.0",
159 "dependencies": {
160 "bare-assert": "^1.2.0",
161 "bare-stream": "^2.6.3"
162 },
163 "peerDependencies": {
164 "bare-buffer": "*"
165 },
166 "peerDependenciesMeta": {
167 "bare-buffer": {
168 "optional": true
169 }
170 }
171 },
172 "node_modules/bare-debug-log": {
173 "version": "2.0.0",
174 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
175 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
176 "dev": true,
177 "license": "Apache-2.0",
178 "dependencies": {
179 "bare-os": "^3.0.1"
180 }
181 },
182 "node_modules/bare-dns": {
183 "version": "2.1.4",
184 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
185 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
186 "dev": true,
187 "license": "Apache-2.0",
188 "engines": {
189 "bare": ">=1.7.0"
190 }
191 },
192 "node_modules/bare-encoding": {
193 "version": "1.0.3",
194 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
195 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
196 "dev": true,
197 "license": "Apache-2.0",
198 "peerDependencies": {
199 "bare-buffer": "*"
200 },
201 "peerDependenciesMeta": {
202 "bare-buffer": {
203 "optional": true
204 }
205 }
206 },
207 "node_modules/bare-env": {
208 "version": "3.0.0",
209 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
210 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
211 "dev": true,
212 "license": "Apache-2.0",
213 "dependencies": {
214 "bare-os": "^3.0.1"
215 }
216 },
217 "node_modules/bare-events": {
218 "version": "2.8.2",
219 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
220 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
221 "license": "Apache-2.0",
222 "peerDependencies": {
223 "bare-abort-controller": "*"
224 },
225 "peerDependenciesMeta": {
226 "bare-abort-controller": {
227 "optional": true
228 }
229 }
230 },
231 "node_modules/bare-format": {
232 "version": "1.0.2",
233 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz",
234 "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==",
235 "dev": true,
236 "license": "Apache-2.0",
237 "dependencies": {
238 "bare-inspect": "^3.0.0"
239 }
240 },
241 "node_modules/bare-fs": {
242 "version": "4.5.6",
243 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz",
244 "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==",
245 "dev": true,
246 "license": "Apache-2.0",
247 "dependencies": {
248 "bare-events": "^2.5.4",
249 "bare-path": "^3.0.0",
250 "bare-stream": "^2.6.4",
251 "bare-url": "^2.2.2",
252 "fast-fifo": "^1.3.2"
253 },
254 "engines": {
255 "bare": ">=1.16.0"
256 },
257 "peerDependencies": {
258 "bare-buffer": "*"
259 },
260 "peerDependenciesMeta": {
261 "bare-buffer": {
262 "optional": true
263 }
264 }
265 },
266 "node_modules/bare-hrtime": {
267 "version": "2.1.1",
268 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
269 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
270 "dev": true,
271 "license": "Apache-2.0"
272 },
273 "node_modules/bare-http-parser": {
274 "version": "1.1.3",
275 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz",
276 "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==",
277 "dev": true,
278 "license": "Apache-2.0"
279 },
280 "node_modules/bare-http1": {
281 "version": "4.5.5",
282 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.5.tgz",
283 "integrity": "sha512-ADITiRo0huP76JGMbv6Arsh9KehHqjEBoYcmjvAo67IY78+/9mV2MeKLLCkiJSFxA85T/m8cTaE44OwJQCcwdw==",
284 "dev": true,
285 "license": "Apache-2.0",
286 "dependencies": {
287 "bare-events": "^2.6.0",
288 "bare-http-parser": "^1.1.1",
289 "bare-stream": "^2.10.0",
290 "bare-tcp": "^2.2.0"
291 },
292 "peerDependencies": {
293 "bare-buffer": "*",
294 "bare-url": "*"
295 },
296 "peerDependenciesMeta": {
297 "bare-buffer": {
298 "optional": true
299 },
300 "bare-url": {
301 "optional": true
302 }
303 }
304 },
305 "node_modules/bare-https": {
306 "version": "2.1.3",
307 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.3.tgz",
308 "integrity": "sha512-0TI/mJXQGYXmG7UUyWEG+KCJusayIAQLywUjFAskDoKuxfqVnGF+M/mTMrEV8J64DaIdU+5x761FvgmT7M68tA==",
309 "dev": true,
310 "license": "Apache-2.0",
311 "dependencies": {
312 "bare-http1": "^4.4.0",
313 "bare-tcp": "^2.2.0",
314 "bare-tls": "^2.0.0"
315 }
316 },
317 "node_modules/bare-inspect": {
318 "version": "3.1.4",
319 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
320 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
321 "license": "Apache-2.0",
322 "dependencies": {
323 "bare-ansi-escapes": "^2.1.0",
324 "bare-type": "^1.0.0"
325 },
326 "engines": {
327 "bare": ">=1.18.0"
328 }
329 },
330 "node_modules/bare-inspector": {
331 "version": "4.0.3",
332 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
333 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
334 "dev": true,
335 "license": "Apache-2.0",
336 "dependencies": {
337 "bare-events": "^2.1.0",
338 "bare-http1": "^4.0.0",
339 "bare-stream": "^2.0.0",
340 "bare-url": "^2.0.0",
341 "bare-ws": "^2.0.0"
342 },
343 "engines": {
344 "bare": ">=1.2.0"
345 },
346 "peerDependencies": {
347 "bare-tcp": "*"
348 },
349 "peerDependenciesMeta": {
350 "bare-tcp": {
351 "optional": true
352 }
353 }
354 },
355 "node_modules/bare-module": {
356 "version": "6.1.3",
357 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
358 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
359 "dev": true,
360 "license": "Apache-2.0",
361 "dependencies": {
362 "bare-bundle": "^1.3.0",
363 "bare-module-lexer": "^1.0.0",
364 "bare-module-resolve": "^1.8.0",
365 "bare-path": "^3.0.0",
366 "bare-url": "^2.0.1"
367 },
368 "engines": {
369 "bare": ">=1.23.0"
370 },
371 "peerDependencies": {
372 "bare-buffer": "*"
373 },
374 "peerDependenciesMeta": {
375 "bare-buffer": {
376 "optional": true
377 }
378 }
379 },
380 "node_modules/bare-module-lexer": {
381 "version": "1.4.7",
382 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
383 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
384 "dev": true,
385 "license": "Apache-2.0",
386 "dependencies": {
387 "require-addon": "^1.0.2"
388 },
389 "peerDependencies": {
390 "bare-buffer": "*"
391 },
392 "peerDependenciesMeta": {
393 "bare-buffer": {
394 "optional": true
395 }
396 }
397 },
398 "node_modules/bare-module-resolve": {
399 "version": "1.12.1",
400 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
401 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
402 "license": "Apache-2.0",
403 "dependencies": {
404 "bare-semver": "^1.0.0"
405 },
406 "peerDependencies": {
407 "bare-url": "*"
408 },
409 "peerDependenciesMeta": {
410 "bare-url": {
411 "optional": true
412 }
413 }
414 },
415 "node_modules/bare-net": {
416 "version": "2.3.1",
417 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz",
418 "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==",
419 "dev": true,
420 "license": "Apache-2.0",
421 "dependencies": {
422 "bare-events": "^2.2.2",
423 "bare-pipe": "^4.0.0",
424 "bare-stream": "^2.0.0",
425 "bare-tcp": "^2.0.0"
426 }
427 },
428 "node_modules/bare-os": {
429 "version": "3.8.4",
430 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.4.tgz",
431 "integrity": "sha512-4JboWUl7/2LhgU536tjUszzaVC8/WEWKtyX5crayvlN71ih8+O2SdvBhotQeDsuhhmPZmLCrPBJEcwVPhI/kkQ==",
432 "devOptional": true,
433 "license": "Apache-2.0",
434 "engines": {
435 "bare": ">=1.14.0"
436 }
437 },
438 "node_modules/bare-path": {
439 "version": "3.0.0",
440 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
441 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
442 "devOptional": true,
443 "license": "Apache-2.0",
444 "dependencies": {
445 "bare-os": "^3.0.1"
446 }
447 },
448 "node_modules/bare-pipe": {
449 "version": "4.1.5",
450 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz",
451 "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==",
452 "dev": true,
453 "license": "Apache-2.0",
454 "dependencies": {
455 "bare-events": "^2.0.0",
456 "bare-stream": "^2.0.0"
457 },
458 "engines": {
459 "bare": ">=1.16.0"
460 }
461 },
462 "node_modules/bare-process": {
463 "version": "4.4.0",
464 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.4.0.tgz",
465 "integrity": "sha512-G5O+Lk1lsPiYjVXP+xS8KUqNzFwibrwvqJ7DO6x8ceLSTwxyMq7NwpbmbGQQZiaiOsdZZHBFs9n+we7OuANhaw==",
466 "dev": true,
467 "license": "Apache-2.0",
468 "dependencies": {
469 "bare-abort": "^2.0.13",
470 "bare-env": "^3.0.0",
471 "bare-events": "^2.3.1",
472 "bare-fs": "^4.5.6",
473 "bare-hrtime": "^2.0.0",
474 "bare-os": "^3.7.1",
475 "bare-signals": "^4.0.0",
476 "bare-tty": "^5.0.0"
477 }
478 },
479 "node_modules/bare-semver": {
480 "version": "1.0.2",
481 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
482 "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
483 "license": "Apache-2.0"
484 },
485 "node_modules/bare-signals": {
486 "version": "4.2.0",
487 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
488 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
489 "dev": true,
490 "license": "Apache-2.0",
491 "dependencies": {
492 "bare-events": "^2.5.3",
493 "bare-os": "^3.3.1"
494 },
495 "engines": {
496 "bare": ">=1.7.0"
497 }
498 },
499 "node_modules/bare-stream": {
500 "version": "2.11.0",
501 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.11.0.tgz",
502 "integrity": "sha512-Y/+iQ49fL3rIn6w/AVxI/2+BRrpmzJvdWt5Jv8Za6Ngqc6V227c+pYjYYgLdpR3MwQ9ObVXD0ZrqoBztakM0rw==",
503 "license": "Apache-2.0",
504 "dependencies": {
505 "streamx": "^2.25.0",
506 "teex": "^1.0.1"
507 },
508 "peerDependencies": {
509 "bare-abort-controller": "*",
510 "bare-buffer": "*",
511 "bare-events": "*"
512 },
513 "peerDependenciesMeta": {
514 "bare-abort-controller": {
515 "optional": true
516 },
517 "bare-buffer": {
518 "optional": true
519 },
520 "bare-events": {
521 "optional": true
522 }
523 }
524 },
525 "node_modules/bare-stylize": {
526 "version": "0.0.1",
527 "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz",
528 "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==",
529 "dev": true,
530 "license": "Apache-2.0",
531 "dependencies": {
532 "bare-ansi-escapes": "^2.2.3",
533 "bare-process": "^4.2.1"
534 }
535 },
536 "node_modules/bare-subprocess": {
537 "version": "5.2.3",
538 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
539 "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
540 "dev": true,
541 "license": "Apache-2.0",
542 "dependencies": {
543 "bare-env": "^3.0.0",
544 "bare-events": "^2.5.4",
545 "bare-os": "^3.0.1",
546 "bare-pipe": "^4.0.0",
547 "bare-url": "^2.2.2"
548 },
549 "engines": {
550 "bare": ">=1.7.0"
551 },
552 "peerDependencies": {
553 "bare-buffer": "*"
554 },
555 "peerDependenciesMeta": {
556 "bare-buffer": {
557 "optional": true
558 }
559 }
560 },
561 "node_modules/bare-tcp": {
562 "version": "2.2.7",
563 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz",
564 "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==",
565 "dev": true,
566 "license": "Apache-2.0",
567 "dependencies": {
568 "bare-dns": "^2.0.4",
569 "bare-events": "^2.5.4",
570 "bare-stream": "^2.6.4"
571 },
572 "engines": {
573 "bare": ">=1.16.0"
574 }
575 },
576 "node_modules/bare-tls": {
577 "version": "2.2.1",
578 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.2.1.tgz",
579 "integrity": "sha512-hZ+ZqwrUO4dyH7/6WYkYWjgAFNJKjzwEYJiDaMnMs+eRleBDjQ3CvNZawpkw0Ar9jnM9NZK6+f6GqjkZ2FLGmQ==",
580 "dev": true,
581 "license": "Apache-2.0",
582 "dependencies": {
583 "bare-net": "^2.0.1",
584 "bare-stream": "^2.6.4"
585 },
586 "engines": {
587 "bare": ">=1.7.0"
588 }
589 },
590 "node_modules/bare-tty": {
591 "version": "5.1.0",
592 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.0.tgz",
593 "integrity": "sha512-EZLvW4A+XiJgI3TW+e1pMME9PIJsfEXe/DA/WSKzIkq/v7Yarpv/rvG6Z5pGnpo4V/Bd+qopwnCLSR71hMMBYA==",
594 "dev": true,
595 "license": "Apache-2.0",
596 "dependencies": {
597 "bare-events": "^2.2.0",
598 "bare-signals": "^4.0.0",
599 "bare-stream": "^2.0.0"
600 },
601 "engines": {
602 "bare": ">=1.16.0"
603 }
604 },
605 "node_modules/bare-type": {
606 "version": "1.1.0",
607 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
608 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
609 "license": "Apache-2.0",
610 "engines": {
611 "bare": ">=1.2.0"
612 }
613 },
614 "node_modules/bare-url": {
615 "version": "2.4.0",
616 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
617 "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
618 "devOptional": true,
619 "license": "Apache-2.0",
620 "peer": true,
621 "dependencies": {
622 "bare-path": "^3.0.0"
623 }
624 },
625 "node_modules/bare-utils": {
626 "version": "1.6.0",
627 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz",
628 "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==",
629 "dev": true,
630 "license": "Apache-2.0",
631 "dependencies": {
632 "bare-debug-log": "^2.0.0",
633 "bare-encoding": "^1.0.0",
634 "bare-format": "^1.0.0",
635 "bare-inspect": "^3.0.0",
636 "bare-stylize": "^0.0.1",
637 "bare-type": "^1.0.6"
638 }
639 },
640 "node_modules/bare-v8-to-istanbul": {
641 "version": "1.0.2",
642 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
643 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
644 "dev": true,
645 "license": "Apache-2.0",
646 "dependencies": {
647 "bare-assert": "^1.0.2",
648 "bare-fs": "^4.1.2",
649 "bare-module": "^6.1.2",
650 "bare-path": "^3.0.0",
651 "bare-process": "^4.2.0",
652 "bare-url": "^2.1.5",
653 "bare-utils": "^1.2.0",
654 "v8-to-istanbul": "^9.3.0",
655 "which-runtime": "^1.2.1"
656 }
657 },
658 "node_modules/bare-ws": {
659 "version": "2.1.0",
660 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.1.0.tgz",
661 "integrity": "sha512-2gEWlPK9iyBchACdIY6oQXgmDz3KLrChdwrPgmU3IVXOFTnxTqSUT27WE/+izd4QojHj/SsqDkQiD2HDvuTdAA==",
662 "dev": true,
663 "license": "Apache-2.0",
664 "dependencies": {
665 "bare-crypto": "^1.2.0",
666 "bare-events": "^2.3.1",
667 "bare-http1": "^4.0.0",
668 "bare-https": "^2.0.0",
669 "bare-stream": "^2.1.2"
670 },
671 "peerDependencies": {
672 "bare-buffer": "*",
673 "bare-url": "*"
674 },
675 "peerDependenciesMeta": {
676 "bare-buffer": {
677 "optional": true
678 },
679 "bare-url": {
680 "optional": true
681 }
682 }
683 },
684 "node_modules/brittle": {
685 "version": "3.19.1",
686 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
687 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
688 "dev": true,
689 "license": "Apache-2.0",
690 "dependencies": {
691 "b4a": "^1.6.0",
692 "bare-assert": "^1.0.2",
693 "bare-cov": "^1.1.0",
694 "bare-fs": "^4.1.6",
695 "bare-os": "^3.6.1",
696 "bare-path": "^3.0.0",
697 "bare-process": "^4.2.1",
698 "bare-subprocess": "^5.0.0",
699 "bare-url": "^2.1.6",
700 "error-stack-parser": "^2.1.4",
701 "globbie": "^1.0.2",
702 "paparam": "^1.6.2",
703 "same-object": "^1.0.2",
704 "test-tmp": "^1.4.0",
705 "tmatch": "^5.0.0"
706 },
707 "bin": {
708 "brittle": "bin/node.js",
709 "brittle-bare": "bin/bare.js",
710 "brittle-node": "bin/node.js",
711 "brittle-pear": "bin/pear.js"
712 }
713 },
714 "node_modules/compact-encoding": {
715 "version": "2.19.2",
716 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.2.tgz",
717 "integrity": "sha512-/YjhHQE/5L4F7l5Bht69dRbP9RV6zoJPeowi8bMKQxNKe3Nh6hOY8pBGoVE9fz5GaWfEd8fWJ2aU9sB4KZuMYg==",
718 "license": "Apache-2.0",
719 "dependencies": {
720 "b4a": "^1.3.0"
721 }
722 },
723 "node_modules/convert-source-map": {
724 "version": "2.0.0",
725 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
726 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
727 "dev": true,
728 "license": "MIT"
729 },
730 "node_modules/error-stack-parser": {
731 "version": "2.1.4",
732 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
733 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
734 "dev": true,
735 "license": "MIT",
736 "dependencies": {
737 "stackframe": "^1.3.4"
738 }
739 },
740 "node_modules/events-universal": {
741 "version": "1.0.1",
742 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
743 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
744 "license": "Apache-2.0",
745 "dependencies": {
746 "bare-events": "^2.7.0"
747 }
748 },
749 "node_modules/fast-fifo": {
750 "version": "1.3.2",
751 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
752 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
753 "license": "MIT"
754 },
755 "node_modules/globbie": {
756 "version": "1.0.3",
757 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
758 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
759 "dev": true,
760 "license": "Apache-2.0",
761 "dependencies": {
762 "bare-fs": "^4.1.2",
763 "bare-path": "^3.0.0",
764 "bare-process": "^4.2.1",
765 "picomatch": "^4.0.2"
766 }
767 },
768 "node_modules/hypercore-crypto": {
769 "version": "3.6.1",
770 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
771 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
772 "license": "MIT",
773 "dependencies": {
774 "b4a": "^1.6.6",
775 "compact-encoding": "^2.15.0",
776 "sodium-universal": "^5.0.0"
777 }
778 },
779 "node_modules/paparam": {
780 "version": "1.10.1",
781 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.1.tgz",
782 "integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==",
783 "dev": true,
784 "license": "Apache-2.0"
785 },
786 "node_modules/picomatch": {
787 "version": "4.0.4",
788 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
789 "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
790 "dev": true,
791 "license": "MIT",
792 "engines": {
793 "node": ">=12"
794 },
795 "funding": {
796 "url": "https://github.com/sponsors/jonschlinkert"
797 }
798 },
799 "node_modules/require-addon": {
800 "version": "1.2.0",
801 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
802 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
803 "license": "Apache-2.0",
804 "dependencies": {
805 "bare-addon-resolve": "^1.3.0"
806 },
807 "engines": {
808 "bare": ">=1.10.0"
809 }
810 },
811 "node_modules/same-object": {
812 "version": "1.0.2",
813 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
814 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
815 "dev": true,
816 "license": "MIT"
817 },
818 "node_modules/sodium-native": {
819 "version": "5.1.0",
820 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
821 "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==",
822 "license": "MIT",
823 "dependencies": {
824 "bare-assert": "^1.2.0",
825 "require-addon": "^1.1.0",
826 "which-runtime": "^1.2.1"
827 },
828 "engines": {
829 "bare": ">=1.16.0"
830 }
831 },
832 "node_modules/sodium-universal": {
833 "version": "5.0.1",
834 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
835 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
836 "license": "MIT",
837 "dependencies": {
838 "sodium-native": "^5.0.1"
839 },
840 "peerDependencies": {
841 "sodium-javascript": "~0.8.0"
842 },
843 "peerDependenciesMeta": {
844 "sodium-javascript": {
845 "optional": true
846 }
847 }
848 },
849 "node_modules/stackframe": {
850 "version": "1.3.4",
851 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
852 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
853 "dev": true,
854 "license": "MIT"
855 },
856 "node_modules/streamx": {
857 "version": "2.25.0",
858 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
859 "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
860 "license": "MIT",
861 "dependencies": {
862 "events-universal": "^1.0.0",
863 "fast-fifo": "^1.3.2",
864 "text-decoder": "^1.1.0"
865 }
866 },
867 "node_modules/teex": {
868 "version": "1.0.1",
869 "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
870 "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
871 "license": "MIT",
872 "dependencies": {
873 "streamx": "^2.12.5"
874 }
875 },
876 "node_modules/test-tmp": {
877 "version": "1.4.0",
878 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
879 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
880 "dev": true,
881 "license": "MIT",
882 "dependencies": {
883 "bare-fs": "^4.0.1",
884 "bare-os": "^3.3.0",
885 "bare-path": "^3.0.0"
886 }
887 },
888 "node_modules/text-decoder": {
889 "version": "1.2.7",
890 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
891 "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
892 "license": "Apache-2.0",
893 "dependencies": {
894 "b4a": "^1.6.4"
895 }
896 },
897 "node_modules/tmatch": {
898 "version": "5.0.0",
899 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
900 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
901 "dev": true,
902 "license": "ISC",
903 "engines": {
904 "node": ">=8"
905 }
906 },
907 "node_modules/v8-to-istanbul": {
908 "version": "9.3.0",
909 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
910 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
911 "dev": true,
912 "license": "ISC",
913 "dependencies": {
914 "@jridgewell/trace-mapping": "^0.3.12",
915 "@types/istanbul-lib-coverage": "^2.0.1",
916 "convert-source-map": "^2.0.0"
917 },
918 "engines": {
919 "node": ">=10.12.0"
920 }
921 },
922 "node_modules/which-runtime": {
923 "version": "1.3.2",
924 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
925 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
926 "license": "Apache-2.0"
927 }
928 }
929}
package.json +20 -0
……@@ -0,0 +1,20 @@
1{
2 "name": "hyper-web-ui",
3 "version": "1.0.0",
4 "description": "Shared web framework for P2P apps — JSON API router, SSR+SPA, identity client",
5 "type": "module",
6 "main": "index.js",
7 "exports": {
8 ".": "./index.js",
9 "./browser/identity": "./web/js/identity.js",
10 "./browser/api": "./web/js/api.js",
11 "./browser/router": "./web/js/router.js",
12 "./theme": "./web/css/theme.css"
13 },
14 "dependencies": {
15 "hypercore-crypto": "^3.0.0"
16 },
17 "devDependencies": {
18 "brittle": "^3.0.0"
19 }
20}
test/router.test.js +153 -0
……@@ -0,0 +1,153 @@
1import test from 'brittle'
2import { Router, json, html, readJson } from '../lib/router.js'
3
4// Mock req/res
5function mockReq (method, url, opts = {}) {
6 const req = {
7 method,
8 url,
9 headers: opts.headers || {},
10 params: {},
11 query: {},
12 [Symbol.asyncIterator]: async function * () {
13 if (opts.body) yield Buffer.from(JSON.stringify(opts.body))
14 }
15 }
16 return req
17}
18
19function mockRes () {
20 const res = {
21 statusCode: null,
22 headers: {},
23 body: '',
24 writableEnded: false,
25 writeHead (status, headers) { res.statusCode = status; res.headers = headers },
26 end (data) { res.body = data || ''; res.writableEnded = true },
27 write (data) { res.body += data }
28 }
29 return res
30}
31
32test('router matches pattern with params', async (t) => {
33 const router = new Router()
34 let captured = null
35
36 router.get('/api/repos/:name', async (req) => {
37 captured = req.params
38 return { name: req.params.name }
39 })
40
41 const req = mockReq('GET', '/api/repos/pear-git')
42 const res = mockRes()
43 const handled = await router.handle(req, res)
44
45 t.is(handled, true)
46 t.is(captured.name, 'pear-git')
47 t.is(res.statusCode, 200)
48 t.ok(res.body.includes('pear-git'))
49})
50
51test('router returns JSON by default', async (t) => {
52 const router = new Router()
53 router.get('/api/status', async () => ({ ok: true }))
54
55 const req = mockReq('GET', '/api/status')
56 const res = mockRes()
57 await router.handle(req, res)
58
59 t.is(res.headers['Content-Type'], 'application/json')
60 t.is(JSON.parse(res.body).ok, true)
61})
62
63test('content negotiation — SSR when Accept: text/html + renderer', async (t) => {
64 const router = new Router()
65 router.get('/repos/:name', async (req) => ({ name: req.params.name }), {
66 render: (data) => `<h1>${data.name}</h1>`
67 })
68
69 const req = mockReq('GET', '/repos/pear-git', { headers: { accept: 'text/html' } })
70 const res = mockRes()
71 await router.handle(req, res)
72
73 t.is(res.headers['Content-Type'], 'text/html; charset=utf-8')
74 t.ok(res.body.includes('<h1>pear-git</h1>'))
75})
76
77test('content negotiation — JSON when Accept: text/html but NO renderer', async (t) => {
78 const router = new Router()
79 router.get('/api/repos/:name', async (req) => ({ name: req.params.name }))
80
81 const req = mockReq('GET', '/api/repos/pear-git', { headers: { accept: 'text/html' } })
82 const res = mockRes()
83 await router.handle(req, res)
84
85 t.is(res.headers['Content-Type'], 'application/json')
86})
87
88test('router handles prefix', async (t) => {
89 const router = new Router('/git')
90 router.get('/api/status', async () => ({ ok: true }))
91
92 const req = mockReq('GET', '/git/api/status')
93 const res = mockRes()
94 const handled = await router.handle(req, res)
95
96 t.is(handled, true)
97 t.is(JSON.parse(res.body).ok, true)
98})
99
100test('router returns false for unmatched routes', async (t) => {
101 const router = new Router()
102 router.get('/api/status', async () => ({ ok: true }))
103
104 const req = mockReq('GET', '/api/nonexistent')
105 const res = mockRes()
106 const handled = await router.handle(req, res)
107
108 t.is(handled, false)
109})
110
111test('query string parsing', async (t) => {
112 const router = new Router()
113 let captured = null
114 router.get('/api/repos', async (req) => { captured = req.query; return {} })
115
116 const req = mockReq('GET', '/api/repos?status=open&sort=newest')
117 const res = mockRes()
118 await router.handle(req, res)
119
120 t.is(captured.status, 'open')
121 t.is(captured.sort, 'newest')
122})
123
124test('POST with JSON body', async (t) => {
125 const router = new Router()
126 let captured = null
127
128 router.post('/api/issues', async (req) => {
129 captured = await readJson(req)
130 return { id: 1 }
131 })
132
133 const req = mockReq('POST', '/api/issues', { body: { title: 'Bug', author: 'alice' } })
134 const res = mockRes()
135 await router.handle(req, res)
136
137 t.is(captured.title, 'Bug')
138 t.is(captured.author, 'alice')
139 t.is(JSON.parse(res.body).id, 1)
140 t.is(res.statusCode, 200)
141})
142
143test('error handler returns 500', async (t) => {
144 const router = new Router()
145 router.get('/api/crash', async () => { throw new Error('boom') })
146
147 const req = mockReq('GET', '/api/crash')
148 const res = mockRes()
149 await router.handle(req, res)
150
151 t.is(res.statusCode, 500)
152 t.is(JSON.parse(res.body).error, 'boom')
153})
web/css/theme.css +103 -0
……@@ -0,0 +1,103 @@
1* { margin: 0; padding: 0; box-sizing: border-box; }
2
3body {
4 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
5 background: #0d1117;
6 color: #c9d1d9;
7 line-height: 1.5;
8}
9
10nav {
11 display: flex;
12 align-items: center;
13 gap: 24px;
14 padding: 12px 24px;
15 background: #161b22;
16 border-bottom: 1px solid #30363d;
17}
18
19nav .logo {
20 font-weight: 700;
21 font-size: 18px;
22 color: #f0f6fc;
23 text-decoration: none;
24}
25
26.nav-links { display: flex; gap: 16px; }
27.nav-links a { color: #8b949e; text-decoration: none; font-size: 14px; }
28.nav-links a:hover { color: #f0f6fc; }
29
30#user-info { margin-left: auto; font-size: 13px; color: #8b949e; }
31
32main { max-width: 1000px; margin: 24px auto; padding: 0 24px; }
33
34h1 { font-size: 24px; color: #f0f6fc; margin-bottom: 16px; }
35h2 { font-size: 18px; color: #f0f6fc; margin-bottom: 12px; }
36
37.repo-list { list-style: none; }
38.repo-item {
39 padding: 16px;
40 border: 1px solid #30363d;
41 border-radius: 6px;
42 margin-bottom: 8px;
43 background: #161b22;
44}
45.repo-item:hover { border-color: #58a6ff; }
46.repo-name { font-size: 16px; font-weight: 600; }
47.repo-name a { color: #58a6ff; text-decoration: none; }
48.repo-meta { font-size: 12px; color: #8b949e; margin-top: 4px; }
49
50.issue-list { list-style: none; }
51.issue-item {
52 padding: 12px 16px;
53 border-bottom: 1px solid #21262d;
54}
55.issue-title { color: #f0f6fc; font-weight: 500; }
56.issue-title a { color: #f0f6fc; text-decoration: none; }
57.issue-title a:hover { color: #58a6ff; }
58.issue-meta { font-size: 12px; color: #8b949e; margin-top: 2px; }
59.issue-status { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; }
60.issue-status.open { background: #238636; color: #fff; }
61.issue-status.closed { background: #8b949e; color: #fff; }
62
63.label { display: inline-block; padding: 1px 6px; border-radius: 12px; font-size: 11px; margin-left: 4px; }
64
65.btn {
66 padding: 6px 16px;
67 border-radius: 6px;
68 border: 1px solid #30363d;
69 background: #21262d;
70 color: #c9d1d9;
71 cursor: pointer;
72 font-size: 13px;
73}
74.btn:hover { background: #30363d; }
75.btn-primary { background: #238636; border-color: #238636; color: #fff; }
76.btn-primary:hover { background: #2ea043; }
77
78input, textarea {
79 width: 100%;
80 padding: 8px 12px;
81 background: #0d1117;
82 border: 1px solid #30363d;
83 border-radius: 6px;
84 color: #c9d1d9;
85 font-size: 14px;
86 margin-bottom: 8px;
87}
88textarea { min-height: 100px; resize: vertical; }
89
90.clone-box {
91 background: #161b22;
92 border: 1px solid #30363d;
93 border-radius: 6px;
94 padding: 12px;
95 margin: 12px 0;
96 font-family: monospace;
97 font-size: 13px;
98 color: #8b949e;
99 word-break: break-all;
100}
101
102.loading { color: #8b949e; padding: 40px; text-align: center; }
103.error { color: #f85149; padding: 16px; }
web/js/api.js +39 -0
……@@ -0,0 +1,39 @@
1/**
2 * API Client — fetch wrapper for pear-git-v2 JSON API
3 */
4
5const BASE = window.location.origin
6
7export async function get (path) {
8 const res = await fetch(BASE + path, {
9 headers: { 'Accept': 'application/json' }
10 })
11 if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
12 return res.json()
13}
14
15export async function post (path, body) {
16 const res = await fetch(BASE + path, {
17 method: 'POST',
18 headers: {
19 'Content-Type': 'application/json',
20 'Accept': 'application/json'
21 },
22 body: JSON.stringify(body)
23 })
24 if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
25 return res.json()
26}
27
28export async function patch (path, body) {
29 const res = await fetch(BASE + path, {
30 method: 'PATCH',
31 headers: {
32 'Content-Type': 'application/json',
33 'Accept': 'application/json'
34 },
35 body: JSON.stringify(body)
36 })
37 if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
38 return res.json()
39}
web/js/identity.js +186 -0
……@@ -0,0 +1,186 @@
1/**
2 * Identity Client — Ed25519 keypair in localStorage + challenge-response auth
3 *
4 * Shared pattern across pear apps (pear-git, hypernote, hyper-booking).
5 * Same origin on 7rnx.net means identity works across all apps.
6 */
7
8import { get, post } from './api.js'
9
10const STORAGE_KEY = 'pear-git-identity'
11const TOKEN_KEY = 'pear-git-token'
12
13let _keypair = null
14let _sodium = null
15
16// Load sodium for Ed25519 operations
17async function getSodium () {
18 if (_sodium) return _sodium
19 // Use noble-ed25519 (pure JS, no native deps, works in browser)
20 // For now, use Web Crypto API for signing
21 _sodium = { ready: true }
22 return _sodium
23}
24
25/**
26 * Get or create Ed25519 keypair (stored in localStorage)
27 */
28export async function getKeypair () {
29 if (_keypair) return _keypair
30
31 const stored = localStorage.getItem(STORAGE_KEY)
32 if (stored) {
33 try {
34 _keypair = JSON.parse(stored)
35 return _keypair
36 } catch {}
37 }
38
39 // Generate new keypair using Web Crypto
40 const keyPair = await crypto.subtle.generateKey(
41 { name: 'Ed25519' },
42 true,
43 ['sign', 'verify']
44 )
45
46 const publicKeyRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey)
47 const privateKeyRaw = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
48
49 _keypair = {
50 publicKey: bufToHex(publicKeyRaw),
51 privateKey: bufToHex(privateKeyRaw),
52 // Store the CryptoKey objects for signing
53 _signKey: keyPair.privateKey,
54 _verifyKey: keyPair.publicKey
55 }
56
57 // Persist (without CryptoKey objects)
58 localStorage.setItem(STORAGE_KEY, JSON.stringify({
59 publicKey: _keypair.publicKey,
60 privateKey: _keypair.privateKey
61 }))
62
63 return _keypair
64}
65
66/**
67 * Get public key hex string
68 */
69export async function getPublicKey () {
70 const kp = await getKeypair()
71 return kp.publicKey
72}
73
74/**
75 * Sign data with private key
76 */
77export async function sign (data) {
78 const kp = await getKeypair()
79
80 // Re-import private key if needed (CryptoKey objects don't persist in JSON)
81 if (!kp._signKey) {
82 kp._signKey = await crypto.subtle.importKey(
83 'pkcs8',
84 hexToBuf(kp.privateKey),
85 { name: 'Ed25519' },
86 false,
87 ['sign']
88 )
89 }
90
91 const dataBuffer = typeof data === 'string' ? new TextEncoder().encode(data) : data
92 const signature = await crypto.subtle.sign('Ed25519', kp._signKey, dataBuffer)
93 return bufToBase64(signature)
94}
95
96/**
97 * Authenticate with server — challenge-response flow
98 */
99export async function authenticate () {
100 const publicKey = await getPublicKey()
101
102 // 1. Get challenge
103 const { challengeId, challenge } = await get('/api/auth/challenge')
104
105 // 2. Sign challenge
106 const signature = await sign(challenge)
107
108 // 3. Verify with server
109 const result = await post('/api/auth/verify', {
110 publicKey,
111 signature,
112 challengeId
113 })
114
115 // 4. Store token
116 localStorage.setItem(TOKEN_KEY, result.token)
117
118 return result
119}
120
121/**
122 * Get stored auth token
123 */
124export function getToken () {
125 return localStorage.getItem(TOKEN_KEY)
126}
127
128/**
129 * Check if authenticated
130 */
131export function isAuthenticated () {
132 return !!getToken()
133}
134
135/**
136 * Get current user info
137 */
138export async function getMe () {
139 const token = getToken()
140 if (!token) return null
141
142 try {
143 const res = await fetch(window.location.origin + '/api/users/me', {
144 headers: { 'Authorization': `Bearer ${token}` }
145 })
146 if (!res.ok) return null
147 return res.json()
148 } catch {
149 return null
150 }
151}
152
153/**
154 * Logout — clear token and keypair
155 */
156export function logout () {
157 localStorage.removeItem(TOKEN_KEY)
158 // Don't remove keypair — user might want to re-auth with same key
159}
160
161/**
162 * Clear everything (nuclear option)
163 */
164export function clearIdentity () {
165 localStorage.removeItem(TOKEN_KEY)
166 localStorage.removeItem(STORAGE_KEY)
167 _keypair = null
168}
169
170// ── Helpers ───────────────────────────────────────────────────
171
172function bufToHex (buf) {
173 return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('')
174}
175
176function hexToBuf (hex) {
177 const bytes = new Uint8Array(hex.length / 2)
178 for (let i = 0; i < hex.length; i += 2) {
179 bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
180 }
181 return bytes.buffer
182}
183
184function bufToBase64 (buf) {
185 return btoa(String.fromCharCode(...new Uint8Array(buf)))
186}
web/js/router.js +40 -0
……@@ -0,0 +1,40 @@
1/**
2 * Browser-side SPA Router
3 *
4 * Hash-based routing shared across P2P apps.
5 * Each app registers its own pages.
6 *
7 * Usage:
8 * import { createRouter } from './router.js'
9 * const router = createRouter(document.getElementById('app'))
10 * router.add(/^\/$/, pageHome)
11 * router.add(/^\/repos\/([^/]+)$/, pageRepo)
12 * router.start()
13 */
14
15export function createRouter (container) {
16 const routes = []
17
18 function add (pattern, handler) {
19 routes.push({ pattern, handler })
20 }
21
22 function navigate () {
23 const hash = window.location.hash.slice(1) || '/'
24 for (const r of routes) {
25 const match = hash.match(r.pattern)
26 if (match) {
27 r.handler(container, ...match.slice(1))
28 return
29 }
30 }
31 container.innerHTML = '<div class="error">404 — Page not found</div>'
32 }
33
34 function start () {
35 window.addEventListener('hashchange', navigate)
36 navigate()
37 }
38
39 return { add, start, navigate }
40}