fedipod 1.29.2 → 1.30.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/device-agent.md CHANGED
@@ -10,18 +10,25 @@ connect to it; and it can host a [group](groups.md).
10
10
  ## Requirements
11
11
 
12
12
  - Node 20 or newer.
13
- <!-- CLAUDE 2026-09-23 — any pod; delete these markers when done -->
14
13
  - A Solid pod, such as `https://alice.solidcommunity.net/` or
15
14
  `https://server.example/alice/`. The address is `@handle@yourpod`, or
16
15
  `@handle@fedipod.net` when the pod is on a path of a shared host, since
17
16
  the shared host cannot answer for the handle; the posts, key and data
18
17
  stay on the pod either way.
19
- <!-- /CLAUDE -->
20
18
  - Followers-only and direct posts need a pod that enforces WAC access control;
21
19
  on one that does not, the composer refuses those two and says why.
22
20
  - While the agent is off, your mail waits on your pod's host. Run it as a
23
21
  service, or attach to a gateway, so it does not pile up there.
24
22
 
23
+ Moving in from another gateway: a pod that already holds an account whose
24
+ address lives at a gateway (a browser account at fedipod.net, say) can be set
25
+ up here with an address at a different gateway, and the account moves rather
26
+ than being refused. Setup keeps the account's state and key on the pod,
27
+ attaches at the new gateway, publishes the new address with the old one as an
28
+ alias, tells the old gateway, and sends a Move to every follower from the old
29
+ address. The old gateway then serves the old actor as a stub saying where it
30
+ went. An address on the pod itself has nothing to move; it is a sign-in.
31
+
25
32
  ## Installing
26
33
 
