javascript-solid-server 0.0.62 → 0.0.65
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/.claude/settings.local.json +3 -1
- package/package.json +3 -2
- package/src/ap/index.js +32 -33
- package/src/ap/routes/actor.js +17 -1
- package/src/ap/routes/inbox.js +14 -6
- package/src/ap/store.js +181 -108
- package/src/server.js +28 -1
|
@@ -215,7 +215,9 @@
|
|
|
215
215
|
"WebFetch(domain:fonstr.com)",
|
|
216
216
|
"Bash(node -e \"import\\(''nostr-tools''\\).then\\(m => console.log\\(Object.keys\\(m\\).join\\(''\\\\n''\\)\\)\\)\":*)",
|
|
217
217
|
"Bash(gh repo list:*)",
|
|
218
|
-
"Bash(gh search:*)"
|
|
218
|
+
"Bash(gh search:*)",
|
|
219
|
+
"Bash(__NEW_LINE__ echo \"\")",
|
|
220
|
+
"WebFetch(domain:webfinger.net)"
|
|
219
221
|
]
|
|
220
222
|
}
|
|
221
223
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "javascript-solid-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
4
4
|
"description": "A minimal, fast Solid server",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"microfed": "^0.0.14",
|
|
37
37
|
"n3": "^1.26.0",
|
|
38
38
|
"nostr-tools": "^2.19.4",
|
|
39
|
-
"oidc-provider": "^9.6.0"
|
|
39
|
+
"oidc-provider": "^9.6.0",
|
|
40
|
+
"sql.js": "^1.13.0"
|
|
40
41
|
},
|
|
41
42
|
"engines": {
|
|
42
43
|
"node": ">=18.0.0"
|
package/src/ap/index.js
CHANGED
|
@@ -11,6 +11,10 @@ import { createOutboxHandler } from './routes/outbox.js'
|
|
|
11
11
|
import { createCollectionsHandler } from './routes/collections.js'
|
|
12
12
|
import { createActorHandler } from './routes/actor.js'
|
|
13
13
|
|
|
14
|
+
// Shared state for actor handler (accessed by server.js)
|
|
15
|
+
let sharedActorHandler = null
|
|
16
|
+
export function getActorHandler() { return sharedActorHandler }
|
|
17
|
+
|
|
14
18
|
/**
|
|
15
19
|
* ActivityPub Fastify plugin
|
|
16
20
|
* @param {FastifyInstance} fastify
|
|
@@ -23,7 +27,7 @@ import { createActorHandler } from './routes/actor.js'
|
|
|
23
27
|
export async function activityPubPlugin(fastify, options = {}) {
|
|
24
28
|
// Initialize storage and keypair
|
|
25
29
|
const keypair = loadOrCreateKeypair()
|
|
26
|
-
initStore()
|
|
30
|
+
await initStore()
|
|
27
31
|
|
|
28
32
|
// Store config for handlers
|
|
29
33
|
const config = {
|
|
@@ -37,16 +41,37 @@ export async function activityPubPlugin(fastify, options = {}) {
|
|
|
37
41
|
// Decorate fastify with AP config
|
|
38
42
|
fastify.decorate('apConfig', config)
|
|
39
43
|
|
|
44
|
+
// Helper to detect protocol from proxy headers
|
|
45
|
+
const getProtocol = (request) => {
|
|
46
|
+
// Check X-Forwarded-Proto first
|
|
47
|
+
let protocol = request.headers['x-forwarded-proto']
|
|
48
|
+
if (!protocol) {
|
|
49
|
+
// Cloudflare uses cf-visitor: {"scheme":"https"}
|
|
50
|
+
const cfVisitor = request.headers['cf-visitor']
|
|
51
|
+
if (cfVisitor) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(cfVisitor)
|
|
54
|
+
protocol = parsed.scheme
|
|
55
|
+
} catch { /* ignore */ }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// If still no protocol and hostname looks like a public domain, assume https
|
|
59
|
+
if (!protocol && request.hostname && !request.hostname.match(/^(localhost|127\.|192\.168\.|10\.)/)) {
|
|
60
|
+
protocol = 'https'
|
|
61
|
+
}
|
|
62
|
+
return protocol || request.protocol
|
|
63
|
+
}
|
|
64
|
+
|
|
40
65
|
// Helper to build actor ID from request
|
|
41
66
|
const getActorId = (request) => {
|
|
42
|
-
const protocol = request
|
|
67
|
+
const protocol = getProtocol(request)
|
|
43
68
|
const host = request.headers['x-forwarded-host'] || request.hostname
|
|
44
69
|
return `${protocol}://${host}/profile/card#me`
|
|
45
70
|
}
|
|
46
71
|
|
|
47
72
|
// Helper to get base URL
|
|
48
73
|
const getBaseUrl = (request) => {
|
|
49
|
-
const protocol = request
|
|
74
|
+
const protocol = getProtocol(request)
|
|
50
75
|
const host = request.headers['x-forwarded-host'] || request.hostname
|
|
51
76
|
return `${protocol}://${host}`
|
|
52
77
|
}
|
|
@@ -110,7 +135,7 @@ export async function activityPubPlugin(fastify, options = {}) {
|
|
|
110
135
|
version: '2.1',
|
|
111
136
|
software: {
|
|
112
137
|
name: 'jss',
|
|
113
|
-
version: '0.0.
|
|
138
|
+
version: '0.0.65',
|
|
114
139
|
repository: 'https://github.com/JavaScriptSolidServer/JavaScriptSolidServer'
|
|
115
140
|
},
|
|
116
141
|
protocols: ['activitypub', 'solid'],
|
|
@@ -127,37 +152,11 @@ export async function activityPubPlugin(fastify, options = {}) {
|
|
|
127
152
|
})
|
|
128
153
|
})
|
|
129
154
|
|
|
130
|
-
// Actor endpoint -
|
|
155
|
+
// Actor endpoint - expose handler for profile/card AP requests
|
|
131
156
|
const actorHandler = createActorHandler(config, keypair)
|
|
132
157
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Register dedicated GET route for /profile/card with AP content negotiation
|
|
137
|
-
// This needs to run BEFORE the wildcard LDP routes
|
|
138
|
-
fastify.get('/profile/card', {
|
|
139
|
-
// Run this handler first, before wildcard routes
|
|
140
|
-
preHandler: async (request, reply) => {
|
|
141
|
-
const accept = request.headers.accept || ''
|
|
142
|
-
const wantsAP = accept.includes('activity+json') ||
|
|
143
|
-
accept.includes('ld+json; profile="https://www.w3.org/ns/activitystreams"')
|
|
144
|
-
|
|
145
|
-
if (wantsAP) {
|
|
146
|
-
const actor = actorHandler(request)
|
|
147
|
-
request.apHandled = true
|
|
148
|
-
return reply
|
|
149
|
-
.header('Content-Type', 'application/activity+json')
|
|
150
|
-
.send(actor)
|
|
151
|
-
}
|
|
152
|
-
// If not AP, skip and let the request continue (but this route won't have a main handler)
|
|
153
|
-
// We return early - the request will 404 on this route but get caught by wildcard
|
|
154
|
-
}
|
|
155
|
-
}, async (request, reply) => {
|
|
156
|
-
// This handler won't be reached if AP was handled
|
|
157
|
-
// For non-AP requests, we need to pass through to LDP
|
|
158
|
-
// But we can't easily do that here, so we'll handle it differently
|
|
159
|
-
reply.callNotFound()
|
|
160
|
-
})
|
|
158
|
+
// Store actorHandler in shared state for use by server-level hook
|
|
159
|
+
sharedActorHandler = actorHandler
|
|
161
160
|
|
|
162
161
|
// Inbox endpoint
|
|
163
162
|
const inboxHandler = createInboxHandler(config, keypair)
|
package/src/ap/routes/actor.js
CHANGED
|
@@ -11,8 +11,24 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export function createActorHandler(config, keypair) {
|
|
13
13
|
return (request) => {
|
|
14
|
-
|
|
14
|
+
// Check various proxy headers for protocol detection
|
|
15
|
+
let protocol = request.headers['x-forwarded-proto']
|
|
16
|
+
if (!protocol) {
|
|
17
|
+
// Cloudflare uses cf-visitor: {"scheme":"https"}
|
|
18
|
+
const cfVisitor = request.headers['cf-visitor']
|
|
19
|
+
if (cfVisitor) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(cfVisitor)
|
|
22
|
+
protocol = parsed.scheme
|
|
23
|
+
} catch { /* ignore */ }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// If still no protocol and hostname looks like a public domain, assume https
|
|
15
27
|
const host = request.headers['x-forwarded-host'] || request.hostname
|
|
28
|
+
if (!protocol && host && !host.match(/^(localhost|127\.|192\.168\.|10\.)/)) {
|
|
29
|
+
protocol = 'https'
|
|
30
|
+
}
|
|
31
|
+
protocol = protocol || request.protocol
|
|
16
32
|
const baseUrl = `${protocol}://${host}`
|
|
17
33
|
const profileUrl = `${baseUrl}/profile/card`
|
|
18
34
|
const actorId = `${profileUrl}#me`
|
package/src/ap/routes/inbox.js
CHANGED
|
@@ -17,9 +17,10 @@ import { getKeyId } from '../keys.js'
|
|
|
17
17
|
/**
|
|
18
18
|
* Fetch remote actor (with caching)
|
|
19
19
|
* @param {string} id - Actor URL
|
|
20
|
+
* @param {object} log - Logger instance (optional)
|
|
20
21
|
* @returns {Promise<object|null>} Actor object or null
|
|
21
22
|
*/
|
|
22
|
-
async function fetchActor(id) {
|
|
23
|
+
async function fetchActor(id, log) {
|
|
23
24
|
// Strip fragment for fetching
|
|
24
25
|
const fetchUrl = id.replace(/#.*$/, '')
|
|
25
26
|
const cached = getCachedActor(id)
|
|
@@ -27,14 +28,21 @@ async function fetchActor(id) {
|
|
|
27
28
|
|
|
28
29
|
try {
|
|
29
30
|
const response = await fetch(fetchUrl, {
|
|
30
|
-
headers: {
|
|
31
|
+
headers: {
|
|
32
|
+
'Accept': 'application/activity+json',
|
|
33
|
+
'User-Agent': 'JSS/1.0 (+https://github.com/JavaScriptSolidServer/JavaScriptSolidServer)'
|
|
34
|
+
}
|
|
31
35
|
})
|
|
32
|
-
if (!response.ok)
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
if (log) log.warn(`Actor fetch failed: ${response.status} ${response.statusText} for ${fetchUrl}`)
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
33
40
|
|
|
34
41
|
const actor = await response.json()
|
|
35
42
|
cacheActor(actor)
|
|
36
43
|
return actor
|
|
37
|
-
} catch {
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (log) log.error(`Actor fetch error for ${fetchUrl}: ${err.message}`)
|
|
38
46
|
return null
|
|
39
47
|
}
|
|
40
48
|
}
|
|
@@ -66,7 +74,7 @@ async function verifySignature(request, body) {
|
|
|
66
74
|
const actorUrl = keyId.replace(/#.*$/, '')
|
|
67
75
|
|
|
68
76
|
// Fetch the actor to get their public key
|
|
69
|
-
const remoteActor = await fetchActor(actorUrl)
|
|
77
|
+
const remoteActor = await fetchActor(actorUrl, request.log)
|
|
70
78
|
if (!remoteActor) {
|
|
71
79
|
return { valid: false, reason: `Could not fetch actor: ${actorUrl}` }
|
|
72
80
|
}
|
|
@@ -185,7 +193,7 @@ export function createInboxHandler(config, keypair) {
|
|
|
185
193
|
* Handle Follow activity
|
|
186
194
|
*/
|
|
187
195
|
async function handleFollow(activity, actorId, profileUrl, keypair, log) {
|
|
188
|
-
const followerActor = await fetchActor(activity.actor)
|
|
196
|
+
const followerActor = await fetchActor(activity.actor, log)
|
|
189
197
|
if (!followerActor) {
|
|
190
198
|
log.warn('Could not fetch follower actor')
|
|
191
199
|
return
|
package/src/ap/store.js
CHANGED
|
@@ -1,72 +1,117 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ActivityPub SQLite Storage
|
|
3
3
|
* Persistence layer for federation data
|
|
4
|
+
*
|
|
5
|
+
* Uses better-sqlite3 when available (native, fast)
|
|
6
|
+
* Falls back to sql.js on Android/platforms without native builds
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
import
|
|
7
|
-
import { existsSync, mkdirSync } from 'fs'
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
8
10
|
import { dirname } from 'path'
|
|
9
11
|
|
|
10
12
|
let db = null
|
|
13
|
+
let dbPath = null
|
|
14
|
+
let usingSqlJs = false
|
|
15
|
+
|
|
16
|
+
// SQL schema
|
|
17
|
+
const SCHEMA = `
|
|
18
|
+
-- Followers (people following us)
|
|
19
|
+
CREATE TABLE IF NOT EXISTS followers (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
actor TEXT NOT NULL,
|
|
22
|
+
inbox TEXT,
|
|
23
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
-- Following (people we follow)
|
|
27
|
+
CREATE TABLE IF NOT EXISTS following (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
actor TEXT NOT NULL,
|
|
30
|
+
accepted INTEGER DEFAULT 0,
|
|
31
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
-- Activities (inbox)
|
|
35
|
+
CREATE TABLE IF NOT EXISTS activities (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
type TEXT NOT NULL,
|
|
38
|
+
actor TEXT,
|
|
39
|
+
object TEXT,
|
|
40
|
+
raw TEXT NOT NULL,
|
|
41
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
-- Posts (our outbox)
|
|
45
|
+
CREATE TABLE IF NOT EXISTS posts (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
content TEXT NOT NULL,
|
|
48
|
+
in_reply_to TEXT,
|
|
49
|
+
published TEXT DEFAULT CURRENT_TIMESTAMP
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- Known actors (cache)
|
|
53
|
+
CREATE TABLE IF NOT EXISTS actors (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
data TEXT NOT NULL,
|
|
56
|
+
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
57
|
+
);
|
|
58
|
+
`
|
|
11
59
|
|
|
12
60
|
/**
|
|
13
61
|
* Initialize the database
|
|
62
|
+
* Tries better-sqlite3 first, falls back to sql.js
|
|
14
63
|
* @param {string} path - Path to SQLite file
|
|
15
64
|
*/
|
|
16
|
-
export function initStore(path = 'data/activitypub.db') {
|
|
65
|
+
export async function initStore(path = 'data/activitypub.db') {
|
|
17
66
|
// Ensure directory exists
|
|
18
67
|
const dir = dirname(path)
|
|
19
68
|
if (!existsSync(dir)) {
|
|
20
69
|
mkdirSync(dir, { recursive: true })
|
|
21
70
|
}
|
|
22
71
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
content TEXT NOT NULL,
|
|
57
|
-
in_reply_to TEXT,
|
|
58
|
-
published TEXT DEFAULT CURRENT_TIMESTAMP
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
-- Known actors (cache)
|
|
62
|
-
CREATE TABLE IF NOT EXISTS actors (
|
|
63
|
-
id TEXT PRIMARY KEY,
|
|
64
|
-
data TEXT NOT NULL,
|
|
65
|
-
fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
66
|
-
);
|
|
67
|
-
`)
|
|
72
|
+
dbPath = path
|
|
73
|
+
|
|
74
|
+
// Try better-sqlite3 first (fast, native)
|
|
75
|
+
try {
|
|
76
|
+
const Database = (await import('better-sqlite3')).default
|
|
77
|
+
db = new Database(path)
|
|
78
|
+
db.exec(SCHEMA)
|
|
79
|
+
usingSqlJs = false
|
|
80
|
+
return db
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// Fall back to sql.js (WASM, works everywhere)
|
|
83
|
+
console.log('ActivityPub: Using sql.js (WASM) for SQLite storage')
|
|
84
|
+
|
|
85
|
+
const initSqlJs = (await import('sql.js')).default
|
|
86
|
+
const SQL = await initSqlJs()
|
|
87
|
+
|
|
88
|
+
// Load existing database if it exists
|
|
89
|
+
if (existsSync(path)) {
|
|
90
|
+
const buffer = readFileSync(path)
|
|
91
|
+
db = new SQL.Database(buffer)
|
|
92
|
+
} else {
|
|
93
|
+
db = new SQL.Database()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
db.run(SCHEMA)
|
|
97
|
+
usingSqlJs = true
|
|
98
|
+
|
|
99
|
+
// Save initial database
|
|
100
|
+
saveDatabase()
|
|
101
|
+
|
|
102
|
+
return db
|
|
103
|
+
}
|
|
104
|
+
}
|
|
68
105
|
|
|
69
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Save sql.js database to disk
|
|
108
|
+
*/
|
|
109
|
+
function saveDatabase() {
|
|
110
|
+
if (usingSqlJs && db && dbPath) {
|
|
111
|
+
const data = db.export()
|
|
112
|
+
const buffer = Buffer.from(data)
|
|
113
|
+
writeFileSync(dbPath, buffer)
|
|
114
|
+
}
|
|
70
115
|
}
|
|
71
116
|
|
|
72
117
|
/**
|
|
@@ -79,128 +124,156 @@ export function getStore() {
|
|
|
79
124
|
return db
|
|
80
125
|
}
|
|
81
126
|
|
|
127
|
+
// Helper to run prepared statements across both implementations
|
|
128
|
+
function runStmt(sql, params = []) {
|
|
129
|
+
if (usingSqlJs) {
|
|
130
|
+
db.run(sql, params)
|
|
131
|
+
saveDatabase()
|
|
132
|
+
} else {
|
|
133
|
+
db.prepare(sql).run(...params)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getOne(sql, params = []) {
|
|
138
|
+
if (usingSqlJs) {
|
|
139
|
+
const stmt = db.prepare(sql)
|
|
140
|
+
stmt.bind(params)
|
|
141
|
+
if (stmt.step()) {
|
|
142
|
+
const row = stmt.getAsObject()
|
|
143
|
+
stmt.free()
|
|
144
|
+
return row
|
|
145
|
+
}
|
|
146
|
+
stmt.free()
|
|
147
|
+
return null
|
|
148
|
+
} else {
|
|
149
|
+
return db.prepare(sql).get(...params)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getAll(sql, params = []) {
|
|
154
|
+
if (usingSqlJs) {
|
|
155
|
+
const results = []
|
|
156
|
+
const stmt = db.prepare(sql)
|
|
157
|
+
stmt.bind(params)
|
|
158
|
+
while (stmt.step()) {
|
|
159
|
+
results.push(stmt.getAsObject())
|
|
160
|
+
}
|
|
161
|
+
stmt.free()
|
|
162
|
+
return results
|
|
163
|
+
} else {
|
|
164
|
+
return db.prepare(sql).all(...params)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
82
168
|
// Followers
|
|
83
169
|
|
|
84
170
|
export function addFollower(actorId, inbox) {
|
|
85
|
-
|
|
86
|
-
INSERT OR REPLACE INTO followers (id, actor, inbox)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
stmt.run(actorId, actorId, inbox)
|
|
171
|
+
runStmt(
|
|
172
|
+
'INSERT OR REPLACE INTO followers (id, actor, inbox) VALUES (?, ?, ?)',
|
|
173
|
+
[actorId, actorId, inbox]
|
|
174
|
+
)
|
|
90
175
|
}
|
|
91
176
|
|
|
92
177
|
export function removeFollower(actorId) {
|
|
93
|
-
|
|
94
|
-
stmt.run(actorId)
|
|
178
|
+
runStmt('DELETE FROM followers WHERE id = ?', [actorId])
|
|
95
179
|
}
|
|
96
180
|
|
|
97
181
|
export function getFollowers() {
|
|
98
|
-
|
|
99
|
-
return stmt.all()
|
|
182
|
+
return getAll('SELECT * FROM followers ORDER BY created_at DESC')
|
|
100
183
|
}
|
|
101
184
|
|
|
102
185
|
export function getFollowerCount() {
|
|
103
|
-
const
|
|
104
|
-
return
|
|
186
|
+
const row = getOne('SELECT COUNT(*) as count FROM followers')
|
|
187
|
+
return row ? row.count : 0
|
|
105
188
|
}
|
|
106
189
|
|
|
107
190
|
export function getFollowerInboxes() {
|
|
108
|
-
|
|
109
|
-
|
|
191
|
+
return getAll('SELECT DISTINCT inbox FROM followers WHERE inbox IS NOT NULL')
|
|
192
|
+
.map(row => row.inbox)
|
|
110
193
|
}
|
|
111
194
|
|
|
112
195
|
// Following
|
|
113
196
|
|
|
114
197
|
export function addFollowing(actorId, accepted = false) {
|
|
115
|
-
|
|
116
|
-
INSERT OR REPLACE INTO following (id, actor, accepted)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
stmt.run(actorId, actorId, accepted ? 1 : 0)
|
|
198
|
+
runStmt(
|
|
199
|
+
'INSERT OR REPLACE INTO following (id, actor, accepted) VALUES (?, ?, ?)',
|
|
200
|
+
[actorId, actorId, accepted ? 1 : 0]
|
|
201
|
+
)
|
|
120
202
|
}
|
|
121
203
|
|
|
122
204
|
export function acceptFollowing(actorId) {
|
|
123
|
-
|
|
124
|
-
stmt.run(actorId)
|
|
205
|
+
runStmt('UPDATE following SET accepted = 1 WHERE id = ?', [actorId])
|
|
125
206
|
}
|
|
126
207
|
|
|
127
208
|
export function removeFollowing(actorId) {
|
|
128
|
-
|
|
129
|
-
stmt.run(actorId)
|
|
209
|
+
runStmt('DELETE FROM following WHERE id = ?', [actorId])
|
|
130
210
|
}
|
|
131
211
|
|
|
132
212
|
export function getFollowing() {
|
|
133
|
-
|
|
134
|
-
return stmt.all()
|
|
213
|
+
return getAll('SELECT * FROM following WHERE accepted = 1 ORDER BY created_at DESC')
|
|
135
214
|
}
|
|
136
215
|
|
|
137
216
|
export function getFollowingCount() {
|
|
138
|
-
const
|
|
139
|
-
return
|
|
217
|
+
const row = getOne('SELECT COUNT(*) as count FROM following WHERE accepted = 1')
|
|
218
|
+
return row ? row.count : 0
|
|
140
219
|
}
|
|
141
220
|
|
|
142
221
|
// Activities
|
|
143
222
|
|
|
144
223
|
export function saveActivity(activity) {
|
|
145
|
-
|
|
146
|
-
INSERT OR REPLACE INTO activities (id, type, actor, object, raw)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
JSON.stringify(activity)
|
|
224
|
+
runStmt(
|
|
225
|
+
'INSERT OR REPLACE INTO activities (id, type, actor, object, raw) VALUES (?, ?, ?, ?, ?)',
|
|
226
|
+
[
|
|
227
|
+
activity.id,
|
|
228
|
+
activity.type,
|
|
229
|
+
typeof activity.actor === 'string' ? activity.actor : activity.actor?.id,
|
|
230
|
+
typeof activity.object === 'string' ? activity.object : JSON.stringify(activity.object),
|
|
231
|
+
JSON.stringify(activity)
|
|
232
|
+
]
|
|
155
233
|
)
|
|
156
234
|
}
|
|
157
235
|
|
|
158
236
|
export function getActivities(limit = 20) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
237
|
+
return getAll('SELECT * FROM activities ORDER BY created_at DESC LIMIT ?', [limit])
|
|
238
|
+
.map(row => ({
|
|
239
|
+
...row,
|
|
240
|
+
raw: JSON.parse(row.raw)
|
|
241
|
+
}))
|
|
164
242
|
}
|
|
165
243
|
|
|
166
244
|
// Posts
|
|
167
245
|
|
|
168
246
|
export function savePost(id, content, inReplyTo = null) {
|
|
169
|
-
|
|
170
|
-
INSERT INTO posts (id, content, in_reply_to)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
stmt.run(id, content, inReplyTo)
|
|
247
|
+
runStmt(
|
|
248
|
+
'INSERT INTO posts (id, content, in_reply_to) VALUES (?, ?, ?)',
|
|
249
|
+
[id, content, inReplyTo]
|
|
250
|
+
)
|
|
174
251
|
}
|
|
175
252
|
|
|
176
253
|
export function getPosts(limit = 20) {
|
|
177
|
-
|
|
178
|
-
return stmt.all(limit)
|
|
254
|
+
return getAll('SELECT * FROM posts ORDER BY published DESC LIMIT ?', [limit])
|
|
179
255
|
}
|
|
180
256
|
|
|
181
257
|
export function getPost(id) {
|
|
182
|
-
|
|
183
|
-
return stmt.get(id)
|
|
258
|
+
return getOne('SELECT * FROM posts WHERE id = ?', [id])
|
|
184
259
|
}
|
|
185
260
|
|
|
186
261
|
export function getPostCount() {
|
|
187
|
-
const
|
|
188
|
-
return
|
|
262
|
+
const row = getOne('SELECT COUNT(*) as count FROM posts')
|
|
263
|
+
return row ? row.count : 0
|
|
189
264
|
}
|
|
190
265
|
|
|
191
266
|
// Actor cache
|
|
192
267
|
|
|
193
268
|
export function cacheActor(actor) {
|
|
194
|
-
|
|
195
|
-
INSERT OR REPLACE INTO actors (id, data, fetched_at)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
stmt.run(actor.id, JSON.stringify(actor))
|
|
269
|
+
runStmt(
|
|
270
|
+
'INSERT OR REPLACE INTO actors (id, data, fetched_at) VALUES (?, ?, datetime("now"))',
|
|
271
|
+
[actor.id, JSON.stringify(actor)]
|
|
272
|
+
)
|
|
199
273
|
}
|
|
200
274
|
|
|
201
275
|
export function getCachedActor(id) {
|
|
202
|
-
const
|
|
203
|
-
const row = stmt.get(id)
|
|
276
|
+
const row = getOne('SELECT * FROM actors WHERE id = ?', [id])
|
|
204
277
|
return row ? JSON.parse(row.data) : null
|
|
205
278
|
}
|
|
206
279
|
|
package/src/server.js
CHANGED
|
@@ -12,7 +12,7 @@ import { idpPlugin } from './idp/index.js';
|
|
|
12
12
|
import { isGitRequest, isGitWriteOperation, handleGit } from './handlers/git.js';
|
|
13
13
|
import { AccessMode } from './wac/parser.js';
|
|
14
14
|
import { registerNostrRelay } from './nostr/relay.js';
|
|
15
|
-
import { activityPubPlugin } from './ap/index.js';
|
|
15
|
+
import { activityPubPlugin, getActorHandler } from './ap/index.js';
|
|
16
16
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
|
|
@@ -202,6 +202,33 @@ export function createServer(options = {}) {
|
|
|
202
202
|
// Note: OPTIONS requests are handled by handleOptions to include Accept-* headers
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
// ActivityPub actor endpoint - dedicated route for /profile/card with AP Accept header
|
|
206
|
+
// Registered before wildcard routes to take priority
|
|
207
|
+
if (activitypubEnabled) {
|
|
208
|
+
fastify.route({
|
|
209
|
+
method: 'GET',
|
|
210
|
+
url: '/profile/card',
|
|
211
|
+
handler: async (request, reply) => {
|
|
212
|
+
const accept = request.headers.accept || '';
|
|
213
|
+
const wantsAP = accept.includes('activity+json') ||
|
|
214
|
+
accept.includes('ld+json; profile="https://www.w3.org/ns/activitystreams"');
|
|
215
|
+
|
|
216
|
+
const actorHandler = getActorHandler();
|
|
217
|
+
if (wantsAP && actorHandler) {
|
|
218
|
+
const actor = actorHandler(request);
|
|
219
|
+
return reply
|
|
220
|
+
.type('application/activity+json')
|
|
221
|
+
.send(actor);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Not AP request - serve the HTML profile from disk
|
|
225
|
+
// This is handled by importing the resource handler
|
|
226
|
+
const { handleGet } = await import('./handlers/resource.js');
|
|
227
|
+
return handleGet(request, reply);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
205
232
|
// Security: Block access to dotfiles except allowed Solid-specific ones
|
|
206
233
|
// This prevents exposure of .git/, .env, .htpasswd, etc.
|
|
207
234
|
// Git protocol requests bypass this check when git is enabled
|