microfed 0.0.12 → 0.0.14

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/README.md CHANGED
@@ -3,80 +3,209 @@
3
3
  <h1><a href="https://microfed.org/">Microfed</a></h1>
4
4
  </div>
5
5
 
6
- <div align="center">
7
- <i>Micro Services Meets the Fediverse</i>
6
+ <div align="center">
7
+ <i>Minimal, modular ActivityPub microservices</i>
8
8
  </div>
9
9
 
10
10
  ---
11
11
 
12
- <div align="center">
13
- <h4>Documentation</h4>
14
- </div>
15
-
16
- ---
17
-
18
12
  [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/micro-fed/microfed.org/blob/gh-pages/LICENSE)
19
13
  [![npm](https://img.shields.io/npm/v/microfed)](https://npmjs.com/package/microfed)
20
14
  [![npm](https://img.shields.io/npm/dw/microfed.svg)](https://npmjs.com/package/microfed)
21
15
  [![Github Stars](https://img.shields.io/github/stars/micro-fed/microfed.org.svg)](https://github.com/micro-fed/microfed.org/)
22
-
23
- # ⚡️ Introduction
24
-
25
- This project is still at concept stage and aims to brainstorm the intersection of [micro services](https://en.wikipedia.org/wiki/Microservices) and the [fediverse](https://en.wikipedia.org/wiki/Fediverse).
26
-
27
- # 🎨 Design
28
-
29
- Microfed follows a modular design approach, ensuring flexibility and maintainability. The high-level design focuses on the interaction between microservices and the fediverse.
30
-
31
- The idea is that each component of a fediverse server can be composed from smaller services.
32
-
33
- These include:
34
- - [Profile](#Profile)
35
- - [Inbox](#Inbox)
36
- - [Outbox](#Outbox)
37
- - [Authentication](#Authentication)
38
-
39
- [Design Documentation](./DESIGN.md)
40
-
41
-
42
- # ✍️ Profile
43
-
44
- Your Profile page is the starting point for microfed services. It will generally be an HTTP page, but the data should be agnostic to HTTP or any other protocol so that it can live in a database, or run over a P2P network.
45
-
46
- The Profile will be in HTML, with the data in in JSON(-LD). It will contain:
47
-
48
- &nbsp;&nbsp;✓&nbsp;The Profile page
49
- &nbsp;&nbsp;✓&nbsp;The User / Actor / Agent
50
- &nbsp;&nbsp;✓&nbsp;Attributes about the User
51
- &nbsp;&nbsp;✓&nbsp;Ability to store a public key
52
- &nbsp;&nbsp;✓&nbsp;A list of connections (friends, knows, followers etc.)
53
- &nbsp;&nbsp;✓&nbsp;Endpoint for Inbox
54
- &nbsp;&nbsp;✓&nbsp;Endpoint for Outbox
55
- &nbsp;&nbsp;✓&nbsp;Authentictation endpoints
56
- &nbsp;&nbsp;✓&nbsp;Arbitrary fields specified by the user
57
-
58
- The Profile can be self-hosted, or part of a multi user service. It should be able to run on a mobile device, or in the browser.
59
-
60
- [Profile Design](./PROFILE.md)
61
-
62
- # 📬 Inbox
63
-
64
- The Inbox should be a place where people can send messages in JSON. The micro service can filter out messages based on user preferences. The message format should be as far as possible compatible with Activity Pub JSON. Signatures can be used to verify the authenticity of a message.
65
-
66
- [Inbox Design](./INBOX.md)
67
-
68
- # 📤 Outbox
69
-
70
- The Outbox is a service that allows messages to be sent to other inboxes. It should also have to ability to store a private key on behalf of a user, in order to sign outgoing messages. It should be able to route messages to the right endpoints.
71
-
72
- [Outbox Design](./OUTBOX.md)
73
-
74
- # 🔐 Authentication
75
16
 
76
- Initially, strong authentication via PKI will be supported. Delegated authentication, such as OAuth and OIDC may be considered desirable. A loosely coupled authentication suite will allow the user to add different authentication modules. This could also work with enterprise authentication.
17
+ ## Features
18
+
19
+ - **Pure JavaScript** — No TypeScript, no build step
20
+ - **Zero dependencies** — Only Node.js built-ins
21
+ - **Modular** — Use only what you need
22
+ - **Fast** — Minimal overhead
23
+ - **Standards compliant** — ActivityPub, WebFinger, HTTP Signatures
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install microfed
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```javascript
34
+ import { profile, auth, outbox } from 'microfed'
35
+
36
+ // Generate keypair for signing
37
+ const { publicKey, privateKey } = auth.generateKeypair()
38
+
39
+ // Create an actor
40
+ const actor = profile.createActor({
41
+ id: 'https://example.com/users/alice',
42
+ username: 'alice',
43
+ name: 'Alice',
44
+ publicKey
45
+ })
46
+
47
+ // Create a post
48
+ const note = outbox.createNote({
49
+ actor: actor.id,
50
+ content: '<p>Hello, Fediverse!</p>'
51
+ })
52
+
53
+ // Wrap in Create activity
54
+ const activity = outbox.wrapCreate(actor.id, note)
55
+
56
+ // Send to a remote inbox
57
+ await outbox.send({
58
+ activity,
59
+ inbox: 'https://remote.example/users/bob/inbox',
60
+ privateKey,
61
+ keyId: `${actor.id}#main-key`
62
+ })
63
+ ```
64
+
65
+ ## Modules
66
+
67
+ ### profile — Actor generation
68
+
69
+ ```javascript
70
+ import { createActor, createMinimalActor } from 'microfed/profile'
71
+
72
+ const actor = createActor({
73
+ id: 'https://example.com/users/alice',
74
+ username: 'alice',
75
+ name: 'Alice',
76
+ summary: '<p>Hello!</p>',
77
+ publicKey: '-----BEGIN PUBLIC KEY-----...',
78
+ icon: 'https://example.com/avatar.png'
79
+ })
80
+ ```
81
+
82
+ ### auth — Keypairs and HTTP Signatures
83
+
84
+ ```javascript
85
+ import { generateKeypair, sign, verify } from 'microfed/auth'
86
+
87
+ // Generate RSA keypair
88
+ const { publicKey, privateKey } = generateKeypair()
89
+
90
+ // Sign a request
91
+ const headers = sign({
92
+ privateKey,
93
+ keyId: 'https://example.com/users/alice#main-key',
94
+ method: 'POST',
95
+ url: 'https://remote.example/inbox',
96
+ body: JSON.stringify(activity)
97
+ })
98
+
99
+ // Verify incoming signature
100
+ const valid = verify({
101
+ publicKey,
102
+ signature: req.headers.signature,
103
+ method: 'POST',
104
+ path: '/inbox',
105
+ headers: req.headers
106
+ })
107
+ ```
108
+
109
+ ### webfinger — Discovery
110
+
111
+ ```javascript
112
+ import { createResponse, lookup, resolve } from 'microfed/webfinger'
113
+
114
+ // Create WebFinger response
115
+ const response = createResponse(
116
+ 'alice@example.com',
117
+ 'https://example.com/users/alice'
118
+ )
119
+
120
+ // Lookup remote actor
121
+ const actor = await resolve('bob@remote.example')
122
+ ```
123
+
124
+ ### inbox — Receive activities
125
+
126
+ ```javascript
127
+ import { createHandler } from 'microfed/inbox'
128
+
129
+ const handler = createHandler({
130
+ getPublicKey: async (keyId) => {
131
+ // Fetch and return public key for keyId
132
+ },
133
+ handlers: {
134
+ Follow: async (activity) => {
135
+ console.log(`Follow from ${activity.actor}`)
136
+ },
137
+ Create: async (activity) => {
138
+ console.log(`New post: ${activity.object.content}`)
139
+ }
140
+ }
141
+ })
142
+ ```
143
+
144
+ ### outbox — Send activities
145
+
146
+ ```javascript
147
+ import { createNote, createFollow, send, deliver } from 'microfed/outbox'
148
+
149
+ // Create a post
150
+ const note = createNote({
151
+ actor: 'https://example.com/users/alice',
152
+ content: '<p>Hello!</p>'
153
+ })
154
+
155
+ // Create a follow
156
+ const follow = createFollow(
157
+ 'https://example.com/users/alice',
158
+ 'https://remote.example/users/bob'
159
+ )
160
+
161
+ // Deliver to multiple inboxes
162
+ const results = await deliver({
163
+ activity,
164
+ inboxes: ['https://server1.example/inbox', 'https://server2.example/inbox'],
165
+ privateKey,
166
+ keyId: 'https://example.com/users/alice#main-key'
167
+ })
168
+ ```
169
+
170
+ ## Example Server
171
+
172
+ Run the demo server:
173
+
174
+ ```bash
175
+ npm run example
176
+ # → http://localhost:3000
177
+ ```
178
+
179
+ Test it:
180
+
181
+ ```bash
182
+ # WebFinger
183
+ curl "http://localhost:3000/.well-known/webfinger?resource=acct:alice@localhost:3000"
184
+
185
+ # Actor
186
+ curl -H "Accept: application/activity+json" http://localhost:3000/users/alice
187
+ ```
188
+
189
+ ## Testing
190
+
191
+ ```bash
192
+ npm test
193
+ ```
194
+
195
+ ## Design
196
+
197
+ Microfed decomposes a fediverse server into modular microservices:
198
+
199
+ | Module | Purpose |
200
+ |--------|---------|
201
+ | **profile** | Actor/user representation |
202
+ | **auth** | Cryptographic identity and signatures |
203
+ | **webfinger** | Actor discovery |
204
+ | **inbox** | Receive and process activities |
205
+ | **outbox** | Create and send activities |
77
206
 
78
- [Authentication Design](./AUTHENTICATION.md)
207
+ Each module can be used independently or combined. See the [Design Documentation](./DESIGN.md) for architecture details.
79
208
 
80
- # ⚖️ License
209
+ ## License
81
210
 
82
- - MIT
211
+ MIT
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Microfed Example Server
3
+ * Minimal ActivityPub server using native Node.js http
4
+ *
5
+ * Run: node example/server.js
6
+ * Test: curl http://localhost:3000/.well-known/webfinger?resource=acct:alice@localhost:3000
7
+ */
8
+
9
+ import { createServer } from 'http'
10
+ import { profile, auth, webfinger, inbox } from '../index.js'
11
+
12
+ // Configuration
13
+ const PORT = process.env.PORT || 3000
14
+ const DOMAIN = process.env.DOMAIN || `localhost:${PORT}`
15
+ const PROTOCOL = DOMAIN.includes('localhost') ? 'http' : 'https'
16
+
17
+ // Generate keypair for our actor
18
+ const { publicKey, privateKey } = auth.generateKeypair()
19
+
20
+ // Create our actor
21
+ const actor = profile.createActor({
22
+ id: `${PROTOCOL}://${DOMAIN}/users/alice`,
23
+ username: 'alice',
24
+ name: 'Alice (Microfed Demo)',
25
+ summary: '<p>A demo actor running on Microfed</p>',
26
+ publicKey
27
+ })
28
+
29
+ // Store for received activities
30
+ const activities = []
31
+ const followers = []
32
+
33
+ // Activity handlers
34
+ const handlers = {
35
+ Follow: async (activity) => {
36
+ console.log(`Follow from: ${activity.actor}`)
37
+ followers.push(activity.actor)
38
+ // In production, you'd send an Accept activity back
39
+ },
40
+
41
+ Create: async (activity) => {
42
+ console.log(`Create: ${activity.object?.type}`)
43
+ activities.push(activity)
44
+ },
45
+
46
+ Like: async (activity) => {
47
+ console.log(`Like from: ${activity.actor}`)
48
+ },
49
+
50
+ Undo: async (activity) => {
51
+ console.log(`Undo: ${activity.object?.type}`)
52
+ if (activity.object?.type === 'Follow') {
53
+ const idx = followers.indexOf(activity.actor)
54
+ if (idx > -1) followers.splice(idx, 1)
55
+ }
56
+ }
57
+ }
58
+
59
+ // Get public key for signature verification
60
+ async function getPublicKey(keyId) {
61
+ // In production, fetch the actor and extract publicKey
62
+ // For demo, we just return null (skip verification)
63
+ console.log(`Key lookup: ${keyId}`)
64
+ return null
65
+ }
66
+
67
+ // Create inbox handler
68
+ const inboxHandler = inbox.createHandler({
69
+ getPublicKey,
70
+ handlers,
71
+ verifySignatures: false // Disable for local testing
72
+ })
73
+
74
+ // Request handler
75
+ async function handleRequest(req, res) {
76
+ const url = new URL(req.url, `${PROTOCOL}://${DOMAIN}`)
77
+ const path = url.pathname
78
+ const accept = req.headers.accept || ''
79
+ const isActivityPub = accept.includes('activity+json') || accept.includes('ld+json')
80
+
81
+ console.log(`${req.method} ${path}`)
82
+
83
+ // CORS headers
84
+ res.setHeader('Access-Control-Allow-Origin', '*')
85
+ res.setHeader('Access-Control-Allow-Headers', '*')
86
+
87
+ if (req.method === 'OPTIONS') {
88
+ res.writeHead(204)
89
+ return res.end()
90
+ }
91
+
92
+ // WebFinger
93
+ if (path === '/.well-known/webfinger') {
94
+ const resource = url.searchParams.get('resource')
95
+ const parsed = webfinger.parseResource(resource)
96
+
97
+ if (!parsed || parsed.username !== 'alice') {
98
+ res.writeHead(404)
99
+ return res.end('Not found')
100
+ }
101
+
102
+ const response = webfinger.createResponse(
103
+ `alice@${DOMAIN}`,
104
+ actor.id,
105
+ { profileUrl: `${PROTOCOL}://${DOMAIN}/@alice` }
106
+ )
107
+
108
+ res.setHeader('Content-Type', 'application/jrd+json')
109
+ return res.end(JSON.stringify(response, null, 2))
110
+ }
111
+
112
+ // Actor profile
113
+ if (path === '/users/alice') {
114
+ res.setHeader('Content-Type', 'application/activity+json')
115
+ return res.end(JSON.stringify(actor, null, 2))
116
+ }
117
+
118
+ // Inbox
119
+ if (path === '/users/alice/inbox' && req.method === 'POST') {
120
+ // Convert Node request to Web Request for handler
121
+ const chunks = []
122
+ for await (const chunk of req) chunks.push(chunk)
123
+ const body = Buffer.concat(chunks).toString()
124
+
125
+ const webReq = new Request(url.href, {
126
+ method: 'POST',
127
+ headers: req.headers,
128
+ body
129
+ })
130
+
131
+ const webRes = await inboxHandler(webReq)
132
+ res.writeHead(webRes.status)
133
+ return res.end(await webRes.text())
134
+ }
135
+
136
+ // Outbox (read-only collection)
137
+ if (path === '/users/alice/outbox') {
138
+ const collection = {
139
+ '@context': 'https://www.w3.org/ns/activitystreams',
140
+ type: 'OrderedCollection',
141
+ id: `${actor.id}/outbox`,
142
+ totalItems: 0,
143
+ orderedItems: []
144
+ }
145
+ res.setHeader('Content-Type', 'application/activity+json')
146
+ return res.end(JSON.stringify(collection, null, 2))
147
+ }
148
+
149
+ // Followers collection
150
+ if (path === '/users/alice/followers') {
151
+ const collection = {
152
+ '@context': 'https://www.w3.org/ns/activitystreams',
153
+ type: 'OrderedCollection',
154
+ id: `${actor.id}/followers`,
155
+ totalItems: followers.length,
156
+ orderedItems: followers
157
+ }
158
+ res.setHeader('Content-Type', 'application/activity+json')
159
+ return res.end(JSON.stringify(collection, null, 2))
160
+ }
161
+
162
+ // Following collection
163
+ if (path === '/users/alice/following') {
164
+ const collection = {
165
+ '@context': 'https://www.w3.org/ns/activitystreams',
166
+ type: 'OrderedCollection',
167
+ id: `${actor.id}/following`,
168
+ totalItems: 0,
169
+ orderedItems: []
170
+ }
171
+ res.setHeader('Content-Type', 'application/activity+json')
172
+ return res.end(JSON.stringify(collection, null, 2))
173
+ }
174
+
175
+ // HTML profile page
176
+ if (path === '/@alice' || (path === '/users/alice' && !isActivityPub)) {
177
+ res.setHeader('Content-Type', 'text/html')
178
+ return res.end(`<!DOCTYPE html>
179
+ <html>
180
+ <head>
181
+ <title>${actor.name}</title>
182
+ <style>
183
+ body { font-family: system-ui; max-width: 600px; margin: 2rem auto; padding: 1rem; }
184
+ .handle { color: #666; }
185
+ </style>
186
+ </head>
187
+ <body>
188
+ <h1>${actor.name}</h1>
189
+ <p class="handle">@alice@${DOMAIN}</p>
190
+ <p>${actor.summary}</p>
191
+ <p><strong>Followers:</strong> ${followers.length}</p>
192
+ <hr>
193
+ <p><small>Powered by <a href="https://microfed.org">Microfed</a></small></p>
194
+ </body>
195
+ </html>`)
196
+ }
197
+
198
+ // Home
199
+ if (path === '/') {
200
+ res.setHeader('Content-Type', 'text/html')
201
+ return res.end(`<!DOCTYPE html>
202
+ <html>
203
+ <head>
204
+ <title>Microfed Demo</title>
205
+ <style>
206
+ body { font-family: system-ui; max-width: 600px; margin: 2rem auto; padding: 1rem; }
207
+ code { background: #f0f0f0; padding: 0.2rem 0.4rem; border-radius: 3px; }
208
+ pre { background: #f0f0f0; padding: 1rem; overflow-x: auto; }
209
+ </style>
210
+ </head>
211
+ <body>
212
+ <h1>Microfed Demo Server</h1>
213
+ <p>A minimal ActivityPub server.</p>
214
+
215
+ <h2>Endpoints</h2>
216
+ <ul>
217
+ <li><a href="/.well-known/webfinger?resource=acct:alice@${DOMAIN}">WebFinger</a></li>
218
+ <li><a href="/users/alice">Actor (JSON)</a></li>
219
+ <li><a href="/@alice">Profile (HTML)</a></li>
220
+ <li><code>POST /users/alice/inbox</code> - Receive activities</li>
221
+ </ul>
222
+
223
+ <h2>Test with curl</h2>
224
+ <pre>curl -H "Accept: application/activity+json" ${PROTOCOL}://${DOMAIN}/users/alice</pre>
225
+
226
+ <p><a href="https://microfed.org">microfed.org</a></p>
227
+ </body>
228
+ </html>`)
229
+ }
230
+
231
+ res.writeHead(404)
232
+ res.end('Not found')
233
+ }
234
+
235
+ // Start server
236
+ const server = createServer(handleRequest)
237
+
238
+ server.listen(PORT, () => {
239
+ console.log(`
240
+ Microfed Demo Server
241
+ ====================
242
+ Running at: ${PROTOCOL}://${DOMAIN}
243
+
244
+ Endpoints:
245
+ - GET /.well-known/webfinger?resource=acct:alice@${DOMAIN}
246
+ - GET /users/alice
247
+ - POST /users/alice/inbox
248
+ - GET /@alice
249
+
250
+ Try: curl -H "Accept: application/activity+json" ${PROTOCOL}://${DOMAIN}/users/alice
251
+ `)
252
+ })
package/index.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Microfed - Minimal, modular ActivityPub microservices
3
+ *
4
+ * @example
5
+ * import { profile, auth, webfinger, inbox, outbox } from 'microfed'
6
+ *
7
+ * // Generate keypair
8
+ * const { publicKey, privateKey } = auth.generateKeypair()
9
+ *
10
+ * // Create actor
11
+ * const actor = profile.createActor({
12
+ * id: 'https://example.com/users/alice',
13
+ * username: 'alice',
14
+ * name: 'Alice',
15
+ * publicKey
16
+ * })
17
+ *
18
+ * // Create and send a post
19
+ * const note = outbox.createNote({
20
+ * actor: actor.id,
21
+ * content: '<p>Hello, Fediverse!</p>'
22
+ * })
23
+ * const activity = outbox.wrapCreate(actor.id, note)
24
+ */
25
+
26
+ export * as profile from './src/profile.js'
27
+ export * as auth from './src/auth.js'
28
+ export * as webfinger from './src/webfinger.js'
29
+ export * as inbox from './src/inbox.js'
30
+ export * as outbox from './src/outbox.js'
31
+
32
+ // Convenience re-exports
33
+ export { createActor, createMinimalActor } from './src/profile.js'
34
+ export { generateKeypair, sign, verify } from './src/auth.js'
35
+ export { lookup, resolve } from './src/webfinger.js'
36
+ export { createActivity, createNote, send, deliver } from './src/outbox.js'
package/package.json CHANGED
@@ -1,10 +1,20 @@
1
1
  {
2
2
  "name": "microfed",
3
- "version": "0.0.12",
4
- "description": "microfed",
3
+ "version": "0.0.14",
4
+ "description": "Minimal, modular ActivityPub microservices",
5
+ "type": "module",
5
6
  "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./profile": "./src/profile.js",
10
+ "./auth": "./src/auth.js",
11
+ "./webfinger": "./src/webfinger.js",
12
+ "./inbox": "./src/inbox.js",
13
+ "./outbox": "./src/outbox.js"
14
+ },
6
15
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
16
+ "example": "node example/server.js",
17
+ "test": "node --test 'test/*.test.js'"
8
18
  },
9
19
  "repository": {
10
20
  "type": "git",
@@ -15,5 +25,13 @@
15
25
  "bugs": {
16
26
  "url": "https://github.com/micro-fed/microfed.org/issues"
17
27
  },
18
- "homepage": "https://github.com/micro-fed/microfed.org#readme"
28
+ "homepage": "https://microfed.org",
29
+ "keywords": [
30
+ "activitypub",
31
+ "fediverse",
32
+ "microservices",
33
+ "federation",
34
+ "activitystreams",
35
+ "webfinger"
36
+ ]
19
37
  }