27
34
  ```
package/gateway.md CHANGED
@@ -120,7 +120,6 @@ not a note, an annotation say, is kept as the app sent it, under your name.
120
120
 
121
121
  ## Moving to another gateway
122
122
 
123
- <!-- CLAUDE 2026-09-23 — new in 1.29.0, browser build; delete these markers when done -->
124
123
  An address that lives at a gateway, `@you@gateway-a`, can move to another
125
124
  one and keep its pod, its posts, its followers and its key. Open the new
126
125
  gateway, choose **create an account**, and sign in with the same pod. The
@@ -147,9 +146,12 @@ What happens then, in order:
147
146
  The old gateway keeps the stub for as long as its row stands; the owner can
148
147
  remove the row from the roster later. An address on the pod, `@you@yourpod`,
149
148
  needs none of this: it detaches from one mail door and attaches to another.
150
- Only the browser build moves in this way for now; the DeviceAgent's setup
151
- does not yet read a pod that already holds an account.
152
- <!-- /CLAUDE -->
149
+
150
+ A DeviceAgent moves the same way: set it up with the pod you already have
151
+ and an address at the new gateway (`--address front`, or "At the gateway"
152
+ on the setup page). Setup reads the account on the pod, keeps its state and
153
+ key there, attaches at the new gateway, and completes the move when the
154
+ agent first acts. See [the DeviceAgent](device-agent.md).
153
155
 
154
156
  ## What the gateway can see
155
157
 
@@ -106,6 +106,9 @@ export class Publisher {
106
106
  image: this.config.image || null, fields: this.config.fields || [],
107
107
  webId: this.remote.webId || null,
108
108
  aliases: this.config.aliases || [],
109
+ // A group says whether only its moderators may open posts (Lemmy's
110
+ // term); a person's actor carries nothing of the kind.
111
+ postingRestrictedToMods: this.config.kind === 'group' ? !!this.config.postingRestrictedToMods : null,
109
112
  moderators,
110
113
  pendingFollowers: priv ? urls.pendingFollowers : null,
111
114
  pendingFollowing: priv ? urls.pendingFollowing : null,
package/lib/core/wire.mjs CHANGED
@@ -3,6 +3,10 @@
3
3
  // protocol documents, not our RDF.
4
4
 
5
5
  export const AS_CTX = 'https://www.w3.org/ns/activitystreams';
6
+ // Lemmy's own context, held locally (lib/core/contexts/join-lemmy.json). It
7
+ // names the three terms a Lemmy reader looks for on a community and its
8
+ // posts: postingRestrictedToMods, stickied, commentsEnabled.
9
+ export const LEMMY_CTX = 'https://join-lemmy.org/context.json';
6
10
  export const SEC_CTX = 'https://w3id.org/security/v1';
7
11
  export const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
8
12
 
@@ -80,7 +84,8 @@ export const assertionKeyId = (urls) => urls.actor + '#ed25519-key';
80
84
  export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null, movedTo = null, kind = 'person',
81
85
  approveJoins = false, summary = null, icon = null, image = null, fields = [],
82
86
  webId = null, aliases = [], moderators = null, pendingFollowers = null, pendingFollowing = null,
83
- blocked = null, inbox = null, outbox = null, oauthAuthorize = null, oauthToken = null }) {
87
+ blocked = null, inbox = null, outbox = null, oauthAuthorize = null, oauthToken = null,
88
+ postingRestrictedToMods = null }) {
84
89
  // manuallyApprovesFollowers is NOT in the base AS2 context, so it is declared
85
90
  // inline exactly as Mastodon declares it — and only when we actually use it.
86
91
  // It is what makes a client show "Request to follow" rather than "Follow" and
@@ -118,6 +123,9 @@ export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null
118
123
  // old accounts elsewhere whose servers check for their own URL here before
119
124
  // they will send a Move. Declared inline exactly as Mastodon declares it.
120
125
  if (webId || aliases.length) context.push({ alsoKnownAs: { '@id': 'as:alsoKnownAs', '@type': '@id' } });
126
+ // A community says whether only its moderators may open posts in it, in
127
+ // Lemmy's term, declared through Lemmy's context. Groups only.
128
+ if (kind === 'group' && postingRestrictedToMods !== null) context.push(LEMMY_CTX);
121
129
  if (fields.length) {
122
130
  context.push({ schema: 'http://schema.org#', PropertyValue: 'schema:PropertyValue', value: 'schema:value' });
123
131
  }
@@ -134,6 +142,7 @@ export function actorDoc({ urls, handle, name, publicKeyPem, assertionKey = null
134
142
  }] } : {}),
135
143
  ...(movedTo ? { movedTo } : {}),
136
144
  ...(webId || aliases.length ? { alsoKnownAs: [...(webId ? [webId] : []), ...aliases] } : {}),
145
+ ...(kind === 'group' && postingRestrictedToMods !== null ? { postingRestrictedToMods: !!postingRestrictedToMods } : {}),
137
146
  preferredUsername: handle,
138
147
  name: name || handle,
139
148
  // The bio, and the avatar. For a group, `summary` is where what the group
@@ -52,8 +52,12 @@ if (!newAccount) {
52
52
  const { resourceExists } = await import(new URL('../../../../lib/pod/root.mjs', import.meta.url));
53
53
  const { apUrls, DEFAULT_ROOT: DR } = await import(new URL('../../../../lib/core/wire.mjs', import.meta.url));
54
54
  if (await resourceExists(fetch, apUrls(pod, DR).actor)) {
55
- console.error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod.');
56
- process.exit(2);
55
+ if (String(flag('address') || 'pod').toLowerCase() !== 'front') {
56
+ console.error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod.');
57
+ console.error('To move an account whose address is at another gateway to this one, add --address front.');
58
+ process.exit(2);
59
+ }
60
+ console.log('The pod already holds an account; if its address is at another gateway, setup moves it here.');
57
61
  }
58
62
  }
59
63
 
@@ -0,0 +1,58 @@
1
+ // gateway-move.mjs — the second half of a DeviceAgent moving an address from
2
+ // one gateway to another (lib/device/setup.mjs does the first, when a pod
3
+ // that already holds an account at another gateway is set up with an address
4
+ // at this one). The browser build has the same two halves in
5
+ // web/app/gateway-move.mjs; this is the Node one, which signs and delivers
6
+ // itself instead of going through a relay.
7
+ //
8
+ // 1. The old gateway is told the address moved (POST /api/move, proved with
9
+ // the pod session as attach was). From then on it serves the old actor
10
+ // as a moved stub — same key, old id, `movedTo` the new one.
11
+ // 2. A Move goes to every follower, FROM the old actor, signed under the
12
+ // old key id: the only signature a follower's server can verify against
13
+ // the old actor. Same key material; only the key id differs.
14
+ //
15
+ // Runs when the agent starts acting, and is safe to run again: a move left
16
+ // pending (the old gateway unreachable) is tried at the next start.
17
+ import { Deliverer } from '../core/deliver.mjs';
18
+ import { moveActivity } from '../core/wire.mjs';
19
+
20
+ export async function completeGatewayMove(agent, { makeDeliverer = (o) => new Deliverer(o) } = {}) {
21
+ const cfg = agent.store.getConfig();
22
+ const mv = cfg?.movedFrom;
23
+ if (!mv || mv.completedAt) return null;
24
+ const log = agent.log || (() => {});
25
+ const newActor = agent.urls.actor;
26
+
27
+ // 1. the old gateway
28
+ let told;
29
+ try {
30
+ told = await agent.remote.session.fetch(`${mv.gateway}/api/move`, {
31
+ method: 'POST', headers: { 'content-type': 'application/json' },
32
+ body: JSON.stringify({ handle: mv.handle, movedTo: newActor }),
33
+ });
34
+ } catch (e) { told = { status: 0, statusText: e.message }; }
35
+ if (told.status !== 200) {
36
+ log(`gateway move: ${mv.gateway} did not mark @${mv.handle} as moved (${told.status || told.statusText}) — will retry at the next start`);
37
+ return { told: false };
38
+ }
39
+
40
+ // 2. the Move, from the old address
41
+ const contacts = agent.store.getContacts();
42
+ const inboxes = [...new Set(contacts.followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
43
+ const activity = moveActivity({ actor: mv.actor }, newActor, Date.now());
44
+ const sender = makeDeliverer({
45
+ store: agent.store, rsaPrivate: agent.deliverer.rsaPrivate,
46
+ keyId: `${mv.actor}#main-key`, actorId: mv.actor, log, passive: true,
47
+ });
48
+ let sent = 0; let failed = 0;
49
+ for (const inbox of inboxes) {
50
+ try { await sender.deliverNow(inbox, activity); sent++; }
51
+ catch (e) { failed++; log(`gateway move: Move to ${inbox} failed: ${e.message}`); }
52
+ }
53
+ const done = { ...mv, completedAt: new Date().toISOString(), moveSent: sent, moveFailed: failed };
54
+ agent.store.setConfig({ ...agent.store.getConfig(), movedFrom: done });
55
+ await agent.store.flush?.();
56
+ log(`moved from ${mv.actor} to ${newActor}: Move sent to ${sent} inbox(es)${failed ? `, ${failed} failed` : ''}`);
57
+ return done;
58
+ }
@@ -27,7 +27,9 @@ export const layoutOf = (cred) => Number(cred?.layout) || 0;
27
27
  // deliberately, with `--private-root`, has said where they want it; a migration
