fedipod-server 0.28.1 → 0.29.0
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 +27 -28
- package/config/server.json +22 -0
- package/dist/claims.js +3 -2
- package/dist/directory.d.ts +2 -0
- package/dist/handler.d.ts +4 -1
- package/dist/handler.js +32 -3
- package/dist/handler.jsonld +4 -0
- package/dist/store-pod.js +37 -0
- package/lib/client/c2s.mjs +97 -43
- package/lib/client/masto/accounts.mjs +1 -0
- package/lib/core/deliver.mjs +17 -1
- package/lib/core/intake/activities.mjs +18 -2
- package/lib/core/intake/group.mjs +3 -1
- package/lib/core/intake/index.mjs +33 -2
- package/lib/core/intake/notes.mjs +12 -5
- package/lib/core/place.mjs +82 -0
- package/lib/core/publisher/collections.mjs +37 -2
- package/lib/core/publisher/index.mjs +23 -1
- package/lib/core/publisher/notes.mjs +73 -7
- package/lib/core/publisher/own.mjs +143 -0
- package/lib/core/publisher/questions.mjs +5 -3
- package/lib/core/social.mjs +62 -32
- package/lib/core/store.mjs +11 -1
- package/lib/core/wire.mjs +51 -15
- package/lib/device/admin/routes/lifecycle.mjs +1 -1
- package/lib/device/admin/routes/setup.mjs +17 -1
- package/lib/device/cli/commands/setup.mjs +50 -8
- package/lib/device/cli/context.mjs +1 -1
- package/lib/device/migrate.mjs +1 -1
- package/lib/device/setup.mjs +45 -8
- package/lib/gateway/front-core.mjs +54 -58
- package/lib/gateway/gateway-core.mjs +87 -8
- package/lib/gateway/relay-extras.mjs +89 -0
- package/lib/gateway/token-claims.mjs +16 -0
- package/lib/pod/location.mjs +52 -0
- package/lib/pod/notes.mjs +2 -4
- package/lib/pod/transport.mjs +171 -22
- package/lib/pod/type-index.mjs +101 -0
- package/lib/pod/urls.mjs +6 -0
- package/lib/server/embed.mjs +7 -6
- package/lib/session/fedi-account.mjs +19 -10
- package/lib/session/package.json +1 -1
- package/package.json +1 -1
- package/run-agent.mjs +2 -0
- package/web/admin/actors.js +2 -0
- package/web/admin/index.html +9 -0
- package/web/admin/record.js +4 -1
- package/web/admin/setup/index.html +17 -1
- package/web/admin/setup/setup.js +18 -5
- package/web/app/README.md +1 -1
- package/web/app/admin-facade.mjs +1 -1
- package/web/app/agent.mjs +11 -2
- package/web/app/boot.mjs +72 -32
- package/web/app/dist/boot.js +430 -86
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +3294 -2648
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/index.html +16 -0
- package/web/app/signup.mjs +65 -26
- package/web/front/run.html +7 -1
- package/web/front/run.js +30 -4
package/README.md
CHANGED
|
@@ -30,13 +30,12 @@ before installing: a server that does not meet them cannot offer accounts, and
|
|
|
30
30
|
sign-up refuses rather than half-working.
|
|
31
31
|
|
|
32
32
|
- **Community Solid Server 7.**
|
|
33
|
-
- **
|
|
34
|
-
address (`@mei@mei.example.org`); a
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
server also runs.
|
|
33
|
+
- **Subdomained or suffixed pods.** A subdomained pod answers on its own
|
|
34
|
+
address (`@mei@mei.example.org`); a suffixed pod answers under its path, and
|
|
35
|
+
the server fronts it so its address is the server's host
|
|
36
|
+
(`@aisha@server.example`, for a pod at `https://server.example/aisha/`).
|
|
37
|
+
Either works — a suffixed pod cannot resolve its own handle, so it is
|
|
38
|
+
followable only through the server's apex, which the server also runs.
|
|
40
39
|
- **A single worker.** Run the server as one copy of itself — `--workers 1`,
|
|
41
40
|
which is the default. With more, it still serves pods normally, but no pod
|
|
42
41
|
can be an account: sign-up is refused, and an account set up earlier goes on
|
|
@@ -70,8 +69,8 @@ Add the package context to your CSS config and import the shipped snippet:
|
|
|
70
69
|
|
|
71
70
|
Then set what you need on the `urn:fedipod:server:Handler` node and restart
|
|
72
71
|
the server.
|
|
73
|
-
A
|
|
74
|
-
|
|
72
|
+
A CSS config with subdomained pods gives each identity an origin of its own;
|
|
73
|
+
one with suffixed pods hosts each under its own path. Both opt in.
|
|
75
74
|
The snippet places the component in the routing waterfall ahead of
|
|
76
75
|
the LDP catch-all, on the initializer and finalizer lists so identities start
|
|
77
76
|
and stop with the server, and on the websocket handler list for the live feed.
|
|
@@ -88,9 +87,9 @@ and stop with the server, and on the websocket handler list for the live feed.
|
|
|
88
87
|
```
|
|
89
88
|
|
|
90
89
|
An identity is provisioned when its owner opts in: its name is the pod's
|
|
91
|
-
subdomain label, or its last path segment on a
|
|
90
|
+
subdomain label, or its last path segment on a suffixed pod, and it publishes an
|
|
92
91
|
actor and a signing key on the pod itself. Its handle resolves on the pod for a
|
|
93
|
-
|
|
92
|
+
subdomained pod, and through the server's apex for a suffixed pod. Everything it is made of lives on its pod — its state, its private
|
|
94
93
|
signing key, the secret guarding its own pages, and the credentials for any
|
|
95
94
|
accounts its owner connects on other servers. Each identity has its own
|
|
96
95
|
secret; one owner's opens nobody else's door. The opt-in reply is where the
|
|
@@ -108,7 +107,7 @@ naming the pod the identity runs on. Nothing private is in it.
|
|
|
108
107
|
|---|---|
|
|
109
108
|
| `agentRuntimeOptIn` | Whether pod owners can sign up. With it off, nothing runs. |
|
|
110
109
|
| `agentDataDir` | Where each identity keeps the file naming its pod; its log lines go to the server's own log. Required whenever sign-up is on. |
|
|
111
|
-
| `agentUiPath` | Where the owner's pages live on the pod's origin. `/fp/` by default; empty serves no pages. On a
|
|
110
|
+
| `agentUiPath` | Where the owner's pages live on the pod's origin. `/fp/` by default; empty serves no pages. On a suffixed pod it is under the pod's path, e.g. `/aisha/fp/`. |
|
|
112
111
|
| `agentRegistryContainer` | The internal container holding the sign-up rows. |
|
|
113
112
|
| `runPage` | The HTML of the page where a pod owner opts in or out. The package's own `web/front/run.html` is served unless you set this. |
|
|
114
113
|
| `runPath` | Where that page answers. `/.fediverse-account` unless you set it. The path it takes is one the pod no longer serves, so it is yours to name; the dot keeps it out of the way of anybody's data. |
|
|
@@ -155,11 +154,11 @@ that document's address and registers nothing here. Once in, it posts to the
|
|
|
155
154
|
outbox the actor names, and reads what the identity received at `/ap/inbox`,
|
|
156
155
|
which nobody but the owner may read.
|
|
157
156
|
|
|
158
|
-
A
|
|
159
|
-
pod
|
|
160
|
-
`/aisha/
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
A subdomained pod gets an origin of its own and answers at its root; a
|
|
158
|
+
suffixed pod answers under its own path (`/aisha/ap/actor`, `/aisha/api/…`,
|
|
159
|
+
`/aisha/oauth/…`, its door at `/aisha/fp/`). So a host carries at most one
|
|
160
|
+
root identity, but any number of suffixed pods beside it; a second root
|
|
161
|
+
identity on a host, or a suffixed pod nesting under another, is refused at
|
|
163
162
|
opt-in rather than half-working.
|
|
164
163
|
|
|
165
164
|
### Before you turn it on
|
|
@@ -173,22 +172,22 @@ for anyone hosting other people, it is a promise being made to them.
|
|
|
173
172
|
owner's door, with that identity's own door secret:
|
|
174
173
|
|
|
175
174
|
```
|
|
176
|
-
#
|
|
175
|
+
# subdomained pod:
|
|
177
176
|
curl -X POST https://mei.example.org/fp/config \
|
|
178
177
|
-H 'x-dk-token: THE_DOOR_SECRET_FROM_THE_OPT_IN_REPLY' -H 'content-type: application/json' \
|
|
179
178
|
-d '{"password":"the one you will type into your phone"}'
|
|
180
|
-
#
|
|
179
|
+
# suffixed pod — the door is under the pod's path:
|
|
181
180
|
curl -X POST https://server.example/aisha/fp/config \
|
|
182
181
|
-H 'x-dk-token: THE_DOOR_SECRET_FROM_THE_OPT_IN_REPLY' -H 'content-type: application/json' \
|
|
183
182
|
-d '{"password":"the one you will type into your phone"}'
|
|
184
183
|
```
|
|
185
184
|
|
|
186
|
-
**Some pod paths stop being served.** On a
|
|
185
|
+
**Some pod paths stop being served.** On a subdomained pod the paths above
|
|
187
186
|
belong to the identity, so pod resources at those names — a container called
|
|
188
187
|
`api`, `oauth` or `fp`, or documents at `ap/actor`, `ap/outbox`,
|
|
189
188
|
`ap/inbox`, `.well-known/oauth-authorization-server`,
|
|
190
189
|
`.well-known/nodeinfo` and `nodeinfo/2.0` — are not served over HTTP there.
|
|
191
|
-
On a
|
|
190
|
+
On a suffixed pod the same names are taken under the pod's own path
|
|
192
191
|
(`/aisha/api/…`, `/aisha/ap/actor`, `/aisha/fp/`, and so on); the rest of the
|
|
193
192
|
path, and every other pod on the host, is untouched.
|
|
194
193
|
They stay in the pod and in its listings. Every other path is the pod, exactly
|
|
@@ -245,8 +244,8 @@ TypeScript, built the way CSS builds its own components.
|
|
|
245
244
|
npm install
|
|
246
245
|
npm run build # tsc → dist, then componentsjs-generator → dist/components
|
|
247
246
|
npm test # unit tests, plus the transport against a real CSS store
|
|
248
|
-
npm run test:e2e # boots a real server (
|
|
249
|
-
npm run test:e2e:suffix # the same, for pods
|
|
247
|
+
npm run test:e2e # boots a real server (subdomained pods) and uses it as a client would
|
|
248
|
+
npm run test:e2e:suffix # the same, for suffixed pods (mode d)
|
|
250
249
|
```
|
|
251
250
|
|
|
252
251
|
`npm test` drives a genuine CSS store stack — ETags, conditional writes,
|
|
@@ -259,11 +258,11 @@ lease protocol and the deletion deny-list still hold across it.
|
|
|
259
258
|
become identities up front; the third opts in and out at runtime), then signs in as a phone app does, posts,
|
|
260
259
|
receives a follow from another server, and watches the live feed. It takes
|
|
261
260
|
about a minute. `FEDIPOD_E2E_LOG=info` shows the server's log while it runs.
|
|
262
|
-
`npm run test:e2e:suffix` does the same on a server whose pods
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
261
|
+
`npm run test:e2e:suffix` does the same on a server whose pods are suffixed
|
|
262
|
+
(`@aisha@server`): it proves the apex WebFinger resolves straight to the pod
|
|
263
|
+
actor, that the actor, inbox, client API, OAuth and door all answer under the
|
|
264
|
+
pod's path, that two suffixed pods on one origin stay separate, and that a
|
|
265
|
+
signed delivery is verified at the pod's own door.
|
|
267
266
|
|
|
268
267
|
`dist/` and `node_modules/` are gitignored; `src/`, `config/` and this file are
|
|
269
268
|
the sources.
|
package/config/server.json
CHANGED
|
@@ -60,6 +60,28 @@
|
|
|
60
60
|
}
|
|
61
61
|
]
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
"comment": "ActivityStreams documents are stored as application/activity+json. The spec asks a server to answer application/ld+json with the ActivityStreams profile for them too, and CSS alone refuses that form with 501. This relabels the one type as the other, both ways, without touching the bytes, so either Accept reads the same document.",
|
|
65
|
+
"@id": "urn:fedipod:server:ActivityStreamsReplacer",
|
|
66
|
+
"@type": "ContentTypeReplacer",
|
|
67
|
+
"replacements": [
|
|
68
|
+
{ "ContentTypeReplacer:_replacements_key": "application/activity+json", "ContentTypeReplacer:_replacements_value": "application/ld+json" },
|
|
69
|
+
{ "ContentTypeReplacer:_replacements_key": "application/ld+json", "ContentTypeReplacer:_replacements_value": "application/activity+json" }
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"comment": "The replacer goes first in the converter chain, before CSS's own ContentTypeReplacer.",
|
|
74
|
+
"@type": "Override",
|
|
75
|
+
"overrideInstance": { "@id": "urn:solid-server:default:ChainedConverter" },
|
|
76
|
+
"overrideSteps": [
|
|
77
|
+
{
|
|
78
|
+
"@type": "OverrideListInsertBefore",
|
|
79
|
+
"overrideParameter": { "@id": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^7.0.0/dist/storage/conversion/ChainedConverter.jsonld#ChainedConverter_converters" },
|
|
80
|
+
"overrideTarget": { "@id": "urn:solid-server:default:ContentTypeReplacer" },
|
|
81
|
+
"overrideValue": { "@id": "urn:fedipod:server:ActivityStreamsReplacer" }
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
63
85
|
{
|
|
64
86
|
"comment": "Insert the handler into the routing waterfall just before the LDP catch-all, so it claims only the front routes and everything else stays normal CSS. OverrideListInsertBefore is available in componentsjs 5.x (which CSS 7 uses).",
|
|
65
87
|
"@type": "Override",
|
package/dist/claims.js
CHANGED
|
@@ -51,11 +51,12 @@ const AGENT_PATHS = new Set([
|
|
|
51
51
|
]);
|
|
52
52
|
const AGENT_PREFIXES = ['/api/', '/oauth/', '/@']; // /@handle: the short profile address
|
|
53
53
|
// An identity's inbox is the container `/<root>/ap/inbox/`, matched by shape
|
|
54
|
-
// rather than a fixed root
|
|
54
|
+
// rather than a fixed root — and the root may be nested, wherever its owner put
|
|
55
|
+
// the `fedipod/` container. A POST here is a delivery, verified at the door
|
|
55
56
|
// before it is written; the handler checks the exact path per identity. The
|
|
56
57
|
// path is relative to the identity's mount (see agentClaims), so a suffix pod's
|
|
57
58
|
// `/aisha/fedipod/ap/inbox/` arrives here already stripped to `/fedipod/ap/inbox/`.
|
|
58
|
-
const isInboxPath = (pathname) => /^\/[^/]+\/ap\/inbox\/$/u.test(pathname);
|
|
59
|
+
const isInboxPath = (pathname) => /^\/(?:[^/]+\/)+ap\/inbox\/$/u.test(pathname);
|
|
59
60
|
exports.isInboxPath = isInboxPath;
|
|
60
61
|
/**
|
|
61
62
|
* True when this path belongs to an identity's client surface.
|
package/dist/directory.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface AgentRegistryRecord {
|
|
|
38
38
|
host: string;
|
|
39
39
|
webId: string;
|
|
40
40
|
optedInAt: string;
|
|
41
|
+
/** Where on the pod the account lives, relative to it; absent on older rows, which are at fedipod/. */
|
|
42
|
+
root?: string;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* The registry key for an opted-in pod: its host plus its path. Several pods
|
package/dist/handler.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ export declare class FediPodServerHandler extends HttpHandler implements Initial
|
|
|
70
70
|
private readonly logger;
|
|
71
71
|
private readonly claimed;
|
|
72
72
|
private readonly agentHandles;
|
|
73
|
+
private readonly roots;
|
|
73
74
|
private readonly frontHost;
|
|
74
75
|
private readonly uiPath;
|
|
75
76
|
private readonly identities;
|
|
@@ -164,9 +165,11 @@ export declare class FediPodServerHandler extends HttpHandler implements Initial
|
|
|
164
165
|
get(name: string): string | null;
|
|
165
166
|
};
|
|
166
167
|
}): Promise<string[]>;
|
|
167
|
-
optInPod({ podBase, webId }: {
|
|
168
|
+
optInPod({ podBase, webId, container, createIndex }: {
|
|
168
169
|
podBase: string;
|
|
169
170
|
webId: string;
|
|
171
|
+
container?: string;
|
|
172
|
+
createIndex?: boolean;
|
|
170
173
|
}): Promise<Record<string, unknown> & {
|
|
171
174
|
httpStatus: number;
|
|
172
175
|
}>;
|
package/dist/handler.js
CHANGED
|
@@ -36,6 +36,8 @@ function pathContains(a, b) {
|
|
|
36
36
|
const LIB_ROOT = (0, node_fs_1.existsSync)((0, node_path_1.join)(__dirname, '../lib/server/embed.mjs')) ? '../lib' : '../../../lib';
|
|
37
37
|
const FRONT_CORE = `${LIB_ROOT}/gateway/front-core.mjs`;
|
|
38
38
|
const EMBED = `${LIB_ROOT}/server/embed.mjs`;
|
|
39
|
+
const PLACE = `${LIB_ROOT}/core/place.mjs`;
|
|
40
|
+
const TRANSPORT = `${LIB_ROOT}/pod/transport.mjs`;
|
|
39
41
|
const esmImport = new Function('s', 'return import(s)');
|
|
40
42
|
// The front's pages and the files they load, carried in the same two layouts
|
|
41
43
|
// as lib/. A missing file is not fatal: the route it feeds answers 404.
|
|
@@ -86,6 +88,7 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
86
88
|
// request is matched to an identity by host AND mount, never host alone.
|
|
87
89
|
claimed = new Map(); // pod base → claim
|
|
88
90
|
agentHandles = new Map(); // handle → pod base
|
|
91
|
+
roots = new Map(); // pod base → where on it the account lives
|
|
89
92
|
frontHost;
|
|
90
93
|
uiPath;
|
|
91
94
|
identities = new Map();
|
|
@@ -165,7 +168,7 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
165
168
|
if (c.host !== host)
|
|
166
169
|
continue;
|
|
167
170
|
if (pathContains(c.mount, mount) || pathContains(mount, c.mount)) {
|
|
168
|
-
throw new Error(`${podBase} nests with the identity already at ${c.podBase} — a
|
|
171
|
+
throw new Error(`${podBase} nests with the identity already at ${c.podBase} — a suffixed pod owns only its own subtree`);
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
174
|
}
|
|
@@ -254,6 +257,7 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
254
257
|
}
|
|
255
258
|
this.claimed.set(row.podBase, claim);
|
|
256
259
|
this.agentHandles.set(row.handle, row.podBase);
|
|
260
|
+
this.roots.set(row.podBase, row.root || 'fedipod/');
|
|
257
261
|
pods.push(row.podBase);
|
|
258
262
|
}
|
|
259
263
|
}
|
|
@@ -298,6 +302,7 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
298
302
|
const { ensureDoorSecret } = await esmImport(EMBED);
|
|
299
303
|
return ensureDoorSecret(session ?? (0, store_pod_1.makeStoreSession)(this.args.resourceStore, podBase), podBase, {
|
|
300
304
|
...opts,
|
|
305
|
+
root: this.roots.get(podBase) ?? 'fedipod/',
|
|
301
306
|
dataDir: this.args.agentDataDir,
|
|
302
307
|
handle: deriveHandle(podBase),
|
|
303
308
|
log: (message) => { this.logger.info(`@${deriveHandle(podBase)}: ${message}`); },
|
|
@@ -322,6 +327,7 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
322
327
|
}
|
|
323
328
|
const identity = await startEmbeddedAgent({
|
|
324
329
|
podBase,
|
|
330
|
+
root: this.roots.get(podBase) ?? 'fedipod/',
|
|
325
331
|
dataDir: this.args.agentDataDir,
|
|
326
332
|
session,
|
|
327
333
|
resourceStore: this.args.resourceStore,
|
|
@@ -474,7 +480,7 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
474
480
|
return [];
|
|
475
481
|
}
|
|
476
482
|
}
|
|
477
|
-
async optInPod({ podBase, webId }) {
|
|
483
|
+
async optInPod({ podBase, webId, container = '', createIndex = false }) {
|
|
478
484
|
if (!this.registry)
|
|
479
485
|
return { httpStatus: 501, error: 'this server does not offer runtime opt-in' };
|
|
480
486
|
if (this.args.clusterManager && !this.args.clusterManager.isSingleThreaded()) {
|
|
@@ -498,8 +504,31 @@ class FediPodServerHandler extends community_server_1.HttpHandler {
|
|
|
498
504
|
return { httpStatus: 409, error: e.message };
|
|
499
505
|
}
|
|
500
506
|
const { host } = claim;
|
|
507
|
+
// Where on the pod: the container its owner named holds `fedipod/`. The
|
|
508
|
+
// place is recorded in their public type index — made only on their yes —
|
|
509
|
+
// before anything else is written, and read here with the server's own
|
|
510
|
+
// access to the pod.
|
|
511
|
+
const place = await esmImport(PLACE);
|
|
512
|
+
const chosen = place.chosenRoot(base, container);
|
|
513
|
+
if (chosen.problem || !chosen.root)
|
|
514
|
+
return { httpStatus: 400, error: `Where to store it: ${chosen.problem}.` };
|
|
515
|
+
const root = chosen.root;
|
|
516
|
+
const { PodTransport } = await esmImport(TRANSPORT);
|
|
517
|
+
const pod = new PodTransport((0, store_pod_1.makeStoreSession)(this.args.resourceStore, base), { webId });
|
|
518
|
+
if (!createIndex && !await place.hasPublicIndex(pod, base)) {
|
|
519
|
+
return { httpStatus: 409, code: 'needs-index',
|
|
520
|
+
error: 'Your pod has no public type index, the list Solid apps use to find your things. '
|
|
521
|
+
+ 'FediPod needs one to record where your account lives.' };
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
await place.recordPlace(pod, base, `${base}${root}ap/actor`, { create: createIndex });
|
|
525
|
+
}
|
|
526
|
+
catch (e) {
|
|
527
|
+
return { httpStatus: 500, error: `could not record where the account lives in your type index (${e.message})` };
|
|
528
|
+
}
|
|
529
|
+
this.roots.set(base, root);
|
|
501
530
|
try {
|
|
502
|
-
await this.registry.add({ podBase: base, handle, host, webId, optedInAt: new Date().toISOString() });
|
|
531
|
+
await this.registry.add({ podBase: base, handle, host, webId, root, optedInAt: new Date().toISOString() });
|
|
503
532
|
}
|
|
504
533
|
catch (e) {
|
|
505
534
|
this.logger.error(`opt-in row for ${base} could not be written: ${e.message}`);
|
package/dist/handler.jsonld
CHANGED
|
@@ -273,6 +273,10 @@
|
|
|
273
273
|
"@id": "fps:dist/handler.jsonld#FediPodServerHandler__member_agentHandles",
|
|
274
274
|
"memberFieldName": "agentHandles"
|
|
275
275
|
},
|
|
276
|
+
{
|
|
277
|
+
"@id": "fps:dist/handler.jsonld#FediPodServerHandler__member_roots",
|
|
278
|
+
"memberFieldName": "roots"
|
|
279
|
+
},
|
|
276
280
|
{
|
|
277
281
|
"@id": "fps:dist/handler.jsonld#FediPodServerHandler__member_frontHost",
|
|
278
282
|
"memberFieldName": "frontHost"
|
package/dist/store-pod.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.makeStoreFetch = makeStoreFetch;
|
|
17
17
|
exports.makeStoreSession = makeStoreSession;
|
|
18
|
+
const node_stream_1 = require("node:stream");
|
|
18
19
|
const community_server_1 = require("@solid/community-server");
|
|
19
20
|
/** Header lookup that does not care about case, over either shape fetch accepts. */
|
|
20
21
|
function headerReader(init) {
|
|
@@ -131,6 +132,42 @@ function makeStoreFetch(resourceStore, podBase) {
|
|
|
131
132
|
const written = changes?.get?.(identifier);
|
|
132
133
|
return respond(205, null, written && etags.getETag(written));
|
|
133
134
|
}
|
|
135
|
+
// An N3 Patch of an RDF document — a profile, a type index — read by the
|
|
136
|
+
// parser a Solid server reads one with, and applied by the store's own
|
|
137
|
+
// patching. Only the statements change; the document is never rewritten
|
|
138
|
+
// around them. A store that cannot patch answers 501, which is the one
|
|
139
|
+
// answer that sends a caller to writing the whole document instead.
|
|
140
|
+
if (method === 'PATCH') {
|
|
141
|
+
stats.writes++;
|
|
142
|
+
if (!/^text\/n3\b/u.test(header('content-type') ?? ''))
|
|
143
|
+
return respond(415, 'an N3 Patch is the patch this takes');
|
|
144
|
+
let patch;
|
|
145
|
+
try {
|
|
146
|
+
patch = await new community_server_1.N3PatchBodyParser().handle({
|
|
147
|
+
metadata: new community_server_1.RepresentationMetadata(identifier, 'text/n3'),
|
|
148
|
+
request: (0, community_server_1.guardStream)(node_stream_1.Readable.from([String(init.body ?? '')])),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return respond(400, `not a patch this can read: ${e.message}`);
|
|
153
|
+
}
|
|
154
|
+
const ifMatch = header('if-match');
|
|
155
|
+
const conditions = ifMatch
|
|
156
|
+
? new community_server_1.BasicConditions(etags, { matchesETag: ifMatch.split(',').map((t) => t.trim()) })
|
|
157
|
+
: undefined;
|
|
158
|
+
try {
|
|
159
|
+
await resourceStore.modifyResource(identifier, patch, conditions);
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
// What the store said, as the status it said it with: 409 for a
|
|
163
|
+
// deletion of something not there, 412 for a stale If-Match, 501
|
|
164
|
+
// for a store with no patching at all.
|
|
165
|
+
if (community_server_1.HttpError.isInstance(e))
|
|
166
|
+
return respond(e.statusCode, e.message);
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
return respond(205, null);
|
|
170
|
+
}
|
|
134
171
|
if (method === 'DELETE') {
|
|
135
172
|
stats.deletes++;
|
|
136
173
|
try {
|