fedipod-server 0.28.0 → 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.
Files changed (71) hide show
  1. package/README.md +27 -28
  2. package/config/server.json +22 -0
  3. package/dist/claims.js +3 -2
  4. package/dist/directory.d.ts +2 -0
  5. package/dist/handler.d.ts +4 -1
  6. package/dist/handler.js +32 -3
  7. package/dist/handler.jsonld +4 -0
  8. package/dist/store-pod.js +37 -0
  9. package/lib/client/c2s.mjs +97 -43
  10. package/lib/client/masto/accounts.mjs +1 -0
  11. package/lib/connections/bskyfeed.mjs +6 -0
  12. package/lib/core/deliver.mjs +68 -22
  13. package/lib/core/intake/activities.mjs +18 -2
  14. package/lib/core/intake/group.mjs +3 -1
  15. package/lib/core/intake/index.mjs +50 -6
  16. package/lib/core/intake/notes.mjs +12 -5
  17. package/lib/core/lease.mjs +7 -7
  18. package/lib/core/place.mjs +82 -0
  19. package/lib/core/publisher/collections.mjs +37 -2
  20. package/lib/core/publisher/index.mjs +23 -1
  21. package/lib/core/publisher/notes.mjs +73 -7
  22. package/lib/core/publisher/own.mjs +143 -0
  23. package/lib/core/publisher/questions.mjs +5 -3
  24. package/lib/core/social.mjs +62 -32
  25. package/lib/core/store.mjs +36 -6
  26. package/lib/core/wire.mjs +51 -15
  27. package/lib/device/admin/routes/lifecycle.mjs +1 -1
  28. package/lib/device/admin/routes/setup.mjs +17 -1
  29. package/lib/device/cli/commands/setup.mjs +50 -8
  30. package/lib/device/cli/context.mjs +1 -1
  31. package/lib/device/migrate.mjs +1 -1
  32. package/lib/device/setup.mjs +45 -8
  33. package/lib/gateway/caches.mjs +36 -0
  34. package/lib/gateway/front-core.mjs +80 -111
  35. package/lib/gateway/gateway-core.mjs +91 -9
  36. package/lib/gateway/headers.mjs +49 -0
  37. package/lib/gateway/quiet.mjs +7 -2
  38. package/lib/gateway/relay-extras.mjs +89 -0
  39. package/lib/gateway/token-claims.mjs +16 -0
  40. package/lib/pod/containers.mjs +5 -1
  41. package/lib/pod/inbox.mjs +22 -3
  42. package/lib/pod/location.mjs +52 -0
  43. package/lib/pod/notes.mjs +2 -4
  44. package/lib/pod/transport.mjs +207 -25
  45. package/lib/pod/type-index.mjs +101 -0
  46. package/lib/pod/urls.mjs +6 -0
  47. package/lib/server/embed.mjs +7 -6
  48. package/lib/session/README.md +14 -0
  49. package/lib/session/fedi-account.mjs +73 -2
  50. package/lib/session/package.json +10 -2
  51. package/package.json +1 -1
  52. package/run-agent.mjs +18 -0
  53. package/web/admin/actors.js +2 -0
  54. package/web/admin/index.html +9 -0
  55. package/web/admin/record.js +4 -1
  56. package/web/admin/setup/index.html +17 -1
  57. package/web/admin/setup/setup.js +18 -5
  58. package/web/app/README.md +1 -1
  59. package/web/app/admin-facade.mjs +1 -1
  60. package/web/app/agent.mjs +24 -10
  61. package/web/app/boot.mjs +72 -32
  62. package/web/app/deliver-relay.mjs +47 -7
  63. package/web/app/dist/boot.js +466 -89
  64. package/web/app/dist/boot.js.map +4 -4
  65. package/web/app/dist/sw.js +2792 -1996
  66. package/web/app/dist/sw.js.map +4 -4
  67. package/web/app/index.html +16 -0
  68. package/web/app/signup.mjs +65 -26
  69. package/web/app/update.js +4 -0
  70. package/web/front/run.html +7 -1
  71. 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
- - **Pods on subdomains or on paths.** A subdomain pod answers on its own
34
- address (`@mei@mei.example.org`); a suffix pod on a path of the server's own
35
- host answers under that path, and the server fronts it so its address is the
36
- server's host (`@aisha@server.example`, for a pod at
37
- `https://server.example/aisha/`). Either works — a suffix pod cannot resolve
38
- its own handle, so it is followable only through the server's apex, which the
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 subdomain-pods CSS config gives each identity an origin of its own; a
74
- suffix (path) config hosts each on a path of the shared host. Both opt in.
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 suffix pod, and it publishes an
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
- subdomain, and through the server's apex for a suffix pod. Everything it is made of lives on its pod — its state, its private
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 suffix pod it is under the pod's path, e.g. `/aisha/fp/`. |
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 subdomain pod gets an origin of its own and answers at its root; a suffix
159
- pod shares the host and answers under its own path (`/aisha/ap/actor`,
160
- `/aisha/api/…`, `/aisha/oauth/…`, its door at `/aisha/fp/`). So a host carries
161
- at most one root identity, but any number of path pods beside it; a second
162
- root identity on a host, or a path pod nesting under another, is refused at
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
- # subdomain pod:
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
- # suffix pod — the door is under the pod's path:
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 subdomain pod the paths above
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 suffix pod the same names are taken under the pod's own path
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 (subdomain pods) and uses it as a client would
249
- npm run test:e2e:suffix # the same, for pods on paths of one host (mode d)
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 live on paths of
263
- one host (`@aisha@server`): it proves the apex WebFinger resolves straight to
264
- the pod actor, that the actor, inbox, client API, OAuth and door all answer
265
- under the pod's path, that two path pods on one origin stay separate, and that
266
- a signed delivery is verified at the pod's own door.
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.
@@ -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. A POST here is a delivery, verified at the door
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.
@@ -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 path pod owns only its own subtree`);
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}`);
@@ -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 {