28
28
  // that overrode that would be moving data on its own initiative.
29
29
  export function needsStateMove(cred) {
30
- return !cred?.privateRoot;
30
+ // `stateOnPod` is a move-in from a browser account (setup.mjs): the state
31
+ // is on the pod because that is where the account already lives.
32
+ return !cred?.privateRoot && !cred?.stateOnPod;
31
33
  }
32
34
 
33
35
  export function pendingSteps(cred) {
@@ -21,6 +21,7 @@ import { rootOf, recordLastUsed, writeJsonAtomic } from './home.mjs';
21
21
  import { insecureUrlReason } from '../shared/safefetch.mjs';
22
22
  import { resourceExists } from '../pod/root.mjs';
23
23
  import { CURRENT_LAYOUT, isCurrent } from './migrate.mjs';
24
+ import { completeGatewayMove } from './gateway-move.mjs';
24
25
 
25
26
  const SOLID = $rdf.Namespace('http://www.w3.org/ns/solid/terms#');
26
27
 
@@ -125,6 +126,18 @@ export function frontedAddress({ pod, shape }) {
125
126
  return pathPod || shape === 'front';
126
127
  }
127
128
 
129
+ // The account a pod already holds, read as its owner: the config and the key
130
+ // record in its state container. Both null when there is none, or when the
131
+ // credential is not the owner's. Injected for tests.
132
+ export async function readPodAccount({ home, credential, pod, root, log = () => {} }) {
133
+ const remote = new RemotePod(credential, { home, log });
134
+ await remote.warmup?.();
135
+ const urls = apUrls(pod, root || DEFAULT_ROOT);
136
+ const config = await remote.getJson(urls.state + 'config.json').catch(() => null);
137
+ const keys = await remote.getJson(urls.state + 'keys.json').catch(() => null);
138
+ return { config, keys };
139
+ }
140
+
128
141
  // Take an address at a Gateway: attach the pod to it, fronted, and return the
129
142
  // gateway config bootstrap writes and connect reads. The pod session proves
130
143
  // the pod — no password reaches the Gateway. Injected for tests.
@@ -231,8 +244,13 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
231
244
  // several writes, and that pod is a server somebody runs. `state --to` moves
232
245
  // it afterwards; starting on the pod and moving later is strictly worse,
233
246
  // because the copy left behind was on the pod the whole time.
234
- const privateRoot = answers.privateRoot
247
+ let privateRoot = answers.privateRoot
235
248
  || pathToFileURL(path.join(home, 'private')).href + '/';
249
+ // A pod that already holds an account at ANOTHER gateway, set up with an
250
+ // address at this one: not a second account but the same one moving in.
251
+ // Its state and key are on the pod, and stay there.
252
+ let movingIn = false;
253
+ let movedFrom = null;
236
254
  // Everything that leaves this function — notes, errors, the log — goes
237
255
  // through here. Nothing today echoes a password back, but /setup/progress is
238
256
  // polled repeatedly and would re-serve one forever if anything ever did.
@@ -266,10 +284,15 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
266
284
  // reachable, and no silent 401 later on a pod whose profile is empty.
267
285
  const usable = await checkPod(pod);
268
286
  if (!usable.ok) throw new Error(usable.error);
269
- if (await (deps.resourceExists || resourceExists)(deps.fetch || fetch, apUrls(pod, root || DEFAULT_ROOT).actor)) {
270
- throw new Error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod.');
287
+ const held = await (deps.resourceExists || resourceExists)(deps.fetch || fetch, apUrls(pod, root || DEFAULT_ROOT).actor);
288
+ if (held && !frontedAddress({ pod, shape })) {
289
+ throw new Error('The pod already hosts a FediPod account. If you want a second account, put it on a different pod. '
290
+ + 'To move an account whose address is at another gateway to this one, ask for an address at this gateway.');
271
291
  }
272
- skip('account', 'using the pod you already have');
292
+ if (held && kind === 'group') throw new Error('The pod already hosts a FediPod account, and a group cannot move between gateways.');
293
+ movingIn = held;
294
+ if (movingIn) { privateRoot = null; }
295
+ skip('account', movingIn ? 'the pod already holds the account — its address moves here' : 'using the pod you already have');
273
296
  }
274
297
 
275
298
  // --- credential: the point of no return, and the durability boundary ---
@@ -295,12 +318,14 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
295
318
  remotePod: pod.endsWith('/') ? pod : pod + '/',
296
319
  createdAt: new Date().toISOString(),
297
320
  ...(root ? { root } : {}),
298
- ...(keys === 'pod' ? { keysMode: 'pod' } : {}),
299
- // Where the private half lives. Per-machine, like keysMode.
321
+ ...(keys === 'pod' || movingIn ? { keysMode: 'pod' } : {}),
322
+ // Where the private half lives. Per-machine, like keysMode. A move-in
323
+ // keeps it on the pod, where the account already is, and says so.
300
324
  ...(privateRoot ? { privateRoot } : {}),
325
+ ...(movingIn ? { stateOnPod: true } : {}),
301
326
  // What shape this install is, so `upgrade` can tell an old one from a
302
327
  // new one without guessing from the fields.
303
- ...(isCurrent({ privateRoot }) ? { layout: CURRENT_LAYOUT } : {}),
328
+ ...(isCurrent({ privateRoot, stateOnPod: movingIn }) ? { layout: CURRENT_LAYOUT } : {}),
304
329
  };
