fedipod-server 0.17.0 → 0.18.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.
@@ -9596,6 +9596,7 @@ __export(wire_exports, {
9596
9596
  acceptActivity: () => acceptActivity,
9597
9597
  actorDoc: () => actorDoc,
9598
9598
  addRemoveActivity: () => addRemoveActivity,
9599
+ addressing: () => addressing,
9599
9600
  announceActivity: () => announceActivity,
9600
9601
  apUrls: () => apUrls2,
9601
9602
  assertionKeyId: () => assertionKeyId,
@@ -10056,6 +10057,14 @@ function contentHtml(text, mentions = []) {
10056
10057
  }
10057
10058
  return "<p>" + html.replace(/\n+/g, "</p><p>") + "</p>";
10058
10059
  }
10060
+ function addressing(urls, visibility, who = []) {
10061
+ return {
10062
+ public: { to: [PUBLIC], cc: [urls.followers, ...who] },
10063
+ unlisted: { to: [urls.followers], cc: [PUBLIC, ...who] },
10064
+ private: { to: [urls.followers], cc: who },
10065
+ direct: { to: who, cc: [] }
10066
+ }[visibility] || { to: [PUBLIC], cc: [urls.followers, ...who] };
10067
+ }
10059
10068
  function noteDoc({
10060
10069
  urls,
10061
10070
  slug,
@@ -10071,12 +10080,7 @@ function noteDoc({
10071
10080
  }) {
10072
10081
  const id = (container || urls.notes) + slug;
10073
10082
  const who = mentions.map((m) => m.actor);
10074
- const addressing = {
10075
- public: { to: [PUBLIC], cc: [urls.followers, ...who] },
10076
- unlisted: { to: [urls.followers], cc: [PUBLIC, ...who] },
10077
- private: { to: [urls.followers], cc: who },
10078
- direct: { to: who, cc: [] }
10079
- }[visibility] || { to: [PUBLIC], cc: [urls.followers, ...who] };
10083
+ const addressed = addressing(urls, visibility, who);
10080
10084
  const note = {
10081
10085
  "@context": AS_CTX,
10082
10086
  id,
@@ -10084,8 +10088,8 @@ function noteDoc({
10084
10088
  attributedTo: urls.actor,
10085
10089
  content: contentHtml(content, mentions),
10086
10090
  published,
10087
- to: addressing.to,
10088
- cc: addressing.cc,
10091
+ to: addressed.to,
10092
+ cc: addressed.cc,
10089
10093
  replies: repliesId(id)
10090
10094
  };
10091
10095
  if (summary) note.summary = summary;
@@ -10279,7 +10283,7 @@ var init_wire = __esm({
10279
10283
  AS_CTX = "https://www.w3.org/ns/activitystreams";
10280
10284
  SEC_CTX = "https://w3id.org/security/v1";
10281
10285
  PUBLIC = "https://www.w3.org/ns/activitystreams#Public";
10282
- DEFAULT_ROOT = "activitypods-js/";
10286
+ DEFAULT_ROOT = "fedipod/";
10283
10287
  assertionKeyId = (urls) => urls.actor + "#ed25519-key";
10284
10288
  OUTBOX_PAGE_SIZE = 20;
10285
10289
  outboxPageId = (outboxId, n) => `${outboxId}-${n}`;
@@ -24128,20 +24132,20 @@ var require_yallist = __commonJS({
24128
24132
  return acc;
24129
24133
  };
24130
24134
  Yallist.prototype.toArray = function() {
24131
- var arr = new Array(this.length);
24135
+ var arr2 = new Array(this.length);
24132
24136
  for (var i = 0, walker = this.head; walker !== null; i++) {
24133
- arr[i] = walker.value;
24137
+ arr2[i] = walker.value;
24134
24138
  walker = walker.next;
24135
24139
  }
24136
- return arr;
24140
+ return arr2;
24137
24141
  };
24138
24142
  Yallist.prototype.toArrayReverse = function() {
24139
- var arr = new Array(this.length);
24143
+ var arr2 = new Array(this.length);
24140
24144
  for (var i = 0, walker = this.tail; walker !== null; i++) {
24141
- arr[i] = walker.value;
24145
+ arr2[i] = walker.value;
24142
24146
  walker = walker.prev;
24143
24147
  }
24144
- return arr;
24148
+ return arr2;
24145
24149
  };
24146
24150
  Yallist.prototype.slice = function(from, to) {
24147
24151
  to = to || this.length;
@@ -24469,11 +24473,11 @@ var require_lru_cache = __commonJS({
24469
24473
  del(key) {
24470
24474
  del(this, this[CACHE].get(key));
24471
24475
  }
24472
- load(arr) {
24476
+ load(arr2) {
24473
24477
  this.reset();
24474
24478
  const now = Date.now();
24475
- for (let l = arr.length - 1; l >= 0; l--) {
24476
- const hit = arr[l];
24479
+ for (let l = arr2.length - 1; l >= 0; l--) {
24480
+ const hit = arr2[l];
24477
24481
  const expiresAt = hit.e || 0;
24478
24482
  if (expiresAt === 0)
24479
24483
  this.set(hit.k, hit.v);
@@ -30207,26 +30211,26 @@ var require_base64_js = __commonJS({
30207
30211
  var lens = getLens(b642);
30208
30212
  var validLen = lens[0];
30209
30213
  var placeHoldersLen = lens[1];
30210
- var arr = new Arr(_byteLength(b642, validLen, placeHoldersLen));
30214
+ var arr2 = new Arr(_byteLength(b642, validLen, placeHoldersLen));
30211
30215
  var curByte = 0;
30212
30216
  var len2 = placeHoldersLen > 0 ? validLen - 4 : validLen;
30213
30217
  var i2;
30214
30218
  for (i2 = 0; i2 < len2; i2 += 4) {
30215
30219
  tmp = revLookup[b642.charCodeAt(i2)] << 18 | revLookup[b642.charCodeAt(i2 + 1)] << 12 | revLookup[b642.charCodeAt(i2 + 2)] << 6 | revLookup[b642.charCodeAt(i2 + 3)];
30216
- arr[curByte++] = tmp >> 16 & 255;
30217
- arr[curByte++] = tmp >> 8 & 255;
30218
- arr[curByte++] = tmp & 255;
30220
+ arr2[curByte++] = tmp >> 16 & 255;
30221
+ arr2[curByte++] = tmp >> 8 & 255;
30222
+ arr2[curByte++] = tmp & 255;
30219
30223
  }
30220
30224
  if (placeHoldersLen === 2) {
30221
30225
  tmp = revLookup[b642.charCodeAt(i2)] << 2 | revLookup[b642.charCodeAt(i2 + 1)] >> 4;
30222
- arr[curByte++] = tmp & 255;
30226
+ arr2[curByte++] = tmp & 255;
30223
30227
  }
30224
30228
  if (placeHoldersLen === 1) {
30225
30229
  tmp = revLookup[b642.charCodeAt(i2)] << 10 | revLookup[b642.charCodeAt(i2 + 1)] << 4 | revLookup[b642.charCodeAt(i2 + 2)] >> 2;
30226
- arr[curByte++] = tmp >> 8 & 255;
30227
- arr[curByte++] = tmp & 255;
30230
+ arr2[curByte++] = tmp >> 8 & 255;
30231
+ arr2[curByte++] = tmp & 255;
30228
30232
  }
30229
- return arr;
30233
+ return arr2;
30230
30234
  }
30231
30235
  function tripletToBase64(num) {
30232
30236
  return lookup2[num >> 18 & 63] + lookup2[num >> 12 & 63] + lookup2[num >> 6 & 63] + lookup2[num & 63];
@@ -30368,13 +30372,13 @@ var require_buffer = __commonJS({
30368
30372
  }
30369
30373
  function typedArraySupport() {
30370
30374
  try {
30371
- const arr = new Uint8Array(1);
30375
+ const arr2 = new Uint8Array(1);
30372
30376
  const proto = { foo: function() {
30373
30377
  return 42;
30374
30378
  } };
30375
30379
  Object.setPrototypeOf(proto, Uint8Array.prototype);
30376
- Object.setPrototypeOf(arr, proto);
30377
- return arr.foo() === 42;
30380
+ Object.setPrototypeOf(arr2, proto);
30381
+ return arr2.foo() === 42;
30378
30382
  } catch (e) {
30379
30383
  return false;
30380
30384
  }
@@ -30894,14 +30898,14 @@ var require_buffer = __commonJS({
30894
30898
  }
30895
30899
  throw new TypeError("val must be string, number or Buffer");
30896
30900
  }
30897
- function arrayIndexOf(arr, val, byteOffset, encoding, dir) {
30901
+ function arrayIndexOf(arr2, val, byteOffset, encoding, dir) {
30898
30902
  let indexSize = 1;
30899
- let arrLength = arr.length;
30903
+ let arrLength = arr2.length;
30900
30904
  let valLength = val.length;
30901
30905
  if (encoding !== void 0) {
30902
30906
  encoding = String(encoding).toLowerCase();
30903
30907
  if (encoding === "ucs2" || encoding === "ucs-2" || encoding === "utf16le" || encoding === "utf-16le") {
30904
- if (arr.length < 2 || val.length < 2) {
30908
+ if (arr2.length < 2 || val.length < 2) {
30905
30909
  return -1;
30906
30910
  }
30907
30911
  indexSize = 2;
@@ -30921,7 +30925,7 @@ var require_buffer = __commonJS({
30921
30925
  if (dir) {
30922
30926
  let foundIndex = -1;
30923
30927
  for (i = byteOffset; i < arrLength; i++) {
30924
- if (read2(arr, i) === read2(val, foundIndex === -1 ? 0 : i - foundIndex)) {
30928
+ if (read2(arr2, i) === read2(val, foundIndex === -1 ? 0 : i - foundIndex)) {
30925
30929
  if (foundIndex === -1) foundIndex = i;
30926
30930
  if (i - foundIndex + 1 === valLength) return foundIndex * indexSize;
30927
30931
  } else {
@@ -30934,7 +30938,7 @@ var require_buffer = __commonJS({
30934
30938
  for (i = byteOffset; i >= 0; i--) {
30935
30939
  let found = true;
30936
30940
  for (let j = 0; j < valLength; j++) {
30937
- if (read2(arr, i + j) !== read2(val, j)) {
30941
+ if (read2(arr2, i + j) !== read2(val, j)) {
30938
30942
  found = false;
30939
30943
  break;
30940
30944
  }
@@ -33960,11 +33964,11 @@ var require_src = __commonJS({
33960
33964
  function serializeFunction(fn) {
33961
33965
  return JSON.stringify(fn);
33962
33966
  }
33963
- function serializeArray(arr) {
33967
+ function serializeArray(arr2) {
33964
33968
  let str = "[";
33965
- const length = arr.length;
33969
+ const length = arr2.length;
33966
33970
  for (let i = 0; i < length; i++) {
33967
- const val = arr[i];
33971
+ const val = arr2[i];
33968
33972
  if (i !== 0) str += ",";
33969
33973
  str += serialize3(val);
33970
33974
  }
@@ -34119,6 +34123,7 @@ function dropFollower(contacts, actor, why) {
34119
34123
  }
34120
34124
  var PodStore = class {
34121
34125
  constructor({ storage = null, log: log2 = console.log } = {}) {
34126
+ this.lastSkipped = [];
34122
34127
  this.storage = storage;
34123
34128
  this.log = log2;
34124
34129
  this.cache = /* @__PURE__ */ new Map();
@@ -34181,6 +34186,7 @@ var PodStore = class {
34181
34186
  this.log(`state load ${name}: unparsable (${e.message})`);
34182
34187
  }
34183
34188
  }
34189
+ this.lastSkipped = skipped;
34184
34190
  if (skipped.length) this.log(`state load skipped ${skipped.length}: ${skipped.join(", ")}`);
34185
34191
  this.log(`state loaded: ${this.cache.size} doc(s) from ${this.base} (${fetched} re-fetched)`);
34186
34192
  }
@@ -36882,11 +36888,11 @@ function arrayToStatements(rdfFactory, subject, data) {
36882
36888
  }, subject);
36883
36889
  return statements;
36884
36890
  }
36885
- function ArrayIndexOf(arr, item, i = 0) {
36886
- var length = arr.length;
36891
+ function ArrayIndexOf(arr2, item, i = 0) {
36892
+ var length = arr2.length;
36887
36893
  if (i < 0) i = length + i;
36888
36894
  for (; i < length; i++) {
36889
- if (arr[i] === item) {
36895
+ if (arr2[i] === item) {
36890
36896
  return i;
36891
36897
  }
36892
36898
  }
@@ -45218,8 +45224,8 @@ async function writeProfilePage(pod, urls, html) {
45218
45224
  await pod.put(urls.profileHtml, html, "text/html");
45219
45225
  await pod.setAcl(urls.profileHtml, PUBLIC_READ2);
45220
45226
  }
45221
- function linkInWebIdProfile(pod, { actorUrl, accountName, kind = "person" }) {
45222
- return pod.linkAccountInProfile({ actorUrl, accountName, kind });
45227
+ function linkInWebIdProfile(pod, { actorUrl, accountName, kind = "person", outbox = null }) {
45228
+ return pod.linkAccountInProfile({ actorUrl, accountName, kind, outbox });
45223
45229
  }
45224
45230
 
45225
45231
  // lib/pod/inbox.mjs
@@ -55489,6 +55495,126 @@ var miscellany_default = {
55489
55495
  }
55490
55496
  };
55491
55497
 
55498
+ // lib/core/contexts/anno.json
55499
+ var anno_default = {
55500
+ "@context": {
55501
+ oa: "http://www.w3.org/ns/oa#",
55502
+ dc: "http://purl.org/dc/elements/1.1/",
55503
+ dcterms: "http://purl.org/dc/terms/",
55504
+ dctypes: "http://purl.org/dc/dcmitype/",
55505
+ foaf: "http://xmlns.com/foaf/0.1/",
55506
+ rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
55507
+ rdfs: "http://www.w3.org/2000/01/rdf-schema#",
55508
+ skos: "http://www.w3.org/2004/02/skos/core#",
55509
+ xsd: "http://www.w3.org/2001/XMLSchema#",
55510
+ iana: "http://www.iana.org/assignments/relation/",
55511
+ owl: "http://www.w3.org/2002/07/owl#",
55512
+ as: "http://www.w3.org/ns/activitystreams#",
55513
+ schema: "http://schema.org/",
55514
+ id: { "@type": "@id", "@id": "@id" },
55515
+ type: { "@type": "@id", "@id": "@type" },
55516
+ Annotation: "oa:Annotation",
55517
+ Dataset: "dctypes:Dataset",
55518
+ Image: "dctypes:StillImage",
55519
+ Video: "dctypes:MovingImage",
55520
+ Audio: "dctypes:Sound",
55521
+ Text: "dctypes:Text",
55522
+ TextualBody: "oa:TextualBody",
55523
+ ResourceSelection: "oa:ResourceSelection",
55524
+ SpecificResource: "oa:SpecificResource",
55525
+ FragmentSelector: "oa:FragmentSelector",
55526
+ CssSelector: "oa:CssSelector",
55527
+ XPathSelector: "oa:XPathSelector",
55528
+ TextQuoteSelector: "oa:TextQuoteSelector",
55529
+ TextPositionSelector: "oa:TextPositionSelector",
55530
+ DataPositionSelector: "oa:DataPositionSelector",
55531
+ SvgSelector: "oa:SvgSelector",
55532
+ RangeSelector: "oa:RangeSelector",
55533
+ TimeState: "oa:TimeState",
55534
+ HttpRequestState: "oa:HttpRequestState",
55535
+ CssStylesheet: "oa:CssStyle",
55536
+ Choice: "oa:Choice",
55537
+ Person: "foaf:Person",
55538
+ Software: "as:Application",
55539
+ Organization: "foaf:Organization",
55540
+ AnnotationCollection: "as:OrderedCollection",
55541
+ AnnotationPage: "as:OrderedCollectionPage",
55542
+ Audience: "schema:Audience",
55543
+ Motivation: "oa:Motivation",
55544
+ bookmarking: "oa:bookmarking",
55545
+ classifying: "oa:classifying",
55546
+ commenting: "oa:commenting",
55547
+ describing: "oa:describing",
55548
+ editing: "oa:editing",
55549
+ highlighting: "oa:highlighting",
55550
+ identifying: "oa:identifying",
55551
+ linking: "oa:linking",
55552
+ moderating: "oa:moderating",
55553
+ questioning: "oa:questioning",
55554
+ replying: "oa:replying",
55555
+ reviewing: "oa:reviewing",
55556
+ assessing: "oa:assessing",
55557
+ tagging: "oa:tagging",
55558
+ auto: "oa:autoDirection",
55559
+ ltr: "oa:ltrDirection",
55560
+ rtl: "oa:rtlDirection",
55561
+ body: { "@type": "@id", "@id": "oa:hasBody" },
55562
+ target: { "@type": "@id", "@id": "oa:hasTarget" },
55563
+ source: { "@type": "@id", "@id": "oa:hasSource" },
55564
+ selector: { "@type": "@id", "@id": "oa:hasSelector" },
55565
+ state: { "@type": "@id", "@id": "oa:hasState" },
55566
+ scope: { "@type": "@id", "@id": "oa:hasScope" },
55567
+ refinedBy: { "@type": "@id", "@id": "oa:refinedBy" },
55568
+ startSelector: { "@type": "@id", "@id": "oa:hasStartSelector" },
55569
+ endSelector: { "@type": "@id", "@id": "oa:hasEndSelector" },
55570
+ renderedVia: { "@type": "@id", "@id": "oa:renderedVia" },
55571
+ creator: { "@type": "@id", "@id": "dcterms:creator" },
55572
+ generator: { "@type": "@id", "@id": "as:generator" },
55573
+ rights: { "@type": "@id", "@id": "dcterms:rights" },
55574
+ homepage: { "@type": "@id", "@id": "foaf:homepage" },
55575
+ via: { "@type": "@id", "@id": "oa:via" },
55576
+ canonical: { "@type": "@id", "@id": "oa:canonical" },
55577
+ stylesheet: { "@type": "@id", "@id": "oa:styledBy" },
55578
+ cached: { "@type": "@id", "@id": "oa:cachedSource" },
55579
+ conformsTo: { "@type": "@id", "@id": "dcterms:conformsTo" },
55580
+ items: { "@type": "@id", "@id": "as:items", "@container": "@list" },
55581
+ partOf: { "@type": "@id", "@id": "as:partOf" },
55582
+ first: { "@type": "@id", "@id": "as:first" },
55583
+ last: { "@type": "@id", "@id": "as:last" },
55584
+ next: { "@type": "@id", "@id": "as:next" },
55585
+ prev: { "@type": "@id", "@id": "as:prev" },
55586
+ audience: { "@type": "@id", "@id": "schema:audience" },
55587
+ motivation: { "@type": "@vocab", "@id": "oa:motivatedBy" },
55588
+ purpose: { "@type": "@vocab", "@id": "oa:hasPurpose" },
55589
+ textDirection: { "@type": "@vocab", "@id": "oa:textDirection" },
55590
+ accessibility: "schema:accessibilityFeature",
55591
+ bodyValue: "oa:bodyValue",
55592
+ format: "dc:format",
55593
+ language: "dc:language",
55594
+ processingLanguage: "oa:processingLanguage",
55595
+ value: "rdf:value",
55596
+ exact: "oa:exact",
55597
+ prefix: "oa:prefix",
55598
+ suffix: "oa:suffix",
55599
+ styleClass: "oa:styleClass",
55600
+ name: "foaf:name",
55601
+ email: "foaf:mbox",
55602
+ email_sha1: "foaf:mbox_sha1sum",
55603
+ nickname: "foaf:nick",
55604
+ label: "rdfs:label",
55605
+ created: { "@id": "dcterms:created", "@type": "xsd:dateTime" },
55606
+ modified: { "@id": "dcterms:modified", "@type": "xsd:dateTime" },
55607
+ generated: { "@id": "dcterms:issued", "@type": "xsd:dateTime" },
55608
+ sourceDate: { "@id": "oa:sourceDate", "@type": "xsd:dateTime" },
55609
+ sourceDateStart: { "@id": "oa:sourceDateStart", "@type": "xsd:dateTime" },
55610
+ sourceDateEnd: { "@id": "oa:sourceDateEnd", "@type": "xsd:dateTime" },
55611
+ start: { "@id": "oa:start", "@type": "xsd:nonNegativeInteger" },
55612
+ end: { "@id": "oa:end", "@type": "xsd:nonNegativeInteger" },
55613
+ total: { "@id": "as:totalItems", "@type": "xsd:nonNegativeInteger" },
55614
+ startIndex: { "@id": "as:startIndex", "@type": "xsd:nonNegativeInteger" }
55615
+ }
55616
+ };
55617
+
55492
55618
  // lib/core/contexts/index.mjs
55493
55619
  var CONTEXTS = {
55494
55620
  "https://www.w3.org/ns/activitystreams": activitystreams_default,
@@ -55504,7 +55630,8 @@ var CONTEXTS = {
55504
55630
  "https://w3id.org/fep/5711": fep_5711_default,
55505
55631
  "https://join-lemmy.org/context.json": join_lemmy_default,
55506
55632
  "http://joinmastodon.org/ns": joinmastodon_default,
55507
- "https://purl.archive.org/miscellany": miscellany_default
55633
+ "https://purl.archive.org/miscellany": miscellany_default,
55634
+ "http://www.w3.org/ns/anno.jsonld": anno_default
55508
55635
  };
55509
55636
 
55510
55637
  // lib/core/graphview.mjs
@@ -55762,146 +55889,6 @@ async function readLenient(raw) {
55762
55889
  }
55763
55890
  }
55764
55891
 
55765
- // lib/core/publisher/restore.mjs
55766
- var ACCEPT_AP = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
55767
- var REBUILD_MAX_PER_RUN = 200;
55768
- async function reconcileFollowers(publisher, contacts) {
55769
- let published = [];
55770
- try {
55771
- published = await publisher.readPublishedFollowers() || [];
55772
- } catch {
55773
- return 0;
55774
- }
55775
- const known2 = new Set(contacts.followers.map((f) => f.actor));
55776
- const removed = new Set((contacts.removedFollowers || []).map((r) => r.actor));
55777
- const missing = published.filter((a) => typeof a === "string" && !known2.has(a) && !removed.has(a));
55778
- if (!missing.length) return 0;
55779
- let recovered = 0;
55780
- for (const actor of missing.slice(0, 200)) {
55781
- try {
55782
- const res = await publisher.deliverer.signedFetch(actor, { headers: { accept: ACCEPT_AP } });
55783
- if (!res.ok) continue;
55784
- const { doc, view, degraded } = await readLenient(await res.json());
55785
- const actorDoc2 = view ?? doc;
55786
- if (degraded) publisher.log?.(`actor ${actor} grounded to read: ${degraded}`);
55787
- if (!actorDoc2?.inbox) continue;
55788
- contacts.followers.push({
55789
- actor,
55790
- inbox: actorDoc2.inbox,
55791
- sharedInbox: actorDoc2.endpoints?.sharedInbox || null,
55792
- recovered: true,
55793
- // Said explicitly, because onUndo reads it: the pod publishes WHO
55794
- // follows, never the id of the Follow that did it, so a recovered
55795
- // record has nothing an Undo can be matched against and must not be
55796
- // evictable by one naming anything at all.
55797
- followId: null
55798
- });
55799
- recovered++;
55800
- } catch {
55801
- }
55802
- }
55803
- if (recovered) {
55804
- publisher.store.setContacts(contacts);
55805
- publisher.log(`reconciled ${recovered} follower(s) the pod knew about and this machine did not \u2014 a restored or copied state was behind`);
55806
- }
55807
- return recovered;
55808
- }
55809
- async function reconcileOutbox(publisher, outbox) {
55810
- let published = [];
55811
- try {
55812
- published = await publisher.readPublishedOutbox() || [];
55813
- } catch {
55814
- return 0;
55815
- }
55816
- if (!Array.isArray(published) || !published.length) return 0;
55817
- const idOf = (i) => typeof i === "string" ? i : i?.id || null;
55818
- const known2 = new Set(outbox.map(idOf).filter(Boolean));
55819
- const removed = new Set(publisher.store.read("outbox-removed.json", []).map((r) => r.id));
55820
- const missing = published.filter((i) => {
55821
- const id = idOf(i);
55822
- return id && !known2.has(id) && !removed.has(id);
55823
- });
55824
- if (!missing.length) return 0;
55825
- outbox.push(...missing);
55826
- publisher.store.write("outbox.json", outbox);
55827
- publisher.log(`reconciled ${missing.length} outbox entr(ies) the pod carried and this machine did not`);
55828
- return missing.length;
55829
- }
55830
- async function rebuildStatuses(publisher, { fromNotes = false } = {}) {
55831
- const { urls } = publisher;
55832
- const ids = /* @__PURE__ */ new Set();
55833
- const boosts = [];
55834
- let indexed = false;
55835
- const published = await publisher.readPublishedOutbox().catch(() => null);
55836
- if (published) {
55837
- indexed = true;
55838
- for (const item of published) {
55839
- if (typeof item === "string") {
55840
- if (item.startsWith(urls.notes)) ids.add(item);
55841
- } else if (item?.type === "Announce") boosts.push(item);
55842
- }
55843
- }
55844
- if (fromNotes) {
55845
- for (const child of await list2(publisher.remote, urls).catch(() => [])) {
55846
- ids.add(child.url);
55847
- }
55848
- indexed = true;
55849
- }
55850
- if (!indexed) return { indexed: 0, recovered: 0, reblogs: 0, landed: false, why: "the pod would not answer for its outbox" };
55851
- const statuses = publisher.store.getStatuses();
55852
- const have = new Set(statuses.map((s) => s.noteId));
55853
- const removed = new Set(publisher.store.read("outbox-removed.json", []).map((r) => r.id));
55854
- const recovered = [];
55855
- let budget = REBUILD_MAX_PER_RUN;
55856
- for (const id of ids) {
55857
- if (budget <= 0) {
55858
- publisher.log(`rebuild: stopping at ${REBUILD_MAX_PER_RUN} this run \u2014 run it again for the rest`);
55859
- break;
55860
- }
55861
- if (have.has(id) || removed.has(id)) continue;
55862
- budget--;
55863
- const note = await read(publisher.remote, id).catch(() => null);
55864
- if (note?.type !== "Note" || note.id !== id || note.attributedTo !== urls.actor) continue;
55865
- const attachments = attachmentsOf(note);
55866
- const mentions = (Array.isArray(note.tag) ? note.tag : []).filter((t) => t?.type === "Mention" && t.href).map((t) => ({ href: t.href, name: t.name }));
55867
- recovered.push({
55868
- noteId: note.id,
55869
- actor: urls.actor,
55870
- content: note.content || "",
55871
- published: note.published || null,
55872
- ...note.inReplyTo ? { inReplyTo: note.inReplyTo } : {},
55873
- kind: "post",
55874
- slug: note.id.slice(urls.notes.length),
55875
- ...attachments.length ? { attachments } : {},
55876
- ...mentions.length ? { mentions } : {},
55877
- recovered: true
55878
- });
55879
- }
55880
- const merged = [...statuses, ...recovered];
55881
- let reblogs = 0;
55882
- for (const act of boosts) {
55883
- const object = typeof act.object === "string" ? act.object : act.object?.id;
55884
- const s = object && merged.find((x) => x.noteId === object);
55885
- if (!s || s.reblogged) continue;
55886
- s.reblogged = true;
55887
- s.announceActivity = act;
55888
- reblogs++;
55889
- }
55890
- if (!recovered.length && !reblogs) return { indexed: ids.size, recovered: 0, reblogs: 0, landed: true };
55891
- merged.sort((a, b) => String(b.published || "").localeCompare(String(a.published || "")));
55892
- const kept = merged.slice(0, 1e3);
55893
- publisher.store.write("statuses.json", kept);
55894
- const landed = await publisher.store.commit();
55895
- publisher.log(`rebuilt ${recovered.length} post(s) and ${reblogs} boost(s) from the pod${landed ? "" : " \u2014 THE STATE WRITE DID NOT LAND"}`);
55896
- return {
55897
- indexed: ids.size,
55898
- recovered: recovered.length,
55899
- reblogs,
55900
- landed,
55901
- dropped: Math.max(0, merged.length - kept.length)
55902
- };
55903
- }
55904
-
55905
55892
  // lib/core/publisher/notes.mjs
55906
55893
  init_node_crypto();
55907
55894
  init_wire();
@@ -55965,7 +55952,21 @@ function assertDirectAddressed(content, mentions) {
55965
55952
  e.code = "unaddressed";
55966
55953
  throw e;
55967
55954
  }
55968
- async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null } = {}) {
55955
+ var SLUG_OK = /^[A-Za-z0-9._-]{1,64}$/u;
55956
+ var safeSlug = (s) => typeof s === "string" && SLUG_OK.test(s) && !/^\.+$/u.test(s) ? s : null;
55957
+ async function slugFor(publisher, container, wanted, published) {
55958
+ const name = safeSlug(wanted);
55959
+ if (name && !await read(publisher.remote, container + name).catch(() => null)) return name;
55960
+ return published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
55961
+ }
55962
+ function rowContent(obj) {
55963
+ if (typeof obj?.content === "string" && obj.content.trim()) return sanitizeHtml(obj.content);
55964
+ const plain = [obj?.bodyValue, obj?.name, obj?.summary].find((v) => typeof v === "string" && v.trim());
55965
+ if (plain) return contentHtml(plain);
55966
+ const esc = (v) => String(v).replace(/[&<>"]/gu, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
55967
+ return `<p><a href="${esc(obj?.id || "")}">${esc(obj?.type || "object")}</a></p>`;
55968
+ }
55969
+ async function publishNote(publisher, content, { inReplyTo, attachments, visibility = "public", spoilerText = null, slug: wanted = null } = {}) {
55969
55970
  const { urls } = publisher;
55970
55971
  const priv = visibility === "private" || visibility === "direct";
55971
55972
  if (priv) {
@@ -55973,7 +55974,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
55973
55974
  if (ready !== true) throw new Error(ready);
55974
55975
  }
55975
55976
  const published = (/* @__PURE__ */ new Date()).toISOString();
55976
- const slug = published.slice(0, 10) + "-" + node_crypto_default.randomBytes(4).toString("hex");
55977
+ const slug = await slugFor(publisher, priv ? urls.privateNotes : urls.notes, wanted, published);
55977
55978
  const mentions = await publisher._mentionsFor(content, inReplyTo);
55978
55979
  if (visibility === "direct") assertDirectAddressed(content, mentions);
55979
55980
  const note = noteDoc({
@@ -56009,6 +56010,7 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56009
56010
  ...attachments?.length ? { attachments } : {},
56010
56011
  ...note.tag?.length ? { mentions: note.tag.map((t) => ({ href: t.href, name: t.name })) } : {}
56011
56012
  });
56013
+ if (await publisher.store.commit?.() === false) publisher.log(`post published but its timeline row was refused: ${note.id}`);
56012
56014
  const create = createActivity(note, urls);
56013
56015
  await writeCreate(publisher.remote, create.id, create);
56014
56016
  const contacts = publisher.store.getContacts();
@@ -56033,6 +56035,57 @@ async function publishNote(publisher, content, { inReplyTo, attachments, visibil
56033
56035
  }
56034
56036
  return note;
56035
56037
  }
56038
+ async function publishObject(publisher, object, { visibility = "public", slug: wanted = null } = {}) {
56039
+ const { urls } = publisher;
56040
+ const priv = visibility === "private" || visibility === "direct";
56041
+ if (priv) {
56042
+ const ready = await publisher.privateReady();
56043
+ if (ready !== true) throw new Error(ready);
56044
+ }
56045
+ const published = (/* @__PURE__ */ new Date()).toISOString();
56046
+ const container = priv ? urls.privateNotes : urls.notes;
56047
+ const pod = publisher.config?.remotePod || urls.base;
56048
+ const ownId = typeof object.id === "string" && /^https?:\/\//u.test(object.id) && object.id.startsWith(pod) ? object.id : null;
56049
+ const name = await slugFor(publisher, container, wanted, published);
56050
+ const addressed = addressing(urls, visibility);
56051
+ let doc = null;
56052
+ const id = ownId || container + name;
56053
+ if (!ownId) {
56054
+ doc = { ...object, id, attributedTo: urls.actor, published: object.published || published, to: addressed.to, cc: addressed.cc };
56055
+ if (!doc["@context"]) doc["@context"] = AS_CTX;
56056
+ if (typeof doc.content === "string") doc.content = sanitizeHtml(doc.content);
56057
+ await write4(publisher.remote, id, doc);
56058
+ }
56059
+ if (!priv) await publisher.recordOutbox(id);
56060
+ const row = doc || object;
56061
+ publisher.store.addStatus({
56062
+ noteId: id,
56063
+ actor: urls.actor,
56064
+ content: rowContent(row),
56065
+ published: row.published || published,
56066
+ kind: "post",
56067
+ slug: name,
56068
+ visibility
56069
+ });
56070
+ if (await publisher.store.commit?.() === false) publisher.log(`object published but its timeline row was refused: ${id}`);
56071
+ const createId = doc ? createActivityId(id) : createActivityId(container + name);
56072
+ const create = {
56073
+ "@context": AS_CTX,
56074
+ id: createId,
56075
+ type: "Create",
56076
+ actor: urls.actor,
56077
+ published: row.published || published,
56078
+ to: addressed.to,
56079
+ cc: addressed.cc,
56080
+ object: doc || id
56081
+ };
56082
+ await writeCreate(publisher.remote, create.id, create);
56083
+ const contacts = publisher.store.getContacts();
56084
+ const inboxes = visibility === "direct" ? [] : [...new Set(contacts.followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
56085
+ await publisher.deliverer.deliverToAll(inboxes, create);
56086
+ publisher.log(`${row.type || "object"} published: ${id} \u2192 ${inboxes.length} inbox(es)`);
56087
+ return { id, createId, copied: !ownId };
56088
+ }
56036
56089
  async function updateNote(publisher, s, { content, spoilerText = null, attachments = null } = {}) {
56037
56090
  const { urls } = publisher;
56038
56091
  const updated = (/* @__PURE__ */ new Date()).toISOString();
@@ -56084,6 +56137,158 @@ async function updateNote(publisher, s, { content, spoilerText = null, attachmen
56084
56137
  return patched;
56085
56138
  }
56086
56139
 
56140
+ // lib/core/publisher/restore.mjs
56141
+ var ACCEPT_AP = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
56142
+ var REBUILD_MAX_PER_RUN = 200;
56143
+ async function reconcileFollowers(publisher, contacts) {
56144
+ let published = [];
56145
+ try {
56146
+ published = await publisher.readPublishedFollowers() || [];
56147
+ } catch {
56148
+ return 0;
56149
+ }
56150
+ const known2 = new Set(contacts.followers.map((f) => f.actor));
56151
+ const removed = new Set((contacts.removedFollowers || []).map((r) => r.actor));
56152
+ const missing = published.filter((a) => typeof a === "string" && !known2.has(a) && !removed.has(a));
56153
+ if (!missing.length) return 0;
56154
+ let recovered = 0;
56155
+ for (const actor of missing.slice(0, 200)) {
56156
+ try {
56157
+ const res = await publisher.deliverer.signedFetch(actor, { headers: { accept: ACCEPT_AP } });
56158
+ if (!res.ok) continue;
56159
+ const { doc, view, degraded } = await readLenient(await res.json());
56160
+ const actorDoc2 = view ?? doc;
56161
+ if (degraded) publisher.log?.(`actor ${actor} grounded to read: ${degraded}`);
56162
+ if (!actorDoc2?.inbox) continue;
56163
+ contacts.followers.push({
56164
+ actor,
56165
+ inbox: actorDoc2.inbox,
56166
+ sharedInbox: actorDoc2.endpoints?.sharedInbox || null,
56167
+ recovered: true,
56168
+ // Said explicitly, because onUndo reads it: the pod publishes WHO
56169
+ // follows, never the id of the Follow that did it, so a recovered
56170
+ // record has nothing an Undo can be matched against and must not be
56171
+ // evictable by one naming anything at all.
56172
+ followId: null
56173
+ });
56174
+ recovered++;
56175
+ } catch {
56176
+ }
56177
+ }
56178
+ if (recovered) {
56179
+ publisher.store.setContacts(contacts);
56180
+ publisher.log(`reconciled ${recovered} follower(s) the pod knew about and this machine did not \u2014 a restored or copied state was behind`);
56181
+ }
56182
+ return recovered;
56183
+ }
56184
+ async function reconcileOutbox(publisher, outbox) {
56185
+ let published = [];
56186
+ try {
56187
+ published = await publisher.readPublishedOutbox() || [];
56188
+ } catch {
56189
+ return 0;
56190
+ }
56191
+ if (!Array.isArray(published) || !published.length) return 0;
56192
+ const idOf2 = (i) => typeof i === "string" ? i : i?.id || null;
56193
+ const known2 = new Set(outbox.map(idOf2).filter(Boolean));
56194
+ const removed = new Set(publisher.store.read("outbox-removed.json", []).map((r) => r.id));
56195
+ const missing = published.filter((i) => {
56196
+ const id = idOf2(i);
56197
+ return id && !known2.has(id) && !removed.has(id);
56198
+ });
56199
+ if (!missing.length) return 0;
56200
+ outbox.push(...missing);
56201
+ publisher.store.write("outbox.json", outbox);
56202
+ publisher.log(`reconciled ${missing.length} outbox entr(ies) the pod carried and this machine did not`);
56203
+ return missing.length;
56204
+ }
56205
+ async function rebuildStatuses(publisher, { fromNotes = false } = {}) {
56206
+ const { urls } = publisher;
56207
+ const ids = /* @__PURE__ */ new Set();
56208
+ const boosts = [];
56209
+ let indexed = false;
56210
+ const published = await publisher.readPublishedOutbox().catch(() => null);
56211
+ if (published) {
56212
+ indexed = true;
56213
+ for (const item of published) {
56214
+ if (typeof item === "string") {
56215
+ if (item.startsWith(urls.notes)) ids.add(item);
56216
+ } else if (item?.type === "Announce") boosts.push(item);
56217
+ }
56218
+ }
56219
+ if (fromNotes) {
56220
+ for (const child of await list2(publisher.remote, urls).catch(() => [])) {
56221
+ ids.add(child.url);
56222
+ }
56223
+ indexed = true;
56224
+ }
56225
+ if (!indexed) return { indexed: 0, recovered: 0, reblogs: 0, landed: false, why: "the pod would not answer for its outbox" };
56226
+ const statuses = publisher.store.getStatuses();
56227
+ const have = new Set(statuses.map((s) => s.noteId));
56228
+ const removed = new Set(publisher.store.read("outbox-removed.json", []).map((r) => r.id));
56229
+ const recovered = [];
56230
+ let budget = REBUILD_MAX_PER_RUN;
56231
+ for (const id of ids) {
56232
+ if (budget <= 0) {
56233
+ publisher.log(`rebuild: stopping at ${REBUILD_MAX_PER_RUN} this run \u2014 run it again for the rest`);
56234
+ break;
56235
+ }
56236
+ if (have.has(id) || removed.has(id)) continue;
56237
+ budget--;
56238
+ const note = await read(publisher.remote, id).catch(() => null);
56239
+ if (!note?.type || note.id !== id || note.attributedTo !== urls.actor) continue;
56240
+ const attachments = attachmentsOf(note);
56241
+ const mentions = (Array.isArray(note.tag) ? note.tag : []).filter((t) => t?.type === "Mention" && t.href).map((t) => ({ href: t.href, name: t.name }));
56242
+ recovered.push({
56243
+ noteId: note.id,
56244
+ actor: urls.actor,
56245
+ content: rowContent(note),
56246
+ published: note.published || null,
56247
+ ...note.inReplyTo ? { inReplyTo: note.inReplyTo } : {},
56248
+ kind: "post",
56249
+ slug: note.id.slice(urls.notes.length),
56250
+ ...attachments.length ? { attachments } : {},
56251
+ ...mentions.length ? { mentions } : {},
56252
+ recovered: true
56253
+ });
56254
+ }
56255
+ const merged = [...statuses, ...recovered];
56256
+ let reblogs = 0;
56257
+ for (const act of boosts) {
56258
+ const object = typeof act.object === "string" ? act.object : act.object?.id;
56259
+ const s = object && merged.find((x) => x.noteId === object);
56260
+ if (!s || s.reblogged) continue;
56261
+ s.reblogged = true;
56262
+ s.announceActivity = act;
56263
+ reblogs++;
56264
+ }
56265
+ if (!recovered.length && !reblogs) return { indexed: ids.size, recovered: 0, reblogs: 0, landed: true };
56266
+ merged.sort((a, b) => String(b.published || "").localeCompare(String(a.published || "")));
56267
+ const kept = merged.slice(0, 1e3);
56268
+ publisher.store.write("statuses.json", kept);
56269
+ const landed = await publisher.store.commit();
56270
+ publisher.log(`rebuilt ${recovered.length} post(s) and ${reblogs} boost(s) from the pod${landed ? "" : " \u2014 THE STATE WRITE DID NOT LAND"}`);
56271
+ return {
56272
+ indexed: ids.size,
56273
+ recovered: recovered.length,
56274
+ reblogs,
56275
+ landed,
56276
+ dropped: Math.max(0, merged.length - kept.length)
56277
+ };
56278
+ }
56279
+ async function healStatuses(publisher) {
56280
+ const { urls, store } = publisher;
56281
+ const own = store.read("outbox.json", []).map((i) => typeof i === "string" ? i : null).filter((id) => id && id.startsWith(urls.notes));
56282
+ if (!own.length) return { missing: 0, recovered: 0 };
56283
+ const have = new Set(store.getStatuses().map((s) => s.noteId));
56284
+ const removed = new Set(store.read("outbox-removed.json", []).map((r2) => r2.id));
56285
+ const missing = own.filter((id) => !have.has(id) && !removed.has(id));
56286
+ if (!missing.length) return { missing: 0, recovered: 0 };
56287
+ const r = await rebuildStatuses(publisher);
56288
+ publisher.log(`timeline index healed: ${missing.length} own post(s) were missing, ${r.recovered} recovered`);
56289
+ return { missing: missing.length, recovered: r.recovered };
56290
+ }
56291
+
56087
56292
  // lib/core/publisher/questions.mjs
56088
56293
  init_node_crypto();
56089
56294
  init_wire();
@@ -56225,6 +56430,7 @@ async function publishQuestion(publisher, content, {
56225
56430
  ...spoilerText ? { spoiler: spoilerText } : {},
56226
56431
  ...question.tag?.length ? { mentions: question.tag.map((t) => ({ href: t.href, name: t.name })) } : {}
56227
56432
  });
56433
+ if (await publisher.store.commit?.() === false) publisher.log(`poll published but its timeline row was refused: ${question.id}`);
56228
56434
  const create = publisher._pollActivity("Create", question, createActivityId(question.id));
56229
56435
  await writeCreate(publisher.remote, create.id, create);
56230
56436
  const contacts = publisher.store.getContacts();
@@ -56405,6 +56611,14 @@ var Publisher = class {
56405
56611
  // republish button. Without it, an actor lost from the pod would match the
56406
56612
  // digest, be skipped, and leave the agent reporting success while nobody can
56407
56613
  // resolve it.
56614
+ // The Gateway's outbox door for this account, when a Gateway is attached:
56615
+ // beside its inbox door, or under the fronted actor.
56616
+ gatewayOutbox() {
56617
+ const gw = this.config.gateway;
56618
+ if (!(gw && gw.url && gw.mode && gw.mode !== "off")) return null;
56619
+ if (gw.frontActor) return String(gw.frontActor).replace(/ap\/actor\/?$/u, "ap/outbox");
56620
+ return String(gw.url).replace(/ap\/inbox\/?$/u, "ap/outbox");
56621
+ }
56408
56622
  async publishProfile({ force = false } = {}) {
56409
56623
  const { urls } = this;
56410
56624
  const host = new URL(urls.base).host;
@@ -56434,7 +56648,8 @@ var Publisher = class {
56434
56648
  inbox: gwActive ? gw.url : null,
56435
56649
  // The agent's own outbox endpoint, where it is reachable: a client
56436
56650
  // following the actor must arrive somewhere that will take a write.
56437
- outbox: this.clientOrigin ? `${this.clientOrigin}ap/outbox` : null,
56651
+ // Otherwise the Gateway's outbox door, when one is attached.
56652
+ outbox: this.clientOrigin ? `${this.clientOrigin}ap/outbox` : this.gatewayOutbox(),
56438
56653
  // How a client-to-server client finds the way in with nothing configured
56439
56654
  // by hand. Advertised only where the surface is publicly reachable.
56440
56655
  oauthAuthorize: this.clientOrigin ? `${this.clientOrigin}oauth/authorize` : null,
@@ -56485,7 +56700,9 @@ var Publisher = class {
56485
56700
  const wrote = await linkInWebIdProfile(this.remote, {
56486
56701
  actorUrl: urls.actor,
56487
56702
  accountName: `@${this.config.handle}@${host}`,
56488
- kind: this.config.kind
56703
+ kind: this.config.kind,
56704
+ // Where a Solid client posts: dokieli reads `as:outbox` off the WebID.
56705
+ outbox: this.clientOrigin ? `${this.clientOrigin}ap/outbox` : this.gatewayOutbox()
56489
56706
  });
56490
56707
  if (wrote) this.log("WebID profile now lists the actor as a foaf:account");
56491
56708
  } catch (e) {
@@ -56732,6 +56949,9 @@ var Publisher = class {
56732
56949
  rebuildStatuses(...a) {
56733
56950
  return rebuildStatuses(this, ...a);
56734
56951
  }
56952
+ healStatuses(...a) {
56953
+ return healStatuses(this, ...a);
56954
+ }
56735
56955
  // notes.mjs
56736
56956
  ensureMediaContainer(...a) {
56737
56957
  return ensureMediaContainer(this, ...a);
@@ -56751,6 +56971,9 @@ var Publisher = class {
56751
56971
  publishNote(...a) {
56752
56972
  return publishNote(this, ...a);
56753
56973
  }
56974
+ publishObject(...a) {
56975
+ return publishObject(this, ...a);
56976
+ }
56754
56977
  updateNote(...a) {
56755
56978
  return updateNote(this, ...a);
56756
56979
  }
@@ -56833,13 +57056,13 @@ var MAX_FORWARDS_PER_DRAIN = 20;
56833
57056
  var ACCEPT_AP2 = 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
56834
57057
  function trimActivity(a) {
56835
57058
  if (!a || typeof a !== "object") return a ?? null;
56836
- const idOf = (v) => typeof v === "string" ? v : v?.id ?? null;
57059
+ const idOf2 = (v) => typeof v === "string" ? v : v?.id ?? null;
56837
57060
  const out = {};
56838
57061
  for (const k of ["id", "type", "actor", "target"]) {
56839
- const v = idOf(a[k]);
57062
+ const v = idOf2(a[k]);
56840
57063
  if (v) out[k] = String(v).slice(0, 2048);
56841
57064
  }
56842
- const obj = idOf(a.object);
57065
+ const obj = idOf2(a.object);
56843
57066
  if (obj) out.object = String(obj).slice(0, 2048);
56844
57067
  else if (a.object && typeof a.object === "object" && a.object.type) {
56845
57068
  out.object = { type: String(a.object.type).slice(0, 64) };
@@ -60814,8 +61037,8 @@ function walkPath(graph2, subject, path, visited = new node_set_default()) {
60814
61037
  pathValues.addAll(deeperValues);
60815
61038
  return pathValues;
60816
61039
  }
60817
- function flatMap(arr, func) {
60818
- return [...arr].reduce((acc, x) => acc.concat(func(x)), []);
61040
+ function flatMap(arr2, func) {
61041
+ return [...arr2].reduce((acc, x) => acc.concat(func(x)), []);
60819
61042
  }
60820
61043
 
60821
61044
  // node_modules/rdf-validate-shacl/src/dataset-utils.js
@@ -62764,8 +62987,10 @@ var ATTEMPTS_TTL_MS = 7 * 24 * 60 * 6e4;
62764
62987
  var MAX_ITEM_ATTEMPTS = 5;
62765
62988
  var MAX_ITEMS_PER_DRAIN = 50;
62766
62989
  var Intake = class {
62767
- constructor({ config, urls, remote, store, deliverer, publisher, log: log2 = console.log, lease = null, archive = null, push = true, pollSeconds = null }) {
62768
- Object.assign(this, { config, urls, remote, store, deliverer, publisher, log: log2, lease, archive, push, pollSeconds });
62990
+ // `ownerPost(activity, { raw, slug })` is the client-to-server dispatcher,
62991
+ // for an item the Gateway's outbox door took on the owner's behalf.
62992
+ constructor({ config, urls, remote, store, deliverer, publisher, log: log2 = console.log, lease = null, archive = null, push = true, pollSeconds = null, ownerPost = null }) {
62993
+ Object.assign(this, { config, urls, remote, store, deliverer, publisher, log: log2, lease, archive, push, pollSeconds, ownerPost });
62769
62994
  this.serial = Date.now();
62770
62995
  this.stopped = false;
62771
62996
  this.lastDrain = null;
@@ -63073,9 +63298,10 @@ var Intake = class {
63073
63298
  }
63074
63299
  const receipt = activity ? await this._readReceipt(url) : null;
63075
63300
  if (activity && this.gatewaySecret()) this._bumpGatewayStat(!!receipt?.verified);
63076
- const rejection = activity ? await this.handle(activity, receipt) : "unparsable JSON";
63077
- if (!rejection && raw) await this._archive(url, raw, activity);
63078
- if (!rejection) await this._maybeForward(activity);
63301
+ const owned = activity && this.isOwnerPost(receipt);
63302
+ const rejection = !activity ? "unparsable JSON" : owned ? await this.ownerPostFrom(activity, raw, receipt) : await this.handle(activity, receipt);
63303
+ if (!rejection && raw && !owned) await this._archive(url, raw, activity);
63304
+ if (!rejection && !owned) await this._maybeForward(activity);
63079
63305
  if (rejection) {
63080
63306
  this.store.addDeadLetter({
63081
63307
  inboxUrl: url,
@@ -63223,6 +63449,28 @@ var Intake = class {
63223
63449
  _readReceipt(...a) {
63224
63450
  return readReceipt(this, ...a);
63225
63451
  }
63452
+ // A receipt the door stamped `c2s` for THIS actor, and only a receipt whose
63453
+ // HMAC verified (readReceipt returns nothing else). A stranger appending an
63454
+ // item that claims to be ours has no such receipt and is read as mail.
63455
+ isOwnerPost(receipt) {
63456
+ return !!(this.ownerPost && receipt?.verified && receipt.method === "c2s" && receipt.actor === this.urls.actor);
63457
+ }
63458
+ // Publish what the owner posted at the door. The bytes as the client wrote
63459
+ // them go to the dispatcher (an object stored as sent must be the client's
63460
+ // document, not our reading of it); the graph is what it decides from.
63461
+ // Returns null when published, else the reason it was not — a dead letter.
63462
+ async ownerPostFrom(activity, raw, receipt) {
63463
+ let asSent = null;
63464
+ try {
63465
+ asSent = raw ? JSON.parse(raw) : null;
63466
+ } catch {
63467
+ asSent = null;
63468
+ }
63469
+ const r = await this.ownerPost(activity, { raw: asSent, slug: receipt.slug || null });
63470
+ if (!r || r.status >= 300) return `owner post refused (${r?.status || "?"}): ${r?.body?.error || ""}`;
63471
+ this.log(`owner post from the outbox door published: ${r.body?.object || r.body?.id || activity.type}`);
63472
+ return null;
63473
+ }
63226
63474
  receiptVouchesFor(...a) {
63227
63475
  return receiptVouchesFor(this, ...a);
63228
63476
  }
@@ -63306,6 +63554,846 @@ var Intake = class {
63306
63554
  }
63307
63555
  };
63308
63556
 
63557
+ // lib/core/social.mjs
63558
+ var social_exports = {};
63559
+ __export(social_exports, {
63560
+ admitRequest: () => admitRequest,
63561
+ announceModeration: () => announceModeration,
63562
+ applyModeration: () => applyModeration,
63563
+ blockActor: () => blockActor,
63564
+ confirmDelegation: () => confirmDelegation,
63565
+ deleteNote: () => deleteNote,
63566
+ ejectFollower: () => ejectFollower,
63567
+ favourite: () => favourite,
63568
+ followActor: () => followActor,
63569
+ followHandle: () => followHandle,
63570
+ lookupWebFinger: () => lookupWebFinger,
63571
+ pinStatus: () => pinStatus,
63572
+ reblog: () => reblog,
63573
+ refuseRequest: () => refuseRequest,
63574
+ resolveHandle: () => resolveHandle,
63575
+ retractAnnouncement: () => retractAnnouncement,
63576
+ selfLink: () => selfLink,
63577
+ unblockActor: () => unblockActor,
63578
+ unfavourite: () => unfavourite,
63579
+ unfollowActor: () => unfollowActor,
63580
+ unreblog: () => unreblog,
63581
+ votePoll: () => votePoll
63582
+ });
63583
+ init_node_crypto();
63584
+ init_wire();
63585
+ async function lookupWebFinger(acct, { fetchImpl = null } = {}) {
63586
+ const clean = String(acct || "").replace(/^acct:/, "");
63587
+ const at = clean.lastIndexOf("@");
63588
+ if (at < 1) return null;
63589
+ const [user, host] = [clean.slice(0, at), clean.slice(at + 1)];
63590
+ if (!host || /[/\\?#]/.test(host)) return null;
63591
+ const { safeFetch: safeFetch2, readCapped: readCapped3 } = await Promise.resolve().then(() => (init_safefetch(), safefetch_exports));
63592
+ const url = `https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${clean}`)}`;
63593
+ const headers = { accept: "application/jrd+json, application/json" };
63594
+ const res = await (fetchImpl ? fetchImpl(url, { headers }) : safeFetch2(url, { headers })).catch(() => null);
63595
+ if (!res || res.status >= 400) return null;
63596
+ let jrd2;
63597
+ try {
63598
+ jrd2 = JSON.parse(await readCapped3(res, 256 * 1024));
63599
+ } catch {
63600
+ return null;
63601
+ }
63602
+ const subject = String(jrd2?.subject || "").replace(/^acct:/, "").toLowerCase();
63603
+ if (subject && subject !== clean.toLowerCase()) {
63604
+ return null;
63605
+ }
63606
+ return jrd2;
63607
+ }
63608
+ var selfLink = (jrd2) => jrd2?.links?.find((l) => l.rel === "self" && /application\/(activity\+json|ld\+json)/.test(l.type || ""));
63609
+ async function confirmDelegation(asked, doc, lookup2 = lookupWebFinger) {
63610
+ let home;
63611
+ try {
63612
+ home = new URL(doc.id).host.toLowerCase();
63613
+ } catch {
63614
+ throw new Error(`actor id is not a URL (${doc.id})`);
63615
+ }
63616
+ if (home === asked.toLowerCase()) return;
63617
+ const user = doc.preferredUsername;
63618
+ if (!user) {
63619
+ throw new Error(`${asked} points at ${doc.id} on another host, which names no username to confirm it by`);
63620
+ }
63621
+ const back = await lookup2(`acct:${user}@${home}`);
63622
+ if (selfLink(back)?.href !== doc.id) {
63623
+ throw new Error(`${asked} claims ${doc.id}, but ${home} does not agree that is @${user}@${home} \u2014 refusing to take one host's word for another's actor`);
63624
+ }
63625
+ }
63626
+ async function resolveHandle(agent2, handle7) {
63627
+ const clean = String(handle7 || "").replace(/^@/, "");
63628
+ if (!/^[^@]+@[^@]+$/.test(clean)) throw new Error("handle must look like user@host");
63629
+ if (agent2.store.isBlocked("https://" + clean.split("@")[1] + "/")) throw new Error("domain is blocked");
63630
+ const remote = agent2.intake?.deliverer?.signedFetch ? (u, i) => agent2.intake.deliverer.signedFetch(u, i) : null;
63631
+ const lookup2 = (a) => lookupWebFinger(a, { fetchImpl: remote });
63632
+ const jrd2 = await lookup2("acct:" + clean);
63633
+ const self2 = selfLink(jrd2);
63634
+ if (!self2?.href) throw new Error(`webfinger found no actor for ${clean}`);
63635
+ const doc = await agent2.intake.fetchAP(self2.href);
63636
+ if (!doc?.id) throw new Error(`actor document unusable for ${clean}`);
63637
+ if (agent2.store.isBlocked(doc.id)) throw new Error("actor is blocked");
63638
+ await confirmDelegation(clean.slice(clean.lastIndexOf("@") + 1), doc, lookup2);
63639
+ await cacheCounts(agent2, doc);
63640
+ return doc;
63641
+ }
63642
+ async function cacheCounts(agent2, doc) {
63643
+ const total = async (url) => {
63644
+ if (!url) return null;
63645
+ try {
63646
+ const res = await agent2.intake.fetchAP(url);
63647
+ return Number.isInteger(res?.totalItems) ? res.totalItems : null;
63648
+ } catch {
63649
+ return null;
63650
+ }
63651
+ };
63652
+ const [followers, following] = await Promise.all([total(doc.followers), total(doc.following)]);
63653
+ if (followers === null && following === null) return;
63654
+ const actors = agent2.store.getActors();
63655
+ if (!actors[doc.id]) return;
63656
+ actors[doc.id] = { ...actors[doc.id], counts: { followers, following } };
63657
+ agent2.store.write("actors.json", actors);
63658
+ }
63659
+ async function followActor(agent2, actorUrl, { publish = true, doc: fetched = null } = {}) {
63660
+ const doc = fetched || await agent2.intake.fetchAP(actorUrl);
63661
+ if (!doc?.inbox) throw new Error(`actor document unusable (${actorUrl})`);
63662
+ if (agent2.store.isBlocked(doc.id)) throw new Error("actor is blocked");
63663
+ const contacts = agent2.store.getContacts();
63664
+ let rec = contacts.following.find((f) => f.actor === doc.id);
63665
+ if (!rec) {
63666
+ rec = { actor: doc.id, inbox: doc.inbox, accepted: false };
63667
+ contacts.following.push(rec);
63668
+ }
63669
+ const follow = followActivity({ urls: agent2.publisher.urls, targetActor: doc.id, serial: Date.now() });
63670
+ rec.followActivity = follow;
63671
+ agent2.store.setContacts(contacts);
63672
+ await agent2.deliverer.deliver(doc.inbox, follow);
63673
+ if (publish) await agent2.publisher.publishCollections({ following: true, pending: true });
63674
+ return doc;
63675
+ }
63676
+ async function followHandle(agent2, handle7) {
63677
+ const doc = await resolveHandle(agent2, handle7);
63678
+ await followActor(agent2, doc.id);
63679
+ const clean = String(handle7 || "").replace(/^@/, "");
63680
+ const contacts = agent2.store.getContacts();
63681
+ const rec = contacts.following.find((f) => f.actor === doc.id);
63682
+ if (rec && !rec.handle) {
63683
+ rec.handle = clean;
63684
+ agent2.store.setContacts(contacts);
63685
+ }
63686
+ return { ok: true, actor: doc.id };
63687
+ }
63688
+ async function unfollowActor(agent2, actor) {
63689
+ const contacts = agent2.store.getContacts();
63690
+ const rec = contacts.following.find((f) => f.actor === actor);
63691
+ if (!rec) throw new Error("not following that actor");
63692
+ if (rec.followActivity) {
63693
+ await agent2.deliverer.deliver(
63694
+ rec.inbox,
63695
+ undoActivity({ urls: agent2.publisher.urls, activity: rec.followActivity, serial: Date.now() })
63696
+ );
63697
+ }
63698
+ contacts.following = contacts.following.filter((f) => f.actor !== actor);
63699
+ agent2.store.setContacts(contacts);
63700
+ await agent2.publisher.publishCollections({ following: true, pending: true });
63701
+ return { ok: true };
63702
+ }
63703
+ async function favourite(agent2, s) {
63704
+ if (s.favourited) return s;
63705
+ const doc = await agent2.intake.fetchAP(s.actor);
63706
+ if (!doc?.inbox) throw new Error("author inbox unavailable");
63707
+ const act = likeActivity({ urls: agent2.publisher.urls, noteId: s.noteId, serial: Date.now() });
63708
+ await agent2.deliverer.deliver(doc.endpoints?.sharedInbox || doc.inbox, act);
63709
+ return agent2.store.updateStatus(s.noteId, { favourited: true, likeActivity: act });
63710
+ }
63711
+ async function unfavourite(agent2, s) {
63712
+ if (s.likeActivity) {
63713
+ const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
63714
+ if (doc?.inbox) {
63715
+ await agent2.deliverer.deliver(
63716
+ doc.endpoints?.sharedInbox || doc.inbox,
63717
+ undoActivity({ urls: agent2.publisher.urls, activity: s.likeActivity, serial: Date.now() })
63718
+ );
63719
+ }
63720
+ }
63721
+ return agent2.store.updateStatus(s.noteId, { favourited: false, likeActivity: void 0 });
63722
+ }
63723
+ async function reblog(agent2, s) {
63724
+ if (s.reblogged) return s;
63725
+ const { urls } = agent2.publisher;
63726
+ const act = announceActivity({ urls, object: s.noteId, serial: Date.now() });
63727
+ const inboxes = new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean));
63728
+ if (s.actor !== urls.actor) {
63729
+ const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
63730
+ if (doc?.inbox) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
63731
+ }
63732
+ await agent2.deliverer.deliverToAll([...inboxes], act);
63733
+ const updated = agent2.store.updateStatus(s.noteId, { reblogged: true, announceActivity: act });
63734
+ await agent2.publisher.recordOutbox(act);
63735
+ if (await agent2.store.commit?.() === false) agent2.log(`boost sent but its timeline row was refused: ${s.noteId}`);
63736
+ return updated;
63737
+ }
63738
+ async function unreblog(agent2, s) {
63739
+ if (s.announceActivity) {
63740
+ const undo = undoActivity({ urls: agent2.publisher.urls, activity: s.announceActivity, serial: Date.now() });
63741
+ const inboxes = new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean));
63742
+ const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
63743
+ if (doc?.inbox && s.actor !== agent2.publisher.urls.actor) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
63744
+ await agent2.deliverer.deliverToAll([...inboxes], undo);
63745
+ await agent2.publisher.unrecordOutbox((i) => i?.id === s.announceActivity.id);
63746
+ }
63747
+ return agent2.store.updateStatus(s.noteId, { reblogged: false, announceActivity: void 0 });
63748
+ }
63749
+ async function ejectFollower(agent2, actor) {
63750
+ const { urls } = agent2.publisher;
63751
+ const contacts = agent2.store.getContacts();
63752
+ const rec = contacts.followers.find((f) => f.actor === actor);
63753
+ if (!rec) throw new Error("not a member");
63754
+ dropFollower(contacts, actor, "ejected");
63755
+ agent2.store.setContacts(contacts);
63756
+ const muted = agent2.store.getMuted();
63757
+ muted.actors = [.../* @__PURE__ */ new Set([...muted.actors, actor])];
63758
+ agent2.store.setMuted(muted);
63759
+ await agent2.publisher.publishCollections({ followers: true });
63760
+ await announceModeration(
63761
+ agent2,
63762
+ blockActivity({ urls, targetActor: actor, serial: Date.now() })
63763
+ ).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
63764
+ if (rec.bsky) {
63765
+ await agent2.bskygroup?.blockDid(rec.bsky.did).catch((e) => agent2.log?.(`bluesky block failed for ${rec.bsky.handle}: ${e.message}`));
63766
+ return { ok: true, actor, told: true };
63767
+ }
63768
+ if (rec.inbox) {
63769
+ await agent2.deliverer.deliver(rec.inbox, rejectActivity({
63770
+ urls,
63771
+ followActivity: { id: rec.followId, type: "Follow", actor, object: urls.actor },
63772
+ serial: Date.now()
63773
+ }));
63774
+ }
63775
+ return { ok: true, actor, told: !!rec.inbox };
63776
+ }
63777
+ async function admitRequest(agent2, actor, { publish = true } = {}) {
63778
+ const reqs = agent2.store.getRequests();
63779
+ const req = reqs.find((r) => r.actor === actor);
63780
+ if (!req) throw new Error("no such request");
63781
+ const contacts = agent2.store.getContacts();
63782
+ if (!contacts.followers.some((f) => f.actor === actor)) {
63783
+ const bridgedDid = /^https:\/\/bsky\.brid\.gy\/ap\/(did:[^/]+)$/.exec(actor)?.[1];
63784
+ if (bridgedDid) contacts.followers = contacts.followers.filter((f) => f.bsky?.did !== bridgedDid);
63785
+ contacts.followers.push(req.bsky ? { actor, bsky: req.bsky } : { actor, inbox: req.inbox, sharedInbox: req.sharedInbox, followId: req.activity?.id });
63786
+ agent2.store.setContacts(contacts);
63787
+ agent2.store.addNotification({ type: "follow", actor, ...req.bsky ? { bsky: true } : {} });
63788
+ }
63789
+ agent2.store.setRequests(reqs.filter((r) => r.actor !== actor));
63790
+ if (req.bsky) {
63791
+ await agent2.bskygroup?.nudgeOnce(contacts.followers.find((f) => f.actor === actor));
63792
+ return { ok: true, actor };
63793
+ }
63794
+ if (publish) await agent2.publisher.publishCollections({ followers: true, pending: true });
63795
+ await agent2.deliverer.deliver(req.inbox, acceptActivity({
63796
+ urls: agent2.publisher.urls,
63797
+ followActivity: req.activity,
63798
+ serial: Date.now()
63799
+ }));
63800
+ return { ok: true, actor };
63801
+ }
63802
+ async function refuseRequest(agent2, actor) {
63803
+ const reqs = agent2.store.getRequests();
63804
+ const req = reqs.find((r) => r.actor === actor);
63805
+ if (!req) throw new Error("no such request");
63806
+ agent2.store.setRequests(reqs.filter((r) => r.actor !== actor));
63807
+ await agent2.publisher?.publishCollections({ pending: true }).catch(() => {
63808
+ });
63809
+ if (req.bsky) {
63810
+ await agent2.bskygroup?.blockDid(req.bsky.did).catch((e) => agent2.log?.(`bluesky block failed for ${req.bsky.handle}: ${e.message}`));
63811
+ return { ok: true, actor };
63812
+ }
63813
+ await agent2.deliverer.deliver(req.inbox, rejectActivity({
63814
+ urls: agent2.publisher.urls,
63815
+ followActivity: req.activity,
63816
+ serial: Date.now()
63817
+ }));
63818
+ return { ok: true, actor };
63819
+ }
63820
+ async function retractAnnouncement(agent2, noteId) {
63821
+ return agent2.intake.retract(noteId);
63822
+ }
63823
+ async function votePoll(agent2, s, choices) {
63824
+ const { urls } = agent2.publisher;
63825
+ const poll = s.poll || {};
63826
+ const opts = poll.options || [];
63827
+ if (poll.closed || poll.expiresAt && Date.parse(poll.expiresAt) < Date.now()) {
63828
+ return { ok: false, error: "this poll has closed" };
63829
+ }
63830
+ if (poll.voted) return { ok: false, error: "already voted" };
63831
+ if (s.actor === urls.actor) return { ok: false, error: "this is your own poll" };
63832
+ const picks = [...new Set(choices)].filter((i) => i >= 0 && i < opts.length);
63833
+ if (!picks.length) return { ok: false, error: "invalid choice" };
63834
+ if (!poll.multiple && picks.length > 1) return { ok: false, error: "this poll takes one choice" };
63835
+ const author = await agent2.intake.fetchAP(s.actor).catch(() => null);
63836
+ const inbox = author?.endpoints?.sharedInbox || author?.inbox;
63837
+ if (!inbox) return { ok: false, error: "could not reach the poll author" };
63838
+ const published = (/* @__PURE__ */ new Date()).toISOString();
63839
+ for (const i of picks) {
63840
+ const id = urls.notes + published.slice(0, 10) + "-vote-" + node_crypto_default.randomBytes(4).toString("hex");
63841
+ const note = {
63842
+ "@context": AS_CTX,
63843
+ id,
63844
+ type: "Note",
63845
+ attributedTo: urls.actor,
63846
+ name: opts[i].title,
63847
+ inReplyTo: s.noteId,
63848
+ published,
63849
+ to: [s.actor]
63850
+ };
63851
+ const create = {
63852
+ "@context": AS_CTX,
63853
+ id: createActivityId(id),
63854
+ type: "Create",
63855
+ actor: urls.actor,
63856
+ published,
63857
+ to: [s.actor],
63858
+ object: note
63859
+ };
63860
+ await write4(agent2.remote, id, note);
63861
+ await writeCreate(agent2.remote, create.id, create);
63862
+ await agent2.deliverer.deliverToAll([inbox], create);
63863
+ }
63864
+ agent2.store.updateStatus(s.noteId, {
63865
+ poll: {
63866
+ ...poll,
63867
+ voted: true,
63868
+ ownVotes: picks,
63869
+ options: opts.map((o, j) => picks.includes(j) ? { ...o, votes: (o.votes || 0) + 1 } : o)
63870
+ }
63871
+ });
63872
+ return { ok: true };
63873
+ }
63874
+ async function deleteNote(agent2, s) {
63875
+ const { urls } = agent2.publisher;
63876
+ const inboxes = agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean);
63877
+ await agent2.deliverer.deliverToAll(inboxes, deleteActivity({ urls, noteId: s.noteId }));
63878
+ const stuck = [];
63879
+ const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
63880
+ try {
63881
+ await writeTombstone2(agent2.remote, s.noteId, noteTombstone(s.noteId, deletedAt));
63882
+ } catch {
63883
+ stuck.push(s.noteId);
63884
+ }
63885
+ if (!await dropCreate(agent2.remote, createActivityId(s.noteId))) {
63886
+ stuck.push(createActivityId(s.noteId));
63887
+ }
63888
+ if (stuck.length) {
63889
+ agent2.log?.(`delete ${s.noteId}: the pod kept ${stuck.length} document(s) \u2014 the post is STILL PUBLISHED`);
63890
+ return {
63891
+ ok: false,
63892
+ stillPublished: stuck,
63893
+ error: "the pod would not remove the post \u2014 it is still published, and has been kept here so you can try again"
63894
+ };
63895
+ }
63896
+ await dropReplies(agent2.remote, repliesId(s.noteId));
63897
+ if (s.atproto?.uri && agent2.atproto?.connected()) {
63898
+ await agent2.atproto.deleteCrossPost(s.atproto.uri).then(() => agent2.log?.(`bluesky mirror deleted: ${s.atproto.uri}`)).catch((e) => agent2.log?.(`bluesky mirror not deleted (${e.message}): ${s.atproto.uri}`));
63899
+ }
63900
+ await agent2.publisher.unrecordOutbox((i) => i === s.noteId || i?.id && i.id === s.announceActivity?.id);
63901
+ agent2.store.removeStatus(s.noteId);
63902
+ return { ok: true };
63903
+ }
63904
+ async function announceModeration(agent2, activity) {
63905
+ if (agent2.store.getConfig()?.kind !== "group") return null;
63906
+ const { urls } = agent2.publisher;
63907
+ const act = announceActivity({ urls, object: activity, serial: Date.now(), audience: urls.actor });
63908
+ const inboxes = [...new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
63909
+ await agent2.deliverer.deliverToAll(inboxes, act);
63910
+ await agent2.publisher.recordOutbox(act);
63911
+ return act;
63912
+ }
63913
+ async function blockActor(agent2, actorUrl) {
63914
+ if (!/^https?:\/\/\S+$/.test(String(actorUrl || ""))) throw new Error("actor must be a URL");
63915
+ const b = agent2.store.getBlocklist();
63916
+ if (!b.actors.includes(actorUrl)) b.actors.push(actorUrl);
63917
+ agent2.store.setBlocklist(b);
63918
+ await unfollowActor(agent2, actorUrl).catch(() => {
63919
+ });
63920
+ await agent2.publisher?.publishCollections({ blocked: true }).catch(() => {
63921
+ });
63922
+ await announceModeration(
63923
+ agent2,
63924
+ blockActivity({ urls: agent2.publisher.urls, targetActor: actorUrl, serial: Date.now() })
63925
+ ).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
63926
+ return { ok: true, actor: actorUrl };
63927
+ }
63928
+ async function unblockActor(agent2, actorUrl) {
63929
+ const b = agent2.store.getBlocklist();
63930
+ b.actors = b.actors.filter((a) => a !== actorUrl);
63931
+ agent2.store.setBlocklist(b);
63932
+ await agent2.publisher?.publishCollections({ blocked: true }).catch(() => {
63933
+ });
63934
+ await announceModeration(agent2, undoActivity({
63935
+ urls: agent2.publisher.urls,
63936
+ activity: { type: "Block", actor: agent2.publisher.urls.actor, object: actorUrl },
63937
+ serial: Date.now()
63938
+ })).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
63939
+ return { ok: true, actor: actorUrl };
63940
+ }
63941
+ async function applyModeration(agent2, entry) {
63942
+ const { urls } = agent2.publisher;
63943
+ const target = typeof entry.activity?.object === "string" ? entry.activity.object : entry.activity?.object?.id;
63944
+ switch (entry.type) {
63945
+ case "Block": {
63946
+ if (!target) throw new Error("the queued Block names nobody");
63947
+ const member = agent2.store.getContacts().followers.some((f) => f.actor === target);
63948
+ return member ? ejectFollower(agent2, target) : blockActor(agent2, target);
63949
+ }
63950
+ case "Undo": {
63951
+ const inner = entry.activity?.object;
63952
+ const unbanned = typeof inner === "object" && inner?.type === "Block" ? typeof inner.object === "string" ? inner.object : inner.object?.id : null;
63953
+ if (!unbanned) throw new Error("the queued Undo does not name a Block");
63954
+ const muted = agent2.store.getMuted();
63955
+ muted.actors = muted.actors.filter((a) => a !== unbanned);
63956
+ agent2.store.setMuted(muted);
63957
+ return unblockActor(agent2, unbanned);
63958
+ }
63959
+ case "Delete": {
63960
+ if (!target) throw new Error("the queued Delete names nothing");
63961
+ return agent2.intake.retract(target);
63962
+ }
63963
+ case "Add":
63964
+ case "Remove": {
63965
+ if (!target) throw new Error("the queued roster change names nobody");
63966
+ const cfg = { ...agent2.store.getConfig() };
63967
+ const prev = cfg.moderators || [];
63968
+ cfg.moderators = entry.type === "Add" ? [.../* @__PURE__ */ new Set([...prev, target])] : prev.filter((m) => m !== target);
63969
+ agent2.store.setConfig(cfg);
63970
+ if (agent2.publisher) agent2.publisher.config.moderators = cfg.moderators;
63971
+ await agent2.publisher.publishProfile();
63972
+ await announceModeration(agent2, addRemoveActivity({
63973
+ urls,
63974
+ type: entry.type,
63975
+ object: target,
63976
+ target: urls.moderators,
63977
+ serial: Date.now()
63978
+ })).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
63979
+ return { ok: true, moderators: cfg.moderators };
63980
+ }
63981
+ default:
63982
+ throw new Error(`no way to apply a queued ${entry.type}`);
63983
+ }
63984
+ }
63985
+ async function pinStatus(agent2, s, pinned) {
63986
+ if (s.actor !== agent2.publisher.urls.actor) throw new Error("not your status");
63987
+ const updated = agent2.store.updateStatus(s.noteId, { pinned: !!pinned });
63988
+ await agent2.publisher.publishFeatured().catch((e) => agent2.log?.(`featured: ${e.message}`));
63989
+ agent2.publisher.publishProfile?.().catch(() => {
63990
+ });
63991
+ return updated || s;
63992
+ }
63993
+
63994
+ // lib/client/c2s.mjs
63995
+ init_wire();
63996
+ var MAX_BODY = 512 * 1024;
63997
+ var MAX_INBOX_PAGE = 500;
63998
+ var ACTIVITY_TYPES = /* @__PURE__ */ new Set([
63999
+ "Create",
64000
+ "Update",
64001
+ "Delete",
64002
+ "Follow",
64003
+ "Like",
64004
+ "Announce",
64005
+ "Undo",
64006
+ "Block",
64007
+ "Add",
64008
+ "Remove",
64009
+ "Accept",
64010
+ "Reject",
64011
+ "Move"
64012
+ ]);
64013
+ var arr = (v) => v === void 0 || v === null ? [] : Array.isArray(v) ? v : [v];
64014
+ var idOf = (v) => typeof v === "string" ? v : v?.id || null;
64015
+ function readBody(req) {
64016
+ return new Promise((resolve2, reject) => {
64017
+ let size = 0;
64018
+ const chunks = [];
64019
+ req.on("data", (c) => {
64020
+ size += c.length;
64021
+ if (size > MAX_BODY) {
64022
+ reject(new Error("body too large"));
64023
+ req.destroy();
64024
+ return;
64025
+ }
64026
+ chunks.push(c);
64027
+ });
64028
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
64029
+ req.on("error", reject);
64030
+ });
64031
+ }
64032
+ var C2S = class {
64033
+ constructor({ agent: agent2, log: log2 = console.log, auth }) {
64034
+ this.agent = agent2;
64035
+ this.log = log2;
64036
+ this.auth = auth;
64037
+ }
64038
+ get store() {
64039
+ return this.agent.store;
64040
+ }
64041
+ get urls() {
64042
+ return this.agent.publisher?.urls;
64043
+ }
64044
+ send(res, status2, obj, headers = {}) {
64045
+ res.writeHead(status2, {
64046
+ "content-type": "application/json; charset=utf-8",
64047
+ "cache-control": "no-store",
64048
+ ...headers
64049
+ });
64050
+ res.end(JSON.stringify(obj));
64051
+ return true;
64052
+ }
64053
+ byIri(iri) {
64054
+ return iri ? this.store.getStatuses().find((s) => s.noteId === iri) : null;
64055
+ }
64056
+ /** The months the archive holds, newest first. One container listing. */
64057
+ async archiveMonths() {
64058
+ const archive = this.agent.intake?.archive;
64059
+ if (!archive) return [];
64060
+ const { names } = await archive.list("");
64061
+ return (names || []).map((n) => n.replace(/\/$/u, "")).filter((n) => /^\d{4}-\d{2}$/u.test(n)).sort().reverse();
64062
+ }
64063
+ /**
64064
+ * The owner's own inbox, §5.2. Paged by month because that is how the
64065
+ * archive is stored, so a page costs one listing and a read per item and no
64066
+ * page is dearer for another month being large.
64067
+ */
64068
+ async sendInbox(res, url) {
64069
+ const id = `${this.urls.base}ap/inbox`;
64070
+ const page2 = url?.searchParams?.get("page") || null;
64071
+ let months;
64072
+ try {
64073
+ months = await this.archiveMonths();
64074
+ } catch (e) {
64075
+ return this.send(res, 502, { error: `the archive could not be read: ${e.message}` });
64076
+ }
64077
+ if (!page2) {
64078
+ if (!months.length && this.store.getConfig()?.archiveInbox === false) {
64079
+ this.log("inbox read: nothing to show \u2014 this identity does not keep what it receives");
64080
+ }
64081
+ return this.send(res, 200, {
64082
+ "@context": AS_CTX,
64083
+ id,
64084
+ type: "OrderedCollection",
64085
+ ...months.length ? { first: `${id}?page=${months[0]}` } : { orderedItems: [] }
64086
+ });
64087
+ }
64088
+ if (!/^\d{4}-\d{2}$/u.test(page2)) {
64089
+ return this.send(res, 400, { error: "page names a month, written 2026-09" });
64090
+ }
64091
+ const archive = this.agent.intake?.archive;
64092
+ let names = [];
64093
+ try {
64094
+ ({ names } = await archive.list(`${page2}/`));
64095
+ } catch (e) {
64096
+ return this.send(res, 502, { error: `the archive could not be read: ${e.message}` });
64097
+ }
64098
+ const files = (names || []).filter((n) => n.endsWith(".json")).sort();
64099
+ if (files.length > MAX_INBOX_PAGE) {
64100
+ this.log(`inbox read: ${page2} holds ${files.length} items; serving the first ${MAX_INBOX_PAGE}`);
64101
+ }
64102
+ const kept = [];
64103
+ for (const file of files.slice(0, MAX_INBOX_PAGE)) {
64104
+ const read2 = await archive.read(`${page2}/${file}`, { accept: "*/*" });
64105
+ if (!read2?.ok || !read2.body) continue;
64106
+ try {
64107
+ const record = JSON.parse(read2.body);
64108
+ kept.push({ at: record.receivedAt || "", activity: JSON.parse(record.raw) });
64109
+ } catch {
64110
+ }
64111
+ }
64112
+ kept.sort((a, b) => String(b.at).localeCompare(String(a.at)));
64113
+ const older = months.filter((m) => m < page2)[0] || null;
64114
+ return this.send(res, 200, {
64115
+ "@context": AS_CTX,
64116
+ id: `${id}?page=${page2}`,
64117
+ type: "OrderedCollectionPage",
64118
+ partOf: id,
64119
+ ...older ? { next: `${id}?page=${older}` } : {},
64120
+ orderedItems: kept.map((k) => k.activity)
64121
+ });
64122
+ }
64123
+ async handle(req, res, pathname, url) {
64124
+ if (pathname !== "/ap/outbox" && pathname !== "/ap/actor" && pathname !== "/ap/inbox") return false;
64125
+ if (req.method === "OPTIONS") {
64126
+ res.writeHead(204, pathname === "/ap/inbox" ? { allow: "GET, OPTIONS" } : { allow: "GET, POST, OPTIONS", "accept-post": "application/ld+json, application/activity+json" });
64127
+ res.end();
64128
+ return true;
64129
+ }
64130
+ if (!this.agent.configured() || !this.urls) {
64131
+ return this.send(res, 409, { error: "agent not configured" });
64132
+ }
64133
+ if (pathname === "/ap/inbox") {
64134
+ if (req.method !== "GET" && req.method !== "HEAD") {
64135
+ return this.send(res, 405, { error: "deliveries go to this actor's inbox on the pod, which the actor document names; this address is the owner reading their own" });
64136
+ }
64137
+ const reader = await this.auth(req, pathname);
64138
+ if (!reader.ok) return this.send(res, reader.status, { error: reader.error });
64139
+ return this.sendInbox(res, url);
64140
+ }
64141
+ if (req.method === "GET" || req.method === "HEAD") {
64142
+ const target = pathname === "/ap/actor" ? this.urls.actor : this.urls.outbox;
64143
+ res.writeHead(303, { location: target, "cache-control": "no-store" });
64144
+ res.end();
64145
+ return true;
64146
+ }
64147
+ if (pathname !== "/ap/outbox" || req.method !== "POST") {
64148
+ return this.send(res, 405, { error: "POST the outbox; GET redirects to the pod" });
64149
+ }
64150
+ const who = await this.auth(req, pathname);
64151
+ if (!who.ok) return this.send(res, who.status, { error: who.error });
64152
+ if (this.agent.viewer) {
64153
+ const took = await this.agent.requestTakeover?.();
64154
+ if (!took) return this.send(res, 503, { error: "another agent is active for this pod \u2014 takeover failed, try again" });
64155
+ }
64156
+ let activity;
64157
+ let raw = null;
64158
+ try {
64159
+ const body = await readBody(req);
64160
+ try {
64161
+ raw = JSON.parse(body);
64162
+ } catch {
64163
+ raw = null;
64164
+ }
64165
+ const read2 = await readLenient(body);
64166
+ activity = read2.view ?? read2.doc;
64167
+ } catch (e) {
64168
+ return this.send(res, 400, { error: `unreadable body: ${e.message}` });
64169
+ }
64170
+ if (!activity || typeof activity !== "object" || Array.isArray(activity) || !activity.type) {
64171
+ return this.send(res, 400, { error: "a typed ActivityStreams object is required" });
64172
+ }
64173
+ const slug = safeSlug(req.headers.slug) || null;
64174
+ const r = await this.dispatch(activity, { slug, raw });
64175
+ return this.send(res, r.status, r.body, r.headers);
64176
+ }
64177
+ // Addressing → the facade's four visibilities, inverting the table the
64178
+ // composer writes (wire.addressing). Nothing stated is a public post — what
64179
+ // every client means by a post with no audience chosen, and what a client
64180
+ // that never addresses (dokieli's annotations) needs.
64181
+ visibilityOf(activity, object) {
64182
+ const to = arr(activity.to ?? object?.to).map(idOf);
64183
+ const cc = arr(activity.cc ?? object?.cc).map(idOf);
64184
+ if (!to.length && !cc.length) return "public";
64185
+ if (to.includes(PUBLIC)) return "public";
64186
+ if (cc.includes(PUBLIC)) return "unlisted";
64187
+ if (to.includes(this.urls.followers)) return "private";
64188
+ return "direct";
64189
+ }
64190
+ async dispatch(activity, { slug = null, raw = null } = {}) {
64191
+ const reply = (status2, body, headers = {}) => ({ status: status2, body, headers });
64192
+ if (!activity || typeof activity !== "object" || Array.isArray(activity) || !activity.type) {
64193
+ return reply(400, { error: "a typed ActivityStreams object is required" });
64194
+ }
64195
+ if (!ACTIVITY_TYPES.has(activity.type)) {
64196
+ activity = { type: "Create", object: activity, to: activity.to, cc: activity.cc };
64197
+ }
64198
+ try {
64199
+ return await this._dispatch(activity, { slug, raw, reply });
64200
+ } catch (e) {
64201
+ this.log(`c2s ${activity?.type}: ${e.message}`);
64202
+ return reply(422, { error: e.message || String(e) });
64203
+ }
64204
+ }
64205
+ async _dispatch(activity, { slug, raw, reply }) {
64206
+ const agent2 = this.agent;
64207
+ const object = typeof activity.object === "object" && activity.object !== null ? activity.object : null;
64208
+ const objectId = idOf(activity.object);
64209
+ switch (activity.type) {
64210
+ case "Create": {
64211
+ const makes = object?.type || "Note";
64212
+ if (!object) return reply(422, { error: "a Create carries the object it creates" });
64213
+ const visibility = this.visibilityOf(activity, object);
64214
+ if (makes !== "Note" && makes !== "Question") {
64215
+ const asSent = raw && typeof raw === "object" && !Array.isArray(raw) ? ACTIVITY_TYPES.has(raw.type) ? raw.object && typeof raw.object === "object" ? raw.object : null : raw : null;
64216
+ const made = await agent2.publisher.publishObject(asSent || object, { visibility, slug });
64217
+ return reply(201, { id: made.createId, object: made.id }, { location: made.createId });
64218
+ }
64219
+ const text = String(object.source?.content ?? object.content ?? "");
64220
+ if (!text.trim()) return reply(422, { error: "the note has no content" });
64221
+ if (makes === "Question") {
64222
+ const one = arr(object.oneOf);
64223
+ const many = arr(object.anyOf);
64224
+ const titles = (one.length ? one : many).map((c) => String(c?.name ?? "").trim()).filter(Boolean);
64225
+ try {
64226
+ const question = await agent2.publisher.publishQuestion(text, {
64227
+ options: titles,
64228
+ multiple: !one.length && many.length > 0,
64229
+ expiresAt: object.endTime || null,
64230
+ inReplyTo: idOf(object.inReplyTo) || void 0,
64231
+ visibility,
64232
+ spoilerText: object.summary || null
64233
+ });
64234
+ return reply(
64235
+ 201,
64236
+ { id: createActivityId(question.id), object: question.id },
64237
+ { location: createActivityId(question.id) }
64238
+ );
64239
+ } catch (e) {
64240
+ return reply(422, { error: e.message });
64241
+ }
64242
+ }
64243
+ const attachments = arr(object.attachment).map((a) => ({
64244
+ url: a?.url,
64245
+ mediaType: a?.mediaType,
64246
+ ...a?.name ? { description: a.name } : {}
64247
+ })).filter((a) => a.url);
64248
+ const note = await agent2.publisher.publishNote(text, {
64249
+ inReplyTo: idOf(object.inReplyTo) || void 0,
64250
+ attachments,
64251
+ visibility,
64252
+ spoilerText: object.summary || null,
64253
+ slug
64254
+ });
64255
+ return reply(
64256
+ 201,
64257
+ { id: createActivityId(note.id), object: note.id },
64258
+ { location: createActivityId(note.id) }
64259
+ );
64260
+ }
64261
+ case "Update": {
64262
+ if (objectId === this.urls.actor) {
64263
+ return reply(422, { error: "edit the profile on the admin surface; actor updates are not taken here" });
64264
+ }
64265
+ const s = this.byIri(objectId);
64266
+ if (!s) return reply(404, { error: "no such note here" });
64267
+ if (s.actor !== this.urls.actor || s.kind !== "post") {
64268
+ return reply(403, { error: "not your note" });
64269
+ }
64270
+ const text = String(object?.source?.content ?? object?.content ?? "");
64271
+ if (!text.trim()) return reply(422, { error: "the edit has no content" });
64272
+ const attachments = object?.attachment !== void 0 ? arr(object.attachment).map((a) => ({
64273
+ url: a?.url,
64274
+ mediaType: a?.mediaType,
64275
+ ...a?.name ? { description: a.name } : {}
64276
+ })).filter((a) => a.url) : null;
64277
+ await agent2.publisher.updateNote(s, {
64278
+ content: text,
64279
+ spoilerText: object?.summary || null,
64280
+ attachments
64281
+ });
64282
+ return reply(200, { ok: true, object: s.noteId });
64283
+ }
64284
+ case "Delete": {
64285
+ if (objectId === this.urls.actor) {
64286
+ return reply(422, { error: "retiring the actor is done on the admin surface, where it asks twice" });
64287
+ }
64288
+ const s = this.byIri(objectId);
64289
+ if (!s) return reply(404, { error: "no such note here" });
64290
+ if (s.actor !== this.urls.actor || s.kind !== "post") {
64291
+ return reply(403, { error: "not your note" });
64292
+ }
64293
+ const r = await deleteNote(agent2, s);
64294
+ if (!r.ok) return reply(502, { error: r.error, stillPublished: r.stillPublished });
64295
+ return reply(200, { ok: true });
64296
+ }
64297
+ case "Follow": {
64298
+ if (!objectId) return reply(400, { error: "whom? object must name an actor" });
64299
+ if (/^acct:|^@|^[^/@]+@[^/@]+$/.test(objectId) && !/^https?:/.test(objectId)) {
64300
+ const r = await followHandle(agent2, objectId.replace(/^acct:/, ""));
64301
+ const rec2 = this.store.getContacts().following.find((f) => f.actor === r.actor);
64302
+ return reply(
64303
+ 201,
64304
+ { id: rec2?.followActivity?.id, object: r.actor },
64305
+ rec2?.followActivity?.id ? { location: rec2.followActivity.id } : {}
64306
+ );
64307
+ }
64308
+ const doc = await followActor(agent2, objectId);
64309
+ const rec = this.store.getContacts().following.find((f) => f.actor === doc.id);
64310
+ return reply(
64311
+ 201,
64312
+ { id: rec?.followActivity?.id, object: doc.id },
64313
+ rec?.followActivity?.id ? { location: rec.followActivity.id } : {}
64314
+ );
64315
+ }
64316
+ case "Like": {
64317
+ const s = this.byIri(objectId);
64318
+ if (!s) return reply(422, { error: "that note is not held here \u2014 like what the timeline holds" });
64319
+ const updated = await favourite(agent2, s);
64320
+ return reply(
64321
+ 201,
64322
+ { id: updated.likeActivity?.id, object: s.noteId },
64323
+ updated.likeActivity?.id ? { location: updated.likeActivity.id } : {}
64324
+ );
64325
+ }
64326
+ case "Announce": {
64327
+ const s = this.byIri(objectId);
64328
+ if (!s) return reply(422, { error: "that note is not held here \u2014 boost what the timeline holds" });
64329
+ const updated = await reblog(agent2, s);
64330
+ return reply(
64331
+ 201,
64332
+ { id: updated.announceActivity?.id, object: s.noteId },
64333
+ updated.announceActivity?.id ? { location: updated.announceActivity.id } : {}
64334
+ );
64335
+ }
64336
+ case "Undo": {
64337
+ const inner = object;
64338
+ const innerId = idOf(activity.object);
64339
+ if (inner?.type === "Block") {
64340
+ const target = idOf(inner.object);
64341
+ if (!target) return reply(400, { error: "unblock whom?" });
64342
+ await unblockActor(agent2, target);
64343
+ return reply(200, { ok: true, object: target });
64344
+ }
64345
+ const statuses = this.store.getStatuses();
64346
+ let s = innerId ? statuses.find((x) => x.likeActivity?.id === innerId) : null;
64347
+ if (!s && inner?.type === "Like") s = this.byIri(idOf(inner.object));
64348
+ if (s?.favourited) {
64349
+ const updated = await unfavourite(agent2, s);
64350
+ return reply(200, { ok: true, object: updated.noteId });
64351
+ }
64352
+ s = innerId ? statuses.find((x) => x.announceActivity?.id === innerId) : null;
64353
+ if (!s && inner?.type === "Announce") s = this.byIri(idOf(inner.object));
64354
+ if (s?.reblogged) {
64355
+ const updated = await unreblog(agent2, s);
64356
+ return reply(200, { ok: true, object: updated.noteId });
64357
+ }
64358
+ const following = this.store.getContacts().following;
64359
+ const rec = following.find((f) => f.followActivity?.id === innerId) || (inner?.type === "Follow" ? following.find((f) => f.actor === idOf(inner.object)) : null);
64360
+ if (rec) {
64361
+ await unfollowActor(agent2, rec.actor);
64362
+ return reply(200, { ok: true, object: rec.actor });
64363
+ }
64364
+ return reply(422, { error: "nothing here matches what that Undo names" });
64365
+ }
64366
+ case "Block": {
64367
+ if (!objectId) return reply(400, { error: "block whom? object must name an actor" });
64368
+ await blockActor(agent2, objectId);
64369
+ return reply(200, { ok: true, object: objectId });
64370
+ }
64371
+ case "Add":
64372
+ case "Remove": {
64373
+ if (idOf(activity.target) !== this.urls.featured) {
64374
+ return reply(422, { error: "the featured collection is the one Add/Remove edits here" });
64375
+ }
64376
+ const s = this.byIri(objectId);
64377
+ if (!s) return reply(404, { error: "no such note here" });
64378
+ const updated = await pinStatus(agent2, s, activity.type === "Add");
64379
+ return reply(200, { ok: true, object: updated.noteId, pinned: !!updated.pinned });
64380
+ }
64381
+ case "Accept":
64382
+ case "Reject": {
64383
+ const requester = object?.actor ? idOf(object.actor) : objectId;
64384
+ if (!requester) return reply(400, { error: "whose request? object must name the Follow or its actor" });
64385
+ const r = activity.type === "Accept" ? await admitRequest(agent2, requester).catch((e) => ({ error: e.message })) : await refuseRequest(agent2, requester).catch((e) => ({ error: e.message }));
64386
+ if (r.error) return reply(404, { error: r.error });
64387
+ return reply(200, { ok: true, object: requester });
64388
+ }
64389
+ case "Move":
64390
+ return reply(422, { error: "moving the account is done on the admin surface, where it asks twice" });
64391
+ default:
64392
+ return reply(422, { error: `no handler for ${activity.type}` });
64393
+ }
64394
+ }
64395
+ };
64396
+
63309
64397
  // lib/core/lease.mjs
63310
64398
  init_node_crypto();
63311
64399
  var TTL_MS = 3e5;
@@ -63618,7 +64706,7 @@ function pollParams(body) {
63618
64706
  function htmlToText(html) {
63619
64707
  return String(html).replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>\s*<p[^>]*>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
63620
64708
  }
63621
- function readBody(req) {
64709
+ function readBody2(req) {
63622
64710
  return new Promise((resolve2, reject) => {
63623
64711
  let data = "";
63624
64712
  req.on("data", (c) => {
@@ -63913,7 +65001,7 @@ function rateLimited(api) {
63913
65001
  async function handle(api, ctx) {
63914
65002
  const { req, res, pathname, url, send } = ctx;
63915
65003
  if (pathname === "/api/v1/apps" && req.method === "POST") {
63916
- const body = await readBody(req);
65004
+ const body = await readBody2(req);
63917
65005
  const redirectUris = parseRedirects(body.redirect_uris);
63918
65006
  const app = api.registerApp({
63919
65007
  name: body.client_name,
@@ -63936,7 +65024,7 @@ async function handle(api, ctx) {
63936
65024
  let params = url.searchParams;
63937
65025
  let body = null;
63938
65026
  if (req.method === "POST") {
63939
- body = await readBody(req);
65027
+ body = await readBody2(req);
63940
65028
  params = new URLSearchParams(body);
63941
65029
  }
63942
65030
  const redirect = params.get("redirect_uri") || "";
@@ -64011,7 +65099,7 @@ async function handle(api, ctx) {
64011
65099
  return true;
64012
65100
  }
64013
65101
  if (pathname === "/oauth/token" && req.method === "POST") {
64014
- const body = await readBody(req);
65102
+ const body = await readBody2(req);
64015
65103
  const app = api.findApp(body.client_id || "");
64016
65104
  if (!app && body.code_verifier && /^https:\/\//iu.test(String(body.client_id || ""))) {
64017
65105
  const rec = api.consumeCode(body.code || "");
@@ -64091,7 +65179,7 @@ async function handle(api, ctx) {
64091
65179
  });
64092
65180
  }
64093
65181
  if (pathname === "/oauth/revoke" && req.method === "POST") {
64094
- const body = await readBody(req).catch(() => ({}));
65182
+ const body = await readBody2(req).catch(() => ({}));
64095
65183
  const gone = body?.token;
64096
65184
  if (gone) {
64097
65185
  const kept = api.tokenRecords().filter((r) => r.token !== gone);
@@ -64103,442 +65191,6 @@ async function handle(api, ctx) {
64103
65191
  return false;
64104
65192
  }
64105
65193
 
64106
- // lib/core/social.mjs
64107
- var social_exports = {};
64108
- __export(social_exports, {
64109
- admitRequest: () => admitRequest,
64110
- announceModeration: () => announceModeration,
64111
- applyModeration: () => applyModeration,
64112
- blockActor: () => blockActor,
64113
- confirmDelegation: () => confirmDelegation,
64114
- deleteNote: () => deleteNote,
64115
- ejectFollower: () => ejectFollower,
64116
- favourite: () => favourite,
64117
- followActor: () => followActor,
64118
- followHandle: () => followHandle,
64119
- lookupWebFinger: () => lookupWebFinger,
64120
- pinStatus: () => pinStatus,
64121
- reblog: () => reblog,
64122
- refuseRequest: () => refuseRequest,
64123
- resolveHandle: () => resolveHandle,
64124
- retractAnnouncement: () => retractAnnouncement,
64125
- selfLink: () => selfLink,
64126
- unblockActor: () => unblockActor,
64127
- unfavourite: () => unfavourite,
64128
- unfollowActor: () => unfollowActor,
64129
- unreblog: () => unreblog,
64130
- votePoll: () => votePoll
64131
- });
64132
- init_node_crypto();
64133
- init_wire();
64134
- async function lookupWebFinger(acct, { fetchImpl = null } = {}) {
64135
- const clean = String(acct || "").replace(/^acct:/, "");
64136
- const at = clean.lastIndexOf("@");
64137
- if (at < 1) return null;
64138
- const [user, host] = [clean.slice(0, at), clean.slice(at + 1)];
64139
- if (!host || /[/\\?#]/.test(host)) return null;
64140
- const { safeFetch: safeFetch2, readCapped: readCapped3 } = await Promise.resolve().then(() => (init_safefetch(), safefetch_exports));
64141
- const url = `https://${host}/.well-known/webfinger?resource=${encodeURIComponent(`acct:${clean}`)}`;
64142
- const headers = { accept: "application/jrd+json, application/json" };
64143
- const res = await (fetchImpl ? fetchImpl(url, { headers }) : safeFetch2(url, { headers })).catch(() => null);
64144
- if (!res || res.status >= 400) return null;
64145
- let jrd2;
64146
- try {
64147
- jrd2 = JSON.parse(await readCapped3(res, 256 * 1024));
64148
- } catch {
64149
- return null;
64150
- }
64151
- const subject = String(jrd2?.subject || "").replace(/^acct:/, "").toLowerCase();
64152
- if (subject && subject !== clean.toLowerCase()) {
64153
- return null;
64154
- }
64155
- return jrd2;
64156
- }
64157
- var selfLink = (jrd2) => jrd2?.links?.find((l) => l.rel === "self" && /application\/(activity\+json|ld\+json)/.test(l.type || ""));
64158
- async function confirmDelegation(asked, doc, lookup2 = lookupWebFinger) {
64159
- let home;
64160
- try {
64161
- home = new URL(doc.id).host.toLowerCase();
64162
- } catch {
64163
- throw new Error(`actor id is not a URL (${doc.id})`);
64164
- }
64165
- if (home === asked.toLowerCase()) return;
64166
- const user = doc.preferredUsername;
64167
- if (!user) {
64168
- throw new Error(`${asked} points at ${doc.id} on another host, which names no username to confirm it by`);
64169
- }
64170
- const back = await lookup2(`acct:${user}@${home}`);
64171
- if (selfLink(back)?.href !== doc.id) {
64172
- throw new Error(`${asked} claims ${doc.id}, but ${home} does not agree that is @${user}@${home} \u2014 refusing to take one host's word for another's actor`);
64173
- }
64174
- }
64175
- async function resolveHandle(agent2, handle7) {
64176
- const clean = String(handle7 || "").replace(/^@/, "");
64177
- if (!/^[^@]+@[^@]+$/.test(clean)) throw new Error("handle must look like user@host");
64178
- if (agent2.store.isBlocked("https://" + clean.split("@")[1] + "/")) throw new Error("domain is blocked");
64179
- const remote = agent2.intake?.deliverer?.signedFetch ? (u, i) => agent2.intake.deliverer.signedFetch(u, i) : null;
64180
- const lookup2 = (a) => lookupWebFinger(a, { fetchImpl: remote });
64181
- const jrd2 = await lookup2("acct:" + clean);
64182
- const self2 = selfLink(jrd2);
64183
- if (!self2?.href) throw new Error(`webfinger found no actor for ${clean}`);
64184
- const doc = await agent2.intake.fetchAP(self2.href);
64185
- if (!doc?.id) throw new Error(`actor document unusable for ${clean}`);
64186
- if (agent2.store.isBlocked(doc.id)) throw new Error("actor is blocked");
64187
- await confirmDelegation(clean.slice(clean.lastIndexOf("@") + 1), doc, lookup2);
64188
- await cacheCounts(agent2, doc);
64189
- return doc;
64190
- }
64191
- async function cacheCounts(agent2, doc) {
64192
- const total = async (url) => {
64193
- if (!url) return null;
64194
- try {
64195
- const res = await agent2.intake.fetchAP(url);
64196
- return Number.isInteger(res?.totalItems) ? res.totalItems : null;
64197
- } catch {
64198
- return null;
64199
- }
64200
- };
64201
- const [followers, following] = await Promise.all([total(doc.followers), total(doc.following)]);
64202
- if (followers === null && following === null) return;
64203
- const actors = agent2.store.getActors();
64204
- if (!actors[doc.id]) return;
64205
- actors[doc.id] = { ...actors[doc.id], counts: { followers, following } };
64206
- agent2.store.write("actors.json", actors);
64207
- }
64208
- async function followActor(agent2, actorUrl, { publish = true, doc: fetched = null } = {}) {
64209
- const doc = fetched || await agent2.intake.fetchAP(actorUrl);
64210
- if (!doc?.inbox) throw new Error(`actor document unusable (${actorUrl})`);
64211
- if (agent2.store.isBlocked(doc.id)) throw new Error("actor is blocked");
64212
- const contacts = agent2.store.getContacts();
64213
- let rec = contacts.following.find((f) => f.actor === doc.id);
64214
- if (!rec) {
64215
- rec = { actor: doc.id, inbox: doc.inbox, accepted: false };
64216
- contacts.following.push(rec);
64217
- }
64218
- const follow = followActivity({ urls: agent2.publisher.urls, targetActor: doc.id, serial: Date.now() });
64219
- rec.followActivity = follow;
64220
- agent2.store.setContacts(contacts);
64221
- await agent2.deliverer.deliver(doc.inbox, follow);
64222
- if (publish) await agent2.publisher.publishCollections({ following: true, pending: true });
64223
- return doc;
64224
- }
64225
- async function followHandle(agent2, handle7) {
64226
- const doc = await resolveHandle(agent2, handle7);
64227
- await followActor(agent2, doc.id);
64228
- const clean = String(handle7 || "").replace(/^@/, "");
64229
- const contacts = agent2.store.getContacts();
64230
- const rec = contacts.following.find((f) => f.actor === doc.id);
64231
- if (rec && !rec.handle) {
64232
- rec.handle = clean;
64233
- agent2.store.setContacts(contacts);
64234
- }
64235
- return { ok: true, actor: doc.id };
64236
- }
64237
- async function unfollowActor(agent2, actor) {
64238
- const contacts = agent2.store.getContacts();
64239
- const rec = contacts.following.find((f) => f.actor === actor);
64240
- if (!rec) throw new Error("not following that actor");
64241
- if (rec.followActivity) {
64242
- await agent2.deliverer.deliver(
64243
- rec.inbox,
64244
- undoActivity({ urls: agent2.publisher.urls, activity: rec.followActivity, serial: Date.now() })
64245
- );
64246
- }
64247
- contacts.following = contacts.following.filter((f) => f.actor !== actor);
64248
- agent2.store.setContacts(contacts);
64249
- await agent2.publisher.publishCollections({ following: true, pending: true });
64250
- return { ok: true };
64251
- }
64252
- async function favourite(agent2, s) {
64253
- if (s.favourited) return s;
64254
- const doc = await agent2.intake.fetchAP(s.actor);
64255
- if (!doc?.inbox) throw new Error("author inbox unavailable");
64256
- const act = likeActivity({ urls: agent2.publisher.urls, noteId: s.noteId, serial: Date.now() });
64257
- await agent2.deliverer.deliver(doc.endpoints?.sharedInbox || doc.inbox, act);
64258
- return agent2.store.updateStatus(s.noteId, { favourited: true, likeActivity: act });
64259
- }
64260
- async function unfavourite(agent2, s) {
64261
- if (s.likeActivity) {
64262
- const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
64263
- if (doc?.inbox) {
64264
- await agent2.deliverer.deliver(
64265
- doc.endpoints?.sharedInbox || doc.inbox,
64266
- undoActivity({ urls: agent2.publisher.urls, activity: s.likeActivity, serial: Date.now() })
64267
- );
64268
- }
64269
- }
64270
- return agent2.store.updateStatus(s.noteId, { favourited: false, likeActivity: void 0 });
64271
- }
64272
- async function reblog(agent2, s) {
64273
- if (s.reblogged) return s;
64274
- const { urls } = agent2.publisher;
64275
- const act = announceActivity({ urls, object: s.noteId, serial: Date.now() });
64276
- const inboxes = new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean));
64277
- if (s.actor !== urls.actor) {
64278
- const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
64279
- if (doc?.inbox) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
64280
- }
64281
- await agent2.deliverer.deliverToAll([...inboxes], act);
64282
- const updated = agent2.store.updateStatus(s.noteId, { reblogged: true, announceActivity: act });
64283
- await agent2.publisher.recordOutbox(act);
64284
- return updated;
64285
- }
64286
- async function unreblog(agent2, s) {
64287
- if (s.announceActivity) {
64288
- const undo = undoActivity({ urls: agent2.publisher.urls, activity: s.announceActivity, serial: Date.now() });
64289
- const inboxes = new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean));
64290
- const doc = await agent2.intake.fetchAP(s.actor).catch(() => null);
64291
- if (doc?.inbox && s.actor !== agent2.publisher.urls.actor) inboxes.add(doc.endpoints?.sharedInbox || doc.inbox);
64292
- await agent2.deliverer.deliverToAll([...inboxes], undo);
64293
- await agent2.publisher.unrecordOutbox((i) => i?.id === s.announceActivity.id);
64294
- }
64295
- return agent2.store.updateStatus(s.noteId, { reblogged: false, announceActivity: void 0 });
64296
- }
64297
- async function ejectFollower(agent2, actor) {
64298
- const { urls } = agent2.publisher;
64299
- const contacts = agent2.store.getContacts();
64300
- const rec = contacts.followers.find((f) => f.actor === actor);
64301
- if (!rec) throw new Error("not a member");
64302
- dropFollower(contacts, actor, "ejected");
64303
- agent2.store.setContacts(contacts);
64304
- const muted = agent2.store.getMuted();
64305
- muted.actors = [.../* @__PURE__ */ new Set([...muted.actors, actor])];
64306
- agent2.store.setMuted(muted);
64307
- await agent2.publisher.publishCollections({ followers: true });
64308
- await announceModeration(
64309
- agent2,
64310
- blockActivity({ urls, targetActor: actor, serial: Date.now() })
64311
- ).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64312
- if (rec.bsky) {
64313
- await agent2.bskygroup?.blockDid(rec.bsky.did).catch((e) => agent2.log?.(`bluesky block failed for ${rec.bsky.handle}: ${e.message}`));
64314
- return { ok: true, actor, told: true };
64315
- }
64316
- if (rec.inbox) {
64317
- await agent2.deliverer.deliver(rec.inbox, rejectActivity({
64318
- urls,
64319
- followActivity: { id: rec.followId, type: "Follow", actor, object: urls.actor },
64320
- serial: Date.now()
64321
- }));
64322
- }
64323
- return { ok: true, actor, told: !!rec.inbox };
64324
- }
64325
- async function admitRequest(agent2, actor, { publish = true } = {}) {
64326
- const reqs = agent2.store.getRequests();
64327
- const req = reqs.find((r) => r.actor === actor);
64328
- if (!req) throw new Error("no such request");
64329
- const contacts = agent2.store.getContacts();
64330
- if (!contacts.followers.some((f) => f.actor === actor)) {
64331
- const bridgedDid = /^https:\/\/bsky\.brid\.gy\/ap\/(did:[^/]+)$/.exec(actor)?.[1];
64332
- if (bridgedDid) contacts.followers = contacts.followers.filter((f) => f.bsky?.did !== bridgedDid);
64333
- contacts.followers.push(req.bsky ? { actor, bsky: req.bsky } : { actor, inbox: req.inbox, sharedInbox: req.sharedInbox, followId: req.activity?.id });
64334
- agent2.store.setContacts(contacts);
64335
- agent2.store.addNotification({ type: "follow", actor, ...req.bsky ? { bsky: true } : {} });
64336
- }
64337
- agent2.store.setRequests(reqs.filter((r) => r.actor !== actor));
64338
- if (req.bsky) {
64339
- await agent2.bskygroup?.nudgeOnce(contacts.followers.find((f) => f.actor === actor));
64340
- return { ok: true, actor };
64341
- }
64342
- if (publish) await agent2.publisher.publishCollections({ followers: true, pending: true });
64343
- await agent2.deliverer.deliver(req.inbox, acceptActivity({
64344
- urls: agent2.publisher.urls,
64345
- followActivity: req.activity,
64346
- serial: Date.now()
64347
- }));
64348
- return { ok: true, actor };
64349
- }
64350
- async function refuseRequest(agent2, actor) {
64351
- const reqs = agent2.store.getRequests();
64352
- const req = reqs.find((r) => r.actor === actor);
64353
- if (!req) throw new Error("no such request");
64354
- agent2.store.setRequests(reqs.filter((r) => r.actor !== actor));
64355
- await agent2.publisher?.publishCollections({ pending: true }).catch(() => {
64356
- });
64357
- if (req.bsky) {
64358
- await agent2.bskygroup?.blockDid(req.bsky.did).catch((e) => agent2.log?.(`bluesky block failed for ${req.bsky.handle}: ${e.message}`));
64359
- return { ok: true, actor };
64360
- }
64361
- await agent2.deliverer.deliver(req.inbox, rejectActivity({
64362
- urls: agent2.publisher.urls,
64363
- followActivity: req.activity,
64364
- serial: Date.now()
64365
- }));
64366
- return { ok: true, actor };
64367
- }
64368
- async function retractAnnouncement(agent2, noteId) {
64369
- return agent2.intake.retract(noteId);
64370
- }
64371
- async function votePoll(agent2, s, choices) {
64372
- const { urls } = agent2.publisher;
64373
- const poll = s.poll || {};
64374
- const opts = poll.options || [];
64375
- if (poll.closed || poll.expiresAt && Date.parse(poll.expiresAt) < Date.now()) {
64376
- return { ok: false, error: "this poll has closed" };
64377
- }
64378
- if (poll.voted) return { ok: false, error: "already voted" };
64379
- if (s.actor === urls.actor) return { ok: false, error: "this is your own poll" };
64380
- const picks = [...new Set(choices)].filter((i) => i >= 0 && i < opts.length);
64381
- if (!picks.length) return { ok: false, error: "invalid choice" };
64382
- if (!poll.multiple && picks.length > 1) return { ok: false, error: "this poll takes one choice" };
64383
- const author = await agent2.intake.fetchAP(s.actor).catch(() => null);
64384
- const inbox = author?.endpoints?.sharedInbox || author?.inbox;
64385
- if (!inbox) return { ok: false, error: "could not reach the poll author" };
64386
- const published = (/* @__PURE__ */ new Date()).toISOString();
64387
- for (const i of picks) {
64388
- const id = urls.notes + published.slice(0, 10) + "-vote-" + node_crypto_default.randomBytes(4).toString("hex");
64389
- const note = {
64390
- "@context": AS_CTX,
64391
- id,
64392
- type: "Note",
64393
- attributedTo: urls.actor,
64394
- name: opts[i].title,
64395
- inReplyTo: s.noteId,
64396
- published,
64397
- to: [s.actor]
64398
- };
64399
- const create = {
64400
- "@context": AS_CTX,
64401
- id: createActivityId(id),
64402
- type: "Create",
64403
- actor: urls.actor,
64404
- published,
64405
- to: [s.actor],
64406
- object: note
64407
- };
64408
- await write4(agent2.remote, id, note);
64409
- await writeCreate(agent2.remote, create.id, create);
64410
- await agent2.deliverer.deliverToAll([inbox], create);
64411
- }
64412
- agent2.store.updateStatus(s.noteId, {
64413
- poll: {
64414
- ...poll,
64415
- voted: true,
64416
- ownVotes: picks,
64417
- options: opts.map((o, j) => picks.includes(j) ? { ...o, votes: (o.votes || 0) + 1 } : o)
64418
- }
64419
- });
64420
- return { ok: true };
64421
- }
64422
- async function deleteNote(agent2, s) {
64423
- const { urls } = agent2.publisher;
64424
- const inboxes = agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean);
64425
- await agent2.deliverer.deliverToAll(inboxes, deleteActivity({ urls, noteId: s.noteId }));
64426
- const stuck = [];
64427
- const deletedAt = (/* @__PURE__ */ new Date()).toISOString();
64428
- try {
64429
- await writeTombstone2(agent2.remote, s.noteId, noteTombstone(s.noteId, deletedAt));
64430
- } catch {
64431
- stuck.push(s.noteId);
64432
- }
64433
- if (!await dropCreate(agent2.remote, createActivityId(s.noteId))) {
64434
- stuck.push(createActivityId(s.noteId));
64435
- }
64436
- if (stuck.length) {
64437
- agent2.log?.(`delete ${s.noteId}: the pod kept ${stuck.length} document(s) \u2014 the post is STILL PUBLISHED`);
64438
- return {
64439
- ok: false,
64440
- stillPublished: stuck,
64441
- error: "the pod would not remove the post \u2014 it is still published, and has been kept here so you can try again"
64442
- };
64443
- }
64444
- await dropReplies(agent2.remote, repliesId(s.noteId));
64445
- if (s.atproto?.uri && agent2.atproto?.connected()) {
64446
- await agent2.atproto.deleteCrossPost(s.atproto.uri).then(() => agent2.log?.(`bluesky mirror deleted: ${s.atproto.uri}`)).catch((e) => agent2.log?.(`bluesky mirror not deleted (${e.message}): ${s.atproto.uri}`));
64447
- }
64448
- await agent2.publisher.unrecordOutbox((i) => i === s.noteId || i?.id && i.id === s.announceActivity?.id);
64449
- agent2.store.removeStatus(s.noteId);
64450
- return { ok: true };
64451
- }
64452
- async function announceModeration(agent2, activity) {
64453
- if (agent2.store.getConfig()?.kind !== "group") return null;
64454
- const { urls } = agent2.publisher;
64455
- const act = announceActivity({ urls, object: activity, serial: Date.now(), audience: urls.actor });
64456
- const inboxes = [...new Set(agent2.store.getContacts().followers.map((f) => f.sharedInbox || f.inbox).filter(Boolean))];
64457
- await agent2.deliverer.deliverToAll(inboxes, act);
64458
- await agent2.publisher.recordOutbox(act);
64459
- return act;
64460
- }
64461
- async function blockActor(agent2, actorUrl) {
64462
- if (!/^https?:\/\/\S+$/.test(String(actorUrl || ""))) throw new Error("actor must be a URL");
64463
- const b = agent2.store.getBlocklist();
64464
- if (!b.actors.includes(actorUrl)) b.actors.push(actorUrl);
64465
- agent2.store.setBlocklist(b);
64466
- await unfollowActor(agent2, actorUrl).catch(() => {
64467
- });
64468
- await agent2.publisher?.publishCollections({ blocked: true }).catch(() => {
64469
- });
64470
- await announceModeration(
64471
- agent2,
64472
- blockActivity({ urls: agent2.publisher.urls, targetActor: actorUrl, serial: Date.now() })
64473
- ).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64474
- return { ok: true, actor: actorUrl };
64475
- }
64476
- async function unblockActor(agent2, actorUrl) {
64477
- const b = agent2.store.getBlocklist();
64478
- b.actors = b.actors.filter((a) => a !== actorUrl);
64479
- agent2.store.setBlocklist(b);
64480
- await agent2.publisher?.publishCollections({ blocked: true }).catch(() => {
64481
- });
64482
- await announceModeration(agent2, undoActivity({
64483
- urls: agent2.publisher.urls,
64484
- activity: { type: "Block", actor: agent2.publisher.urls.actor, object: actorUrl },
64485
- serial: Date.now()
64486
- })).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64487
- return { ok: true, actor: actorUrl };
64488
- }
64489
- async function applyModeration(agent2, entry) {
64490
- const { urls } = agent2.publisher;
64491
- const target = typeof entry.activity?.object === "string" ? entry.activity.object : entry.activity?.object?.id;
64492
- switch (entry.type) {
64493
- case "Block": {
64494
- if (!target) throw new Error("the queued Block names nobody");
64495
- const member = agent2.store.getContacts().followers.some((f) => f.actor === target);
64496
- return member ? ejectFollower(agent2, target) : blockActor(agent2, target);
64497
- }
64498
- case "Undo": {
64499
- const inner = entry.activity?.object;
64500
- const unbanned = typeof inner === "object" && inner?.type === "Block" ? typeof inner.object === "string" ? inner.object : inner.object?.id : null;
64501
- if (!unbanned) throw new Error("the queued Undo does not name a Block");
64502
- const muted = agent2.store.getMuted();
64503
- muted.actors = muted.actors.filter((a) => a !== unbanned);
64504
- agent2.store.setMuted(muted);
64505
- return unblockActor(agent2, unbanned);
64506
- }
64507
- case "Delete": {
64508
- if (!target) throw new Error("the queued Delete names nothing");
64509
- return agent2.intake.retract(target);
64510
- }
64511
- case "Add":
64512
- case "Remove": {
64513
- if (!target) throw new Error("the queued roster change names nobody");
64514
- const cfg = { ...agent2.store.getConfig() };
64515
- const prev = cfg.moderators || [];
64516
- cfg.moderators = entry.type === "Add" ? [.../* @__PURE__ */ new Set([...prev, target])] : prev.filter((m) => m !== target);
64517
- agent2.store.setConfig(cfg);
64518
- if (agent2.publisher) agent2.publisher.config.moderators = cfg.moderators;
64519
- await agent2.publisher.publishProfile();
64520
- await announceModeration(agent2, addRemoveActivity({
64521
- urls,
64522
- type: entry.type,
64523
- object: target,
64524
- target: urls.moderators,
64525
- serial: Date.now()
64526
- })).catch((e) => agent2.log?.(`moderation announce failed: ${e.message}`));
64527
- return { ok: true, moderators: cfg.moderators };
64528
- }
64529
- default:
64530
- throw new Error(`no way to apply a queued ${entry.type}`);
64531
- }
64532
- }
64533
- async function pinStatus(agent2, s, pinned) {
64534
- if (s.actor !== agent2.publisher.urls.actor) throw new Error("not your status");
64535
- const updated = agent2.store.updateStatus(s.noteId, { pinned: !!pinned });
64536
- await agent2.publisher.publishFeatured().catch((e) => agent2.log?.(`featured: ${e.message}`));
64537
- agent2.publisher.publishProfile?.().catch(() => {
64538
- });
64539
- return updated || s;
64540
- }
64541
-
64542
65194
  // lib/client/masto/render.mjs
64543
65195
  init_wire();
64544
65196
 
@@ -64809,11 +65461,11 @@ function account(api, actorUrl, { selfAcct } = {}) {
64809
65461
  fields: (self2 ? api.store.getConfig()?.fields : cached.fields) || []
64810
65462
  };
64811
65463
  }
64812
- function page(api, items, url, { limit = 20, max = 40, idOf = (s) => api.store.idFor(s.noteId) } = {}) {
65464
+ function page(api, items, url, { limit = 20, max = 40, idOf: idOf2 = (s) => api.store.idFor(s.noteId) } = {}) {
64813
65465
  const n = Math.min(Number(url.searchParams.get("limit")) || limit, max);
64814
65466
  const ids = /* @__PURE__ */ new Map();
64815
65467
  const idAt = (s) => {
64816
- if (!ids.has(s)) ids.set(s, idOf(s));
65468
+ if (!ids.has(s)) ids.set(s, idOf2(s));
64817
65469
  return ids.get(s);
64818
65470
  };
64819
65471
  const cut = (param) => {
@@ -64837,8 +65489,8 @@ function page(api, items, url, { limit = 20, max = 40, idOf = (s) => api.store.i
64837
65489
  for (const [k, v] of Object.entries(extra)) u.searchParams.set(k, v);
64838
65490
  return u.href;
64839
65491
  };
64840
- const links = [`<${q({ max_id: idOf(pageItems2[pageItems2.length - 1]) })}>; rel="next"`];
64841
- if (pageItems2.length) links.push(`<${q({ min_id: idOf(pageItems2[0]) })}>; rel="prev"`);
65492
+ const links = [`<${q({ max_id: idOf2(pageItems2[pageItems2.length - 1]) })}>; rel="next"`];
65493
+ if (pageItems2.length) links.push(`<${q({ min_id: idOf2(pageItems2[0]) })}>; rel="prev"`);
64842
65494
  return { items: pageItems2, headers: { link: links.join(", ") } };
64843
65495
  }
64844
65496
  async function bskyReply(api, send, body, parent, visibility) {
@@ -65425,7 +66077,7 @@ async function handle3(api, ctx) {
65425
66077
  const entry = api.store.getMedia()[mMedia[1]];
65426
66078
  if (!entry) return send(404, { error: "Record not found" });
65427
66079
  if (req.method === "PUT") {
65428
- const body = await readBody(req);
66080
+ const body = await readBody2(req);
65429
66081
  if (typeof body.description === "string") {
65430
66082
  entry.description = body.description;
65431
66083
  api.store.setMedia(mMedia[1], entry);
@@ -65459,7 +66111,7 @@ async function handle4(api, ctx) {
65459
66111
  const ct = String(req.headers["content-type"] || "");
65460
66112
  let form = {}, files = {};
65461
66113
  if (ct.includes("multipart/form-data")) ({ fields: form, files } = await readMultipart(req));
65462
- else form = await readBody(req);
66114
+ else form = await readBody2(req);
65463
66115
  const cfg = { ...api.store.getConfig() };
65464
66116
  const putImage = async (f) => {
65465
66117
  const ext = (f.filename || "").includes(".") ? f.filename.split(".").pop().replace(/[^\w]/g, "") : "bin";
@@ -65521,7 +66173,7 @@ async function handle4(api, ctx) {
65521
66173
  }
65522
66174
  if (pathname === "/api/v1/markers") {
65523
66175
  if (req.method === "POST") {
65524
- const body = await readBody(req);
66176
+ const body = await readBody2(req);
65525
66177
  const markers = api.store.read("masto-markers.json", {});
65526
66178
  for (const [k, v] of Object.entries(body)) {
65527
66179
  const lastId = v?.last_read_id || v;
@@ -65596,7 +66248,7 @@ async function handle4(api, ctx) {
65596
66248
  return sub ? send(200, api.push.json(token, sub)) : send(404, { error: "Record not found" });
65597
66249
  }
65598
66250
  if (req.method === "POST") {
65599
- const body = await readBody(req);
66251
+ const body = await readBody2(req);
65600
66252
  const sub = api.push.set(token, {
65601
66253
  endpoint: body.subscription?.endpoint,
65602
66254
  keys: body.subscription?.keys,
@@ -65606,7 +66258,7 @@ async function handle4(api, ctx) {
65606
66258
  return send(200, api.push.json(token, sub));
65607
66259
  }
65608
66260
  if (req.method === "PUT") {
65609
- const body = await readBody(req);
66261
+ const body = await readBody2(req);
65610
66262
  const sub = api.push.setAlerts(token, body.data?.alerts);
65611
66263
  return sub ? send(200, api.push.json(token, sub)) : send(404, { error: "Record not found" });
65612
66264
  }
@@ -65699,7 +66351,7 @@ async function handle5(api, ctx) {
65699
66351
  return send(200, api.store.getLists().map(listJson));
65700
66352
  }
65701
66353
  if (pathname === "/api/v1/lists" && req.method === "POST") {
65702
- const body = await readBody(req);
66354
+ const body = await readBody2(req);
65703
66355
  const title = String(body.title || "").trim();
65704
66356
  if (!title) return send(422, { error: "a title is required" });
65705
66357
  const lists = api.store.getLists();
@@ -65718,7 +66370,7 @@ async function handle5(api, ctx) {
65718
66370
  return send(200, {});
65719
66371
  }
65720
66372
  if (req.method === "PUT") {
65721
- const body = await readBody(req);
66373
+ const body = await readBody2(req);
65722
66374
  if (body.title) l.title = String(body.title);
65723
66375
  if (body.replies_policy) l.repliesPolicy = body.replies_policy;
65724
66376
  api.store.setLists(lists);
@@ -65731,7 +66383,7 @@ async function handle5(api, ctx) {
65731
66383
  const l = lists.find((x) => x.id === mListAcc[1]);
65732
66384
  if (!l) return send(404, { error: "Record not found" });
65733
66385
  if (req.method === "GET") return send(200, (l.members || []).map((a) => api.account(a)));
65734
- const body = await readBody(req).catch(() => ({}));
66386
+ const body = await readBody2(req).catch(() => ({}));
65735
66387
  const ids = [].concat(body.account_ids || body["account_ids[]"] || url.searchParams.getAll("account_ids[]")).filter(Boolean);
65736
66388
  const actors = ids.map((id) => api.store.urlFor(id)).filter(Boolean);
65737
66389
  if (req.method === "POST") l.members = [.../* @__PURE__ */ new Set([...l.members || [], ...actors])];
@@ -65763,7 +66415,7 @@ async function handle5(api, ctx) {
65763
66415
  return send(200, api.store.getFilters().map(filterJson));
65764
66416
  }
65765
66417
  if (pathname === "/api/v2/filters" && req.method === "POST") {
65766
- const body = await readBody(req);
66418
+ const body = await readBody2(req);
65767
66419
  const title = String(body.title || "").trim();
65768
66420
  if (!title) return send(422, { error: "a title is required" });
65769
66421
  const filters = api.store.getFilters();
@@ -65789,7 +66441,7 @@ async function handle5(api, ctx) {
65789
66441
  return send(200, {});
65790
66442
  }
65791
66443
  if (req.method === "PUT") {
65792
- const body = await readBody(req);
66444
+ const body = await readBody2(req);
65793
66445
  if (body.title) f.title = String(body.title);
65794
66446
  if (body.context) f.context = [].concat(body.context);
65795
66447
  if (body.filter_action) f.action = body.filter_action;
@@ -65846,7 +66498,7 @@ async function handle5(api, ctx) {
65846
66498
  if (sinceId) cut(sinceId, true);
65847
66499
  const page2 = q.get("min_id") && !q.get("since_id") ? items.slice(Math.max(0, items.length - limit)) : items.slice(0, limit);
65848
66500
  if (page2.length) {
65849
- const base = `${api.scheme || (req.socket?.encrypted ? "https" : "http")}://${req.headers.host}${pathname}`;
66501
+ const base = `${api.scheme || (req.socket?.encrypted ? "https" : "http")}://${req.headers.host}${api.mount || ""}${pathname}`;
65850
66502
  const link = (params) => {
65851
66503
  const u = new URL(base);
65852
66504
  for (const [k, v] of q) if (k !== "max_id" && k !== "since_id" && k !== "min_id") u.searchParams.append(k, v);
@@ -65912,7 +66564,7 @@ init_node_crypto();
65912
66564
  async function handle6(api, ctx) {
65913
66565
  const { req, res, pathname, url, send } = ctx;
65914
66566
  if (pathname === "/api/v1/statuses" && req.method === "POST") {
65915
- const body = await readBody(req);
66567
+ const body = await readBody2(req);
65916
66568
  if (!body.status) return send(422, { error: "status text required" });
65917
66569
  const visibility = body.visibility || "public";
65918
66570
  if (!["public", "unlisted", "private", "direct"].includes(visibility)) {
@@ -66030,7 +66682,7 @@ async function handle6(api, ctx) {
66030
66682
  const s = noteUrl && api.store.getStatuses().find((x) => x.noteId === noteUrl);
66031
66683
  if (!s) return send(404, { error: "Record not found" });
66032
66684
  if (s.actor !== api.urls.actor) return send(403, { error: "not your status" });
66033
- const body = await readBody(req);
66685
+ const body = await readBody2(req);
66034
66686
  if (!body.status) return send(422, { error: "status text required" });
66035
66687
  const mediaIds = [].concat(body.media_ids || body["media_ids[]"] || []).filter(Boolean);
66036
66688
  const media = api.store.getMedia();
@@ -66148,7 +66800,7 @@ async function handle6(api, ctx) {
66148
66800
  return send(200, {});
66149
66801
  }
66150
66802
  if (req.method === "PUT") {
66151
- const body = await readBody(req);
66803
+ const body = await readBody2(req);
66152
66804
  const at = Date.parse(body.scheduled_at || "");
66153
66805
  if (!Number.isFinite(at)) return send(422, { error: "scheduled_at required" });
66154
66806
  e.scheduledAt = new Date(at).toISOString();
@@ -66162,7 +66814,7 @@ async function handle6(api, ctx) {
66162
66814
  const s = noteUrl && api.store.getStatuses().find((x) => x.noteId === noteUrl);
66163
66815
  if (!s?.poll) return send(404, { error: "Record not found" });
66164
66816
  if (mPoll[2] && req.method === "POST") {
66165
- const body = await readBody(req);
66817
+ const body = await readBody2(req);
66166
66818
  const choices = [].concat(body.choices || body["choices[]"] || []).map(Number).filter(Number.isInteger);
66167
66819
  if (!choices.length) return send(422, { error: "choices required" });
66168
66820
  const r = await votePoll(api.agent, s, choices);
@@ -66211,11 +66863,13 @@ var MastoApi = class _MastoApi {
66211
66863
  allowed = null,
66212
66864
  scheme = null,
66213
66865
  embedded = false,
66866
+ mount = "",
66214
66867
  streaming = true,
66215
66868
  webPush = true,
66216
66869
  scheduling = true
66217
66870
  }) {
66218
66871
  this.agent = agent2;
66872
+ this.mount = mount;
66219
66873
  this.embedded = embedded;
66220
66874
  this.log = log2;
66221
66875
  this.allowed = allowed;
@@ -66907,7 +67561,7 @@ var PodTransport = class {
66907
67561
  * says all of it. The parsed graph must mention the WebID before anything is
66908
67562
  * written back — an empty or foreign body must never become the new profile.
66909
67563
  */
66910
- async linkAccountInProfile({ actorUrl, accountName, kind = "person" }) {
67564
+ async linkAccountInProfile({ actorUrl, accountName, kind = "person", outbox = null }) {
66911
67565
  const docUrl = this.webId.split("#")[0];
66912
67566
  const res = await this.fetch(docUrl, { headers: { accept: "text/turtle" } });
66913
67567
  if (res.status >= 400) throw new Error(`[${this.label}] GET ${docUrl} \u2192 ${res.status}`);
@@ -66923,10 +67577,16 @@ var PodTransport = class {
66923
67577
  [me, FOAF("account"), actor],
66924
67578
  [actor, RDF4("type"), FOAF("OnlineAccount")],
66925
67579
  [actor, RDF4("type"), kind === "group" ? AS("Group") : AS("Person")],
66926
- [actor, FOAF("accountName"), literal2(accountName)]
67580
+ [actor, FOAF("accountName"), literal2(accountName)],
67581
+ // Where a Solid client posts on this person's behalf (`as:outbox` on the
67582
+ // WebID is what dokieli reads); only where a door exists to take it.
67583
+ ...outbox ? [[me, AS("outbox"), namedNode2(outbox)]] : []
66927
67584
  ];
66928
67585
  const missing = wanted.filter(([s, p, o]) => !g.holds(s, p, o, doc));
66929
- const stale = g.statementsMatching(actor, FOAF("accountName"), null, doc).filter((st2) => st2.object.value !== accountName);
67586
+ const stale = [
67587
+ ...g.statementsMatching(actor, FOAF("accountName"), null, doc).filter((st2) => st2.object.value !== accountName),
67588
+ ...outbox ? g.statementsMatching(me, AS("outbox"), null, doc).filter((st2) => st2.object.value !== outbox) : []
67589
+ ];
66930
67590
  if (!missing.length && !stale.length) return false;
66931
67591
  const deletes = stale.map((st2) => [st2.subject, st2.predicate, st2.object]);
66932
67592
  if (await this.patchDocument(docUrl, missing, deletes)) return true;
@@ -69664,6 +70324,7 @@ var BrowserAgent = class _BrowserAgent {
69664
70324
  this.lease.startRenewal();
69665
70325
  try {
69666
70326
  await this.store.load({ force: true }).catch((e) => this.log(`re-reading state: ${e.message}`));
70327
+ await this.publisher.healStatuses().catch((e) => this.log(`healing the timeline index: ${e.message}`));
69667
70328
  this.deliverer?.startQueue?.();
69668
70329
  await this.publisher.publishProfile();
69669
70330
  await this.store.flush?.();
@@ -69794,6 +70455,7 @@ var BrowserAgent = class _BrowserAgent {
69794
70455
  this.publisher.atproto = this.atproto;
69795
70456
  this.fediaccts = new BrowserFediAccounts({ store: this.store, actorId: this.urls.actor, log: this.log });
69796
70457
  this.lease = new Lease({ url: this.urls.state + "lease.json", fetchImpl: podFetch, log: this.log });
70458
+ this.c2s = new C2S({ agent: this, log: this.log });
69797
70459
  this.intake = new Intake({
69798
70460
  config: this.store.getConfig(),
69799
70461
  urls: this.urls,
@@ -69803,7 +70465,8 @@ var BrowserAgent = class _BrowserAgent {
69803
70465
  publisher: this.publisher,
69804
70466
  log: this.log,
69805
70467
  push: true,
69806
- lease: this.lease
70468
+ lease: this.lease,
70469
+ ownerPost: (a, o) => this.c2s.dispatch(a, o)
69807
70470
  });
69808
70471
  this.masto = new MastoApi({
69809
70472
  agent: this,
@@ -69863,7 +70526,8 @@ var BrowserAgent = class _BrowserAgent {
69863
70526
  atproto: this.atproto?.status() || null,
69864
70527
  podRequests: this.remote?.stats?.() || null,
69865
70528
  update: null,
69866
- inboxCooldownFor: 0
70529
+ inboxCooldownFor: 0,
70530
+ stateSkipped: this.store?.lastSkipped || []
69867
70531
  };
69868
70532
  }
69869
70533
  // Rotate the signing key (POST /rotate-key). Mint a fresh keypair, replace the