javascript-solid-server 0.0.3 → 0.0.5
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/package.json +1 -1
- package/src/auth/middleware.js +97 -0
- package/src/auth/token.js +112 -0
- package/src/handlers/container.js +24 -4
- package/src/handlers/resource.js +19 -10
- package/src/ldp/headers.js +31 -6
- package/src/server.js +20 -0
- package/src/wac/checker.js +257 -0
- package/src/wac/parser.js +284 -0
- package/test/auth.test.js +175 -0
- package/test/helpers.js +38 -4
- package/test/ldp.test.js +61 -20
- package/test/pod.test.js +16 -23
- package/test/wac.test.js +189 -0
- package/benchmark-report-2025-03-31T14-25-24.234Z.json +0 -44
package/package.json
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization middleware
|
|
3
|
+
* Combines authentication (token verification) with WAC checking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getWebIdFromRequest } from './token.js';
|
|
7
|
+
import { checkAccess, getRequiredMode } from '../wac/checker.js';
|
|
8
|
+
import * as storage from '../storage/filesystem.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if request is authorized
|
|
12
|
+
* @param {object} request - Fastify request
|
|
13
|
+
* @param {object} reply - Fastify reply
|
|
14
|
+
* @returns {Promise<{authorized: boolean, webId: string|null, wacAllow: string}>}
|
|
15
|
+
*/
|
|
16
|
+
export async function authorize(request, reply) {
|
|
17
|
+
const urlPath = request.url.split('?')[0];
|
|
18
|
+
const method = request.method;
|
|
19
|
+
|
|
20
|
+
// Skip auth for .acl files (they need special handling)
|
|
21
|
+
// and for OPTIONS (CORS preflight)
|
|
22
|
+
if (urlPath.endsWith('.acl') || method === 'OPTIONS') {
|
|
23
|
+
return { authorized: true, webId: null, wacAllow: 'user="read write append control", public="read write append"' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get WebID from token (null if not authenticated)
|
|
27
|
+
const webId = getWebIdFromRequest(request);
|
|
28
|
+
|
|
29
|
+
// Get resource info
|
|
30
|
+
const stats = await storage.stat(urlPath);
|
|
31
|
+
const resourceExists = stats !== null;
|
|
32
|
+
const isContainer = stats?.isDirectory || urlPath.endsWith('/');
|
|
33
|
+
|
|
34
|
+
// Build resource URL
|
|
35
|
+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
36
|
+
|
|
37
|
+
// Get required access mode for this method
|
|
38
|
+
const requiredMode = getRequiredMode(method);
|
|
39
|
+
|
|
40
|
+
// For write operations on non-existent resources, check parent container
|
|
41
|
+
let checkPath = urlPath;
|
|
42
|
+
let checkUrl = resourceUrl;
|
|
43
|
+
let checkIsContainer = isContainer;
|
|
44
|
+
|
|
45
|
+
if (!resourceExists && (method === 'PUT' || method === 'POST' || method === 'PATCH')) {
|
|
46
|
+
// Check write permission on parent container
|
|
47
|
+
const parentPath = getParentPath(urlPath);
|
|
48
|
+
checkPath = parentPath;
|
|
49
|
+
checkUrl = `${request.protocol}://${request.hostname}${parentPath}`;
|
|
50
|
+
checkIsContainer = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check WAC permissions
|
|
54
|
+
const { allowed, wacAllow } = await checkAccess({
|
|
55
|
+
resourceUrl: checkUrl,
|
|
56
|
+
resourcePath: checkPath,
|
|
57
|
+
isContainer: checkIsContainer,
|
|
58
|
+
agentWebId: webId,
|
|
59
|
+
requiredMode
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return { authorized: allowed, webId, wacAllow };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get parent container path
|
|
67
|
+
*/
|
|
68
|
+
function getParentPath(path) {
|
|
69
|
+
const normalized = path.endsWith('/') ? path.slice(0, -1) : path;
|
|
70
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
71
|
+
if (lastSlash <= 0) return '/';
|
|
72
|
+
return normalized.substring(0, lastSlash + 1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Handle unauthorized request
|
|
77
|
+
* @param {object} reply - Fastify reply
|
|
78
|
+
* @param {boolean} isAuthenticated - Whether user is authenticated
|
|
79
|
+
* @param {string} wacAllow - WAC-Allow header value
|
|
80
|
+
*/
|
|
81
|
+
export function handleUnauthorized(reply, isAuthenticated, wacAllow) {
|
|
82
|
+
reply.header('WAC-Allow', wacAllow);
|
|
83
|
+
|
|
84
|
+
if (!isAuthenticated) {
|
|
85
|
+
// Not authenticated - return 401
|
|
86
|
+
return reply.code(401).send({
|
|
87
|
+
error: 'Unauthorized',
|
|
88
|
+
message: 'Authentication required'
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
// Authenticated but not authorized - return 403
|
|
92
|
+
return reply.code(403).send({
|
|
93
|
+
error: 'Forbidden',
|
|
94
|
+
message: 'Access denied'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple token-based authentication
|
|
3
|
+
*
|
|
4
|
+
* For now, we use a simple JWT-like approach:
|
|
5
|
+
* - Token format: base64(JSON({webId, iat, exp}))
|
|
6
|
+
* - In production, this would be replaced with proper Solid-OIDC DPoP tokens
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
|
|
11
|
+
// Secret for signing tokens (in production, use env var)
|
|
12
|
+
const SECRET = process.env.TOKEN_SECRET || 'dev-secret-change-in-production';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a simple token for a WebID
|
|
16
|
+
* @param {string} webId - The WebID to create token for
|
|
17
|
+
* @param {number} expiresIn - Expiration time in seconds (default 1 hour)
|
|
18
|
+
* @returns {string} Token string
|
|
19
|
+
*/
|
|
20
|
+
export function createToken(webId, expiresIn = 3600) {
|
|
21
|
+
const payload = {
|
|
22
|
+
webId,
|
|
23
|
+
iat: Math.floor(Date.now() / 1000),
|
|
24
|
+
exp: Math.floor(Date.now() / 1000) + expiresIn
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const data = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
28
|
+
const signature = crypto
|
|
29
|
+
.createHmac('sha256', SECRET)
|
|
30
|
+
.update(data)
|
|
31
|
+
.digest('base64url');
|
|
32
|
+
|
|
33
|
+
return `${data}.${signature}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Verify and decode a token
|
|
38
|
+
* @param {string} token - The token to verify
|
|
39
|
+
* @returns {{webId: string, iat: number, exp: number} | null} Decoded payload or null
|
|
40
|
+
*/
|
|
41
|
+
export function verifyToken(token) {
|
|
42
|
+
if (!token || typeof token !== 'string') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parts = token.split('.');
|
|
47
|
+
if (parts.length !== 2) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const [data, signature] = parts;
|
|
52
|
+
|
|
53
|
+
// Verify signature
|
|
54
|
+
const expectedSig = crypto
|
|
55
|
+
.createHmac('sha256', SECRET)
|
|
56
|
+
.update(data)
|
|
57
|
+
.digest('base64url');
|
|
58
|
+
|
|
59
|
+
if (signature !== expectedSig) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Decode payload
|
|
64
|
+
try {
|
|
65
|
+
const payload = JSON.parse(Buffer.from(data, 'base64url').toString());
|
|
66
|
+
|
|
67
|
+
// Check expiration
|
|
68
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return payload;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Extract token from Authorization header
|
|
80
|
+
* @param {string} authHeader - Authorization header value
|
|
81
|
+
* @returns {string | null} Token or null
|
|
82
|
+
*/
|
|
83
|
+
export function extractToken(authHeader) {
|
|
84
|
+
if (!authHeader || typeof authHeader !== 'string') {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Support "Bearer <token>" format
|
|
89
|
+
if (authHeader.startsWith('Bearer ')) {
|
|
90
|
+
return authHeader.slice(7);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Also support raw token
|
|
94
|
+
return authHeader;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract WebID from request
|
|
99
|
+
* @param {object} request - Fastify request object
|
|
100
|
+
* @returns {string | null} WebID or null if not authenticated
|
|
101
|
+
*/
|
|
102
|
+
export function getWebIdFromRequest(request) {
|
|
103
|
+
const authHeader = request.headers.authorization;
|
|
104
|
+
const token = extractToken(authHeader);
|
|
105
|
+
|
|
106
|
+
if (!token) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const payload = verifyToken(token);
|
|
111
|
+
return payload?.webId || null;
|
|
112
|
+
}
|
|
@@ -2,9 +2,8 @@ import * as storage from '../storage/filesystem.js';
|
|
|
2
2
|
import { getAllHeaders } from '../ldp/headers.js';
|
|
3
3
|
import { isContainer } from '../utils/url.js';
|
|
4
4
|
import { generateProfile, generatePreferences, generateTypeIndex, serialize } from '../webid/profile.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const PROFILE_CONTENT_TYPE = 'text/html';
|
|
5
|
+
import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, serializeAcl } from '../wac/parser.js';
|
|
6
|
+
import { createToken } from '../auth/token.js';
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Handle POST request to container (create new resource)
|
|
@@ -133,6 +132,23 @@ export async function handleCreatePod(request, reply) {
|
|
|
133
132
|
const privateTypeIndex = generateTypeIndex(`${podUri}settings/privateTypeIndex`);
|
|
134
133
|
await storage.write(`${podPath}settings/privateTypeIndex`, serialize(privateTypeIndex));
|
|
135
134
|
|
|
135
|
+
// Create default ACL files
|
|
136
|
+
// Pod root: owner full control, public read
|
|
137
|
+
const rootAcl = generateOwnerAcl(podUri, webId, true);
|
|
138
|
+
await storage.write(`${podPath}.acl`, serializeAcl(rootAcl));
|
|
139
|
+
|
|
140
|
+
// Private folder: owner only (no public)
|
|
141
|
+
const privateAcl = generatePrivateAcl(`${podUri}private/`, webId);
|
|
142
|
+
await storage.write(`${podPath}private/.acl`, serializeAcl(privateAcl));
|
|
143
|
+
|
|
144
|
+
// Settings folder: owner only
|
|
145
|
+
const settingsAcl = generatePrivateAcl(`${podUri}settings/`, webId);
|
|
146
|
+
await storage.write(`${podPath}settings/.acl`, serializeAcl(settingsAcl));
|
|
147
|
+
|
|
148
|
+
// Inbox: owner full, public append
|
|
149
|
+
const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
|
|
150
|
+
await storage.write(`${podPath}inbox/.acl`, serializeAcl(inboxAcl));
|
|
151
|
+
|
|
136
152
|
} catch (err) {
|
|
137
153
|
console.error('Pod creation error:', err);
|
|
138
154
|
// Cleanup on failure
|
|
@@ -146,9 +162,13 @@ export async function handleCreatePod(request, reply) {
|
|
|
146
162
|
|
|
147
163
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
148
164
|
|
|
165
|
+
// Generate token for the pod owner
|
|
166
|
+
const token = createToken(webId);
|
|
167
|
+
|
|
149
168
|
return reply.code(201).send({
|
|
150
169
|
name,
|
|
151
170
|
webId,
|
|
152
|
-
podUri
|
|
171
|
+
podUri,
|
|
172
|
+
token
|
|
153
173
|
});
|
|
154
174
|
}
|
package/src/handlers/resource.js
CHANGED
|
@@ -15,6 +15,7 @@ export async function handleGet(request, reply) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const origin = request.headers.origin;
|
|
18
|
+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
18
19
|
|
|
19
20
|
// Handle container
|
|
20
21
|
if (stats.isDirectory) {
|
|
@@ -31,7 +32,8 @@ export async function handleGet(request, reply) {
|
|
|
31
32
|
isContainer: true,
|
|
32
33
|
etag: indexStats?.etag || stats.etag,
|
|
33
34
|
contentType: 'text/html',
|
|
34
|
-
origin
|
|
35
|
+
origin,
|
|
36
|
+
resourceUrl
|
|
35
37
|
});
|
|
36
38
|
|
|
37
39
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
@@ -40,14 +42,14 @@ export async function handleGet(request, reply) {
|
|
|
40
42
|
|
|
41
43
|
// No index.html, return JSON-LD container listing
|
|
42
44
|
const entries = await storage.listContainer(urlPath);
|
|
43
|
-
const
|
|
44
|
-
const jsonLd = generateContainerJsonLd(baseUrl, entries || []);
|
|
45
|
+
const jsonLd = generateContainerJsonLd(resourceUrl, entries || []);
|
|
45
46
|
|
|
46
47
|
const headers = getAllHeaders({
|
|
47
48
|
isContainer: true,
|
|
48
49
|
etag: stats.etag,
|
|
49
50
|
contentType: 'application/ld+json',
|
|
50
|
-
origin
|
|
51
|
+
origin,
|
|
52
|
+
resourceUrl
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
@@ -65,7 +67,8 @@ export async function handleGet(request, reply) {
|
|
|
65
67
|
isContainer: false,
|
|
66
68
|
etag: stats.etag,
|
|
67
69
|
contentType,
|
|
68
|
-
origin
|
|
70
|
+
origin,
|
|
71
|
+
resourceUrl
|
|
69
72
|
});
|
|
70
73
|
|
|
71
74
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
@@ -84,13 +87,15 @@ export async function handleHead(request, reply) {
|
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
const origin = request.headers.origin;
|
|
90
|
+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
87
91
|
const contentType = stats.isDirectory ? 'application/ld+json' : getContentType(urlPath);
|
|
88
92
|
|
|
89
93
|
const headers = getAllHeaders({
|
|
90
94
|
isContainer: stats.isDirectory,
|
|
91
95
|
etag: stats.etag,
|
|
92
96
|
contentType,
|
|
93
|
-
origin
|
|
97
|
+
origin,
|
|
98
|
+
resourceUrl
|
|
94
99
|
});
|
|
95
100
|
|
|
96
101
|
if (!stats.isDirectory) {
|
|
@@ -135,8 +140,9 @@ export async function handlePut(request, reply) {
|
|
|
135
140
|
}
|
|
136
141
|
|
|
137
142
|
const origin = request.headers.origin;
|
|
138
|
-
const
|
|
139
|
-
headers
|
|
143
|
+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
144
|
+
const headers = getAllHeaders({ isContainer: false, origin, resourceUrl });
|
|
145
|
+
headers['Location'] = resourceUrl;
|
|
140
146
|
|
|
141
147
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
142
148
|
return reply.code(existed ? 204 : 201).send();
|
|
@@ -159,7 +165,8 @@ export async function handleDelete(request, reply) {
|
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
const origin = request.headers.origin;
|
|
162
|
-
const
|
|
168
|
+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
169
|
+
const headers = getAllHeaders({ isContainer: false, origin, resourceUrl });
|
|
163
170
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
164
171
|
|
|
165
172
|
return reply.code(204).send();
|
|
@@ -173,9 +180,11 @@ export async function handleOptions(request, reply) {
|
|
|
173
180
|
const stats = await storage.stat(urlPath);
|
|
174
181
|
|
|
175
182
|
const origin = request.headers.origin;
|
|
183
|
+
const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
|
|
176
184
|
const headers = getAllHeaders({
|
|
177
185
|
isContainer: stats?.isDirectory || isContainer(urlPath),
|
|
178
|
-
origin
|
|
186
|
+
origin,
|
|
187
|
+
resourceUrl
|
|
179
188
|
});
|
|
180
189
|
|
|
181
190
|
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
package/src/ldp/headers.js
CHANGED
|
@@ -7,9 +7,10 @@ const LDP = 'http://www.w3.org/ns/ldp#';
|
|
|
7
7
|
/**
|
|
8
8
|
* Get Link headers for a resource
|
|
9
9
|
* @param {boolean} isContainer
|
|
10
|
+
* @param {string} aclUrl - URL to the ACL resource
|
|
10
11
|
* @returns {string}
|
|
11
12
|
*/
|
|
12
|
-
export function getLinkHeader(isContainer) {
|
|
13
|
+
export function getLinkHeader(isContainer, aclUrl = null) {
|
|
13
14
|
const links = [`<${LDP}Resource>; rel="type"`];
|
|
14
15
|
|
|
15
16
|
if (isContainer) {
|
|
@@ -17,18 +18,42 @@ export function getLinkHeader(isContainer) {
|
|
|
17
18
|
links.push(`<${LDP}BasicContainer>; rel="type"`);
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
// Add acl link for auxiliary resource discovery
|
|
22
|
+
if (aclUrl) {
|
|
23
|
+
links.push(`<${aclUrl}>; rel="acl"`);
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
return links.join(', ');
|
|
21
27
|
}
|
|
22
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Get the ACL URL for a resource
|
|
31
|
+
* @param {string} resourceUrl - Full URL of the resource
|
|
32
|
+
* @param {boolean} isContainer - Whether the resource is a container
|
|
33
|
+
* @returns {string} ACL URL
|
|
34
|
+
*/
|
|
35
|
+
export function getAclUrl(resourceUrl, isContainer) {
|
|
36
|
+
if (isContainer) {
|
|
37
|
+
// Container ACL: /path/.acl
|
|
38
|
+
const base = resourceUrl.endsWith('/') ? resourceUrl : resourceUrl + '/';
|
|
39
|
+
return base + '.acl';
|
|
40
|
+
}
|
|
41
|
+
// Resource ACL: /path/file.acl
|
|
42
|
+
return resourceUrl + '.acl';
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
/**
|
|
24
46
|
* Get standard LDP response headers
|
|
25
47
|
* @param {object} options
|
|
26
48
|
* @returns {object}
|
|
27
49
|
*/
|
|
28
|
-
export function getResponseHeaders({ isContainer = false, etag = null, contentType = null }) {
|
|
50
|
+
export function getResponseHeaders({ isContainer = false, etag = null, contentType = null, resourceUrl = null, wacAllow = null }) {
|
|
51
|
+
// Calculate ACL URL if resource URL provided
|
|
52
|
+
const aclUrl = resourceUrl ? getAclUrl(resourceUrl, isContainer) : null;
|
|
53
|
+
|
|
29
54
|
const headers = {
|
|
30
|
-
'Link': getLinkHeader(isContainer),
|
|
31
|
-
'WAC-Allow': 'user="read write append control", public="read write append"',
|
|
55
|
+
'Link': getLinkHeader(isContainer, aclUrl),
|
|
56
|
+
'WAC-Allow': wacAllow || 'user="read write append control", public="read write append"',
|
|
32
57
|
'Accept-Patch': 'application/sparql-update',
|
|
33
58
|
'Allow': 'GET, HEAD, PUT, DELETE, OPTIONS' + (isContainer ? ', POST' : ''),
|
|
34
59
|
'Vary': 'Accept, Authorization, Origin'
|
|
@@ -70,9 +95,9 @@ export function getCorsHeaders(origin) {
|
|
|
70
95
|
* @param {object} options
|
|
71
96
|
* @returns {object}
|
|
72
97
|
*/
|
|
73
|
-
export function getAllHeaders({ isContainer = false, etag = null, contentType = null, origin = null }) {
|
|
98
|
+
export function getAllHeaders({ isContainer = false, etag = null, contentType = null, origin = null, resourceUrl = null, wacAllow = null }) {
|
|
74
99
|
return {
|
|
75
|
-
...getResponseHeaders({ isContainer, etag, contentType }),
|
|
100
|
+
...getResponseHeaders({ isContainer, etag, contentType, resourceUrl, wacAllow }),
|
|
76
101
|
...getCorsHeaders(origin)
|
|
77
102
|
};
|
|
78
103
|
}
|
package/src/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import Fastify from 'fastify';
|
|
|
2
2
|
import { handleGet, handleHead, handlePut, handleDelete, handleOptions } from './handlers/resource.js';
|
|
3
3
|
import { handlePost, handleCreatePod } from './handlers/container.js';
|
|
4
4
|
import { getCorsHeaders } from './ldp/headers.js';
|
|
5
|
+
import { authorize, handleUnauthorized } from './auth/middleware.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Create and configure Fastify server
|
|
@@ -34,6 +35,25 @@ export function createServer(options = {}) {
|
|
|
34
35
|
}
|
|
35
36
|
});
|
|
36
37
|
|
|
38
|
+
// Authorization hook - check WAC permissions
|
|
39
|
+
// Skip for pod creation endpoint (needs special handling)
|
|
40
|
+
fastify.addHook('preHandler', async (request, reply) => {
|
|
41
|
+
// Skip auth for pod creation and OPTIONS
|
|
42
|
+
if (request.url === '/.pods' || request.method === 'OPTIONS') {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { authorized, webId, wacAllow } = await authorize(request, reply);
|
|
47
|
+
|
|
48
|
+
// Store webId and wacAllow on request for handlers to use
|
|
49
|
+
request.webId = webId;
|
|
50
|
+
request.wacAllow = wacAllow;
|
|
51
|
+
|
|
52
|
+
if (!authorized) {
|
|
53
|
+
return handleUnauthorized(reply, webId !== null, wacAllow);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
37
57
|
// Pod creation endpoint
|
|
38
58
|
fastify.post('/.pods', handleCreatePod);
|
|
39
59
|
|