305
330
  fs.mkdirSync(home, { recursive: true, mode: 0o700 });
306
331
  writeJsonAtomic(credPath, rec);
@@ -316,6 +341,24 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
316
341
  done('credential', credPath);
317
342
  }
318
343
 
344
+ // --- a move-in: the account on the pod, read with the credential just
345
+ // minted, has to be one whose address lives at another gateway ---
346
+ if (movingIn) {
347
+ const cred = JSON.parse(fs.readFileSync(credPath, 'utf8'));
348
+ const account = await (deps.readPodAccount || readPodAccount)({ home, credential: cred, pod, root: root || DEFAULT_ROOT, log });
349
+ if (!account?.config) throw new Error('the pod holds an account, but its record could not be read with this credential — is this account the pod\'s owner?');
350
+ const frontActor = account.config.gateway?.frontActor || null;
351
+ const gwHost = new URL(gatewayOrigin).host;
352
+ if (!frontActor) throw new Error('The pod already hosts a FediPod account whose address lives on the pod: nothing to move. Sign in to it instead.');
353
+ if (new URL(frontActor).host === gwHost) throw new Error(`The pod already hosts a FediPod account at ${gwHost}: sign in to it instead of setting up again.`);
354
+ if ((account.config.kind || 'person') === 'group') throw new Error('a group cannot move between gateways');
355
+ if (!account.keys?.rsa || account.keys.ct) {
356
+ throw new Error(`this account's key is still under a password from an earlier version — sign in once at ${new URL(frontActor).host} first, then set up here`);
357
+ }
358
+ movedFrom = { actor: frontActor, gateway: new URL(frontActor).origin, handle: account.config.handle, at: new Date().toISOString() };
359
+ log(`moving @${account.config.handle}@${new URL(frontActor).host} here, as @${handle}@${gwHost}`);
360
+ }
361
+
319
362
  // --- take an address at the Gateway, if this pod needs one or asked for
