javascript-solid-server 0.0.135 → 0.0.137
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 +8 -1
- package/README.md +6 -0
- package/docs/payments.md +35 -13
- package/jss-architecture.svg +182 -0
- package/jsserve/LICENSE +21 -0
- package/jsserve/README.md +194 -0
- package/jsserve/bin/jsserve.js +329 -0
- package/jsserve/package-lock.json +1832 -0
- package/jsserve/package.json +45 -0
- package/package.json +1 -1
- package/src/notifications/websocket.js +11 -4
- package/src/server.js +4 -0
- package/src/utils/url.js +31 -8
- package/test/url.test.js +75 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "servejss",
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "Static file server with REST write support - npx serve alternative",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"serve",
|
|
7
|
+
"static",
|
|
8
|
+
"server",
|
|
9
|
+
"http",
|
|
10
|
+
"rest",
|
|
11
|
+
"webdav",
|
|
12
|
+
"file-server",
|
|
13
|
+
"development",
|
|
14
|
+
"solid"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"bin": {
|
|
18
|
+
"servejss": "./bin/jsserve.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test test/*.test.js"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"chalk": "^5.3.0",
|
|
29
|
+
"commander": "^12.1.0",
|
|
30
|
+
"javascript-solid-server": ">=0.0.136"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/JavaScriptSolidServer/jsserve.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/JavaScriptSolidServer/jsserve#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/JavaScriptSolidServer/jsserve/issues"
|
|
39
|
+
},
|
|
40
|
+
"author": "JavaScriptSolidServer Contributors",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -204,11 +204,18 @@ export function broadcast(url) {
|
|
|
204
204
|
// Notify direct subscribers
|
|
205
205
|
notifySubscribers(url);
|
|
206
206
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
// Walk up all ancestor containers so subscribing to a root
|
|
208
|
+
// catches changes in nested paths (e.g. /db/mydata/ catches /db/mydata/issues/1)
|
|
209
|
+
// Stop at the origin root to avoid climbing past the hostname
|
|
210
|
+
let originRoot;
|
|
211
|
+
try { originRoot = new URL(url).origin + '/'; } catch (e) { return; }
|
|
212
|
+
|
|
213
|
+
let currentUrl = url;
|
|
214
|
+
let containerUrl = getParentContainer(currentUrl);
|
|
215
|
+
while (containerUrl && containerUrl !== currentUrl && containerUrl.length >= originRoot.length) {
|
|
211
216
|
notifySubscribers(containerUrl);
|
|
217
|
+
currentUrl = containerUrl;
|
|
218
|
+
containerUrl = getParentContainer(currentUrl);
|
|
212
219
|
}
|
|
213
220
|
}
|
|
214
221
|
|
package/src/server.js
CHANGED
|
@@ -182,6 +182,8 @@ export function createServer(options = {}) {
|
|
|
182
182
|
fastify.decorateRequest('defaultQuota', null);
|
|
183
183
|
fastify.decorateRequest('config', null);
|
|
184
184
|
fastify.decorateRequest('liveReloadEnabled', null);
|
|
185
|
+
fastify.decorateRequest('singleUser', null);
|
|
186
|
+
fastify.decorateRequest('singleUserName', null);
|
|
185
187
|
fastify.addHook('onRequest', async (request) => {
|
|
186
188
|
request.connegEnabled = connegEnabled;
|
|
187
189
|
request.notificationsEnabled = notificationsEnabled || liveReloadEnabled;
|
|
@@ -195,6 +197,8 @@ export function createServer(options = {}) {
|
|
|
195
197
|
request.defaultQuota = defaultQuota;
|
|
196
198
|
request.config = { public: options.public, readOnly: options.readOnly };
|
|
197
199
|
request.liveReloadEnabled = liveReloadEnabled;
|
|
200
|
+
request.singleUser = singleUser;
|
|
201
|
+
request.singleUserName = singleUserName;
|
|
198
202
|
|
|
199
203
|
// Extract pod name from subdomain if enabled
|
|
200
204
|
if (subdomainsEnabled && baseDomain) {
|
package/src/utils/url.js
CHANGED
|
@@ -150,22 +150,45 @@ export function getResourceName(urlPath) {
|
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
152
|
* Extract pod name from URL path or request
|
|
153
|
+
*
|
|
154
|
+
* Resolves to one of four shapes, by deployment mode:
|
|
155
|
+
*
|
|
156
|
+
* - Subdomain mode with a recognized subdomain → `request.podName` (from hostname).
|
|
157
|
+
* - Subdomain mode with no recognized subdomain → `null` (base-domain access;
|
|
158
|
+
* callers guard with `if (podName)` and skip pod-scoped side effects).
|
|
159
|
+
* - Single-user, root-pod (`singleUserName` empty or '/') → `'.'` so
|
|
160
|
+
* `path.join(dataRoot, '.', QUOTA_FILE)` collapses to `<dataRoot>/QUOTA_FILE`.
|
|
161
|
+
* - Single-user, named pod → `singleUserName` (all requests share the one pod,
|
|
162
|
+
* independent of URL — avoids mistaking a URL segment like `index.html`
|
|
163
|
+
* for a pod name).
|
|
164
|
+
* - Path-based multi-pod (default, no flags) → first URL segment, or `null`
|
|
165
|
+
* for requests at `/` that aren't inside any pod.
|
|
166
|
+
*
|
|
167
|
+
* Background: before this function knew about single-user mode, a
|
|
168
|
+
* `PUT /index.html` on a single-user root-pod deployment produced a pod name
|
|
169
|
+
* of `"index.html"`, and the quota sidecar landed at
|
|
170
|
+
* `<dataRoot>/index.html/.quota.json` → `ENOTDIR` (index.html is a file).
|
|
171
|
+
*
|
|
153
172
|
* @param {string|object} pathOrRequest - URL path string or Fastify request object
|
|
154
|
-
* @returns {string|null} - Pod name or null
|
|
173
|
+
* @returns {string|null} - Pod name, `'.'` for root-pod, or `null` when no pod applies
|
|
155
174
|
*/
|
|
156
175
|
export function getPodName(pathOrRequest) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return pathOrRequest.podName;
|
|
176
|
+
if (typeof pathOrRequest === 'object' && pathOrRequest !== null) {
|
|
177
|
+
// Subdomain mode: hostname drives it. Unrecognized host → no pod.
|
|
178
|
+
if (pathOrRequest.subdomainsEnabled) {
|
|
179
|
+
return pathOrRequest.podName || null;
|
|
162
180
|
}
|
|
163
|
-
//
|
|
181
|
+
// Single-user mode: always the one pod, regardless of URL path.
|
|
182
|
+
if (pathOrRequest.singleUser) {
|
|
183
|
+
const name = pathOrRequest.singleUserName;
|
|
184
|
+
return (!name || name === '/') ? '.' : name;
|
|
185
|
+
}
|
|
186
|
+
// Path-based multi-pod: first URL segment.
|
|
164
187
|
const urlPath = pathOrRequest.url?.split('?')[0] || '';
|
|
165
188
|
return getPodNameFromPath(urlPath);
|
|
166
189
|
}
|
|
167
190
|
|
|
168
|
-
//
|
|
191
|
+
// String form: path-based pod extraction.
|
|
169
192
|
return getPodNameFromPath(pathOrRequest);
|
|
170
193
|
}
|
|
171
194
|
|
package/test/url.test.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for src/utils/url.js
|
|
3
|
+
*
|
|
4
|
+
* Focus: getPodName() resolution across the four supported deployment modes.
|
|
5
|
+
* Regression guard for #278 (single-user root-pod PUT → ENOTDIR).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it } from 'node:test';
|
|
9
|
+
import assert from 'node:assert';
|
|
10
|
+
import { getPodName } from '../src/utils/url.js';
|
|
11
|
+
|
|
12
|
+
describe('getPodName', () => {
|
|
13
|
+
describe('subdomain mode', () => {
|
|
14
|
+
it('returns request.podName when the subdomain is recognized', () => {
|
|
15
|
+
const req = { subdomainsEnabled: true, podName: 'alice', url: '/profile/card' };
|
|
16
|
+
assert.strictEqual(getPodName(req), 'alice');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns null on base-domain access (no recognized subdomain)', () => {
|
|
20
|
+
const req = { subdomainsEnabled: true, podName: null, url: '/anything' };
|
|
21
|
+
assert.strictEqual(getPodName(req), null);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('single-user mode', () => {
|
|
26
|
+
it("returns '.' for a root pod (singleUserName empty)", () => {
|
|
27
|
+
const req = { singleUser: true, singleUserName: '', url: '/index.html' };
|
|
28
|
+
assert.strictEqual(getPodName(req), '.');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns '.' for a root pod (singleUserName '/')", () => {
|
|
32
|
+
const req = { singleUser: true, singleUserName: '/', url: '/index.html' };
|
|
33
|
+
assert.strictEqual(getPodName(req), '.');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns singleUserName for a named pod, regardless of URL', () => {
|
|
37
|
+
const req = { singleUser: true, singleUserName: 'me', url: '/index.html' };
|
|
38
|
+
assert.strictEqual(getPodName(req), 'me');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('does not mistake a URL segment for a pod in single-user mode', () => {
|
|
42
|
+
// Regression for #278: PUT /index.html previously produced pod
|
|
43
|
+
// "index.html", making the quota sidecar path <dataRoot>/index.html/.quota.json.
|
|
44
|
+
const req = { singleUser: true, singleUserName: '', url: '/index.html' };
|
|
45
|
+
assert.notStrictEqual(getPodName(req), 'index.html');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('path-based multi-pod (default)', () => {
|
|
50
|
+
it('returns the first URL segment as the pod name', () => {
|
|
51
|
+
const req = { url: '/alice/profile/card' };
|
|
52
|
+
assert.strictEqual(getPodName(req), 'alice');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns null for requests at /', () => {
|
|
56
|
+
const req = { url: '/' };
|
|
57
|
+
assert.strictEqual(getPodName(req), null);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('skips system paths beginning with a dot', () => {
|
|
61
|
+
const req = { url: '/.well-known/openid-configuration' };
|
|
62
|
+
assert.strictEqual(getPodName(req), null);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('string-form input', () => {
|
|
67
|
+
it('extracts pod name from a URL path string', () => {
|
|
68
|
+
assert.strictEqual(getPodName('/alice/foo'), 'alice');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns null for the root path', () => {
|
|
72
|
+
assert.strictEqual(getPodName('/'), null);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|