javascript-solid-server 0.0.48 → 0.0.49
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/SECURITY-AUDIT-2026-01-03.md +208 -0
- package/package.json +1 -1
- package/src/auth/middleware.js +44 -3
- package/src/auth/token.js +56 -28
- package/test/wac.test.js +10 -3
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# JSS Security Audit Report
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-01-03
|
|
4
|
+
**Auditor:** Security Review
|
|
5
|
+
**Version Audited:** 0.0.48
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Executive Summary
|
|
10
|
+
|
|
11
|
+
A security audit of JavaScriptSolidServer revealed **2 critical**, **2 high**, and **2 medium** severity vulnerabilities. The most severe allows unauthenticated users to read and write ACL (Access Control List) files, effectively bypassing all authorization.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Critical Vulnerabilities
|
|
16
|
+
|
|
17
|
+
### 1. ACL Files Bypass Authorization (CRITICAL) ⚠️
|
|
18
|
+
|
|
19
|
+
**Location:** `src/auth/middleware.js:24-28`
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
if (urlPath.endsWith('.acl') || method === 'OPTIONS') {
|
|
23
|
+
return { authorized: true, webId: null, wacAllow: '...', authError: null };
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Description:** The authorization middleware explicitly skips authentication and authorization checks for all requests to `.acl` files. This allows any unauthenticated user to:
|
|
28
|
+
|
|
29
|
+
1. **Read any ACL file** - Discover permission structures
|
|
30
|
+
2. **Write/Create any ACL file** - Grant themselves access to any resource
|
|
31
|
+
3. **Modify existing ACL files** - Lock out legitimate owners
|
|
32
|
+
|
|
33
|
+
**Proof of Concept:**
|
|
34
|
+
```bash
|
|
35
|
+
# Read root ACL without authentication
|
|
36
|
+
curl https://example.com/.acl
|
|
37
|
+
|
|
38
|
+
# Create malicious ACL without authentication
|
|
39
|
+
curl -X PUT https://example.com/victim/.acl \
|
|
40
|
+
-H "Content-Type: application/ld+json" \
|
|
41
|
+
-d '{"@graph":[{"@id":"#attacker","@type":"acl:Authorization","acl:agent":{"@id":"https://attacker.com/card#me"},"acl:accessTo":{"@id":"https://example.com/victim/"},"acl:mode":[{"@id":"acl:Read"},{"@id":"acl:Write"},{"@id":"acl:Control"}]}]}'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Impact:** Complete authorization bypass. Attacker can gain full control of any resource.
|
|
45
|
+
|
|
46
|
+
**CVSS Score:** 9.8 (Critical)
|
|
47
|
+
|
|
48
|
+
**Fix Required:** ACL files should require `acl:Control` permission on the resource they protect.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### 2. JWT Token Signature Not Verified (CRITICAL) ⚠️
|
|
53
|
+
|
|
54
|
+
**Location:** `src/auth/token.js:93-122`
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
function verifyJwtToken(token) {
|
|
58
|
+
const parts = token.split('.');
|
|
59
|
+
if (parts.length !== 3) return null;
|
|
60
|
+
|
|
61
|
+
// Decode the payload (middle part) - NO SIGNATURE VERIFICATION!
|
|
62
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
63
|
+
|
|
64
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (payload.webid) {
|
|
69
|
+
return { webId: payload.webid, iat: payload.iat, exp: payload.exp };
|
|
70
|
+
}
|
|
71
|
+
// ...
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Description:** The `verifyJwtToken` function decodes JWT tokens but **never verifies the cryptographic signature**. An attacker can craft arbitrary JWT tokens with any WebID.
|
|
76
|
+
|
|
77
|
+
**Proof of Concept:**
|
|
78
|
+
```bash
|
|
79
|
+
# Forge a JWT token with attacker's WebID (signature is ignored)
|
|
80
|
+
# Header: {"alg":"RS256","typ":"JWT"}
|
|
81
|
+
# Payload: {"webid":"https://attacker.com/card#me","exp":9999999999}
|
|
82
|
+
curl https://example.com/private/ \
|
|
83
|
+
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJ3ZWJpZCI6Imh0dHBzOi8vYXR0YWNrZXIuY29tL2NhcmQjbWUiLCJleHAiOjk5OTk5OTk5OTl9.fakesig"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Impact:** Complete authentication bypass. Attacker can impersonate any user.
|
|
87
|
+
|
|
88
|
+
**CVSS Score:** 9.8 (Critical)
|
|
89
|
+
|
|
90
|
+
**Fix Required:** Verify JWT signatures against the issuer's JWKS before accepting tokens.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## High Severity Vulnerabilities
|
|
95
|
+
|
|
96
|
+
### 3. Pod Creation Without Authentication (HIGH)
|
|
97
|
+
|
|
98
|
+
**Location:** `src/server.js:203,228`
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
// Auth bypass list includes /.pods
|
|
102
|
+
if (request.url === '/.pods' || ...) {
|
|
103
|
+
return; // Skip auth
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Anyone can create pods
|
|
107
|
+
fastify.post('/.pods', handleCreatePod);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Description:** The `/.pods` endpoint allows anyone to create new pods without authentication.
|
|
111
|
+
|
|
112
|
+
**Impact:**
|
|
113
|
+
- Resource exhaustion (DoS)
|
|
114
|
+
- Username/namespace squatting
|
|
115
|
+
- Disk space exhaustion
|
|
116
|
+
|
|
117
|
+
**CVSS Score:** 7.5 (High)
|
|
118
|
+
|
|
119
|
+
**Fix Required:** Require authentication or implement rate limiting and CAPTCHA.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### 4. Default Token Secret in Production (HIGH)
|
|
124
|
+
|
|
125
|
+
**Location:** `src/auth/token.js:15`
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
const SECRET = process.env.TOKEN_SECRET || 'dev-secret-change-in-production';
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Description:** If `TOKEN_SECRET` environment variable is not set, a hardcoded default secret is used.
|
|
132
|
+
|
|
133
|
+
**Impact:** Tokens can be forged by anyone who knows the default secret.
|
|
134
|
+
|
|
135
|
+
**CVSS Score:** 8.1 (High)
|
|
136
|
+
|
|
137
|
+
**Fix Required:** Fail to start if TOKEN_SECRET is not set in production, or generate a random secret on first run.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Medium Severity Vulnerabilities
|
|
142
|
+
|
|
143
|
+
### 5. No Rate Limiting on Authentication Endpoints (MEDIUM)
|
|
144
|
+
|
|
145
|
+
**Location:** `src/idp/interactions.js`, `src/idp/credentials.js`
|
|
146
|
+
|
|
147
|
+
**Description:** Login, registration, and credential endpoints have no rate limiting, allowing brute force attacks.
|
|
148
|
+
|
|
149
|
+
**Impact:** Account takeover through credential stuffing or brute force.
|
|
150
|
+
|
|
151
|
+
**CVSS Score:** 5.3 (Medium)
|
|
152
|
+
|
|
153
|
+
**Fix Required:** Implement rate limiting (e.g., 5 attempts per minute per IP).
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### 6. Information Disclosure via Error Messages (MEDIUM)
|
|
158
|
+
|
|
159
|
+
**Location:** Various handlers
|
|
160
|
+
|
|
161
|
+
**Description:** Error messages may reveal internal paths or stack traces.
|
|
162
|
+
|
|
163
|
+
**Impact:** Information leakage useful for further attacks.
|
|
164
|
+
|
|
165
|
+
**CVSS Score:** 4.3 (Medium)
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Recommendations
|
|
170
|
+
|
|
171
|
+
### Immediate Actions (Critical)
|
|
172
|
+
|
|
173
|
+
1. **Fix ACL bypass** - Require `acl:Control` permission to modify ACL files
|
|
174
|
+
2. **Verify JWT signatures** - Use `jose` library to verify against issuer JWKS
|
|
175
|
+
|
|
176
|
+
### Short-term Actions (High)
|
|
177
|
+
|
|
178
|
+
3. **Protect pod creation** - Add authentication or rate limiting
|
|
179
|
+
4. **Enforce TOKEN_SECRET** - Fail startup if not configured
|
|
180
|
+
|
|
181
|
+
### Medium-term Actions
|
|
182
|
+
|
|
183
|
+
5. **Add rate limiting** - Use `@fastify/rate-limit` plugin
|
|
184
|
+
6. **Sanitize error messages** - Remove internal details from user-facing errors
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Remediation Status
|
|
189
|
+
|
|
190
|
+
| Issue | Severity | Status | Fixed In |
|
|
191
|
+
|-------|----------|--------|----------|
|
|
192
|
+
| ACL bypass | Critical | 🟢 Fixed | v0.0.49 |
|
|
193
|
+
| JWT signature bypass | Critical | 🟢 Fixed | v0.0.49 |
|
|
194
|
+
| Unauthenticated pod creation | High | 🔴 Open | - |
|
|
195
|
+
| Default token secret | High | 🔴 Open | - |
|
|
196
|
+
| No rate limiting | Medium | 🔴 Open | - |
|
|
197
|
+
| Information disclosure | Medium | 🔴 Open | - |
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Changelog
|
|
202
|
+
|
|
203
|
+
### v0.0.49 (2026-01-03)
|
|
204
|
+
- **Fixed ACL bypass**: ACL files now require `acl:Control` permission on the protected resource
|
|
205
|
+
- **Fixed JWT signature bypass**: JWTs are now verified against the IdP's JWKS before accepting
|
|
206
|
+
|
|
207
|
+
*Report generated: 2026-01-03*
|
|
208
|
+
*Last updated: 2026-01-03*
|
package/package.json
CHANGED
package/src/auth/middleware.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { getWebIdFromRequestAsync } from './token.js';
|
|
8
8
|
import { checkAccess, getRequiredMode } from '../wac/checker.js';
|
|
9
|
+
import { AccessMode } from '../wac/parser.js';
|
|
9
10
|
import * as storage from '../storage/filesystem.js';
|
|
10
11
|
import { getEffectiveUrlPath } from '../utils/url.js';
|
|
11
12
|
|
|
@@ -21,15 +22,19 @@ export async function authorize(request, reply, options = {}) {
|
|
|
21
22
|
const urlPath = request.url.split('?')[0];
|
|
22
23
|
const method = request.method;
|
|
23
24
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
if (urlPath.endsWith('.acl') || method === 'OPTIONS') {
|
|
25
|
+
// OPTIONS is always allowed (CORS preflight)
|
|
26
|
+
if (method === 'OPTIONS') {
|
|
27
27
|
return { authorized: true, webId: null, wacAllow: 'user="read write append control", public="read write append"', authError: null };
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// Get WebID from token (supports both simple and Solid-OIDC tokens)
|
|
31
31
|
const { webId, error: authError } = await getWebIdFromRequestAsync(request);
|
|
32
32
|
|
|
33
|
+
// ACL files require special handling - check Control permission on protected resource
|
|
34
|
+
if (urlPath.endsWith('.acl')) {
|
|
35
|
+
return authorizeAclAccess(request, urlPath, method, webId, authError);
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
// Log auth failures for debugging
|
|
34
39
|
if (authError) {
|
|
35
40
|
request.log.warn({ authError, method, urlPath, hasAuth: !!request.headers.authorization }, 'Auth error');
|
|
@@ -114,3 +119,39 @@ export function handleUnauthorized(reply, isAuthenticated, wacAllow, authError =
|
|
|
114
119
|
});
|
|
115
120
|
}
|
|
116
121
|
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Authorize access to ACL files
|
|
125
|
+
* ACL files require acl:Control permission on the resource they protect
|
|
126
|
+
*
|
|
127
|
+
* @param {object} request - Fastify request
|
|
128
|
+
* @param {string} urlPath - URL path to the ACL file
|
|
129
|
+
* @param {string} method - HTTP method
|
|
130
|
+
* @param {string|null} webId - Authenticated user's WebID
|
|
131
|
+
* @param {string|null} authError - Authentication error if any
|
|
132
|
+
* @returns {Promise<{authorized: boolean, webId: string|null, wacAllow: string, authError: string|null}>}
|
|
133
|
+
*/
|
|
134
|
+
async function authorizeAclAccess(request, urlPath, method, webId, authError) {
|
|
135
|
+
// Determine the protected resource URL
|
|
136
|
+
// /foo/.acl protects /foo/ (container)
|
|
137
|
+
// /foo/bar.acl protects /foo/bar (resource)
|
|
138
|
+
const protectedPath = urlPath.replace(/\.acl$/, '');
|
|
139
|
+
const isProtectedContainer = protectedPath.endsWith('/');
|
|
140
|
+
const protectedUrl = `${request.protocol}://${request.hostname}${protectedPath}`;
|
|
141
|
+
|
|
142
|
+
// Get storage path for the protected resource
|
|
143
|
+
const storagePath = getEffectiveUrlPath(request).replace(/\.acl$/, '');
|
|
144
|
+
|
|
145
|
+
// All ACL operations require Control permission on the protected resource
|
|
146
|
+
// This is stricter than the Solid spec (which allows Read for reading ACLs)
|
|
147
|
+
// but simpler and more secure
|
|
148
|
+
const { allowed, wacAllow } = await checkAccess({
|
|
149
|
+
resourceUrl: protectedUrl,
|
|
150
|
+
resourcePath: storagePath,
|
|
151
|
+
isContainer: isProtectedContainer,
|
|
152
|
+
agentWebId: webId,
|
|
153
|
+
requiredMode: AccessMode.CONTROL
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return { authorized: allowed, webId, wacAllow, authError };
|
|
157
|
+
}
|
package/src/auth/token.js
CHANGED
|
@@ -37,7 +37,11 @@ export function createToken(webId, expiresIn = 3600) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Verify and decode a token (
|
|
40
|
+
* Verify and decode a simple token (2-part HMAC-signed)
|
|
41
|
+
*
|
|
42
|
+
* SECURITY: Only accepts 2-part simple tokens signed with HMAC.
|
|
43
|
+
* JWT tokens (3-part) require async verification via verifyTokenAsync().
|
|
44
|
+
*
|
|
41
45
|
* @param {string} token - The token to verify
|
|
42
46
|
* @returns {{webId: string, iat: number, exp: number} | null} Decoded payload or null
|
|
43
47
|
*/
|
|
@@ -48,9 +52,9 @@ export function verifyToken(token) {
|
|
|
48
52
|
|
|
49
53
|
const parts = token.split('.');
|
|
50
54
|
|
|
51
|
-
//
|
|
55
|
+
// JWT tokens (3 parts) require async verification - reject in sync function
|
|
52
56
|
if (parts.length === 3) {
|
|
53
|
-
return
|
|
57
|
+
return null;
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
if (parts.length !== 2) {
|
|
@@ -59,13 +63,19 @@ export function verifyToken(token) {
|
|
|
59
63
|
|
|
60
64
|
const [data, signature] = parts;
|
|
61
65
|
|
|
62
|
-
// Verify signature
|
|
66
|
+
// Verify HMAC signature
|
|
63
67
|
const expectedSig = crypto
|
|
64
68
|
.createHmac('sha256', SECRET)
|
|
65
69
|
.update(data)
|
|
66
70
|
.digest('base64url');
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
// Constant-time comparison to prevent timing attacks
|
|
73
|
+
try {
|
|
74
|
+
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSig))) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// If lengths don't match, timingSafeEqual throws
|
|
69
79
|
return null;
|
|
70
80
|
}
|
|
71
81
|
|
|
@@ -85,38 +95,45 @@ export function verifyToken(token) {
|
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
/**
|
|
88
|
-
* Verify a JWT token from credentials endpoint
|
|
89
|
-
*
|
|
98
|
+
* Verify a JWT token from the credentials endpoint
|
|
99
|
+
* Properly verifies signature against IdP's JWKS
|
|
100
|
+
*
|
|
90
101
|
* @param {string} token - JWT token
|
|
91
|
-
* @returns {{webId: string, iat: number, exp: number} | null}
|
|
102
|
+
* @returns {Promise<{webId: string, iat: number, exp: number} | null>}
|
|
92
103
|
*/
|
|
93
|
-
function
|
|
104
|
+
async function verifyJwtFromIdp(token) {
|
|
94
105
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Decode the payload (middle part)
|
|
101
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
|
106
|
+
// Dynamically import to avoid circular dependencies
|
|
107
|
+
const { getPublicJwks } = await import('../idp/keys.js');
|
|
108
|
+
const jose = await import('jose');
|
|
102
109
|
|
|
103
|
-
|
|
104
|
-
if (
|
|
110
|
+
const jwks = await getPublicJwks();
|
|
111
|
+
if (!jwks || !jwks.keys || jwks.keys.length === 0) {
|
|
105
112
|
return null;
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
return { webId: payload.webid, iat: payload.iat, exp: payload.exp };
|
|
111
|
-
}
|
|
115
|
+
// Create JWKS for verification
|
|
116
|
+
const keySet = jose.createLocalJWKSet(jwks);
|
|
112
117
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
// Verify the token
|
|
119
|
+
const { payload } = await jose.jwtVerify(token, keySet, {
|
|
120
|
+
// Allow some clock skew
|
|
121
|
+
clockTolerance: 60,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Extract webid claim
|
|
125
|
+
const webId = payload.webid || payload.webId || payload.sub;
|
|
126
|
+
if (!webId) {
|
|
127
|
+
return null;
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
return
|
|
119
|
-
|
|
130
|
+
return {
|
|
131
|
+
webId,
|
|
132
|
+
iat: payload.iat,
|
|
133
|
+
exp: payload.exp
|
|
134
|
+
};
|
|
135
|
+
} catch (err) {
|
|
136
|
+
// Verification failed - invalid signature, expired, etc.
|
|
120
137
|
return null;
|
|
121
138
|
}
|
|
122
139
|
}
|
|
@@ -190,16 +207,27 @@ export async function getWebIdFromRequestAsync(request) {
|
|
|
190
207
|
return verifyNostrAuth(request);
|
|
191
208
|
}
|
|
192
209
|
|
|
193
|
-
// Fall back to
|
|
210
|
+
// Fall back to Bearer tokens
|
|
194
211
|
const token = extractToken(authHeader);
|
|
195
212
|
if (!token) {
|
|
196
213
|
return { webId: null, error: null };
|
|
197
214
|
}
|
|
198
215
|
|
|
216
|
+
// Try simple 2-part token first
|
|
199
217
|
const payload = verifyToken(token);
|
|
200
218
|
if (payload?.webId) {
|
|
201
219
|
return { webId: payload.webId, error: null };
|
|
202
220
|
}
|
|
203
221
|
|
|
222
|
+
// If 3-part JWT, verify against IdP's JWKS
|
|
223
|
+
const parts = token.split('.');
|
|
224
|
+
if (parts.length === 3) {
|
|
225
|
+
const jwtPayload = await verifyJwtFromIdp(token);
|
|
226
|
+
if (jwtPayload?.webId) {
|
|
227
|
+
return { webId: jwtPayload.webId, error: null };
|
|
228
|
+
}
|
|
229
|
+
return { webId: null, error: 'Invalid or unverifiable JWT token' };
|
|
230
|
+
}
|
|
231
|
+
|
|
204
232
|
return { webId: null, error: 'Invalid token' };
|
|
205
233
|
}
|
package/test/wac.test.js
CHANGED
|
@@ -226,15 +226,22 @@ describe('WAC Integration', () => {
|
|
|
226
226
|
|
|
227
227
|
describe('ACL Files', () => {
|
|
228
228
|
it('should create root .acl on pod creation', async () => {
|
|
229
|
-
|
|
229
|
+
// ACL files require Control permission - must be authenticated as pod owner
|
|
230
|
+
const res = await request('/wactest/.acl', { auth: 'wactest' });
|
|
230
231
|
|
|
231
232
|
assertStatus(res, 200);
|
|
232
233
|
const content = await res.json();
|
|
233
234
|
assert.ok(content['@graph'], 'Should be JSON-LD');
|
|
234
235
|
});
|
|
235
236
|
|
|
237
|
+
it('should deny unauthenticated access to .acl files', async () => {
|
|
238
|
+
// Security: ACL files must require authentication
|
|
239
|
+
const res = await request('/wactest/.acl');
|
|
240
|
+
assertStatus(res, 401);
|
|
241
|
+
});
|
|
242
|
+
|
|
236
243
|
it('should create private folder .acl', async () => {
|
|
237
|
-
const res = await request('/wactest/private/.acl');
|
|
244
|
+
const res = await request('/wactest/private/.acl', { auth: 'wactest' });
|
|
238
245
|
|
|
239
246
|
assertStatus(res, 200);
|
|
240
247
|
const content = await res.json();
|
|
@@ -248,7 +255,7 @@ describe('WAC Integration', () => {
|
|
|
248
255
|
});
|
|
249
256
|
|
|
250
257
|
it('should create inbox .acl with public append', async () => {
|
|
251
|
-
const res = await request('/wactest/inbox/.acl');
|
|
258
|
+
const res = await request('/wactest/inbox/.acl', { auth: 'wactest' });
|
|
252
259
|
|
|
253
260
|
assertStatus(res, 200);
|
|
254
261
|
const content = await res.json();
|