320
363
  // one --- before bootstrap, so the config it writes carries the Gateway
321
364
  // ids and the key connect mints below is stamped to the Gateway actor from
@@ -346,6 +389,17 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
346
389
  if (!ready.ok) throw new Error(ready.error);
347
390
  }
348
391
  await agent.bootstrap({ handle, name: name || handle, root, kind, approveJoins, summary, icon, gateway: gatewayCfg });
392
+ if (movingIn && movedFrom) {
393
+ // The old actor becomes an alias, which is what a server checks on the
394
+ // new actor before it honours a Move; the Move itself is pending until
395
+ // the new actor is published (completeGatewayMove, below and at every
396
+ // start). The key stays; its stamp follows the actor.
397
+ const cfg = agent.store.getConfig() || {};
398
+ agent.store.setConfig({ ...cfg, aliases: [...new Set([...(cfg.aliases || []), movedFrom.actor])], movedFrom });
399
+ const keyRec = agent.store.read?.('keys.json', null);
400
+ if (keyRec?.mintedFor && gatewayCfg?.frontActor) agent.store.write?.('keys.json', { ...keyRec, mintedFor: gatewayCfg.frontActor });
401
+ await agent.store.flush();
402
+ }
349
403
  done('bootstrap');
350
404
 
351
405
  begin('connect');
