fedbox 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Fedbox Server
3
3
  * ActivityPub server using microfed
4
+ * Solid-compatible URI structure
4
5
  */
5
6
 
6
7
  import { createServer } from 'http'
@@ -19,6 +20,7 @@ import {
19
20
  getActivities,
20
21
  savePost,
21
22
  getPosts,
23
+ getPost,
22
24
  cacheActor,
23
25
  getCachedActor
24
26
  } from './store.js'
@@ -26,6 +28,11 @@ import {
26
28
  let config = null
27
29
  let actor = null
28
30
 
31
+ // Rate limiting: track requests per IP
32
+ const rateLimits = new Map()
33
+ const RATE_LIMIT_WINDOW = 60000 // 1 minute
34
+ const RATE_LIMIT_MAX = 100 // max requests per window
35
+
29
36
  /**
30
37
  * Load configuration
31
38
  */
@@ -52,32 +59,95 @@ function getProtocol() {
52
59
  }
53
60
 
54
61
  /**
55
- * Build actor object
62
+ * Get base URL
56
63
  */
57
- function buildActor() {
58
- const domain = getDomain()
59
- const protocol = getProtocol()
60
- const baseUrl = `${protocol}://${domain}`
64
+ function getBaseUrl() {
65
+ return `${getProtocol()}://${getDomain()}`
66
+ }
61
67
 
62
- return profile.createActor({
63
- id: `${baseUrl}/users/${config.username}`,
64
- username: config.username,
68
+ /**
69
+ * Build actor object with Solid-compatible URIs
70
+ * /alice is the profile document
71
+ * /alice#me is the WebID (the Person)
72
+ */
73
+ function buildActor() {
74
+ const baseUrl = getBaseUrl()
75
+ const profileUrl = `${baseUrl}/${config.username}`
76
+ const actorId = `${profileUrl}#me`
77
+
78
+ // Build actor manually for more control over structure
79
+ return {
80
+ '@context': [
81
+ 'https://www.w3.org/ns/activitystreams',
82
+ 'https://w3id.org/security/v1'
83
+ ],
84
+ type: 'Person',
85
+ id: actorId,
86
+ url: profileUrl,
87
+ preferredUsername: config.username,
65
88
  name: config.displayName,
66
89
  summary: config.summary ? `<p>${config.summary}</p>` : '',
67
- publicKey: config.publicKey,
68
- sharedInbox: `${baseUrl}/inbox`
69
- })
90
+ inbox: `${profileUrl}/inbox`,
91
+ outbox: `${profileUrl}/outbox`,
92
+ followers: `${profileUrl}/followers`,
93
+ following: `${profileUrl}/following`,
94
+ endpoints: {
95
+ sharedInbox: `${baseUrl}/inbox`
96
+ },
97
+ publicKey: {
98
+ id: `${profileUrl}#main-key`,
99
+ owner: actorId,
100
+ publicKeyPem: config.publicKey
101
+ }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Check rate limit
107
+ */
108
+ function checkRateLimit(ip) {
109
+ const now = Date.now()
110
+ const record = rateLimits.get(ip)
111
+
112
+ if (!record || now - record.start > RATE_LIMIT_WINDOW) {
113
+ rateLimits.set(ip, { start: now, count: 1 })
114
+ return true
115
+ }
116
+
117
+ record.count++
118
+ if (record.count > RATE_LIMIT_MAX) {
119
+ return false
120
+ }
121
+
122
+ return true
70
123
  }
71
124
 
125
+ /**
126
+ * Clean old rate limit entries (run periodically)
127
+ */
128
+ function cleanRateLimits() {
129
+ const now = Date.now()
130
+ for (const [ip, record] of rateLimits) {
131
+ if (now - record.start > RATE_LIMIT_WINDOW) {
132
+ rateLimits.delete(ip)
133
+ }
134
+ }
135
+ }
136
+
137
+ // Clean rate limits every minute
138
+ setInterval(cleanRateLimits, RATE_LIMIT_WINDOW)
139
+
72
140
  /**
73
141
  * Fetch remote actor (with caching)
74
142
  */
75
143
  async function fetchActor(id) {
144
+ // Strip fragment for fetching
145
+ const fetchUrl = id.replace(/#.*$/, '')
76
146
  const cached = getCachedActor(id)
77
147
  if (cached) return cached
78
148
 
79
149
  try {
80
- const response = await fetch(id, {
150
+ const response = await fetch(fetchUrl, {
81
151
  headers: { 'Accept': 'application/activity+json' }
82
152
  })
83
153
  if (!response.ok) return null
@@ -90,6 +160,65 @@ async function fetchActor(id) {
90
160
  }
91
161
  }
92
162
 
163
+ /**
164
+ * Verify HTTP signature on incoming request
165
+ */
166
+ async function verifySignature(req, body) {
167
+ const signature = req.headers['signature']
168
+ if (!signature) {
169
+ return { valid: false, reason: 'No signature header' }
170
+ }
171
+
172
+ // Parse signature header
173
+ const sigParts = {}
174
+ signature.split(',').forEach(part => {
175
+ const [key, ...rest] = part.split('=')
176
+ sigParts[key.trim()] = rest.join('=').replace(/^"|"$/g, '')
177
+ })
178
+
179
+ const keyId = sigParts.keyId
180
+ if (!keyId) {
181
+ return { valid: false, reason: 'No keyId in signature' }
182
+ }
183
+
184
+ // Extract actor URL from keyId (strip fragment like #main-key)
185
+ const actorUrl = keyId.replace(/#.*$/, '')
186
+
187
+ // Fetch the actor to get their public key
188
+ const remoteActor = await fetchActor(actorUrl)
189
+ if (!remoteActor) {
190
+ return { valid: false, reason: `Could not fetch actor: ${actorUrl}` }
191
+ }
192
+
193
+ const publicKeyPem = remoteActor.publicKey?.publicKeyPem
194
+ if (!publicKeyPem) {
195
+ return { valid: false, reason: 'Actor has no public key' }
196
+ }
197
+
198
+ // Build the signing string
199
+ const headers = sigParts.headers?.split(' ') || ['(request-target)', 'host', 'date']
200
+ const signingParts = headers.map(header => {
201
+ if (header === '(request-target)') {
202
+ return `(request-target): ${req.method.toLowerCase()} ${req.url}`
203
+ }
204
+ if (header === 'digest' && body) {
205
+ const crypto = require('crypto')
206
+ const digest = crypto.createHash('sha256').update(body).digest('base64')
207
+ return `digest: SHA-256=${digest}`
208
+ }
209
+ return `${header}: ${req.headers[header.toLowerCase()] || ''}`
210
+ })
211
+ const signingString = signingParts.join('\n')
212
+
213
+ // Verify the signature
214
+ try {
215
+ const isValid = auth.verify(signingString, sigParts.signature, publicKeyPem)
216
+ return { valid: isValid, actor: remoteActor }
217
+ } catch (err) {
218
+ return { valid: false, reason: `Verification error: ${err.message}` }
219
+ }
220
+ }
221
+
93
222
  /**
94
223
  * Handle incoming activities
95
224
  */
@@ -108,7 +237,7 @@ async function handleActivity(activity) {
108
237
  handleAccept(activity)
109
238
  break
110
239
  case 'Create':
111
- console.log(` New post: ${activity.object?.content?.slice(0, 50)}...`)
240
+ console.log(` 📝 New post: ${activity.object?.content?.slice(0, 50)}...`)
112
241
  break
113
242
  case 'Like':
114
243
  console.log(` ❤️ Liked: ${activity.object}`)
@@ -141,7 +270,7 @@ async function handleFollow(activity) {
141
270
  activity: accept,
142
271
  inbox: followerActor.inbox,
143
272
  privateKey: config.privateKey,
144
- keyId: `${actor.id}#main-key`
273
+ keyId: `${getBaseUrl()}/${config.username}#main-key`
145
274
  })
146
275
  console.log(` 📤 Sent Accept to ${followerActor.inbox}`)
147
276
  } catch (err) {
@@ -173,7 +302,19 @@ function handleAccept(activity) {
173
302
  * Request handler
174
303
  */
175
304
  async function handleRequest(req, res) {
176
- const url = new URL(req.url, `${getProtocol()}://${getDomain()}`)
305
+ // Get client IP
306
+ const ip = req.headers['x-forwarded-for']?.split(',')[0] ||
307
+ req.socket.remoteAddress ||
308
+ 'unknown'
309
+
310
+ // Check rate limit
311
+ if (!checkRateLimit(ip)) {
312
+ console.log(`🚫 Rate limited: ${ip}`)
313
+ res.writeHead(429, { 'Retry-After': '60' })
314
+ return res.end('Too many requests')
315
+ }
316
+
317
+ const url = new URL(req.url, getBaseUrl())
177
318
  const path = url.pathname
178
319
  const accept = req.headers.accept || ''
179
320
  const isAP = accept.includes('activity+json') || accept.includes('ld+json')
@@ -200,62 +341,112 @@ async function handleRequest(req, res) {
200
341
  return res.end('Not found')
201
342
  }
202
343
 
203
- const response = webfinger.createResponse(
204
- `${config.username}@${getDomain()}`,
205
- actor.id,
206
- { profileUrl: `${getProtocol()}://${getDomain()}/@${config.username}` }
207
- )
344
+ // Return /alice#me as the actor (Solid-compatible WebID)
345
+ const profileUrl = `${getBaseUrl()}/${config.username}`
346
+ const response = {
347
+ subject: `acct:${config.username}@${getDomain()}`,
348
+ links: [
349
+ {
350
+ rel: 'self',
351
+ type: 'application/activity+json',
352
+ href: `${profileUrl}#me`
353
+ },
354
+ {
355
+ rel: 'http://webfinger.net/rel/profile-page',
356
+ type: 'text/html',
357
+ href: profileUrl
358
+ }
359
+ ]
360
+ }
208
361
 
209
362
  res.setHeader('Content-Type', 'application/jrd+json')
210
363
  return res.end(JSON.stringify(response, null, 2))
211
364
  }
212
365
 
213
- // Actor
214
- if (path === `/users/${config.username}`) {
215
- res.setHeader('Content-Type', 'application/activity+json')
216
- return res.end(JSON.stringify(actor, null, 2))
366
+ // Nodeinfo discovery
367
+ if (path === '/.well-known/nodeinfo') {
368
+ const response = {
369
+ links: [
370
+ {
371
+ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
372
+ href: `${getBaseUrl()}/nodeinfo/2.1`
373
+ }
374
+ ]
375
+ }
376
+ res.setHeader('Content-Type', 'application/json')
377
+ return res.end(JSON.stringify(response, null, 2))
217
378
  }
218
379
 
219
- // Inbox
220
- if (path === `/users/${config.username}/inbox` || path === '/inbox') {
221
- if (req.method !== 'POST') {
222
- res.writeHead(405)
223
- return res.end('Method not allowed')
380
+ // Nodeinfo 2.1
381
+ if (path === '/nodeinfo/2.1') {
382
+ const response = {
383
+ version: '2.1',
384
+ software: {
385
+ name: 'fedbox',
386
+ version: '0.0.4',
387
+ repository: 'https://github.com/micro-fed/fedbox'
388
+ },
389
+ protocols: ['activitypub'],
390
+ services: { inbound: [], outbound: [] },
391
+ usage: {
392
+ users: { total: 1, activeMonth: 1, activeHalfyear: 1 },
393
+ localPosts: getPosts(1000).length
394
+ },
395
+ openRegistrations: false,
396
+ metadata: {
397
+ nodeName: config.displayName || config.username,
398
+ nodeDescription: config.summary || 'A Fedbox instance'
399
+ }
224
400
  }
401
+ res.setHeader('Content-Type', 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.1#"')
402
+ return res.end(JSON.stringify(response, null, 2))
403
+ }
404
+
405
+ // Shared inbox
406
+ if (path === '/inbox') {
407
+ return handleInbox(req, res)
408
+ }
225
409
 
226
- const chunks = []
227
- for await (const chunk of req) chunks.push(chunk)
228
- const body = Buffer.concat(chunks).toString()
229
-
230
- try {
231
- const activity = JSON.parse(body)
232
- await handleActivity(activity)
233
- res.writeHead(202)
234
- return res.end()
235
- } catch (err) {
236
- console.error('Inbox error:', err)
237
- res.writeHead(400)
238
- return res.end('Bad request')
410
+ // Profile routes: /alice, /alice/inbox, /alice/outbox, etc.
411
+ if (path === `/${config.username}`) {
412
+ if (isAP) {
413
+ // Return Actor JSON-LD
414
+ res.setHeader('Content-Type', 'application/activity+json')
415
+ return res.end(JSON.stringify(actor, null, 2))
416
+ } else {
417
+ // Return HTML with embedded JSON-LD
418
+ res.setHeader('Content-Type', 'text/html')
419
+ return res.end(renderProfile())
239
420
  }
240
421
  }
241
422
 
423
+ // Inbox
424
+ if (path === `/${config.username}/inbox`) {
425
+ return handleInbox(req, res)
426
+ }
427
+
242
428
  // Outbox
243
- if (path === `/users/${config.username}/outbox`) {
429
+ if (path === `/${config.username}/outbox`) {
244
430
  const posts = getPosts(20)
431
+ const profileUrl = `${getBaseUrl()}/${config.username}`
245
432
  const collection = {
246
433
  '@context': 'https://www.w3.org/ns/activitystreams',
247
434
  type: 'OrderedCollection',
248
- id: `${actor.id}/outbox`,
435
+ id: `${profileUrl}/outbox`,
249
436
  totalItems: posts.length,
250
437
  orderedItems: posts.map(p => ({
251
438
  type: 'Create',
252
- actor: actor.id,
439
+ actor: `${profileUrl}#me`,
440
+ published: p.published,
253
441
  object: {
254
442
  type: 'Note',
255
443
  id: p.id,
256
444
  content: p.content,
257
445
  published: p.published,
258
- attributedTo: actor.id
446
+ attributedTo: `${profileUrl}#me`,
447
+ to: ['https://www.w3.org/ns/activitystreams#Public'],
448
+ cc: [`${profileUrl}/followers`],
449
+ ...(p.in_reply_to ? { inReplyTo: p.in_reply_to } : {})
259
450
  }
260
451
  }))
261
452
  }
@@ -264,12 +455,13 @@ async function handleRequest(req, res) {
264
455
  }
265
456
 
266
457
  // Followers
267
- if (path === `/users/${config.username}/followers`) {
458
+ if (path === `/${config.username}/followers`) {
268
459
  const followers = getFollowers()
460
+ const profileUrl = `${getBaseUrl()}/${config.username}`
269
461
  const collection = {
270
462
  '@context': 'https://www.w3.org/ns/activitystreams',
271
463
  type: 'OrderedCollection',
272
- id: `${actor.id}/followers`,
464
+ id: `${profileUrl}/followers`,
273
465
  totalItems: followers.length,
274
466
  orderedItems: followers.map(f => f.actor)
275
467
  }
@@ -278,11 +470,12 @@ async function handleRequest(req, res) {
278
470
  }
279
471
 
280
472
  // Following
281
- if (path === `/users/${config.username}/following`) {
473
+ if (path === `/${config.username}/following`) {
474
+ const profileUrl = `${getBaseUrl()}/${config.username}`
282
475
  const collection = {
283
476
  '@context': 'https://www.w3.org/ns/activitystreams',
284
477
  type: 'OrderedCollection',
285
- id: `${actor.id}/following`,
478
+ id: `${profileUrl}/following`,
286
479
  totalItems: getFollowingCount(),
287
480
  orderedItems: []
288
481
  }
@@ -290,10 +483,38 @@ async function handleRequest(req, res) {
290
483
  return res.end(JSON.stringify(collection, null, 2))
291
484
  }
292
485
 
293
- // HTML Profile
294
- if (path === `/@${config.username}` || (path === `/users/${config.username}` && !isAP)) {
295
- res.setHeader('Content-Type', 'text/html')
296
- return res.end(renderProfile())
486
+ // Individual post: /alice/posts/123
487
+ const postMatch = path.match(new RegExp(`^/${config.username}/posts/(\\d+)$`))
488
+ if (postMatch) {
489
+ const postId = `${getBaseUrl()}${path}`
490
+ const post = getPost(postId)
491
+ if (!post) {
492
+ res.writeHead(404)
493
+ return res.end('Not found')
494
+ }
495
+
496
+ const profileUrl = `${getBaseUrl()}/${config.username}`
497
+ const note = {
498
+ '@context': 'https://www.w3.org/ns/activitystreams',
499
+ type: 'Note',
500
+ id: post.id,
501
+ attributedTo: `${profileUrl}#me`,
502
+ content: post.content,
503
+ published: post.published,
504
+ to: ['https://www.w3.org/ns/activitystreams#Public'],
505
+ cc: [`${profileUrl}/followers`]
506
+ }
507
+ if (post.in_reply_to) {
508
+ note.inReplyTo = post.in_reply_to
509
+ }
510
+
511
+ if (isAP) {
512
+ res.setHeader('Content-Type', 'application/activity+json')
513
+ return res.end(JSON.stringify(note, null, 2))
514
+ } else {
515
+ res.setHeader('Content-Type', 'text/html')
516
+ return res.end(renderPost(post, note))
517
+ }
297
518
  }
298
519
 
299
520
  // Home
@@ -307,11 +528,46 @@ async function handleRequest(req, res) {
307
528
  }
308
529
 
309
530
  /**
310
- * Render HTML profile
531
+ * Handle inbox POST
532
+ */
533
+ async function handleInbox(req, res) {
534
+ if (req.method !== 'POST') {
535
+ res.writeHead(405)
536
+ return res.end('Method not allowed')
537
+ }
538
+
539
+ const chunks = []
540
+ for await (const chunk of req) chunks.push(chunk)
541
+ const body = Buffer.concat(chunks).toString()
542
+
543
+ // Verify signature
544
+ const sigResult = await verifySignature(req, body)
545
+ if (!sigResult.valid) {
546
+ console.log(` ⚠️ Signature: ${sigResult.reason}`)
547
+ } else {
548
+ console.log(` 🔐 Signature verified`)
549
+ }
550
+
551
+ try {
552
+ const activity = JSON.parse(body)
553
+ await handleActivity(activity)
554
+ res.writeHead(202)
555
+ return res.end()
556
+ } catch (err) {
557
+ console.error('Inbox error:', err)
558
+ res.writeHead(400)
559
+ return res.end('Bad request')
560
+ }
561
+ }
562
+
563
+ /**
564
+ * Render HTML profile with embedded JSON-LD
311
565
  */
312
566
  function renderProfile() {
313
567
  const followers = getFollowerCount()
314
568
  const following = getFollowingCount()
569
+ const posts = getPosts(10)
570
+ const profileUrl = `${getBaseUrl()}/${config.username}`
315
571
 
316
572
  return `<!DOCTYPE html>
317
573
  <html>
@@ -319,6 +575,10 @@ function renderProfile() {
319
575
  <meta charset="utf-8">
320
576
  <title>${config.displayName} (@${config.username}@${getDomain()})</title>
321
577
  <meta name="viewport" content="width=device-width, initial-scale=1">
578
+ <link rel="alternate" type="application/activity+json" href="${profileUrl}">
579
+ <script type="application/ld+json">
580
+ ${JSON.stringify(actor, null, 2)}
581
+ </script>
322
582
  <style>
323
583
  * { box-sizing: border-box; }
324
584
  body {
@@ -335,6 +595,7 @@ function renderProfile() {
335
595
  border-radius: 16px;
336
596
  padding: 2rem;
337
597
  text-align: center;
598
+ margin-bottom: 1.5rem;
338
599
  }
339
600
  .avatar {
340
601
  width: 120px;
@@ -363,11 +624,21 @@ function renderProfile() {
363
624
  margin-top: 1.5rem;
364
625
  font-size: 0.9rem;
365
626
  }
627
+ .posts { margin-top: 1rem; }
628
+ .post {
629
+ background: #16213e;
630
+ border-radius: 12px;
631
+ padding: 1rem;
632
+ margin-bottom: 1rem;
633
+ }
634
+ .post-content { margin-bottom: 0.5rem; }
635
+ .post-meta { color: #666; font-size: 0.8rem; }
636
+ .post a { color: #667eea; text-decoration: none; }
366
637
  </style>
367
638
  </head>
368
639
  <body>
369
640
  <div class="card">
370
- <div class="avatar">🍺</div>
641
+ <div class="avatar">📦</div>
371
642
  <h1>${config.displayName}</h1>
372
643
  <p class="handle">@${config.username}@${getDomain()}</p>
373
644
  ${config.summary ? `<p class="bio">${config.summary}</p>` : ''}
@@ -380,9 +651,94 @@ function renderProfile() {
380
651
  <div class="stat-num">${following}</div>
381
652
  <div class="stat-label">Following</div>
382
653
  </div>
654
+ <div class="stat">
655
+ <div class="stat-num">${posts.length}</div>
656
+ <div class="stat-label">Posts</div>
657
+ </div>
383
658
  </div>
384
659
  <div class="badge">📦 Powered by Fedbox</div>
385
660
  </div>
661
+
662
+ ${posts.length > 0 ? `
663
+ <div class="posts">
664
+ ${posts.map(p => `
665
+ <div class="post">
666
+ <div class="post-content">${p.content}</div>
667
+ <div class="post-meta">
668
+ <a href="${p.id}">${new Date(p.published).toLocaleString()}</a>
669
+ </div>
670
+ </div>
671
+ `).join('')}
672
+ </div>
673
+ ` : ''}
674
+ </body>
675
+ </html>`
676
+ }
677
+
678
+ /**
679
+ * Render individual post with embedded JSON-LD
680
+ */
681
+ function renderPost(post, note) {
682
+ return `<!DOCTYPE html>
683
+ <html>
684
+ <head>
685
+ <meta charset="utf-8">
686
+ <title>Post by ${config.displayName}</title>
687
+ <meta name="viewport" content="width=device-width, initial-scale=1">
688
+ <link rel="alternate" type="application/activity+json" href="${post.id}">
689
+ <script type="application/ld+json">
690
+ ${JSON.stringify(note, null, 2)}
691
+ </script>
692
+ <style>
693
+ * { box-sizing: border-box; }
694
+ body {
695
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
696
+ max-width: 600px;
697
+ margin: 0 auto;
698
+ padding: 2rem;
699
+ background: #1a1a2e;
700
+ color: #eee;
701
+ min-height: 100vh;
702
+ }
703
+ .post {
704
+ background: #16213e;
705
+ border-radius: 16px;
706
+ padding: 2rem;
707
+ }
708
+ .author {
709
+ display: flex;
710
+ align-items: center;
711
+ gap: 1rem;
712
+ margin-bottom: 1rem;
713
+ }
714
+ .avatar {
715
+ width: 48px;
716
+ height: 48px;
717
+ border-radius: 50%;
718
+ background: linear-gradient(135deg, #667eea, #764ba2);
719
+ display: flex;
720
+ align-items: center;
721
+ justify-content: center;
722
+ font-size: 1.5rem;
723
+ }
724
+ .author-info a { color: #667eea; text-decoration: none; }
725
+ .author-info p { margin: 0; color: #888; font-size: 0.9rem; }
726
+ .content { font-size: 1.2rem; line-height: 1.6; margin: 1rem 0; }
727
+ .meta { color: #666; font-size: 0.9rem; }
728
+ </style>
729
+ </head>
730
+ <body>
731
+ <div class="post">
732
+ <div class="author">
733
+ <div class="avatar">📦</div>
734
+ <div class="author-info">
735
+ <a href="/${config.username}">${config.displayName}</a>
736
+ <p>@${config.username}@${getDomain()}</p>
737
+ </div>
738
+ </div>
739
+ <div class="content">${post.content}</div>
740
+ <div class="meta">${new Date(post.published).toLocaleString()}</div>
741
+ </div>
386
742
  </body>
387
743
  </html>`
388
744
  }
@@ -391,6 +747,7 @@ function renderProfile() {
391
747
  * Render home page
392
748
  */
393
749
  function renderHome() {
750
+ const profileUrl = `${getBaseUrl()}/${config.username}`
394
751
  return `<!DOCTYPE html>
395
752
  <html>
396
753
  <head>
@@ -418,15 +775,16 @@ function renderHome() {
418
775
  <p>Your Fediverse server is running!</p>
419
776
 
420
777
  <h2>Your Profile</h2>
421
- <p><a href="/@${config.username}">@${config.username}@${getDomain()}</a></p>
778
+ <p><a href="/${config.username}">@${config.username}@${getDomain()}</a></p>
422
779
 
423
780
  <h2>Endpoints</h2>
424
781
  <ul class="endpoints">
425
782
  <li><code>/.well-known/webfinger</code> - Discovery</li>
426
- <li><code>/users/${config.username}</code> - Actor</li>
427
- <li><code>/users/${config.username}/inbox</code> - Inbox</li>
428
- <li><code>/users/${config.username}/outbox</code> - Outbox</li>
429
- <li><code>/users/${config.username}/followers</code> - Followers</li>
783
+ <li><code>/${config.username}</code> - Profile (HTML + JSON-LD)</li>
784
+ <li><code>/${config.username}#me</code> - WebID (Actor)</li>
785
+ <li><code>/${config.username}/inbox</code> - Inbox</li>
786
+ <li><code>/${config.username}/outbox</code> - Outbox</li>
787
+ <li><code>/${config.username}/followers</code> - Followers</li>
430
788
  </ul>
431
789
 
432
790
  <h2>Federation</h2>
@@ -451,10 +809,10 @@ export async function startServer() {
451
809
  console.log(`
452
810
  📦 Fedbox is running!
453
811
 
454
- Profile: http://localhost:${config.port}/@${config.username}
455
- Actor: http://localhost:${config.port}/users/${config.username}
812
+ Profile: http://localhost:${config.port}/${config.username}
813
+ WebID: http://localhost:${config.port}/${config.username}#me
456
814
 
457
- ${config.domain ? ` Federated: https://${config.domain}/@${config.username}` : ' ⚠️ Set "domain" in fedbox.json for federation'}
815
+ ${config.domain ? ` Federated: https://${config.domain}/${config.username}` : ' ⚠️ Set "domain" in fedbox.json for federation'}
458
816
 
459
817
  Press Ctrl+C to stop
460
818
  `)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fedbox",
3
- "version": "0.0.1",
3
+ "version": "0.0.4",
4
4
  "description": "Zero to Fediverse in 60 seconds",
5
5
  "type": "module",
6
6
  "main": "lib/server.js",