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 +194 -65
- package/example/server.js +252 -0
- package/index.js +36 -0
- package/package.json +22 -4
- package/src/auth.js +152 -0
- package/src/inbox.js +188 -0
- package/src/outbox.js +268 -0
- package/src/profile.js +114 -0
- package/src/webfinger.js +182 -0
- package/test/auth.test.js +218 -0
- package/test/inbox.test.js +135 -0
- package/test/live.test.js +109 -0
- package/test/mastodon.test.js +243 -0
- package/test/outbox.test.js +195 -0
- package/test/profile.test.js +213 -0
- package/test/webfinger.test.js +139 -0
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>
|
|
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
|
[](https://github.com/micro-fed/microfed.org/blob/gh-pages/LICENSE)
|
|
19
13
|
[](https://npmjs.com/package/microfed)
|
|
20
14
|
[](https://npmjs.com/package/microfed)
|
|
21
15
|
[](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
|
-
✓ The Profile page
|
|
49
|
-
✓ The User / Actor / Agent
|
|
50
|
-
✓ Attributes about the User
|
|
51
|
-
✓ Ability to store a public key
|
|
52
|
-
✓ A list of connections (friends, knows, followers etc.)
|
|
53
|
-
✓ Endpoint for Inbox
|
|
54
|
-
✓ Endpoint for Outbox
|
|
55
|
-
✓ Authentictation endpoints
|
|
56
|
-
✓ 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
|
-
|
|
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
|
-
[
|
|
207
|
+
Each module can be used independently or combined. See the [Design Documentation](./DESIGN.md) for architecture details.
|
|
79
208
|
|
|
80
|
-
|
|
209
|
+
## License
|
|
81
210
|
|
|
82
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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
|
-
"
|
|
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://
|
|
28
|
+
"homepage": "https://microfed.org",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"activitypub",
|
|
31
|
+
"fediverse",
|
|
32
|
+
"microservices",
|
|
33
|
+
"federation",
|
|
34
|
+
"activitystreams",
|
|
35
|
+
"webfinger"
|
|
36
|
+
]
|
|
19
37
|
}
|