@@ -360,6 +414,12 @@ export async function runSetup({ home, agent, answers, run, deps = {}, log = ()
360
414
  }
361
415
  const published = await agent.publisher.publishProfile();
362
416
  await agent.store.flush();
417
+ if (movingIn) {
418
+ // Tell the old gateway and the followers. Not fatal: left pending, it
419
+ // is tried again when the agent next starts acting.
420
+ const moved = await (deps.completeGatewayMove || completeGatewayMove)(agent).catch((e) => { log(`gateway move: ${e.message}`); return null; });
421
+ if (moved?.completedAt) log(`Move sent from ${movedFrom.actor} to ${moved.moveSent} inbox(es)`);
422
+ }
363
423
  done('publish');
364
424
 
365
425
  // --- and say plainly whether the world can actually see it ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fedipod",
3
- "version": "1.29.2",
3
+ "version": "1.30.0",
4
4
  "description": "Standalone single-actor ActivityPub agent whose wire face, RDF truth and state all live on a Solid pod (CSS). Bundles a Phanpy UI and a Mastodon client-API facade.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/run-agent.mjs CHANGED
@@ -33,6 +33,7 @@ import { apRoot, rootOf, writeJsonAtomic } from './lib/device/home.mjs';
33
33
  import { ensureTrustedTls } from './lib/device/certs.mjs';
34
34
  import { storageFor } from './lib/core/storage.mjs';
35
35
  import { resolveKeys } from './lib/core/keys.mjs';
36
+ import { completeGatewayMove } from './lib/device/gateway-move.mjs';
36
37
  import { RemotePod } from './lib/device/remote.mjs';
37
38
  import { Deliverer } from './lib/core/deliver.mjs';
38
39
  import { Publisher } from './lib/core/publisher/index.mjs';
@@ -547,6 +548,10 @@ export class Agent {
547
548
  return; // renewal is already running, above
548
549
  }
549
550
  await this.intake.start();
551
+ // An address that moved here from another gateway, whose move was left
552
+ // pending (the old gateway could not be told): try again now that this
553
+ // agent acts and the actor is published.
554
+ completeGatewayMove(this).catch(e => this.log(`gateway move: ${e.message}`));
550
555
  // Reused, not replaced: demote() stops this instance but does not clear it,
551
556
  // so constructing a new one over the top orphaned the old chain beyond the
552
557
  // reach of any later stop().
@@ -9700,6 +9700,7 @@ __export(wire_exports, {
9700
9700
  DEFAULT_ROOT: () => DEFAULT_ROOT,
9701
9701
  FOLLOWERS_PAGE_SIZE: () => FOLLOWERS_PAGE_SIZE,
9702
9702
  HASHTAG_RE: () => HASHTAG_RE,
9703
+ LEMMY_CTX: () => LEMMY_CTX,
9703
9704
  MENTION_RE: () => MENTION_RE,
9704
9705
  OUTBOX_PAGE_SIZE: () => OUTBOX_PAGE_SIZE,
9705
9706
  POLICY_CTX: () => POLICY_CTX,
@@ -9812,7 +9813,8 @@ function actorDoc({
9812
9813
  inbox = null,
9813
9814
  outbox = null,
9814
9815
  oauthAuthorize = null,
9815
- oauthToken = null
9816
+ oauthToken = null,
9817
+ postingRestrictedToMods = null
9816
9818
  }) {
9817
9819
  const context = [AS_CTX, SEC_CTX];
9818
9820
  context.push({ toot: "http://joinmastodon.org/ns#", featured: { "@id": "toot:featured", "@type": "@id" } });
@@ -9835,6 +9837,7 @@ function actorDoc({
9835
9837
  });
9836
9838
  }
9837
9839
  if (webId || aliases.length) context.push({ alsoKnownAs: { "@id": "as:alsoKnownAs", "@type": "@id" } });
9840
+ if (kind === "group" && postingRestrictedToMods !== null) context.push(LEMMY_CTX);
9838
9841
  if (fields.length) {
9839
9842
  context.push({ schema: "http://schema.org#", PropertyValue: "schema:PropertyValue", value: "schema:value" });
9840
9843
  }
@@ -9853,6 +9856,7 @@ function actorDoc({
9853
9856
  }] } : {},
9854
9857
  ...movedTo ? { movedTo } : {},
9855
9858
  ...webId || aliases.length ? { alsoKnownAs: [...webId ? [webId] : [], ...aliases] } : {},
9859
+ ...kind === "group" && postingRestrictedToMods !== null ? { postingRestrictedToMods: !!postingRestrictedToMods } : {},
9856
9860
  preferredUsername: handle7,
9857
9861
  name: name || handle7,
9858
9862
  // The bio, and the avatar. For a group, `summary` is where what the group
@@ -10375,7 +10379,7 @@ function addRemoveActivity({ urls, type, object, target, serial }) {
10375
10379
  target
10376
10380
  };
10377
10381
  }
10378
- var import_sanitize_html, AS_CTX, SEC_CTX, PUBLIC, DEFAULT_ROOT, assertionKeyId, OUTBOX_PAGE_SIZE, outboxPageId, outboxPageCount, outboxItemId, outboxWireItem, outboxLocalItem, FOLLOWERS_PAGE_SIZE, followersPageId, followersPageCount, ALLOWED_TAGS, ALLOWED_ATTRS, MAX_ATTACHMENTS, MAX_ATTACHMENT_URL, attachmentUrl, HTML_ESCAPES2, MENTION_RE, HASHTAG_RE, QUOTE_CTX, POLICY_CTX;
10382
+ var import_sanitize_html, AS_CTX, LEMMY_CTX, SEC_CTX, PUBLIC, DEFAULT_ROOT, assertionKeyId, OUTBOX_PAGE_SIZE, outboxPageId, outboxPageCount, outboxItemId, outboxWireItem, outboxLocalItem, FOLLOWERS_PAGE_SIZE, followersPageId, followersPageCount, ALLOWED_TAGS, ALLOWED_ATTRS, MAX_ATTACHMENTS, MAX_ATTACHMENT_URL, attachmentUrl, HTML_ESCAPES2, MENTION_RE, HASHTAG_RE, QUOTE_CTX, POLICY_CTX;
10379
10383
  var init_wire = __esm({
10380
10384
  "lib/core/wire.mjs"() {
10381
10385
  init_urls();
@@ -10383,6 +10387,7 @@ var init_wire = __esm({
10383
10387
  init_profile_page();
10384
10388
  import_sanitize_html = __toESM(require_sanitize_html(), 1);
10385
10389
  AS_CTX = "https://www.w3.org/ns/activitystreams";
10390
+ LEMMY_CTX = "https://join-lemmy.org/context.json";
10386
10391
  SEC_CTX = "https://w3id.org/security/v1";
10387
10392
  PUBLIC = "https://www.w3.org/ns/activitystreams#Public";
10388
10393
  DEFAULT_ROOT = "fedipod/";
@@ -57180,6 +57185,9 @@ var Publisher = class {
57180
57185
  fields: this.config.fields || [],
57181
57186
  webId: this.remote.webId || null,
57182
57187
  aliases: this.config.aliases || [],
57188
+ // A group says whether only its moderators may open posts (Lemmy's
57189
+ // term); a person's actor carries nothing of the kind.
57190
+ postingRestrictedToMods: this.config.kind === "group" ? !!this.config.postingRestrictedToMods : null,
57183
57191
  moderators,
57184
57192
  pendingFollowers: priv ? urls.pendingFollowers : null,
57185
57193
  pendingFollowing: priv ? urls.pendingFollowing : null,